Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
804d485
chore: 이슈 및 PR 템플릿 추가 (#3)
sjungwon03 Jul 2, 2026
82216b9
chore: Spring Boot 3.5.16 다운그레이드 및 에러 구조 분리, AI 컨벤션 문서 추가 (#5)
myh7754 Jul 3, 2026
820acc3
feat: BaseEntity 및 도메인 엔티티, 스키마 SQL 추가 (#10)
myh7754 Jul 8, 2026
684b389
chore: CodeRabbit 리뷰 설정 파일 추가 (#8)
sjungwon03 Jul 8, 2026
48d4648
chore: Claude Code 하네스 재구성 (.claude/rules + 스킬/커맨드/에이전트/훅) (#12)
sjungwon03 Jul 8, 2026
514a025
feat: 온보딩 프로필 제출 API 추가 (#17)
jowoojin030 Jul 8, 2026
81349a9
chore: 백엔드 CI 워크플로 추가 (GitHub Actions) (#18)
sjungwon03 Jul 8, 2026
6178ec4
feat: Flyway 도입 및 로컬 MySQL 전환 (V1 초기 스키마 + V2 missing_count) (#14)
myh7754 Jul 9, 2026
c5a9701
chore: IntelliJ 코드 스타일 XML 추가 (google-java-format AOSP) (#23)
sjungwon03 Jul 10, 2026
f710724
refac: ApiResponse meta를 Map에서 PageMeta record로 변경 (#30)
myh7754 Jul 10, 2026
c730cf0
docs: API 경로를 엔티티 리소스 중심으로 재설계 (#32)
sjungwon03 Jul 10, 2026
4a59c2b
feat: 카카오, 구글, 네이버 소셜 로그인 인증 기능 추가 (#22)
sjungwon03 Jul 13, 2026
b1f4505
docs: Claude Code 코드 포맷(Google AOSP Style) 규칙 문서화 (#34)
sjungwon03 Jul 13, 2026
b17cdd1
feat: 앱 로그를 Log4j2 + JDBC로 app_logs 테이블에 적재 (#42)
sjungwon03 Jul 15, 2026
ca4fbe7
feat: 관리자 - 사용자 관리 API 추가 (#44)
sjungwon03 Jul 15, 2026
dad5519
feat: 관리자 - 정책/지역 관리 API 추가 (#47)
sjungwon03 Jul 15, 2026
9c69881
feat: 관리자 - 로그인 이력 API 추가 (#59)
sjungwon03 Jul 15, 2026
a166aee
feat: 관리자 - 검색 로그 조회 API 추가 (#53)
sjungwon03 Jul 15, 2026
977a64f
feat: 관리자 - 정책 신청 관리 API 추가 (#49)
sjungwon03 Jul 15, 2026
2822909
feat: 관리자 - 커뮤니티 관리 API 추가 (#51)
sjungwon03 Jul 15, 2026
ec04545
feat: 관리자 - 애플리케이션 로그 조회 API 추가 (#55)
sjungwon03 Jul 15, 2026
fc37de5
feat: 관리자 - 배치 작업 로그 조회 API 추가 (#57)
sjungwon03 Jul 15, 2026
402a020
chore: regions 지역 마스터 시드 마이그레이션 추가 (V3) (#38)
myh7754 Jul 15, 2026
58bacf7
fix: 관리자 애플리케이션 로그 API의 AppLog→ApplicationLog 리네임 누락 정리 (#62)
sjungwon03 Jul 15, 2026
8122597
fix: Flyway V4 마이그레이션 버전 충돌 해소 (정책 soft delete → V6) (#64)
sjungwon03 Jul 15, 2026
dd3b45f
fix: db/seed의 V3__seed_regions.sql과 마이그레이션 버전 충돌 해소 (V3→V4로 원복) (#66)
sjungwon03 Jul 15, 2026
9be0647
feat: 관리자 - 정책 수집 요약 조회 API 추가 (policy-sync-jobs/summary) (#72)
sjungwon03 Jul 15, 2026
989d7da
fix: 관리자 커뮤니티 게시글/댓글 API 경로를 다른 관리자 API와 일관되게 정리 (#70)
sjungwon03 Jul 15, 2026
92daab1
refac: refresh token 재발급(rotate) 제거, 발급 시점 TTL까지 그대로 재사용 (#68)
sjungwon03 Jul 15, 2026
8a72515
feat: 온통청년 정책 데이터 수집 배치(스케줄러) 구현 (#73)
myh7754 Jul 16, 2026
caec61b
feat: 최근 본 정책 기능 추가 (정책 상세 조회 시 기록 + 목록 API) (#75)
sjungwon03 Jul 16, 2026
d3ae95f
feat: 정책 상세 응답 지역을 코드→이름(RegionResponse)으로 보강 (#77)
myh7754 Jul 16, 2026
bda114d
feat: 게시판 CRUD API 추가 (#81)
seohyeon-tomato Jul 16, 2026
efbc24a
refac: 어드민 코드를 최상위 admin 도메인 패키지로 분리 (#83)
sjungwon03 Jul 19, 2026
38f471e
feat: 정책 목록 조회 공개 API (GET /api/v1/policies) (#79) (#84)
myh7754 Jul 20, 2026
d15fe44
feat: 정책 목록 keyword/region/age 검색 필터 추가 (#86) (#87)
myh7754 Jul 20, 2026
7d89e00
feat: 정책 관리(PolicyApplication) + 체크리스트(ApplicationChecklist) 구현 (#25)
canfactor Jul 20, 2026
8923fb6
feat: 정책별 STOMP 실시간 채팅 지원 (#89)
sjungwon03 Jul 20, 2026
74afca6
feat: 게시글 조회 시 조회수 증가 및 Redis 중복 방지 적용 (#97) (#99)
sjungwon03 Jul 20, 2026
d8e6d61
feat: MinIO 파일 업로드 및 다운로드 API 추가 (#96)
sjungwon03 Jul 20, 2026
d01ad85
feat: user_profiles 온보딩 항목 확장 및 지역 API 추가 (#101)
sjungwon03 Jul 20, 2026
f14e3c1
fix: 정책 카드 지역 라벨 조립을 백엔드에서 제거하고 원본 지역 목록으로 응답 (#100)
sjungwon03 Jul 20, 2026
1698596
feat: 마이페이지 프로필 조회 GET API 추가 (#104)
sjungwon03 Jul 20, 2026
acf49a0
fix: 정책 신청 재등록 시 soft delete 행 재활성화 (#107)
sjungwon03 Jul 20, 2026
4584415
feat: 게시글 이미지 첨부 저장 (#109)
sjungwon03 Jul 20, 2026
66ec075
refac: OnboardingController를 UserProfileController로 도메인 엔티티 기준 리네이밍 (…
sjungwon03 Jul 21, 2026
be8a5bc
feat: 정책 비교 API 추가 (policy-comparisons) (#40)
imi21123 Jul 21, 2026
4e82a60
feat: 게시글 목록 조회에 검색어/카테고리 필터와 정렬 지원 추가 (#114)
sjungwon03 Jul 21, 2026
f184f1f
fix: region=전국이 결과를 좁히던 동작 제거 (#93) (#94)
myh7754 Jul 21, 2026
fdc7c1b
feat: 온보딩 프로필 수정(PATCH) API 추가 (#121)
sjungwon03 Jul 21, 2026
4e92e69
fix: 지역 목록을 한글 가나다순으로 정렬 (#117)
sjungwon03 Jul 21, 2026
dc87ee9
feat: 지역 선택 시 전국 정책을 뒤로 미루는 정렬 (#120) (#122)
myh7754 Jul 21, 2026
2bd85da
refac: 정책 신청관리 register → create 네이밍 통일 및 Javadoc 주석 정리 (#91)
canfactor Jul 21, 2026
ccfcfa3
fix: 신청기간 정보 없는 정책이 사업기간 종료 후에도 상시로 노출되는 문제 수정 (#124)
sjungwon03 Jul 22, 2026
0c7b91d
feat: 정책 목록 조회에 취업상태(jobCode) 필터·정렬 추가 (#133) (#134)
myh7754 Jul 22, 2026
5cb8673
feat: 맞춤정책 조회 API 추가 (#132)
imi21123 Jul 22, 2026
a9f992e
chore: 로컬 개발 CORS 허용 origin에 5174 포트 추가 (#138)
sjungwon03 Jul 22, 2026
bfcd2c7
docs: 리드미 새로 작성 (#136)
sjungwon03 Jul 22, 2026
04530af
docs: 리드미에 구현 특징 섹션 추가 (#158)
sjungwon03 Jul 22, 2026
286372a
fix: 정책 채팅 STOMP 브로커에 하트비트 설정 추가 (#156)
sjungwon03 Jul 22, 2026
41b39ff
fix: 프레임워크 표준 4xx 예외를 catch-all 500 대신 정확한 상태코드로 처리
sjungwon03 Jul 22, 2026
46629b6
fix: 정책 채팅 구독 receipt 응답 추가 (#162)
sjungwon03 Jul 22, 2026
2b149e7
fix: 온보딩 프로필 제출 API 인증 필수화 및 본인 검증 추가
sjungwon03 Jul 22, 2026
0b9b809
fix: posts 공개 API 입력 검증 보강 (category 패턴·자유글 정책 거부·본문 길이)
sjungwon03 Jul 22, 2026
d16a0af
fix: auth 견고성 보강 — soft-delete 인증 차단, 제재 시 토큰 회수, state 소비 원자화
sjungwon03 Jul 22, 2026
84623b6
fix: 프로필 생성 API 미인증 접근·타인 userId 제출(IDOR) 차단
sjungwon03 Jul 22, 2026
7651100
fix: BoardErrorCode 중복 해소(COMMENT_NOT_FOUND B005 재배번) 및 board 패키지를 po…
sjungwon03 Jul 22, 2026
cea6178
fix: 삭제된 신청의 체크리스트 접근 시 500 대신 P005로 응답
sjungwon03 Jul 22, 2026
260a4f4
fix: 관리자 숨김 정책의 배치 재노출 방지 (#175)
sjungwon03 Jul 22, 2026
3065f73
fix: 비공개 정책의 비교와 신청 등록 차단 (#177)
sjungwon03 Jul 22, 2026
e26c2d3
fix: 목록 API 정렬과 페이지 입력을 방어 (#178)
sjungwon03 Jul 22, 2026
b2c63dc
fix: CORS 허용 Origin을 환경 설정으로 외부화 (#179)
sjungwon03 Jul 22, 2026
2ebdb71
fix: 파일 장애 로그와 정책 검색 로그를 추가 (#183)
sjungwon03 Jul 22, 2026
ef04bea
fix: 게시글 목록 category를 enum으로 검증 (#185)
sjungwon03 Jul 22, 2026
c1eb845
Merge branch 'fix/159-framework-4xx-handlers' into dev
sjungwon03 Jul 22, 2026
ad4fc47
Merge branch 'fix/163-profile-authz' into dev
sjungwon03 Jul 22, 2026
4999f8c
Merge branch 'fix/165-posts-input-validation' into dev
sjungwon03 Jul 22, 2026
e650e99
Merge branch 'fix/167-auth-robustness' into dev
sjungwon03 Jul 22, 2026
ffaa557
Merge branch 'fix/168-profile-idor' into dev
sjungwon03 Jul 22, 2026
7fea5af
Merge branch 'fix/169-checklist-orphan-500' into dev
sjungwon03 Jul 22, 2026
aed907d
Merge branch 'fix/170-post-domain-cleanup' into dev
sjungwon03 Jul 22, 2026
4cd2f71
Merge branch 'fix/175-admin-hidden-sync' into dev
sjungwon03 Jul 22, 2026
4590da1
Merge branch 'fix/177-policy-public-access' into dev
sjungwon03 Jul 22, 2026
fd32da5
Merge branch 'fix/178-pagination-input-guard' into dev
sjungwon03 Jul 22, 2026
efc7c46
Merge branch 'fix/179-configurable-origins' into dev
sjungwon03 Jul 22, 2026
068bf6c
Merge branch 'fix/183-file-search-logging' into dev
sjungwon03 Jul 22, 2026
acc6f97
Merge branch 'fix/185-post-category-validation' into dev
sjungwon03 Jul 22, 2026
48161b1
fix: 로그 비동기화 및 인프라 견고성 (#187) (#188)
sjungwon03 Jul 22, 2026
f999c18
fix: 프로필 용어 리팩토링 및 값 검증 (#189) (#190)
sjungwon03 Jul 22, 2026
485bce4
fix: 정책 신청 API 리팩토링 및 쿼리 최적화 (#191) (#192)
sjungwon03 Jul 22, 2026
892d317
fix: 조회수 IP 스푸핑 방지 및 Redis 캐시 오버헤드 차단 (#193) (#194)
sjungwon03 Jul 22, 2026
9b0e007
fix: 동기화 배치 안정성 강화 (백오프 & 실패 임계치) (#195) (#196)
sjungwon03 Jul 22, 2026
d6fb1d1
fix: API 명세 갱신 및 로버스트니스 강화 (#197) (#198)
sjungwon03 Jul 22, 2026
d9a0eb0
fix: 정책 키워드 검색에 지역명 부분일치 포함 (#200)
myh7754 Jul 23, 2026
83ebe70
feat: 게시글 목록 응답에 본문 요약 필드 추가 (#202)
sjungwon03 Jul 23, 2026
ec6b7ac
fix: 정책 수집 실패율 초과 메시지의 포맷 문자열 이스케이프 누락 수정 (#204)
myh7754 Jul 23, 2026
d9c883e
feat: 게시글 댓글 CRUD API 추가 (#212)
sjungwon03 Jul 24, 2026
4c15b62
fix: 정책 수집을 매 회차 실패시키던 컬럼 길이 상한 확대 (#209)
myh7754 Jul 24, 2026
6a948b4
chore: docker compose로 프론트+백엔드 통합 기동 및 배포 도메인 CORS 대응 (#211)
sjungwon03 Jul 24, 2026
6fa6b08
docs: Swagger(OpenAPI) API 문서 도입 (#214)
sjungwon03 Jul 24, 2026
85837a6
docs: 공통 에러 응답(ErrorResponse) 스키마와 4XX/5XX 기본 응답 추가 (#216)
sjungwon03 Jul 24, 2026
53471de
docs: PageMeta에 example 명시해 Swagger 의미없는 샘플값 제거 (#218)
sjungwon03 Jul 24, 2026
d1d5d57
fix: PolicyMapper에 컬럼 길이 상한 절단 가드 추가 (#221)
sjungwon03 Jul 24, 2026
1b4c43d
feat: 정책 목록/상세 응답에 자격조건 코드 필드 추가 (#220)
sjungwon03 Jul 24, 2026
7ed93e2
fix: 실패율 초과 FAILED 이력에 신규/변경/누락/실패 건수를 기록한다 (#219)
sjungwon03 Jul 24, 2026
6ab54cc
fix: PolicySyncServiceTest 전량실패 테스트가 길이절단 가드와 충돌해 CI 실패 (#224)
sjungwon03 Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
13 changes: 13 additions & 0 deletions .claude/agent-memory/code-reviewer/MEMORY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# code-reviewer 메모리 (인덱스)

이 파일은 code-reviewer 에이전트가 리뷰를 반복하며 축적하는 학습의 인덱스다. 상세 노트는 같은 디렉토리의 주제별 파일로 분리한다.

## 프로젝트 리뷰 기준 (요약)

- 규칙 원본은 `.claude/rules/`. 리뷰 지적은 항상 규칙 문서 근거와 함께 남긴다.
- 에러코드는 `ErrorCode` 인터페이스 + `{domain}.exception.{Domain}ErrorCode` enum 구조. 접두어 체계(C/A/S/U/P/F/D)를 지키는지 본다.
- 성공 응답은 `ApiResponse`(data+meta), 에러는 `ErrorResponse`(code 기반). 페이지 정보는 `meta`에.

## 반복 발견 패턴

(아직 없음 — 리뷰를 진행하며 자주 나오는 위반/오탐 패턴을 여기에 축적한다.)
30 changes: 30 additions & 0 deletions .claude/agents/code-reviewer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
name: code-reviewer
description: YouthPick 백엔드 코드 리뷰 전용 에이전트. 규칙 위반·버그·보안 관점으로 검토하고 보고만 한다. 코드를 수정하지 않는다. PR 전이나 변경분 리뷰가 필요할 때 사용.
tools: Read, Grep, Glob, Bash
model: sonnet
---

너는 YouthPick 백엔드 코드 리뷰어다. **코드를 수정하지 않는다.** 발견 사항을 심각도순으로 보고만 한다.

## 기준 문서

리뷰 전에 관련 규칙을 읽는다: [`.claude/rules/`](../rules/) 의 `architecture.md`, `api-design.md`, `error-handling.md`, `entity-jpa.md`, `service.md`, `lombok.md`, `auth-security.md`, `testing.md`.

## 리뷰 관점 (순서대로)

1. **규칙 위반** — 패키지 구조, 얇은 Controller, `@Valid` 누락, Entity 직접 반환, `ApiResponse` 미사용, raw `RuntimeException`, Pageable 미사용, 금지 Lombok.
2. **버그/정확성** — 트랜잭션 경계, null/Optional 처리, 경계 조건, 동시성.
3. **보안** — secret/토큰 노출, 인가 누락(IDOR), 입력 검증 우회. (깊은 점검은 `security-review` 스킬)
4. **테스트** — A(정상)/E(예외)/X(경계) 관점 누락 여부.

## 출력 형식

각 발견 사항을 다음으로 보고한다:

- **심각도**: critical / high / medium / low
- **위치**: `파일:라인`
- **문제**: 무엇이 왜 잘못됐는지 한 문장
- **제안**: 어떻게 고칠지

확신이 없으면 "가능성"으로 표시하고 근거를 남긴다. 근거 없는 확정 지적을 하지 않는다. 발견이 없으면 없다고 명확히 말한다.
15 changes: 15 additions & 0 deletions .claude/commands/fix-issue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
description: GitHub 이슈를 Git 컨벤션대로 브랜치 생성부터 검증까지 처리한다
argument-hint: <이슈번호>
allowed-tools: Bash(git checkout:*), Bash(git switch:*), Bash(git branch:*), Bash(git add:*), Bash(git commit:*), Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(gh issue view:*), Bash(gh pr create:*), Bash(./gradlew test:*), Read, Edit, Write, Grep, Glob
---

이슈 #$1 을 이 레포의 Git 컨벤션([.claude/rules/workflow.md](../rules/workflow.md))에 따라 처리한다.

1. `gh issue view $1` 로 이슈 내용·작업 범위·검증 기준을 파악한다.
2. 기본 분기(`dev` 있으면 `dev`, 없으면 `main`)에서 `type/$1-{short-name}` 브랜치를 만든다. type은 이슈 성격에 맞춰 `feat|fix|docs|refac` 중 고른다.
3. 관련 규칙 파일(`.claude/rules/`)을 먼저 읽고, 규칙을 지키며 구현한다.
4. 관련 작은 테스트 → 전체 `./gradlew test` 순으로 검증한다. (JAVA_HOME 필요 시 `CLAUDE.local.md` 참고)
5. `type: subject` 커밋을 만들고, 변경 내용·검증 결과·`Closes #$1` 을 담은 PR 본문 초안을 제시한다.

파괴적이거나 이슈 범위를 벗어나는 결정이 필요하면 진행 전에 사용자에게 확인한다.
130 changes: 130 additions & 0 deletions .claude/hooks/block-secrets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/usr/bin/env bash
# PreToolUse(Bash) 훅: 위험한 파괴 명령과 시크릿 파일 접근을 차단한다.
# 차단 시 exit code 2 + stderr 사유. Claude Code가 사유를 읽고 명령을 막는다.
#
# 정규식만으로는 `rm -r -f`, `rm --recursive --force`, `git add .` 같은 우회가
# 쉬우므로, python3 + shlex로 토큰을 파싱해 플래그/대상/서브명령을 판별한다.
# 프로그램은 -c 인자로 넘겨 stdin(훅 JSON)을 그대로 python에 전달한다.

set -uo pipefail

exec python3 -c '
import json, sys, shlex, os, re, subprocess

try:
data = json.load(sys.stdin)
except Exception:
sys.exit(0)

cmd = (data.get("tool_input", {}) or {}).get("command", "") or ""
if not cmd.strip():
sys.exit(0)

def block(msg):
sys.stderr.write("block-secrets: 차단됨 — " + msg + "\n")
sys.exit(2)

segments = re.split(r"[;\n]|&&|\|\||\||&", cmd)

BROAD_TARGETS = {"/", "~", "$HOME", ".", "./", "*", "./*", "~/", "/*", ".."}
ENV_RE = re.compile(r"^\.env(\..+)?$")
ASSIGN_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")

def is_env_file(arg):
base = os.path.basename(arg)
return bool(ENV_RE.fullmatch(base)) and base != ".env.example"

def dangerous_target(t):
tt = t.rstrip("/")
return (
t in BROAD_TARGETS
or tt in ("", "/", "~", "$HOME", ".", "..")
or t.startswith(("/", "~", "$HOME"))
or "*" in t
)

for seg in segments:
try:
tokens = shlex.split(seg)
except ValueError:
tokens = seg.split()
if not tokens:
continue

idx = 0
while idx < len(tokens):
t = tokens[idx]
if t in ("sudo", "env") or ASSIGN_RE.match(t):
idx += 1
continue
break
if idx >= len(tokens):
continue

name = os.path.basename(tokens[idx])
args = tokens[idx + 1:]

# 1) 파괴적 rm (플래그 결합/분리/롱옵션 모두 탐지)
if name == "rm":
recursive = force = False
targets = []
for a in args:
if a == "--":
continue
if a == "--recursive":
recursive = True
elif a == "--force":
force = True
elif a.startswith("--"):
pass
elif a.startswith("-") and len(a) > 1:
flags = a[1:]
if "r" in flags or "R" in flags:
recursive = True
if "f" in flags:
force = True
else:
targets.append(a)
if recursive and any(dangerous_target(x) for x in targets):
block("위험한 rm -r 대상(루트/홈/현재 디렉토리/와일드카드). 삭제 대상을 구체적 경로로 좁혀라.")

# 2) 시크릿 파일(.env) 내용 노출
if name in ("cat", "less", "more", "head", "tail", "cp", "scp",
"curl", "nc", "xxd", "base64", "strings", "bat", "grep"):
if any(is_env_file(a) for a in args if not a.startswith("-")):
block("시크릿 파일(.env) 내용 노출 시도. .env는 커밋·출력·전송하지 않는다.")

# 3) git add 로 .env 스테이징 (명시적/광범위/force 모두 고려)
if name == "git":
# git 전역 옵션(-C <path>, -c <k=v>, --git-dir <path> 등)을 먼저 건너뛰고
# 실제 서브명령을 찾는다. 값을 따로 받는 옵션은 다음 토큰까지 스킵.
# (git -C x add ., git -c k=v add .env 우회 방지)
VALUE_OPTS = ("-C", "-c", "--git-dir", "--work-tree", "--namespace", "--super-prefix")
gi = 0
while gi < len(args) and args[gi].startswith("-"):
gi += 2 if args[gi] in VALUE_OPTS else 1
subcmd = args[gi] if gi < len(args) else None
add_args = args[gi + 1:]

if subcmd == "add":
positionals = [a for a in add_args if not a.startswith("-")]
force_add = any(a in ("-f", "--force") for a in add_args)
explicit_env = any(is_env_file(a) for a in positionals)
broad = any(a in (".", "./", "-A", "--all", "-u", "--update", "*") for a in add_args)

if explicit_env:
block(".env를 git에 추가하려는 시도. .env는 커밋 금지.")
if broad or force_add:
status_cmd = ["git", "status", "--porcelain"]
if force_add:
status_cmd.append("--ignored")
status_cmd += ["--", ".env"]
try:
out = subprocess.run(status_cmd, capture_output=True, text=True, timeout=5)
if out.stdout.strip():
block(".env가 스테이징 대상에 포함될 수 있음. .env는 커밋 금지.")
except Exception:
pass

sys.exit(0)
'
39 changes: 39 additions & 0 deletions .claude/hooks/format-on-save.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# PostToolUse(Edit|Write) 훅: .java 파일을 저장하면 Spotless로 포맷한다.
# Spotless가 build.gradle에 없으면 조용히 통과한다(no-op).
# 훅 stdin은 Claude Code가 주는 JSON. tool_input.file_path 를 뽑는다.

set -euo pipefail

PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"

file_path="$(python3 -c '
import json, sys
try:
data = json.load(sys.stdin)
except Exception:
print(""); sys.exit(0)
ti = data.get("tool_input", {}) or {}
print(ti.get("file_path", "") or "")
' 2>/dev/null || true)"

# .java 파일이 아니면 아무것도 하지 않는다.
case "$file_path" in
*.java) ;;
*) exit 0 ;;
esac

# Spotless 미설정이면 no-op.
if ! grep -q "spotless" "$PROJECT_DIR/build.gradle" 2>/dev/null; then
exit 0
fi

cd "$PROJECT_DIR"
# JAVA_HOME이 없으면 gradle이 실패하므로, 있을 때만 시도한다.
if [ -z "${JAVA_HOME:-}" ] && ! command -v java >/dev/null 2>&1; then
echo "format-on-save: JAVA_HOME 미설정으로 스킵" >&2
exit 0
fi

./gradlew spotlessApply "-PspotlessFiles=$(printf '%s' "$file_path" | sed 's/[.[\*^$/]/\\&/g')" >/dev/null 2>&1 || true
exit 0
12 changes: 12 additions & 0 deletions .claude/output-styles/teaching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
name: Teaching
description: 변경한 코드에 왜 그렇게 했는지 설명을 덧붙이는 말투. 학습·온보딩용.
---

너는 평소처럼 YouthPick 백엔드 작업을 수행하되, 결과에 **왜 그렇게 했는지**를 함께 설명한다. 신입 개발자가 옆에서 배우는 상황을 가정한다.

- 코드를 바꾼 뒤에는 "무엇을 바꿨는지"뿐 아니라 "왜 이 방식인지"를 한두 문장으로 덧붙인다. 특히 이 레포의 규칙(`.claude/rules/`)과 연결해 설명한다. 예: "요청 DTO에 `@Valid`를 붙인 이유는 검증 실패를 `GlobalExceptionHandler`가 `C001`로 통일해 내려주기 때문이다."
- 대안이 있었다면 왜 그 대안을 택하지 않았는지 짧게 언급한다.
- Spring/JPA 관용구가 처음 등장하면 한 줄로 개념을 풀어준다. 단, 이미 다룬 개념을 반복 설명하지 않는다.
- 설명은 **간결하게**. 코드보다 설명이 길어지지 않게 한다. 장황한 이론 강의가 아니라 "이 결정의 이유"에 집중한다.
- 실제 동작·검증(테스트 결과)은 여전히 사실대로 보고한다. 설명 말투가 결과 왜곡의 핑계가 되지 않는다.
65 changes: 65 additions & 0 deletions .claude/rules/api-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
paths:
- "src/main/java/**"
---

# Controller / DTO / 응답 규칙

## Controller

- 생성자 주입(`@RequiredArgsConstructor` + `final` 필드). 필드 주입(`@Autowired`) 금지.
- Entity를 API 응답으로 직접 반환하지 않는다.
- 요청 body 파라미터에는 `@Valid @RequestBody`를 붙인다. validation 없는 요청 DTO, `@Valid` 없는 바인딩은 금지.
- 성공 응답은 항상 `global.common.ApiResponse`로 감싼다.

## 요청 DTO (`{domain}.dto`)

- `record`를 우선 사용한다. 모든 사용자 입력 필드에 의도를 드러내는 Bean Validation annotation을 붙인다.
- validation message는 한국어로 작성한다.
- 문자열은 보통 `@NotBlank`(`@NotNull`은 공백을 못 막는다), 길이는 `@Size`, 허용값 문자열은 `@Pattern`, 숫자 범위는 `@Min`/`@Max`/`@Positive`. 중첩 DTO 필드에는 `@Valid`.
- enum을 직접 바인딩하면 Jackson deserialization 단계에서 터진다. 잘못된 enum 값을 `C001` 입력값 오류로 내려야 하면 문자열 필드 + `@Pattern`을 사용한다.

```java
public record UserSignupRequest(
@NotBlank(message = "아이디는 필수입니다.")
@Size(min = 4, max = 20, message = "아이디는 4~20자여야 합니다.")
String username,

@NotBlank(message = "비밀번호는 필수입니다.")
String password
) {
}
```

## 응답 DTO

- Entity를 받아 정적 팩토리 메서드 `from(...)`으로 생성한다.
- Entity의 민감 필드, 내부 상태, raw payload, secret 값을 노출하지 않는다.

```java
public record PolicyCardResponse(Long id, String title, String summary) {
public static PolicyCardResponse from(Policy policy) {
return new PolicyCardResponse(policy.getId(), policy.getTitle(), policy.getSummary());
}
}
```

## 공통 응답 봉투 `ApiResponse` (data + meta)

- 단일/객체 데이터는 `ApiResponse.ok(data)`. 페이지 부가정보(`page`, `totalCount`, `totalPages`)는 **`data`가 아니라 `meta`(`PageMeta`)에** 담는다.
- `meta`는 Swagger(OpenAPI) 문서에 필드 스키마가 드러나도록 `Map`이 아니라 타입 있는 `PageMeta` record를 사용한다. 새 부가정보가 필요하면 `Map`으로 돌아가지 말고 `PageMeta`에 필드를 추가하거나 전용 meta 타입을 정의한다.
- 200 외 상태 코드가 필요하면 `ResponseEntity<ApiResponse<?>>`로 감싼다.
- 에러 응답은 `ApiResponse`가 아니라 `global.error.ErrorResponse`로 내려간다 (→ `error-handling.md`).

## 페이지네이션 — Spring Data `Pageable`/`Page`

```java
@GetMapping
public ApiResponse<List<PolicyCardResponse>> search(@PageableDefault(size = 20) Pageable pageable) {
Page<PolicyCardResponse> page = policySearchService.search(pageable);
return ApiResponse.ok(page.getContent(), page);
}
```

- max page size는 코드의 `Math.min`이 아니라 설정(`spring.data.web.pageable.max-page-size`)으로 제한한다.
- 금지: `@RequestParam int page/size` 직접 수신, `Math.min(size, 50)` 정규화, `totalPages` 직접 계산.
41 changes: 41 additions & 0 deletions .claude/rules/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
paths:
- "src/main/java/**"
---

# 패키지 구조

도메인 우선 + 단순 구조를 유지한다. 패키지 루트는 `com.bop.youthpick`.

```text
com.bop.youthpick
├── global
│ ├── common // ApiResponse 등 공통 응답 봉투
│ ├── config // SecurityConfig, RestAuthenticationEntryPoint 등
│ └── error // ErrorCode(인터페이스), CustomException, ErrorResponse, GlobalExceptionHandler
├── admin // 관리자 전용 코드 — 도메인별 하위 패키지로만 구성
│ └── {domain} // admin.policy, admin.board, admin.log, admin.sync, admin.user ...
│ ├── controller // Admin* 컨트롤러 (/api/v1/admin/** 경로)
│ ├── service // Admin* 서비스
│ ├── repository // Admin*Specifications 등 admin 전용 query-spec
│ └── dto // admin 응답/요청 전용 DTO
└── {domain} // user, policy, auth, favorite(즐겨찾기), diagnosis(자가진단) ...
├── controller // HTTP Controller와 API 입출력 조립
├── service // 비즈니스 흐름, scheduler, lock, 외부 API client
├── repository // Spring Data JPA repository interface
├── dto // 요청/응답 DTO, service 결과 DTO, 외부 API payload DTO
├── entity // JPA Entity, domain enum
└── exception // {Domain}ErrorCode(ErrorCode 구현 enum), {Domain}Exception(CustomException 상속)
```

- `global`에는 여러 도메인이 공유하는 공통 응답·설정·예외 처리만 둔다.
- 관리자(Admin) 전용 컨트롤러/서비스/query-spec/전용 DTO는 각 도메인이 아니라 최상위 `admin.{domain}` 패키지에 둔다(예: `admin.policy.controller.AdminPolicyController`). 여러 도메인에서 공유하는 Entity·일반 Repository·`{Domain}ErrorCode`는 그대로 각 도메인 패키지에 남기고, admin 쪽에서 그 도메인 패키지를 참조한다 — 반대 방향(도메인 → admin) 의존은 만들지 않는다.
- 도메인에 아직 필요 없는 하위 패키지는 만들지 않는다.
- Controller는 얇게: HTTP 파싱, Bean Validation, Service 호출, 응답 DTO 변환만. 비즈니스 판단·중복 검사·외부 API 호출·Entity 상태 변경은 Service에 둔다.

**금지 구조** — DDD식 패키지를 만들지 않는다:

```text
com.bop.youthpick.{domain}
├── api / application / domain / infrastructure / external // ❌
```
Loading
Loading