Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
10 changes: 10 additions & 0 deletions .claude/rules/admin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
paths:
- "src/main/java/ssu/eatssu/domain/admin/**/*.java"
---

# Admin Domain Notes

- admin은 별도 인증 체계가 아니라 일반 API와 동일한 JWT(`ROLE_ADMIN`)를 사용한다. 서버 렌더링 UI가 아니라 다른 도메인과 동일한 `@ResponseBody` JSON API다.
- admin 패키지는 다른 도메인(`review`, `menu` 등)의 엔티티를 그 도메인의 서비스/리포지토리를 거치지 않고 `domain/admin/persistence`의 자체 리포지토리로 직접 조회/수정한다. 이는 이 도메인의 의도된 컨벤션이며, 새 admin 기능을 추가할 때도 이 패턴을 따른다.
- 리뷰 삭제 시 연관 리포트 삭제는 `ApplicationEventPublisher`로 발행되는 `ReviewDeleteEvent`를 `ReportEventListener`가 동기적으로(`@TransactionalEventListener`가 아닌 일반 `@EventListener`) 처리한다.
14 changes: 14 additions & 0 deletions .claude/rules/auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
paths:
- "src/main/java/ssu/eatssu/domain/auth/**/*.java"
---

# Auth Domain Notes

- Access/refresh 토큰이 클레임 구조상 구분되지 않는다(`token_type` 클레임 없음). `JwtTokenProvider`의 `validateToken`/`getAuthentication`은 refresh 토큰도 access 토큰 자리에서 그대로 통과시킨다.
- Public 엔드포인트 화이트리스트는 `SecurityConfig.AUTH_WHITELIST`/`RESOURCE_LIST`와 `JwtAuthenticationFilter.AUTH_WHITELIST` 두 곳에 중복 관리된다. 새 public 엔드포인트를 추가할 때는 반드시 두 곳을 함께 수정한다.
- OAuth 로그인의 "비밀번호"는 `provider + providerId`를 BCrypt 해시한 값이다(`UserService.createCredentials`/`OAuthService.makeOauthCredentials`). 이 조합 로직을 변경하면 기존 유저 전원이 로그인할 수 없게 된다.
- 동일 이메일로 다른 provider(Kakao/Apple)로 로그인하면 `findFirstByEmailOrderByIdAsc`로 기존 계정에 조용히 합쳐진다(코드 내 FIXME로 명시된 known issue). `users` 테이블에 email unique 제약은 없다.
- Apple 이메일 릴레이 감지(`OAuthService.isHideEmail`)는 `@privaterelay.appleid.com` 접미사 + 길이 25자 초과라는 하드코딩된 조건에 의존한다.
- V1(`/oauths/kakao`, `/oauths/apple`)과 V2(`/oauths/v2/kakao`, `/oauths/v2/apple`)가 공존하며 V1은 로그인/회원가입 마이그레이션 이후 삭제 예정으로 표시되어 있다. 새 기능은 V2 기준으로 작성한다.
- CORS는 `addAllowedOriginPattern("*")` + `setAllowCredentials(true)`로 모든 출처를 허용한다.
9 changes: 9 additions & 0 deletions .claude/rules/inquiry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
paths:
- "src/main/java/ssu/eatssu/domain/inquiry/**/*.java"
Comment on lines +2 to +3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

규칙 설명에 언급된 것처럼 문의 상태(InquiryStatus) 전이 로직은 ssu/eatssu/domain/admin/service/ManageInquiryService.java에서 처리됩니다. Claude가 해당 어드민 서비스를 수정할 때 이 규칙을 놓치지 않도록 paths 목록에 ManageInquiryService.java 경로를 추가하는 것이 좋습니다.

Suggested change
paths:
- "src/main/java/ssu/eatssu/domain/inquiry/**/*.java"
paths:
- "src/main/java/ssu/eatssu/domain/inquiry/**/*.java"
- "src/main/java/ssu/eatssu/domain/admin/service/ManageInquiryService.java"

---

# Inquiry Domain Notes

- `InquiryController`(문의 작성 API)는 `@Deprecated`다. 문의 기능이 카카오톡으로 이전되어 더는 사용되지 않지만 코드는 아직 남아 동작한다. 새 기능을 여기에 추가하지 않는다.
- 문의 상태(`InquiryStatus`: WAITING/ANSWERED/HOLD) 전이는 `inquiry` 패키지가 아니라 `domain/admin`(`ManageInquiryService`)에서 처리한다. `InquiryStatus`를 변경하면 admin 패키지도 함께 확인해야 한다.
11 changes: 11 additions & 0 deletions .claude/rules/menu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
paths:
- "src/main/java/ssu/eatssu/domain/menu/**/*.java"
---

# Menu Domain Notes

- `Menu`에는 이름이 있고 `Meal`에는 없다. VARIABLE 타입(`Menu.createVariable`) 메뉴는 `(name, restaurant)` 기준으로 get-or-create되어 여러 `Meal`에서 `MealMenu`를 통해 재사용된다 — Meal마다 새로 생성되는 것이 아니다.
- i18n 컬럼(`name_en`/`name_ja`/`name_vi`)은 `Menu`에만 있고 `Meal`/`MealMenu`/`MenuCategory`에는 없다. 그마저도 SNACK_CORNER 메뉴만 채워져 있고 FOOD_COURT나 VARIABLE로 생성된 메뉴는 비어 있다. `MealController`는 Language를 아예 받지 않아 변동 식단 메뉴명은 항상 한국어로 나간다.
- `Restaurant.FACULTY`는 `MenuType`/`RestaurantType`의 FIXED/VARIABLE 어느 분류에도 속하지 않는다. 검증 로직(`validateMenuRestaurant`/`validateMealRestaurant`)이 화이트리스트가 아니라 블랙리스트 방식이라 새 restaurant enum 값을 추가할 때 분류 누락에 주의해야 한다.
- Meal 재조회/중복 방지(`getExistingMealId`)는 같은 date/timePart/restaurant에서 메뉴 이름 리스트를 정렬해 문자열로 정확히 비교한다. 공백/오탈자 하나만 달라도 별도 Meal로 새로 생성된다.
12 changes: 12 additions & 0 deletions .claude/rules/partnership.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
paths:
- "src/main/java/ssu/eatssu/domain/partnership/**/*.java"
---

# Partnership Domain Notes

- `PartnershipRestaurant`(가게, 부모)와 `Partnership`(개별 혜택, 자식)은 1:N 관계다. `PartnershipLike`는 개별 혜택이 아니라 가게 단위로 붙는다.
- `Partnership` 엔티티에 `@Where(clause = "end_date >= CURRENT_DATE")`가 걸려 있어 `findById`를 포함한 모든 JPA 조회가 만료된 혜택을 자동으로 숨긴다. 만료된 partnership을 `findById`로 찾으면 "만료됨"이 아니라 "찾을 수 없음"으로 처리된다는 점에 주의한다.
- `PeriodType.FESTIVAL`은 `CreatePartnershipRequest`에 필드가 없어 API로는 생성할 수 없다. FESTIVAL 혜택은 현재 DB를 직접 조작해야만 만들 수 있다.
- 앱 API로 생성한 `Partnership`/`PartnershipRestaurant`는 i18n 필드(`description_en/ja/vi`, `store_name_en/ja/vi`)가 채워지지 않는다. 지금까지 이 필드들은 전부 별도 flyway 마이그레이션으로만 채워졌다.
- 좋아요 수(`likeCount`)는 원자적 카운터가 아니라 로드된 컬렉션의 `size()`로 계산되고, 좋아요 토글도 잠금 없는 read-check-write 방식이다. 동시 요청에 대한 정합성을 기대하지 않는다.
10 changes: 10 additions & 0 deletions .claude/rules/rating.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
paths:
- "src/main/java/ssu/eatssu/domain/rating/**/*.java"
Comment on lines +2 to +3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

실제 별점 집계가 이루어지는 핵심 구현체인 QuerydslMealRatingCalculatorQuerydslMenuRatingCalculatorssu/eatssu/domain/menu/persistence/ 패키지에 위치해 있습니다. Claude가 이 계산기 클래스들을 수정할 때 별점 집계 관련 규칙을 참고할 수 있도록 paths 목록에 해당 경로를 추가하는 것이 좋습니다.

Suggested change
paths:
- "src/main/java/ssu/eatssu/domain/rating/**/*.java"
paths:
- "src/main/java/ssu/eatssu/domain/rating/**/*.java"
- "src/main/java/ssu/eatssu/domain/menu/persistence/*RatingCalculator.java"

---

# Rating Domain Notes

- 별점 집계는 매 조회마다 DB에서 `AVG()`/`COUNT()`를 다시 계산하는 방식이다(캐시나 비정규화된 집계 필드 없음). `QuerydslMealRatingCalculator`/`QuerydslMenuRatingCalculator` 등 `menu/persistence` 쪽 구현체가 실제 사용 경로다.
- `rating/entity`의 `JpaLoadCollectionRatingCalculator`, `JpaProjectionRatingCalculator`, `ReviewRating`, `RatingCountMap`은 벤치마크용으로 만들어진 미사용 코드다. 특히 `ReviewRating`은 enum을 mutable singleton으로 사용해 스레드 세이프하지 않으니, 되살려서 쓰지 말 것.
- `amountRating`/`tasteRating`은 값이 있어도 응답 DTO에서 세팅되지 않아 항상 null로 내려간다. `Ratings.java`의 FIXME(2026-07-04)에 이 필드들의 필수 여부가 아직 비즈니스적으로 미정임이 명시되어 있으니, 고칠 때 비즈니스 요구사항부터 확인한다.
10 changes: 10 additions & 0 deletions .claude/rules/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
paths:
- "src/main/java/ssu/eatssu/domain/report/**/*.java"
Comment on lines +2 to +3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Report 엔티티(Report.java)는 ssu/eatssu/domain/review/entity/Report.java 경로에 위치해 있습니다. 현재 설정된 paths 필터(src/main/java/ssu/eatssu/domain/report/**/*.java)만으로는 Claude가 Report 엔티티 파일을 직접 수정할 때 이 규칙 파일(report.md)을 로드하지 못하게 됩니다. 따라서 paths 목록에 Report.java 경로를 추가하여 Claude가 엔티티를 수정할 때도 관련 규칙을 인지할 수 있도록 개선하는 것이 좋습니다.

Suggested change
paths:
- "src/main/java/ssu/eatssu/domain/report/**/*.java"
paths:
- "src/main/java/ssu/eatssu/domain/report/**/*.java"
- "src/main/java/ssu/eatssu/domain/review/entity/Report.java"

---

# Report Domain Notes

- `Report` 엔티티는 `report` 패키지가 아니라 `review.entity.Report`에 있다. `ReportType`/`ReportStatus`/서비스/컨트롤러만 `report` 패키지에 있다.
- 신고 접수는 Slack 알림(`SlackChannel.REPORT_CHANNEL`)만 보낼 뿐, 리뷰를 자동으로 숨기거나 카운트를 올리는 등의 후속 처리가 전혀 없다. 실제 조치는 전부 Slack을 보고 admin이 수동으로 처리한다.
- `ReportStatus`는 `PENDING` 외 값(`IN_PROGRESS`/`RESOLVED`/`REJECTED`/`FALSE_REPORT`)으로 전이시키는 코드가 어디에도 없다. 상태 관리 기능을 새로 만들기 전까지는 사실상 `PENDING` 고정이라고 생각한다.
9 changes: 9 additions & 0 deletions .claude/rules/restaurant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
paths:
- "src/main/java/ssu/eatssu/domain/restaurant/**/*.java"
---

# Restaurant Domain Notes

- 이 패키지는 자체 엔티티/테이블이 없고 `Restaurant`(캠퍼스 식당 enum), `RestaurantType`(FIXED/VARIABLE 분류 enum) 두 개로만 구성된다. `menu` 도메인의 하위 개념에 가깝다.
- `partnership` 패키지에도 이름이 완전히 같은 `RestaurantType` enum이 따로 있다(`restaurant`는 FIXED/VARIABLE, `partnership`은 RESTAURANT/CAFE/PUB로 의미가 전혀 다름). import할 때 IDE 자동완성이 잘못된 쪽을 잡아줄 수 있으니 패키지 경로를 반드시 확인한다.
12 changes: 12 additions & 0 deletions .claude/rules/review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
paths:
- "src/main/java/ssu/eatssu/domain/review/**/*.java"
---

# Review Domain Notes

- 같은 `Review` 엔티티에 V1용 `@Embedded Ratings ratings`와 V2용 `Integer rating`이 함께 존재한다(엔티티 상단 주석 참고). 두 필드는 서로 배타적으로 채워질 수 있으니 어느 한쪽만 보고 값이 있다고 가정하면 안 된다.
- V1 리뷰는 항상 `review.menu`로 연결되고, V2 meal 리뷰는 `review.meal`을 직접 사용한다. V1 API(`GET /reviews`)는 `review.menu` 기준으로만 조회하므로 V2로 작성된 meal 리뷰는 V1에서 보이지 않는다.
- V2 집계(`ReviewServiceV2.findMenuReviews`/`findMealReviews`/`findRestaurantReviews`)는 리뷰 리스트를 전부 로드한 뒤 Java에서 평균/카운트를 계산한다. V1(`ReviewRatingService`)은 QueryDSL로 DB에서 집계한다. 새 집계 기능을 추가할 때 어느 패턴을 따를지 확인한다.
- 리뷰 이미지 S3 업로드는 V1 엔드포인트(`POST /reviews/upload/image`)에서만 처리한다. V2 리뷰 생성 API는 이미 업로드된 `imageUrls`만 받으므로, V2 클라이언트도 이미지 첨부 시 V1 업로드 API를 먼저 호출해야 한다.
- `Report` 엔티티는 `report` 패키지가 아니라 `review.entity.Report`에 있다. 리뷰 신고 관련 로직을 찾을 때 `report` 패키지만 보면 안 된다.
9 changes: 9 additions & 0 deletions .claude/rules/slack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
paths:
- "src/main/java/ssu/eatssu/domain/slack/**/*.java"
---

# Slack Domain Notes

- 예외 필터링 없이 모든 예외(비즈니스 예외 포함)가 `ControllerLogAspect`를 통해 Slack 전송 대상이 된다. 실제 전송 여부는 `SlackErrorNotifier`가 `server.env == "prod"`인지로만 가른다.
- 전송은 동기 방식이며 큐잉/rate-limit/dedup이 없다. 장애로 예외가 몰리면 실패 요청마다 Slack API 호출 지연이 응답 시간에 그대로 추가된다.
9 changes: 9 additions & 0 deletions .claude/rules/slice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
paths:
- "src/main/java/ssu/eatssu/domain/slice/**/*.java"
---

# Slice Domain Notes

- 커서는 별도 인코딩 없이 정렬 대상 PK(`review.id`) 값 그대로 쓴다. 커서 기반 페이지네이션을 다른 정렬 기준으로 확장하려면 이 "PK 그대로" 가정이 깨진다는 점을 먼저 고려한다.
- 컨트롤러의 `@PageableDefault(sort = "date")`는 실제로 무시된다. 리포지토리(QueryDSL)는 항상 `id` 기준으로 정렬하므로, 클라이언트가 sort 파라미터를 보내도 조용히 무시된다.
11 changes: 11 additions & 0 deletions .claude/rules/user.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
paths:
- "src/main/java/ssu/eatssu/domain/user/**/*.java"
---

# User Domain Notes

- `users` 테이블에 email unique 제약이 없다. 다른 provider로 같은 이메일 로그인 시 `findFirstByEmailOrderByIdAsc`로 기존 계정에 합쳐진다([[auth]]와 연결된 이슈).
- `College`/`Department`의 `name_en`은 어떤 마이그레이션에서도 채워진 적이 없다(V10/V11은 ja/vi만 채움). EN 유저는 college/department 이름이 항상 한국어로 폴백된다.
- 회원 탈퇴(`UserService.withdraw`)는 하드 delete이지만 엔티티마다 처리 방식이 다르다: `Review`/`Inquiry`는 `clearUser()`로 FK만 null 처리해 데이터를 보존하고, `reviewLikes`/`partnershipLikes`(`orphanRemoval=true`)와 `reviewReports`(`cascade=ALL`)는 그대로 cascade 삭제된다. `User`에 새 `@OneToMany` 연관관계를 추가할 때 탈퇴 시 어떻게 처리할지 반드시 정해야 한다.
- `NicknameValidator`는 한글/영문 비속어·브랜드명·관리자 사칭 방지 정규식을 담은 코드로, 수정 빈도는 낮지만 잘못 건드리면 닉네임 검증 전체(등록/검증 두 진입점 모두)에 영향을 준다.
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ src/main/java/ssu/eatssu/
- **rating**: `Ratings` 임베디드 객체로 별점 집계 관리
- **user**: User 엔티티, 학과/단과대학 정보(`department` 하위 패키지), 다국어 설정(`Language` enum: KO/EN/JA/VI)
- **partnership**: 제휴 식당 정보 및 좋아요
- **admin**: Mustache 템플릿 기반 어드민 UI. 식단/메뉴/카테고리/문의/리포트/리뷰 관리
- **admin**: 일반 API와 동일한 JWT(`ROLE_ADMIN`) 인증을 쓰는 `@ResponseBody` JSON API. 식단/메뉴/카테고리/문의/리포트/리뷰 관리. `login.mustache`가 남아있으나 어떤 컨트롤러도 뷰를 렌더링하지 않는 죽은 코드
- **slice**: 커서 기반 페이지네이션 (`SliceResponse<T>`)
- **slack**: 서버 에러 발생 시 Slack 알림 (`SlackErrorNotifier`)

Expand Down
Loading