diff --git a/.claude/hooks/notify.ps1 b/.claude/hooks/notify.ps1 deleted file mode 100644 index 20b49a249..000000000 --- a/.claude/hooks/notify.ps1 +++ /dev/null @@ -1,9 +0,0 @@ -Add-Type -AssemblyName System.Windows.Forms -$n = New-Object System.Windows.Forms.NotifyIcon -$n.Icon = [System.Drawing.SystemIcons]::Information -$n.Visible = $true -$n.BalloonTipTitle = "Claude Code" -$n.BalloonTipText = "Awaiting your input" -$n.ShowBalloonTip(5000) -Start-Sleep -Milliseconds 5100 -$n.Dispose() diff --git a/.claude/hooks/notify.py b/.claude/hooks/notify.py deleted file mode 100644 index 6a2828d41..000000000 --- a/.claude/hooks/notify.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 -import os -import platform -import subprocess - -system = platform.system() -script_dir = os.path.dirname(os.path.abspath(__file__)) - -if system == "Darwin": - subprocess.run([ - "osascript", "-e", - 'display notification "Awaiting your input" with title "Claude Code"' - ]) -elif system == "Windows": - ps1_path = os.path.join(script_dir, "notify.ps1") - # VS Code extension 환경에서는 PATH에 powershell이 없을 수 있으므로 절대 경로 사용 - powershell_candidates = [ - r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe", - "powershell", - ] - for ps in powershell_candidates: - try: - subprocess.run( - [ps, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ps1_path], - timeout=10, - ) - break - except (FileNotFoundError, subprocess.TimeoutExpired): - continue diff --git a/.claude/hooks/post-edit-check.py b/.claude/hooks/post-edit-check.py deleted file mode 100755 index 0b5c1b14c..000000000 --- a/.claude/hooks/post-edit-check.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 -import json -import sys -import re - -data = json.load(sys.stdin) -file_path = data.get("tool_input", {}).get("file_path", "") - -if not file_path.endswith(".java") or not file_path: - sys.exit(0) - -try: - with open(file_path) as f: - content = f.read() - lines = content.split("\n") -except Exception: - sys.exit(0) - -warnings = [] - -# 1. 와일드카드 import 체크 -for i, line in enumerate(lines, 1): - if re.match(r"\s*import\s+.*\.\*;", line): - warnings.append(f"L{i}: 와일드카드 import 발견 -> 명시적 import 필요") - -# 2. 파일 끝 줄바꿈 체크 -if content and not content.endswith("\n"): - warnings.append("파일 끝 줄바꿈 누락") - -# 3. Entity 클래스의 @Column 체크 -if "@Entity" in content: - field_pattern = re.compile(r"^\s+private\s+\w+(?:<[^>]+>)?\s+\w+;") - relation_annotations = { - "@Column", "@Id", "@ManyToOne", "@OneToMany", - "@JoinColumn", "@OneToOne", "@ManyToMany", - "@Transient", "@Version", "@Embedded", "@EmbeddedId", - } - for i, line in enumerate(lines): - if field_pattern.match(line): - preceding = "\n".join(lines[max(0, i - 5):i]) - has_annotation = any(ann in preceding for ann in relation_annotations) - if not has_annotation: - warnings.append(f"L{i + 1}: Entity 필드에 @Column 누락 가능성: {line.strip()}") - -if warnings: - print(f"[컨벤션 체크 - {file_path.split('/')[-1]}]") - for w in warnings: - print(f" - {w}") diff --git a/.claude/rules/code-style.md b/.claude/rules/code-style.md new file mode 100644 index 000000000..4ba1fc53e --- /dev/null +++ b/.claude/rules/code-style.md @@ -0,0 +1,23 @@ +--- +paths: + - "src/**/*.java" +--- + +# Code Style + +## Naming +- DTO factory: `of(A, B)` for multiple params / `from(X)` for single param +- Request/Response classes: `XxxRequest`, `XxxResponse` +- REST endpoints: kebab-case only (`/user-profile`, NOT `/userProfile`) + +## Formatting +- Blank line before every class declaration +- Newline at end of every file +- No wildcard imports +- Controller method params: always on separate lines +- 3+ params anywhere: always on separate lines +- Private methods: placed immediately below the public method that calls them + +## Types +- Non-null: primitives (`int`, `long`, `boolean`) +- Nullable: wrapper types (`Integer`, `Long`, `Boolean`) diff --git a/.claude/rules/database.md b/.claude/rules/database.md new file mode 100644 index 000000000..f1c98bce2 --- /dev/null +++ b/.claude/rules/database.md @@ -0,0 +1,37 @@ +--- +paths: + - "src/main/java/**/domain/*.java" + - "src/main/resources/db/migration/*.sql" +--- + +# Database + +## JPA Entity + +- Every field requires `@Column(name = "...", nullable = ...)` +- Field name must match DB column name exactly (snake_case) +- `@Table(name = "...")` required on every entity class + +```java +@Entity +@Table(name = "chat_room") +public class ChatRoom { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "is_group", nullable = false) + private boolean isGroup; + + @Column(name = "mentoring_id", nullable = true) + private Long mentoringId; +} +``` + +## Flyway + +- Location: `src/main/resources/db/migration/` +- Naming: `V{VERSION}__{DESCRIPTION}.sql` (double underscore) +- NEVER modify a deployed migration — always create a new version diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 000000000..e1c2c1c3a --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,46 @@ +--- +paths: + - "src/test/**/*.java" +--- + +# Testing + +Base annotation: `@TestContainerSpringBootTest` for all integration tests. + +## Test data — Fixture pattern + +```java +@TestComponent +@RequiredArgsConstructor +public class ChatRoomFixture { + private final ChatRoomFixtureBuilder chatRoomFixtureBuilder; + + public ChatRoom 채팅방(boolean isGroup) { ... } // Korean method names + public ChatRoom 멘토링_채팅방(long mentoringId) { ... } +} +``` + +## Test class structure + +```java +@TestContainerSpringBootTest +@DisplayName("XXX 서비스 테스트") +class XxxServiceTest { + + @Nested + class 기능명 { + + @Test + void 한국어_메서드명() { + // given + // when + // then + } + } +} +``` + +- `@Nested` for grouping by feature +- Korean names on `@Nested` classes and `@Test` methods +- Given-When-Then with inline comments +- Tests are independent — no shared state across tests diff --git a/.claude/settings.json b/.claude/settings.json index f6c5b8ec9..b360c691c 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,29 +1,5 @@ { "env": { "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" - }, - "hooks": { - "Notification": [ - { - "matcher": "", - "hooks": [ - { - "type": "command", - "command": "python3 .claude/hooks/notify.py 2>/dev/null || python .claude/hooks/notify.py" - } - ] - } - ], - "PostToolUse": [ - { - "matcher": "Edit|Write", - "hooks": [ - { - "type": "command", - "command": "python3 .claude/hooks/post-edit-check.py 2>/dev/null || python .claude/hooks/post-edit-check.py" - } - ] - } - ] } } diff --git a/.claude/skills/review-pr/SKILL.md b/.claude/skills/review-pr/SKILL.md index 695db1278..9c48fadb0 100644 --- a/.claude/skills/review-pr/SKILL.md +++ b/.claude/skills/review-pr/SKILL.md @@ -1,281 +1,97 @@ --- name: review-pr -description: Pull Request를 체계적으로 리뷰하여 프로젝트 컨벤션 준수 여부와 코드 품질을 검증합니다 -args: (예: /review-pr 666) +description: Fetch a GitHub PR and review it against project conventions and code quality +args: (e.g. /review-pr 666) +context: fork +allowed-tools: + - Bash + - Read + - Grep + - Glob --- -# Pull Request 리뷰 가이드 +# PR Review -이 skill은 solid-connect-server 프로젝트의 Pull Request를 체계적으로 리뷰합니다. +## Step 1: Fetch PR data -## 사용법 - -```bash -/review-pr -``` - -**예제:** ```bash -/review-pr 666 +gh pr view $ARGUMENTS -R solid-connection/solid-connect-server --json title,body,author,number,url,headRefName +gh pr diff $ARGUMENTS -R solid-connection/solid-connect-server +gh pr checks $ARGUMENTS -R solid-connection/solid-connect-server ``` ---- - -## 리뷰 프로세스 - -### 1단계: PR 정보 수집 +## Step 2: Review checklist + +Work through each area in order. For each finding, record: file:line / problem / suggestion. + +### Architecture +- Controller → Service → Repository only. No reverse references. +- Business logic must not leak into Controller or Repository. +- Ref: `CLAUDE.md` - Architecture + +### Naming +- REST endpoints: kebab-case (`/user-profile`, not `/userProfile`) +- DTO factory: `from(X)` single param / `of(A, B)` multiple params +- Classes: `XxxRequest`, `XxxResponse` +- Ref: `.claude/rules/code-style.md` + +### Code style +- No wildcard imports +- Blank line before class declaration +- Controller params: always on separate lines; 3+ params anywhere: separate lines +- Private methods: immediately below the calling public method +- Ref: `.claude/rules/code-style.md` + +### Entity / JPA +- Every field: `@Column(name = "...", nullable = ...)` +- Non-null → primitives; nullable → wrapper types +- `@Table(name = "...")` on every entity +- Ref: `.claude/rules/database.md` + +### Flyway +- Schema changes must have a matching migration file +- Naming: `V{VERSION}__{DESCRIPTION}.sql` +- Existing migration files must not be modified +- Ref: `.claude/rules/database.md` + +### Tests +- New service/repository methods must have tests +- `@TestContainerSpringBootTest`, `@Nested`, Korean method names, Given-When-Then +- Fixture pattern: FixtureBuilder + Fixture +- Ref: `.claude/skills/test/SKILL.md` + +### Commit messages +- Format: `type: description` where type is feat|fix|refactor|docs|test|chore|perf +- Ref: `CLAUDE.md` - Git + +### Code quality +- `@Transactional(readOnly = true)` on read-only service methods +- Use `CustomException` — no raw `RuntimeException` +- Check for N+1 queries (missing fetch join or `@BatchSize`) +- No sensitive data exposed in responses + +## Step 3: Output format -GitHub CLI로 PR의 기본 정보와 변경사항을 파악합니다. - -```bash -gh pr view <번호> -R solid-connection/solid-connect-server # PR 기본 정보 조회 -gh pr diff <번호> -R solid-connection/solid-connect-server # 변경된 파일과 diff 확인 -gh pr checks <번호> -R solid-connection/solid-connect-server # CI/CD 상태 확인 ``` +## PR Review: #{number} - {title} -**수집할 정보:** -- PR 제목 및 설명 -- 관련 이슈 번호 -- 변경된 파일 목록 -- CI/CD 체크 상태 - -### 2단계: 변경 파일 분석 - -**도구 우선순위:** +Link: {url} +Author: {author} Branch: {branch} CI: {pass/fail} -1. **Serena MCP** (Java 코드 분석에 최적화) - - `mcp__serena__get_symbols_overview <파일경로>` - 파일의 클래스/메서드 구조 파악 - - `mcp__serena__find_symbol <심볼명>` - 특정 심볼 검색 - - `mcp__serena__search_for_pattern <패턴>` - 컨벤션 위반 패턴 검색 +### Summary +{brief description of what the PR does} -2. **Read/Grep** (보조 분석) - - `Read <파일경로>` - 파일 전체 읽기 - - `Grep --pattern <패턴>` - 패턴 검색 +### Passed +- {item} -### 3단계: 체크리스트 검증 - -아래 체크리스트를 순서대로 확인합니다. - ---- +### Suggestions (non-blocking) +- {file:line} — {problem} — {suggestion} -## 리뷰 체크리스트 +### Required changes +- {file:line} — {problem} — {fix direction} -각 항목의 상세 컨벤션은 참조 문서를 확인하세요. +### Overall +{conclusion} -### 1. 아키텍처 및 계층 구조 - -**검증 항목:** -- 계층형 아키텍처 준수 (Controller → Service → Repository) -- 역계층 참조 금지 -- 순환 의존성 없음 - -👉 **참고:** `CLAUDE.md` - "아키텍처" 섹션 - ---- - -### 2. 네이밍 컨벤션 - -**검증 항목:** -- API 엔드포인트: kebab-case 사용 (예: `/user-profile`) -- DTO 변환 메서드: 단일 파라미터 `from()`, 다중 파라미터 `of()` -- Request/Response: `XXXRequest`, `XXXResponse` 형식 -- 테스트 메서드: 한국어, `어떤_것을_하면_어떤_결과가_나온다()` 패턴 - -👉 **참고:** `CLAUDE.md` - "네이밍 컨벤션" 섹션 - ---- - -### 3. 코드 스타일 - -**검증 항목:** -- 와일드카드(`*`) import 금지 -- 클래스 선언 전 빈 줄 존재 -- private 메서드는 호출하는 public 메서드 바로 아래 위치 -- Controller: 모든 파라미터 줄바꿈 필수 -- 일반 메서드: 3개 이상 파라미터 시 줄바꿈 -- 파일 끝 개행 문자 - -**패턴 검색 예제:** -```bash -mcp__serena__search_for_pattern "import.*\\*" # 와일드카드 import 검색 +Verdict: Approved / Approved with suggestions / Changes requested ``` - -👉 **참고:** `CLAUDE.md` - "코드 스타일" 섹션 - ---- - -### 4. Entity 및 JPA - -**검증 항목:** -- 모든 필드에 `@Column` 어노테이션 존재 -- `name` 속성으로 컬럼명 명시 -- `nullable` 속성 명시 -- null 불가: 원시 타입 (`int`, `long`, `boolean`) -- nullable: Wrapper 타입 (`Integer`, `Long`, `Boolean`) -- 양방향 연관관계 시 편의 메서드 존재 - -👉 **참고:** `CLAUDE.md` - "데이터베이스 접근" 섹션 - ---- - -### 5. 데이터베이스 마이그레이션 - -**검증 항목:** -- 스키마 변경 시 Flyway 마이그레이션 파일 추가 -- 파일명 형식: `V{VERSION}__{DESCRIPTION}.sql` -- 위치: `src/main/resources/db/migration/` -- Entity 변경과 마이그레이션 일치 -- 기존 마이그레이션 파일 수정 금지 (새 버전 생성) - -👉 **참고:** `CLAUDE.md` - "데이터베이스 마이그레이션" 섹션 - ---- - -### 6. 테스트 코드 - -**검증 항목:** -- 새로운 Service/Repository 메서드에 대한 테스트 존재 -- 예외 케이스 테스트 포함 -- `@TestContainerSpringBootTest` 어노테이션 사용 -- `@DisplayName`으로 한글 설명 제공 -- `@Nested`로 기능별 그룹화 -- Given-When-Then 구조 준수 -- Fixture 패턴 사용 (FixtureBuilder + Fixture) - -👉 **참고:** `.claude/skills/test/SKILL.md` - ---- - -### 7. 커밋 메시지 - -**검증 항목:** -- `: ` 형식 -- Type: `feat`, `fix`, `refactor`, `test`, `chore`, `docs`, `perf` -- 간결하고 명확한 설명 - -👉 **참고:** `CLAUDE.md` - "Git 커밋 컨벤션" 섹션 - ---- - -### 8. 코드 품질 및 잠재적 이슈 - -**검증 항목:** -- 비즈니스 로직은 Service 계층에만 -- Controller는 요청/응답 처리만 -- `@Transactional` 적절하게 사용 (읽기 전용: `readOnly = true`) -- CustomException 사용 -- N+1 쿼리 문제 없음 -- 인증/인가 처리 (`@AuthorizedUser`) -- 민감 정보 노출 없음 - -👉 **참고:** `CLAUDE.md` - "아키텍처", "기술 스택 상세" 섹션 - ---- - -## 도구 사용 가이드 - -### Serena MCP (우선 사용) - -```bash -# 파일의 클래스/메서드 구조 파악 -mcp__serena__get_symbols_overview src/main/java/.../MentorService.java - -# 특정 심볼 검색 -mcp__serena__find_symbol "MentorDetailResponse" - -# 컨벤션 위반 패턴 검색 -mcp__serena__search_for_pattern "import.*\\*" -``` - -### GitHub CLI - -```bash -# PR 정보 -gh pr view 666 -R solid-connection/solid-connect-server --json title,body,author,number,url - -# 변경사항 -gh pr diff 666 -R solid-connection/solid-connect-server --patch - -# CI 상태 -gh pr checks 666 -R solid-connection/solid-connect-server -``` - -### 보조 도구 - -```bash -# 파일 읽기 -Read src/main/java/.../MentorService.java - -# 패턴 검색 -Grep --pattern "@Column" --glob "*.java" --path src/main/java/.../domain -``` - ---- - -## 리뷰 결과 출력 형식 - -다음 형식으로 리뷰 결과를 정리하여 제공합니다. - -```markdown -## PR 리뷰 결과: #{번호} - {제목} - -**PR 링크:** {GitHub URL} -**관련 이슈:** #{이슈번호} - -### 📊 PR 정보 요약 - -- **작성자:** {작성자} -- **변경 파일:** {숫자}개 -- **추가 라인:** +{숫자}, **삭제 라인:** -{숫자} -- **CI/CD 상태:** {통과/실패} - -### 주요 변경사항 - -{PR 설명 요약} - ---- - -### ✅ 통과 항목 - -- 아키텍처 계층 구조 준수 -- 네이밍 컨벤션 준수 -- ... - -### ⚠️ 개선 권장 항목 - -- **코드 스타일**: 와일드카드 import 사용 - - 파일: `src/main/java/.../MentorService.java:5` - - 개선: 명시적 import로 변경 - -### ❌ 필수 수정 항목 - -- **Entity**: @Column 어노테이션 누락 - - 파일: `src/main/java/.../domain/Mentor.java:30` - - 수정 방향: 모든 필드에 `@Column` 어노테이션 추가 - ---- - -### 💡 종합 의견 - -{전반적인 리뷰 의견} - -**승인 상태:** ✅ 승인 / ⚠️ 조건부 승인 / ❌ 수정 후 재검토 -``` - ---- - -## 리뷰 시 주의사항 - -1. **컨텍스트 이해 우선**: PR 설명과 관련 이슈를 먼저 읽고 변경의 목적 파악 -2. **Serena MCP 우선 사용**: Java 코드 분석 시 효율적 -3. **건설적 피드백**: 문제점 지적 시 구체적인 개선 방향 제시 -4. **긍정적 피드백**: 잘된 부분도 언급하여 균형 잡힌 리뷰 -5. **우선순위**: 아키텍처 > 네이밍 > 스타일 순으로 중요도 판단 - ---- - -## 참고 자료 - -- **프로젝트 컨벤션**: `CLAUDE.md` - 전체 개발 컨벤션 -- **테스트 가이드**: `.claude/skills/test/SKILL.md` - 테스트 작성 가이드 -- **개발 컨벤션 위키**: https://github.com/solid-connection/solid-connect-server/wiki/개발-컨벤션-정리 diff --git a/.claude/skills/test/SKILL.md b/.claude/skills/test/SKILL.md index c23122578..e60007607 100644 --- a/.claude/skills/test/SKILL.md +++ b/.claude/skills/test/SKILL.md @@ -1,247 +1,130 @@ --- name: test -description: 테스트 코드를 작성하거나 수정할 때 이 프로젝트의 테스트 컨벤션과 패턴을 참고합니다 +description: Generate a test class for a given service. Reads the implementation, checks existing fixtures, and produces a complete test file skeleton. +argument-hint: (e.g. /test ChatService) +context: fork +allowed-tools: + - Read + - Bash + - Grep + - Glob + - Write + - Edit --- -# 테스트 코드 작성 가이드 +# Generate Test Class -## 테스트 기본 설정 +Target: $ARGUMENTS -모든 통합 테스트는 `@TestContainerSpringBootTest` 어노테이션을 사용합니다. +## Step 1: Locate the service -```java -@TestContainerSpringBootTest -@DisplayName("채팅 서비스 테스트") -class ChatServiceTest { - // 테스트 코드 -} -``` - -**제공 기능:** -- MySQL, Redis 자동 실행 -- Spring Boot 컨텍스트 로드 -- 테스트 후 자동 DB 초기화 -- JUnit 5 기반 - -## Fixture 패턴 - -테스트 데이터는 Fixture로 생성합니다 (FixtureBuilder + Fixture 패턴). - -**위치:** `src/test/java/com/example/solidconnection/[domain]/fixture/` - -``` -fixture/ -├── [Entity]FixtureBuilder.java # Builder 패턴 구현 -└── [Entity]Fixture.java # 편의 메서드 제공 +```bash +find src/main/java -name "$ARGUMENTS.java" ``` -### 예제: ChatRoomFixtureBuilder - -```java -@TestComponent -@RequiredArgsConstructor -public class ChatRoomFixtureBuilder { - - private final ChatRoomRepository chatRoomRepository; - - private boolean isGroup; - private Long mentoringId; - - public ChatRoomFixtureBuilder chatRoom() { - return new ChatRoomFixtureBuilder(chatRoomRepository); - } +Read the file. Identify: +- Package path +- All public methods (name, params, return type) +- Injected dependencies (repositories, other services) +- Thrown exceptions (`CustomException` with `ErrorCode`) - public ChatRoomFixtureBuilder isGroup(boolean isGroup) { - this.isGroup = isGroup; - return this; - } +## Step 2: Identify required fixtures - public ChatRoomFixtureBuilder mentoringId(long mentoringId) { - this.mentoringId = mentoringId; - return this; - } +For each entity type used as method input or returned from repositories: - public ChatRoom create() { - ChatRoom chatRoom = new ChatRoom(mentoringId, isGroup); - return chatRoomRepository.save(chatRoom); // DB 저장 - } -} +```bash +find src/test -name "Fixture.java" +find src/test -name "FixtureBuilder.java" ``` -### 예제: ChatRoomFixture +- Fixture exists → note its Korean convenience methods for use in tests +- Fixture missing → flag it; offer to generate it after the test file -```java -@TestComponent -@RequiredArgsConstructor -public class ChatRoomFixture { - - private final ChatRoomFixtureBuilder chatRoomFixtureBuilder; - - // 편의 메서드: 기본값으로 생성 - public ChatRoom 채팅방(boolean isGroup) { - return chatRoomFixtureBuilder.chatRoom() - .isGroup(isGroup) - .create(); - } +## Step 3: Determine test package and file path - public ChatRoom 멘토링_채팅방(long mentoringId) { - return chatRoomFixtureBuilder.chatRoom() - .mentoringId(mentoringId) - .isGroup(false) - .create(); - } -} ``` - -**편의 메서드 작성 팁:** - -- 한국어 메서드명 사용 (가독성) -- 자주 사용되는 기본값 조합만 제공 -- Builder를 조합하여 필요한 데이터 설정 - -### 테스트에서 사용 - -```java -@TestContainerSpringBootTest -class ChatServiceTest { - - @Autowired - private ChatRoomFixture chatRoomFixture; - - @Test - void 채팅방을_생성할_수_있다() { - // 편의 메서드 사용 - ChatRoom room = chatRoomFixture.채팅방(false); - - // Builder 직접 사용 - ChatRoom customRoom = chatRoomFixture.chatRoomFixtureBuilder.chatRoom() - .isGroup(true) - .mentoringId(100L) - .create(); - } -} +src/test/java/com/example/solidconnection/[domain]/service/[ServiceName]Test.java ``` -## 테스트 네이밍 컨벤션 - -### 테스트 메서드 네이밍 규칙 - -테스트 메서드명은 **한국어로 명확하게** 작성하며, 다음 패턴을 따릅니다: - -#### 1. 정상 동작 테스트 - -```java -// 패턴: 어떤_것을_하면_어떤_결과가_나온다 -@Test -void 채팅방이_없으면_빈_목록을_반환한다() { ... } - -@Test -void 최신_메시지_순으로_정렬되어_조회한다() { ... } +Derive `[domain]` from the service's package path. -@Test -void 참여자는_메시지를_전송할_수_있다() { ... } - -@Test -void 페이징이_정상_작동한다() { ... } -``` +## Step 4: Generate the test file -#### 2. 예외 테스트 +Follow every rule in `.claude/rules/testing.md`. Skeleton structure: ```java -// 패턴: 어떤_것을_하면_예외_응답을_반환한다 -@Test -void 참여하지_않은_채팅방에_접근하면_예외_응답을_반환한다() { ... } +package com.example.solidconnection.[domain].service; -@Test -void 존재하지_않는_사용자로_메시지를_전송하면_예외_응답을_반환한다() { ... } +// static imports first +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; -@Test -void 권한이_없으면_예외_응답을_반환한다() { ... } +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.support.TestContainerSpringBootTest; +// ... fixture and domain imports -@Test -void 필수_파라미터가_없으면_예외_응답을_반환한다() { ... } -``` - -## BDD 테스트 작성 +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; -테스트는 Given-When-Then 구조로 작성합니다. +@TestContainerSpringBootTest +@DisplayName("[ServiceName] 테스트") +class [ServiceName]Test { -```java -@Test -@DisplayName("최신 메시지순으로 채팅방 목록을 조회한다") -void 최신_메시지_순으로_조회한다() { - // Given: 테스트 사전 조건 - SiteUser user = siteUserFixture.사용자(); - ChatRoom room1 = chatRoomFixture.채팅방(false); - ChatRoom room2 = chatRoomFixture.채팅방(false); - chatMessageFixture.메시지("오래된 메시지", user.getId(), room1); - chatMessageFixture.메시지("최신 메시지", user.getId(), room2); - - // When: 실제 동작 - ChatRoomListResponse response = chatService.getChatRooms(user.getId()); - - // Then: 결과 검증 - assertAll( - () -> assertThat(response.chatRooms()).hasSize(2), - () -> assertThat(response.chatRooms().get(0).id()).isEqualTo(room2.getId()) - ); -} -``` + @Autowired + private [ServiceName] [serviceField]; -## 테스트 그룹화 (@Nested) + // @Autowired repositories for direct state verification (not mocks) + // @Autowired fixtures -기능별로 테스트를 그룹화합니다. + // shared test data as fields if reused across @Nested classes + // private Entity entity; -```java -@TestContainerSpringBootTest -class ChatServiceTest { + @BeforeEach + void setUp() { + // initialize shared fixtures here + } @Nested - @DisplayName("채팅방 목록 조회") - class 채팅방_목록을_조회한다 { - - @Test - void 빈_목록을_반환한다() { ... } + class [메서드명_or_기능명] { @Test - void 최신_메시지_순으로_조회한다() { ... } - } + void [정상_케이스_한국어_메서드명]() { + // given - @Nested - @DisplayName("채팅 메시지 전송") - class 채팅_메시지를_전송한다 { + // when - @BeforeEach - void setUp() { - // 이 그룹에만 적용되는 초기 설정 + // then } @Test - void 참여자는_메시지를_전송할_수_있다() { ... } + void [예외_케이스_한국어_메서드명]() { + // given + + // when & then + assertThatCode(() -> [service].[method](...)) + .isInstanceOf(CustomException.class); + } } } ``` -## 자주 사용하는 Assertion +Rules to apply while generating: +- One `@Nested` class per public method (or per logical feature if a method is simple) +- Every `@Nested` class has at least one happy-path test and one exception test (if the method throws) +- Korean names on all `@Nested` classes and `@Test` methods +- `// given / // when / // then` comments on every test +- Use fixture convenience methods for test data — never construct entities manually +- No `@MockBean`; use real DB via TestContainers +- `@Autowired` repositories directly when you need to verify persisted state -```java -// 기본 검증 -assertThat(value).isEqualTo(expected); -assertThat(value).isNotNull(); - -// 컬렉션 -assertThat(list).hasSize(3); -assertThat(list).isEmpty(); -assertThat(list).contains(item); - -// 예외 검증 -assertThatCode(() -> method()) - .isInstanceOf(CustomException.class) - .hasMessage("error message"); - -// 복수 검증 -assertAll( - () -> assertThat(a).isEqualTo(1), - () -> assertThat(b).isEqualTo(2) -); -``` +## Step 5: Write the file + +Write the generated test class to the correct path. + +Then report: +- File created: `path/to/XxxServiceTest.java` +- Test methods generated: N (list them) +- Missing fixtures (if any): list entities that need FixtureBuilder + Fixture diff --git a/.gitignore b/.gitignore index f6e780891..beb17bc4c 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ application-prod.yml ### docker volumes ### mysql_data_local redis_data_local + +### macOS ### +.DS_Store diff --git a/claude.md b/claude.md index fb2d436a1..6b7a46ab4 100644 --- a/claude.md +++ b/claude.md @@ -1,382 +1,45 @@ -# CLAUDE.md +# solid-connect-server -이 파일은 Claude Code가 solid-connect-server 저장소에서 작업할 때 참고하는 가이드입니다. +Exchange student support platform — Java 21, Spring Boot 3.5.x, MySQL, Redis, Flyway. -## 프로젝트 개요 - -Solid Connect Server는 교환학생 준비생을 위해 대학 정보, 멘토 매칭, 모의지원 기능 등을 제공하는 교환학생 지원 통합 플랫폼입니다. - -- **언어**: Java 21 -- **프레임워크**: Spring Boot 3.5.11 -- **빌드 도구**: Gradle -- **데이터베이스**: MySQL (주), Redis (캐싱) -- **마이그레이션**: Flyway - ---- - -## 빌드 및 개발 명령어 - -### Gradle 빌드 명령어 - -```bash -# 전체 빌드 -./gradlew build - -# 테스트 실행 -./gradlew test - -# 특정 테스트만 실행 -./gradlew test --tests ChatServiceTest - -# 애플리케이션 실행 -./gradlew bootRun - -# 로컬 개발 환경 시작 (MySQL, Redis) -docker-compose -f docker-compose.local.yml up -d -``` - -### 프로필별 실행 +## Commands ```bash -# 로컬 개발 환경 +./gradlew build # build + flywayValidate +./gradlew test # all tests +./gradlew test --tests # single class ./gradlew bootRun --args='--spring.profiles.active=local' - -# 개발 환경 -./gradlew bootRun --args='--spring.profiles.active=dev' - -# 운영 환경 -./gradlew bootRun --args='--spring.profiles.active=prod' -``` - ---- - -## 프로젝트 구조 - -``` -solid-connect-server/ -├── src/ -│ ├── main/ -│ │ ├── java/com/example/solidconnection/ -│ │ │ ├── [domain]/ # 도메인별 폴더 -│ │ │ │ ├── controller/ # REST API 엔드포인트 -│ │ │ │ ├── service/ # 비즈니스 로직 -│ │ │ │ ├── domain/ # JPA Entity -│ │ │ │ ├── repository/ # 데이터 접근 계층 -│ │ │ │ └── dto/ # DTO (Request/Response) -│ │ │ └── common/ # 공통 기능 -│ │ │ ├── exception/ # 커스텀 예외 -│ │ │ ├── config/ # Spring 설정 -│ │ │ └── util/ # 유틸리티 -│ │ └── resources/ -│ │ ├── db/migration/ # Flyway 마이그레이션 -│ │ └── application*.yml # 설정 파일 -│ └── test/ -│ └── java/com/example/solidconnection/ -│ ├── [domain]/fixture/ # 테스트 Fixture -│ ├── [domain]/service/ # 서비스 테스트 -│ ├── support/ # 테스트 설정 -│ └── ... -├── docker-compose.local.yml # 로컬 컨테이너 -├── docker-compose.dev.yml # 개발 컨테이너 -├── docker-compose.prod.yml # 운영 컨테이너 -├── Dockerfile # 이미지 빌드 -├── build.gradle # Gradle 설정 -``` - ---- - -## 아키텍처 - -### 계층형 아키텍처 (Layered Architecture) - -각 계층은 자신의 바로 아래 계층만 참조할 수 있습니다. - -``` -Controller → Service → Repository/Domain -``` - -**각 계층의 역할:** - -- **Controller**: HTTP 요청 처리, 입력값 검증, 응답 포맷팅 -- **Service**: 비즈니스 로직 처리, DTO 변환, 트랜잭션 관리 -- **Repository**: 데이터 접근 계층, DB 쿼리 작성 -- **Domain (Entity)**: JPA 엔티티, 도메인 모델 - -**주요 규칙:** - -- ✅ 역계층 참조 금지 (예: Repository에서 Service 참조 불가) -- ✅ Service는 Repository를 주입받아 사용 -- ✅ Controller는 Service를 주입받아 사용 -- ✅ Entity는 도메인 로직만 포함 -- ✅ DTO는 요청/응답 시에만 사용 - -### 패키지 구조 - -``` -[domain]/ -├── controller/ # REST API 엔드포인트 -├── service/ # 비즈니스 로직 (Service) -├── domain/ # JPA Entity -├── repository/ # 데이터 접근 계층 (Repository) -└── dto/ # DTO (Request/Response) -``` - ---- - -## 개발 컨벤션 - -### 코드 스타일 - -프로젝트의 개발 컨벤션을 따릅니다: [개발-컨벤션-정리](https://github.com/solid-connection/solid-connect-server/wiki/개발-컨벤션-정리) - -**주요 규칙:** - -- **클래스 선언 전 줄바꿈**: 클래스 정의 앞에 빈 줄 필수 -- **파일 끝 줄바꿈**: 모든 파일은 개행 문자로 종료 -- **와일드카드 import 금지**: 명시적 import만 사용 -- **파라미터 줄바꿈**: Controller는 필수, 3개 이상의 파라미터가 있으면 줄바꿈 -- **private 메서드 위치**: 호출하는 public 메서드 바로 아래 위치 -- **원시 타입 사용**: null이 아닌 값은 `int`, `long` 등 원시 타입 사용, nullable은 Wrapper 사용 -- **JPA @Column**: Entity의 모든 필드에 `@Column` 속성과 필드명 지정 - -### 네이밍 컨벤션 - -```java -// DTO 변환 -// 다중 파라미터: of() 메서드 -public static UserDto of(User user, Profile profile) { ... } - -// 단일 파라미터: from() 메서드 -public static UserDto from(User user) { ... } - -// API 요청/응답 -// XXXRequest, XXXResponse 형식 -public class UserCreateRequest { ... } -public class UserCreateResponse { ... } - -// REST API 엔드포인트 -// kebab-case 사용 -@GetMapping("/user-profile") // O -@GetMapping("/userProfile") // X -``` - ---- - -## 기술 스택 상세 - -### Core Framework - -- **Spring Boot 3.5.11**: 스프링 부트 -- **Spring Security**: JWT 기반 인증 -- **Spring Data JPA**: ORM -- **QueryDSL**: 동적 쿼리 생성 - -### 데이터베이스 - -- **MySQL**: 주 데이터베이스 -- **Redis**: 캐싱 저장소 -- **Flyway**: 데이터베이스 버전 관리 - -### 모니터링 & 보안 - -- **Spring Boot Actuator**: 애플리케이션 모니터링 -- **Prometheus**: 메트릭 수집 -- **Sentry**: 에러 추적 -- **JWT**: JWT 토큰 관리 - -### 개발 도구 - -- **Lombok**: 보일러플레이트 코드 감소 -- **AWS S3 SDK**: 파일 저장소 -- **WebSocket**: 실시간 통신 -- **TestContainers**: 통합 테스트용 컨테이너 - ---- - -## 테스트 코드 작성 - -테스트 작성 시 `/test` skill을 참고하세요. (테스트 관련 작업 시 자동으로 로드됩니다) - -- `@TestContainerSpringBootTest` 기반 통합 테스트 -- FixtureBuilder + Fixture 패턴으로 테스트 데이터 생성 -- 한국어 메서드명, Given-When-Then 구조, @Nested 그룹화 - ---- - -## Git 커밋 컨벤션 - -### 형식 - -``` -: - -[optional body] -``` - -### Type 목록 - -``` -feat: 새로운 기능 추가 -fix: 버그 수정 -refactor: 코드 리팩토링 (기능 변경 없음) -docs: 문서 변경 -test: 테스트 추가/수정 -chore: 빌드 설정, 패키지 관리 -perf: 성능 개선 -``` - -### 예제 - -```bash -# 기능 추가 -feat: 대학 검색 기능 추가 - -# 버그 수정 -fix: 채팅방 조회 시 정렬 버그 수정 - -# 리팩토링 -refactor: ChatService 메서드 분리 - -# 테스트 추가 -test: ChatService 테스트 케이스 추가 - -# 브랜치명 -refactor/529-shortening-cd-time -``` - ---- - -## 데이터베이스 마이그레이션 - -### Flyway 사용 - -모든 DB 스키마 변경사항은 Flyway로 관리합니다. - -**위치:** `src/main/resources/db/migration/` - -**파일명 형식:** `V{VERSION}__{DESCRIPTION}.sql` - +docker-compose -f docker-compose.local.yml up -d # MySQL + Redis ``` -V1__init_schema.sql -V2__add_chat_table.sql -V3__add_user_role_column.sql -``` - -### 마이그레이션 추가 - -1. `V{next_version}__{description}.sql` 파일 생성 -2. SQL 작성 -3. `./gradlew build` 시 자동 검증 (flywayValidate) -**주의:** 한 번 배포된 마이그레이션은 수정 불가 (새 버전으로 생성) +## Architecture ---- +Strict layered: Controller → Service → Repository/Domain. No reverse references. -## 데이터베이스 접근 +- Controller: HTTP handling, input validation, response formatting +- Service: business logic, DTO conversion, @Transactional +- Repository: data access only +- Domain/Entity: domain logic only — no Service/Repository references +- DTO: request/response boundary only -### JPA Entity +Package root: `com.example.solidconnection.[domain]/{controller,service,domain,repository,dto}/` -```java -@Entity -@Table(name = "chat_room") -public class ChatRoom { +## Conventions - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id") - private Long id; +@.claude/rules/code-style.md +@.claude/rules/testing.md +@.claude/rules/database.md - @Column(name = "is_group", nullable = false) - private boolean isGroup; +## Git - @Column(name = "mentoring_id", nullable = true) - private Long mentoringId; -} ``` - -**규칙:** -- `@Column` 필수 (모든 필드) -- 필드명과 DB 컬럼명 일치 -- nullable 명시 - -### Repository - -```java -public interface ChatRoomRepository extends JpaRepository { - Optional findByMentoringId(Long mentoringId); - List findByIsGroup(boolean isGroup); -} +Commit : feat|fix|refactor|docs|test|chore|perf: +Branch : type/issue-number-short-desc ``` ---- - -## 주요 파일 위치 - -| 파일/폴더 | 설명 | -|----------|------| -| `src/main/java/com/example/solidconnection/` | 메인 소스 코드 | -| `src/test/java/com/example/solidconnection/` | 테스트 코드 | -| `src/main/resources/db/migration/` | Flyway 마이그레이션 | -| `src/main/resources/application.yml` | 공통 설정 | -| `docker-compose.*.yml` | 환경별 도커 설정 | -| `build.gradle` | Gradle 빌드 설정 | - ---- - -### 프로필 -- **local**: Development with embedded Tomcat -- **dev**: Development server (stage.solid-connection.com) -- **prod**: Production server (solid-connection.com) - ---- - -## 자주하는 작업 - -### 새 기능 추가 - -1. Entity 생성 (`src/main/java/.../domain/`) -2. Repository 작성 (`src/main/java/.../repository/`) -3. Service 구현 (`src/main/java/.../service/`) -4. Controller 작성 (`src/main/java/.../controller/`) -5. DTO 정의 (`src/main/java/.../dto/`) -6. Flyway 마이그레이션 작성 -7. 테스트 코드 작성 - -### 테스트 작성 - -1. FixtureBuilder 생성 (필요시) -2. Fixture 편의 메서드 추가 (필요시) -3. 테스트 클래스 작성 (`*Test.java`) -4. @Nested로 테스트 그룹화 -5. Given-When-Then 구조로 작성 -6. `./gradlew test` 실행 - -### DB 스키마 변경 - -1. `V{next}__{description}.sql` 파일 생성 -2. 마이그레이션 SQL 작성 -3. Entity 업데이트 (필요시) -4. 테스트 실행 - ---- - -## 참고 자료 - -- **개발 컨벤션**: https://github.com/solid-connection/solid-connect-server/wiki/개발-컨벤션-정리 -- **테스트 가이드**: `test.md` 파일 참고 -- **Spring Boot**: https://spring.io/projects/spring-boot -- **JPA**: https://spring.io/projects/spring-data-jpa -- **TestContainers**: https://www.testcontainers.org/ -- **Flyway**: https://flywaydb.org/ - ---- - - -## 주의사항 +## IMPORTANT: Do not violate these -1. **Flyway 마이그레이션은 되돌릴 수 없음** - 신중하게 작성 -2. **QueryDSL Q클래스는 자동 생성** - 수동 수정 금지 -3. **테스트는 독립적** - 테스트 간 데이터 공유 불가 -4. **환경별 설정 분리** - application-local.yml, application-dev.yml, application-prod.yml -5. **한국어 메서드명** - 테스트 가독성 향상을 위해 사용 +- Flyway migrations are irreversible once deployed — create a new version, never edit +- QueryDSL Q-classes are auto-generated — never edit manually +- Tests must be fully independent — no shared state between tests