Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .github/workflows/ai-pr-review.yml
Original file line number Diff line number Diff line change
@@ -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<T>로 감싸서 응답
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'
});
}
}
4 changes: 1 addition & 3 deletions docs/erd-diagram.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ erDiagram
bigint session_id FK "세션 ID"
text content "메시지 내용"
enum sender_type "USER, AI"
json metadata "AI 도구 사용 정보"
datetime created_at "메시지 전송 시간"
}

Expand Down Expand Up @@ -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 | 메시지 전송 시간 |

**제약조건:**
Expand Down Expand Up @@ -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로 실시간 응답 전송
```

Expand Down
Original file line number Diff line number Diff line change
@@ -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")),
)
Original file line number Diff line number Diff line change
@@ -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")),
)

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.back.koreaTravelGuide.domain.ai.aiChat.entity

enum class SenderType {
USER,
AI,
}