From fa84f7b812ccfcf7b18e7ef949239dc57fb3d504 Mon Sep 17 00:00:00 2001 From: Mrbaeksang Date: Wed, 24 Sep 2025 15:18:39 +0900 Subject: [PATCH 1/6] =?UTF-8?q?GitHub=20Actions=20AI=20PR=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Grok-4-fast 모델 사용 - 글로벌 익셉션 처리, ApiResponse 사용, Kotlin 최적화, ktlint 규칙 검토 - PR 생성/수정 시 자동 코드 리뷰 실행 --- .github/workflows/ai-pr-review.yml | 117 +++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 .github/workflows/ai-pr-review.yml diff --git a/.github/workflows/ai-pr-review.yml b/.github/workflows/ai-pr-review.yml new file mode 100644 index 0000000..8270ab2 --- /dev/null +++ b/.github/workflows/ai-pr-review.yml @@ -0,0 +1,117 @@ +name: AI PR Review + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + ai-review: + runs-on: ubuntu-latest + if: github.actor != 'dependabot[bot]' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get changed files + id: changed-files + run: | + # Get the list of changed files + CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -E '\.(kt|kts|java|sql|yml|yaml|properties|md)$' | head -20) + echo "files<> $GITHUB_OUTPUT + echo "$CHANGED_FILES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Read changed files content + id: file-content + run: | + CONTENT="" + while IFS= read -r file; do + if [ -f "$file" ] && [ -s "$file" ]; then + echo "Processing: $file" + CONTENT="$CONTENT\n\n--- File: $file ---\n" + # Limit file size to avoid API limits + head -c 8000 "$file" >> temp_content.txt + CONTENT="$CONTENT$(cat temp_content.txt)" + rm -f temp_content.txt + fi + done <<< "${{ steps.changed-files.outputs.files }}" + + echo "content<> $GITHUB_OUTPUT + echo -e "$CONTENT" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: AI Code Review + id: ai-review + run: | + if [ -z "${{ steps.file-content.outputs.content }}" ]; then + echo "No relevant files changed" + echo "review=No Kotlin/configuration files were changed in this PR." >> $GITHUB_OUTPUT + exit 0 + fi + + REVIEW=$(curl -s -X POST "https://openrouter.ai/api/v1/chat/completions" \ + -H "Authorization: Bearer ${{ secrets.OPENROUTER_API_KEY }}" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "x-ai/grok-4-fast:free", + "messages": [ + { + "role": "system", + "content": "코드 리뷰어입니다. 다음 규칙만 검토하세요:\n\n## 검토 항목\n1. **글로벌 익셉션 처리**: @ControllerAdvice 사용, 표준 에러 응답\n2. **ApiResponse 사용**: 모든 API는 ApiResponse로 감싸서 응답\n3. **Kotlin 최적화**: data class, null safety, when 표현식, 확장함수 등\n4. **ktlint 규칙**: 포맷팅, 네이밍 컨벤션\n\n## 응답 형식\n### ✅ 준수사항\n- [잘 지켜진 부분]\n\n### ❌ 위반사항 \n- [파일:라인] 문제점과 수정방법\n\n**점수**: X/10" + }, + { + "role": "user", + "content": "다음 PR의 변경사항을 리뷰해주세요:\n\nPR 제목: ${{ github.event.pull_request.title }}\nPR 설명: ${{ github.event.pull_request.body }}\n\n변경된 파일들:\n${{ steps.file-content.outputs.content }}" + } + ], + "temperature": 0.3, + "max_tokens": 4000 + }') + + # Extract the review content + REVIEW_CONTENT=$(echo "$REVIEW" | jq -r '.choices[0].message.content // "리뷰 생성에 실패했습니다."') + + echo "review<> $GITHUB_OUTPUT + echo "$REVIEW_CONTENT" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Post review comment + uses: actions/github-script@v7 + with: + script: | + const review = `${{ steps.ai-review.outputs.review }}`; + + // Find existing review comment + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.data.find(comment => + comment.user.login === 'github-actions[bot]' && + comment.body.includes('🤖 AI 코드 리뷰') + ); + + const commentBody = "## 🤖 AI 코드 리뷰\n\n" + review + "\n\n---\n
\n💡 리뷰 정보\n\n- 모델: Grok-4-fast\n- 리뷰 시간: " + new Date().toLocaleString('ko-KR', {timeZone: 'Asia/Seoul'}) + "\n- PR: #${{ github.event.number }}\n\n
"; + + if (botComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: commentBody + }); + } else { + // Create new comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: commentBody + }); + } \ No newline at end of file From 1c0fe6bdcd9a003cf6bb911a77adaea16d736684 Mon Sep 17 00:00:00 2001 From: Mrbaeksang Date: Wed, 24 Sep 2025 18:07:40 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat(be):=20AiChatMessage=20+=20Session=20+?= =?UTF-8?q?=20Enum=20Class=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20erd=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/erd-diagram.md | 4 +-- .../domain/ai/aiChat/entity/AiChatMessage.kt | 34 +++++++++++++++++++ .../domain/ai/aiChat/entity/AiChatSession.kt | 25 ++++++++++++++ .../domain/ai/aiChat/entity/ChatHistory.kt | 4 --- .../domain/ai/aiChat/entity/SenderType.kt | 6 ++++ 5 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/entity/AiChatMessage.kt create mode 100644 src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/entity/AiChatSession.kt delete mode 100644 src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/entity/ChatHistory.kt create mode 100644 src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/entity/SenderType.kt 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, +} From e7951980fb7a02c3570f6cc653be95776e4171a2 Mon Sep 17 00:00:00 2001 From: Mrbaeksang Date: Wed, 24 Sep 2025 18:14:17 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20ai=20pr=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ai-pr-review.yml | 32 ++++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ai-pr-review.yml b/.github/workflows/ai-pr-review.yml index 8270ab2..3171616 100644 --- a/.github/workflows/ai-pr-review.yml +++ b/.github/workflows/ai-pr-review.yml @@ -28,20 +28,23 @@ jobs: id: file-content run: | CONTENT="" - while IFS= read -r file; do - if [ -f "$file" ] && [ -s "$file" ]; then - echo "Processing: $file" - CONTENT="$CONTENT\n\n--- File: $file ---\n" - # Limit file size to avoid API limits - head -c 8000 "$file" >> temp_content.txt - CONTENT="$CONTENT$(cat temp_content.txt)" - rm -f temp_content.txt - fi - done <<< "${{ steps.changed-files.outputs.files }}" + if [ -n "${{ steps.changed-files.outputs.files }}" ]; then + while IFS= read -r file; do + if [ -f "$file" ] && [ -s "$file" ]; then + echo "Processing: $file" + CONTENT="$CONTENT\n\n--- File: $file ---\n" + head -c 8000 "$file" | cat >> temp_content.txt + CONTENT="$CONTENT$(cat temp_content.txt)" + rm -f temp_content.txt + fi + done <<< "${{ steps.changed-files.outputs.files }}" + fi - echo "content<> $GITHUB_OUTPUT - echo -e "$CONTENT" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + { + echo "content<> $GITHUB_OUTPUT - name: AI Code Review id: ai-review @@ -67,8 +70,7 @@ jobs: "content": "다음 PR의 변경사항을 리뷰해주세요:\n\nPR 제목: ${{ github.event.pull_request.title }}\nPR 설명: ${{ github.event.pull_request.body }}\n\n변경된 파일들:\n${{ steps.file-content.outputs.content }}" } ], - "temperature": 0.3, - "max_tokens": 4000 + "temperature": 0.3 }') # Extract the review content From 9398d3f263f3b7c9597e63eebafc84ddf4f9824b Mon Sep 17 00:00:00 2001 From: Mrbaeksang Date: Wed, 24 Sep 2025 18:15:30 +0900 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20GitHub=20Actions=20HEREDOC=20?= =?UTF-8?q?=EB=AC=B8=EB=B2=95=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ai-pr-review.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ai-pr-review.yml b/.github/workflows/ai-pr-review.yml index 3171616..e3dceb6 100644 --- a/.github/workflows/ai-pr-review.yml +++ b/.github/workflows/ai-pr-review.yml @@ -20,9 +20,9 @@ jobs: run: | # Get the list of changed files CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -E '\.(kt|kts|java|sql|yml|yaml|properties|md)$' | head -20) - echo "files<> $GITHUB_OUTPUT + echo "files<> $GITHUB_OUTPUT echo "$CHANGED_FILES" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + echo "CHANGEDFILES" >> $GITHUB_OUTPUT - name: Read changed files content id: file-content @@ -32,19 +32,19 @@ jobs: while IFS= read -r file; do if [ -f "$file" ] && [ -s "$file" ]; then echo "Processing: $file" - CONTENT="$CONTENT\n\n--- File: $file ---\n" - head -c 8000 "$file" | cat >> temp_content.txt - CONTENT="$CONTENT$(cat temp_content.txt)" - rm -f temp_content.txt + CONTENT="${CONTENT} + +--- File: $file --- +" + FILE_CONTENT=$(head -c 8000 "$file") + CONTENT="${CONTENT}${FILE_CONTENT}" fi done <<< "${{ steps.changed-files.outputs.files }}" fi - { - echo "content<> $GITHUB_OUTPUT + echo "content<> $GITHUB_OUTPUT + echo "$CONTENT" >> $GITHUB_OUTPUT + echo "FILECONTENT" >> $GITHUB_OUTPUT - name: AI Code Review id: ai-review From 156e9c737f831fc7daabb4f3d665d0a5714db64f Mon Sep 17 00:00:00 2001 From: Mrbaeksang Date: Wed, 24 Sep 2025 18:18:21 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20AI=20PR=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EC=99=84?= =?UTF-8?q?=EC=A0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ai-pr-review.yml | 197 ++++++++++++++++------------- 1 file changed, 108 insertions(+), 89 deletions(-) diff --git a/.github/workflows/ai-pr-review.yml b/.github/workflows/ai-pr-review.yml index e3dceb6..6ccb5f1 100644 --- a/.github/workflows/ai-pr-review.yml +++ b/.github/workflows/ai-pr-review.yml @@ -8,6 +8,10 @@ jobs: ai-review: runs-on: ubuntu-latest if: github.actor != 'dependabot[bot]' + permissions: + contents: read + pull-requests: write + issues: write steps: - name: Checkout code @@ -15,105 +19,120 @@ jobs: with: fetch-depth: 0 - - name: Get changed files - id: changed-files - run: | - # Get the list of changed files - CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -E '\.(kt|kts|java|sql|yml|yaml|properties|md)$' | head -20) - echo "files<> $GITHUB_OUTPUT - echo "$CHANGED_FILES" >> $GITHUB_OUTPUT - echo "CHANGEDFILES" >> $GITHUB_OUTPUT - - - name: Read changed files content - id: file-content - run: | - CONTENT="" - if [ -n "${{ steps.changed-files.outputs.files }}" ]; then - while IFS= read -r file; do - if [ -f "$file" ] && [ -s "$file" ]; then - echo "Processing: $file" - CONTENT="${CONTENT} - ---- File: $file --- -" - FILE_CONTENT=$(head -c 8000 "$file") - CONTENT="${CONTENT}${FILE_CONTENT}" - fi - done <<< "${{ steps.changed-files.outputs.files }}" - fi - - echo "content<> $GITHUB_OUTPUT - echo "$CONTENT" >> $GITHUB_OUTPUT - echo "FILECONTENT" >> $GITHUB_OUTPUT - - name: AI Code Review - id: ai-review - run: | - if [ -z "${{ steps.file-content.outputs.content }}" ]; then - echo "No relevant files changed" - echo "review=No Kotlin/configuration files were changed in this PR." >> $GITHUB_OUTPUT - exit 0 - fi - - REVIEW=$(curl -s -X POST "https://openrouter.ai/api/v1/chat/completions" \ - -H "Authorization: Bearer ${{ secrets.OPENROUTER_API_KEY }}" \ - -H "Content-Type: application/json" \ - -d '{ - "model": "x-ai/grok-4-fast:free", - "messages": [ - { - "role": "system", - "content": "코드 리뷰어입니다. 다음 규칙만 검토하세요:\n\n## 검토 항목\n1. **글로벌 익셉션 처리**: @ControllerAdvice 사용, 표준 에러 응답\n2. **ApiResponse 사용**: 모든 API는 ApiResponse로 감싸서 응답\n3. **Kotlin 최적화**: data class, null safety, when 표현식, 확장함수 등\n4. **ktlint 규칙**: 포맷팅, 네이밍 컨벤션\n\n## 응답 형식\n### ✅ 준수사항\n- [잘 지켜진 부분]\n\n### ❌ 위반사항 \n- [파일:라인] 문제점과 수정방법\n\n**점수**: X/10" - }, - { - "role": "user", - "content": "다음 PR의 변경사항을 리뷰해주세요:\n\nPR 제목: ${{ github.event.pull_request.title }}\nPR 설명: ${{ github.event.pull_request.body }}\n\n변경된 파일들:\n${{ steps.file-content.outputs.content }}" - } - ], - "temperature": 0.3 - }') - - # Extract the review content - REVIEW_CONTENT=$(echo "$REVIEW" | jq -r '.choices[0].message.content // "리뷰 생성에 실패했습니다."') - - echo "review<> $GITHUB_OUTPUT - echo "$REVIEW_CONTENT" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Post review comment uses: actions/github-script@v7 with: script: | - const review = `${{ steps.ai-review.outputs.review }}`; + const OPENROUTER_API_KEY = '${{ secrets.OPENROUTER_API_KEY }}'; - // Find existing review comment - const comments = await github.rest.issues.listComments({ + const diff = await github.rest.repos.compareCommits({ owner: context.repo.owner, repo: context.repo.repo, - issue_number: context.issue.number, + base: context.payload.pull_request.base.sha, + head: context.payload.pull_request.head.sha }); - const botComment = comments.data.find(comment => - comment.user.login === 'github-actions[bot]' && - comment.body.includes('🤖 AI 코드 리뷰') + const filesToReview = diff.data.files.filter(file => + file.patch && + !file.filename.includes('test/') && + !file.filename.includes('build/') && + (file.filename.endsWith('.kt') || + file.filename.endsWith('.kts') || + file.filename.endsWith('.java') || + file.filename.endsWith('.yml') || + file.filename.endsWith('.yaml') || + file.filename.endsWith('.md')) ); - const commentBody = "## 🤖 AI 코드 리뷰\n\n" + review + "\n\n---\n
\n💡 리뷰 정보\n\n- 모델: Grok-4-fast\n- 리뷰 시간: " + new Date().toLocaleString('ko-KR', {timeZone: 'Asia/Seoul'}) + "\n- PR: #${{ github.event.number }}\n\n
"; + if (filesToReview.length === 0) { + console.log('No files to review'); + return; + } - if (botComment) { - // Update existing comment - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body: commentBody - }); - } else { - // Create new comment - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: commentBody + console.log(`Found ${filesToReview.length} files to review`); + + let overallReview = `## 🤖 AI 코드 리뷰\n\n`; + + let allChanges = ''; + const filesSummary = []; + + for (const file of filesToReview) { + filesSummary.push(`- ${file.filename} (+${file.additions}/-${file.deletions})`); + allChanges += `\n### ${file.filename}\n`; + allChanges += `\`\`\`diff\n${file.patch}\n\`\`\`\n`; + } + + try { + const response = await fetch('https://openrouter.ai/api/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${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 규칙**: 포맷팅, 네이밍 컨벤션 + +## 응답 형식 +### ✅ 준수사항 +- [잘 지켜진 부분] + +### ❌ 위반사항 +- [파일:라인] 문제점과 수정방법 + +**점수**: X/10` + }, { + role: 'user', + content: `다음 PR의 변경사항을 리뷰해주세요: + +PR 제목: ${{ github.event.pull_request.title }} +PR 설명: ${{ github.event.pull_request.body }} + +변경된 파일: +${filesSummary.join('\n')} + +전체 변경사항: +${allChanges}` + }], + temperature: 0.3 + }) }); - } \ No newline at end of file + + if (response.ok) { + const result = await response.json(); + overallReview += result.choices[0].message.content + '\n\n'; + console.log('✅ API review completed successfully!'); + } else { + const errorText = await response.text(); + console.error('API request failed:', response.status, errorText); + overallReview += `> ⚠️ 리뷰 실패: ${response.status} - ${errorText}\n\n`; + } + } catch (error) { + console.error('Error during review:', error); + overallReview += `> ⚠️ 리뷰 실패: ${error.message}\n\n`; + } + + overallReview += `---\n`; + overallReview += `
\n💡 리뷰 정보\n\n`; + overallReview += `- 모델: Grok-4-fast\n`; + overallReview += `- 리뷰 시간: ${new Date().toLocaleString('ko-KR', {timeZone: 'Asia/Seoul'})}\n`; + overallReview += `- PR: #${{ github.event.number }}\n\n`; + overallReview += `
`; + + await github.rest.pulls.createReview({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + body: overallReview, + event: 'COMMENT' + }); + + console.log(`✅ Review completed for ${filesToReview.length} files!`); \ No newline at end of file From 97003f959397c58eb4a37a4bab430de745d2649e Mon Sep 17 00:00:00 2001 From: Mrbaeksang Date: Wed, 24 Sep 2025 18:25:40 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20AI=20PR=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=EA=B0=80=20=EC=9A=B0=EB=A6=AC=20=EA=B8=80=EB=A1=9C=EB=B2=8C=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=EC=B2=B4=ED=81=AC=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ai-pr-review.yml | 181 ++++++++++------------------- 1 file changed, 62 insertions(+), 119 deletions(-) diff --git a/.github/workflows/ai-pr-review.yml b/.github/workflows/ai-pr-review.yml index 6ccb5f1..fda4b31 100644 --- a/.github/workflows/ai-pr-review.yml +++ b/.github/workflows/ai-pr-review.yml @@ -1,138 +1,81 @@ -name: AI PR Review +name: AI Code Review on: pull_request: - types: [opened, synchronize, reopened] + types: [opened, synchronize] jobs: ai-review: runs-on: ubuntu-latest - if: github.actor != 'dependabot[bot]' permissions: contents: read pull-requests: write - issues: write steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: AI Code Review - uses: actions/github-script@v7 - with: - script: | - const OPENROUTER_API_KEY = '${{ secrets.OPENROUTER_API_KEY }}'; - - 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 filesToReview = diff.data.files.filter(file => - file.patch && - !file.filename.includes('test/') && - !file.filename.includes('build/') && - (file.filename.endsWith('.kt') || - file.filename.endsWith('.kts') || - file.filename.endsWith('.java') || - file.filename.endsWith('.yml') || - file.filename.endsWith('.yaml') || - file.filename.endsWith('.md')) - ); - - if (filesToReview.length === 0) { - console.log('No files to review'); - return; - } - - console.log(`Found ${filesToReview.length} files to review`); - - let overallReview = `## 🤖 AI 코드 리뷰\n\n`; - - let allChanges = ''; - const filesSummary = []; - - for (const file of filesToReview) { - filesSummary.push(`- ${file.filename} (+${file.additions}/-${file.deletions})`); - allChanges += `\n### ${file.filename}\n`; - allChanges += `\`\`\`diff\n${file.patch}\n\`\`\`\n`; - } - - try { - const response = await fetch('https://openrouter.ai/api/v1/chat/completions', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${OPENROUTER_API_KEY}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - model: 'x-ai/grok-4-fast:free', - messages: [{ - role: 'system', - content: `코드 리뷰어입니다. 다음 규칙만 검토하세요: + - 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 규칙**: 포맷팅, 네이밍 컨벤션 ## 응답 형식 -### ✅ 준수사항 -- [잘 지켜진 부분] - -### ❌ 위반사항 -- [파일:라인] 문제점과 수정방법 - -**점수**: X/10` - }, { - role: 'user', - content: `다음 PR의 변경사항을 리뷰해주세요: - -PR 제목: ${{ github.event.pull_request.title }} -PR 설명: ${{ github.event.pull_request.body }} - -변경된 파일: -${filesSummary.join('\n')} - -전체 변경사항: -${allChanges}` - }], - temperature: 0.3 - }) - }); - - if (response.ok) { - const result = await response.json(); - overallReview += result.choices[0].message.content + '\n\n'; - console.log('✅ API review completed successfully!'); - } else { - const errorText = await response.text(); - console.error('API request failed:', response.status, errorText); - overallReview += `> ⚠️ 리뷰 실패: ${response.status} - ${errorText}\n\n`; - } - } catch (error) { - console.error('Error during review:', error); - overallReview += `> ⚠️ 리뷰 실패: ${error.message}\n\n`; - } - - overallReview += `---\n`; - overallReview += `
\n💡 리뷰 정보\n\n`; - overallReview += `- 모델: Grok-4-fast\n`; - overallReview += `- 리뷰 시간: ${new Date().toLocaleString('ko-KR', {timeZone: 'Asia/Seoul'})}\n`; - overallReview += `- PR: #${{ github.event.number }}\n\n`; - overallReview += `
`; - - await github.rest.pulls.createReview({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.payload.pull_request.number, - body: overallReview, - event: 'COMMENT' - }); - - console.log(`✅ Review completed for ${filesToReview.length} files!`); \ No newline at end of file +🟢 **좋은점**: [규칙을 잘 지킨 부분] +🟡 **개선사항**: [더 좋게 할 수 있는 부분] +🔴 **문제점**: [반드시 수정해야 할 부분]` + }, { + 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