|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Commands |
| 6 | + |
| 7 | +```bash |
| 8 | +# 빌드 |
| 9 | +./gradlew build |
| 10 | +./gradlew clean build |
| 11 | + |
| 12 | +# 실행 (local 프로파일) |
| 13 | +./gradlew bootRun --args='--spring.profiles.active=local' |
| 14 | + |
| 15 | +# 테스트 전체 실행 |
| 16 | +./gradlew test |
| 17 | + |
| 18 | +# 특정 테스트 클래스 실행 |
| 19 | +./gradlew test --tests "akuma.whiplash.domains.alarm.domain.service.AlarmCommandServiceTest" |
| 20 | + |
| 21 | +# 특정 테스트 메서드 실행 |
| 22 | +./gradlew test --tests "akuma.whiplash.domains.alarm.domain.service.AlarmCommandServiceTest.RingAlarmTest.success" |
| 23 | + |
| 24 | +# Docker Compose로 전체 스택 실행 (MySQL, Redis, Prometheus, Grafana, k6) |
| 25 | +docker-compose up -d |
| 26 | +``` |
| 27 | + |
| 28 | +## 아키텍처 |
| 29 | + |
| 30 | +### 레이어 구조 (도메인별로 반복) |
| 31 | + |
| 32 | +``` |
| 33 | +presentation (Controller) |
| 34 | + ↓ |
| 35 | +application (UseCase) ← 도메인 서비스 조합, DTO 변환 |
| 36 | + ↓ |
| 37 | +domain (CommandService / QueryService) ← 비즈니스 규칙 |
| 38 | + ↓ |
| 39 | +persistence (Repository / Entity) |
| 40 | +``` |
| 41 | + |
| 42 | +각 도메인(`alarm`, `auth`, `member`, `place`)은 위 4개 레이어를 독립적으로 가진다. |
| 43 | + |
| 44 | +### 도메인 패키지 구조 |
| 45 | + |
| 46 | +``` |
| 47 | +akuma.whiplash/ |
| 48 | +├── domains/ |
| 49 | +│ └── {domain}/ |
| 50 | +│ ├── application/ |
| 51 | +│ │ ├── dto/ (request, response, etc) |
| 52 | +│ │ ├── mapper/ (Entity ↔ DTO) |
| 53 | +│ │ ├── usecase/ (UseCase 인터페이스 + 구현) |
| 54 | +│ │ └── scheduler/ (alarm 도메인만 존재) |
| 55 | +│ ├── domain/ |
| 56 | +│ │ ├── constant/ (enum) |
| 57 | +│ │ └── service/ (CommandService, QueryService) |
| 58 | +│ ├── persistence/ |
| 59 | +│ │ ├── entity/ |
| 60 | +│ │ └── repository/ |
| 61 | +│ ├── presentation/ |
| 62 | +│ └── exception/ (도메인별 ErrorCode enum) |
| 63 | +│ |
| 64 | +├── infrastructure/ |
| 65 | +│ ├── redis/ (RedisRepository, RedisService, RingingAlarmRedisRepository) |
| 66 | +│ └── firebase/ (FcmService, MockFcmService) |
| 67 | +│ |
| 68 | +└── global/ |
| 69 | + ├── config/ |
| 70 | + │ ├── security/ (JWT, Spring Security) |
| 71 | + │ └── scheduler/ (스케줄러 ThreadPool) |
| 72 | + ├── exception/ (ApplicationException) |
| 73 | + └── response/code/ (공통 ErrorCode, SuccessCode) |
| 74 | +``` |
| 75 | + |
| 76 | +### 예외 처리 규칙 |
| 77 | + |
| 78 | +- 모든 도메인 예외는 `ApplicationException.from(ErrorCode)` 패턴 사용 |
| 79 | +- 각 도메인에 `{Domain}ErrorCode` enum 존재 (예: `AlarmErrorCode`, `AuthErrorCode`) |
| 80 | +- ErrorCode enum 포맷: `DOMAIN_STATUS(HttpStatus.STATUS, "Domain_x0n", "message")` |
| 81 | +- 허용 HTTP 상태: `400`, `401`, `403`, `404`, `409`만 사용 |
| 82 | + |
| 83 | +### API 응답 규칙 |
| 84 | + |
| 85 | +- 공통 래퍼: `ApplicationResponse<T>` |
| 86 | +- 성공 코드: `SuccessCode` enum |
| 87 | +- 오류 코드: `CommonErrorCode` / 도메인별 `*ErrorCode` enum |
| 88 | + |
| 89 | +## 코드 컨벤션 |
| 90 | + |
| 91 | +### 레이어별 메서드 명명 규칙 |
| 92 | + |
| 93 | +| 동작 | Controller / UseCase / Service | Repository | |
| 94 | +|---|---|---| |
| 95 | +| 조회 | `getXxx` | `findByXxx`, `countByXxx`, `existsByXxx` | |
| 96 | +| 생성 | `createXxx` | `insertXxx` | |
| 97 | +| 삭제 | `removeXxx` | `deleteXxx` | |
| 98 | +| 수정 | `modifyXxx` | `updateXxx` | |
| 99 | + |
| 100 | +### 요청/응답 객체 필드 규칙 |
| 101 | + |
| 102 | +- PK는 반드시 도메인명을 붙인다: `Alarm.id` → `alarmId` |
| 103 | +- URL에 도메인이 명시되는 요청 객체는 도메인명 생략 (PK 제외): `POST /api/alarms` 의 바디는 `purpose` (not `alarmPurpose`) |
| 104 | +- 응답 List 필드명: `{도메인명}s` (자료형 명시 X) → `alarms`, `tickets` |
| 105 | +- 중첩 객체 속성은 엔티티 이름 생략 (PK 제외) |
| 106 | +- Enum 값은 `.name()` 그대로 반환 |
| 107 | +- 날짜는 `ISO_LOCAL_DATE`(2011-12-03) 또는 `ISO_LOCAL_DATE_TIME`(2011-12-03T10:15:30) 포맷 사용 |
| 108 | +- 페이지네이션 파라미터: `page`, `size`, `sortType` |
| 109 | + |
| 110 | +## 테스트 컨벤션 |
| 111 | + |
| 112 | +### 어떤 테스트를 선택할까 |
| 113 | + |
| 114 | +| 목적 | 사용할 어노테이션 | |
| 115 | +|---|---| |
| 116 | +| 컨트롤러 요청-응답 / 검증 / 예외 핸들링 | `@WebMvcTest` | |
| 117 | +| 서비스 비즈니스 로직 / 트랜잭션 경계 | `@ExtendWith(MockitoExtension.class)` | |
| 118 | +| 리포지토리 / 엔티티 / JPQL | `@PersistenceTest` | |
| 119 | +| 보안 + 필터 + DB/Redis 연동 + 전체 플로우 | `@IntegrationTest` | |
| 120 | +| Redis Sorted Set 등 Redis 슬라이스 | `@DataRedisTest` + `RedisContainerInitializer` | |
| 121 | + |
| 122 | +### 테스트 어노테이션 상세 |
| 123 | + |
| 124 | +- **`@ExtendWith(MockitoExtension.class)`**: 서비스 단위 테스트. DB/Redis 없음, Mockito만 사용 |
| 125 | +- **`@WebMvcTest`**: 컨트롤러 슬라이스. 보안 필터 제외 시 `@AutoConfigureMockMvc(addFilters = false)`. 협력 빈은 `@MockitoBean`으로 주입 |
| 126 | +- **`@PersistenceTest`**: MySQL Testcontainer + `@DataJpaTest`. JPA Auditing 필요 시 `@Import(JpaAuditingConfig.class)` |
| 127 | +- **`@IntegrationTest`**: 전체 Spring 컨텍스트, MySQL + Redis Testcontainer, 트랜잭션 롤백 |
| 128 | +- **`@DataRedisTest`** + `@ContextConfiguration(initializers = RedisContainerInitializer.class)`: Redis 슬라이스 |
| 129 | + |
| 130 | +### 테스트 구조 규칙 (@Nested) |
| 131 | + |
| 132 | +하나의 테스트 클래스는 **클래스의 각 메서드마다 `@Nested` inner class 하나**로 구성한다. |
| 133 | +각 inner class 안에 성공/실패 케이스를 모두 작성한다. |
| 134 | + |
| 135 | +``` |
| 136 | +{원본클래스}Test |
| 137 | + └── @Nested {메서드명}Test ← 메서드마다 inner class |
| 138 | + ├── success() |
| 139 | + ├── fail_{비즈니스실패이유}() |
| 140 | + └── fail_{다른실패이유}() |
| 141 | +``` |
| 142 | + |
| 143 | +예시: |
| 144 | +```java |
| 145 | +@DisplayName("AlarmCommandService Unit Test") |
| 146 | +@ExtendWith(MockitoExtension.class) |
| 147 | +class AlarmCommandServiceTest { |
| 148 | + |
| 149 | + @Mock private AlarmRepository alarmRepository; |
| 150 | + @InjectMocks private AlarmCommandServiceImpl alarmCommandService; |
| 151 | + |
| 152 | + @Nested |
| 153 | + @DisplayName("alarmOff - 알람 끄기(OFF)") |
| 154 | + class AlarmOffTest { |
| 155 | + |
| 156 | + @Test |
| 157 | + @DisplayName("성공: 주간 OFF 한도 내에서 알람을 끈다") |
| 158 | + void success() { ... } |
| 159 | + |
| 160 | + @Test |
| 161 | + @DisplayName("실패: 주간 OFF 한도를 초과하면 예외를 던진다") |
| 162 | + void fail_weeklyLimitExceeded() { ... } |
| 163 | + } |
| 164 | + |
| 165 | + @Nested |
| 166 | + @DisplayName("ringAlarm - 알람 울림") |
| 167 | + class RingAlarmTest { |
| 168 | + |
| 169 | + @Test |
| 170 | + @DisplayName("성공: alarmRinging=true, 로그 저장, Redis 적재가 모두 수행된다") |
| 171 | + void success() { ... } |
| 172 | + |
| 173 | + @Test |
| 174 | + @DisplayName("실패: 알람 시간이 되지 않았으면 예외를 던진다") |
| 175 | + void fail_notAlarmTime() { ... } |
| 176 | + } |
| 177 | +} |
| 178 | +``` |
| 179 | + |
| 180 | +### 클래스 / 메서드 네이밍 |
| 181 | + |
| 182 | +- 루트 테스트 클래스: `{원본클래스명}Test` |
| 183 | +- inner 테스트 클래스: `{메서드명}Test` (ex. `alarmOff` → `AlarmOffTest`) |
| 184 | +- 테스트 메서드: `success` / `fail_{비즈니스_관점_실패이유}` (ex. `fail_weeklyLimitExceeded`, `fail_memberNotFound`) |
| 185 | + |
| 186 | +### DisplayName 규칙 |
| 187 | + |
| 188 | +- 문장형으로 작성. "~테스트" 금지 |
| 189 | +- 결과까지 기술: `"성공: 주간 OFF 한도 내에서 알람을 끈다"` / `"실패: 한도 초과 시 예외를 던진다"` |
| 190 | +- inner class의 `@DisplayName`: `"{메서드명} - {한글 기능 설명}"` (ex. `"alarmOff - 알람 끄기(OFF)"`) |
| 191 | +- 도메인 용어 사용 (메서드 이름 관점 X, 정책 관점 O) |
| 192 | + |
| 193 | +### BDD 스타일 (Given / When / Then) |
| 194 | + |
| 195 | +모든 테스트는 `// given`, `// when`, `// then` 주석으로 구분한다. |
| 196 | + |
| 197 | +### 테스트 픽스처 |
| 198 | + |
| 199 | +`src/test/java/akuma/whiplash/common/fixture/`에 enum 기반 픽스처 존재: |
| 200 | +- `MemberFixture` — 테스트용 멤버 (`MEMBER_1` ~ `MEMBER_N`), `toMockEntity()` 제공 |
| 201 | +- `AlarmFixture` — 테스트용 알람 (`ALARM_01` ~ `ALARM_N`), `toMockEntity()` 제공 |
| 202 | +- `AlarmOccurrenceFixture` |
| 203 | + |
| 204 | +### FCM 테스트 |
| 205 | + |
| 206 | +프로파일 `test`에서는 `MockFcmService`가 자동으로 등록되어 실제 FCM 요청을 보내지 않는다. |
| 207 | + |
| 208 | +## 커밋 메시지 컨벤션 |
| 209 | + |
| 210 | +``` |
| 211 | +[#이슈번호] :Emoji: <type>: <subject> |
| 212 | +
|
| 213 | +<body> |
| 214 | +- 파일명 |
| 215 | + - 변경 내용 |
| 216 | +
|
| 217 | +<footer> |
| 218 | +- 해결: #이슈번호 |
| 219 | +``` |
| 220 | + |
| 221 | +| Type | Emoji | 설명 | |
| 222 | +|---|---|---| |
| 223 | +| Feature | ✨ | 새로운 기능 추가 | |
| 224 | +| Fix | 🐛 | 버그 수정 | |
| 225 | +| Docs | 📝 | 문서 수정 | |
| 226 | +| Style | 🎨 | 코드 포맷팅 (로직 변경 없음) | |
| 227 | +| Refactor | ♻️ | 리팩토링 | |
| 228 | +| Test | ✅ | 테스트 코드 추가/수정 | |
| 229 | +| Chore | 🔧 | 빌드, 패키지 매니저 수정 | |
| 230 | + |
| 231 | +Subject 규칙: 50자 이하, 마침표 없음, 한글 개조식 또는 영문 동사원형 대문자 시작. |
| 232 | + |
| 233 | +## 인프라 의존성 |
| 234 | + |
| 235 | +| 서비스 | 용도 | 설정 파일 | |
| 236 | +|---|---|---| |
| 237 | +| MySQL 8.0 | 메인 DB | `mysql.yml` | |
| 238 | +| Redis 7.2 | 토큰 캐시, 알람 울림 상태 (`alarm:ringing` Sorted Set) | `redis.yml` | |
| 239 | +| Firebase FCM | 푸시 알림 | `whiplash-firebase-key.json` | |
| 240 | +| Google Sheets API | 알람 삭제 사유 로깅 | `oauth.yml` | |
| 241 | +| Prometheus + Grafana | 메트릭 수집/시각화 | `docker-compose.yml` | |
| 242 | +| Sentry | 에러 트래킹 | `sentry.yml` | |
| 243 | + |
| 244 | + |
| 245 | +## 스프링 프로파일 |
| 246 | + |
| 247 | +`local` / `dev` / `qa` / `prod` — `--spring.profiles.active={profile}`으로 지정. |
| 248 | +각 프로파일은 `resources/` 하위의 `mysql.yml`, `redis.yml` 등을 import한다. |
| 249 | + |
| 250 | +## Claude Code 관련 질문 처리 |
| 251 | + |
| 252 | +claude-code-guide는 틀린 답을 낼 때가 있다. 사용자가 Claude Code 기능에 대해 추가 질문을 하면, 공식 문서를 curl로 직접 참조해서 답한다. |
| 253 | + |
| 254 | +```bash |
| 255 | +curl https://code.claude.com/docs/ko/overview.md |
| 256 | +``` |
| 257 | + |
| 258 | +문서 URL 패턴: `https://code.claude.com/docs/ko/{페이지명}.md` |
| 259 | + |
| 260 | +답변 후에는 `AskUserQuestion`으로 퀴즈를 내서 사용자가 직접 따라해보도록 안내한다. |
0 commit comments