diff --git a/.claude/agents/logic-reviewer.md b/.claude/agents/logic-reviewer.md deleted file mode 100644 index 8c3dacc..0000000 --- a/.claude/agents/logic-reviewer.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: logic-reviewer -description: 사용자가 구현하고자 하는 기능과 실제 코드가 일치하는지 검증한다. Controller → Service → Repository 흐름을 따라가며 의도와 구현의 정합성을 점검한다. "로직 리뷰해줘", "코드 리뷰해줘" 등의 요청 시 사용. -tools: Read, Grep, Glob -model: sonnet ---- - -USS 프로젝트의 로직 리뷰어다. Layered Architecture (Controller → Service → Repository) 구조를 따르는 프로젝트다. -**Facade 레이어가 없다.** 여러 도메인의 데이터가 필요하면 Service가 해당 도메인 Repository를 직접 주입해 접근한다. - -## Phase 1: 의도 파악 - -1. 사용자가 지정한 리뷰 대상 파일을 확인하라 -2. 의도가 불명확하면 사용자에게 "어떤 기능을 구현한 코드인지" 물어라 - -> 다음 Phase 조건: 구현 의도가 파악되었을 때 - -> Skip 조건: 사용자가 "이 파일 로직 리뷰해줘"처럼 의도를 별도로 전달한 경우 - -## Phase 2: 코드 흐름 추적 - -1. Controller에서 시작하여 해당 엔드포인트를 찾아라 -2. Controller → Service → Repository 순서로 호출 체인을 따라가며 각 파일을 Read하라 -3. 각 레이어에서 수행하는 동작을 정리하라 - -> 다음 Phase 조건: 전체 호출 체인을 끝까지 추적했을 때 - -> Skip 조건: 리뷰 대상이 단일 Service 메서드이고 Controller가 관련 없는 경우 — 해당 Service만 읽고 Phase 3으로 진행 - -## Phase 3: 점검 - -다음 항목을 점검하라: - -- 의도 vs 구현 불일치: 사용자가 원하는 기능과 실제 구현된 동작이 다른 경우 -- 서비스 정책 위반: `.claude/spec/service-policy/`의 해당 도메인 파일과 구현이 어긋나는 경우 - (어느 파일인지는 같은 디렉토리의 `README.md` 목록에서 찾는다) -- 누락된 엣지 케이스: 의도에는 포함되지만 구현에서 빠진 분기 처리 -- 검증 순서 누락: 신청·담기처럼 검증이 여러 개인 흐름에서 검증 하나가 빠지거나 순서가 뒤바뀐 경우 - (`CartService.addCart`, `RegistrationService.registerCourse`가 기준 패턴이다) -- 레이어 의존성 위반: Repository가 Service를 참조하는 역방향 의존 -- Service 간 직접 의존: Service가 다른 Service를 주입하는 경우 (해당 도메인 Repository를 주입해야 한다) -- Controller에 비즈니스 로직이 포함된 경우 -- 예외 처리 누락: Repository 조회 결과가 없을 때 `RestApiException` 대신 null/빈값 반환 -- 트랜잭션 정합성: 여러 쓰기 작업이 하나의 트랜잭션으로 묶여야 하는데 분리된 경우 -- 카운터 정합성: `Course.currentEnrollment`처럼 증감으로 관리되는 값이 신청·취소 양쪽에서 짝을 이루는지 -- 코드 패턴 위반: - - Entity·Response DTO 생성 시 정적 팩토리(`create()` / `of()` / `from()`) 대신 생성자·Builder 직접 호출 - - DTO가 record가 아닌 class로 작성된 경우 - - `ExceptionCode`를 static import 없이 `ExceptionCode.XXX`로 표기한 경우 - -> 다음 Phase 조건: 모든 점검 항목을 확인했을 때 - -> Skip 조건: 없음 (필수 Phase) - -## Phase 4: 결과 보고 - -각 이슈에 대해 다음을 출력하라: -1. 위치 (파일:라인) -2. 문제 설명 (한 줄) -3. 개선 방안 - -이슈가 없으면 "로직 이슈 없음"이라고 보고하라. diff --git a/.claude/agents/performance-reviewer.md b/.claude/agents/performance-reviewer.md deleted file mode 100644 index 87d8041..0000000 --- a/.claude/agents/performance-reviewer.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: performance-reviewer -description: 성능 관점에서 코드를 리뷰한다. N+1 쿼리, 불필요한 DB 호출, 트랜잭션 범위, 인덱스 누락 등을 점검한다. "성능 리뷰해줘", "쿼리 성능 확인해줘" 등의 요청 시 사용. -tools: Read, Grep, Glob, Bash(git diff*) -model: sonnet ---- - -USS 프로젝트의 성능 리뷰어다. Java 17 / Spring Boot 4.0.1 / JPA / MySQL(InnoDB) 기반 프로젝트다. -수강신청 시뮬레이터라 **대용량 조회와 동시 신청이 핵심 부하**다. 조회 API는 강의 목록 전체를 훑고, -신청 API는 같은 강의 행에 쓰기가 몰린다. - -## Phase 1: 대상 파악 - -1. 사용자가 지정한 리뷰 대상 파일을 Read로 읽어라 -2. 대상이 지정되지 않았으면 `git diff --name-only`로 최근 변경 파일 중 Service, Repository, Entity 파일을 우선 대상으로 선정하라 - -> 다음 Phase 조건: 리뷰 대상 파일 목록이 확정되었을 때 - -> Skip 조건: 없음 (필수 Phase) - -## Phase 2: 쿼리/DB 점검 - -1. N+1 쿼리: `@OneToMany`, `@ManyToOne` 관계에서 Lazy Loading으로 인한 N+1 발생 여부를 확인하라 - - `Course.courseSchedules`가 대표 지점이다. `LEFT JOIN FETCH`(`findByCourseDepartment`, `findByIdWithSchedules`) - 또는 `@BatchSize`로 막고 있는지, 새 조회 경로가 그 방어를 우회하지 않는지 확인하라 - - `Cart.course`, `Registration.course`처럼 컬렉션을 순회하며 연관 Entity를 꺼내는 코드는 fetch join 여부를 확인하라 -2. 불필요한 DB 호출: 루프 안에서 Repository 호출, 같은 데이터를 중복 조회하는 코드를 찾아라 - - 이미 조회한 컬렉션으로 판정할 수 있는데 다시 `existsBy...`를 호출하는 경우도 여기에 해당한다 -3. 인덱스 누락: WHERE 절이나 JOIN에 사용되는 컬럼에 인덱스가 있는지 - `src/main/resources/database/migration/` 파일들에서 확인하라 - - 인덱스는 테이블 정의 안에 `INDEX idx_{용도} (컬럼)`으로 인라인 선언되어 있다 - - 전문 검색은 `FULLTEXT INDEX ... WITH PARSER ngram`이다. `LIKE '%keyword%'`로 대체된 코드가 있으면 지적하라 -4. 페이징 없이 대량 데이터를 전체 조회하는 경우를 찾아라 -5. `nativeQuery = true`가 FULLTEXT 등 DB 종속 기능이 아닌 곳에 쓰였는지 확인하라 - -> 다음 Phase 조건: 쿼리/DB 관련 점검이 완료되었을 때 - -> Skip 조건: 리뷰 대상에 Repository, Entity 파일이 없고 DB 호출 코드도 없는 경우 - -## Phase 3: 트랜잭션/동시성 점검 - -1. `@Transactional`이 불필요하게 넓은 범위에 걸려 있는 경우를 찾아라 -2. 조회 메서드에 `@Transactional(readOnly = true)`가 빠져 있는지 확인하라 -3. 외부 API 호출이나 파일 I/O가 트랜잭션 안에 포함되어 있는지 확인하라 - - `EmailSender` 호출이 쓰기 트랜잭션 안에 있는 구간이 대표 사례다 -4. 동시 수강신청 경합: 정원 검사 후 증가시키는 흐름(`validateCourseCapacity` → `incrementEnrollment`)처럼 - 조회-판정-갱신이 분리된 코드가 있으면 초과 등록 가능성을 지적하라 - -> 다음 Phase 조건: 트랜잭션/동시성 점검이 완료되었을 때 - -> Skip 조건: 리뷰 대상에 `@Transactional`을 사용하는 Service 파일이 없는 경우 - -## Phase 4: 결과 보고 - -각 이슈에 대해 다음을 출력하라: -1. 위치 (파일:라인) -2. 문제 설명 (한 줄) -3. 개선 방안 - -이슈가 없으면 "성능 이슈 없음"이라고 보고하라. - -측정 없이 단정하지 마라. 수치가 필요한 판정은 `optimize-performance` 스킬을 권하고 넘겨라. diff --git a/.claude/agents/security-reviewer.md b/.claude/agents/security-reviewer.md deleted file mode 100644 index 12beb77..0000000 --- a/.claude/agents/security-reviewer.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -name: security-reviewer -description: 보안 관점에서 코드를 리뷰한다. 인증/인가 우회, 시크릿 노출, SQL Injection, 입력 검증 누락 등을 점검한다. "보안 리뷰해줘", "보안 점검해줘" 등의 요청 시 사용. -tools: Read, Grep, Glob, Bash(git diff*) -model: sonnet ---- - -USS 프로젝트의 보안 리뷰어다. **Spring Security의 필터체인을 쓰지 않는다.** -직접 등록한 서블릿 필터(`JwtAuthenticationFilter`)로 JWT를 검증하고, -`AuthArgumentResolver`가 `@Auth` 파라미터에 회원 식별자를 주입하는 커스텀 인증 구조다. -회원가입은 이메일 인증(`EmailVerificationCode`)을 거친다. - -인증 구조상 **인증이 필요한 경로를 명시하는 곳이 없다.** `WhitelistEndpoint`에 없는 모든 경로가 인증 대상이므로, -화이트리스트에 잘못 추가되는 것이 곧 인증 우회다. - -## Phase 1: 대상 파악 - -1. 사용자가 지정한 리뷰 대상 파일을 Read로 읽어라 -2. 대상이 지정되지 않았으면 `git diff --name-only`로 최근 변경 파일을 확인하고, - 보안 관련 파일(`auth/`, `global/config/`, `global/http/WhitelistEndpoint.java`, Controller, `application*.yml`)을 - 우선 대상으로 선정하라 - -> 다음 Phase 조건: 리뷰 대상 파일 목록이 확정되었을 때 - -> Skip 조건: 없음 (필수 Phase) - -## Phase 2: 인증/인가 점검 - -1. `WhitelistEndpoint.WHITELIST`에 새로 추가된 경로가 정말 인증 없이 열려야 하는 경로인지 확인하라 - - `httpMethod`가 `null`이면 모든 메서드가 열린다. Swagger 경로 외에 `null`을 쓴 항목이 있으면 지적하라 - - `/**` 패턴은 `startsWith` 접두 매칭이다. 접두가 짧으면 의도보다 넓게 열린다 -2. 새 엔드포인트가 인증을 필요로 하는데 `@Auth`로 회원 식별자를 받지 않는 경우를 확인하라 -3. 사용자 식별 시 `@Auth`로 주입된 `memberId` 대신 클라이언트가 전달한 memberId(RequestParam, PathVariable, RequestBody)를 - 신뢰하는 코드가 있는지 Grep으로 검색하라 -4. 자원 소유권 검증: 다른 사람의 자원을 식별자만으로 조회·삭제할 수 있는지 확인하라 - - 조회·삭제 쿼리가 `memberId`와 함께 조회하는지가 기준이다 - (`findByMemberIdAndCourseId`가 기준 패턴, `findById`만으로 삭제하면 위반) - -> 다음 Phase 조건: 인증/인가 관련 점검이 완료되었을 때 - -> Skip 조건: 리뷰 대상에 Controller, `WhitelistEndpoint`, `auth/` 관련 파일이 없는 경우 - -## Phase 3: 데이터 보안 점검 - -1. 시크릿 노출: 코드나 설정 파일에 JWT 시크릿, DB 비밀번호, 메일 계정이 하드코딩되어 있는지 Grep으로 검색하라 - - 값은 `application-{profile}.yml`에 CD 워크플로가 주입한다. 규칙은 `.claude/spec/secret-convention.md`를 따른다 -2. SQL Injection: `nativeQuery = true` 쿼리에 문자열 연결로 파라미터를 넣는 코드가 있는지 확인하라 - (`@Param` 바인딩을 사용해야 한다). `CourseRepository.findByKeyword`가 유일한 native 쿼리다 -3. 입력 검증 누락: Request DTO에 `@NotNull`, `@NotBlank` 등이 빠져 있는지, Controller에 `@Valid`가 붙어 있는지 확인하라 - - RequestParam은 `@ParamValidation`(길이 등) / `@EnumValidation`으로 검증한다 -4. CORS 설정: `CorsConfig`에 와일드카드(`*`) origin 허용이 추가되었는지 확인하라 - (`allowCredentials(true)`와 함께 쓰면 특히 위험하다) -5. 응답에 민감 정보가 실리는지 확인하라 (비밀번호 해시, 인증코드, 다른 회원의 개인정보) -6. 인증코드·비밀번호 시도 횟수 제한이 우회 가능한지 확인하라 - (`resendCount`, `failedCount` 상한이 검증 순서상 실제로 걸리는지) - -> 다음 Phase 조건: 데이터 보안 점검이 완료되었을 때 - -> Skip 조건: 리뷰 대상이 `auth/` 파일만이고 DTO나 Repository 변경이 없는 경우 — Phase 3의 2~3번만 스킵 - -## Phase 4: 결과 보고 - -각 이슈에 대해 다음을 출력하라: -1. 심각도 (Critical / Warning) -2. 위치 (파일:라인) -3. 문제 설명 (한 줄) -4. 개선 방안 - -이슈가 없으면 "보안 이슈 없음"이라고 보고하라. diff --git a/.claude/resources/plans/PLAN-94.md b/.claude/resources/plans/PLAN-94.md new file mode 100644 index 0000000..2a703ce --- /dev/null +++ b/.claude/resources/plans/PLAN-94.md @@ -0,0 +1,138 @@ +# [PLAN-94] 리뷰 에이전트 제거와 스킬 구조 정리 + +> 이슈: #94 +> 브랜치: chore/94-claude-tooling-cleanup + +## 목표 +실제 작업 흐름에서 쓰이지 않는 리뷰 에이전트 3종과 `write-api-docs` 스킬을 제거하고, `run-test`를 `write-test`에 병합해 Claude 도구 구성을 슬림화한다. 삭제 대상을 참조하던 다른 스킬 문서의 끊어진 링크를 함께 정리한다. + +## 영향 범위 +### 삭제 파일 +- `.claude/agents/logic-reviewer.md` — 리뷰 에이전트 +- `.claude/agents/security-reviewer.md` — 리뷰 에이전트 +- `.claude/agents/performance-reviewer.md` — 리뷰 에이전트 +- `.claude/agents/` — 위 3개 삭제 후 빈 디렉토리 제거 +- `.claude/skills/run-test/SKILL.md` — write-test로 병합 +- `.claude/skills/write-api-docs/SKILL.md` — 스킬 폐기 +- `.claude/skills/write-api-docs/template/api-docs-template.md` — 스킬 폐기 (코드 골격은 아래 3번대로 스펙에 이관) + +> 주의: `write-api-docs` 두 파일은 **이미 index에 삭제로 스테이징**되어 있으나 작업 트리에는 untracked로 남아 있다(`git status --short` → `D`/`??`). 파일 시스템에서 실제로 지워야 정리가 끝난다. `rm -rf`는 settings.json deny 목록이므로 파일 단위 `rm` + `rmdir`로 지운다. + +### 수정 파일 +- `.claude/spec/api-docs-convention.md` — 폐기되는 템플릿의 인터페이스 골격을 코드 블록으로 흡수 (500 응답 블록 제외) +- `.claude/skills/write-test/SKILL.md` — run-test 병합 (frontmatter description·allowed-tools, Phase 5 확장) +- `.claude/skills/implement/template/output.md` — 19행 `**다음**: write-test → run-test → open-pr` → `write-test → open-pr` +- `.claude/skills/optimize-performance/SKILL.md` — 6행 `Do NOT use for`에서 `코드만 보고 하는 정적 성능 리뷰(→ performance-reviewer)` 제거 +- `.claude/skills/fix-concurrency/SKILL.md` — 6행 `Do NOT use for`에서 `코드만 보고 하는 정적 리뷰(→ performance-reviewer)` 제거 +- `.claude/skills/review-feedback/SKILL.md` — 6행 `Do NOT use for`의 `자체 코드 리뷰(→ logic-reviewer 등 리뷰 에이전트)` 제거 + +## 구현 계획 +> `.claude/` 도구 정리 작업이라 애플리케이션 레이어(Entity/Repository/Service/DTO/Controller) 변경과 DB 마이그레이션은 없다. 서비스 정책(`.claude/spec/service-policy/`) 변경도 없다. + +### 1. 에이전트 삭제 +`.claude/agents/` 하위 3개 파일을 삭제하고 디렉토리를 제거한다. +```bash +rm .claude/agents/logic-reviewer.md .claude/agents/security-reviewer.md .claude/agents/performance-reviewer.md +rmdir .claude/agents +``` + +### 2. write-api-docs 스킬 삭제 +```bash +rm .claude/skills/write-api-docs/SKILL.md .claude/skills/write-api-docs/template/api-docs-template.md +rmdir .claude/skills/write-api-docs/template .claude/skills/write-api-docs +``` +API 문서 작성 규칙 자체는 `.claude/spec/api-docs-convention.md`에 남으며 CLAUDE.md의 참조도 유지한다. Docs 인터페이스 작성은 계획서에 명시되면 `implement`가 이 스펙을 읽고 수행한다(PLAN-92의 `AuthControllerDocs` 작업이 실제로 그렇게 진행됐다). + +### 3. 인터페이스 골격을 api-docs-convention.md로 이관 +`.claude/spec/api-docs-convention.md` 맨 끝(`## DTO @Schema` 섹션 뒤)에 `## 인터페이스 골격` 섹션을 추가하고, 폐기되는 템플릿의 코드 블록을 옮긴다. 단 **500 응답 `@ApiResponse` 블록은 옮기지 않는다** — 기존 Docs 인터페이스 9개 전부가 500을 선언하지 않아 템플릿 쪽이 코드와 어긋난 상태였다. 옮길 골격은 다음과 같다. + +```java +package uss.code.{domain}.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import uss.code.auth.annotation.Auth; +import uss.code.global.exception.dto.response.ErrorResponse; + +@Tag(name = "{Domain} API", description = "{도메인} 관련 API") +public interface {Controller}Docs { + + @Operation(summary = "{기능 요약}", description = "{상세 설명}
" + + "🔐 Jwt 필요
") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "✅ {성공 메시지}"), + @ApiResponse(responseCode = "404", description = "🚨 {에러 설명}", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + examples = { + @ExampleObject( + name = "{에러명}", + value = "{\"code\" : {코드}, \"message\" : \"에러 메시지\"}" + ) + }, + schema = @Schema(implementation = ErrorResponse.class)) + ) + }) + ResponseEntity<{ResponseType}> methodName(@Auth final long memberId); +} +``` + +### 4. run-test → write-test 병합 +`.claude/skills/write-test/SKILL.md`를 다음과 같이 고친다. + +**frontmatter** +- `description`의 `Trigger`에 run-test 트리거를 흡수: `"테스트 돌려줘"`, `"테스트 실행해줘"`, `"테스트 결과 확인해줘"` 추가 +- `description`의 `Do NOT use for`에서 `테스트 실행/결과 확인(→ run-test)` 제거 +- `allowed-tools`의 `Bash`는 그대로 둔다 (`./gradlew test` 실행 필요) + +**본문 상단 인자 형식** +- `<클래스명>` 단일 형식에서, 인자가 이미 존재하는 테스트 클래스이거나 비어 있으면 작성 단계를 건너뛰고 실행 단계로 진입한다는 분기를 명시 + +**Phase 1~4** +- Skip 조건에 "실행만 요청된 경우(대상 테스트 클래스가 이미 존재하거나 인자가 비어 있음) — Phase 5로 직행" 추가 + +**Phase 5: 테스트 실행 (run-test Phase 1~3 흡수)** +1. 인자가 비어 있으면 `./gradlew test`, 있으면 `./gradlew test --tests "{패키지}.{클래스}"` +2. 전부 통과하면 한 줄 보고 후 종료 +3. 실패 시 실패 로그에서 `클래스명#메서드명` 추출, 에러 메시지와 스택 트레이스로 핵심 원인 파악, 필요하면 실패 테스트 소스를 Read +4. 실패 원인이 **이번에 작성한 테스트 코드** 쪽이면 수정 후 재실행을 반복한다 +5. 실패 원인이 **테스트 대상(프로덕션) 코드의 버그**면 원인과 수정 방안을 보고만 하고 사용자 확인 없이 고치지 마라 (스킬 Boundary와 동일) +6. 최종 결과 보고: 실행 대상, 통과/실패 수, 실패 시 원인 요약 + +> 위 5번 항목은 기존 write-test Phase 5의 "실패하면 분석하고 수정하라"와 run-test의 "사용자 확인 없이 수정하지 마라"가 충돌하는 지점을 정리한 것이다. 스킬이 방금 쓴 테스트는 스킬 책임이므로 고치고, 대상 코드 버그는 write-test의 기존 Boundary대로 보고만 한다. + +### 5. 끊어진 참조 정리 +| 파일 | 현재 | 변경 후 | +|---|---|---| +| `implement/template/output.md:19` | `**다음**: \`write-test\` → \`run-test\` → \`open-pr\`` | `**다음**: \`write-test\` → \`open-pr\`` | +| `optimize-performance/SKILL.md:6` | `Do NOT use for: 코드만 보고 하는 정적 성능 리뷰(→ performance-reviewer), 구현 계획 수립(→ write-plan), 계획 기반 구현(→ implement)` | `Do NOT use for: 구현 계획 수립(→ write-plan), 계획 기반 구현(→ implement)` | +| `fix-concurrency/SKILL.md:6` | `Do NOT use for: 응답시간, 처리량이 목표인 성능 개선(→ optimize-performance), 코드만 보고 하는 정적 리뷰(→ performance-reviewer), 구현 계획 수립(→ write-plan)` | `Do NOT use for: 응답시간, 처리량이 목표인 성능 개선(→ optimize-performance), 구현 계획 수립(→ write-plan)` | +| `review-feedback/SKILL.md:6` | `Do NOT use for: 자체 코드 리뷰(→ logic-reviewer 등 리뷰 에이전트), PR 생성(→ open-pr), 계획 기반 구현(→ implement)` | `Do NOT use for: PR 생성(→ open-pr), 계획 기반 구현(→ implement)` | + +`.claude/CLAUDE.md`는 에이전트나 삭제 대상 스킬을 참조하지 않으므로 수정하지 않는다. `.claude/settings.json`과 `.claude/hooks/`도 참조가 없다. +`.claude/resources/plans/PLAN-92.md`의 `api-docs-convention.md` 언급은 과거 작업 기록이므로 건드리지 않는다. + +## 결정 필요 (Decisions needed) +- [x] **`write-api-docs/template/api-docs-template.md`의 코드 골격 처리** — A) 그대로 폐기 / B) `.claude/spec/api-docs-convention.md`로 이관 + → **B 확정.** 위 3번대로 이관한다. 단 템플릿의 500 응답 블록은 실제 Docs 인터페이스 9개 중 어디에도 없어 코드와 어긋나므로 옮기지 않는다. +- [x] **`write-test`의 실행 전용 모드 지원 범위** — A) 인자 비었거나 기존 테스트 클래스면 작성 건너뛰고 실행만 / B) 작성 전용 유지하고 "테스트 돌려줘"는 스킬 없이 처리 + → **A 확정.** 위 4번 구현 계획이 A 기준으로 작성되어 있다. + +## 검증 +- 실행할 애플리케이션 테스트 없음 (`src/` 변경 없음). 필요 시 `./gradlew test`로 회귀만 확인. +- 문서 정합성 확인: + - `grep -rn "run-test\|write-api-docs\|logic-reviewer\|security-reviewer\|performance-reviewer" .claude --include="*.md"` 결과가 `PLAN-*.md`(과거 기록)와 `PLAN-94.md` 외에 남지 않을 것 + - `git status --short`에 `.claude/skills/write-api-docs/` untracked 항목이 사라질 것 + - `.claude/agents/`, `.claude/skills/run-test/`, `.claude/skills/write-api-docs/` 디렉토리가 존재하지 않을 것 + +## Deviation Log +- `.claude/spec/api-docs-convention.md`: 이관한 골격 코드 블록 아래에 `- 선언할 @ApiResponse는 해당 엔드포인트에서 실제로 발생하는 응답만이다. 500은 선언하지 마라` 한 줄을 추가 — 이유: 계획은 500 블록을 "옮기지 않는다"까지만 지시했다. 제외 이유를 남기지 않으면 다음 작성자가 관성으로 500을 다시 넣는다. +- `.claude/skills/write-test/SKILL.md`: H1을 `테스트 코드 작성` → `테스트 코드 작성과 실행`, description 첫 줄을 `테스트 코드를 작성한다` → `테스트 코드를 작성하고 실행한다`로 변경 — 이유: 계획은 Trigger 목록과 Phase만 명시했으나, 스킬이 실행까지 담당하게 된 이상 제목과 한 줄 요약이 범위를 그대로 드러내야 한다. +- `.claude/skills/write-test/SKILL.md`: `Do NOT use for`에 run-test에 있던 `빌드 설정 변경`을 흡수 — 이유: 계획은 `테스트 실행/결과 확인(→ run-test)` 제거만 지시했다. run-test가 사라지면서 이 경계 문구를 받아줄 곳이 write-test뿐이다. +- `.claude/skills/write-test/SKILL.md`: Phase 4의 `최종 결과를 사용자에게 보고하라` → `작성 결과를 정리하라`로 변경하고 `다음 Phase 조건`을 추가 — 이유: 실행(Phase 5) 앞에서 "최종 보고"를 하면 최종 보고가 두 번 나온다. diff --git a/.claude/skills/fix-concurrency/SKILL.md b/.claude/skills/fix-concurrency/SKILL.md index a814070..232d8ab 100644 --- a/.claude/skills/fix-concurrency/SKILL.md +++ b/.claude/skills/fix-concurrency/SKILL.md @@ -3,7 +3,7 @@ name: fix-concurrency description: | 동시 요청에서 깨지는 불변식을 재현하고, 동시성 제어 기법을 비교 검증해 하나를 채택한다. 기법 선택과 부하 실행은 호출자가 한다. Trigger: "/fix-concurrency {대상}", "동시성 문제 해결하자", "정원 초과 막자", "락 걸어서 정합성 맞추자" - Do NOT use for: 응답시간, 처리량이 목표인 성능 개선(→ optimize-performance), 코드만 보고 하는 정적 리뷰(→ performance-reviewer), 구현 계획 수립(→ write-plan) + Do NOT use for: 응답시간, 처리량이 목표인 성능 개선(→ optimize-performance), 구현 계획 수립(→ write-plan) Boundary: 불변식 정의, 경합 재현, 기법 제시, 후보별 적용과 검증, 채택 결정 기록까지 수행한다. 부하 테스트와 데이터베이스 조회 실행은 호출자가 직접 한다. 어떤 기법을 채택할지는 스킬이 단독으로 정하지 않는다. allowed-tools: Read, Grep, Glob, Edit, Write, Skill, Bash(git *), Bash(gh *) model: opus diff --git a/.claude/skills/implement/template/output.md b/.claude/skills/implement/template/output.md index 4c07c9c..f1a8374 100644 --- a/.claude/skills/implement/template/output.md +++ b/.claude/skills/implement/template/output.md @@ -16,4 +16,4 @@ **정책 문서**: {갱신한 service-policy 파일과 바뀐 규칙, 없으면 "변경 없음"} **이슈**: {Deviation Log 한 줄 요약, 없으면 "없음"} -**다음**: `write-test` → `run-test` → `open-pr` +**다음**: `write-test` → `open-pr` diff --git a/.claude/skills/optimize-performance/SKILL.md b/.claude/skills/optimize-performance/SKILL.md index faca63c..ec178bf 100644 --- a/.claude/skills/optimize-performance/SKILL.md +++ b/.claude/skills/optimize-performance/SKILL.md @@ -3,7 +3,7 @@ name: optimize-performance description: | 지정한 API의 성능을 측정, 진단하고 개선 기법을 근거와 함께 제시한다. 기법 선택과 부하 테스트 실행은 호출자가 한다. Trigger: "/optimize-performance {엔드포인트}", "이 API 성능 개선하자", "느린 API 최적화하자" - Do NOT use for: 코드만 보고 하는 정적 성능 리뷰(→ performance-reviewer), 구현 계획 수립(→ write-plan), 계획 기반 구현(→ implement) + Do NOT use for: 구현 계획 수립(→ write-plan), 계획 기반 구현(→ implement) Boundary: 측정 설계, 관측 결과 정리, 기법 제시, 호출자와의 설계 협의, 확정된 설계의 적용, 기록까지 수행한다. 부하 테스트와 DB 조회 실행은 호출자가 직접 한다. 무엇이 병목인지, 어떤 기법을 쓸지, 어떻게 설계할지는 스킬이 단독으로 정하지 않는다. allowed-tools: Read, Grep, Glob, Edit, Write, Skill, Bash(git *), Bash(gh *) model: opus diff --git a/.claude/skills/review-feedback/SKILL.md b/.claude/skills/review-feedback/SKILL.md index 59b6cde..557ba22 100644 --- a/.claude/skills/review-feedback/SKILL.md +++ b/.claude/skills/review-feedback/SKILL.md @@ -3,7 +3,7 @@ name: review-feedback description: | PR에 달린 코드래빗 피드백을 수집해 코드베이스 기준으로 타당성을 상중하로 판정하고, 사용자가 고른 항목만 수정한다. Trigger: "코드래빗 피드백 봐줘", "리뷰 피드백 검토해줘", "PR 리뷰 확인해줘", "코드래빗 뭐라는지 봐줘" - Do NOT use for: 자체 코드 리뷰(→ logic-reviewer 등 리뷰 에이전트), PR 생성(→ open-pr), 계획 기반 구현(→ implement) + Do NOT use for: PR 생성(→ open-pr), 계획 기반 구현(→ implement) Boundary: 피드백 판정, 사용자가 선택한 항목의 수정, 반영한 항목의 스레드 resolve까지 수행한다. 판정만으로 코드를 고치지 않는다. allowed-tools: Read, Grep, Glob, Edit, Bash(gh *), Bash(git *) model: opus diff --git a/.claude/skills/run-test/SKILL.md b/.claude/skills/run-test/SKILL.md deleted file mode 100644 index 36b6892..0000000 --- a/.claude/skills/run-test/SKILL.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: run-test -description: | - 테스트를 실행하고 결과를 분석한다. - Trigger: "테스트 돌려줘", "테스트 실행해줘", "이 테스트 통과하는지 확인해줘", "테스트 결과 확인해줘", "빌드 돌려줘" - Do NOT use for: 테스트 코드 작성(→ write-test), 테스트 코드 수정(직접 Edit), 빌드 설정 변경 - Boundary: 테스트 실행과 결과 분석까지만 수행한다. 실패한 테스트의 코드 수정은 사용자 확인 후 별도로 진행한다. -allowed-tools: Bash(./gradlew *), Read, Grep -model: sonnet -effort: xhigh ---- - -# 테스트 실행 - -## Phase 1: 실행 - -1. $ARGUMENTS가 비어 있으면 전체 테스트를 실행하라: `./gradlew test` -2. $ARGUMENTS가 있으면 해당 클래스/메서드만 실행하라: `./gradlew test --tests "$ARGUMENTS"` - -> 다음 Phase 조건: Gradle 명령이 종료되었을 때 (성공/실패 무관) - -> Skip 조건: 없음 (필수 Phase) - -## Phase 2: 결과 분석 - -1. 테스트가 모두 통과하면 성공 결과를 한 줄로 보고하고 종료하라 -2. 실패한 테스트가 있으면 Phase 3으로 진행하라 - -> 다음 Phase 조건: 실패한 테스트가 존재할 때 - -> Skip 조건: 모든 테스트가 통과한 경우 — 결과 보고 후 종료 - -## Phase 3: 실패 원인 분석 - -1. 실패 로그에서 실패한 테스트 클래스와 메서드명을 추출하라 -2. 각 실패에 대해 에러 메시지와 스택 트레이스의 핵심 원인을 파악하라 -3. 필요하면 실패한 테스트 소스 파일을 Read로 읽어 맥락을 확인하라 -4. 사용자에게 다음을 보고하라: - - 실패한 테스트 목록 (클래스명#메서드명) - - 각 실패의 원인 요약 (1~2문장) - - 수정 방안 제안 -5. 사용자 확인 없이 코드를 수정하지 마라 - -> Skip 조건: 없음 (Phase 2에서 실패가 있으면 필수) \ No newline at end of file diff --git a/.claude/skills/write-api-docs/SKILL.md b/.claude/skills/write-api-docs/SKILL.md deleted file mode 100644 index 6c5907e..0000000 --- a/.claude/skills/write-api-docs/SKILL.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: write-api-docs -description: | - Swagger API 문서(ControllerDocs 인터페이스)를 작성한다. - Trigger: "API 문서 작성해줘", "Swagger 문서 만들어줘", "Docs 인터페이스 작성해줘", "XXXController 문서화해줘", "API 문서 추가해줘" - Do NOT use for: DTO에 @Schema만 추가하는 작업(직접 Edit), Controller 로직 수정, 기존 Docs 인터페이스의 단순 오타 수정 - Boundary: Controller 자체의 구현 변경은 이 스킬 범위 밖이다. Docs 인터페이스 생성과 Controller의 implements 연결까지만 수행한다. -allowed-tools: Read, Grep, Glob, Edit, Write -model: sonnet -effort: xhigh ---- - -# API 문서 작성 - -대상 Controller: $ARGUMENTS - -## Phase 1: 엔드포인트 분석 - -1. $ARGUMENTS로 지정된 Controller 클래스를 Read로 읽어라 -2. 모든 `@GetMapping`, `@PostMapping`, `@PutMapping`, `@PatchMapping`, `@DeleteMapping` 엔드포인트를 목록화하라 -3. 각 엔드포인트의 파라미터 타입(PathVariable, RequestParam, RequestBody, AuthenticationPrincipal)을 파악하라 -4. 각 엔드포인트의 반환 타입을 파악하라 - -> 다음 Phase 조건: 모든 엔드포인트의 메서드 시그니처가 파악되었을 때 - -> Skip 조건: 없음 (필수 Phase) - -## Phase 2: 예외 수집 - -1. 각 엔드포인트가 호출하는 Service 메서드를 추적하라 (Read로 해당 파일을 읽어라) -2. 추적한 메서드에서 throw하는 `RestApiException`의 `ExceptionCode`를 수집하라 -3. `ExceptionCode` enum 파일을 읽어 수집한 코드의 HTTP status, code, message를 확인하라 -4. 엔드포인트별로 발생 가능한 에러 목록을 정리하라 - -> 다음 Phase 조건: 모든 엔드포인트의 에러 코드 목록이 정리되었을 때 - -> Skip 조건: Controller가 단순 조회(GET)만 있고, Service에서 예외를 던지지 않는 경우 — 이 경우 500 응답만 포함하고 Phase 3으로 진행 - -## Phase 3: Docs 인터페이스 작성 - -1. `.claude/spec/api-docs-convention.md`를 읽어 어노테이션 규칙과 작성 컨벤션을 확인하라 -2. [template/api-docs-template.md](template/api-docs-template.md)를 읽어 코드 템플릿 구조를 확인하라 -3. 컨벤션과 템플릿에 따라 `controller/{Controller}Docs.java` 파일을 생성하라 - -> 다음 Phase 조건: Docs 인터페이스 파일이 작성되었을 때 - -> Skip 조건: 없음 (필수 Phase) - -## Phase 4: Controller 연결 및 검증 - -1. Controller 클래스가 생성한 Docs 인터페이스를 `implements` 하고 있는지 확인하라 -2. `implements`가 없으면 Controller 클래스에 추가하라 -3. Docs 인터페이스의 메서드 시그니처와 Controller의 실제 메서드 시그니처가 일치하는지 대조하라 -4. `@ExampleObject`에 사용한 에러코드 값이 `ExceptionCode` enum과 일치하는지 최종 확인하라 -5. 결과를 사용자에게 보고하라: 생성된 파일 경로, 문서화된 엔드포인트 수, 매핑된 에러코드 수 - -> Skip 조건: 없음 (필수 Phase) \ No newline at end of file diff --git a/.claude/skills/write-api-docs/template/api-docs-template.md b/.claude/skills/write-api-docs/template/api-docs-template.md deleted file mode 100644 index c812cf6..0000000 --- a/.claude/skills/write-api-docs/template/api-docs-template.md +++ /dev/null @@ -1,48 +0,0 @@ -# API 문서 템플릿 - -```java -package uss.code.{domain}.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import uss.code.auth.annotation.Auth; -import uss.code.global.exception.dto.response.ErrorResponse; - -@Tag(name = "{Domain} API", description = "{도메인} 관련 API") -public interface {Controller}Docs { - - @Operation(summary = "{기능 요약}", description = "{상세 설명}
" - + "🔐 Jwt 필요
") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "✅ {성공 메시지}"), - @ApiResponse(responseCode = "404", description = "🚨 {에러 설명}", - content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, - examples = { - @ExampleObject( - name = "{에러명}", - value = "{\"code\" : {코드}, \"message\" : \"에러 메시지\"}" - ) - }, - schema = @Schema(implementation = ErrorResponse.class)) - ), - @ApiResponse(responseCode = "500", description = "🚨 예기치 못한 예외 발생", - content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, - examples = { - @ExampleObject( - name = "예기치 못한 예외 발생", - value = "{\"code\" : 9999, \"message\" : \"서버 내부 오류가 발생했습니다.\"}" - ) - }, - schema = @Schema(implementation = ErrorResponse.class)) - ) - }) - ResponseEntity<{ResponseType}> methodName(@Auth final long memberId); -} -``` diff --git a/.claude/skills/write-test/SKILL.md b/.claude/skills/write-test/SKILL.md index 4493f83..a361688 100644 --- a/.claude/skills/write-test/SKILL.md +++ b/.claude/skills/write-test/SKILL.md @@ -1,21 +1,22 @@ --- name: write-test description: | - 테스트 코드를 작성한다. - Trigger: "테스트 작성해줘", "테스트 코드 만들어줘", "이 클래스 테스트해줘", "XXXService 테스트", "테스트 추가해줘" - Do NOT use for: 테스트 실행/결과 확인(→ run-test), 기존 테스트 수정만 필요한 경우(직접 Edit), 코드 리뷰 + 테스트 코드를 작성하고 실행한다. + Trigger: "테스트 작성해줘", "테스트 코드 만들어줘", "이 클래스 테스트해줘", "XXXService 테스트", "테스트 추가해줘", "테스트 돌려줘", "테스트 실행해줘", "테스트 결과 확인해줘" + Do NOT use for: 기존 테스트 수정만 필요한 경우(직접 Edit), 코드 리뷰, 빌드 설정 변경 Boundary: 테스트 대상 코드의 버그 수정은 이 스킬 범위 밖이다. 테스트 작성 중 버그를 발견하면 사용자에게 보고만 하라. allowed-tools: Read, Grep, Glob, Edit, Write, Bash model: opus effort: xhigh --- -# 테스트 코드 작성 +# 테스트 코드 작성과 실행 대상: $ARGUMENTS -**인자 형식**: `<클래스명>` (테스트 대상 클래스명, 필수) -- 예: `CourseService`, `RegistrationService` +**인자 형식**: `<클래스명>` (테스트 대상 클래스명 또는 실행할 테스트 클래스명) +- 아직 테스트가 없는 대상 클래스명이면 Phase 1부터 진행한다 (예: `CourseService`, `RegistrationService`) +- 이미 존재하는 테스트 클래스명이거나 인자가 비어 있으면 작성 Phase(1~4)를 건너뛰고 Phase 5(실행)로 직행한다 모든 테스트는 통합 테스트(`@IntegrationTest`)로 작성한다. @@ -27,7 +28,7 @@ effort: xhigh > 다음 Phase 조건: 대상 클래스의 메서드와 의존성 목록이 파악되었을 때 -> Skip 조건: 없음 (필수 Phase) +> Skip 조건: 실행만 요청된 경우(인자가 이미 존재하는 테스트 클래스이거나 비어 있음) — Phase 5로 직행 ## Phase 2: Fixture 확인 및 생성 @@ -37,7 +38,8 @@ effort: xhigh > 다음 Phase 조건: 테스트에 필요한 모든 Fixture가 준비되었을 때 -> Skip 조건: 대상 메서드가 Entity를 사용하지 않거나, 필요한 Fixture가 모두 이미 존재할 때 +> Skip 조건: 실행만 요청된 경우 — Phase 5로 직행. +> 또는 대상 메서드가 Entity를 사용하지 않거나, 필요한 Fixture가 모두 이미 존재할 때 ## Phase 3: 테스트 코드 작성 @@ -53,21 +55,32 @@ effort: xhigh > 다음 Phase 조건: 테스트 파일 작성이 완료되었을 때 -> Skip 조건: 없음 (필수 Phase) +> Skip 조건: 실행만 요청된 경우 — Phase 5로 직행 ## Phase 4: 검증 1. 작성한 테스트 파일이 컴파일 가능한지 import 누락, 타입 불일치를 점검하라 2. 누락된 테스트 케이스가 없는지 Phase 1의 분기 목록과 대조하라 -3. 최종 결과를 사용자에게 보고하라: 작성된 파일 경로, 테스트 메서드 수, 커버한 분기 +3. 작성 결과를 정리하라: 작성된 파일 경로, 테스트 메서드 수, 커버한 분기 -> Skip 조건: 없음 (필수 Phase) +> 다음 Phase 조건: 작성한 테스트가 검증되었을 때 + +> Skip 조건: 실행만 요청된 경우 — Phase 5로 직행 ## Phase 5: 테스트 실행 -1. 작성한 테스트 파일을 Bash로 실행하라: `./gradlew test --tests "{패키지}.{테스트클래스명}"` (H2) -2. 실패한 테스트가 있으면 에러 메시지를 분석하고 수정하라 -3. 모든 테스트가 통과할 때까지 수정 → 재실행을 반복하라 -4. 최종 통과 결과를 사용자에게 보고하라 +1. 테스트를 Bash로 실행하라 (H2): + - 인자가 비어 있으면 전체 실행: `./gradlew test` + - 인자가 있으면 해당 클래스/메서드만 실행: `./gradlew test --tests "{패키지}.{테스트클래스명}"` +2. 모두 통과하면 결과를 한 줄로 보고하고 종료하라 +3. 실패한 테스트가 있으면 원인을 분석하라: + - 실패 로그에서 실패한 테스트 클래스와 메서드명(`클래스명#메서드명`)을 추출하라 + - 각 실패의 에러 메시지와 스택 트레이스에서 핵심 원인을 파악하라 + - 필요하면 실패한 테스트 소스 파일을 Read로 읽어 맥락을 확인하라 +4. 실패 원인이 **이번에 작성한 테스트 코드** 쪽이면 수정하고, 모두 통과할 때까지 수정 → 재실행을 반복하라 +5. 실패 원인이 **테스트 대상(프로덕션) 코드의 버그**면 원인과 수정 방안을 보고만 하고, 사용자 확인 없이 대상 코드를 수정하지 마라 +6. 최종 결과를 사용자에게 보고하라: + - 실행 대상과 통과/실패 수 + - 실패가 남았으면 테스트 목록(`클래스명#메서드명`)과 각 원인 요약 (1~2문장) > Skip 조건: 없음 (필수 Phase) diff --git a/.claude/spec/api-docs-convention.md b/.claude/spec/api-docs-convention.md index 9a12132..1505146 100644 --- a/.claude/spec/api-docs-convention.md +++ b/.claude/spec/api-docs-convention.md @@ -53,3 +53,44 @@ description: Swagger API 문서(ControllerDocs 인터페이스) 작성 규칙 example = "1" ) ``` + +## 인터페이스 골격 + +```java +package uss.code.{domain}.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import uss.code.auth.annotation.Auth; +import uss.code.global.exception.dto.response.ErrorResponse; + +@Tag(name = "{Domain} API", description = "{도메인} 관련 API") +public interface {Controller}Docs { + + @Operation(summary = "{기능 요약}", description = "{상세 설명}
" + + "🔐 Jwt 필요
") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "✅ {성공 메시지}"), + @ApiResponse(responseCode = "404", description = "🚨 {에러 설명}", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + examples = { + @ExampleObject( + name = "{에러명}", + value = "{\"code\" : {코드}, \"message\" : \"에러 메시지\"}" + ) + }, + schema = @Schema(implementation = ErrorResponse.class)) + ) + }) + ResponseEntity<{ResponseType}> methodName(@Auth final long memberId); +} +``` + +- 선언할 `@ApiResponse`는 해당 엔드포인트에서 실제로 발생하는 응답만이다. 500은 선언하지 마라