diff --git a/.claude/rules/comments.md b/.claude/rules/comments.md new file mode 100644 index 000000000..9b191f4b5 --- /dev/null +++ b/.claude/rules/comments.md @@ -0,0 +1,25 @@ +# Comment Rules + +Write comments only when the logic is not self-evident. Do NOT add excessive comments. + +## When to write + +- Non-obvious business rules or domain constraints +- Workarounds for specific bugs or framework quirks +- Hidden invariants that a future reader could not infer from the code + +## When NOT to write + +- Restating what well-named identifiers already convey +- Narrating step-by-step what the code does +- Marking referenced tickets, callers, or the current task (those belong in the commit/PR description) + +```kotlin +// WRONG — restates the obvious +// Find student by id +val student = studentRepository.findById(id).orElseThrow { ... } + +// CORRECT — surfaces a non-obvious constraint +// NEIS API returns 0 instead of null for "no allergy", so treat 0 as absent. +val allergy = response.allergyCode.takeIf { it != 0 } +``` diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 000000000..b64f3e75a --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,35 @@ +# Testing Rules + +## Framework + +Use **Kotest `DescribeSpec` + MockK + JUnit 5** with the **Given-When-Then** structure. + +## Naming + +Test names are written in Korean. Use `describe("클래스명 클래스의")` for the top-level scope. + +```kotlin +class StudentServiceImplTest : DescribeSpec({ + describe("StudentServiceImpl 클래스의") { + context("execute 메서드를 호출할 때") { + it("학생을 조회한다") { + // Given + val id = 1L + every { studentRepository.findById(id) } returns Optional.of(student) + + // When + val result = service.execute(id) + + // Then + result.id shouldBe id + } + } + } +}) +``` + +## Structure + +- **Given**: prepare data, configure mocks (`every { } returns ...`) +- **When**: invoke the system under test +- **Then**: assert results (`shouldBe`, `shouldThrow`, `verify { }`) diff --git a/AGENTS.md b/AGENTS.md index 9c4f30963..8cba3b41c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,99 +2,57 @@ ## Project Overview -DataGSM is a Spring Boot REST API service providing school information (students, clubs, meals, schedules) for Gwangju Software Meister High School. The system uses Google OAuth2 authentication with JWT token and API key management. +DataGSM is a Spring Boot REST API server that exposes Gwangju Software Meister High School data (students, clubs, meals, timetable, etc.). It provides Google OAuth2 (JWT) authentication for internal clients and API-Key authentication for external public APIs. ## Tech Stack -- **Backend**: Kotlin, Spring Boot 4.0, Spring Security, Spring Data JPA -- **Database**: MySQL (main data), Redis (caching, sessions) -- **Query & Integration**: QueryDSL for complex queries, OpenFeign for external APIs -- **Serialization**: Jackson 3.0 for JSON processing -- **Testing**: Kotest + MockK + JUnit 5 (Given-When-Then style) +- **Language / Framework**: Kotlin 2.3.10, Spring Boot 4.0.3, Spring Security, Spring Data JPA +- **Build**: Gradle (Java 25 toolchain), multi-module +- **Database**: MySQL (main), Redis (cache, session) +- **Query / Integration**: QueryDSL, OpenFeign +- **Serialization**: Jackson 3.0 +- **Testing**: Kotest (`DescribeSpec`) + MockK + JUnit 5 -## Project Structure (Multi-module) +## Project Structure ``` datagsm-server/ -├── datagsm-common/ # Shared library (Entity, DTO, Repository, Config, Health API) +├── datagsm-common/ # Shared Entity/DTO/Repository/Config, Health API (library module) ├── datagsm-oauth-authorization/ # OAuth2 authentication, account lifecycle (signup, password reset) -├── datagsm-oauth-userinfo/ # OAuth2 UserInfo endpoint (external clients) -├── datagsm-openapi/ # Public read-only API (students, clubs, NEIS) -└── datagsm-web/ # Web service API (user features, admin features, Excel) +├── datagsm-oauth-userinfo/ # OAuth2 UserInfo endpoint (for external clients) +├── datagsm-openapi/ # External public API (API-Key auth) +│ # Domains: student, club, project, webhook, neis +└── datagsm-web/ # Web service API (includes Excel processing) + # Domains: account, auth, application, client, student, club, project, utility ``` -Each module follows: `controller/`, `service/`, `repository/`, `entity/`, `dto/` +Each module follows the `controller/ → service/ → repository/` layering with `entity/` and `dto/` packages per domain. -**Note**: `/v1/health` endpoint is provided by `HealthController` in `datagsm-common/global/controller/` and is shared across all modules. +**Key Paths** -## Commands +- The `/v1/health` endpoint is served by `HealthController` in `datagsm-common` and is shared across all runnable modules. +- Shared entities: `datagsm-common/src/main/kotlin/team/themoment/datagsm/common/domain/` +- Global exception handling: `datagsm-common/src/main/kotlin/team/themoment/datagsm/common/global/common/error/` +- API response: controllers return DTOs directly — `the-sdk`'s `ResponseBodyAdvice` automatically wraps them in `CommonApiResponse`. Use `CommonApiResponse` explicitly only when no data is returned (e.g. delete operations). Exception responses are wrapped by `GlobalExceptionHandler`. -- Build: `./gradlew build` -- Test: `./gradlew test` -- Format: `./gradlew ktlintFormat` -- Run: `./gradlew ::bootRun` +## Runnable Modules -## Coding Conventions +Modules launched via `./gradlew ::bootRun`: `datagsm-oauth-authorization`, `datagsm-oauth-userinfo`, `datagsm-openapi`, `datagsm-web`. (`datagsm-common` is a library module and is not run directly.) -### Kotlin Style +## Detailed Rules (`.claude/rules/`) -- Prefer `val` over `var`. Use `var` only when reassignment is strictly required. -- Always use constructor injection — never `@Autowired` field injection. -- Use Kotlin null-safety features (`?.`, `?:`) instead of `!!`. -- Do NOT add excessive comments — only where logic is not self-evident. +The following rule files are auto-loaded when working on related code: -### DTO Annotations - -- Jackson: always use `@field:` target — never `@param:` (e.g., `@field:JsonProperty("user_name")`) -- Swagger/OpenAPI: - - Request DTOs (`*ReqDto`): use `@param:Schema` - - Response DTOs (`*ResDto`): use `@field:Schema` - -### API Conventions - -- 1–2 query params: use `@RequestParam`; 3+ or with validation: use `@ModelAttribute` + DTO -- `@RequestBody` variable: `reqDto`; `@ModelAttribute` query: `queryReq` -- `@Transactional` must be at **method level only** — never class level -- Read operations: `@Transactional(readOnly = true)` / Write operations: `@Transactional` -- Use `CommonApiResponse` wrapper for all API responses - -### Logging - -- English only — verb-led sentences -- SLF4J `{}` placeholder only — no Kotlin string interpolation, no colon separators -- Correct: `logger().info("Deleted {} expired API keys", deletedCount)` -- Wrong: `logger().error("에러 발생: $message")` or `logger().error("Failed: {}", msg)` - -### Exception Handling - -- Use `ExpectedException` directly — do NOT subclass it -- Message: Korean (합쇼체) + period, no dynamic data (IDs, names, variables) -- Correct: `ExpectedException("학생을 찾을 수 없습니다.", HttpStatus.NOT_FOUND)` -- Wrong: `ExpectedException("학생 ID: $id 없음", HttpStatus.NOT_FOUND)` - -### Commit Conventions - -Format: `type(scope): 설명` - -- Types: `add` / `update` / `fix` / `refactor` / `ci/cd` / `docs` / `test` / `merge` -- Scope: domain name (`auth`, `student`, `club`, `application`, etc.) — NOT module names -- Cross-cutting only: `global`, `ci/cd`, or module names (`web`, `openapi`, `oauth`) -- Description: Korean, no period - -## Key Practices - -### JPA -- Avoid N+1 problems — use Fetch Join or `@EntityGraph` -- Use `@Transactional(readOnly = true)` for read operations - -### Testing -- Write Kotest tests for business logic -- Use Kotest `DescribeSpec` with `describe/context/it` blocks -- Use MockK for mocking; Given-When-Then structure inside `it` blocks -- Test names in Korean: `describe("클래스명 클래스의")`, `describe("메서드명 메서드는")` +- `kotlin-style.md` — `val/var`, constructor injection, null safety +- `dto-annotations.md` — Jackson/Swagger `@field:` vs `@param:` targets +- `api-conventions.md` — `@RequestParam` vs `@ModelAttribute`, DTO naming, `@Transactional` placement +- `logging.md` — English only, SLF4J `{}` placeholder, no colon separators +- `exception.md` — `ExpectedException` usage and message format +- `testing.md` — Kotest `DescribeSpec` + MockK + Given-When-Then structure +- `comments.md` — when to write comments (only for non-obvious logic) +- `commit-conventions.md` — commit `type(scope): description` rules (scope = domain name) ## Notes -- This project uses Java 25 for Gradle builds -- Always check `.gitignore` and `.geminiignore` when suggesting file changes -- When analyzing code, consider the multi-module structure +- Always check `.gitignore` before proposing file changes. +- When analyzing code, consider the multi-module structure and inter-module dependencies (`datagsm-common` is the base for every runnable module). diff --git a/CLAUDE.md b/CLAUDE.md index b42b308e5..a22182026 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,55 +1,5 @@ -## Project Overview +@AGENTS.md -School information API server for Gwangju Software Meister High School (students, clubs, meals, schedules) +## Claude Code Specific -## Module Structure - -- datagsm-common: Shared Entity/DTO/Repository, Health API -- datagsm-oauth-authorization: OAuth2 authentication, account lifecycle (signup, password reset) -- datagsm-oauth-userinfo: OAuth2 UserInfo endpoint (external clients) -- datagsm-openapi: Public read-only API (students, clubs, NEIS integration) -- datagsm-web: Web service API (user features, admin features, Excel processing) - -**Note**: `/v1/health` endpoint is provided by `HealthController` in `datagsm-common` module and is shared across all modules. - -## Commands - -- Build: `./gradlew build` -- Test: `./gradlew test` -- Format: `./gradlew ktlintFormat` -- Run: `./gradlew ::bootRun` (modules: `datagsm-oauth-authorization`, `datagsm-openapi`, `datagsm-oauth-userinfo`, `datagsm-web`) - -## Tech Stack - -Kotlin, Spring Boot 4.0, Spring Data JPA, QueryDSL, Redis, MySQL - -## Coding Rules - -- Controller → Service → Repository pattern -- Use constructor injection -- Test: Kotest + MockK (Given-When-Then) -- Do NOT add excessive comments - only add comments where logic is not self-evident - -Detailed rules are split into `.claude/rules/`: -- `dto-annotations.md` — `@field:` vs `@param:` rules for Jackson and Swagger -- `logging.md` — English-only, SLF4J `{}` placeholders, no colon separators -- `exception.md` — `ExpectedException` usage and message format -- `kotlin-style.md` — `val/var`, constructor injection, null safety -- `api-conventions.md` — `@RequestParam` vs `@ModelAttribute`, DTO naming, `@Transactional` placement -- `commit-conventions.md` — commit type/scope rules - -## Context Compaction Rules - -Priority order when compressing conversation history: -1. Project Overview (module structure) -2. Common Mistakes section -3. DTO Annotations rules -4. Commit/PR conventions -5. Tech Stack -6. Reduce: Commands, Key Paths - -## Key Paths - -- Common Entity: `datagsm-common/src/main/kotlin/.../domain/` -- Exception Handler: `datagsm-common/.../global/common/error/` -- API Response: Use `CommonApiResponse` wrapper +- `.claude/rules/`의 세부 규칙은 Claude Code가 작업 컨텍스트에 맞춰 자동 로드합니다. 자동 로드된 규칙과 충돌하는 코드를 발견하면 단순 수정에 그치지 말고 규칙 파일에 누락된 항목이 있는지 점검하고 보완을 제안하세요. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cdc856ea6..f873c6da5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ DataGSM 프로젝트에 기여해주셔서 감사합니다! 이 문서는 프로 프로젝트 개발을 위해 다음 환경이 필요합니다: - **JDK**: Java 25 (Temurin 권장) -- **Kotlin**: 2.3.0 (자동 설정) +- **Kotlin**: 2.3.10 (자동 설정) - **Gradle**: 최신 버전 (래퍼 사용) - **Docker & Docker Compose**: 로컬 개발용 - **MySQL**: 8.0