From 21266f1772a426d74fb690d5e1d17109291f5f95 Mon Sep 17 00:00:00 2001 From: chimaek Date: Tue, 29 Jul 2025 16:08:26 +0900 Subject: [PATCH 01/13] feat: Add comprehensive test examples for all action features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove old test-files and test directories - Add examples directory with diverse code samples - Include JavaScript, TypeScript, Python, Java, Go, and Rust examples - Each example contains various types of issues: * Security vulnerabilities (SQL injection, hardcoded secrets, etc.) * Performance issues (inefficient algorithms, memory leaks) * Code style problems (naming conventions, inconsistent formatting) - Examples demonstrate all review_type options (full, security, performance, style) - Perfect for testing all Claude Code Review Action capabilities ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 2 +- examples/DatabaseManager.java | 194 ++++++++++++++++++ examples/data_analyzer.py | 123 +++++++++++ examples/file_processor.go | 212 +++++++++++++++++++ {example => examples}/images/pr_example.png | Bin examples/payment-processor.ts | 100 +++++++++ examples/security_manager.rs | 216 ++++++++++++++++++++ examples/user-service.js | 91 +++++++++ test-files/api-endpoints.js | 119 ----------- test-files/buggy-calculator.js | 60 ------ test-files/user-service.js | 109 ---------- test/additional-test.js | 41 ---- test/sample-code.js | 106 ---------- test/sample-security.py | 90 -------- 14 files changed, 937 insertions(+), 526 deletions(-) create mode 100644 examples/DatabaseManager.java create mode 100644 examples/data_analyzer.py create mode 100644 examples/file_processor.go rename {example => examples}/images/pr_example.png (100%) create mode 100644 examples/payment-processor.ts create mode 100644 examples/security_manager.rs create mode 100644 examples/user-service.js delete mode 100644 test-files/api-endpoints.js delete mode 100644 test-files/buggy-calculator.js delete mode 100644 test-files/user-service.js delete mode 100644 test/additional-test.js delete mode 100644 test/sample-code.js delete mode 100644 test/sample-security.py diff --git a/README.md b/README.md index fef9de1..281c551 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Claude API๋ฅผ ํ™œ์šฉํ•œ ์ง€๋Šฅํ˜• AI ์ฝ”๋“œ ๋ฆฌ๋ทฐ GitHub Action์ž…๋‹ˆ๋‹ค. Pull ... (์ƒ์„ธํ•œ ์ด์Šˆ ๋ชฉ๋ก) ``` -![PR Review Comment](https://github.com/chimaek/claude-code-review-action/blob/master/example/images/pr_example.png) +![PR Review Comment](https://github.com/chimaek/claude-code-review-action/blob/master/examples/images/pr_example.png) ## ๐Ÿš€ ๋น ๋ฅธ ์‹œ์ž‘ diff --git a/examples/DatabaseManager.java b/examples/DatabaseManager.java new file mode 100644 index 0000000..1ea6657 --- /dev/null +++ b/examples/DatabaseManager.java @@ -0,0 +1,194 @@ +// Java ์˜ˆ์‹œ: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งค๋‹ˆ์ € (๋ณด์•ˆ ๋ฐ ์„ฑ๋Šฅ ์ด์Šˆ) +package com.example.database; + +import java.sql.*; +import java.util.*; +import java.io.FileWriter; +import java.io.IOException; +import java.util.logging.Logger; + +public class DatabaseManager { + // ๋ณด์•ˆ ์ด์Šˆ: ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ •๋ณด + private static final String DB_URL = "jdbc:mysql://localhost:3306/mydb"; + private static final String USERNAME = "admin"; + private static final String PASSWORD = "password123"; + + private Connection connection; + private static Logger logger = Logger.getLogger(DatabaseManager.class.getName()); + + // ์„ฑ๋Šฅ ์ด์Šˆ: ์‹ฑ๊ธ€ํ†ค ํŒจํ„ด ๋ฏธ์‚ฌ์šฉ, ๋งค๋ฒˆ ์ƒˆ ์—ฐ๊ฒฐ + public DatabaseManager() { + try { + Class.forName("com.mysql.cj.jdbc.Driver"); + connection = DriverManager.getConnection(DB_URL, USERNAME, PASSWORD); + } catch (Exception e) { + // ๋ณด์•ˆ ์ด์Šˆ: ์ƒ์„ธํ•œ ์—๋Ÿฌ ์ •๋ณด ๋…ธ์ถœ + logger.severe("Database connection failed: " + e.getMessage()); + e.printStackTrace(); + } + } + + // ๋ณด์•ˆ ์ด์Šˆ: SQL Injection ์ทจ์•ฝ์  + public List getUsersByRole(String role) { + List users = new ArrayList<>(); + + try { + // ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ ์—†์ด ๋ฌธ์ž์—ด ์—ฐ๊ฒฐ + String query = "SELECT * FROM users WHERE role = '" + role + "'"; + logger.info("Executing query: " + query); // ์ฟผ๋ฆฌ ๋กœ๊น… (์ •๋ณด ๋…ธ์ถœ) + + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(query); + + while (rs.next()) { + User user = new User(); + user.setId(rs.getInt("id")); + user.setUsername(rs.getString("username")); + user.setPassword(rs.getString("password")); // ๋ณด์•ˆ ์ด์Šˆ: ๋น„๋ฐ€๋ฒˆํ˜ธ ํฌํ•จ + user.setRole(rs.getString("role")); + users.add(user); + } + + // ๋ฆฌ์†Œ์Šค ํ•ด์ œ ์•ˆํ•จ (๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜) + + } catch (SQLException e) { + logger.severe("Query execution failed: " + e.getMessage()); + } + + return users; + } + + // ์„ฑ๋Šฅ ์ด์Šˆ: N+1 ์ฟผ๋ฆฌ ๋ฌธ์ œ + public void updateUserProfiles(List userIds) { + for (Integer userId : userIds) { + try { + // ๊ฐ ์‚ฌ์šฉ์ž๋งˆ๋‹ค ๊ฐœ๋ณ„ ์ฟผ๋ฆฌ ์‹คํ–‰ + String query = "UPDATE users SET last_updated = NOW() WHERE id = ?"; + PreparedStatement stmt = connection.prepareStatement(query); + stmt.setInt(1, userId); + stmt.executeUpdate(); + + // ์—ฐ๊ฒฐ ๋‹ซ์ง€ ์•Š์Œ (๋ฆฌ์†Œ์Šค ๋ˆ„์ˆ˜) + + } catch (SQLException e) { + logger.severe("Update failed for user " + userId + ": " + e.getMessage()); + } + } + } + + // ๋ณด์•ˆ ์ด์Šˆ: ๊ถŒํ•œ ๊ฒ€์ฆ ์—†๋Š” ์‚ญ์ œ + public boolean deleteUser(String username) { + try { + // ๊ถŒํ•œ ํ™•์ธ ์—†์ด ์‚ญ์ œ ์‹คํ–‰ + String query = "DELETE FROM users WHERE username = '" + username + "'"; + Statement stmt = connection.createStatement(); + int affectedRows = stmt.executeUpdate(query); + + // ๋ณด์•ˆ ์ด์Šˆ: ์‚ญ์ œ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด ๋กœ๊น… + logger.info("Deleted user: " + username + ", affected rows: " + affectedRows); + + return affectedRows > 0; + } catch (SQLException e) { + logger.severe("User deletion failed: " + e.getMessage()); + return false; + } + } + + // ์„ฑ๋Šฅ ์ด์Šˆ: ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ๋ฒˆ์— ๋ฉ”๋ชจ๋ฆฌ๋กœ ๋กœ๋“œ + public List getAllUserEmails() { + List emails = new ArrayList<>(); + + try { + String query = "SELECT email FROM users"; // LIMIT ์—†์Œ + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(query); + + // ๋ชจ๋“  ๊ฒฐ๊ณผ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅ + while (rs.next()) { + emails.add(rs.getString("email")); + } + + } catch (SQLException e) { + logger.severe("Failed to fetch emails: " + e.getMessage()); + } + + return emails; + } + + // ๋ณด์•ˆ ์ด์Šˆ: ๋ฏผ๊ฐํ•œ ์ •๋ณด๋ฅผ ํ‰๋ฌธ ํŒŒ์ผ๋กœ ์ €์žฅ + public void exportUserData(List users) { + try { + FileWriter writer = new FileWriter("user_export.txt"); + + for (User user : users) { + // ๋น„๋ฐ€๋ฒˆํ˜ธ ํฌํ•จํ•˜์—ฌ ๋‚ด๋ณด๋‚ด๊ธฐ + String line = user.getId() + "," + user.getUsername() + "," + + user.getPassword() + "," + user.getRole() + "\n"; + writer.write(line); + } + + writer.close(); + logger.info("User data exported to user_export.txt"); + + } catch (IOException e) { + logger.severe("Export failed: " + e.getMessage()); + } + } + + // ์ฝ”๋“œ ์Šคํƒ€์ผ ์ด์Šˆ: ๋„ค์ด๋ฐ ์ปจ๋ฒค์…˜ ์œ„๋ฐ˜ + public void ExecuteBatchOperation(List SQLQueries) { + try { + connection.setAutoCommit(false); + Statement stmt = connection.createStatement(); + + // ์ฝ”๋“œ ์Šคํƒ€์ผ ์ด์Šˆ: ์ผ๊ด€์„ฑ ์—†๋Š” ๋“ค์—ฌ์“ฐ๊ธฐ + for(String query : SQLQueries) { + stmt.addBatch(query); // SQL Injection ๊ฐ€๋Šฅ + logger.info("Added to batch: " + query); + } + + int[] results = stmt.executeBatch(); + connection.commit(); + + // ์„ฑ๋Šฅ ์ด์Šˆ: ๋ถˆํ•„์š”ํ•œ ๋กœ๊น… + for (int i = 0; i < results.length; i++) { + logger.info("Batch operation " + i + " affected " + results[i] + " rows"); + } + + } catch (SQLException e) { + try { + connection.rollback(); + } catch (SQLException rollbackError) { + logger.severe("Rollback failed: " + rollbackError.getMessage()); + } + logger.severe("Batch operation failed: " + e.getMessage()); + } + } + + // ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜: finalize์—์„œ ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ (์ž˜๋ชป๋œ ํŒจํ„ด) + @Override + protected void finalize() throws Throwable { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + super.finalize(); + } +} + +// ์ฝ”๋“œ ์Šคํƒ€์ผ ์ด์Šˆ: ๊ฐ™์€ ํŒŒ์ผ์— ์—ฌ๋Ÿฌ ํด๋ž˜์Šค +class User { + private int id; + private String username; + private String password; + private String role; + + // getter/setter ์ƒ๋žต + public int getId() { return id; } + public void setId(int id) { this.id = id; } + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + public String getRole() { return role; } + public void setRole(String role) { this.role = role; } +} \ No newline at end of file diff --git a/examples/data_analyzer.py b/examples/data_analyzer.py new file mode 100644 index 0000000..f712968 --- /dev/null +++ b/examples/data_analyzer.py @@ -0,0 +1,123 @@ +# Python ์˜ˆ์‹œ: ๋ฐ์ดํ„ฐ ๋ถ„์„๊ธฐ (์„ฑ๋Šฅ ๋ฐ ๋ณด์•ˆ ์ด์Šˆ) +import os +import pickle +import hashlib +import subprocess +import pandas as pd +from typing import List, Dict, Any + +class DataAnalyzer: + def __init__(self, data_path: str): + self.data_path = data_path + self.cache = {} + self.api_key = "sk-1234567890abcdef" # ๋ณด์•ˆ ์ด์Šˆ: ํ•˜๋“œ์ฝ”๋”ฉ๋œ API ํ‚ค + + # ๋ณด์•ˆ ์ด์Šˆ: ๊ฒฝ๋กœ ์ˆœํšŒ ๊ณต๊ฒฉ ๊ฐ€๋Šฅ + def load_data(self, filename: str) -> pd.DataFrame: + # ์ž…๋ ฅ ๊ฒ€์ฆ ์—†์Œ + full_path = os.path.join(self.data_path, filename) + print(f"Loading data from: {full_path}") # ๊ฒฝ๋กœ ์ •๋ณด ๋…ธ์ถœ + + try: + # ๋ณด์•ˆ ์ด์Šˆ: pickle.load ์‚ฌ์šฉ (์ฝ”๋“œ ์‹คํ–‰ ๊ฐ€๋Šฅ) + if filename.endswith('.pkl'): + with open(full_path, 'rb') as f: + return pickle.load(f) # ๋งค์šฐ ์œ„ํ—˜! + else: + return pd.read_csv(full_path) + except Exception as e: + print(f"Error loading data: {str(e)}") # ์—๋Ÿฌ ์ •๋ณด ๋…ธ์ถœ + return pd.DataFrame() + + # ์„ฑ๋Šฅ ์ด์Šˆ: ๋น„ํšจ์œจ์ ์ธ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ + def analyze_data(self, data: pd.DataFrame) -> Dict[str, Any]: + results = {} + + # ์„ฑ๋Šฅ ์ด์Šˆ: ๋ฐ˜๋ณต๋ฌธ์œผ๋กœ ํ†ต๊ณ„ ๊ณ„์‚ฐ + for column in data.columns: + column_stats = [] + for value in data[column]: + # ๋งค์šฐ ๋น„ํšจ์œจ์ ์ธ ์ฒ˜๋ฆฌ + if str(value).isdigit(): + column_stats.append(float(value)) + + if column_stats: + results[column] = { + 'mean': sum(column_stats) / len(column_stats), + 'max': max(column_stats), + 'min': min(column_stats) + } + + return results + + # ๋ณด์•ˆ ์ด์Šˆ: ๋ช…๋ น์–ด ์ธ์ ์…˜ ๊ฐ€๋Šฅ + def execute_query(self, query: str) -> str: + # ์ž…๋ ฅ ๊ฒ€์ฆ ์—†์ด ์‹œ์Šคํ…œ ๋ช…๋ น์–ด ์‹คํ–‰ + command = f"mysql -e '{query}'" + result = subprocess.run(command, shell=True, capture_output=True, text=True) + return result.stdout + + # ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜: ์บ์‹œ ์ •๋ฆฌ ์—†์Œ + def cache_result(self, key: str, data: Any) -> None: + # ์บ์‹œ ํฌ๊ธฐ ์ œํ•œ ์—†์Œ + self.cache[key] = data + print(f"Cached {len(str(data))} bytes for key: {key}") + + # ๋ณด์•ˆ ์ด์Šˆ: ์•ฝํ•œ ํ•ด์‹œ ํ•จ์ˆ˜ ์‚ฌ์šฉ + def generate_hash(self, data: str) -> str: + # MD5๋Š” ๋ณด์•ˆ์ƒ ์ทจ์•ฝํ•จ + return hashlib.md5(data.encode()).hexdigest() + + # ์„ฑ๋Šฅ ์ด์Šˆ: ์ค‘์ฒฉ ๋ฐ˜๋ณต๋ฌธ + def find_duplicates(self, data_list: List[Dict]) -> List[Dict]: + duplicates = [] + + # O(nยฒ) ๋ณต์žก๋„ + for i in range(len(data_list)): + for j in range(i + 1, len(data_list)): + if data_list[i] == data_list[j]: + duplicates.append(data_list[i]) + + return duplicates + + # ์ฝ”๋“œ ์Šคํƒ€์ผ ์ด์Šˆ: ๋„ค์ด๋ฐ ์ปจ๋ฒค์…˜ ์œ„๋ฐ˜ + def ProcessLargeDataset(self, DataSet: List) -> None: + # ๋ณ€์ˆ˜๋ช…์ด ์ผ๊ด€์„ฑ ์—†์Œ + processedData = [] + Total_Count = 0 + + for item in DataSet: + # ๋“ค์—ฌ์“ฐ๊ธฐ ์ผ๊ด€์„ฑ ์—†์Œ + processed_item = self.process_item(item) + processedData.append(processed_item) + Total_Count += 1 + + print(f"Processed {Total_Count} items") + + def process_item(self, item): + # ํƒ€์ž… ํžŒํŠธ ์—†์Œ + return item + + # ๋ณด์•ˆ ์ด์Šˆ: ๋ฏผ๊ฐํ•œ ์ •๋ณด ๋กœ๊น… + def authenticate_user(self, username: str, password: str) -> bool: + print(f"Authenticating user: {username} with password: {password}") # ๋น„๋ฐ€๋ฒˆํ˜ธ ๋กœ๊น…! + + # ๋ณด์•ˆ ์ด์Šˆ: ์•ฝํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ •์ฑ… + if len(password) >= 4: + return True + return False + + # ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜: ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์‹œ ๋ฌธ์ œ + def load_all_data(self) -> List[pd.DataFrame]: + all_data = [] + + # ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๊ณ ๋ ค ์—†์Œ + for filename in os.listdir(self.data_path): + if filename.endswith(('.csv', '.pkl')): + data = self.load_data(filename) + all_data.append(data) # ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— ๋ณด๊ด€ + + return all_data + +# ๋ชจ๋“ˆ ๋ ˆ๋ฒจ์—์„œ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ (์ข‹์ง€ ์•Š์€ ํŒจํ„ด) +analyzer = DataAnalyzer("/tmp/data") \ No newline at end of file diff --git a/examples/file_processor.go b/examples/file_processor.go new file mode 100644 index 0000000..b06cc4f --- /dev/null +++ b/examples/file_processor.go @@ -0,0 +1,212 @@ +// Go ์˜ˆ์‹œ: ํŒŒ์ผ ์ฒ˜๋ฆฌ๊ธฐ (๋ณด์•ˆ ๋ฐ ์„ฑ๋Šฅ ์ด์Šˆ) +package main + +import ( + "bufio" + "crypto/md5" // ๋ณด์•ˆ ์ด์Šˆ: ์•ฝํ•œ ํ•ด์‹œ ํ•จ์ˆ˜ + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +// ๋ณด์•ˆ ์ด์Šˆ: ์ „์—ญ ๋ณ€์ˆ˜์— ๋ฏผ๊ฐํ•œ ์ •๋ณด +var ( + API_KEY = "secret-api-key-12345" + SECRET_KEY = "my-secret-key" +) + +type FileProcessor struct { + basePath string + cache map[string][]byte // ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๊ฐ€๋Šฅ์„ฑ + processedFiles int +} + +// ์ƒ์„ฑ์ž์—์„œ ๊ฒ€์ฆ ๋ถ€์กฑ +func NewFileProcessor(basePath string) *FileProcessor { + return &FileProcessor{ + basePath: basePath, // ๊ฒฝ๋กœ ๊ฒ€์ฆ ์—†์Œ + cache: make(map[string][]byte), + } +} + +// ๋ณด์•ˆ ์ด์Šˆ: ๊ฒฝ๋กœ ์ˆœํšŒ ๊ณต๊ฒฉ ๊ฐ€๋Šฅ +func (fp *FileProcessor) ReadFile(filename string) ([]byte, error) { + // ์ž…๋ ฅ ๊ฒ€์ฆ ์—†์Œ + fullPath := filepath.Join(fp.basePath, filename) + + // ๋ณด์•ˆ ์ด์Šˆ: ๊ฒฝ๋กœ ์ •๋ณด ๋กœ๊น… + log.Printf("Reading file: %s", fullPath) + + // ์„ฑ๋Šฅ ์ด์Šˆ: ํŒŒ์ผ์„ ํ†ต์งธ๋กœ ๋ฉ”๋ชจ๋ฆฌ์— ๋กœ๋“œ + data, err := ioutil.ReadFile(fullPath) + if err != nil { + // ๋ณด์•ˆ ์ด์Šˆ: ์—๋Ÿฌ ์ •๋ณด ๋…ธ์ถœ + log.Printf("File read error: %v", err) + return nil, err + } + + // ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜: ์บ์‹œ ํฌ๊ธฐ ์ œํ•œ ์—†์Œ + fp.cache[filename] = data + + return data, nil +} + +// ๋ณด์•ˆ ์ด์Šˆ: ๋ช…๋ น์–ด ์ธ์ ์…˜ ๊ฐ€๋Šฅ +func (fp *FileProcessor) ExecuteCommand(command string) (string, error) { + // ์ž…๋ ฅ ๊ฒ€์ฆ ์—†์ด ์‹œ์Šคํ…œ ๋ช…๋ น์–ด ์‹คํ–‰ + log.Printf("Executing command: %s", command) // ๋ช…๋ น์–ด ๋กœ๊น… + + cmd := exec.Command("sh", "-c", command) // ๋งค์šฐ ์œ„ํ—˜! + output, err := cmd.Output() + + if err != nil { + log.Printf("Command execution failed: %v", err) + return "", err + } + + return string(output), nil +} + +// ์„ฑ๋Šฅ ์ด์Šˆ: ๋น„ํšจ์œจ์ ์ธ ํŒŒ์ผ ์ฒ˜๋ฆฌ +func (fp *FileProcessor) ProcessFiles(pattern string) error { + files, err := filepath.Glob(filepath.Join(fp.basePath, pattern)) + if err != nil { + return err + } + + // ์„ฑ๋Šฅ ์ด์Šˆ: ๋ชจ๋“  ํŒŒ์ผ์„ ์ˆœ์ฐจ ์ฒ˜๋ฆฌ (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ ์—†์Œ) + for _, file := range files { + // ์„ฑ๋Šฅ ์ด์Šˆ: ๋งค๋ฒˆ ํŒŒ์ผ ํฌ๊ธฐ ํ™•์ธ + info, err := os.Stat(file) + if err != nil { + continue + } + + // ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ๋„ ๋™์ผํ•˜๊ฒŒ ์ฒ˜๋ฆฌ + if info.Size() > 0 { + data, err := fp.ReadFile(filepath.Base(file)) + if err != nil { + continue + } + + // ์„ฑ๋Šฅ ์ด์Šˆ: ๋ถˆํ•„์š”ํ•œ ๋ฌธ์ž์—ด ๋ณ€ํ™˜ + content := string(data) + + // ๋ณด์•ˆ ์ด์Šˆ: ์•ฝํ•œ ํ•ด์‹œ ํ•จ์ˆ˜ ์‚ฌ์šฉ + hash := md5.Sum(data) + log.Printf("Processed file: %s, hash: %x", file, hash) + + // ์„ฑ๋Šฅ ์ด์Šˆ: ๋น„ํšจ์œจ์ ์ธ ๋ฌธ์ž์—ด ๊ฒ€์ƒ‰ + for i := 0; i < len(content); i++ { + if strings.HasPrefix(content[i:], "password") { + log.Printf("Found password reference in %s at position %d", file, i) + } + } + } + + fp.processedFiles++ + } + + return nil +} + +// ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜: ๋ฌดํ•œ ์„ฑ์žฅํ•˜๋Š” ์บ์‹œ +func (fp *FileProcessor) GetFromCache(filename string) []byte { + // ์บ์‹œ ๋งŒ๋ฃŒ ๋กœ์ง ์—†์Œ + return fp.cache[filename] +} + +// ๋ณด์•ˆ ์ด์Šˆ: ๋ฏผ๊ฐํ•œ ์ •๋ณด ํ‰๋ฌธ ์ €์žฅ +func (fp *FileProcessor) SaveConfig(config map[string]string) error { + file, err := os.Create("config.txt") + if err != nil { + return err + } + defer file.Close() + + writer := bufio.NewWriter(file) + + for key, value := range config { + // ๋น„๋ฐ€๋ฒˆํ˜ธ๋„ ํ‰๋ฌธ์œผ๋กœ ์ €์žฅ + line := fmt.Sprintf("%s=%s\n", key, value) + writer.WriteString(line) + } + + writer.Flush() + + // ๋ณด์•ˆ ์ด์Šˆ: ํŒŒ์ผ ๊ถŒํ•œ ์„ค์ • ์—†์Œ (๊ธฐ๋ณธ์ ์œผ๋กœ ๋ชจ๋“  ์‚ฌ์šฉ์ž๊ฐ€ ์ฝ๊ธฐ ๊ฐ€๋Šฅ) + log.Println("Configuration saved to config.txt") + + return nil +} + +// ์„ฑ๋Šฅ ์ด์Šˆ: ๊ณ ๋ฃจํ‹ด ๋ˆ„์ˆ˜ ๊ฐ€๋Šฅ์„ฑ +func (fp *FileProcessor) StartBackgroundProcessor() { + // ๊ณ ๋ฃจํ‹ด ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ ์—†์Œ + go func() { + for { + // ๋ฌดํ•œ ๋ฃจํ”„์—์„œ ์ง€์†์ ์œผ๋กœ ์ž‘์—… + time.Sleep(1 * time.Second) + + // ์„ฑ๋Šฅ ์ด์Šˆ: ๋งค์ดˆ๋งˆ๋‹ค ๋ชจ๋“  ํŒŒ์ผ ์Šค์บ” + files, _ := filepath.Glob(fp.basePath + "/*") + for _, file := range files { + // ๋ถˆํ•„์š”ํ•œ ํŒŒ์ผ ์ ‘๊ทผ + os.Stat(file) + } + + log.Printf("Background scan completed, processed files: %d", fp.processedFiles) + } + }() +} + +// ์ฝ”๋“œ ์Šคํƒ€์ผ ์ด์Šˆ: ๋„ค์ด๋ฐ ์ปจ๋ฒค์…˜ ์œ„๋ฐ˜ +func (fp *FileProcessor) Process_Large_File(FileName string) error { + // ๋ณ€์ˆ˜๋ช… ์ผ๊ด€์„ฑ ์—†์Œ + file_path := filepath.Join(fp.basePath, FileName) + + // ๋“ค์—ฌ์“ฐ๊ธฐ ์ผ๊ด€์„ฑ ์—†์Œ + data, err := ioutil.ReadFile(file_path) + if err != nil { + return err + } + + // ์„ฑ๋Šฅ ์ด์Šˆ: ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ์„ ํ•œ๋ฒˆ์— ์ฒ˜๋ฆฌ + lines := strings.Split(string(data), "\n") + + // ์„ฑ๋Šฅ ์ด์Šˆ: ๋น„ํšจ์œจ์ ์ธ ๋ฌธ์ž์—ด ์—ฐ์‚ฐ + var result string + for _, line := range lines { + result += line + "\n" // ๋งค๋ฒˆ ์ƒˆ ๋ฌธ์ž์—ด ์ƒ์„ฑ + } + + log.Printf("Processed %d lines", len(lines)) + return nil +} + +// ๋ณด์•ˆ ์ด์Šˆ: ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ์ง์ ‘ ์‹œ์Šคํ…œ ๋ช…๋ น์–ด๋กœ ์‹คํ–‰ +func (fp *FileProcessor) CustomCommand(userInput string) { + // ์ž…๋ ฅ ๊ฒ€์ฆ ์ „ํ˜€ ์—†์Œ + command := fmt.Sprintf("echo %s > output.txt", userInput) + + // ๋ช…๋ น์–ด ์ธ์ ์…˜ ์ทจ์•ฝ์  + exec.Command("sh", "-c", command).Run() +} + +func main() { + processor := NewFileProcessor("/tmp") + + // ๋ณด์•ˆ ์ด์Šˆ: ํ•˜๋“œ์ฝ”๋”ฉ๋œ ํŒจํ„ด + processor.ProcessFiles("*.txt") + + // ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํ”„๋กœ์„ธ์„œ ์‹œ์ž‘ (๊ณ ๋ฃจํ‹ด ๋ˆ„์ˆ˜ ์œ„ํ—˜) + processor.StartBackgroundProcessor() + + // ๋ฉ”์ธ ๊ณ ๋ฃจํ‹ด ์ข…๋ฃŒ๋˜์ง€ ์•Š์Œ + select {} +} \ No newline at end of file diff --git a/example/images/pr_example.png b/examples/images/pr_example.png similarity index 100% rename from example/images/pr_example.png rename to examples/images/pr_example.png diff --git a/examples/payment-processor.ts b/examples/payment-processor.ts new file mode 100644 index 0000000..e57f69a --- /dev/null +++ b/examples/payment-processor.ts @@ -0,0 +1,100 @@ +// TypeScript ์˜ˆ์‹œ: ๊ฒฐ์ œ ์ฒ˜๋ฆฌ๊ธฐ (๋ณด์•ˆ ๋ฐ ์„ฑ๋Šฅ ์ด์Šˆ) +import * as crypto from 'crypto'; + +interface PaymentRequest { + amount: number; + currency: string; + cardNumber: string; + cvv: string; + expiryDate: string; +} + +interface PaymentResponse { + success: boolean; + transactionId?: string; + error?: string; +} + +class PaymentProcessor { + private apiKey: string = process.env.PAYMENT_API_KEY || "default-key"; // ๋ณด์•ˆ ์ด์Šˆ: ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • + private transactions: PaymentRequest[] = []; // ๋ฉ”๋ชจ๋ฆฌ์— ๋ฏผ๊ฐํ•œ ์ •๋ณด ์ €์žฅ + + // ๋ณด์•ˆ ์ด์Šˆ: ์‹ ์šฉ์นด๋“œ ์ •๋ณด ๋กœ๊น… + async processPayment(request: PaymentRequest): Promise { + console.log("Processing payment:", JSON.stringify(request)); // ๋ฏผ๊ฐํ•œ ์ •๋ณด ๋กœ๊น… + + // ์ž…๋ ฅ ๊ฒ€์ฆ ๋ถ€์กฑ + if (!request.amount || request.amount <= 0) { + throw new Error("Invalid amount"); + } + + // ๋ณด์•ˆ ์ด์Šˆ: ์‹ ์šฉ์นด๋“œ ๋ฒˆํ˜ธ ๊ฒ€์ฆ ์—†์Œ + if (request.cardNumber.length < 13) { + return { success: false, error: "Invalid card number" }; + } + + // ์„ฑ๋Šฅ ์ด์Šˆ: ๋™๊ธฐ ์ฒ˜๋ฆฌ + const isValid = this.validateCard(request); + if (!isValid) { + return { success: false, error: "Card validation failed" }; + } + + // ๋ณด์•ˆ ์ด์Šˆ: ์•ฝํ•œ ํŠธ๋žœ์žญ์…˜ ID ์ƒ์„ฑ + const transactionId = Math.random().toString(36).substr(2, 9); + + // ๋ฉ”๋ชจ๋ฆฌ์— ๋ฏผ๊ฐํ•œ ์ •๋ณด ์ €์žฅ + this.transactions.push(request); + + return { + success: true, + transactionId: transactionId + }; + } + + // ์„ฑ๋Šฅ ์ด์Šˆ: ๋น„ํšจ์œจ์ ์ธ ๊ฒ€์ƒ‰ + private validateCard(request: PaymentRequest): boolean { + // ์‹ค์ œ ์นด๋“œ ๊ฒ€์ฆ ๋กœ์ง ์—†์Œ + for (let i = 0; i < 10000; i++) { + // ๋ถˆํ•„์š”ํ•œ ๋ฐ˜๋ณต + if (request.cardNumber.includes("0000")) { + return false; + } + } + + // ๋ณด์•ˆ ์ด์Šˆ: CVV ๊ฒ€์ฆ ์—†์Œ + return true; + } + + // ๋ณด์•ˆ ์ด์Šˆ: ๊ถŒํ•œ ๊ฒ€์ฆ ์—†์ด ๋ชจ๋“  ๊ฑฐ๋ž˜ ๋ฐ˜ํ™˜ + public getAllTransactions(): PaymentRequest[] { + return this.transactions; + } + + // ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜: ๊ฑฐ๋ž˜ ๋‚ด์—ญ ์ •๋ฆฌ ์—†์Œ + public getTransactionHistory(limit?: number): PaymentRequest[] { + const history = [...this.transactions]; + + // ์„ฑ๋Šฅ ์ด์Šˆ: ๋งค๋ฒˆ ์ƒˆ ๋ฐฐ์—ด ์ƒ์„ฑ + if (limit) { + return history.slice(-limit); + } + + return history; + } + + // ๋ณด์•ˆ ์ด์Šˆ: ํ‰๋ฌธ์œผ๋กœ ์นด๋“œ ์ •๋ณด ์ €์žฅ + private storeCardInfo(cardNumber: string, cvv: string): void { + const cardInfo = { + number: cardNumber, // ํ‰๋ฌธ ์ €์žฅ + cvv: cvv, // ํ‰๋ฌธ ์ €์žฅ + timestamp: Date.now() + }; + + // ํŒŒ์ผ์— ํ‰๋ฌธ์œผ๋กœ ์ €์žฅ (๋งค์šฐ ์œ„ํ—˜) + require('fs').appendFileSync('cards.txt', JSON.stringify(cardInfo) + '\n'); + } +} + +// ์ฝ”๋“œ ์Šคํƒ€์ผ ์ด์Šˆ: export ๋ฐฉ์‹ ์ผ๊ด€์„ฑ ์—†์Œ +export default PaymentProcessor; +export { PaymentRequest, PaymentResponse }; \ No newline at end of file diff --git a/examples/security_manager.rs b/examples/security_manager.rs new file mode 100644 index 0000000..bf84863 --- /dev/null +++ b/examples/security_manager.rs @@ -0,0 +1,216 @@ +// Rust ์˜ˆ์‹œ: ๋ณด์•ˆ ๋งค๋‹ˆ์ € (์•ˆ์ „ํ•˜์ง€ ์•Š์€ ์ฝ”๋“œ ์‚ฌ์šฉ ๋ฐ ๋ณด์•ˆ ์ด์Šˆ) +use std::collections::HashMap; +use std::fs::File; +use std::io::{Read, Write}; +use std::process::Command; +use std::ffi::CString; +use std::ptr; + +// ๋ณด์•ˆ ์ด์Šˆ: ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์‹œํฌ๋ฆฟ +const SECRET_KEY: &str = "hardcoded-secret-key-123"; +const ADMIN_PASSWORD: &str = "admin123"; + +pub struct SecurityManager { + users: HashMap, + sessions: HashMap, + // ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๊ฐ€๋Šฅ์„ฑ: Vec์ด ๋ฌด์ œํ•œ ์„ฑ์žฅ + audit_log: Vec, +} + +impl SecurityManager { + pub fn new() -> Self { + Self { + users: HashMap::new(), + sessions: HashMap::new(), + audit_log: Vec::new(), + } + } + + // ๋ณด์•ˆ ์ด์Šˆ: ํ‰๋ฌธ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ €์žฅ + pub fn create_user(&mut self, username: String, password: String) -> bool { + // ์ž…๋ ฅ ๊ฒ€์ฆ ์—†์Œ + if username.is_empty() || password.is_empty() { + return false; + } + + // ๋ณด์•ˆ ์ด์Šˆ: ๋น„๋ฐ€๋ฒˆํ˜ธ ํ‰๋ฌธ ์ €์žฅ + self.users.insert(username.clone(), password); + + // ๋ณด์•ˆ ์ด์Šˆ: ๋ฏผ๊ฐํ•œ ์ •๋ณด ๋กœ๊น… + let log_entry = format!("User created: {} with password: {}", username, password); + self.audit_log.push(log_entry); + + true + } + + // ๋ณด์•ˆ ์ด์Šˆ: ์•ฝํ•œ ์„ธ์…˜ ID ์ƒ์„ฑ + pub fn authenticate(&mut self, username: &str, password: &str) -> Option { + if let Some(stored_password) = self.users.get(username) { + // ๋ณด์•ˆ ์ด์Šˆ: ํ‰๋ฌธ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋น„๊ต + if stored_password == password { + // ๋ณด์•ˆ ์ด์Šˆ: ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ์„ธ์…˜ ID + let session_id = format!("session_{}", self.sessions.len()); + self.sessions.insert(session_id.clone(), 1); + + // ๋ณด์•ˆ ์ด์Šˆ: ์ธ์ฆ ์ •๋ณด ๋กœ๊น… + println!("Authentication successful for: {}", username); + + return Some(session_id); + } + } + + // ๋ณด์•ˆ ์ด์Šˆ: ์‚ฌ์šฉ์ž ์กด์žฌ ์—ฌ๋ถ€ ์œ ์ถ” ๊ฐ€๋Šฅํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + println!("Authentication failed: Invalid username or password"); + None + } + + // ์•ˆ์ „ํ•˜์ง€ ์•Š์€ ์ฝ”๋“œ: ๋ฉ”๋ชจ๋ฆฌ ์•ˆ์ „์„ฑ ์œ„๋ฐ˜ ๊ฐ€๋Šฅ + pub unsafe fn process_raw_data(&self, data: *const u8, len: usize) -> Vec { + let mut result = Vec::new(); + + // ๊ฒฝ๊ณ„ ๊ฒ€์‚ฌ ์—†์Œ - ๋ฒ„ํผ ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ ๊ฐ€๋Šฅ + for i in 0..len { + let byte_val = *data.offset(i as isize); + result.push(byte_val); + } + + result + } + + // ๋ณด์•ˆ ์ด์Šˆ: ๋ช…๋ น์–ด ์ธ์ ์…˜ ์ทจ์•ฝ์  + pub fn execute_system_command(&self, user_input: &str) -> Result { + // ์ž…๋ ฅ ๊ฒ€์ฆ ์—†์Œ + let command = format!("echo {}", user_input); + + // ๋ช…๋ น์–ด ์ธ์ ์…˜ ๊ฐ€๋Šฅ + let output = Command::new("sh") + .arg("-c") + .arg(&command) + .output()?; + + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } + + // ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜: ๋ฌดํ•œ ์„ฑ์žฅํ•˜๋Š” ๋กœ๊ทธ + pub fn add_audit_log(&mut self, message: String) { + // ๋กœ๊ทธ ํฌ๊ธฐ ์ œํ•œ ์—†์Œ + self.audit_log.push(message); + + // ์„ฑ๋Šฅ ์ด์Šˆ: ๋งค๋ฒˆ ์ „์ฒด ๋กœ๊ทธ ์ถœ๋ ฅ + for (i, log) in self.audit_log.iter().enumerate() { + println!("Log {}: {}", i, log); + } + } + + // ๋ณด์•ˆ ์ด์Šˆ: ๋ฏผ๊ฐํ•œ ์ •๋ณด๋ฅผ ํ‰๋ฌธ ํŒŒ์ผ๋กœ ์ €์žฅ + pub fn export_user_data(&self, filename: &str) -> Result<(), std::io::Error> { + let mut file = File::create(filename)?; + + // ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋ฅผ ํ‰๋ฌธ์œผ๋กœ ์ €์žฅ + for (username, password) in &self.users { + let line = format!("{}:{}\n", username, password); + file.write_all(line.as_bytes())?; + } + + // ๋ณด์•ˆ ์ด์Šˆ: ํŒŒ์ผ ๊ฒฝ๋กœ ๋กœ๊น… + println!("User data exported to: {}", filename); + + Ok(()) + } + + // ์•ˆ์ „ํ•˜์ง€ ์•Š์€ ์ฝ”๋“œ: C ์Šคํƒ€์ผ ํฌ์ธํ„ฐ ์‚ฌ์šฉ + pub unsafe fn manipulate_memory(&self, ptr: *mut u8, size: usize) { + // ๊ฒฝ๊ณ„ ๊ฒ€์‚ฌ ์—†์Œ + for i in 0..size * 2 { // ์˜๋„์ ์œผ๋กœ ์ž˜๋ชป๋œ ํฌ๊ธฐ + *ptr.offset(i as isize) = 0xFF; // ๋ฒ„ํผ ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ ๊ฐ€๋Šฅ + } + } + + // ์„ฑ๋Šฅ ์ด์Šˆ: ๋น„ํšจ์œจ์ ์ธ ๊ฒ€์ƒ‰ + pub fn find_user_by_partial_name(&self, partial: &str) -> Vec { + let mut results = Vec::new(); + + // O(n) ๊ฒ€์ƒ‰์„ ์—ฌ๋Ÿฌ ๋ฒˆ ์ˆ˜ํ–‰ + for username in self.users.keys() { + // ์„ฑ๋Šฅ ์ด์Šˆ: ๋น„ํšจ์œจ์ ์ธ ๋ฌธ์ž์—ด ๋น„๊ต + for i in 0..username.len() { + if username[i..].starts_with(partial) { + results.push(username.clone()); + break; + } + } + } + + results + } + + // ๋ณด์•ˆ ์ด์Šˆ: ๊ถŒํ•œ ๊ฒ€์ฆ ์—†๋Š” ๊ด€๋ฆฌ์ž ๊ธฐ๋Šฅ + pub fn admin_reset_all_passwords(&mut self) { + // ๊ถŒํ•œ ํ™•์ธ ์—†์Œ + for (username, password) in &mut self.users { + *password = "temp123".to_string(); // ๋ชจ๋“  ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•ฝํ•œ ๊ฒƒ์œผ๋กœ ๋ณ€๊ฒฝ + + // ๋ณด์•ˆ ์ด์Šˆ: ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ๋กœ๊น… + println!("Reset password for user: {}", username); + } + } + + // ์•ˆ์ „ํ•˜์ง€ ์•Š์€ ์ฝ”๋“œ: ์›์‹œ ํฌ์ธํ„ฐ๋กœ ๋ฌธ์ž์—ด ์ƒ์„ฑ + pub unsafe fn create_c_string(&self, data: &[u8]) -> *const i8 { + let c_string = CString::new(data).unwrap(); + let ptr = c_string.as_ptr(); + + // ๋ฉ”๋ชจ๋ฆฌ ํ•ด์ œ ์—†์ด ํฌ์ธํ„ฐ ๋ฐ˜ํ™˜ (use-after-free ๊ฐ€๋Šฅ์„ฑ) + std::mem::forget(c_string); + + ptr + } +} + +// ์ „์—ญ ๋ณ€์ˆ˜ (thread-safety ๋ฌธ์ œ) +static mut GLOBAL_MANAGER: Option = None; + +// ๋ณด์•ˆ ์ด์Šˆ: ์Šค๋ ˆ๋“œ ์•ˆ์ „์„ฑ ์—†๋Š” ์ „์—ญ ์ ‘๊ทผ +pub unsafe fn get_global_manager() -> &'static mut SecurityManager { + if GLOBAL_MANAGER.is_none() { + GLOBAL_MANAGER = Some(SecurityManager::new()); + } + + GLOBAL_MANAGER.as_mut().unwrap() +} + +// ์ฝ”๋“œ ์Šคํƒ€์ผ ์ด์Šˆ: ๋„ค์ด๋ฐ ์ปจ๋ฒค์…˜ ์œ„๋ฐ˜ +pub struct User_Session { + pub Session_ID: String, + pub User_Name: String, + pub Is_Admin: bool, +} + +impl User_Session { + // ๋ณด์•ˆ ์ด์Šˆ: ๊ด€๋ฆฌ์ž ๊ถŒํ•œ ์ฒดํฌ ๋กœ์ง ๊ฒฐํ•จ + pub fn Check_Admin_Access(&self) -> bool { + // ๋‹จ์ˆœํ•œ ๋ฌธ์ž์—ด ๋น„๊ต๋กœ ๊ด€๋ฆฌ์ž ํ™•์ธ + self.User_Name == "admin" || self.Session_ID.contains("admin") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_unsafe_operations() { + let mut manager = SecurityManager::new(); + + unsafe { + // ์•ˆ์ „ํ•˜์ง€ ์•Š์€ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ + let data = vec![1, 2, 3, 4, 5]; + let processed = manager.process_raw_data(data.as_ptr(), data.len()); + assert_eq!(processed, data); + + // ๋ฉ”๋ชจ๋ฆฌ ์˜ค๋ฅ˜ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋Š” ํ…Œ์ŠคํŠธ + let mut buffer = vec![0u8; 10]; + manager.manipulate_memory(buffer.as_mut_ptr(), 5); // ์˜๋„์ ์œผ๋กœ ์ž‘์€ ํฌ๊ธฐ + } + } +} \ No newline at end of file diff --git a/examples/user-service.js b/examples/user-service.js new file mode 100644 index 0000000..27a6309 --- /dev/null +++ b/examples/user-service.js @@ -0,0 +1,91 @@ +// JavaScript ์˜ˆ์‹œ: ์‚ฌ์šฉ์ž ์„œ๋น„์Šค (๋‹ค์–‘ํ•œ ์ด์Šˆ ํฌํ•จ) +const crypto = require('crypto'); +const jwt = require('jsonwebtoken'); + +class UserService { + constructor() { + this.users = new Map(); + this.SECRET_KEY = "hardcoded-secret-123"; // ๋ณด์•ˆ ์ด์Šˆ: ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์‹œํฌ๋ฆฟ + } + + // ์„ฑ๋Šฅ ์ด์Šˆ: ๋น„ํšจ์œจ์ ์ธ ๊ฒ€์ƒ‰ + findUserByEmail(email) { + for (let [id, user] of this.users) { + if (user.email === email) { + return user; + } + } + return null; + } + + // ๋ณด์•ˆ ์ด์Šˆ: SQL Injection ๊ฐ€๋Šฅ์„ฑ + authenticateUser(email, password) { + const query = `SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`; + console.log("Executing query:", query); // ๋ณด์•ˆ ์ด์Šˆ: ๋น„๋ฐ€๋ฒˆํ˜ธ ๋กœ๊น… + + const user = this.findUserByEmail(email); + if (!user) return null; + + // ๋ณด์•ˆ ์ด์Šˆ: ํ‰๋ฌธ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋น„๊ต + if (user.password === password) { + return this.generateToken(user); + } + return null; + } + + // ์ฝ”๋“œ ์Šคํƒ€์ผ ์ด์Šˆ: ๋„ค์ด๋ฐ ์ปจ๋ฒค์…˜ ์œ„๋ฐ˜ + CreateNewUser(Email, Password, UserName) { + // ์ž…๋ ฅ ๊ฒ€์ฆ ์—†์Œ (๋ณด์•ˆ ์ด์Šˆ) + const userId = Math.random().toString(36); // ์•ฝํ•œ ID ์ƒ์„ฑ + + const newUser = { + id: userId, + email: Email, + password: Password, // ํ‰๋ฌธ ์ €์žฅ (๋ณด์•ˆ ์ด์Šˆ) + username: UserName, + createdAt: new Date() + }; + + this.users.set(userId, newUser); + return newUser; + } + + generateToken(user) { + // ๋ณด์•ˆ ์ด์Šˆ: ํ† ํฐ ๋งŒ๋ฃŒ์‹œ๊ฐ„ ์—†์Œ + return jwt.sign( + { + userId: user.id, + email: user.email, + password: user.password // ๋ณด์•ˆ ์ด์Šˆ: ํ† ํฐ์— ๋น„๋ฐ€๋ฒˆํ˜ธ ํฌํ•จ + }, + this.SECRET_KEY + ); + } + + // ์„ฑ๋Šฅ ์ด์Šˆ: ๋™๊ธฐ ์ฒ˜๋ฆฌ + validateToken(token) { + try { + const decoded = jwt.verify(token, this.SECRET_KEY); + return decoded; + } catch (error) { + console.log("Token validation error:", error.message); + return null; + } + } + + // ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๊ฐ€๋Šฅ์„ฑ + getUserSessions(userId) { + const sessions = []; + // ์„ธ์…˜ ์ •๋ฆฌ ๋กœ์ง ์—†์Œ + for (let i = 0; i < 1000; i++) { + sessions.push({ + id: i, + userId: userId, + data: new Array(1000).fill("session-data") + }); + } + return sessions; + } +} + +module.exports = UserService; \ No newline at end of file diff --git a/test-files/api-endpoints.js b/test-files/api-endpoints.js deleted file mode 100644 index 180fee7..0000000 --- a/test-files/api-endpoints.js +++ /dev/null @@ -1,119 +0,0 @@ -// Express API ์—”๋“œํฌ์ธํŠธ - ๋‹ค์–‘ํ•œ ๋ฌธ์ œ์ ๋“ค - -const express = require('express'); -const app = express(); - -// CORS ์„ค์ • ๋ฌธ์ œ -app.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', '*'); // ๋ชจ๋“  ๋„๋ฉ”์ธ ํ—ˆ์šฉ - ๋ณด์•ˆ ์œ„ํ—˜ - res.header('Access-Control-Allow-Headers', '*'); - next(); -}); - -// Rate limiting ์—†์Œ - DoS ๊ณต๊ฒฉ ์œ„ํ—˜ -app.post('/api/login', async (req, res) => { - const { username, password } = req.body; - - // ์ž…๋ ฅ ๊ฒ€์ฆ ์—†์Œ - if (!username || !password) { - return res.status(400).json({ error: 'Missing credentials' }); - } - - // ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜ - try { - const user = authenticateUser(username, password); // await ๋ˆ„๋ฝ - res.json({ success: true, user }); - } catch (error) { - console.log(error); // ์—๋Ÿฌ ์ •๋ณด ๋…ธ์ถœ - res.status(500).json({ error: error.message }); // ๋‚ด๋ถ€ ์—๋Ÿฌ ๋…ธ์ถœ - } -}); - -// Path traversal ์ทจ์•ฝ์  -app.get('/api/files/:filename', (req, res) => { - const filename = req.params.filename; - const filePath = `./uploads/${filename}`; // ๊ฒฝ๋กœ ๊ฒ€์ฆ ์—†์Œ - - // ํŒŒ์ผ ์กด์žฌ ์—ฌ๋ถ€๋งŒ ํ™•์ธ - if (fs.existsSync(filePath)) { - res.sendFile(filePath); // ์ ˆ๋Œ€ ๊ฒฝ๋กœ๊ฐ€ ์•„๋‹˜ - } else { - res.status(404).send('File not found'); - } -}); - -// SQL ์ธ์ ์…˜ ์œ„ํ—˜ -app.get('/api/users/:id', (req, res) => { - const userId = req.params.id; - - // ๋งค๊ฐœ๋ณ€์ˆ˜ํ™”๋œ ์ฟผ๋ฆฌ ๋Œ€์‹  ๋ฌธ์ž์—ด ์—ฐ๊ฒฐ - const query = `SELECT * FROM users WHERE id = ${userId}`; - - db.query(query, (err, results) => { - if (err) { - console.error('Database error:', err); - return res.status(500).json({ error: 'Database error' }); - } - res.json(results); - }); -}); - -// ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ์œ„ํ—˜ -const cache = new Map(); -app.get('/api/cache/:key', (req, res) => { - const key = req.params.key; - const value = generateExpensiveData(key); - - cache.set(key, value); // ์บ์‹œ ํฌ๊ธฐ ์ œํ•œ ์—†์Œ - res.json({ data: value }); -}); - -// ์ธ์ฆ ์—†์ด ๋ฏผ๊ฐํ•œ ์ •๋ณด ๋…ธ์ถœ -app.get('/api/admin/logs', (req, res) => { - // ๊ถŒํ•œ ๊ฒ€์‚ฌ ์—†์Œ - const logs = fs.readFileSync('./app.log', 'utf8'); - res.json({ logs }); -}); - -// ๋น„ํšจ์œจ์ ์ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ -app.get('/api/dashboard', async (req, res) => { - const users = []; - - // N+1 ์ฟผ๋ฆฌ ๋ฌธ์ œ - const userIds = await db.query('SELECT id FROM users'); - for (const userId of userIds) { - const user = await db.query(`SELECT * FROM users WHERE id = ${userId.id}`); - const posts = await db.query(`SELECT * FROM posts WHERE user_id = ${userId.id}`); - users.push({ ...user, posts }); - } - - res.json(users); -}); - -// ํŒŒ์ผ ์—…๋กœ๋“œ ๊ฒ€์ฆ ๋ถ€์กฑ -app.post('/api/upload', upload.single('file'), (req, res) => { - const file = req.file; - - // ํŒŒ์ผ ํƒ€์ž…, ํฌ๊ธฐ ๊ฒ€์ฆ ์—†์Œ - if (!file) { - return res.status(400).json({ error: 'No file uploaded' }); - } - - // ์›๋ณธ ํŒŒ์ผ๋ช… ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ (๊ฒฝ๋กœ ์กฐ์ž‘ ์œ„ํ—˜) - const filename = file.originalname; - fs.writeFileSync(`./uploads/${filename}`, file.buffer); - - res.json({ message: 'File uploaded', filename }); -}); - -// ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์„ค์ •๊ฐ’๋“ค -const PORT = 3000; -const DB_PASSWORD = 'admin123'; // ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ -const JWT_SECRET = 'my-secret-key'; // ํ•˜๋“œ์ฝ”๋”ฉ๋œ JWT ์‹œํฌ๋ฆฟ - -app.listen(PORT, () => { - console.log(`Server running on port ${PORT}`); - console.log(`Database password: ${DB_PASSWORD}`); // ๋กœ๊ทธ์— ๋น„๋ฐ€๋ฒˆํ˜ธ ์ถœ๋ ฅ -}); - -module.exports = app; \ No newline at end of file diff --git a/test-files/buggy-calculator.js b/test-files/buggy-calculator.js deleted file mode 100644 index 65a0eed..0000000 --- a/test-files/buggy-calculator.js +++ /dev/null @@ -1,60 +0,0 @@ -// ์˜๋„์ ์œผ๋กœ ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š” ๊ณ„์‚ฐ๊ธฐ ์ฝ”๋“œ (ํ…Œ์ŠคํŠธ์šฉ) - -class Calculator { - constructor() { - this.history = []; - } - - // ๋ณด์•ˆ ๋ฌธ์ œ: ์ž…๋ ฅ ๊ฒ€์ฆ ์—†์Œ - add(a, b) { - let result = eval(a + '+' + b); // XSS ์ทจ์•ฝ์  ์œ„ํ—˜ - this.history.push(result); - return result; - } - - // ์„ฑ๋Šฅ ๋ฌธ์ œ: ๋น„ํšจ์œจ์ ์ธ ์•Œ๊ณ ๋ฆฌ์ฆ˜ - fibonacci(n) { - if (n <= 1) return n; - return this.fibonacci(n - 1) + this.fibonacci(n - 2); // O(2^n) ๋ณต์žก๋„ - } - - // ์Šคํƒ€์ผ ๋ฌธ์ œ: ์ผ๊ด€์„ฑ ์—†๋Š” ์ฝ”๋”ฉ ์Šคํƒ€์ผ - divide(x,y){ - if(y==0)return "Cannot divide by zero" // ์„ธ๋ฏธ์ฝœ๋ก  ๋ˆ„๋ฝ, ๊ณต๋ฐฑ ๋ถˆ์ผ์น˜ - var result=x/y // var ์‚ฌ์šฉ (let/const ๊ถŒ์žฅ) - return result - } - - // ๋ฒ„๊ทธ: ์ž˜๋ชป๋œ ๋กœ์ง - getAverage() { - if (this.history.length = 0) { // = ์‚ฌ์šฉ (== ๋˜๋Š” === ์˜๋„) - return 0; - } - - let sum = 0; - for (let i = 0; i <= this.history.length; i++) { // off-by-one ์—๋Ÿฌ - sum += this.history[i]; - } - - return sum / this.history.length; - } - - // ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ์œ„ํ—˜ - addToHistory(value) { - this.history.push(value); - // history ๋ฐฐ์—ด์ด ๋ฌดํ•œ์ • ์ปค์งˆ ์ˆ˜ ์žˆ์Œ - } - - // ํƒ€์ž… ์•ˆ์ „์„ฑ ๋ฌธ์ œ - multiply(a, b) { - return a * b; // ํƒ€์ž… ์ฒดํฌ ์—†์Œ - } -} - -// ์ „์—ญ ๋ณ€์ˆ˜ ์‚ฌ์šฉ (์•ˆํ‹ฐํŒจํ„ด) -var globalCalculator = new Calculator(); - -// ์ฝ˜์†”์— ๋ฏผ๊ฐํ•œ ์ •๋ณด ๋กœ๊น… -console.log("API Key: sk-1234567890abcdef"); // ๋ณด์•ˆ ์œ„ํ—˜ - -module.exports = Calculator; \ No newline at end of file diff --git a/test-files/user-service.js b/test-files/user-service.js deleted file mode 100644 index 77dd603..0000000 --- a/test-files/user-service.js +++ /dev/null @@ -1,109 +0,0 @@ -// ์‚ฌ์šฉ์ž ์„œ๋น„์Šค - ๋ณด์•ˆ ๋ฐ ์„ฑ๋Šฅ ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š” ์ฝ”๋“œ - -const bcrypt = require('bcrypt'); -const jwt = require('jsonwebtoken'); - -class UserService { - constructor() { - this.users = []; - this.secret = 'hardcoded-secret-key'; // ๋ณด์•ˆ ๋ฌธ์ œ: ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์‹œํฌ๋ฆฟ - } - - // SQL ์ธ์ ์…˜ ์ทจ์•ฝ์  - async getUserByEmail(email) { - const query = `SELECT * FROM users WHERE email = '${email}'`; // ์œ„ํ—˜ํ•œ ๋ฌธ์ž์—ด ์—ฐ๊ฒฐ - // ์‹ค์ œ DB ์ฟผ๋ฆฌ ์‹คํ–‰ ์ฝ”๋“œ๋Š” ์ƒ๋žต - return this.users.find(user => user.email === email); - } - - // ์•ฝํ•œ ํŒจ์Šค์›Œ๋“œ ์ •์ฑ… - createUser(userData) { - if (!userData.password || userData.password.length < 3) { // ๋„ˆ๋ฌด ์•ฝํ•œ ์ตœ์†Œ ๊ธธ์ด - throw new Error('Password too short'); - } - - // ํŒจ์Šค์›Œ๋“œ ํ•ด์‹ฑ ์—†์ด ์ €์žฅ - const user = { - id: Date.now(), // ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ID - email: userData.email, - password: userData.password, // ํ‰๋ฌธ ์ €์žฅ! - createdAt: new Date() - }; - - this.users.push(user); - return user; - } - - // ๋ธŒ๋ฃจํŠธํฌ์Šค ๊ณต๊ฒฉ์— ์ทจ์•ฝ - async login(email, password) { - const user = await this.getUserByEmail(email); - - if (!user) { - return null; // ์‹œ๊ฐ„ ๊ธฐ๋ฐ˜ ๊ณต๊ฒฉ ๊ฐ€๋Šฅ - } - - // ํ‰๋ฌธ ๋น„๊ต - if (user.password === password) { - const token = jwt.sign({ userId: user.id }, this.secret); - return { token, user }; - } - - return null; - } - - // ๊ถŒํ•œ ๊ฒ€์ฆ ๋ˆ„๋ฝ - deleteUser(userId) { - const index = this.users.findIndex(user => user.id == userId); // == ์‚ฌ์šฉ - if (index !== -1) { - this.users.splice(index, 1); - return true; - } - return false; - } - - // ์„ฑ๋Šฅ ๋ฌธ์ œ: N+1 ์ฟผ๋ฆฌ ํŒจํ„ด - async getUsersWithProfiles() { - const users = await this.getAllUsers(); - const usersWithProfiles = []; - - for (const user of users) { - const profile = await this.getUserProfile(user.id); // ๊ฐ ์‚ฌ์šฉ์ž๋งˆ๋‹ค ๊ฐœ๋ณ„ ์ฟผ๋ฆฌ - usersWithProfiles.push({ - ...user, - profile - }); - } - - return usersWithProfiles; - } - - // ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ์œ„ํ—˜ - cacheUserData(userId, data) { - if (!this.cache) { - this.cache = new Map(); - } - - this.cache.set(userId, data); // ์บ์‹œ ๋งŒ๋ฃŒ๋‚˜ ์ •๋ฆฌ ๋กœ์ง ์—†์Œ - } - - // ์ž…๋ ฅ ๊ฒ€์ฆ ๋ถ€์กฑ - updateUser(userId, updateData) { - const user = this.users.find(u => u.id === userId); - if (user) { - Object.assign(user, updateData); // ๋ชจ๋“  ํ•„๋“œ ์—…๋ฐ์ดํŠธ ํ—ˆ์šฉ - } - return user; - } - - // ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜ - async sendEmailVerification(email) { - // await ์—†์ด ๋น„๋™๊ธฐ ํ•จ์ˆ˜ ํ˜ธ์ถœ - this.emailService.sendEmail(email, 'Verify your account'); - console.log('Email sent'); // ์‹ค์ œ๋กœ๋Š” ์•„์ง ์ „์†ก๋˜์ง€ ์•Š์•˜์„ ์ˆ˜ ์žˆ์Œ - } -} - -// ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์—†์ด ๋ฏผ๊ฐํ•œ ์ •๋ณด ๋…ธ์ถœ -process.env.NODE_ENV !== 'production' && console.log('Database password: admin123'); - -module.exports = UserService; \ No newline at end of file diff --git a/test/additional-test.js b/test/additional-test.js deleted file mode 100644 index 513538f..0000000 --- a/test/additional-test.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * ์ถ”๊ฐ€ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ - PR ํ…Œ์ŠคํŠธ์šฉ - * ์ด ํŒŒ์ผ์€ PR์—์„œ Claude AI ์ฝ”๋“œ ๋ฆฌ๋ทฐ๊ฐ€ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. - */ - -// ์˜๋„์ ์ธ ๋ฌธ์ œ๋“ค์ด ํฌํ•จ๋œ ์ฝ”๋“œ -function badFunction() { - // ๋ฌธ์ œ 1: var ์‚ฌ์šฉ - var x = 10; - - // ๋ฌธ์ œ 2: == ๋Œ€์‹  === ์‚ฌ์šฉํ•ด์•ผ ํ•จ - if (x == "10") { - console.log("Equal"); - } - - // ๋ฌธ์ œ 3: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋ณ€์ˆ˜ - let unusedVar = "This is not used"; - - // ๋ฌธ์ œ 4: ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์—†์Œ - JSON.parse('{"invalid": json}'); - - return x; -} - -// ๊ฐœ์„ ๋œ ์ฝ”๋“œ -function goodFunction() { - const x = 10; - - if (x === 10) { - console.log("Equal"); - } - - try { - return JSON.parse('{"valid": "json"}'); - } catch (error) { - console.error('JSON parsing failed:', error); - return null; - } -} - -module.exports = { badFunction, goodFunction }; \ No newline at end of file diff --git a/test/sample-code.js b/test/sample-code.js deleted file mode 100644 index f7c6680..0000000 --- a/test/sample-code.js +++ /dev/null @@ -1,106 +0,0 @@ -/** - * ํ…Œ์ŠคํŠธ์šฉ ์ƒ˜ํ”Œ ์ฝ”๋“œ - * ์ด ํŒŒ์ผ์€ Claude AI ์ฝ”๋“œ ๋ฆฌ๋ทฐ ์•ก์…˜์ด ์ œ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค. - * ์˜๋„์ ์œผ๋กœ ๋ช‡ ๊ฐ€์ง€ ๋ฌธ์ œ์ ์„ ํฌํ•จํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. - */ - -// ๋ฌธ์ œ 1: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋ณ€์ˆ˜ -const unusedVariable = "This variable is never used"; - -// ๋ฌธ์ œ 2: ๊ธ€๋กœ๋ฒŒ ๋ณ€์ˆ˜ ์‚ฌ์šฉ -var globalVar = "Should avoid global variables"; - -// ๋ฌธ์ œ 3: ์•ˆ์ „ํ•˜์ง€ ์•Š์€ eval ์‚ฌ์šฉ (๋ณด์•ˆ ๋ฌธ์ œ) -function executeCode(code) { - return eval(code); // ๋ณด์•ˆ ์ทจ์•ฝ์  -} - -// ๋ฌธ์ œ 4: ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์—†๋Š” ๋น„๋™๊ธฐ ํ•จ์ˆ˜ -async function fetchData(url) { - const response = await fetch(url); // ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์—†์Œ - return response.json(); -} - -// ๋ฌธ์ œ 5: ๋งค์ง ๋„˜๋ฒ„ ์‚ฌ์šฉ -function calculateDiscount(price) { - if (price > 100) { - return price * 0.1; // ๋งค์ง ๋„˜๋ฒ„ 0.1 - } - return 0; -} - -// ๋ฌธ์ œ 6: ๊นŠ์€ ์ค‘์ฒฉ๊ณผ ๋ณต์žกํ•œ ์กฐ๊ฑด๋ฌธ -function complexFunction(data) { - if (data) { - if (data.length > 0) { - for (let i = 0; i < data.length; i++) { - if (data[i].active) { - if (data[i].type === 'premium') { - if (data[i].score > 80) { - console.log('Premium user with high score'); - } - } - } - } - } - } -} - -// ๋ฌธ์ œ 7: ํ•จ์ˆ˜๋ช…์ด ๋ช…ํ™•ํ•˜์ง€ ์•Š์Œ -function doStuff(x, y) { - return x + y * 2 - 5; -} - -// ๋ฌธ์ œ 8: ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๊ฐ’๋“ค -const config = { - apiUrl: 'http://localhost:3000/api', // ํ•˜๋“œ์ฝ”๋”ฉ๋œ URL - timeout: 5000, - retries: 3 -}; - -// ๋ฌธ์ œ 9: SQL ์ธ์ ์…˜ ๊ฐ€๋Šฅ์„ฑ (๋ณด์•ˆ ๋ฌธ์ œ) -function getUserData(userId) { - const query = `SELECT * FROM users WHERE id = ${userId}`; // SQL ์ธ์ ์…˜ ์œ„ํ—˜ - return database.query(query); -} - -// ๋ฌธ์ œ 10: ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๊ฐ€๋Šฅ์„ฑ -let cache = {}; -function addToCache(key, value) { - cache[key] = value; // ์บ์‹œ๊ฐ€ ๊ณ„์† ์ฆ๊ฐ€ํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๊ฐ€๋Šฅ -} - -// ์ข‹์€ ์ฝ”๋“œ ์˜ˆ์‹œ (Claude๊ฐ€ ๊ธ์ •์ ์œผ๋กœ ํ‰๊ฐ€ํ•  ๋ถ€๋ถ„) -/** - * ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜ - * @param {string} userId - ์‚ฌ์šฉ์ž ID - * @returns {Promise} ์‚ฌ์šฉ์ž ์ •๋ณด - */ -async function getUserInfoSafely(userId) { - try { - // ์ž…๋ ฅ ๊ฒ€์ฆ - if (!userId || typeof userId !== 'string') { - throw new Error('Invalid user ID'); - } - - // ์•ˆ์ „ํ•œ ํŒŒ๋ผ๋ฏธํ„ฐํ™”๋œ ์ฟผ๋ฆฌ - const query = 'SELECT * FROM users WHERE id = ?'; - const result = await database.query(query, [userId]); - - return result; - } catch (error) { - console.error('Error fetching user info:', error); - throw error; - } -} - -module.exports = { - executeCode, - fetchData, - calculateDiscount, - complexFunction, - doStuff, - getUserData, - addToCache, - getUserInfoSafely -}; \ No newline at end of file diff --git a/test/sample-security.py b/test/sample-security.py deleted file mode 100644 index ae29ab7..0000000 --- a/test/sample-security.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -๋ณด์•ˆ ์ทจ์•ฝ์  ํ…Œ์ŠคํŠธ์šฉ Python ์ฝ”๋“œ -Claude AI๊ฐ€ ๋ณด์•ˆ ๋ฌธ์ œ๋ฅผ ์ž˜ ์ฐพ์•„๋‚ด๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ ์ƒ˜ํ”Œ์ž…๋‹ˆ๋‹ค. -""" - -import os -import subprocess -import pickle -import hashlib - -# ๋ฌธ์ œ 1: ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ -SECRET_KEY = "hardcoded_secret_123" -DATABASE_PASSWORD = "admin123" - -# ๋ฌธ์ œ 2: ์•ˆ์ „ํ•˜์ง€ ์•Š์€ ๋ช…๋ น์–ด ์‹คํ–‰ -def execute_command(user_input): - # ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ์ง์ ‘ ์‹คํ–‰ - ๋ช…๋ น์–ด ์ธ์ ์…˜ ์œ„ํ—˜ - result = subprocess.run(user_input, shell=True, capture_output=True) - return result.stdout - -# ๋ฌธ์ œ 3: ์•ˆ์ „ํ•˜์ง€ ์•Š์€ pickle ์‚ฌ์šฉ -def load_data(file_path): - with open(file_path, 'rb') as f: - # pickle.load๋Š” ์ž„์˜ ์ฝ”๋“œ ์‹คํ–‰ ๊ฐ€๋Šฅ - return pickle.load(f) - -# ๋ฌธ์ œ 4: ์•ฝํ•œ ํ•ด์‹œ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์‚ฌ์šฉ -def hash_password(password): - # MD5๋Š” ๋” ์ด์ƒ ์•ˆ์ „ํ•˜์ง€ ์•Š์Œ - return hashlib.md5(password.encode()).hexdigest() - -# ๋ฌธ์ œ 5: ํŒŒ์ผ ๊ฒฝ๋กœ ๊ฒ€์ฆ ์—†์Œ (Path Traversal) -def read_file(filename): - # ../../../etc/passwd ๊ฐ™์€ ๊ฒฝ๋กœ๋กœ ์‹œ์Šคํ…œ ํŒŒ์ผ ์ ‘๊ทผ ๊ฐ€๋Šฅ - with open(f"/uploads/{filename}", 'r') as f: - return f.read() - -# ๋ฌธ์ œ 6: SQL ์ธ์ ์…˜ ์œ„ํ—˜ -def get_user(user_id): - query = f"SELECT * FROM users WHERE id = {user_id}" - # ํŒŒ๋ผ๋ฏธํ„ฐํ™”๋˜์ง€ ์•Š์€ ์ฟผ๋ฆฌ - return database.execute(query) - -# ๋ฌธ์ œ 7: ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์—์„œ ๋ฏผ๊ฐํ•œ ์ •๋ณด ๋…ธ์ถœ -def connect_database(): - try: - connection = database.connect( - host="192.168.1.100", - user="admin", - password=DATABASE_PASSWORD - ) - return connection - except Exception as e: - # ์—๋Ÿฌ ๋ฉ”์‹œ์ง€์— ๋ฏผ๊ฐํ•œ ์ •๋ณด๊ฐ€ ํฌํ•จ๋  ์ˆ˜ ์žˆ์Œ - print(f"Database connection failed: {e}") - raise - -# ๋ฌธ์ œ 8: ์•ˆ์ „ํ•˜์ง€ ์•Š์€ ๋žœ๋ค ์ƒ์„ฑ -import random -def generate_token(): - # random ๋ชจ๋“ˆ์€ ์•”ํ˜ธํ•™์ ์œผ๋กœ ์•ˆ์ „ํ•˜์ง€ ์•Š์Œ - return ''.join(random.choices('abcdefghijklmnopqrstuvwxyz', k=32)) - -# ์ข‹์€ ์ฝ”๋“œ ์˜ˆ์‹œ -import secrets -import bcrypt -from pathlib import Path - -def hash_password_securely(password): - """๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ํ•ด์‹œํ™”""" - # bcrypt ์‚ฌ์šฉ์œผ๋กœ ์•ˆ์ „ํ•œ ํ•ด์‹œ - salt = bcrypt.gensalt() - return bcrypt.hashpw(password.encode('utf-8'), salt) - -def generate_secure_token(): - """์•”ํ˜ธํ•™์ ์œผ๋กœ ์•ˆ์ „ํ•œ ํ† ํฐ ์ƒ์„ฑ""" - return secrets.token_urlsafe(32) - -def read_file_safely(filename): - """์•ˆ์ „ํ•œ ํŒŒ์ผ ์ฝ๊ธฐ""" - # ๊ฒฝ๋กœ ๊ฒ€์ฆ - safe_path = Path("/uploads") / filename - if not str(safe_path).startswith("/uploads/"): - raise ValueError("Invalid file path") - - try: - with open(safe_path, 'r') as f: - return f.read() - except FileNotFoundError: - return None \ No newline at end of file From 33fe58595b88502f60dea5b87dcebb5e2caf2a60 Mon Sep 17 00:00:00 2001 From: chimaek Date: Tue, 29 Jul 2025 16:11:45 +0900 Subject: [PATCH 02/13] test: Add simple test file to debug action --- examples/simple-test.js | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 examples/simple-test.js diff --git a/examples/simple-test.js b/examples/simple-test.js new file mode 100644 index 0000000..48a554d --- /dev/null +++ b/examples/simple-test.js @@ -0,0 +1,6 @@ +// ๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ ํŒŒ์ผ - Action์ด ๊ฐ์ง€ํ•˜๋Š”์ง€ ํ™•์ธ +function simpleFunction() { + var password = "12345"; // ๋ณด์•ˆ ์ด์Šˆ: ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ + console.log("Password is: " + password); // ๋ณด์•ˆ ์ด์Šˆ: ๋น„๋ฐ€๋ฒˆํ˜ธ ๋กœ๊น… + return true; +} \ No newline at end of file From a87a4b900c21333ab0d5d89a41f572cccf2fa32c Mon Sep 17 00:00:00 2001 From: chimaek Date: Tue, 29 Jul 2025 16:13:23 +0900 Subject: [PATCH 03/13] debug: Add logging to identify file filtering issues - Add console.log statements to track file filtering process - Log file patterns, exclude patterns, and filtering results - Debug why code files are not being reviewed while README.md is --- src/file-analyzer.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/file-analyzer.js b/src/file-analyzer.js index 3fbb4d4..9f968e4 100644 --- a/src/file-analyzer.js +++ b/src/file-analyzer.js @@ -70,10 +70,19 @@ class FileAnalyzer { }); // ์‚ญ์ œ๋œ ํŒŒ์ผ์€ ์ œ์™ธํ•˜๊ณ , ์‹ค์ œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ๋Š” ํŒŒ์ผ๋งŒ ๋ฐ˜ํ™˜ - return files.filter(file => + const filteredFiles = files.filter(file => file.status !== 'removed' && file.additions + file.deletions > 0 ); + + console.log('PR files from GitHub API:', filteredFiles.map(f => ({ + filename: f.filename, + status: f.status, + additions: f.additions, + deletions: f.deletions + }))); + + return filteredFiles; } /** @@ -154,19 +163,29 @@ class FileAnalyzer { * @returns {Promise} ํ•„ํ„ฐ๋ง๋œ ํŒŒ์ผ ๋ชฉ๋ก */ async filterFiles(files) { + console.log('Starting file filtering...'); + console.log('File patterns:', this.filePatterns); + console.log('Exclude patterns:', this.excludePatterns); + console.log('Files to filter:', files.map(f => f.filename || f)); + // 1. ํŒจํ„ด ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง const patternFiltered = files.filter(file => { + const filename = file.filename || file; + // ํฌํ•จ ํŒจํ„ด ์ฒดํฌ: ํ•˜๋‚˜๋ผ๋„ ๋งค์น˜๋˜๋ฉด ํฌํ•จ const isIncluded = this.filePatterns.some(pattern => - minimatch(file.filename, pattern) + minimatch(filename, pattern) ); // ์ œ์™ธ ํŒจํ„ด ์ฒดํฌ: ํ•˜๋‚˜๋ผ๋„ ๋งค์น˜๋˜๋ฉด ์ œ์™ธ const isExcluded = this.excludePatterns.some(pattern => - minimatch(file.filename, pattern) + minimatch(filename, pattern) ); - return isIncluded && !isExcluded; + const shouldInclude = isIncluded && !isExcluded; + console.log(`File ${filename}: included=${isIncluded}, excluded=${isExcluded}, result=${shouldInclude}`); + + return shouldInclude; }); // 2. ํŒŒ์ผ ํฌ๊ธฐ ๋ฐ ๋ณต์žก๋„ ํ•„ํ„ฐ๋ง (์†๋„ ๊ฐœ์„ ) From 4464348c7208011157a9c5eec79b8a7aa65d1b76 Mon Sep 17 00:00:00 2001 From: chimaek Date: Tue, 29 Jul 2025 16:14:53 +0900 Subject: [PATCH 04/13] debug: Add input logging to identify pattern source issue - Log raw inputs from GitHub Action - Check why file_patterns is not using default values - Identify source of wrong patterns: src/**/*.js, test-files/**/*.js, *.yml, *.json, *.md --- src/index.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/index.js b/src/index.js index 966c815..74be06b 100644 --- a/src/index.js +++ b/src/index.js @@ -23,6 +23,10 @@ async function run() { try { // 1. ์•ก์…˜ ์ž…๋ ฅ๊ฐ’ ์ˆ˜์ง‘ // core.getInput()์„ ํ†ตํ•ด action.yml์— ์ •์˜๋œ ์ž…๋ ฅ๊ฐ’๋“ค์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค + console.log('Raw inputs from GitHub Action:'); + console.log('file_patterns input:', core.getInput('file_patterns')); + console.log('exclude_patterns input:', core.getInput('exclude_patterns')); + const inputs = { anthropicApiKey: core.getInput('anthropic_api_key', { required: true }), githubToken: core.getInput('github_token', { required: true }), @@ -33,6 +37,11 @@ async function run() { language: core.getInput('language') || 'en', severityFilter: core.getInput('severity_filter') || 'medium' }; + + console.log('Final inputs used:', { + filePatterns: inputs.filePatterns, + excludePatterns: inputs.excludePatterns + }); // GitHub ์ปจํ…์ŠคํŠธ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ // PR ์ •๋ณด, ์ปค๋ฐ‹ ์ •๋ณด, ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ •๋ณด ๋“ฑ์ด ํฌํ•จ๋จ From 60b54b906216d6b8a1badc1ef38a9fb7d8a50bee Mon Sep 17 00:00:00 2001 From: chimaek Date: Tue, 29 Jul 2025 16:15:18 +0900 Subject: [PATCH 05/13] fix: Update workflow file_patterns to include all code files - Change file_patterns from src/**/*.js,test-files/**/*.js,*.yml,*.json,*.md - To proper pattern: **/*.js,**/*.ts,**/*.jsx,**/*.tsx,**/*.py,**/*.java,**/*.go,**/*.rs - This was causing examples/ files to be filtered out - Now all example code files should be reviewed properly --- .github/workflows/ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 864111f..3da7d3d 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -45,7 +45,7 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} review_type: full language: ko - file_patterns: "src/**/*.js,test-files/**/*.js,*.yml,*.json,*.md" + file_patterns: "**/*.js,**/*.ts,**/*.jsx,**/*.tsx,**/*.py,**/*.java,**/*.go,**/*.rs" exclude_patterns: "dist/**,node_modules/**,test/**" max_files: 8 severity_filter: medium From b6cbfc35092ff8089e00f629a3891708ef278a9e Mon Sep 17 00:00:00 2001 From: chimaek Date: Tue, 29 Jul 2025 16:18:35 +0900 Subject: [PATCH 06/13] fix: Improve JSON parsing robustness to handle API response errors - Add multiple JSON extraction methods (code blocks, general patterns, brace matching) - Clean JSON text by removing trailing commas and escaping special characters - Increase maxTokens from 2000 to 4000 to prevent truncated responses - Add detailed logging for debugging parsing issues - Graceful fallback when JSON parsing fails completely This should reduce the 'Review Processing Error' messages caused by: - Unterminated strings in JSON - Expected property name or '}' errors - Expected ',' or ']' after array element errors --- src/code-reviewer.js | 66 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/src/code-reviewer.js b/src/code-reviewer.js index 0e66afb..4daea25 100644 --- a/src/code-reviewer.js +++ b/src/code-reviewer.js @@ -21,7 +21,7 @@ class CodeReviewer { // Claude API ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” this.client = new Anthropic({ apiKey }); this.language = language; - this.maxTokens = 2000; // Claude ์‘๋‹ต ์ตœ๋Œ€ ํ† ํฐ ์ˆ˜ (์†๋„ ๊ฐœ์„ ) + this.maxTokens = 4000; // Claude ์‘๋‹ต ์ตœ๋Œ€ ํ† ํฐ ์ˆ˜ (JSON ์™„์„ฑ๋„ ํ–ฅ์ƒ) } /** @@ -174,20 +174,60 @@ JSON ์‘๋‹ต (๊ฐ„๊ฒฐํ•˜๊ฒŒ): */ parseResponse(responseText) { try { - // JSON ์‘๋‹ต ์ถ”์ถœ (Claude๊ฐ€ ๋งˆํฌ๋‹ค์šด ๋ธ”๋ก์œผ๋กœ ๊ฐ์Œ€ ์ˆ˜ ์žˆ์Œ) - const jsonMatch = responseText.match(/\{[\s\S]*\}/); - if (!jsonMatch) { - throw new Error('No valid JSON found in response'); + console.log('Raw Claude response:', responseText); + + // 1. ์—ฌ๋Ÿฌ ๋ฐฉ๋ฒ•์œผ๋กœ JSON ์ถ”์ถœ ์‹œ๋„ + let jsonText = null; + + // ๋ฐฉ๋ฒ• 1: ๋งˆํฌ๋‹ค์šด ์ฝ”๋“œ ๋ธ”๋ก์—์„œ ์ถ”์ถœ + const codeBlockMatch = responseText.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/); + if (codeBlockMatch) { + jsonText = codeBlockMatch[1]; + } + + // ๋ฐฉ๋ฒ• 2: ์ผ๋ฐ˜ JSON ํŒจํ„ด ๋งค์นญ + if (!jsonText) { + const jsonMatch = responseText.match(/\{[\s\S]*\}/); + if (jsonMatch) { + jsonText = jsonMatch[0]; + } + } + + // ๋ฐฉ๋ฒ• 3: ์ฒซ ๋ฒˆ์งธ { ๋ถ€ํ„ฐ ๋งˆ์ง€๋ง‰ } ๊นŒ์ง€ + if (!jsonText) { + const firstBrace = responseText.indexOf('{'); + const lastBrace = responseText.lastIndexOf('}'); + if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) { + jsonText = responseText.substring(firstBrace, lastBrace + 1); + } } - - const parsed = JSON.parse(jsonMatch[0]); - // ์‘๋‹ต ํ˜•์‹ ๊ฒ€์ฆ - if (!parsed.summary || !Array.isArray(parsed.issues)) { - throw new Error('Invalid response format'); + if (!jsonText) { + throw new Error('No JSON found in response'); + } + + // 2. JSON ์ •๋ฆฌ (์ผ๋ฐ˜์ ์ธ ํŒŒ์‹ฑ ์˜ค๋ฅ˜ ์ˆ˜์ •) + jsonText = jsonText + .replace(/,\s*}/g, '}') // ๋งˆ์ง€๋ง‰ ์ฝค๋งˆ ์ œ๊ฑฐ + .replace(/,\s*]/g, ']') // ๋ฐฐ์—ด ๋งˆ์ง€๋ง‰ ์ฝค๋งˆ ์ œ๊ฑฐ + .replace(/\n/g, '\\n') // ๊ฐœํ–‰ ๋ฌธ์ž ์ด์Šค์ผ€์ดํ”„ + .replace(/\t/g, '\\t') // ํƒญ ๋ฌธ์ž ์ด์Šค์ผ€์ดํ”„ + .replace(/\r/g, '\\r'); // ์บ๋ฆฌ์ง€ ๋ฆฌํ„ด ์ด์Šค์ผ€์ดํ”„ + + console.log('Cleaned JSON text:', jsonText); + + // 3. JSON ํŒŒ์‹ฑ ์‹œ๋„ + const parsed = JSON.parse(jsonText); + + // 4. ์‘๋‹ต ํ˜•์‹ ๊ฒ€์ฆ + if (!parsed.summary) { + parsed.summary = 'Code review completed'; + } + if (!Array.isArray(parsed.issues)) { + parsed.issues = []; } - // ์‘๋‹ต ์ •๊ทœํ™” ๋ฐ ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • + // 5. ์‘๋‹ต ์ •๊ทœํ™” ๋ฐ ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • return { summary: parsed.summary, issues: parsed.issues.map(issue => ({ @@ -203,8 +243,10 @@ JSON ์‘๋‹ต (๊ฐ„๊ฒฐํ•˜๊ฒŒ): overallScore: parsed.overall_score || 5 }; } catch (error) { + console.error('JSON parsing failed:', error.message); + console.error('Problematic text:', responseText); + // JSON ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ fallback ์‘๋‹ต ์ƒ์„ฑ - // ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์•ก์…˜์ด ์™„์ „ํžˆ ์‹คํŒจํ•˜์ง€ ์•Š๊ณ  ๊ณ„์† ์ง„ํ–‰๋จ return { summary: 'Code review completed, but response parsing failed.', issues: [{ From 8542b7379b3b62e2030b5188b76f10471546a050 Mon Sep 17 00:00:00 2001 From: chimaek Date: Tue, 29 Jul 2025 16:19:55 +0900 Subject: [PATCH 07/13] add: MIT License file - Add standard MIT License for open source distribution - Copyright holder: chimaek - Allows free use, modification, and distribution - Standard license for GitHub Actions --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..033cc0c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 chimaek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file From d9a06d52b413d86a9bc19ea29ad51741109b3faf Mon Sep 17 00:00:00 2001 From: chimaek Date: Tue, 29 Jul 2025 16:21:46 +0900 Subject: [PATCH 08/13] fix: Enhance JSON response reliability with system message and clearer prompt - Add system message to enforce JSON-only responses - Clarify prompt with explicit JSON format requirements - Remove ambiguous example values that might confuse Claude - Add explicit instruction to avoid non-JSON text This should fix the 'Expected property name or '}' in JSON at position 1' errors by ensuring Claude responds with pure JSON format only. --- src/code-reviewer.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/code-reviewer.js b/src/code-reviewer.js index 4daea25..ab8eff2 100644 --- a/src/code-reviewer.js +++ b/src/code-reviewer.js @@ -43,6 +43,7 @@ class CodeReviewer { model: 'claude-sonnet-4-20250514', // ์ฝ”๋“œ ๋ถ„์„์— ์ ํ•ฉํ•œ ๋ชจ๋ธ max_tokens: this.maxTokens, temperature: 0.1, // ์ผ๊ด€์„ฑ ์žˆ๋Š” ์‘๋‹ต์„ ์œ„ํ•ด ๋‚ฎ์€ temperature ์‚ฌ์šฉ + system: "You are a senior code reviewer. Always respond with ONLY valid JSON format. Do not include any explanation, markdown, or other text outside the JSON structure.", messages: [{ role: 'user', content: prompt @@ -79,7 +80,7 @@ class CodeReviewer { diff.substring(0, 1000) + '\n// ... (truncated)' : diff; - // ๊ฐ„๊ฒฐํ•œ ํ”„๋กฌํ”„ํŠธ ๊ตฌ์„ฑ (์†๋„ ๊ฐœ์„ ) + // ๋ช…ํ™•ํ•œ JSON ํ˜•์‹ ์š”์ฒญ return `${basePrompt} ${languageInstruction} ํŒŒ์ผ: ${filename} @@ -91,11 +92,21 @@ ${truncatedDiff ? `๋ณ€๊ฒฝ์‚ฌํ•ญ:\n\`\`\`diff\n${truncatedDiff}\n\`\`\`` : ''} ${truncatedContent} \`\`\` -JSON ์‘๋‹ต (๊ฐ„๊ฒฐํ•˜๊ฒŒ): +**์ค‘์š”**: ๋ฐ˜๋“œ์‹œ ์•„๋ž˜ ์ •ํ™•ํ•œ JSON ํ˜•์‹์œผ๋กœ๋งŒ ์‘๋‹ตํ•ด์ฃผ์„ธ์š”. ๋‹ค๋ฅธ ํ…์ŠคํŠธ๋Š” ํฌํ•จํ•˜์ง€ ๋งˆ์„ธ์š”. + { - "summary": "์š”์•ฝ", - "issues": [{"line": 0, "severity": "medium", "type": "bug", "title": "์ œ๋ชฉ", "description": "์„ค๋ช…", "suggestion": "์ œ์•ˆ"}], - "overall_score": 7 + "summary": "๋ฆฌ๋ทฐ ์š”์•ฝ์„ ์—ฌ๊ธฐ์— ์ž‘์„ฑ", + "issues": [ + { + "line": ๋ผ์ธ๋ฒˆํ˜ธ_๋˜๋Š”_null, + "severity": "low|medium|high|critical", + "type": "bug|security|performance|style|maintainability", + "title": "์ด์Šˆ ์ œ๋ชฉ", + "description": "์ƒ์„ธ ์„ค๋ช…", + "suggestion": "๊ฐœ์„  ์ œ์•ˆ" + } + ], + "overall_score": 1๋ถ€ํ„ฐ_10๊นŒ์ง€์˜_์ˆซ์ž }`; } From 26073bc4ddf7ba390ec7f2a75b8ce7b81eeaf795 Mon Sep 17 00:00:00 2001 From: chimaek Date: Tue, 29 Jul 2025 16:26:01 +0900 Subject: [PATCH 09/13] fix: Improve comment update detection and add unique identifier - Add HTML comment identifier to comments - Make bot comment detection more flexible (Bot type or github-actions login) - Add logging to debug existing comments and bot detection - This ensures comments are properly updated instead of creating new ones Now the action should update existing comments when new commits are pushed to PR. --- src/comment-manager.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/comment-manager.js b/src/comment-manager.js index 2675c79..a035513 100644 --- a/src/comment-manager.js +++ b/src/comment-manager.js @@ -77,10 +77,11 @@ class CommentManager { } } - // ๋Œ“๊ธ€ ํ‘ธํ„ฐ + // ๋Œ“๊ธ€ ํ‘ธํ„ฐ (๊ณ ์œ  ์‹๋ณ„์ž ํฌํ•จ) comment += `\n---\n`; comment += `*๋ฆฌ๋ทฐ ์‹œ๊ฐ„: ${new Date().toISOString()}*\n`; - comment += `*Powered by Claude AI* ๐Ÿš€`; + comment += `*Powered by Claude AI* ๐Ÿš€\n\n`; + comment += ``; return comment; } @@ -253,11 +254,20 @@ class CommentManager { issue_number: this.context.payload.pull_request.number }); - // ์ด์ „์— ์ž‘์„ฑํ•œ ๋ด‡ ๋Œ“๊ธ€ ์ฐพ๊ธฐ + // ์ด์ „์— ์ž‘์„ฑํ•œ ๋ด‡ ๋Œ“๊ธ€ ์ฐพ๊ธฐ (๋” ๊ด€๋Œ€ํ•œ ์กฐ๊ฑด) + console.log('Existing comments:', comments.map(c => ({ + id: c.id, + user: c.user.login, + userType: c.user.type, + bodyPreview: c.body.substring(0, 50) + '...' + }))); + const botComment = comments.find(comment => - comment.user.type === 'Bot' && - comment.body.includes('๐Ÿค– Claude AI ์ฝ”๋“œ ๋ฆฌ๋ทฐ') + (comment.user.type === 'Bot' || comment.user.login.includes('github-actions')) && + (comment.body.includes('๐Ÿค– Claude AI ์ฝ”๋“œ ๋ฆฌ๋ทฐ') || comment.body.includes('')) ); + + console.log('Found existing bot comment:', botComment ? botComment.id : 'none'); if (botComment) { // ๊ธฐ์กด ๋Œ“๊ธ€ ์—…๋ฐ์ดํŠธ From a5d4df01825c99af5cf4cdf1720436e9858e16d3 Mon Sep 17 00:00:00 2001 From: chimaek Date: Tue, 29 Jul 2025 16:44:19 +0900 Subject: [PATCH 10/13] refactor: Always create new comments and remove debug logs - Always create new PR comments instead of updating existing ones - Remove debug logging for cleaner console output: - Raw Claude response logging - File filtering debug messages - Input validation logs - Comment detection logs - Remove HTML comment identifier (no longer needed) - Cleaner comment flow for each commit/PR update This provides fresh review comments for each commit while keeping logs minimal. --- src/code-reviewer.js | 7 ------- src/comment-manager.js | 45 ++++++------------------------------------ src/file-analyzer.js | 21 ++------------------ src/index.js | 9 --------- 4 files changed, 8 insertions(+), 74 deletions(-) diff --git a/src/code-reviewer.js b/src/code-reviewer.js index ab8eff2..598f367 100644 --- a/src/code-reviewer.js +++ b/src/code-reviewer.js @@ -185,8 +185,6 @@ ${truncatedContent} */ parseResponse(responseText) { try { - console.log('Raw Claude response:', responseText); - // 1. ์—ฌ๋Ÿฌ ๋ฐฉ๋ฒ•์œผ๋กœ JSON ์ถ”์ถœ ์‹œ๋„ let jsonText = null; @@ -225,8 +223,6 @@ ${truncatedContent} .replace(/\t/g, '\\t') // ํƒญ ๋ฌธ์ž ์ด์Šค์ผ€์ดํ”„ .replace(/\r/g, '\\r'); // ์บ๋ฆฌ์ง€ ๋ฆฌํ„ด ์ด์Šค์ผ€์ดํ”„ - console.log('Cleaned JSON text:', jsonText); - // 3. JSON ํŒŒ์‹ฑ ์‹œ๋„ const parsed = JSON.parse(jsonText); @@ -254,9 +250,6 @@ ${truncatedContent} overallScore: parsed.overall_score || 5 }; } catch (error) { - console.error('JSON parsing failed:', error.message); - console.error('Problematic text:', responseText); - // JSON ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ fallback ์‘๋‹ต ์ƒ์„ฑ return { summary: 'Code review completed, but response parsing failed.', diff --git a/src/comment-manager.js b/src/comment-manager.js index a035513..dd619fe 100644 --- a/src/comment-manager.js +++ b/src/comment-manager.js @@ -77,11 +77,10 @@ class CommentManager { } } - // ๋Œ“๊ธ€ ํ‘ธํ„ฐ (๊ณ ์œ  ์‹๋ณ„์ž ํฌํ•จ) + // ๋Œ“๊ธ€ ํ‘ธํ„ฐ comment += `\n---\n`; comment += `*๋ฆฌ๋ทฐ ์‹œ๊ฐ„: ${new Date().toISOString()}*\n`; - comment += `*Powered by Claude AI* ๐Ÿš€\n\n`; - comment += ``; + comment += `*Powered by Claude AI* ๐Ÿš€`; return comment; } @@ -247,45 +246,13 @@ class CommentManager { */ async postPullRequestComment(commentBody) { try { - // ๊ธฐ์กด ๋ด‡ ๋Œ“๊ธ€ ์ฐพ๊ธฐ (์ค‘๋ณต ๋ฐฉ์ง€) - const { data: comments } = await this.octokit.rest.issues.listComments({ + // ํ•ญ์ƒ ์ƒˆ ๋Œ“๊ธ€ ์ƒ์„ฑ + await this.octokit.rest.issues.createComment({ owner: this.context.repo.owner, repo: this.context.repo.repo, - issue_number: this.context.payload.pull_request.number + issue_number: this.context.payload.pull_request.number, + body: commentBody }); - - // ์ด์ „์— ์ž‘์„ฑํ•œ ๋ด‡ ๋Œ“๊ธ€ ์ฐพ๊ธฐ (๋” ๊ด€๋Œ€ํ•œ ์กฐ๊ฑด) - console.log('Existing comments:', comments.map(c => ({ - id: c.id, - user: c.user.login, - userType: c.user.type, - bodyPreview: c.body.substring(0, 50) + '...' - }))); - - const botComment = comments.find(comment => - (comment.user.type === 'Bot' || comment.user.login.includes('github-actions')) && - (comment.body.includes('๐Ÿค– Claude AI ์ฝ”๋“œ ๋ฆฌ๋ทฐ') || comment.body.includes('')) - ); - - console.log('Found existing bot comment:', botComment ? botComment.id : 'none'); - - if (botComment) { - // ๊ธฐ์กด ๋Œ“๊ธ€ ์—…๋ฐ์ดํŠธ - await this.octokit.rest.issues.updateComment({ - owner: this.context.repo.owner, - repo: this.context.repo.repo, - comment_id: botComment.id, - body: commentBody - }); - } else { - // ์ƒˆ ๋Œ“๊ธ€ ์ƒ์„ฑ - await this.octokit.rest.issues.createComment({ - owner: this.context.repo.owner, - repo: this.context.repo.repo, - issue_number: this.context.payload.pull_request.number, - body: commentBody - }); - } } catch (error) { throw new Error(`Failed to post PR comment: ${error.message}`); } diff --git a/src/file-analyzer.js b/src/file-analyzer.js index 9f968e4..3e74a9f 100644 --- a/src/file-analyzer.js +++ b/src/file-analyzer.js @@ -70,19 +70,10 @@ class FileAnalyzer { }); // ์‚ญ์ œ๋œ ํŒŒ์ผ์€ ์ œ์™ธํ•˜๊ณ , ์‹ค์ œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ๋Š” ํŒŒ์ผ๋งŒ ๋ฐ˜ํ™˜ - const filteredFiles = files.filter(file => + return files.filter(file => file.status !== 'removed' && file.additions + file.deletions > 0 ); - - console.log('PR files from GitHub API:', filteredFiles.map(f => ({ - filename: f.filename, - status: f.status, - additions: f.additions, - deletions: f.deletions - }))); - - return filteredFiles; } /** @@ -163,11 +154,6 @@ class FileAnalyzer { * @returns {Promise} ํ•„ํ„ฐ๋ง๋œ ํŒŒ์ผ ๋ชฉ๋ก */ async filterFiles(files) { - console.log('Starting file filtering...'); - console.log('File patterns:', this.filePatterns); - console.log('Exclude patterns:', this.excludePatterns); - console.log('Files to filter:', files.map(f => f.filename || f)); - // 1. ํŒจํ„ด ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง const patternFiltered = files.filter(file => { const filename = file.filename || file; @@ -182,10 +168,7 @@ class FileAnalyzer { minimatch(filename, pattern) ); - const shouldInclude = isIncluded && !isExcluded; - console.log(`File ${filename}: included=${isIncluded}, excluded=${isExcluded}, result=${shouldInclude}`); - - return shouldInclude; + return isIncluded && !isExcluded; }); // 2. ํŒŒ์ผ ํฌ๊ธฐ ๋ฐ ๋ณต์žก๋„ ํ•„ํ„ฐ๋ง (์†๋„ ๊ฐœ์„ ) diff --git a/src/index.js b/src/index.js index 74be06b..966c815 100644 --- a/src/index.js +++ b/src/index.js @@ -23,10 +23,6 @@ async function run() { try { // 1. ์•ก์…˜ ์ž…๋ ฅ๊ฐ’ ์ˆ˜์ง‘ // core.getInput()์„ ํ†ตํ•ด action.yml์— ์ •์˜๋œ ์ž…๋ ฅ๊ฐ’๋“ค์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค - console.log('Raw inputs from GitHub Action:'); - console.log('file_patterns input:', core.getInput('file_patterns')); - console.log('exclude_patterns input:', core.getInput('exclude_patterns')); - const inputs = { anthropicApiKey: core.getInput('anthropic_api_key', { required: true }), githubToken: core.getInput('github_token', { required: true }), @@ -37,11 +33,6 @@ async function run() { language: core.getInput('language') || 'en', severityFilter: core.getInput('severity_filter') || 'medium' }; - - console.log('Final inputs used:', { - filePatterns: inputs.filePatterns, - excludePatterns: inputs.excludePatterns - }); // GitHub ์ปจํ…์ŠคํŠธ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ // PR ์ •๋ณด, ์ปค๋ฐ‹ ์ •๋ณด, ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ •๋ณด ๋“ฑ์ด ํฌํ•จ๋จ From b0060c6f57dc5036abad3cfa744e460d4153815c Mon Sep 17 00:00:00 2001 From: chimaek Date: Tue, 29 Jul 2025 16:53:46 +0900 Subject: [PATCH 11/13] init --- src/code-reviewer.js | 160 ++++++++++++++++++++++++++++--------------- 1 file changed, 105 insertions(+), 55 deletions(-) diff --git a/src/code-reviewer.js b/src/code-reviewer.js index 598f367..ce9615b 100644 --- a/src/code-reviewer.js +++ b/src/code-reviewer.js @@ -92,22 +92,31 @@ ${truncatedDiff ? `๋ณ€๊ฒฝ์‚ฌํ•ญ:\n\`\`\`diff\n${truncatedDiff}\n\`\`\`` : ''} ${truncatedContent} \`\`\` -**์ค‘์š”**: ๋ฐ˜๋“œ์‹œ ์•„๋ž˜ ์ •ํ™•ํ•œ JSON ํ˜•์‹์œผ๋กœ๋งŒ ์‘๋‹ตํ•ด์ฃผ์„ธ์š”. ๋‹ค๋ฅธ ํ…์ŠคํŠธ๋Š” ํฌํ•จํ•˜์ง€ ๋งˆ์„ธ์š”. +**๋งค์šฐ ์ค‘์š”**: ์‘๋‹ต์€ ๋ฐ˜๋“œ์‹œ ์œ ํšจํ•œ JSON ํ˜•์‹์œผ๋กœ๋งŒ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”. ์–ด๋–ค ์„ค๋ช…์ด๋‚˜ ์ถ”๊ฐ€ ํ…์ŠคํŠธ๋„ ํฌํ•จํ•˜์ง€ ๋ง๊ณ , ์˜ค์ง JSON๋งŒ ๋ฐ˜ํ™˜ํ•ด์ฃผ์„ธ์š”. +์‘๋‹ต ํ˜•์‹: { - "summary": "๋ฆฌ๋ทฐ ์š”์•ฝ์„ ์—ฌ๊ธฐ์— ์ž‘์„ฑ", + "summary": "๋ฆฌ๋ทฐ ์š”์•ฝ ํ…์ŠคํŠธ", "issues": [ { - "line": ๋ผ์ธ๋ฒˆํ˜ธ_๋˜๋Š”_null, - "severity": "low|medium|high|critical", - "type": "bug|security|performance|style|maintainability", + "line": 10, + "severity": "medium", + "type": "bug", "title": "์ด์Šˆ ์ œ๋ชฉ", "description": "์ƒ์„ธ ์„ค๋ช…", "suggestion": "๊ฐœ์„  ์ œ์•ˆ" } ], - "overall_score": 1๋ถ€ํ„ฐ_10๊นŒ์ง€์˜_์ˆซ์ž -}`; + "overall_score": 7 +} + +์ฃผ์˜์‚ฌํ•ญ: +- line์€ ์ˆซ์ž ๋˜๋Š” null๋งŒ ๊ฐ€๋Šฅ +- severity๋Š” "low", "medium", "high", "critical" ์ค‘ ํ•˜๋‚˜ +- type์€ "bug", "security", "performance", "style", "maintainability" ์ค‘ ํ•˜๋‚˜ +- overall_score๋Š” 1-10 ์‚ฌ์ด์˜ ์ˆซ์ž +- ๋ชจ๋“  ๋ฌธ์ž์—ด์€ ๋ฐ˜๋“œ์‹œ ํฐ๋”ฐ์˜ดํ‘œ๋กœ ๊ฐ์‹ธ๊ธฐ +- ๋งˆ์ง€๋ง‰ ํ•ญ๋ชฉ ๋’ค์— ์ฝค๋งˆ ์—†์ด ์ž‘์„ฑ`; } /** @@ -185,81 +194,122 @@ ${truncatedContent} */ parseResponse(responseText) { try { - // 1. ์—ฌ๋Ÿฌ ๋ฐฉ๋ฒ•์œผ๋กœ JSON ์ถ”์ถœ ์‹œ๋„ + // ๋””๋ฒ„๊น…์„ ์œ„ํ•œ ์›๋ณธ ์‘๋‹ต ๋กœ๊น… + console.log('Raw Claude response:', responseText.substring(0, 500)); + + // 1. ์‘๋‹ต ํ…์ŠคํŠธ ์ „์ฒ˜๋ฆฌ + let cleanedText = responseText.trim(); + + // 2. ์—ฌ๋Ÿฌ ๋ฐฉ๋ฒ•์œผ๋กœ JSON ์ถ”์ถœ ์‹œ๋„ let jsonText = null; // ๋ฐฉ๋ฒ• 1: ๋งˆํฌ๋‹ค์šด ์ฝ”๋“œ ๋ธ”๋ก์—์„œ ์ถ”์ถœ - const codeBlockMatch = responseText.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/); + const codeBlockMatch = cleanedText.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/); if (codeBlockMatch) { - jsonText = codeBlockMatch[1]; + jsonText = codeBlockMatch[1].trim(); } - // ๋ฐฉ๋ฒ• 2: ์ผ๋ฐ˜ JSON ํŒจํ„ด ๋งค์นญ + // ๋ฐฉ๋ฒ• 2: ์ผ๋ฐ˜ JSON ํŒจํ„ด ๋งค์นญ (๊ฐ€์žฅ ์™„์ „ํ•œ JSON ์ฐพ๊ธฐ) if (!jsonText) { - const jsonMatch = responseText.match(/\{[\s\S]*\}/); - if (jsonMatch) { - jsonText = jsonMatch[0]; + const jsonMatches = cleanedText.match(/\{[\s\S]*?\}/g); + if (jsonMatches && jsonMatches.length > 0) { + // ๊ฐ€์žฅ ๊ธด JSON ๊ฐ์ฒด ์„ ํƒ (๋” ์™„์ „ํ•  ๊ฐ€๋Šฅ์„ฑ) + jsonText = jsonMatches.reduce((longest, current) => + current.length > longest.length ? current : longest + ); } } // ๋ฐฉ๋ฒ• 3: ์ฒซ ๋ฒˆ์งธ { ๋ถ€ํ„ฐ ๋งˆ์ง€๋ง‰ } ๊นŒ์ง€ if (!jsonText) { - const firstBrace = responseText.indexOf('{'); - const lastBrace = responseText.lastIndexOf('}'); + const firstBrace = cleanedText.indexOf('{'); + const lastBrace = cleanedText.lastIndexOf('}'); if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) { - jsonText = responseText.substring(firstBrace, lastBrace + 1); + jsonText = cleanedText.substring(firstBrace, lastBrace + 1); } } if (!jsonText) { - throw new Error('No JSON found in response'); + throw new Error('No JSON structure found in response'); } - // 2. JSON ์ •๋ฆฌ (์ผ๋ฐ˜์ ์ธ ํŒŒ์‹ฑ ์˜ค๋ฅ˜ ์ˆ˜์ •) - jsonText = jsonText - .replace(/,\s*}/g, '}') // ๋งˆ์ง€๋ง‰ ์ฝค๋งˆ ์ œ๊ฑฐ - .replace(/,\s*]/g, ']') // ๋ฐฐ์—ด ๋งˆ์ง€๋ง‰ ์ฝค๋งˆ ์ œ๊ฑฐ - .replace(/\n/g, '\\n') // ๊ฐœํ–‰ ๋ฌธ์ž ์ด์Šค์ผ€์ดํ”„ - .replace(/\t/g, '\\t') // ํƒญ ๋ฌธ์ž ์ด์Šค์ผ€์ดํ”„ - .replace(/\r/g, '\\r'); // ์บ๋ฆฌ์ง€ ๋ฆฌํ„ด ์ด์Šค์ผ€์ดํ”„ + console.log('Extracted JSON text:', jsonText.substring(0, 200)); - // 3. JSON ํŒŒ์‹ฑ ์‹œ๋„ - const parsed = JSON.parse(jsonText); + // 3. JSON ์ •๋ฆฌ ๋ฐ ์ˆ˜์ • + jsonText = jsonText + // ์ž˜๋ชป๋œ ์ฝค๋งˆ ์ œ๊ฑฐ + .replace(/,(\s*[}\]])/g, '$1') + // ๋ฌธ์ž์—ด ๋‚ด ํŠน์ˆ˜๋ฌธ์ž ์ฒ˜๋ฆฌ + .replace(/\\/g, '\\\\') // ๋ฐฑ์Šฌ๋ž˜์‹œ ์ด์Šค์ผ€์ดํ”„ + .replace(/\n/g, ' ') // ๊ฐœํ–‰์„ ๊ณต๋ฐฑ์œผ๋กœ ๋ณ€๊ฒฝ + .replace(/\r/g, '') // ์บ๋ฆฌ์ง€ ๋ฆฌํ„ด ์ œ๊ฑฐ + .replace(/\t/g, ' ') // ํƒญ์„ ๊ณต๋ฐฑ์œผ๋กœ ๋ณ€๊ฒฝ + // ์ž˜๋ชป๋œ ๋”ฐ์˜ดํ‘œ ์ˆ˜์ • + .replace(/"/g, '"') // ์œ ๋‹ˆ์ฝ”๋“œ ๋”ฐ์˜ดํ‘œ ์ •๊ทœํ™” + .replace(/"/g, '"') + .replace(/'/g, "'") // ์ž‘์€๋”ฐ์˜ดํ‘œ ์ •๊ทœํ™” + // ์—ฌ๋ถ„์˜ ๊ณต๋ฐฑ ์ œ๊ฑฐ + .replace(/\s+/g, ' ') + .trim(); - // 4. ์‘๋‹ต ํ˜•์‹ ๊ฒ€์ฆ - if (!parsed.summary) { - parsed.summary = 'Code review completed'; - } - if (!Array.isArray(parsed.issues)) { - parsed.issues = []; + // 4. JSON ํŒŒ์‹ฑ ์‹œ๋„ (์—ฌ๋Ÿฌ ๋ฒˆ ์‹œ๋„) + let parsed; + try { + parsed = JSON.parse(jsonText); + } catch (firstError) { + console.log('First parse attempt failed:', firstError.message); + + // JSON ์ˆ˜์ • ์žฌ์‹œ๋„ + const fixedJson = jsonText + .replace(/([{,]\s*)(\w+):/g, '$1"$2":') // ํ‚ค์— ๋”ฐ์˜ดํ‘œ ์ถ”๊ฐ€ + .replace(/:\s*([^",\[\]{}]+)(\s*[,}])/g, ': "$1"$2') // ๊ฐ’์— ๋”ฐ์˜ดํ‘œ ์ถ”๊ฐ€ + .replace(/: ""(\d+)""([,}])/g, ': $1$2') // ์ˆซ์ž ๊ฐ’ ์ˆ˜์ • + .replace(/: ""(true|false|null)""([,}])/g, ': $1$2'); // ๋ถˆ๋ฆฐ/null ๊ฐ’ ์ˆ˜์ • + + try { + parsed = JSON.parse(fixedJson); + } catch (secondError) { + console.log('Second parse attempt failed:', secondError.message); + throw new Error(`JSON parsing failed: ${firstError.message}`); + } } - - // 5. ์‘๋‹ต ์ •๊ทœํ™” ๋ฐ ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • - return { - summary: parsed.summary, - issues: parsed.issues.map(issue => ({ - line: issue.line || null, - severity: issue.severity || 'medium', - type: issue.type || 'general', - title: issue.title || 'Issue found', - description: issue.description || '', - suggestion: issue.suggestion || '', - codeExample: issue.code_example || null - })), - positiveFeedback: parsed.positive_feedback || [], - overallScore: parsed.overall_score || 5 + + // 5. ์‘๋‹ต ํ˜•์‹ ๊ฒ€์ฆ ๋ฐ ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • + const result = { + summary: parsed.summary || 'Code review completed', + issues: Array.isArray(parsed.issues) ? parsed.issues : [], + positiveFeedback: Array.isArray(parsed.positive_feedback) ? parsed.positive_feedback : [], + overallScore: typeof parsed.overall_score === 'number' ? parsed.overall_score : 5 }; + + // 6. ์ด์Šˆ ์ •๊ทœํ™” + result.issues = result.issues.map(issue => ({ + line: typeof issue.line === 'number' ? issue.line : null, + severity: ['low', 'medium', 'high', 'critical'].includes(issue.severity) ? issue.severity : 'medium', + type: ['bug', 'security', 'performance', 'style', 'maintainability'].includes(issue.type) ? issue.type : 'general', + title: issue.title || 'Issue found', + description: issue.description || '', + suggestion: issue.suggestion || '', + codeExample: issue.code_example || issue.codeExample || null + })); + + console.log(`Successfully parsed review with ${result.issues.length} issues`); + return result; + } catch (error) { - // JSON ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ fallback ์‘๋‹ต ์ƒ์„ฑ + console.error('Parse error details:', error); + console.error('Original response:', responseText); + + // JSON ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ์•ˆ์ „ํ•œ fallback ์‘๋‹ต ์ƒ์„ฑ return { - summary: 'Code review completed, but response parsing failed.', + summary: 'Code review was processed, but the response format was invalid.', issues: [{ line: null, - severity: 'medium', - type: 'general', - title: 'Review Processing Error', - description: `Failed to parse review response: ${error.message}`, - suggestion: 'Please check the code manually.', + severity: 'low', + type: 'system', + title: 'Response Parsing Issue', + description: `AI ์‘๋‹ต ํŒŒ์‹ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ${error.message}. ์›๋ณธ ์‘๋‹ต์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.`, + suggestion: '์ฝ”๋“œ๋ฅผ ์ˆ˜๋™์œผ๋กœ ๊ฒ€ํ† ํ•˜๊ฑฐ๋‚˜ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.', codeExample: null }], positiveFeedback: [], From e45c69727f7e480718ab2ac9e08c0835d7e87ac2 Mon Sep 17 00:00:00 2001 From: chimaek Date: Tue, 29 Jul 2025 16:57:32 +0900 Subject: [PATCH 12/13] init --- src/code-reviewer.js | 104 +++++++++++++++++++++++++++++++++---------- 1 file changed, 80 insertions(+), 24 deletions(-) diff --git a/src/code-reviewer.js b/src/code-reviewer.js index ce9615b..9821cfa 100644 --- a/src/code-reviewer.js +++ b/src/code-reviewer.js @@ -38,20 +38,24 @@ class CodeReviewer { const prompt = this.buildPrompt(filename, content, diff, reviewType); try { - // Claude API ํ˜ธ์ถœ + // Claude API ํ˜ธ์ถœ (ํ† ํฐ ์ˆ˜ ์ฆ๊ฐ€ ๋ฐ ์ŠคํŠธ๋ฆผ ๋น„ํ™œ์„ฑํ™”) const response = await this.client.messages.create({ model: 'claude-sonnet-4-20250514', // ์ฝ”๋“œ ๋ถ„์„์— ์ ํ•ฉํ•œ ๋ชจ๋ธ - max_tokens: this.maxTokens, + max_tokens: 8000, // ํ† ํฐ ์ˆ˜ ์ฆ๊ฐ€๋กœ ์™„์ „ํ•œ ์‘๋‹ต ๋ณด์žฅ temperature: 0.1, // ์ผ๊ด€์„ฑ ์žˆ๋Š” ์‘๋‹ต์„ ์œ„ํ•ด ๋‚ฎ์€ temperature ์‚ฌ์šฉ - system: "You are a senior code reviewer. Always respond with ONLY valid JSON format. Do not include any explanation, markdown, or other text outside the JSON structure.", + system: "You are a senior code reviewer. CRITICAL: Always respond with complete, valid JSON format. Never truncate your response. Ensure the JSON is properly closed with all brackets and braces. Do not include any explanation, markdown, or other text outside the JSON structure.", messages: [{ role: 'user', content: prompt }] }); + const responseText = response.content[0].text; + console.log(`Response length: ${responseText.length} characters`); + console.log(`Response ends with: "${responseText.slice(-50)}"`); + // API ์‘๋‹ต์„ ๊ตฌ์กฐํ™”๋œ ํ˜•์‹์œผ๋กœ ํŒŒ์‹ฑ - return this.parseResponse(response.content[0].text); + return this.parseResponse(responseText); } catch (error) { throw new Error(`Claude API error: ${error.message}`); } @@ -92,31 +96,25 @@ ${truncatedDiff ? `๋ณ€๊ฒฝ์‚ฌํ•ญ:\n\`\`\`diff\n${truncatedDiff}\n\`\`\`` : ''} ${truncatedContent} \`\`\` -**๋งค์šฐ ์ค‘์š”**: ์‘๋‹ต์€ ๋ฐ˜๋“œ์‹œ ์œ ํšจํ•œ JSON ํ˜•์‹์œผ๋กœ๋งŒ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”. ์–ด๋–ค ์„ค๋ช…์ด๋‚˜ ์ถ”๊ฐ€ ํ…์ŠคํŠธ๋„ ํฌํ•จํ•˜์ง€ ๋ง๊ณ , ์˜ค์ง JSON๋งŒ ๋ฐ˜ํ™˜ํ•ด์ฃผ์„ธ์š”. +**๋งค์šฐ ์ค‘์š”**: ์‘๋‹ต์€ ๋ฐ˜๋“œ์‹œ ์™„์ „ํ•œ ์œ ํšจํ•œ JSON๋งŒ ๋ฐ˜ํ™˜ํ•˜์„ธ์š”. ์„ค๋ช…์ด๋‚˜ ์ถ”๊ฐ€ ํ…์ŠคํŠธ๋Š” ์ ˆ๋Œ€ ํฌํ•จํ•˜์ง€ ๋งˆ์„ธ์š”. ๊ฐ„๊ฒฐํ•˜๊ณ  ํ•ต์‹ฌ์ ์ธ ๋‚ด์šฉ๋งŒ ํฌํ•จํ•˜์„ธ์š”. -์‘๋‹ต ํ˜•์‹: +JSON ํ˜•์‹ (์ตœ๋Œ€ 5๊ฐœ ์ด์Šˆ๋งŒ): { - "summary": "๋ฆฌ๋ทฐ ์š”์•ฝ ํ…์ŠคํŠธ", + "summary": "๊ฐ„๋‹จํ•œ ์š”์•ฝ (50์ž ์ด๋‚ด)", "issues": [ { - "line": 10, - "severity": "medium", - "type": "bug", - "title": "์ด์Šˆ ์ œ๋ชฉ", - "description": "์ƒ์„ธ ์„ค๋ช…", - "suggestion": "๊ฐœ์„  ์ œ์•ˆ" + "line": ์ˆซ์ž_๋˜๋Š”_null, + "severity": "low|medium|high|critical", + "type": "bug|security|performance|style|maintainability", + "title": "๊ฐ„๋‹จํ•œ ์ œ๋ชฉ (30์ž ์ด๋‚ด)", + "description": "ํ•ต์‹ฌ ์„ค๋ช… (100์ž ์ด๋‚ด)", + "suggestion": "๊ฐœ์„  ๋ฐฉ๋ฒ• (100์ž ์ด๋‚ด)" } ], - "overall_score": 7 + "overall_score": 1๋ถ€ํ„ฐ_10๊นŒ์ง€_์ˆซ์ž } -์ฃผ์˜์‚ฌํ•ญ: -- line์€ ์ˆซ์ž ๋˜๋Š” null๋งŒ ๊ฐ€๋Šฅ -- severity๋Š” "low", "medium", "high", "critical" ์ค‘ ํ•˜๋‚˜ -- type์€ "bug", "security", "performance", "style", "maintainability" ์ค‘ ํ•˜๋‚˜ -- overall_score๋Š” 1-10 ์‚ฌ์ด์˜ ์ˆซ์ž -- ๋ชจ๋“  ๋ฌธ์ž์—ด์€ ๋ฐ˜๋“œ์‹œ ํฐ๋”ฐ์˜ดํ‘œ๋กœ ๊ฐ์‹ธ๊ธฐ -- ๋งˆ์ง€๋ง‰ ํ•ญ๋ชฉ ๋’ค์— ์ฝค๋งˆ ์—†์ด ์ž‘์„ฑ`; +์ค‘์š”: JSON์„ ์™„์ „ํžˆ ๋‹ซ์•„์•ผ ํ•˜๋ฉฐ, ์‘๋‹ต์ด ์ž˜๋ฆฌ๋ฉด ์•ˆ๋ฉ๋‹ˆ๋‹ค.`; } /** @@ -259,8 +257,8 @@ ${truncatedContent} } catch (firstError) { console.log('First parse attempt failed:', firstError.message); - // JSON ์ˆ˜์ • ์žฌ์‹œ๋„ - const fixedJson = jsonText + // JSON ์ˆ˜์ • ์žฌ์‹œ๋„ 1: ๊ธฐ๋ณธ ์ˆ˜์ • + let fixedJson = jsonText .replace(/([{,]\s*)(\w+):/g, '$1"$2":') // ํ‚ค์— ๋”ฐ์˜ดํ‘œ ์ถ”๊ฐ€ .replace(/:\s*([^",\[\]{}]+)(\s*[,}])/g, ': "$1"$2') // ๊ฐ’์— ๋”ฐ์˜ดํ‘œ ์ถ”๊ฐ€ .replace(/: ""(\d+)""([,}])/g, ': $1$2') // ์ˆซ์ž ๊ฐ’ ์ˆ˜์ • @@ -270,7 +268,17 @@ ${truncatedContent} parsed = JSON.parse(fixedJson); } catch (secondError) { console.log('Second parse attempt failed:', secondError.message); - throw new Error(`JSON parsing failed: ${firstError.message}`); + + // JSON ์ˆ˜์ • ์žฌ์‹œ๋„ 2: ๋ถˆ์™„์ „ํ•œ JSON ๋ณต๊ตฌ + fixedJson = this.repairIncompleteJson(jsonText); + + try { + parsed = JSON.parse(fixedJson); + console.log('Successfully repaired incomplete JSON'); + } catch (thirdError) { + console.log('Third parse attempt failed:', thirdError.message); + throw new Error(`JSON parsing failed: ${firstError.message}`); + } } } @@ -317,6 +325,54 @@ ${truncatedContent} }; } } + + /** + * ๋ถˆ์™„์ „ํ•œ JSON์„ ๋ณต๊ตฌํ•˜๋Š” ๋ฉ”์„œ๋“œ + * @param {string} incompleteJson - ๋ถˆ์™„์ „ํ•œ JSON ๋ฌธ์ž์—ด + * @returns {string} ๋ณต๊ตฌ๋œ JSON ๋ฌธ์ž์—ด + */ + repairIncompleteJson(incompleteJson) { + console.log('Attempting to repair incomplete JSON...'); + + let repaired = incompleteJson.trim(); + + // 1. ๋ถˆ์™„์ „ํ•œ ๋ฌธ์ž์—ด ๊ฐ’ ์ฒ˜๋ฆฌ + repaired = repaired.replace(/"[^"]*$/, '"incomplete"'); + + // 2. ๋ถˆ์™„์ „ํ•œ ๊ฐ์ฒด ๋˜๋Š” ๋ฐฐ์—ด ๋‹ซ๊ธฐ + const openBraces = (repaired.match(/\{/g) || []).length; + const closeBraces = (repaired.match(/\}/g) || []).length; + const openBrackets = (repaired.match(/\[/g) || []).length; + const closeBrackets = (repaired.match(/\]/g) || []).length; + + // 3. ๋งˆ์ง€๋ง‰ ๋ถˆ์™„์ „ํ•œ ํ•ญ๋ชฉ ์ œ๊ฑฐ (์‰ผํ‘œ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ถˆ์™„์ „ํ•œ ๊ฐ์ฒด) + repaired = repaired.replace(/,\s*\{[^}]*$/, ''); + repaired = repaired.replace(/,\s*"[^"]*":\s*"[^"]*$/, ''); + repaired = repaired.replace(/,\s*"[^"]*":\s*$/, ''); + + // 4. ๋ˆ„๋ฝ๋œ ํฐ๋”ฐ์˜ดํ‘œ ์ถ”๊ฐ€ + repaired = repaired.replace(/:\s*([a-zA-Z][a-zA-Z0-9_]*)\s*([,}])/g, ': "$1"$2'); + + // 5. ํ•„์š”ํ•œ ๊ด„ํ˜ธ ๋‹ซ๊ธฐ + for (let i = closeBrackets; i < openBrackets; i++) { + repaired += ']'; + } + for (let i = closeBraces; i < openBraces; i++) { + repaired += '}'; + } + + // 6. ๊ธฐ๋ณธ ๊ตฌ์กฐ๊ฐ€ ์—†์œผ๋ฉด ์ตœ์†Œ ๊ตฌ์กฐ ์ƒ์„ฑ + if (!repaired.includes('"summary"') || !repaired.includes('"issues"')) { + return JSON.stringify({ + summary: "Code review completed but response was incomplete", + issues: [], + overall_score: 5 + }); + } + + console.log('Repaired JSON preview:', repaired.substring(0, 200)); + return repaired; + } } module.exports = CodeReviewer; \ No newline at end of file From 67cb274b85f3987f76b8cae45dcfdb9853e930d8 Mon Sep 17 00:00:00 2001 From: chimaek Date: Tue, 29 Jul 2025 16:59:49 +0900 Subject: [PATCH 13/13] init --- src/code-reviewer.js | 119 +++++++++++++++++++++++++++++-------------- 1 file changed, 81 insertions(+), 38 deletions(-) diff --git a/src/code-reviewer.js b/src/code-reviewer.js index 9821cfa..c62289c 100644 --- a/src/code-reviewer.js +++ b/src/code-reviewer.js @@ -96,25 +96,10 @@ ${truncatedDiff ? `๋ณ€๊ฒฝ์‚ฌํ•ญ:\n\`\`\`diff\n${truncatedDiff}\n\`\`\`` : ''} ${truncatedContent} \`\`\` -**๋งค์šฐ ์ค‘์š”**: ์‘๋‹ต์€ ๋ฐ˜๋“œ์‹œ ์™„์ „ํ•œ ์œ ํšจํ•œ JSON๋งŒ ๋ฐ˜ํ™˜ํ•˜์„ธ์š”. ์„ค๋ช…์ด๋‚˜ ์ถ”๊ฐ€ ํ…์ŠคํŠธ๋Š” ์ ˆ๋Œ€ ํฌํ•จํ•˜์ง€ ๋งˆ์„ธ์š”. ๊ฐ„๊ฒฐํ•˜๊ณ  ํ•ต์‹ฌ์ ์ธ ๋‚ด์šฉ๋งŒ ํฌํ•จํ•˜์„ธ์š”. +**์ค‘์š”**: ์™„์ „ํ•œ JSON๋งŒ ๋ฐ˜ํ™˜ํ•˜์„ธ์š”. ์ตœ๋Œ€ 3๊ฐœ ์ด์Šˆ๋งŒ ํฌํ•จํ•˜๊ณ  ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ž‘์„ฑํ•˜์„ธ์š”. -JSON ํ˜•์‹ (์ตœ๋Œ€ 5๊ฐœ ์ด์Šˆ๋งŒ): -{ - "summary": "๊ฐ„๋‹จํ•œ ์š”์•ฝ (50์ž ์ด๋‚ด)", - "issues": [ - { - "line": ์ˆซ์ž_๋˜๋Š”_null, - "severity": "low|medium|high|critical", - "type": "bug|security|performance|style|maintainability", - "title": "๊ฐ„๋‹จํ•œ ์ œ๋ชฉ (30์ž ์ด๋‚ด)", - "description": "ํ•ต์‹ฌ ์„ค๋ช… (100์ž ์ด๋‚ด)", - "suggestion": "๊ฐœ์„  ๋ฐฉ๋ฒ• (100์ž ์ด๋‚ด)" - } - ], - "overall_score": 1๋ถ€ํ„ฐ_10๊นŒ์ง€_์ˆซ์ž -} - -์ค‘์š”: JSON์„ ์™„์ „ํžˆ ๋‹ซ์•„์•ผ ํ•˜๋ฉฐ, ์‘๋‹ต์ด ์ž˜๋ฆฌ๋ฉด ์•ˆ๋ฉ๋‹ˆ๋‹ค.`; +ํ˜•์‹: +{"summary":"์š”์•ฝ(30์ž)","issues":[{"line":์ˆซ์ž,"severity":"low/medium/high/critical","type":"bug/security/performance/style","title":"์ œ๋ชฉ(20์ž)","description":"์„ค๋ช…(50์ž)","suggestion":"์ œ์•ˆ(50์ž)"}],"overall_score":์ˆซ์ž}`; } /** @@ -336,24 +321,91 @@ JSON ํ˜•์‹ (์ตœ๋Œ€ 5๊ฐœ ์ด์Šˆ๋งŒ): let repaired = incompleteJson.trim(); - // 1. ๋ถˆ์™„์ „ํ•œ ๋ฌธ์ž์—ด ๊ฐ’ ์ฒ˜๋ฆฌ - repaired = repaired.replace(/"[^"]*$/, '"incomplete"'); + // 1. ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰ ์™„์ „ํ•œ ์ด์Šˆ ์ฐพ๊ธฐ + const issuesMatch = repaired.match(/"issues":\s*\[([\s\S]*)/); + if (issuesMatch) { + const issuesContent = issuesMatch[1]; + + // ์™„์ „ํ•œ ์ด์Šˆ ๊ฐ์ฒด๋“ค๋งŒ ์ถ”์ถœ + const completeIssues = []; + let depth = 0; + let currentIssue = ''; + let inString = false; + let escapeNext = false; + + for (let i = 0; i < issuesContent.length; i++) { + const char = issuesContent[i]; + + if (escapeNext) { + escapeNext = false; + currentIssue += char; + continue; + } + + if (char === '\\') { + escapeNext = true; + currentIssue += char; + continue; + } + + if (char === '"' && !escapeNext) { + inString = !inString; + } + + if (!inString) { + if (char === '{') { + depth++; + } else if (char === '}') { + depth--; + + if (depth === 0) { + currentIssue += char; + + // ์™„์ „ํ•œ ์ด์Šˆ ๊ฐ์ฒด ๋ฐœ๊ฒฌ + try { + const issueObj = JSON.parse(currentIssue); + if (issueObj.title && issueObj.description) { + completeIssues.push(currentIssue); + } + } catch (e) { + // ํŒŒ์‹ฑ ์‹คํŒจํ•œ ๊ฐ์ฒด๋Š” ๋ฌด์‹œ + } + + currentIssue = ''; + continue; + } + } + } + + currentIssue += char; + } + + // ๋ณต๊ตฌ๋œ JSON ์ƒ์„ฑ + const summaryMatch = repaired.match(/"summary":\s*"([^"]*)"/) || ["", "Code review completed"]; + const scoreMatch = repaired.match(/"overall_score":\s*(\d+)/) || ["", "5"]; + + const repairedJson = { + summary: summaryMatch[1] || "Code review completed", + issues: completeIssues.map(issue => JSON.parse(issue)), + overall_score: parseInt(scoreMatch[1]) || 5 + }; + + console.log(`Repaired JSON with ${repairedJson.issues.length} complete issues`); + return JSON.stringify(repairedJson); + } - // 2. ๋ถˆ์™„์ „ํ•œ ๊ฐ์ฒด ๋˜๋Š” ๋ฐฐ์—ด ๋‹ซ๊ธฐ + // ๊ธฐ๋ณธ ๋ณต๊ตฌ ๋กœ์ง (์œ„ ๋ฐฉ๋ฒ•์ด ์‹คํŒจํ•œ ๊ฒฝ์šฐ) const openBraces = (repaired.match(/\{/g) || []).length; const closeBraces = (repaired.match(/\}/g) || []).length; const openBrackets = (repaired.match(/\[/g) || []).length; const closeBrackets = (repaired.match(/\]/g) || []).length; - // 3. ๋งˆ์ง€๋ง‰ ๋ถˆ์™„์ „ํ•œ ํ•ญ๋ชฉ ์ œ๊ฑฐ (์‰ผํ‘œ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ถˆ์™„์ „ํ•œ ๊ฐ์ฒด) + // ๋ถˆ์™„์ „ํ•œ ๋งˆ์ง€๋ง‰ ๋ถ€๋ถ„ ์ œ๊ฑฐ repaired = repaired.replace(/,\s*\{[^}]*$/, ''); - repaired = repaired.replace(/,\s*"[^"]*":\s*"[^"]*$/, ''); - repaired = repaired.replace(/,\s*"[^"]*":\s*$/, ''); - - // 4. ๋ˆ„๋ฝ๋œ ํฐ๋”ฐ์˜ดํ‘œ ์ถ”๊ฐ€ - repaired = repaired.replace(/:\s*([a-zA-Z][a-zA-Z0-9_]*)\s*([,}])/g, ': "$1"$2'); + repaired = repaired.replace(/,\s*"[^"]*":[^,}]*$/, ''); + repaired = repaired.replace(/:\s*"[^"]*$/, ': "incomplete"'); - // 5. ํ•„์š”ํ•œ ๊ด„ํ˜ธ ๋‹ซ๊ธฐ + // ๊ด„ํ˜ธ ๋‹ซ๊ธฐ for (let i = closeBrackets; i < openBrackets; i++) { repaired += ']'; } @@ -361,16 +413,7 @@ JSON ํ˜•์‹ (์ตœ๋Œ€ 5๊ฐœ ์ด์Šˆ๋งŒ): repaired += '}'; } - // 6. ๊ธฐ๋ณธ ๊ตฌ์กฐ๊ฐ€ ์—†์œผ๋ฉด ์ตœ์†Œ ๊ตฌ์กฐ ์ƒ์„ฑ - if (!repaired.includes('"summary"') || !repaired.includes('"issues"')) { - return JSON.stringify({ - summary: "Code review completed but response was incomplete", - issues: [], - overall_score: 5 - }); - } - - console.log('Repaired JSON preview:', repaired.substring(0, 200)); + console.log('Fallback repair applied'); return repaired; } }