From eae185bf2d6dabe3743b56f9777934408726c4b0 Mon Sep 17 00:00:00 2001 From: ChoiWonkeun Date: Thu, 13 Nov 2025 17:46:47 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Feat:=20=EB=A6=AC=EB=B7=B0=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=B0=B1=EC=97=94=EB=93=9C=20=EB=B0=8F=20=ED=94=84?= =?UTF-8?q?=EB=A1=A0=ED=8A=B8=EC=97=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/review-service/main.py | 177 +++++++++++++++---- apps/web/src/api/reviewService.js | 18 +- apps/web/src/pages/Review.jsx | 285 ++++++++++++++++++------------ package-lock.json | 6 + 4 files changed, 333 insertions(+), 153 deletions(-) create mode 100644 package-lock.json diff --git a/apps/review-service/main.py b/apps/review-service/main.py index fd856cd..1e9d47c 100644 --- a/apps/review-service/main.py +++ b/apps/review-service/main.py @@ -1,70 +1,177 @@ import os +import logging +from typing import Optional + import google.generativeai as genai +from google.generativeai.types import GenerationConfig from fastapi import FastAPI, Form, HTTPException from fastapi.middleware.cors import CORSMiddleware -from pydantic import BaseModel -from typing import Optional -app = FastAPI() +# ---------------------------------------------------- +# 로그 설정 +# ---------------------------------------------------- +logging.basicConfig(level=logging.INFO) +log = logging.getLogger("review-service") # ---------------------------------------------------- -# CORS 설정 (web/vite:3000 허용) +# FastAPI 앱 & CORS # ---------------------------------------------------- +app = FastAPI() + app.add_middleware( CORSMiddleware, - allow_origins=["http://localhost:3000"], + # allow_origins는 실제 배포 환경에 맞게 수정하세요. + allow_origins=["http://localhost", "http://localhost:3000", "*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ---------------------------------------------------- -# Gemini AI 설정 +# Gemini 설정 # ---------------------------------------------------- +MODEL_NAME = os.environ.get("GEMINI_MODEL", "gemini-1.5-flash-latest") # 최신 모델 권장 + try: - genai.configure(api_key=os.environ.get("GEMINI_API_KEY")) - model = genai.GenerativeModel('gemini-1.5-flash') + api_key = os.environ.get("GEMINI_API_KEY") + if not api_key: + raise RuntimeError("GEMINI_API_KEY 환경 변수가 설정되지 않았습니다.") + + genai.configure(api_key=api_key) + model = genai.GenerativeModel(MODEL_NAME) + log.info(f"Gemini 모델이 성공적으로 설정되었습니다: {MODEL_NAME}") except Exception as e: - print(f"Error configuring Gemini: {e}") + log.error(f"Gemini 설정 중 오류 발생: {e}") model = None # ---------------------------------------------------- -# 🚀 (신규) AI 코드 리뷰어 역할 부여 (System Prompt) +# 코드 리뷰용 베이스 프롬프트 (한국어 + 간결 요약) # ---------------------------------------------------- REVIEWER_PROMPT = """ -You are an expert Senior Software Engineer acting as a code reviewer. -Your task is to provide a constructive, professional code review for the user's code snippet. - -Follow these steps: -1. **Overall Assessment:** Start with a brief, one-sentence summary of the code's quality (e.g., "This is a clean implementation," "This works, but has some areas for improvement"). -2. **Positive Feedback:** (Optional) Briefly mention one thing that is done well. -3. **Constructive Criticism:** Identify 2-3 key areas for improvement. For each area, provide: - * **Issue:** Clearly state the problem (e.g., "Potential N+1 query," "Variable name is unclear," "Inefficient algorithm"). - * **Suggestion:** Provide a concrete example of how to fix it or a better approach. -4. **Conclusion:** End with an encouraging summary. - -Format your response using Markdown. Use **bold** text for headings (like **Issue:** and **Suggestion:**) and `code snippets` for code. Do not use Markdown headings (#, ##). +당신은 시니어 소프트웨어 엔지니어이자 코드 리뷰어입니다. +다음 요구사항을 지켜 **한국어로 아주 간결하게** 코드 리뷰를 작성하세요. + +- 전체 분량은 15줄 이내로 요약합니다. +- 아래 4가지 관점으로만 평가합니다. + 1) 목적 부합성 및 설계 + 2) 정확성 및 견고성 + 3) 성능 및 효율성 + 4) 유지보수성 및 가독성 +- 각 관점마다 **Issue 0~2개, Suggestion 0~2개**만 적고, 가장 중요한 것부터 써주세요. +- 불필요한 장문 설명, 장황한 예시는 넣지 않습니다. +- 코드 전체를 다시 붙여쓰지 말고, 필요한 경우에만 한두 줄 정도의 예시만 사용하세요. +- 말투는 “~입니다 / ~해 주세요” 형태로 정중하고 담백하게 작성합니다. + +출력 형식: + +[요약] +- 한 줄로 전체 평가 + +[1. 목적 부합성 및 설계] +- Issue: ... +- Suggestion: ... + +[2. 정확성 및 견고성] +- Issue: ... +- Suggestion: ... + +[3. 성능 및 효율성] +- Issue: ... +- Suggestion: ... + +[4. 유지보수성 및 가독성] +- Issue: ... +- Suggestion: ... """ # ---------------------------------------------------- -# API 엔드포인트 정의 +# 헬스 체크 +# ---------------------------------------------------- +@app.get("/") +def read_root(): + return {"status": "Review Service is running.", "model": MODEL_NAME} + + +# ---------------------------------------------------- +# 코드 리뷰 엔드포인트 # ---------------------------------------------------- @app.post("/api/review/") -async def handle_code_review(code: str = Form(...)): # 👈 Review.jsx의 FormData("code")를 받음 +async def handle_code_review( + code: str = Form(...), + comment: Optional[str] = Form(None), + repo_url: Optional[str] = Form(None), +): if not model: - raise HTTPException(status_code=503, detail="Gemini AI model is not configured.") + raise HTTPException( + status_code=503, + detail="Gemini AI 모델이 설정되지 않았습니다. 서버 로그를 확인하세요.", + ) + + if not code.strip(): + raise HTTPException( + status_code=400, + detail="리뷰할 코드가 비어 있습니다.", + ) + # 추가 컨텍스트 정리 + extra_context_parts = [] + if comment: + extra_context_parts.append( + "사용자가 중점적으로 보고 싶은 부분 / 요구사항:\n" + f"{comment.strip()}" + ) + if repo_url: + extra_context_parts.append( + "참고용 GitHub Repository URL:\n" + f"{repo_url.strip()}" + ) + + extra_context = ("\n\n".join(extra_context_parts) + if extra_context_parts + else "별도 요구사항 없음") + + # 프롬프트 구성 + full_prompt = f"""{REVIEWER_PROMPT} + +[프로젝트/코드 맥락] +{extra_context} + +[리뷰 대상 코드] +```text +{code} +``` +""" + # --- ★★★ 여기가 수정된 지점입니다 ★★★ --- + # 1. full_prompt 변수가 `"""`로 위에서 완전히 끝났습니다. + # 2. `try` 블록이 `handle_code_review` 함수 내부에 + # 올바르게 '들여쓰기' 되었습니다. + try: - # 1. 시스템 프롬프트와 사용자 코드를 결합하여 API 호출 - full_prompt = f"{REVIEWER_PROMPT}\n\nHere is the code to review:\n```\n{code}\n```" - response = model.generate_content(full_prompt) + response = await model.generate_content_async( # 비동기(async) 호출 + full_prompt, + generation_config=GenerationConfig( + temperature=0.4, + max_output_tokens=2048, # 약간 여유 있게 늘림 + ), + ) - # 2. AI의 리뷰 텍스트를 반환 (Review.jsx의 data.review에 해당) - return {"review": response.text} + # response.text가 비어있거나 없는 경우를 더 안전하게 처리 + review_text = (response.text or "").strip() + + if not review_text: + log.warning("Gemini에서 빈 응답을 반환했습니다.") + # prompt가 차단되었을 수 있음 + if response.prompt_feedback and response.prompt_feedback.block_reason: + log.error(f"프롬프트가 차단되었습니다: {response.prompt_feedback.block_reason}") + raise HTTPException(status_code=400, detail=f"프롬프트가 차단되었습니다: {response.prompt_feedback.block_reason}") + raise RuntimeError("Gemini에서 빈 응답을 받았습니다.") + + return {"review": review_text} except Exception as e: - raise HTTPException(status_code=500, detail=f"Failed to get review: {str(e)}") - -@app.get("/") -def read_root(): - return {"status": "Review Service is running."} \ No newline at end of file + log.error(f"리뷰 생성 중 오류 발생: {e}") + # API 키 오류 등 구체적인 예외 처리를 추가하면 더 좋습니다. + raise HTTPException( + status_code=500, + detail=f"리뷰 생성에 실패했습니다: {str(e)}", + ) \ No newline at end of file diff --git a/apps/web/src/api/reviewService.js b/apps/web/src/api/reviewService.js index 17adab0..ada5e6c 100644 --- a/apps/web/src/api/reviewService.js +++ b/apps/web/src/api/reviewService.js @@ -1,16 +1,18 @@ -// src/api/reviewService.js - const BASE_URL = "/api"; /** * AI 코드 리뷰 요청 - * @param {string} code - 리뷰할 코드 문자열 - * @returns {Promise} - { review: "..."} 형태 + * @param {string} code + * @param {string} [comment] + * @param {string} [repoUrl] */ -export const fetchCodeReview = async (code) => { +export const fetchCodeReview = async (code, comment, repoUrl) => { const formData = new FormData(); formData.append("code", code); + if (comment) formData.append("comment", comment); + if (repoUrl) formData.append("repo_url", repoUrl); + const res = await fetch(`${BASE_URL}/review/`, { method: "POST", body: formData, @@ -18,7 +20,11 @@ export const fetchCodeReview = async (code) => { if (!res.ok) { const err = await res.json().catch(() => ({})); - throw new Error(err.error || `AI 리뷰 요청 실패: ${res.statusText || res.status}`); + throw new Error( + err.detail || + err.error || + `AI 리뷰 요청 실패: ${res.statusText || res.status}`, + ); } return await res.json(); diff --git a/apps/web/src/pages/Review.jsx b/apps/web/src/pages/Review.jsx index 7d79a97..96dea53 100644 --- a/apps/web/src/pages/Review.jsx +++ b/apps/web/src/pages/Review.jsx @@ -1,7 +1,11 @@ import { useState } from "react"; import { Link } from "react-router-dom"; import { - IconArrowLeft, IconSparkles, IconLoader2, IconAlertTriangle, IconCopy + IconArrowLeft, + IconSparkles, + IconLoader2, + IconAlertTriangle, + IconCopy, } from "@tabler/icons-react"; import Particles from "@tsparticles/react"; import { fetchCodeReview } from "../api/reviewService"; @@ -9,7 +13,11 @@ import { fetchCodeReview } from "../api/reviewService"; const particlesOptions = { background: { color: { value: "transparent" } }, fpsLimit: 60, - interactivity: { events: { resize: true } }, + interactivity: { + events: { + resize: true, + }, + }, particles: { color: { value: "#8eb5ff" }, links: { enable: true, opacity: 0.22, width: 1 }, @@ -22,6 +30,8 @@ const particlesOptions = { export default function Review() { const [code, setCode] = useState(""); + const [userComment, setUserComment] = useState(""); + const [repoUrl, setRepoUrl] = useState(""); const [review, setReview] = useState(""); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -30,13 +40,19 @@ export default function Review() { const handleSubmit = async (e) => { e.preventDefault(); if (!code.trim()) return; + setIsLoading(true); setError(null); setReview(""); + try { - const data = await fetchCodeReview(code); - if (data?.review) setReview(data.review); - else throw new Error(data?.error || "Unknown error"); + // reviewService.js에서 fetchCodeReview를 (code, comment, repoUrl) 받도록 맞춰주면 됨 + const data = await fetchCodeReview(code, userComment, repoUrl); + if (data?.review) { + setReview(data.review); + } else { + throw new Error(data?.error || "Unknown error"); + } } catch (err) { setError(err.message || "Failed to fetch review"); } finally { @@ -49,130 +65,175 @@ export default function Review() { await navigator.clipboard.writeText(review || ""); setCopied(true); setTimeout(() => setCopied(false), 1200); - } catch {} + } catch { + /* noop */ + } }; return ( -
- {/* 배경 입자: 배경 레이어(z-0)로 */} +
+ {/* 배경 입자 */} - {/* Header */} -
- - Home - - -

- AI Code Review -

- -
-
- - {/* Main */} -
- {/* LEFT: Code input */} -
-
-
-
- Paste Your Code -
- -