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
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@ bin/
init/
docker-compose-monitoring.yml
lock-test.yaml
prometheus.yml
reservation-lookup-performance-test.yaml
result.json
result.json.html

### Spring Boot 환경설정 파일 무시 (보안 필수) ###

.env

# 혹시 최상위에 뒀을 경우를 대비해 추가
application-dev.yml
Expand Down
70 changes: 70 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## 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.

## 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
```

## Architecture

**Layered Structure:**
```
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)
```

**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

## 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`

## Testing

- Test profile uses H2 in-memory database (see `application-test.yml`)
- Tests run with: `./gradlew clean test`
- Swagger UI: `http://localhost:8080/swagger-ui.html`

## Key Technical Details

- **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

## CI/CD

- **CI**: Pull requests to main trigger `./gradlew clean test`
- **CD**: Push to main builds JAR and deploys to EC2 via SSH
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.reservation_solution.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.util.Map;

@Tag(name = "Health Check", description = "서버 상태 확인 API")
@RestController
public class HealthCheckController {

@Operation(summary = "헬스 체크", description = "서버 상태를 확인합니다.")
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> healthCheck() {
return ResponseEntity.ok(Map.of(
"status", "UP",
"timestamp", LocalDateTime.now().toString()
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,12 @@
import com.example.reservation_solution.dto.LoginResponse;
import com.example.reservation_solution.dto.SignupRequest;
import com.example.reservation_solution.global.docs.LoginDocs;
import com.example.reservation_solution.global.docs.LogoutDocs;
import com.example.reservation_solution.global.docs.SignupDocs;
import com.example.reservation_solution.global.security.CookieUtils;
import com.example.reservation_solution.service.AuthService;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -39,19 +35,6 @@ public ResponseEntity<Void> signup(@Valid @RequestBody SignupRequest request) {
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
String token = authService.generateLoginToken(request);
ResponseCookie cookie = CookieUtils.createAccessTokenCookie(token);
LoginResponse response = LoginResponse.success(request.email());
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.body(response);
}

@LogoutDocs
@PostMapping("/logout")
public ResponseEntity<Void> logout() {
ResponseCookie cookie = CookieUtils.deleteAccessTokenCookie();
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.build();
return ResponseEntity.ok(LoginResponse.of(token, request.email()));
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
package com.example.reservation_solution.dto;

/**
* 로그인 성공 응답 DTO
* JWT 토큰은 HttpOnly 쿠키로 전송되므로 응답 본문에는 성공 메시지만 포함
*/
public record LoginResponse(
String message,
String accessToken,
String email
) {
/**
* 로그인 성공 응답 생성
*
* @param email 로그인한 사용자 이메일
* @return LoginResponse 객체
*/
public static LoginResponse success(String email) {
return new LoginResponse("로그인이 성공적으로 완료되었습니다.", email);
public static LoginResponse of(String accessToken, String email) {
return new LoginResponse(accessToken, email);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers("/actuator/**").permitAll()
.requestMatchers("/health").permitAll()
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/h2-console/**").permitAll()
.requestMatchers("/api/events/**").permitAll()
Expand All @@ -57,6 +58,7 @@ public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of(
"http://localhost:3000",
"http://localhost:3001",
"https://www.form-pass.life",
"https://form-pass.life",
"https://form-pass-client.vercel.app"
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -32,32 +31,7 @@ protected void doFilterInternal(HttpServletRequest request,
filterChain.doFilter(request, response);
}


private String resolveToken(HttpServletRequest request) {
String tokenFromCookie = resolveTokenFromCookie(request);
if (tokenFromCookie != null) {
return tokenFromCookie;
}
return resolveTokenFromHeader(request);
}

private String resolveTokenFromCookie(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return null;
}
for (Cookie cookie : cookies) {
if (CookieUtils.ACCESS_TOKEN_COOKIE_NAME.equals(cookie.getName())) {
String token = cookie.getValue();
if (StringUtils.hasText(token)) {
return token;
}
}
}
return null;
}

private String resolveTokenFromHeader(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ spring:
application:
name: ticket-form
profiles:
active: local
active: dev

jwt:
expiration: 1800000 # 30분
Loading