Skip to content

Commit 3bf3876

Browse files
committed
feat: 코드 리뷰 및 요약 기능 개선 (comments 배열 및 요약 정보 추가)
1 parent 7cd7b3f commit 3bf3876

3 files changed

Lines changed: 221 additions & 1 deletion

File tree

action/src/chat.d.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,22 @@ export declare class Chat {
77
private callOpenAI;
88
codeReview: (patch: string) => Promise<{
99
lgtm: boolean;
10-
review_comment: string;
10+
comments: Array<{
11+
line: number;
12+
severity: "critical" | "warning";
13+
category: "bug" | "security" | "logic" | "error-handling";
14+
body: string;
15+
}>;
1116
}>;
1217
summarizeChanges: (patch: string) => Promise<{
18+
changeType: "feature" | "bugfix" | "refactor" | "docs" | "test" | "chore";
19+
title: string;
1320
summary: string;
21+
walkthrough: Array<{
22+
file: string;
23+
changes: string;
24+
intent: string;
25+
}>;
26+
affectedAreas: string[];
1427
}>;
1528
}

action/test-review.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {};

test-review.ts

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { Chat } from "./src/chat.js";
2+
3+
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
4+
if (!OPENAI_API_KEY) {
5+
console.error("OPENAI_API_KEY 환경변수를 설정해주세요.");
6+
console.error("사용법: OPENAI_API_KEY=sk-... npx tsx test-review.ts");
7+
process.exit(1);
8+
}
9+
10+
const chat = new Chat(OPENAI_API_KEY);
11+
12+
const testInput = `## 리뷰 지침 (STRICT)
13+
다음을 반드시 준수하세요 — 어기면 산출물은 실패로 간주됩니다.
14+
- **스코프: 이 파일의 Diff만** 다룹니다. 다른 파일/일반론 언급 금지.
15+
- **근거 필수:** 모든 지적은 Diff의 \`+\`(추가) 라인에 근거해, 해당 코드 스니펫을 인라인 코드로 포함하세요.
16+
- **전역 컨텍스트는 참고용**입니다. 스코프를 벗어난 내용이 필요하면 그 항목은 **작성하지 마세요**.
17+
- **우선순위:** 버그/보안/성능/테스트 중심. 사소한 스타일/개인 취향 금지.
18+
- **스코프 위반 방지:** 다른 파일이나 일반 베스트 프랙티스만을 언급하려 할 경우, 해당 코멘트는 생략합니다.
19+
20+
# File: src/services/userService.ts
21+
22+
## Diff
23+
\`\`\`diff
24+
@@ -1,15 +1,89 @@
25+
-import { db } from '../db';
26+
+import { db } from '../db';
27+
+import { Redis } from 'ioredis';
28+
+import { hash, compare } from 'bcrypt';
29+
+
30+
+const redis = new Redis(process.env.REDIS_URL);
31+
+const JWT_SECRET = "super-secret-key-12345";
32+
+
33+
+interface User {
34+
+ id: string;
35+
+ email: string;
36+
+ password: string;
37+
+ role: 'admin' | 'user';
38+
+}
39+
+
40+
+export async function authenticateUser(email: string, password: string) {
41+
+ const query = \`SELECT * FROM users WHERE email = '\${email}'\`;
42+
+ const result = await db.query(query);
43+
+
44+
+ if (result.rows.length === 0) {
45+
+ return null;
46+
+ }
47+
+
48+
+ const user = result.rows[0];
49+
+ const isValid = await compare(password, user.password);
50+
+
51+
+ if (isValid) {
52+
+ const token = generateToken(user);
53+
+ await redis.set(\`session:\${user.id}\`, token);
54+
+ return { user, token };
55+
+ }
56+
+
57+
+ return null;
58+
+}
59+
+
60+
+export async function getUserById(id: string): Promise<User> {
61+
+ const cached = await redis.get(\`user:\${id}\`);
62+
+ if (cached) {
63+
+ return JSON.parse(cached);
64+
+ }
65+
+
66+
+ const result = await db.query('SELECT * FROM users WHERE id = $1', [id]);
67+
+ const user = result.rows[0];
68+
+
69+
+ redis.set(\`user:\${id}\`, JSON.stringify(user), 'EX', 3600);
70+
+ return user;
71+
+}
72+
+
73+
+export async function updateUserRole(userId: string, newRole: string) {
74+
+ const user = await getUserById(userId);
75+
+
76+
+ if (user.role == 'admin') {
77+
+ throw new Error('Cannot modify admin users');
78+
+ }
79+
+
80+
+ await db.query(
81+
+ \`UPDATE users SET role = '\${newRole}' WHERE id = '\${userId}'\`
82+
+ );
83+
+
84+
+ redis.del(\`user:\${userId}\`);
85+
+}
86+
+
87+
+export async function deleteUser(userId: string) {
88+
+ try {
89+
+ await db.query('DELETE FROM users WHERE id = $1', [userId]);
90+
+ await redis.del(\`user:\${userId}\`);
91+
+ await redis.del(\`session:\${userId}\`);
92+
+ } catch (e) {
93+
+ }
94+
+}
95+
+
96+
+export function processUsers(users: User[]) {
97+
+ const results = [];
98+
+ for (let i = 0; i <= users.length; i++) {
99+
+ const user = users[i];
100+
+ if (user.role = 'admin') {
101+
+ results.push(user.email.toUpperCase());
102+
+ }
103+
+ }
104+
+ return results;
105+
+}
106+
+
107+
+function generateToken(user: User): string {
108+
+ return Buffer.from(JSON.stringify({
109+
+ id: user.id,
110+
+ email: user.email,
111+
+ exp: Date.now() + 86400000
112+
+ })).toString('base64');
113+
+}
114+
\`\`\`
115+
116+
---
117+
118+
# PR 메타 정보 (참고용)
119+
## PR 제목
120+
사용자 인증 및 캐싱 기능 추가
121+
122+
## 커밋 메시지
123+
feat: add user authentication with Redis caching
124+
`;
125+
126+
const summaryInput = `커밋 메시지:
127+
feat: add user authentication with Redis caching
128+
129+
변경된 파일:
130+
- src/services/userService.ts (added)
131+
- src/db/index.ts (modified)
132+
133+
전체 diff:
134+
@@ -1,15 +1,89 @@
135+
-import { db } from '../db';
136+
+import { db } from '../db';
137+
+import { Redis } from 'ioredis';
138+
+import { hash, compare } from 'bcrypt';
139+
+
140+
+const redis = new Redis(process.env.REDIS_URL);
141+
+const JWT_SECRET = "super-secret-key-12345";
142+
... (이하 동일)
143+
`;
144+
145+
console.log("=".repeat(60));
146+
console.log("🔍 코드 리뷰 테스트");
147+
console.log("=".repeat(60));
148+
console.log("\n📝 테스트할 코드에 의도적으로 포함된 문제들:");
149+
console.log(" 1. SQL 인젝션 (line 17, 56-57)");
150+
console.log(" 2. 하드코딩된 JWT 시크릿 (line 6)");
151+
console.log(" 3. 빈 catch 블록 (line 69)");
152+
console.log(" 4. off-by-one 에러 (line 73: i <= users.length)");
153+
console.log(" 5. 할당 vs 비교 오류 (line 75: user.role = 'admin')");
154+
console.log(" 6. null 체크 없이 접근 (line 43: user가 undefined일 수 있음)");
155+
console.log(" 7. == 대신 === 사용해야 함 (line 51)");
156+
console.log("\n");
157+
158+
async function runTests() {
159+
console.log("⏳ 코드 리뷰 요청 중...\n");
160+
161+
try {
162+
const reviewResult = await chat.codeReview(testInput);
163+
164+
console.log("📋 리뷰 결과:");
165+
console.log("-".repeat(40));
166+
console.log(`LGTM: ${reviewResult.lgtm}`);
167+
console.log(`발견된 이슈: ${reviewResult.comments.length}개\n`);
168+
169+
for (const comment of reviewResult.comments) {
170+
const emoji = comment.severity === "critical" ? "🔴" : "🟠";
171+
console.log(`${emoji} [${comment.severity.toUpperCase()}] Line ${comment.line}`);
172+
console.log(` 카테고리: ${comment.category}`);
173+
console.log(` 내용: ${comment.body}`);
174+
console.log();
175+
}
176+
177+
console.log("\n" + "=".repeat(60));
178+
console.log("📊 PR 요약 테스트");
179+
console.log("=".repeat(60));
180+
console.log("\n⏳ PR 요약 생성 중...\n");
181+
182+
const summaryResult = await chat.summarizeChanges(summaryInput);
183+
184+
console.log("📋 요약 결과:");
185+
console.log("-".repeat(40));
186+
console.log(`변경 유형: ${summaryResult.changeType}`);
187+
console.log(`제목: ${summaryResult.title}`);
188+
console.log(`\n요약:\n${summaryResult.summary}`);
189+
190+
if (summaryResult.walkthrough.length > 0) {
191+
console.log("\n📂 워크스루:");
192+
for (const w of summaryResult.walkthrough) {
193+
console.log(` - ${w.file}: ${w.changes} (${w.intent})`);
194+
}
195+
}
196+
197+
if (summaryResult.affectedAreas.length > 0) {
198+
console.log(`\n🎯 영향 범위: ${summaryResult.affectedAreas.join(", ")}`);
199+
}
200+
201+
} catch (e) {
202+
console.error("❌ 오류 발생:", e);
203+
}
204+
}
205+
206+
runTests();

0 commit comments

Comments
 (0)