diff --git a/.github/workflows/ai-pr-review.yml b/.github/workflows/ai-pr-review.yml new file mode 100644 index 0000000..fda4b31 --- /dev/null +++ b/.github/workflows/ai-pr-review.yml @@ -0,0 +1,81 @@ +name: AI Code Review + +on: + pull_request: + types: [opened, synchronize] + +jobs: + ai-review: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/github-script@v7 + env: + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + with: + script: | + const diff = await github.rest.repos.compareCommits({ + owner: context.repo.owner, + repo: context.repo.repo, + base: context.payload.pull_request.base.sha, + head: context.payload.pull_request.head.sha + }); + + const kotlinFiles = diff.data.files.filter(file => + (file.filename.endsWith('.kt') || file.filename.endsWith('.kts')) && file.patch + ); + + if (kotlinFiles.length === 0) return; + + for (const file of kotlinFiles.slice(0, 3)) { + const response = await fetch('https://openrouter.ai/api/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: 'x-ai/grok-4-fast:free', + messages: [{ + role: 'system', + content: `코드 리뷰어입니다. 다음 규칙을 검토하세요: + +## 필수 검토 항목 +1. **글로벌 익셉션 처리**: @ControllerAdvice 사용, 표준 에러 응답 +2. **ApiResponse 사용**: 모든 API는 ApiResponse로 감싸서 응답 +3. **Kotlin 최적화**: data class, null safety, when 표현식, 확장함수 등 +4. **ktlint 규칙**: 포맷팅, 네이밍 컨벤션 + +## 응답 형식 +🟢 **좋은점**: [규칙을 잘 지킨 부분] +🟡 **개선사항**: [더 좋게 할 수 있는 부분] +🔴 **문제점**: [반드시 수정해야 할 부분]` + }, { + role: 'user', + content: `파일: ${file.filename}\n\n변경사항:\n${file.patch}` + }], + max_tokens: 500, + temperature: 0 + }) + }); + + if (response.ok) { + const result = await response.json(); + const review = result.choices[0].message.content; + + await github.rest.pulls.createReview({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + body: `## 🤖 AI 리뷰 - \`${file.filename}\`\n\n${review}`, + event: 'COMMENT' + }); + } + } \ No newline at end of file diff --git a/docs/erd-diagram.md b/docs/erd-diagram.md index 239077e..cdfc7bf 100644 --- a/docs/erd-diagram.md +++ b/docs/erd-diagram.md @@ -47,7 +47,6 @@ erDiagram bigint session_id FK "세션 ID" text content "메시지 내용" enum sender_type "USER, AI" - json metadata "AI 도구 사용 정보" datetime created_at "메시지 전송 시간" } @@ -143,7 +142,6 @@ AI 채팅 메시지를 저장하는 테이블입니다. | session_id | BIGINT | FK, NOT NULL | 세션 ID | | content | TEXT | NOT NULL | 메시지 내용 | | sender_type | ENUM | NOT NULL | 발신자 유형 (USER, AI) | -| metadata | JSON | NULL | AI 도구 사용 정보 (WeatherTool, TourTool) | | created_at | DATETIME | NOT NULL | 메시지 전송 시간 | **제약조건:** @@ -243,7 +241,7 @@ Guest가 AI 채팅 세션과 채팅방을 평가하는 테이블입니다. 1. User → AiChatSession 생성 2. User → AiChatMessage 전송 3. Spring AI → WeatherTool/TourTool 호출 -4. AI → AiChatMessage(sender_type=AI) 생성 + metadata 저장 +4. AI → AiChatMessage(sender_type=AI) 생성 5. SSE로 실시간 응답 전송 ``` diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/entity/AiChatMessage.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/entity/AiChatMessage.kt new file mode 100644 index 0000000..184155e --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/entity/AiChatMessage.kt @@ -0,0 +1,34 @@ +package com.back.koreaTravelGuide.domain.ai.aiChat.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table +import java.time.ZoneId +import java.time.ZonedDateTime + +@Entity +@Table(name = "ai_chat_messages") +class AiChatMessage( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + val id: Long? = null, + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "session_id", nullable = false) + val aiChatSession: AiChatSession, + @Column(name = "content", columnDefinition = "TEXT", nullable = false) + val content: String, + @Enumerated(EnumType.STRING) + @Column(name = "sender_type", nullable = false) + val senderType: SenderType, + @Column(name = "created_at", nullable = false) + val createdAt: ZonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul")), +) diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/entity/AiChatSession.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/entity/AiChatSession.kt new file mode 100644 index 0000000..bbf9fe3 --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/entity/AiChatSession.kt @@ -0,0 +1,25 @@ +package com.back.koreaTravelGuide.domain.ai.aiChat.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table +import java.time.ZoneId +import java.time.ZonedDateTime + +@Entity +@Table(name = "ai_chat_sessions") +class AiChatSession( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + val id: Long? = null, + @Column(name = "user_id", nullable = false) + val userId: Long, + @Column(name = "session_title", nullable = true, length = 100) + var sessionTitle: String? = null, + @Column(name = "created_at", nullable = false) + val createdAt: ZonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul")), +) diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/entity/ChatHistory.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/entity/ChatHistory.kt deleted file mode 100644 index c3c7619..0000000 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/entity/ChatHistory.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.back.koreaTravelGuide.domain.ai.aiChat.entity - -// TODO: 채팅 기록 엔티티 - 대화 내용 저장 (향후 구현) -class ChatHistory diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/entity/SenderType.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/entity/SenderType.kt new file mode 100644 index 0000000..04c644f --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/entity/SenderType.kt @@ -0,0 +1,6 @@ +package com.back.koreaTravelGuide.domain.ai.aiChat.entity + +enum class SenderType { + USER, + AI, +}