-
Notifications
You must be signed in to change notification settings - Fork 0
✨Feat: 운동 매칭 기능 추가 #56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4195525
3f08717
050d691
95f838f
beadb16
556ad13
61c37d5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| # CLAUDE.md | ||
|
|
||
| This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||
|
|
||
| ## Tech Stack | ||
|
|
||
| - **Java 21 / Spring Boot 4.0.1** (Gradle) | ||
| - **PostgreSQL 16 + PostGIS** (공간 쿼리), **Redis 7** (캐시) | ||
| - **Spring Security + JWT** (Stateless, `Authorization: Bearer <token>`) | ||
| - **WebSocket / STOMP** (`/ws-stomp` 엔드포인트) | ||
| - **AWS S3** (프로필·클럽·게시글 이미지) | ||
| - **Swagger UI** → `http://localhost:8080/swagger-ui/index.html` | ||
|
|
||
| --- | ||
|
|
||
| ## Commands | ||
|
|
||
| ```bash | ||
| # 로컬 인프라 실행 (PostgreSQL+PostGIS, Redis, Prometheus, Grafana) | ||
| docker compose -f docker/docker-compose.local.yml up -d | ||
|
|
||
| # 빌드 | ||
| ./gradlew build | ||
|
|
||
| # 로컬 프로필로 실행 (application-local.properties 사용) | ||
| ./gradlew bootRun --args='--spring.profiles.active=local' | ||
|
|
||
| # 테스트 전체 실행 | ||
| ./gradlew test | ||
|
|
||
| # 단일 테스트 클래스 실행 | ||
| ./gradlew test --tests "com.be.sportizebe.domain.user.service.UserServiceImplTest" | ||
| ``` | ||
|
|
||
| ### 로컬 환경 변수 (application-local.properties가 읽는 값) | ||
|
|
||
| `application-local.properties`는 환경 변수로 주입받는 값이 있다. | ||
| IDE나 `.env` 파일에 아래를 설정해야 앱이 기동된다. | ||
|
|
||
| ``` | ||
| POSTGRES_USER=angora | ||
| POSTGRES_PASSWORD=password | ||
| AWS_ACCESS_KEY=... | ||
| AWS_SECRET_KEY=... | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Project Structure | ||
|
|
||
| ``` | ||
| src/main/java/com/be/sportizebe/ | ||
| ├── domain/ # 비즈니스 도메인 (아래 참고) | ||
| │ ├── auth/ | ||
| │ ├── user/ | ||
| │ ├── club/ | ||
| │ ├── post/ | ||
| │ ├── comment/ | ||
| │ ├── like/ | ||
| │ ├── match/ | ||
| │ ├── facility/ | ||
| │ ├── notification/ | ||
| │ └── chat/ | ||
| └── global/ # 공통 인프라 | ||
| ├── cache/ # Redis 캐시 설정, UserCacheService, UserAuthInfo DTO | ||
| ├── config/ # CORS, S3, Redis, Swagger, Jackson 설정 | ||
| ├── exception/ # CustomException, GlobalExceptionHandler, BaseErrorCode | ||
| ├── jwt/ # JwtProvider, JwtAuthenticationFilter | ||
| ├── response/ # BaseResponse<T> (공통 응답 래퍼) | ||
| ├── s3/ # S3Service (upload/delete), PathName enum | ||
| └── security/ # SecurityConfig, CustomUserDetailService | ||
| ``` | ||
|
|
||
| 각 도메인은 `controller / dto / entity / repository / service / exception` 패키지로 분리된다. | ||
|
|
||
| --- | ||
|
|
||
| ## Key Architecture Decisions | ||
|
|
||
| ### 인증 흐름 | ||
| `JwtAuthenticationFilter` → `UserCacheService.findUserAuthInfoById()` → Redis 캐시 (TTL 5분, 미스 시 DB 조회) | ||
| `@AuthenticationPrincipal UserAuthInfo`로 컨트롤러에서 꺼낸다. `User` 엔티티가 아닌 `UserAuthInfo` (Serializable DTO)를 principal로 사용하는 이유는 JPA 연관관계로 인한 직렬화 문제를 방지하기 위함이다. | ||
|
|
||
| ### 보안 규칙 (SecurityConfig) | ||
| - `GET /api/**` — 인증 없이 허용 | ||
| - `POST /api/auth/**`, `POST /api/users/signup` — 인증 없이 허용 | ||
| - `/ws-stomp/**` — 인증 없이 허용 | ||
| - 그 외 — `authenticated()` | ||
|
|
||
| ### 캐시 전략 (Redis) | ||
| | 캐시명 | 키 | TTL | 무효화 시점 | | ||
| |--------|----|-----|------------| | ||
| | `userAuthInfo` | `userId` | 5분 | 프로필/이미지 수정 | | ||
| | `postList` | property+pageable | 5분 | 게시글 생성/수정/삭제 | | ||
| | `commentList` | `postId` | 5분 | 댓글 생성/삭제 | | ||
| | `commentCount` | `postId` | 5분 | 댓글 생성/삭제 | | ||
| | `likeCount` / `likeStatus` | `targetType:targetId` | 5분 | 좋아요 토글 | | ||
| | `facilityNear` / `facilityMarkers` | 좌표+반경 | 5분 | — | | ||
|
|
||
| ### 공간 쿼리 (Facility) | ||
| `SportsFacilityRepository`는 PostGIS Native SQL (`ST_DWithin`, `ST_Distance`) 사용. | ||
| `SportsFacility.location`은 JTS `Point` (SRID 4326). `hibernate-spatial` 의존성 필요. | ||
|
|
||
| ### WebSocket / STOMP 채팅 | ||
| - 연결: `ws://host/ws-stomp` | ||
| - 메시지 발행: `SEND /pub/chat.send | chat.join | chat.leave` | ||
| - 구독: `SUBSCRIBE /sub/chat/rooms/{roomId}` | ||
| - 실시간 알림 구독: `SUBSCRIBE /sub/notifications/{userId}` | ||
| - 채팅방 종류: `GROUP` (동호회 생성 시 자동 생성), `NOTE` (게시글 기반 1:1 쪽지) | ||
|
|
||
| ### 파일 업로드 (S3) | ||
| `S3Service.uploadFile(PathName, MultipartFile)` → S3 버킷의 `{PathName}/{UUID}.{ext}` 경로에 저장. | ||
| `PathName` enum: `PROFILE`, `CLUB`, `POST`. | ||
|
|
||
| ### 응답 규격 | ||
| 모든 REST 응답은 `BaseResponse<T>` 래퍼를 사용한다: | ||
| ```json | ||
| { "success": true, "message": "...", "data": { ... } } | ||
| ``` | ||
| 에러는 `GlobalExceptionHandler`가 `CustomException(BaseErrorCode)` 를 잡아 동일 포맷으로 반환한다. | ||
|
|
||
| --- | ||
|
|
||
| ## Git Convention | ||
|
|
||
| 커밋 접두사 (이모지 + 태그): | ||
| - `✨ Feat:` 새 기능 | `🐛 Fix:` 버그 수정 | `♻️ Refactor:` 리팩토링 | ||
| - `🔧 Settings:` 설정 변경 | `📝 Docs:` 문서 | `🔥 Remove:` 파일 삭제 | ||
| - `⏪️ Revert:` 롤백 | `🚀 Deploy:` 배포 | ||
|
|
||
| 브랜치: `feature/{description}` → `develop` → `main` | ||
| 서브모듈 업데이트 시 커밋 메시지: `"submodule push"` / `"submodule latest"` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| global: | ||
| scrape_interval: 5s | ||
|
|
||
| scrape_configs: | ||
| - job_name: "redis-exporter" | ||
| static_configs: | ||
| - targets: ["redis-exporter:9121"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,5 +2,7 @@ | |
|
|
||
| public enum SportType { | ||
| SOCCER, | ||
| BASKETBALL | ||
| BASKETBALL, | ||
| BADMINTON, | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,12 @@ | ||
| // src/main/java/com/be/sportizebe/domain/facility/controller/SportsFacilityController.java | ||
| package com.be.sportizebe.domain.facility.controller; | ||
|
|
||
| import com.be.sportizebe.domain.facility.dto.request.FacilityCreateRequest; | ||
| import com.be.sportizebe.domain.facility.dto.request.FacilityMarkerRequest; | ||
| import com.be.sportizebe.domain.facility.dto.request.FacilityNearRequest; | ||
| import com.be.sportizebe.domain.facility.dto.response.FacilityMarkerResponse; | ||
| import com.be.sportizebe.domain.facility.dto.response.FacilityNearResponse; | ||
| import com.be.sportizebe.domain.facility.dto.response.FacilityResponse; | ||
| import com.be.sportizebe.domain.facility.service.SportsFacilityService; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
|
|
@@ -40,4 +42,16 @@ public List<FacilityMarkerResponse> markers( | |
| ) { | ||
| return sportsFacilityService.getMarkers(request); | ||
| } | ||
|
|
||
| @Operation(summary = "체육시설 등록", description = "체육시설을 등록합니다. (관리자/개발자용)") | ||
| @PostMapping | ||
| public FacilityResponse create(@Valid @RequestBody FacilityCreateRequest request) { | ||
| return sportsFacilityService.create(request); | ||
| } | ||
|
Comment on lines
+46
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Swagger 설명에 "(관리자/개발자용)"이라고 명시되어 있지만, 실제로 권한 검증 로직( 🔒 인가 및 응답 상태 코드 개선 제안 `@Operation`(summary = "체육시설 등록", description = "체육시설을 등록합니다. (관리자/개발자용)")
`@PostMapping`
- public FacilityResponse create(`@Valid` `@RequestBody` FacilityCreateRequest request) {
- return sportsFacilityService.create(request);
+ `@ResponseStatus`(HttpStatus.CREATED)
+ public FacilityResponse create(`@Valid` `@RequestBody` FacilityCreateRequest request) {
+ return sportsFacilityService.create(request);
}인가 처리는 프로젝트의 보안 설정 방식에 따라 별도로 적용해 주세요. 🤖 Prompt for AI Agents |
||
|
|
||
| @Operation(summary = "체육시설 단건 조회", description = "facilityId로 체육시설을 단건 조회합니다.") | ||
| @GetMapping("/{facilityId}") | ||
| public FacilityResponse getById(@PathVariable Long facilityId) { | ||
| return sportsFacilityService.getById(facilityId); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,38 @@ | ||||||||||||||||||||||||||||||||||||||||||
| package com.be.sportizebe.domain.facility.dto.request; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| import com.be.sportizebe.domain.facility.entity.FacilityType; | ||||||||||||||||||||||||||||||||||||||||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||||||||||||||||||||||||||||||||||||||||
| import jakarta.validation.constraints.DecimalMax; | ||||||||||||||||||||||||||||||||||||||||||
| import jakarta.validation.constraints.DecimalMin; | ||||||||||||||||||||||||||||||||||||||||||
| import jakarta.validation.constraints.NotBlank; | ||||||||||||||||||||||||||||||||||||||||||
| import jakarta.validation.constraints.NotNull; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| @Schema(description = "체육시설 등록 요청") | ||||||||||||||||||||||||||||||||||||||||||
| public record FacilityCreateRequest( | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| @Schema(description = "체육시설 이름", example = "OO 풋살장") | ||||||||||||||||||||||||||||||||||||||||||
| @NotBlank | ||||||||||||||||||||||||||||||||||||||||||
| String facilityName, | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| @Schema(description = "시설 소개", example = "샤워실/주차장 있음") | ||||||||||||||||||||||||||||||||||||||||||
| String introduce, | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| @Schema(description = "썸네일 이미지 URL", example = "https://example.com/facility/thumbnail.jpg") | ||||||||||||||||||||||||||||||||||||||||||
| String thumbnailUrl, | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| @Schema(description = "시설 종목 타입", example = "SOCCER") | ||||||||||||||||||||||||||||||||||||||||||
| @NotNull | ||||||||||||||||||||||||||||||||||||||||||
| FacilityType facilityType, | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| @Schema(description = "위도", example = "37.563") | ||||||||||||||||||||||||||||||||||||||||||
| @DecimalMin("-90.0") | ||||||||||||||||||||||||||||||||||||||||||
| @DecimalMax("90.0") | ||||||||||||||||||||||||||||||||||||||||||
| double lat, | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| @Schema(description = "경도", example = "126.982") | ||||||||||||||||||||||||||||||||||||||||||
| @DecimalMin("-180.0") | ||||||||||||||||||||||||||||||||||||||||||
| @DecimalMax("180.0") | ||||||||||||||||||||||||||||||||||||||||||
| double lng | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🛡️ 래퍼 타입 사용 제안 `@Schema`(description = "위도", example = "37.563")
+ `@NotNull`
`@DecimalMin`("-90.0")
`@DecimalMax`("90.0")
- double lat,
+ Double lat,
`@Schema`(description = "경도", example = "126.982")
+ `@NotNull`
`@DecimalMin`("-180.0")
`@DecimalMax`("180.0")
- double lng
+ Double lng📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package com.be.sportizebe.domain.facility.dto.response; | ||
|
|
||
| import com.be.sportizebe.domain.facility.entity.FacilityType; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
|
|
||
| @Schema(description = "체육시설 단건 조회 응답") | ||
| public record FacilityResponse( | ||
|
|
||
| @Schema(description = "체육시설 ID", example = "123") | ||
| Long id, | ||
|
|
||
| @Schema(description = "체육시설 이름", example = "OO 풋살장") | ||
| String facilityName, | ||
|
|
||
| @Schema(description = "시설 소개", example = "잔디 상태가 좋아요") | ||
| String introduce, | ||
|
|
||
| @Schema(description = "썸네일 이미지 URL", example = "https://example.com/facility/thumbnail.jpg") | ||
| String thumbnailUrl, | ||
|
|
||
| @Schema(description = "시설 종목 타입", example = "SOCCER") | ||
| FacilityType facilityType, | ||
|
|
||
| @Schema(description = "위도", example = "37.563") | ||
| double lat, | ||
|
|
||
| @Schema(description = "경도", example = "126.982") | ||
| double lng | ||
|
|
||
| ) { | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: Sportize/be
Length of output: 129
🏁 Script executed:
Repository: Sportize/be
Length of output: 5937
🏁 Script executed:
Repository: Sportize/be
Length of output: 9139
🏁 Script executed:
Repository: Sportize/be
Length of output: 280
🏁 Script executed:
Repository: Sportize/be
Length of output: 1687
🏁 Script executed:
Repository: Sportize/be
Length of output: 37
🏁 Script executed:
Repository: Sportize/be
Length of output: 10188
🏁 Script executed:
# Check MatchCreateRequest to see what parameters are passed cat -n ./src/main/java/com/be/sportizebe/domain/match/dto/request/MatchCreateRequest.javaRepository: Sportize/be
Length of output: 812
🏁 Script executed:
Repository: Sportize/be
Length of output: 2770
매칭 생성 시 스포츠 종목과 체육시설 타입 간 검증 로직 부재
SportType은 3가지(SOCCER,BASKETBALL,BADMINTON)만 지원하지만FacilityType은 6가지를 지원합니다. 현재 구조에서는 매칭 생성 시SportType과facilityId를 독립적으로 받아 처리하며, 두 값 간의 유효성 검증이 없습니다. 따라서 사용자가SOCCER매칭을TENNIS시설에 생성하는 것을 방지하지 못합니다.MatchCreateRequest에서 선택한 스포츠 종목이 해당 시설에서 실제로 지원되는 종목인지 검증하는 로직을 추가하거나,SportType을 확장하여 두 enum을 정렬해야 합니다.🤖 Prompt for AI Agents