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은 선언하지 마라