@@ -11,12 +11,178 @@ on:
 jobs:
   code_review:
     runs-on: ubuntu-latest
-    name: ChatGPT Code Review
+    name: Gemini Code Review
     steps:
-      - uses: anc95/ChatGPT-CodeReview@main
+      - uses: actions/checkout@v4
+
+      - name: Setup Node.js
+        uses: actions/setup-node@v4
+        with:
+          node-version: "20"
+
+      - name: Install dependencies
+        run: npm install @google/generative-ai parse-diff
+
+      - name: Review Changed Files
+        uses: actions/github-script@v7
         env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
-          # optional
-          LANGUAGE: Korean
-          MODEL: gpt-4o-mini-2024-07-18 # https://platform.openai.com/docs/models
+          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
+          GEMINI_MODEL: ${{ secrets.GEMINI_MODEL }}
+          MAX_CONCURRENT_REVIEWS: 3 # 동시에 처리할 파일 수
+          MAX_RETRIES: 3
+          RETRY_DELAY: 1000
+
+        with:
+          script: |
+            const { GoogleGenerativeAI } = require("@google/generative-ai");
+            const parseDiff = require('parse-diff');
+            // core는 이미 github-script에서 제공됨
+
+            // 재시도 로직을 포함한 API 호출 함수
+            async function withRetry(fn, retries = process.env.MAX_RETRIES) {
+              for (let i = 0; i < retries; i++) {
+                try {
+                  return await fn();
+
+                } catch (error) {
+                  if (error.message.includes('rate limit') && i < retries - 1) {
+                    const delay = process.env.RETRY_DELAY * Math.pow(2, i);
+                    console.log(`Rate limit hit, waiting ${delay}ms before retry ${i + 1}`);
+
+                    await new Promise(resolve => setTimeout(resolve, delay));
+                    continue;
+                  }
+
+                  throw error;
+                }
+              }
+            }
+
+            // Gemini AI 클라이언트 초기화
+            const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
+            const model = genAI.getGenerativeModel({ model: process.env.GEMINI_MODEL });
+
+            /**
+             * 코드 변경사항을 검토하고 피드백을 생성
+             * 
+             * @param content 리뷰할 코드 내용
+             * @param filename 파일명
+             * @returns 리뷰 코멘트
+             */
+            async function reviewCode(content, filename) {
+              try {
+                // 파일명 sanitize
+                const sanitizedFilename = filename.replace(/[^a-zA-Z0-9.-_\/]/g, '_');
+
+                const prompt = `당신은 시니어 개발자입니다. 아래 ${sanitizedFilename} 파일의 코드를 검토하고 다음 사항들을 한국어로 리뷰해주세요:
+                1. 코드의 품질과 가독성
+                2. 잠재적인 버그나 문제점
+                3. 성능 개선 포인트
+                4. 보안 관련 이슈
+                5. 개선을 위한 구체적인 제안
+
+                코드:
+                ${content}`;
+
+                const result = await withRetry(() => model.generateContent(prompt));
+                const response = await result.response;
+
+                if (!response.text()) {
+                  throw new Error('Gemini API returned empty response');
+                }
+
+                console.log(`Successfully reviewed ${filename}`);
+                return response.text();
+
+              } catch (error) {
+                console.error(`Error reviewing ${filename}:`, error);
+                return `⚠️ 코드 리뷰 중 오류가 발생했습니다: ${error.message}
+                       다시 시도하시거나 관리자에게 문의해주세요.`;
+              }
+            }
+
+            /**
+             * PR의 각 파일을 처리하고 리뷰 코멘트 작성
+             * @param file PR에서 변경된 파일 정보
+             * @param pr PR 정보
+             */
+            async function processFile(file, pr) {
+              if (file.status === 'removed') {
+                console.log(`Skipping removed file: ${file.filename}`);
+                return;
+              }
+
+              try {
+                if (!file.patch) {
+                  console.warn(`No patch found for ${file.filename}, skipping.`);
+                  return;
+                }
+
+                const diff = parseDiff(file.patch)[0];
+                if (!diff || !diff.chunks) {
+                  console.log(`No valid diff found for ${file.filename}`);
+                  return;
+                }
+
+                // 모든 변경사항을 하나로 합치기 (추가된 부분만 필터링)
+                const changes = diff.chunks
+                  .flatMap(chunk => chunk.changes)
+                  .filter(change => change.type === 'add');
+
+                if (changes.length === 0) {
+                  console.log(`No added changes found in ${file.filename}`);
+                  return;
+                }
+
+                // 변경사항을 하나의 문자열로 합치기
+                const content = changes.map(change => change.content).join('\n');
+                const review = await reviewCode(content, file.filename);
+
+                // PR에 리뷰 코멘트 작성 (파일명을 헤더로 추가)
+                return github.rest.issues.createComment({
+                  owner: context.repo.owner,
+                  repo: context.repo.repo,
+                  issue_number: context.issue.number,
+                  body: `## ${file.filename} 리뷰\n\n${review}`
+                });
+
+              } catch (error) {
+                console.error(`Error processing ${file.filename}:`, error);
+                throw error;
+              }
+            }
+
+            try {
+              // PR 정보 조회
+              const { data: pr } = await github.rest.pulls.get({
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                pull_number: context.issue.number,
+              });
+
+              // 변경된 파일 목록 조회
+              const { data: files } = await github.rest.pulls.listFiles({
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                pull_number: context.issue.number,
+              });
+
+              console.log(`Starting review of ${files.length} files...`);
+
+              // 모든 파일 처리 완료를 기다림
+              const promises = [];
+              const batchSize = parseInt(process.env.MAX_CONCURRENT_REVIEWS);
+            
+              for (let i = 0; i < files.length; i += batchSize) {
+                const batch = files.slice(i, i + batchSize);
+                const batchPromises = batch.map(file => processFile(file, pr));
+                promises.push(...batchPromises);
+              }
+
+              await Promise.all(promises);
+              console.log('All reviews completed successfully');
+
+            } catch (error) {
+              console.error('Workflow failed:', error);
+              core.setFailed(`Workflow failed: ${error.message}`);
+            }

리뷰: ## .github/workflows/code_review.yml 코드 리뷰

다음은 주어진 `.github/workflows/code_review.yml` 파일에 대한 코드 리뷰입니다.

**1. 코드 품질 및 가독성**

*   **전반적으로 양호:** 코드는 비교적 깔끔하고 가독성이 좋습니다.  주석도 적절하게 달려 있어 코드의 의도를 파악하는 데 도움이 됩니다.
*   **일관성:**  들여쓰기, 변수명 규칙 등이 일관성을 유지하고 있습니다.
*   **모듈화:**  `reviewCode`, `processFile` 등의 함수로 로직을 분리하여 재사용성과 유지보수성을 높였습니다.
*   **통계 수집:**  `stats` 객체를 사용하여 코드 리뷰 과정에 대한 통계를 수집하는 것은 유용합니다.  문제 발생 시 디버깅에 도움이 될 수 있습니다.

**2. 잠재적인 버그 또는 문제점**

*   **에러 핸들링:**  `try...catch` 블록을 사용하여 에러를 처리하고 있지만, 에러 메시지를 사용자에게 친절하게 보여주는 것은 중요합니다.  `reviewCode` 함수에서 에러 발생 시 "⚠️ 코드 리뷰 중 오류가 발생했습니다..."와 같은 메시지를 보여주는 것은 좋은 예시입니다.
*   **Rate Limit 처리:** `withRetry` 함수를 통해 rate limit을 처리하는 것은 좋습니다. 하지만 `RETRY_DELAY`를 고정 값으로 사용하는 것보다, 백오프(backoff) 전략을 사용하는 것이 좋습니다. 예를 들어, 매 retry마다 딜레이를 늘리는 방식입니다.
*   **`file.patch` 에러 처리:** `parseDiff(file.patch)[0]` 부분에서, `file.patch`가 존재하지 않거나 유효하지 않은 경우에 대한 예외 처리가 필요할 수 있습니다.  `parseDiff` 함수가 에러를 던질 가능성이 있는지 확인하고, 있다면 `try...catch` 블록으로 감싸는 것을 고려하십시오.
*   **파일 이름 sanitize:** 파일 이름 sanitize 로직이 충분히 안전한지 검토해야 합니다. 특히 특수문자가 포함된 경우 예상치 못한 결과를 초래할 수 있습니다.
*   **동시성 처리:** `MAX_CONCURRENT_REVIEWS`를 사용하여 동시성 처리를 하는 것은 좋지만, 파일 수가 적을 경우 병렬 처리가 오히려 오버헤드가 될 수 있습니다.  파일 수에 따라 동시성 처리를 활성화/비활성화하는 로직을 추가하는 것을 고려해볼 수 있습니다.
*   **`summary` 에러 메시지 누락:** `stats.issues`에 담긴 에러 메시지가 매우 길 경우, summary 코멘트에 전부 담기지 않을 수 있습니다. summary 코멘트의 최대 길이를 고려하여 메시지를 자르거나 요약하는 방안을 고려해볼 수 있습니다.
*   **리뷰 코멘트 위치:** 첫 번째 청크의 시작 라인에만 리뷰 코멘트를 작성하는 것은 변경 사항이 여러 청크에 걸쳐 있을 경우 누락되는 부분이 있을 수 있습니다.  각 청크별로 코멘트를 작성하거나, 변경 사항 전체에 대한 코멘트를 작성하는 것을 고려해볼 수 있습니다.
*   **`create-or-update-comment` 액션의 중복 헤더:** workflow 마지막 부분에 "## Code Review Summary" 헤더가 중복됩니다. 이전 코드에서 Summary를 이미 생성했으므로, 마지막 액션에서는 간단한 완료 메시지만 남기는 것이 좋습니다.

**3. 성능 개선 포인트**

*   **API 호출 최적화:**  Gemini API 호출 횟수를 줄이는 것이 중요합니다.  가능하다면 여러 파일에 대한 리뷰를 한 번의 API 호출로 처리하는 것을 고려하십시오.  (Gemini API의 최대 입력 크기 제한을 고려해야 합니다.)
*   **불필요한 데이터 처리:**  PR 정보 조회(`github.rest.pulls.get`)는 모든 파일에 대해 동일하게 수행됩니다.  PR 정보를 한 번만 조회하고, 파일 처리 함수에 전달하는 것이 효율적입니다.
*   **`parse-diff` 결과 캐싱:**  `parse-diff`는 비교적 무거운 작업일 수 있습니다.  `file.patch`가 동일한 경우 `parse-diff` 결과를 캐싱하여 재사용하는 것을 고려해볼 수 있습니다. (단, 메모리 사용량을 고려해야 합니다.)

**4. 보안 관련 이슈**

*   **API 키 보안:**  `GEMINI_API_KEY`는 GitHub Actions secrets에 안전하게 저장되어 있으므로 안전합니다.  하지만, 코드 내에서 API 키가 노출되지 않도록 주의해야 합니다.
*   **입력값 검증:**  사용자로부터 입력받는 데이터 (예: 파일 이름, 코드 내용)에 대한 검증을 수행하여 injection 공격을 방지해야 합니다.  파일 이름 sanitize는 좋은 예시이지만, 다른 입력값에 대해서도 검증을 수행해야 합니다.
*   **서드파티 라이브러리:**  `@google/generative-ai`, `parse-diff` 등 서드파티 라이브러리의 보안 취약점을 주기적으로 확인하고 업데이트해야 합니다.  Dependabot 등을 사용하여 의존성 관리를 자동화하는 것을 고려하십시오.

**5. 개선을 위한 구체적인 제안**

1.  **백오프(Backoff) 전략 적용:** `withRetry` 함수에서 `RETRY_DELAY`를 고정 값 대신 지수 백오프(exponential backoff) 전략을 사용하여 rate limit 상황에 더욱 효과적으로 대응합니다.
    ```javascript
    async function withRetry(fn, retries = process.env.MAX_RETRIES) {
      for (let i = 0; i < retries; i++) {
        try {
          return await fn();
        } catch (error) {
          if (error.message.includes('rate limit') && i < retries - 1) {
            const delay = process.env.RETRY_DELAY * Math.pow(2, i); // 지수 백오프
            console.log(`Rate limit hit, waiting ${delay}ms before retry ${i + 1}`);
            await new Promise(resolve => setTimeout(resolve, delay));
            continue;
          }
          throw error;
        }
      }
    }
    ```

2.  **PR 정보 캐싱:** PR 정보를 한 번만 조회하고, `processFile` 함수에 전달합니다.
    ```javascript
    // PR 정보 조회
    const { data: pr } = await github.rest.pulls.get({
      owner: context.repo.owner,
      repo: context.repo.repo,
      pull_number: context.issue.number,
    });

    // 파일 처리 (3개씩 동시 처리)
    for (let i = 0; i < files.length; i += 3) {
      const batch = files.slice(i, i + 3);
      await Promise.all(batch.map(file => processFile(file, pr)));
    }

    // processFile 함수 수정
    async function processFile(file, pr) { ... }
    ```

3.  **Rate Limit 정보 로깅:** rate limit에 걸렸을 때, 남아있는 limit과 reset 시간 정보를 로그에 출력하여 디버깅에 활용합니다. (Gemini API에서 해당 정보를 제공하는지 확인해야 합니다.)

4.  **상세 에러 로깅:** 에러 발생 시 스택 트레이스 전체를 로깅하여 문제 해결에 도움을 줍니다.
    ```javascript
    console.error(`Error reviewing ${filename}:`, error, error.stack);
    ```

5.  **리뷰 요약 개선:** summary 코멘트에 에러 메시지가 잘려서 표시되는 문제를 해결하기 위해, 에러 메시지를 요약하거나, 에러 메시지 전체를 표시할 수 있는 별도의 페이지를 링크하는 방안을 고려해볼 수 있습니다.

6.  **마지막 코멘트 액션 수정:** 마지막 코멘트 액션에서 헤더를 제거하고, 간단한 완료 메시지만 남깁니다.
    ```yaml
    - name: Create or Update Comment
      uses: peter-evans/create-or-update-comment@v3
      with:
        comment-id: ${{ steps.fc.outputs.comment-id }}
        issue-number: ${{ github.event.pull_request.number }}
        body: |
          코드 리뷰가 완료되었습니다. 각 파일의 변경사항에 대한 피드백을 확인해주세요.
        edit-mode: replace
    ```

이러한 개선 사항들을 적용하면 코드 리뷰 워크플로우의 품질, 성능, 안정성을 더욱 향상시킬 수 있습니다.