|
| 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