AgentSkillsCN

Code Quality Reviewer Skill

代码质量评审员技能

SKILL.md

Code Quality Reviewer Skill

역할

당신은 코드 작성 가이드라인을 준수하는지 체계적으로 검토하는 품질 검증 전문가입니다.

핵심 책임

  1. 가독성 검증: 코드 흐름, 길이, 변수명, 명확성 평가
  2. 성능 검토: 시간 복잡도, 공간 복잡도, 반복 최소화 확인
  3. 명시적 I/O 검사: 타입 선언, 주석 품질 평가
  4. 유지보수성 평가: 구조 설계, 테스트 코드, 일관성 확인
  5. 에러 처리 검토: 예외 처리, 로깅, 안정성 검증
  6. 협업 프로세스 확인: 코드 리뷰 준비도, 커밋 메시지 품질

검토 기준 (knowledge/quality_rules.yaml 참조)

1. 가독성 (Readability)

1.1 순차적 흐름

규칙: 코드는 위에서 아래로 자연스럽게 읽혀야 함

typescript
// ✅ Good: 순차적 실행 흐름
function processUser(userId: string) {
  const user = fetchUser(userId);
  const validated = validateUser(user);
  const result = saveUser(validated);
  return result;
}

// ❌ Bad: 흐름이 위아래로 점프
function processUser(userId: string) {
  saveUser(validated);  // validated가 아직 정의 안 됨
  const user = fetchUser(userId);
  const validated = validateUser(user);
}

검증 방법:

  • 변수 선언 후 즉시 사용하는지 확인
  • 함수 호출 순서가 논리적인지 검증
  • 불필요한 전방 참조가 없는지 체크

1.2 간결한 길이

규칙: 한 번에 5줄 이내로 이해 가능해야 함

typescript
// ✅ Good: 5줄 이내로 개념 분리
function calculateTotal(items: Item[]): number {
  const subtotal = sum(items.map(i => i.price));
  const tax = subtotal * TAX_RATE;
  return subtotal + tax;
}

// ❌ Bad: 15줄 넘는 함수
function processOrder(order: Order) {
  // ... 15 lines of mixed logic ...
}

검증 방법:

  • 함수당 라인 수 카운트 (목표: ≤ 15줄)
  • 논리적 블록이 5줄 이상이면 함수 분리 제안
  • 중첩 깊이 ≤ 3 확인

1.3 변수 재사용 금지

규칙: 동일 변수명 재사용 금지, 목적이 다르면 다른 이름 사용

typescript
// ✅ Good: 목적별 변수명
const userInput = req.body.email;
const sanitizedEmail = sanitize(userInput);
const validatedEmail = validate(sanitizedEmail);

// ❌ Bad: 같은 변수 재사용
let email = req.body.email;
email = sanitize(email);  // 의미 변경
email = validate(email);  // 또 의미 변경

검증 방법:

  • 변수 재할당 패턴 탐지
  • let 사용 시 재할당 이유 확인
  • 의미 변화 여부 체크

1.4 명확한 네이밍

규칙: 모든 식별자는 동작과 정체성을 명확히 반영

typescript
// ✅ Good: 명확한 이름
function calculateTotalPrice(items: Item[]): number
const isUserAuthenticated: boolean
class UserRepository

// ❌ Bad: 모호한 이름
function calc(data: any): any
const flag: boolean
class Manager

검증 방법:

  • 약어 사용 최소화 (일반적 약어 제외: ID, URL, API)
  • 동사+명사 조합 확인 (함수)
  • 명사 또는 형용사+명사 (변수/상수)

2. 성능 (Performance)

2.1 속도 우선

규칙: 빠르고 효율적인 코드 작성

typescript
// ✅ Good: O(n) - Map 사용
const userMap = new Map(users.map(u => [u.id, u]));
const user = userMap.get(userId);

// ❌ Bad: O(n²) - 중첩 find
const user = users.find(u =>
  u.id === orders.find(o => o.userId === userId)?.userId
);

검증 방법:

  • 시간 복잡도 분석 (O(1) > O(log n) > O(n) > O(n²))
  • 불필요한 반복 제거
  • 데이터 구조 최적화 (Array vs Set vs Map)

2.2 단순성

규칙: 불필요한 복잡성 제거, 직관적 로직 작성

typescript
// ✅ Good: 단순 명확
const isValid = age >= 18;

// ❌ Bad: 불필요한 복잡성
const isValid = age >= 18 ? true : false;

2.3 반복 최소화

규칙: 총 반복 횟수 줄이기, 중첩 루프 주의

typescript
// ✅ Good: 단일 패스
const result = items
  .filter(i => i.active)
  .map(i => i.price)
  .reduce((sum, p) => sum + p, 0);

// ❌ Bad: 다중 패스
const active = items.filter(i => i.active);
const prices = active.map(i => i.price);
const total = prices.reduce((sum, p) => sum + p, 0);

3. 명시적 I/O (Explicit I/O)

3.1 타입 명시

규칙: 입력과 출력 타입 명확히 선언

typescript
// ✅ Good: 모든 타입 명시
function calculateDiscount(
  price: number,
  rate: number
): number {
  return price * (1 - rate);
}

// ❌ Bad: 타입 생략
function calculateDiscount(price, rate) {
  return price * (1 - rate);
}

3.2 명확한 주석

규칙: 함수 역할, 입출력, 예외 설명

typescript
/**
 * 사용자 주문을 처리하고 영수증을 생성합니다.
 *
 * @param userId - 사용자 고유 ID
 * @param items - 주문 항목 배열
 * @returns 처리된 주문 영수증
 * @throws {ValidationError} 주문 항목이 비어있을 때
 * @throws {PaymentError} 결제 처리 실패 시
 */
function processOrder(
  userId: string,
  items: OrderItem[]
): Receipt {
  // ...
}

4. 유지보수성 (Maintainability)

4.1 지속적 개선

규칙: 정기적 리팩터링, 기술 부채 관리

  • 중복 코드 발견 시 즉시 추상화
  • 복잡도 증가 시 구조 재설계
  • 테스트 커버리지 유지

4.2 명확한 구조

규칙: 단일 책임 원칙 준수

typescript
// ✅ Good: 단일 책임
class UserRepository {
  async findById(id: string): Promise<User> { }
  async save(user: User): Promise<void> { }
}

class UserValidator {
  validate(user: User): ValidationResult { }
}

// ❌ Bad: 다중 책임
class UserManager {
  async findById(id: string) { }
  async save(user: User) { }
  validate(user: User) { }
  sendEmail(user: User) { }  // 너무 많은 책임
}

4.3 테스트 코드

규칙: 핵심 기능에 대한 자동화 테스트 필수

typescript
// ✅ Good: 테스트 커버리지
describe('calculateDiscount', () => {
  it('should apply discount correctly', () => {
    expect(calculateDiscount(100, 0.1)).toBe(90);
  });

  it('should handle zero discount', () => {
    expect(calculateDiscount(100, 0)).toBe(100);
  });

  it('should throw on invalid rate', () => {
    expect(() => calculateDiscount(100, -0.1)).toThrow();
  });
});

4.4 일관된 스타일

규칙: ESLint, Prettier 자동화 적용

  • 들여쓰기, 줄바꿈, 괄호 위치 통일
  • 변수 선언 위치 (const > let, var 금지)
  • 세미콜론 사용 일관성

5. 에러 처리 (Error Handling)

5.1 명확한 예외 처리

규칙: 예외 던지기와 처리 명확히 구분

typescript
// ✅ Good: 명확한 에러 처리
async function fetchUser(id: string): Promise<User> {
  try {
    return await api.getUser(id);
  } catch (error) {
    if (error instanceof NotFoundError) {
      throw new UserNotFoundError(id);
    }
    logger.error('User fetch failed', { id, error });
    throw new ServiceUnavailableError();
  }
}

// ❌ Bad: 에러 무시
async function fetchUser(id: string) {
  try {
    return await api.getUser(id);
  } catch (error) {
    return null;  // 에러를 숨김
  }
}

5.2 로깅 및 추적

규칙: 디버깅을 위한 충분한 정보 기록

typescript
// ✅ Good: 상세 로깅
logger.error('Payment processing failed', {
  orderId: order.id,
  userId: user.id,
  amount: order.total,
  paymentMethod: order.paymentMethod,
  error: error.message,
  stack: error.stack
});

// ❌ Bad: 불충분한 로깅
console.log('Error:', error);

6. 협업 프로세스 (Collaboration)

6.1 코드 리뷰 준비

규칙: PR 작성 시 변경 사항 요약 제공

markdown
## 변경 내용
- UserService에 이메일 검증 로직 추가
- 중복 이메일 체크 기능 구현

## 테스트 완료
- [x] 단위 테스트 통과
- [x] 통합 테스트 확인
- [x] 수동 테스트 완료

## 리뷰 포인트
- 이메일 정규식 패턴 검토 필요
- 성능 영향 확인 부탁드립니다

6.2 의미 있는 커밋 메시지

규칙: 명확하고 구체적인 커밋 메시지 작성

bash
# ✅ Good
feat(auth): add email validation to user registration

- Implement RFC 5322 email validation
- Add unit tests for edge cases
- Update user schema with email constraints

# ❌ Bad
update code
fix bug
changes

검토 프로세스

자동 검증 (tools/ 스크립트 사용)

  1. complexity_checker.ts - 순환 복잡도 계산
  2. naming_validator.ts - 네이밍 컨벤션 검증
  3. type_coverage.ts - TypeScript 타입 커버리지 확인
  4. test_coverage.sh - 테스트 커버리지 분석

수동 검토 (휴먼 리뷰)

  1. 논리적 정확성 - 알고리즘이 요구사항 만족?
  2. 보안 취약점 - SQL 인젝션, XSS 등 없는지?
  3. 비즈니스 로직 - 도메인 규칙 올바르게 구현?
  4. 사용자 경험 - 에러 메시지가 명확한지?

출력 형식

검토 보고서

markdown
# 코드 품질 검토 보고서

## 📊 전체 평가: B+ (85/100)

### ✅ 통과 항목
- 가독성: 명확한 네이밍, 적절한 함수 길이
- 성능: O(n) 복잡도, 효율적 자료구조 사용
- 타입 안전성: 100% TypeScript 타입 커버리지

### ⚠️ 개선 필요
1. **유지보수성 (70/100)**
   - `UserService.ts:45-80`: 함수 길이 35줄 (권장: ≤15줄)
   - 제안: `processOrder` 함수를 3개 함수로 분리

2. **에러 처리 (60/100)**
   - `api/users.ts:23`: catch 블록에서 에러 무시
   - 제안: 최소한 로깅 추가, 적절한 fallback 처리

3. **테스트 (55/100)**
   - 테스트 커버리지: 55% (목표: ≥80%)
   - 누락: `calculateDiscount`, `validateEmail` 함수

### 🔴 즉시 수정 필요
- `auth.ts:15`: SQL 인젝션 취약점 (사용자 입력 직접 쿼리)
  ```typescript
  // ❌ 위험
  db.query(`SELECT * FROM users WHERE email = '${email}'`)

  // ✅ 안전
  db.query('SELECT * FROM users WHERE email = ?', [email])

🎯 우선순위 액션 아이템

  1. [P0] SQL 인젝션 수정 (auth.ts:15)
  2. [P1] 테스트 커버리지 80% 이상으로 증가
  3. [P2] 함수 길이 15줄 이하로 리팩터링

📈 개선 추이

  • 이전 리뷰: C+ (75/100)
  • 현재: B+ (85/100)
  • 개선: +10점 (테스트 추가, 타입 안전성 향상)
code

## 도구 통합
- **ESLint**: 자동 스타일 검사
- **Prettier**: 자동 포맷팅
- **TypeScript**: 타입 안전성
- **Jest/Vitest**: 테스트 커버리지
- **SonarQube** (선택): 정적 분석

## 제약 조건
- 검토 시간: 파일당 평균 5분 이내
- 보고서 길이: 2-3페이지 (마크다운)
- 우선순위: 보안 > 기능 > 성능 > 스타일

## 추가 기능: Pull Request 맥락 리뷰

PR 생성 맥락에서 호출될 때, 다음 추가 검토를 수행합니다:

### PR 특화 분석
1. **변경 영향도 분석**
   - Breaking changes 식별
   - API 호환성 검토
   - 하위 호환성 유지 여부 확인

2. **테스트 범위 제안**
   - 변경된 기능별 테스트 필요성 판단
   - 회귀 테스트 시나리오 제안
   - 통합 테스트 필요 여부 평가

3. **마이그레이션 가이드**
   - API 변경 시 업데이트 방법 제시
   - 버전 업그레이드 절차 안내
   - 사용자 영향도 평가

### PR 리뷰 체크리스트
상세한 PR 리뷰 기준은 `knowledge/pr-review-checklist.md`를 참조합니다.

### 출력 형식 (PR 맥락)
```markdown
## 🔍 PR 영향도 분석

### 💥 Breaking Changes
- [있는 경우] 구체적인 변경사항과 영향받는 API
- [없는 경우] 없음

### 🧪 권장 테스트 범위
- [ ] 새로운 기능 테스트
- [ ] 기존 기능 회귀 테스트
- [ ] 통합 테스트 시나리오

### 📦 영향받는 패키지
- `@package/name`: 변경 유형 및 영향도

사용 시나리오

시나리오 1: 성능 문제가 있는 코드 리뷰

상황: 사용자 목록 필터링 함수의 성능 개선 요청

문제 코드:

typescript
// src/services/userService.ts
function getActiveUsers(users: User[], departmentId: string): User[] {
  const result = [];
  for (let i = 0; i < users.length; i++) {
    if (users[i].status === 'active') {
      for (let j = 0; j < users[i].departments.length; j++) {
        if (users[i].departments[j].id === departmentId) {
          result.push(users[i]);
          break;
        }
      }
    }
  }
  return result;
}

리뷰 보고서:

markdown
## 코드 품질 검토: userService.ts

### 🔴 즉시 수정 필요

**1. 성능 문제 (30/100)**
- **위치**: `userService.ts:15-28`
- **문제**: O(n·m) 시간 복잡도 (중첩 루프)
- **영향**: 사용자 1000명, 부서 평균 5개 → 5000회 반복

**개선 방안**:
```typescript
// ✅ O(n) - filter + some 사용
function getActiveUsers(users: User[], departmentId: string): User[] {
  return users.filter(
    user => user.status === 'active' &&
    user.departments.some(dept => dept.id === departmentId)
  );
}

성능 예상:

  • 현재: ~5ms (1000 users)
  • 개선 후: ~1ms (80% 성능 향상)

⚠️ 개선 필요

2. 가독성 (60/100)

  • 변수명 i, j 대신 의미 있는 이름 사용 권장
  • 중첩 루프 대신 고차 함수 사용으로 의도 명확화

3. 타입 안전성 (70/100)

  • User 타입의 departments 배열이 빈 배열일 경우 처리 누락
  • Optional chaining 고려 필요

📊 전체 평가: C (65/100)

🎯 액션 아이템

  1. [P0] 중첩 루프를 filter + some으로 리팩터링
  2. [P1] 엣지 케이스 처리 (빈 배열, undefined)
  3. [P2] 단위 테스트 추가
code

### 시나리오 2: 에러 처리 누락 검토

**상황**: API 호출 함수의 에러 처리 검증

**문제 코드**:
```typescript
// src/api/orderApi.ts
async function createOrder(data: OrderData) {
  const response = await fetch('/api/orders', {
    method: 'POST',
    body: JSON.stringify(data)
  });

  return response.json();
}

리뷰 보고서:

markdown
## 코드 품질 검토: orderApi.ts

### 🔴 즉시 수정 필요

**1. 에러 처리 부재 (20/100)**
- **위치**: `orderApi.ts:8-14`
- **문제점**:
  1. HTTP 상태 코드 확인 없음 (400, 500 등)
  2. 네트워크 에러 처리 없음
  3. JSON 파싱 실패 처리 없음
  4. 로깅 없음 (디버깅 불가)

**보안 위험**:
- 사용자에게 원시 에러 노출 가능
- 재시도 로직 없어 일시적 장애에 취약

**개선 방안**:
```typescript
// ✅ 완전한 에러 처리
async function createOrder(data: OrderData): Promise<Order> {
  try {
    const response = await fetch('/api/orders', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });

    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}));
      logger.error('Order creation failed', {
        status: response.status,
        data: errorData
      });

      throw new OrderCreationError(
        `Failed to create order: ${response.statusText}`,
        response.status,
        errorData
      );
    }

    return await response.json();
  } catch (error) {
    if (error instanceof OrderCreationError) {
      throw error;
    }

    logger.error('Network error during order creation', { error });
    throw new NetworkError('Unable to connect to order service');
  }
}

⚠️ 개선 필요

2. 타입 안전성 (50/100)

  • 반환 타입 명시 없음 → Promise<Order> 추가 필요
  • OrderData 검증 로직 없음

3. 명시적 I/O (40/100)

  • 함수 주석 없음 (JSDoc 필요)
  • 예외 종류 문서화 필요

📊 전체 평가: D (45/100)

🎯 액션 아이템

  1. [P0] 에러 처리 추가 (HTTP 상태, 네트워크, 파싱)
  2. [P0] 커스텀 에러 클래스 정의 (OrderCreationError, NetworkError)
  3. [P1] 로깅 추가
  4. [P1] 반환 타입 명시
  5. [P2] JSDoc 주석 추가
  6. [P2] 재시도 로직 검토 (선택적)
code

### 시나리오 3: 가독성 및 유지보수성 개선

**상황**: 복잡한 비즈니스 로직의 리팩터링 필요성 검토

**문제 코드**:
```typescript
// src/services/discountService.ts
function calculateFinalPrice(p: number, u: any, c: string) {
  let d = 0;
  if (u.type === 'premium' && u.years >= 1) {
    d = 0.15;
  } else if (u.type === 'regular' && u.purchases > 10) {
    d = 0.1;
  }

  if (c === 'SUMMER2024') {
    d += 0.05;
  } else if (c === 'WELCOME') {
    d += 0.1;
  }

  let t = p * (1 - d);

  if (u.birthday && isBirthdayMonth(u.birthday)) {
    t = t * 0.95;
  }

  return t;
}

리뷰 보고서:

markdown
## 코드 품질 검토: discountService.ts

### 🔴 즉시 수정 필요

**1. 가독성 (25/100)**
- **위치**: `discountService.ts:10-32`
- **문제점**:
  1. 변수명 불명확 (`p`, `u`, `c`, `d`, `t`)
  2. 함수 길이 23줄 (권장: ≤15줄)
  3. 비즈니스 로직 혼재 (할인 계산 + 쿠폰 + 생일)
  4. 매직 넘버 다수 (0.15, 0.1, 0.05, 0.95)

**2. 유지보수성 (30/100)**
- 단일 책임 원칙 위반 (5가지 할인 로직 혼재)
- 새로운 할인 규칙 추가 시 함수 수정 필요 (개방-폐쇄 원칙 위반)
- 테스트 어려움 (복잡한 조건 조합)

**3. 타입 안전성 (20/100)**
- `u: any` 사용 (타입 체크 불가)
- 반환 타입 명시 없음

**개선 방안**:
```typescript
// ✅ Step 1: 타입 정의
interface User {
  type: 'premium' | 'regular';
  years: number;
  purchases: number;
  birthday?: Date;
}

interface DiscountRule {
  name: string;
  condition: (user: User) => boolean;
  rate: number;
}

// ✅ Step 2: 할인 규칙 분리
const MEMBERSHIP_DISCOUNTS: DiscountRule[] = [
  {
    name: 'Premium Member',
    condition: (user) => user.type === 'premium' && user.years >= 1,
    rate: 0.15
  },
  {
    name: 'Loyal Customer',
    condition: (user) => user.type === 'regular' && user.purchases > 10,
    rate: 0.1
  }
];

const COUPON_DISCOUNTS: Record<string, number> = {
  'SUMMER2024': 0.05,
  'WELCOME': 0.1
};

const BIRTHDAY_DISCOUNT_RATE = 0.05;

// ✅ Step 3: 함수 분리 및 명확화
function calculateMembershipDiscount(user: User): number {
  const applicableDiscount = MEMBERSHIP_DISCOUNTS.find(
    rule => rule.condition(user)
  );
  return applicableDiscount?.rate ?? 0;
}

function calculateCouponDiscount(couponCode: string): number {
  return COUPON_DISCOUNTS[couponCode] ?? 0;
}

function calculateBirthdayDiscount(user: User): number {
  if (!user.birthday || !isBirthdayMonth(user.birthday)) {
    return 0;
  }
  return BIRTHDAY_DISCOUNT_RATE;
}

function applyDiscount(price: number, discountRate: number): number {
  return price * (1 - discountRate);
}

// ✅ Step 4: 메인 함수 - 5줄 이내로 명확
function calculateFinalPrice(
  price: number,
  user: User,
  couponCode: string
): number {
  const membershipDiscount = calculateMembershipDiscount(user);
  const couponDiscount = calculateCouponDiscount(couponCode);
  const birthdayDiscount = calculateBirthdayDiscount(user);

  const totalDiscount = membershipDiscount + couponDiscount + birthdayDiscount;
  return applyDiscount(price, totalDiscount);
}

개선 효과:

  1. 가독성: 25점 → 95점

    • 명확한 변수명
    • 5줄 이내 함수
    • 의도가 명확한 구조
  2. 유지보수성: 30점 → 90점

    • 새 할인 규칙 추가 시 배열/객체에만 추가
    • 각 함수가 단일 책임
    • 테스트 용이 (각 함수 독립 테스트)
  3. 타입 안전성: 20점 → 100점

    • 강타입 User 인터페이스
    • 모든 함수 타입 명시

⚠️ 추가 권장사항

테스트 코드 예시:

typescript
describe('calculateFinalPrice', () => {
  const premiumUser: User = {
    type: 'premium',
    years: 2,
    purchases: 5,
    birthday: new Date('1990-06-15')
  };

  it('should apply premium discount', () => {
    expect(calculateFinalPrice(100, premiumUser, '')).toBe(85); // 15% off
  });

  it('should stack coupon discount', () => {
    expect(calculateFinalPrice(100, premiumUser, 'SUMMER2024')).toBe(80); // 15% + 5%
  });

  it('should apply birthday discount in June', () => {
    jest.setSystemTime(new Date('2024-06-01'));
    expect(calculateFinalPrice(100, premiumUser, '')).toBe(80.75); // 15% + 5%
  });
});

📊 전체 평가

  • 개선 전: F (25/100)
  • 개선 후: A+ (95/100)
  • 개선: +70점

🎯 액션 아이템

  1. [P0] 변수명 개선 (p→price, u→user, c→couponCode)
  2. [P0] 타입 정의 (User 인터페이스, 반환 타입)
  3. [P0] 함수 분리 (단일 책임 원칙)
  4. [P1] 매직 넘버 상수화
  5. [P1] 할인 규칙 데이터 구조화
  6. [P2] 단위 테스트 추가 (각 할인 함수별)
code

### 시나리오 4: 보안 취약점 발견

**상황**: 사용자 인증 로직의 보안 검토

**문제 코드**:
```typescript
// src/auth/loginController.ts
async function login(req: Request, res: Response) {
  const { email, password } = req.body;

  const user = await db.query(
    `SELECT * FROM users WHERE email = '${email}'`
  );

  if (user && user.password === password) {
    const token = createToken(user.id);
    res.json({ token });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
}

리뷰 보고서:

markdown
## 코드 품질 검토: loginController.ts

### 🚨 심각한 보안 취약점

**1. SQL 인젝션 (CRITICAL)**
- **위치**: `loginController.ts:6-8`
- **심각도**: P0 (즉시 수정 필수)
- **취약점**:
  ```typescript
  // ❌ 사용자 입력 직접 쿼리 삽입
  `SELECT * FROM users WHERE email = '${email}'`

  // 공격 시나리오:
  // email = "' OR '1'='1' --"
  // 결과 쿼리: SELECT * FROM users WHERE email = '' OR '1'='1' --'
  // → 모든 사용자 정보 노출

2. 평문 비밀번호 비교 (CRITICAL)

  • 위치: loginController.ts:11
  • 심각도: P0
  • 취약점:
    • 비밀번호를 평문으로 저장하고 직접 비교
    • 해싱 없음 (bcrypt, argon2 등 미사용)
    • DB 유출 시 모든 비밀번호 노출

3. 타이밍 공격 취약점

  • 위치: loginController.ts:11-17
  • 취약점: 이메일 존재 여부를 응답 시간으로 추측 가능

4. 로깅 부재

  • 로그인 시도 기록 없음 (brute-force 공격 탐지 불가)

개선 방안:

typescript
// ✅ 보안 강화된 로그인
import bcrypt from 'bcryptjs';
import rateLimit from 'express-rate-limit';

// Rate limiting 미들웨어
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15분
  max: 5, // 최대 5번 시도
  message: 'Too many login attempts, please try again later'
});

async function login(req: Request, res: Response) {
  const startTime = Date.now();

  try {
    const { email, password } = req.body;

    // 1. 입력 검증
    if (!email || !password) {
      logger.warn('Login attempt with missing credentials');
      return res.status(400).json({ error: 'Email and password required' });
    }

    // 2. SQL 인젝션 방지 - 파라미터화된 쿼리
    const user = await db.query(
      'SELECT id, email, password_hash FROM users WHERE email = ?',
      [email]
    );

    // 3. 타이밍 공격 방지 - 항상 해시 비교 실행
    const passwordHash = user?.password_hash || '$2a$10$dummyHashForTimingProtection';
    const isValidPassword = await bcrypt.compare(password, passwordHash);

    // 4. 로그인 성공/실패 로깅
    const processingTime = Date.now() - startTime;

    if (user && isValidPassword) {
      logger.info('Successful login', {
        userId: user.id,
        email: user.email,
        ip: req.ip,
        userAgent: req.headers['user-agent']
      });

      const token = await createSecureToken(user.id);

      return res.json({
        token,
        expiresIn: 3600 // 1시간
      });
    } else {
      logger.warn('Failed login attempt', {
        email,
        ip: req.ip,
        reason: !user ? 'user_not_found' : 'invalid_password',
        processingTime
      });

      // 타이밍 공격 방지 - 일정 시간 대기
      const minResponseTime = 1000; // 1초
      if (processingTime < minResponseTime) {
        await sleep(minResponseTime - processingTime);
      }

      return res.status(401).json({
        error: 'Invalid email or password' // 구체적 이유 노출 안 함
      });
    }
  } catch (error) {
    logger.error('Login error', { error, email: req.body.email });
    return res.status(500).json({ error: 'Internal server error' });
  }
}

// 보안 토큰 생성 (JWT with proper settings)
async function createSecureToken(userId: string): Promise<string> {
  return jwt.sign(
    { userId },
    process.env.JWT_SECRET!,
    {
      expiresIn: '1h',
      algorithm: 'HS256'
    }
  );
}

추가 보안 조치:

typescript
// 비밀번호 해싱 (회원가입 시)
async function hashPassword(plainPassword: string): Promise<string> {
  const saltRounds = 12; // bcrypt 권장값
  return await bcrypt.hash(plainPassword, saltRounds);
}

// Rate limiting 적용
app.post('/api/login', loginLimiter, login);

📊 보안 평가

개선 전: F (10/100) - 심각한 보안 취약점

  • SQL 인젝션 가능
  • 평문 비밀번호
  • 타이밍 공격 취약
  • 로깅 없음

개선 후: A (90/100)

  • ✅ SQL 인젝션 방지 (파라미터화)
  • ✅ 비밀번호 해싱 (bcrypt)
  • ✅ 타이밍 공격 방지 (일정 응답 시간)
  • ✅ Rate limiting (brute-force 방지)
  • ✅ 상세 로깅 (감사 추적)

🎯 즉시 조치 필요 (P0)

  1. SQL 쿼리 파라미터화
  2. 비밀번호 해싱 구현 (bcrypt)
  3. Rate limiting 적용
  4. 타이밍 공격 방어
  5. 로그인 시도 로깅

📚 참고 자료

  • OWASP Top 10: A03:2021 - Injection
  • OWASP Top 10: A07:2021 - Identification and Authentication Failures
  • NIST 비밀번호 가이드라인
code

### 시나리오 5: 테스트 코드 품질 검토

**상황**: 단위 테스트의 완성도 평가

**문제 코드**:
```typescript
// src/__tests__/userService.test.ts
describe('UserService', () => {
  it('works', () => {
    const service = new UserService();
    const result = service.createUser({ name: 'John' });
    expect(result).toBeTruthy();
  });
});

리뷰 보고서:

markdown
## 코드 품질 검토: userService.test.ts

### 🔴 테스트 품질 개선 필요

**1. 테스트 커버리지 (20/100)**
- **위치**: `userService.test.ts:3-8`
- **문제점**:
  1. 단 1개의 테스트 케이스 (happy path만)
  2. 엣지 케이스 누락 (빈 이름, 중복 이메일 등)
  3. 에러 케이스 미검증
  4. 테스트 설명 불명확 ("works"는 무엇을 테스트?)

**2. 테스트 품질 (30/100)**
- `toBeTruthy()` 사용 → 구체적 검증 부족
- 테스트 격리 없음 (setup/teardown)
- Mock/Stub 없음 (실제 DB 접근 가능성)

**개선 방안**:
```typescript
// ✅ 완전한 테스트 스위트
describe('UserService', () => {
  let service: UserService;
  let mockRepository: jest.Mocked<UserRepository>;

  // Setup: 각 테스트 전 실행
  beforeEach(() => {
    mockRepository = {
      save: jest.fn(),
      findByEmail: jest.fn(),
      findById: jest.fn()
    } as any;

    service = new UserService(mockRepository);
  });

  // Teardown: 각 테스트 후 실행
  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('createUser', () => {
    const validUserData = {
      name: 'John Doe',
      email: 'john@example.com',
      age: 30
    };

    // Happy Path
    it('should create user with valid data', async () => {
      // Given
      mockRepository.findByEmail.mockResolvedValue(null);
      mockRepository.save.mockResolvedValue({
        id: '123',
        ...validUserData,
        createdAt: new Date()
      });

      // When
      const result = await service.createUser(validUserData);

      // Then
      expect(result).toEqual({
        id: '123',
        name: 'John Doe',
        email: 'john@example.com',
        age: 30,
        createdAt: expect.any(Date)
      });
      expect(mockRepository.save).toHaveBeenCalledWith(
        expect.objectContaining(validUserData)
      );
    });

    // Edge Cases
    it('should trim whitespace from name', async () => {
      mockRepository.findByEmail.mockResolvedValue(null);
      mockRepository.save.mockResolvedValue({ id: '123' } as any);

      await service.createUser({
        ...validUserData,
        name: '  John Doe  '
      });

      expect(mockRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({ name: 'John Doe' })
      );
    });

    it('should normalize email to lowercase', async () => {
      mockRepository.findByEmail.mockResolvedValue(null);
      mockRepository.save.mockResolvedValue({ id: '123' } as any);

      await service.createUser({
        ...validUserData,
        email: 'JOHN@EXAMPLE.COM'
      });

      expect(mockRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({ email: 'john@example.com' })
      );
    });

    // Error Cases
    it('should throw ValidationError for empty name', async () => {
      await expect(
        service.createUser({ ...validUserData, name: '' })
      ).rejects.toThrow(ValidationError);

      await expect(
        service.createUser({ ...validUserData, name: '   ' })
      ).rejects.toThrow('Name cannot be empty');
    });

    it('should throw ValidationError for invalid email', async () => {
      await expect(
        service.createUser({ ...validUserData, email: 'invalid-email' })
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ConflictError for duplicate email', async () => {
      mockRepository.findByEmail.mockResolvedValue({
        id: '999',
        email: 'john@example.com'
      } as any);

      await expect(
        service.createUser(validUserData)
      ).rejects.toThrow(ConflictError);

      await expect(
        service.createUser(validUserData)
      ).rejects.toThrow('Email already exists');
    });

    it('should throw ValidationError for negative age', async () => {
      await expect(
        service.createUser({ ...validUserData, age: -5 })
      ).rejects.toThrow('Age must be positive');
    });

    it('should throw ValidationError for age > 150', async () => {
      await expect(
        service.createUser({ ...validUserData, age: 200 })
      ).rejects.toThrow('Age must be realistic');
    });

    // Boundary Cases
    it('should accept minimum age of 0', async () => {
      mockRepository.findByEmail.mockResolvedValue(null);
      mockRepository.save.mockResolvedValue({ id: '123' } as any);

      await expect(
        service.createUser({ ...validUserData, age: 0 })
      ).resolves.not.toThrow();
    });

    it('should accept maximum age of 150', async () => {
      mockRepository.findByEmail.mockResolvedValue(null);
      mockRepository.save.mockResolvedValue({ id: '123' } as any);

      await expect(
        service.createUser({ ...validUserData, age: 150 })
      ).resolves.not.toThrow();
    });

    // Integration with Repository
    it('should check email uniqueness before saving', async () => {
      mockRepository.findByEmail.mockResolvedValue(null);
      mockRepository.save.mockResolvedValue({ id: '123' } as any);

      await service.createUser(validUserData);

      expect(mockRepository.findByEmail).toHaveBeenCalledWith(
        'john@example.com'
      );
      expect(mockRepository.findByEmail).toHaveBeenCalledBefore(
        mockRepository.save as any
      );
    });

    // Performance
    it('should complete within 100ms', async () => {
      mockRepository.findByEmail.mockResolvedValue(null);
      mockRepository.save.mockResolvedValue({ id: '123' } as any);

      const start = Date.now();
      await service.createUser(validUserData);
      const duration = Date.now() - start;

      expect(duration).toBeLessThan(100);
    });
  });

  // Additional test suites
  describe('updateUser', () => {
    // ... similar comprehensive tests
  });

  describe('deleteUser', () => {
    // ... similar comprehensive tests
  });
});

테스트 커버리지 분석:

markdown
| 카테고리 | 개선 전 | 개선 후 |
|---------|---------|---------|
| 라인 커버리지 | 15% | 95% |
| 브랜치 커버리지 | 0% | 90% |
| 함수 커버리지 | 33% | 100% |
| 테스트 케이스 수 | 1 | 13 |

⚠️ 테스트 작성 원칙

AAA 패턴 준수:

typescript
it('should do something', () => {
  // Arrange (Given): 테스트 준비
  const input = createTestData();

  // Act (When): 실행
  const result = functionUnderTest(input);

  // Assert (Then): 검증
  expect(result).toBe(expected);
});

테스트 케이스 분류:

  1. Happy Path: 정상 동작
  2. Edge Cases: 경계값 (0, 빈 문자열, 최대값)
  3. Error Cases: 예외 상황
  4. Boundary Cases: 최소/최대 허용값
  5. Integration: 의존성 간 상호작용

📊 전체 평가

  • 개선 전: D (30/100)
  • 개선 후: A+ (95/100)
  • 개선: +65점

🎯 액션 아이템

  1. [P0] 엣지 케이스 및 에러 케이스 테스트 추가
  2. [P0] Mock/Stub을 통한 테스트 격리
  3. [P1] AAA 패턴 적용
  4. [P1] 테스트 설명 명확화
  5. [P2] 성능 테스트 추가
  6. [P2] 테스트 커버리지 80% 이상 달성
code

## Knowledge 파일 역할

### quality_rules.yaml
**용도**: 코드 품질 기준의 구조화된 정의

**주요 내용**:
- 각 품질 기준의 가중치 및 임계값
- 자동 검증 가능한 메트릭 정의
- 점수 계산 공식
- 우선순위 매트릭스

**예시**:
```yaml
readability:
  weight: 0.25
  metrics:
    function_length:
      ideal: 15
      warning: 25
      critical: 50
    cyclomatic_complexity:
      ideal: 5
      warning: 10
      critical: 20
    naming_clarity:
      min_length: 3
      banned_names: ['data', 'temp', 'foo', 'bar']

pr-review-checklist.md

용도: Pull Request 리뷰 시 체크리스트

주요 내용:

  • Breaking changes 식별 가이드
  • API 호환성 검토 항목
  • 테스트 범위 제안 템플릿
  • 마이그레이션 가이드 작성 기준

Tools 파일 역할

complexity_checker.ts

용도: 순환 복잡도 자동 계산

사용 예:

bash
$ ts-node tools/complexity_checker.ts src/services/userService.ts

Complexity Report for userService.ts:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Function: createUser
  Lines: 45
  Complexity: 8 ⚠️ (Threshold: 10)

Function: validateEmail (private)
  Lines: 12
  Complexity: 3 ✅

Function: processOrder
  Lines: 67
  Complexity: 15 🔴 (Exceeds threshold!)
  Recommendation: Split into smaller functions
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Overall Score: 72/100
High complexity functions: 1

naming_validator.ts

용도: 네이밍 컨벤션 검증

사용 예:

bash
$ ts-node tools/naming_validator.ts src/**/*.ts

Naming Validation Report:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔴 src/utils/helper.ts:15
  Variable: 'temp'
  Issue: Generic name (banned)
  Suggestion: Use descriptive name like 'sanitizedEmail'

⚠️ src/services/api.ts:23
  Function: 'calc'
  Issue: Abbreviated name
  Suggestion: 'calculate' or 'calculateTotal'

✅ src/models/User.ts
  All names follow conventions
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total Issues: 5
Critical: 2, Warnings: 3

type_coverage.ts

용도: TypeScript 타입 커버리지 분석

사용 예:

bash
$ ts-node tools/type_coverage.ts

Type Coverage Report:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Overall Coverage: 87.5%
Target: 95%

Files with low coverage:
  src/legacy/oldApi.ts: 45% (23 any types)
  src/utils/helpers.ts: 78% (5 implicit any)

Recommendations:
  1. Add explicit types to function parameters
  2. Replace 'any' with proper types or 'unknown'
  3. Enable 'noImplicitAny' in tsconfig.json
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

test_coverage.sh

용도: 테스트 커버리지 자동 분석 및 보고

사용 예:

bash
$ bash tools/test_coverage.sh

Test Coverage Report:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Overall Coverage: 75.2%
Target: 80%

Coverage by Type:
  Lines:      78.5%
  Branches:   72.3% ⚠️
  Functions:  85.0% ✅
  Statements: 77.1%

Uncovered Files:
  src/services/paymentService.ts: 35%
  src/utils/encryption.ts: 50%

Recommendations:
  1. Add tests for payment error scenarios
  2. Test encryption edge cases
  3. Increase branch coverage by 8%
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

다른 리뷰 스킬과의 차이점

vs code-impact-evaluator

code-quality-reviewer (이 스킬):

  • 목적: 코드 자체의 품질 검증 (가독성, 성능, 유지보수성)
  • 대상: 정적 코드 파일 (단일 파일 또는 디렉토리)
  • 분석 방식: 가이드라인 기반 체크리스트
  • 출력: 품질 점수 + 개선 제안

code-impact-evaluator:

  • 목적: Git 변경사항의 영향도 및 리스크 평가
  • 대상: Git diff (commit/branch/staged)
  • 분석 방식: Tree of Thoughts 복잡도 분석
  • 출력: 영향 범위 + 리스크 레벨 + 우선순위 액션

사용 시점 차이

code-quality-reviewer:

  • ✅ 코드 작성 중 품질 확인
  • ✅ PR 리뷰 시 가이드라인 준수 검증
  • ✅ 리팩토링 전/후 품질 측정
  • ✅ CI/CD 파이프라인의 품질 게이트

code-impact-evaluator:

  • ✅ Commit 전 영향도 파악
  • ✅ Branch 머지 전 리스크 평가
  • ✅ 대규모 리팩토링 계획 수립
  • ✅ 변경사항의 우선순위 결정

조합 사용 권장

두 스킬을 함께 사용하면 더 완전한 코드 리뷰가 가능합니다:

bash
# 1단계: 품질 검사 (정적 분석)
./tools/run-quality-check.sh src/
# → 코드 가이드라인 준수 여부 확인

# 2단계: 영향도 분석 (변경 분석)
code-impact-evaluator --mode=branch
# → 변경의 영향 범위 및 리스크 파악

# 3단계: 한글 보고서 생성 (선택)
korean-review-reporter
# → 품질 + 영향도 통합 보고서

각 스킬을 선택하는 기준

code-quality-reviewer를 사용하는 경우:

  • 코드가 프로젝트 가이드라인을 따르는지 확인하고 싶을 때
  • 복잡도, 함수 길이, 네이밍 등 코드 품질을 측정하고 싶을 때
  • Git 변경사항과 무관하게 코드 자체를 평가하고 싶을 때

code-impact-evaluator를 사용하는 경우:

  • Git commit/branch의 영향도를 파악하고 싶을 때
  • 변경사항의 리스크 레벨을 평가하고 싶을 때
  • 어떤 컴포넌트/모듈이 영향을 받는지 알고 싶을 때

두 스킬 모두 사용하는 경우:

  • PR 리뷰 시 품질과 영향도를 종합적으로 평가하고 싶을 때
  • 대규모 리팩토링의 품질과 리스크를 동시에 검증하고 싶을 때

에러 처리

yaml
error_handling:
  severity_high:
    conditions:
      - 소스 코드 파일 읽기 실패 (권한 문제)
      - 코드 파싱 불가 (심각한 구문 오류)
      - 필수 도구 미설치 (ESLint, TypeScript compiler)
      - knowledge/quality_rules.yaml 파일 누락
    action: |
      ❌ 치명적 오류 - 코드 리뷰 중단
      → 파일 권한 확인: ls -la {file_path}
      → 구문 검사: npx tsc --noEmit {file_path}
      → ESLint 설치: yarn add -D eslint
      → quality_rules.yaml 존재 확인
      → 재실행 명령어: ./tools/run-quality-check.sh {file_path}
    examples:
      - condition: "파일 읽기 실패"
        message: "❌ 오류: src/component.tsx를 읽을 수 없습니다 (Permission denied)"
        recovery: "파일 권한 확인: chmod +r src/component.tsx"
      - condition: "심각한 구문 오류"
        message: "❌ 오류: TypeScript 파싱 실패 (line 45: Unexpected token)"
        recovery: "구문 오류 수정 후 재실행: npx tsc --noEmit"

  severity_medium:
    conditions:
      - 코드 복잡도 계산 실패 (부분적 파싱 문제)
      - 타입 커버리지 측정 실패
      - ESLint 실행 실패 (설정 문제)
      - 일부 메트릭 수집 불가 (특정 패턴 미감지)
    action: |
      ⚠️  경고 - 부분적 리뷰 진행
      1. 실패한 메트릭을 N/A로 표시
      2. 수집 가능한 메트릭만으로 품질 점수 계산
      3. 리뷰 보고서에 경고 추가:
         > ⚠️  WARNING: 일부 메트릭을 수집할 수 없었습니다
         > → 다음 메트릭 누락: {missing_metrics}
      4. 수동 검토 권장 항목 표시
    fallback_values:
      complexity_score: "N/A"
      type_coverage: "N/A"
      overall_score: "calculated from available metrics only"
    examples:
      - condition: "타입 커버리지 측정 실패"
        message: "⚠️  경고: TypeScript 타입 분석 실패 (tsconfig.json 누락)"
        fallback: "타입 커버리지 N/A 처리 → 나머지 메트릭으로 점수 계산"
      - condition: "ESLint 실행 실패"
        message: "⚠️  경고: ESLint 설정 파일을 찾을 수 없습니다"
        fallback: "Lint 메트릭 생략 → 수동 스타일 검토 권장"

  severity_low:
    conditions:
      - 선택적 메트릭 수집 실패 (주석 품질 등)
      - 코드 포맷팅 자동 수정 가능
      - 경미한 네이밍 컨벤션 위반
      - 불필요한 공백/줄바꿈
    action: |
      ℹ️  정보: 경미한 문제 감지 - 자동 제안 포함
      → 자동 수정 가능 항목은 제안으로 제공
      → Prettier 실행 권장
      → 품질 점수에 미미한 영향 (감점 5점 이하)
    examples:
      - condition: "주석 품질 측정 불가"
        auto_handling: "주석 메트릭 생략 (선택적 항목)"
      - condition: "포맷팅 문제"
        auto_handling: "Prettier 자동 수정 제안: npx prettier --write {file}"

Best Practice: 긍정적 피드백도 함께 제공 (무엇이 좋았는지) Integration: Pull Request 워크플로우와 연동