Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 64 additions & 41 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,67 +4,90 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project Overview

Form PASS is an event reservation system built with Spring Boot 3.4.12 and Java 17. It allows hosts to manage events, schedules, and reservations with form validation and check-in functionality.
Form PASS is an event reservation system built with Spring Boot 3.4.12 and Java 17. Hosts create events with time-slot schedules and custom form questions; guests reserve tickets and check in via QR codes. Base package: `com.example.reservation_solution`.

## Build Commands

```bash
# Run all tests
./gradlew clean test

# Build without tests
./gradlew clean build -x test

# Run locally (requires MySQL on localhost:3306)
./gradlew bootRun

# Run a specific test
./gradlew test --tests "TestClassName"

# Build production JAR (produces ticket-form.jar)
./gradlew bootJar
./gradlew clean test # Run all tests
./gradlew clean build -x test # Build without tests
./gradlew bootRun # Run locally (default profile: dev)
./gradlew test --tests "TestClassName" # Run a specific test
./gradlew bootJar # Build production JAR (ticket-form.jar)
```

## Architecture

**Layered Structure:**
**Domain-based package structure under `api/`:**
```
controller/ → REST API endpoints (guest/ and host/ packages)
service/ → Business logic
repository/ → JPA data access with custom queries
domain/ → JPA entities
dto/ → Request/Response objects
global/ → Cross-cutting concerns (config, security, exception, mail, image, util)
api/
auth/ → Host signup, login, JWT tokens, email verification
event/ → Event CRUD, schedules, form questions (guest + host controllers)
reservation/ → Guest reservation creation, lookup, cancellation
host/ → Host-only: dashboard stats, reservation list, QR/manual check-in
global/
security/ → JwtProvider, JwtAuthenticationFilter, HostUserDetailsService
exception/ → BusinessException hierarchy + GlobalExceptionHandler
config/ → SecurityConfig, SwaggerConfig, AwsConfig
mail/ → Async email sending (Google SMTP)
image/ → S3 presigned URL generation
common/domain/ → BaseTimeEntity (createdAt/updatedAt auditing)
docs/ → Swagger @Operation annotation interfaces per endpoint
util/ → EncryptionUtils (AES for phone numbers), CodeGenerator
```

**Key Domain Models:**
- `Host` → Event organizer account
- `Event` → Event definition with unique eventCode
- `EventSchedule` → Time slots with capacity (uses pessimistic locking)
- `Reservation` → Booking with QR token for check-in
- `FormQuestion` / `FormAnswer` → Custom form fields per event
Each domain package follows: `controller/ → service/ → repository/ → domain/ → dto/`

**Key Domain Relationships:**
- `Host` → owns many `Event` (via eventId)
- `Event` → has `EventSchedule[]`, `FormQuestion[]`, `EventImage[]`
- `EventSchedule` → has `Reservation[]` (capacity-controlled with pessimistic locking)
- `Reservation` → has `FormAnswer[]`, a `qrToken` (UUID) for check-in, and `ReservationStatus` (CONFIRMED/CANCELLED)

## Conventions

- **Lombok** is used throughout (entities, DTOs). Use `@Getter`, `@NoArgsConstructor(access = PROTECTED)` on entities; `@Builder` on DTOs
- **Error messages are in Korean** (e.g., `"존재하지 않는 이벤트입니다"`)
- **Exception pattern**: Services throw `IllegalArgumentException` (400), `IllegalStateException` (409), `AccessDeniedException` (403), or `BusinessException` subclasses (`BadRequestException`, `ConflictException`, `ForbiddenException`, `NotFoundException`, `UnauthorizedException`). All handled by `GlobalExceptionHandler`
- **Host ownership validation**: Services verify `event.getHost().getEmail().equals(hostEmail)` before allowing mutations
- **Transactions**: Services use `@Transactional(readOnly = true)` at class level, `@Transactional` on write methods
- **Phone number encryption**: Guest phone numbers are encrypted via `EncryptionUtils` (AES-256) before storage and decrypted on read
- **Swagger docs**: Each controller method's `@Operation` annotation is defined in a separate interface in `global/docs/` (e.g., `EventControllerDocs`), which the controller implements
- **Entity creation**: Entities use static factory methods (`create(...)`) instead of public constructors

## Concurrency Control

- `EventSchedule.reservedCount` is protected by `@Lock(LockModeType.PESSIMISTIC_WRITE)` in `EventScheduleRepository` to prevent overbooking race conditions
- Duplicate reservation check: same phone number + same schedule combination is rejected

## Security

- JWT Bearer Token 인증 (Authorization 헤더), 30분 만료
- Public endpoints: `/api/auth/**`, `/api/events/**`, `/api/reservations/**`
- Protected endpoints: `/api/host/**` (requires HOST role)
- Guest phone numbers are encrypted in database using `EncryptionUtils`
- JWT Bearer Token auth (Authorization header), 30-minute expiry, HS256 signing (jjwt 0.11.5)
- `JwtAuthenticationFilter` skips `/api/auth/**` paths
- Public: `/api/auth/**`, `/api/events/**`, `/api/reservations/**`, `/actuator/**`, Swagger UI
- Protected: `/api/host/**` requires `ROLE_HOST`
- Authenticated user's email is extracted from `SecurityContextHolder` principal in controllers
- CORS allowed origins: `localhost:3000`, `localhost:3001`, `form-pass.life`, `form-pass-client.vercel.app`

## Testing

- Test profile uses H2 in-memory database (see `application-test.yml`)
- Test profile (`application-test.yml`): H2 in-memory DB with `MODE=MySQL`, mock mail config, dedicated JWT secret
- Tests run with: `./gradlew clean test`
- Swagger UI: `http://localhost:8080/swagger-ui.html`
- Currently only a context-load test exists (`ReservationSolutionApplicationTests`)

## Key Technical Details
## Profiles

- **Concurrency**: `EventSchedule` uses `@Lock(LockModeType.PESSIMISTIC_WRITE)` to prevent overselling
- **Email Verification**: Uses Caffeine cache for storing verification codes
- **File Uploads**: AWS S3 presigned URLs for direct client uploads
- **Async**: `@EnableAsync` enabled for background tasks like email sending
- `dev` (default, set in `application.yml`): Production RDS MySQL, Prometheus monitoring enabled
- `local`: localhost MySQL on port 3306, `ddl-auto: create`, debug logging
- `test`: H2 in-memory, used automatically in test runs

## CI/CD

- **CI**: Pull requests to main trigger `./gradlew clean test`
- **CD**: Push to main builds JAR and deploys to EC2 via SSH
- **CI**: PRs to `main` → `./gradlew clean test` + publish test results as PR comments
- **CD**: Push to `main` → build JAR (skip tests) → SCP to EC2 → run deploy script
- `application-dev.yml` is injected from GitHub Secrets (`APPLICATION_DEV_YML`) during CD

## Key Dependencies

- **JWT**: jjwt 0.11.5 | **AWS SDK**: v2.27.21 (S3) | **API Docs**: Springdoc OpenAPI 2.7.0
- **Monitoring**: Spring Actuator + Micrometer Prometheus | **Cache**: Caffeine
Loading