release: dev를 main에 반영 - #222
Merged
Merged
Conversation
* chore: 이슈 및 PR 템플릿 추가 * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: 이슈 템플릿 메타데이터 추가 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: PR 템플릿 placeholder 추가 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: PR 템플릿 placeholder 추가 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: 이슈 템플릿 상단 테이블 제거 * chore: 이슈 및 PR 템플릿 메타데이터 추가 --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* chore: Spring Boot 3.5.16 다운그레이드 및 에러 구조 분리, AI 컨벤션 문서 추가 - Spring Boot 4.1.0 -> 3.5.16 다운그레이드 (starter 의존성 이름 3.x 기준 정리, Jackson 2.x import로 변경) - 공통 에러 코드 분리: ErrorCode -> GlobalErrorCode, 도메인별 에러 패키지(user/exception) 추가 - AI 코딩 에이전트용 컨벤션 문서 추가 (CLAUDE.md, AGENTS.md, docs/, .github/) Closes #4 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: 에러 코드 구조 개선 및 GlobalErrorCode 제거 * chore: AGENTS.md 파일 삭제 * docs: 작업 흐름 및 기술 기준 문서 수정 * refactor: 도메인별 ErrorCode 구조 개선 및 예외 처리 규칙 업데이트 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: 손정원 <sjungwon03@gmail.com>
* feat: BaseEntity 및 도메인 엔티티, 스키마 SQL 추가 - global: BaseEntity(createdAt/updatedAt 자동), JpaAuditingConfig - user: User, UserProfile, Role - policy: Policy, Region, PolicyRegion, PolicyApplication(즐겨찾기 통합), ApplicationChecklist - board: Post, Comment(대댓글), Attachment - sync: PolicyBatchHistory - log: AppLog, SearchLog - docs/schema.sql: MySQL 기준 스키마 문서 (연관관계·UNIQUE·인덱스 포함) 검증: ./gradlew test 통과 (H2 create-drop으로 전체 스키마 생성 확인) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: 유저 도메인 전 테이블 감사 컬럼(created/updated) 통일 - users·policy_applications·application_checklists·attachments에 updated_at 추가 - user_profiles에 created_at 추가 - BaseEntity 상속 범위와 스키마 문서 1:1 일치 - 불변 데이터(regions·policy_regions·로그·배치이력)는 의도적 제외 유지 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* chore: Claude Code 하네스 재구성 (.claude/rules + 스킬/커맨드/에이전트/훅/워크플로우) 규칙을 자동 로드되는 .claude/rules/로 이전하고 에이전트 하네스를 구성한다. - 규칙: 낡은 사본(루트 rules.md, docs/rules.md, docs/git-convention.md) 삭제, 실제 코드 기준으로 갱신해 주제별 .claude/rules/ 10개로 분리 (paths frontmatter로 경로 스코프 적용) - 진입점: AGENTS.md 신규(공통 진입점), CLAUDE.md는 AGENTS.md import로 축소 - 스킬: security-review(보안 점검 + 체크리스트) - 커맨드: fix-issue 슬래시 커맨드 - 에이전트: code-reviewer(리뷰 전용, 수정 안 함) - 훅: format-on-save(java Spotless), block-secrets(위험 삭제/시크릿 파일 차단) + settings.json 연결 - 출력 스타일: teaching - 워크플로우: pr-review.js(관점별 병렬 리뷰 후 검증) - .mcp.json, .worktreeinclude, agent-memory 시드 추가 - gradlew 실행 권한 복구, gitignore에 로컬 설정 파일 추가 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: CodeRabbit 리뷰 반영 (하네스 최소권한·훅 견고화·규칙 보강) PR #12 CodeRabbit 지적 4건 반영, 1건은 오탐으로 스킵. - fix-issue 커맨드: allowed-tools를 필요한 하위 명령으로 축소(최소권한). Bash(git:*)/Bash(gh:*) 광범위 허용 제거. - block-secrets 훅: 정규식 우회 차단. python3+shlex 토큰 파싱으로 재작성해 분리 플래그(-r -f)·롱옵션(--recursive --force)·sudo 접두·와일드카드 대상을 탐지. 시크릿 파일/스테이징 검사도 서브명령 단위로 판별. - error-handling 규칙: errors[].value에 민감 입력(비밀번호·토큰) 반사 금지 가이드 추가. - pr-review.js top-level return 지적은 스킵: Workflow 하네스가 본문을 async로 래핑하는 스크립트라 top-level return/await가 정상. 일반 ESM으로 오판한 오탐. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: block-secrets 훅에서 git 전역 옵션 뒤 서브명령 판별 PR #12 CodeRabbit 추가 지적 반영. args[0]=="add"만 보면 `git -C <path> add .` / `git -c k=v add .env` 처럼 전역 옵션이 서브명령 앞에 오는 호출에서 .env 차단 로직이 우회됐다. 값 받는 전역 옵션(-C/-c/--git-dir 등)을 먼저 건너뛰고 서브명령을 판별하도록 수정. 우회/정상 케이스 재검증 완료. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* chore: Region/User 레포지토리 및 신규 에러코드 추가 온보딩 프로필 생성에 필요한 조회/검증용 리포지토리와, 중복 프로필·존재하지 않는 지역코드에 대응하는 에러코드를 추가한다. Refs #13 * feat: UserProfile 생성 팩토리 메서드 추가 Entity에 setter를 두지 않는 규칙을 지키기 위해, User/Region/온보딩 입력값으로 UserProfile을 생성하는 정적 팩토리 메서드 create()를 추가한다. Refs #13 * feat: 온보딩 프로필 제출 API 추가 POST /api/v1/users/{userId}/profile 엔드포인트를 추가한다. 사용자 존재 여부, 중복 프로필 여부, 지역코드 유효성을 검증한 뒤 UserProfile을 생성한다. 인증 기능이 아직 없어 userId는 임시로 경로변수로 받으며, 인증 도입 후 principal 기반으로 교체할 예정이다(TODO 명시). Closes #13 * test: 온보딩 프로필 제출 API 테스트 추가 OnboardingService 단위테스트(정상 생성, 사용자 없음, 중복 프로필, 지역코드 없음)와 OnboardingController MockMvc 테스트(정상 요청 201, 필수값 누락 400)를 추가한다. Refs #13 * fix: 관심분야/관심키워드를 콤마 문자열 대신 배열로 주고받도록 수정 온보딩 화면에서 categories(대분류 5개 중 최대 3개)/keywords(19개 중 선택)는 체크박스 다중선택이라, 요청/응답 모두 List<String>으로 받고 내려준다. 콤마 결합 문자열은 UserProfile 저장 포맷일 뿐이므로 서비스 계층에서만 join/split 처리한다. Refs #13 * fix: 프로필 저장 시 유니크 제약 위반을 PROFILE_ALREADY_EXISTS로 통일 existsByUserId() 확인과 save() 사이에 레이스가 있어, 동시 요청에서는 user_profiles.user_id UNIQUE 제약 위반이 DataIntegrityViolationException으로 전파되어 GlobalExceptionHandler의 일반 Exception 처리(500)로 떨어졌다. save()를 감싸 같은 도메인 에러(PROFILE_ALREADY_EXISTS, 409)로 변환한다. Refs #13
* chore: 백엔드 CI 워크플로 추가 (GitHub Actions) dev/main 대상 PR·push에서 JDK 21(Temurin)로 ./gradlew build를 실행해 빌드·테스트를 자동 검증한다. 테스트는 H2 in-memory로 돌고 Redis 연결은 lazy라 외부 인프라 없이 통과한다. concurrency로 중복 실행을 취소하고, 실패 시 테스트 리포트를 아티팩트로 업로드한다. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: CI를 테스트 대신 Spotless 포맷 검사로 변경 - Spotless(google-java-format) 플러그인 추가, spotlessCheck를 CI 단계로 사용 - CI에서 ./gradlew build(테스트 포함) 제거 → spotlessCheck만 실행 - 기존 34개 java 파일을 google-java-format에 맞게 일괄 리포맷(spotlessApply) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: CI 최소권한/타임아웃/자격증명 정리 및 .editorconfig 추가 CodeRabbit 리뷰 반영: - 워크플로 permissions를 contents: read로 제한 - format job에 timeout-minutes 15 지정 - checkout에 persist-credentials: false 설정 google-java-format(2-space)과 일치하도록 .editorconfig 추가. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: CI에 빌드(assemble) 체크 추가 포맷 검사에 더해 컴파일/패키징 검증을 위해 ./gradlew assemble을 실행한다. 테스트는 CI에서 제외하므로 build 대신 assemble을 사용하고, job 이름을 Format & Build로 변경했다. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: CI에 H2 테스트 실행 추가 - 테스트 스텝 복원: ./gradlew build로 컴파일/패키징 + 테스트 실행 (spotlessCheck는 앞 스텝과 중복이라 -x로 제외) - 테스트가 H2 test 프로파일(application-test.yml, create-drop)을 쓰도록 YouthpickApplicationTests에 @activeprofiles("test") 추가 - 테스트 실패 시 리포트 아티팩트 업로드 복원 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: 포맷을 google-java-format AOSP(4-space)로 변경 - Spotless 설정을 googleJavaFormat().aosp()로 변경 - 기존 java 파일을 4-space로 일괄 리포맷 - .editorconfig의 java 들여쓰기를 4-space로 갱신 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore: Flyway 도입 및 V1 초기 스키마 마이그레이션 추가 - flyway-core, flyway-mysql 의존성 추가 - docs/schema.sql(PR #10) 기반 V1__init_schema.sql 생성 - 기본 프로필 Flyway 비활성(H2는 ddl-auto 유지), prod(MySQL)에서만 활성 + ddl-auto=validate Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: 로컬 MySQL 프로파일에 Flyway 적용 및 V2 마이그레이션 추가 - local 프로파일을 H2 -> docker compose MySQL + Flyway(validate)로 전환 - V2: policies.missing_count 추가 (3회 연속 누락 시 HIDDEN 처리용) - 테스트는 H2 유지: test 전용 application.yml이 main 설정을 가리도록 파일명 정렬 - docker-compose.yml MySQL 서비스 활성화 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: 로컬 DB 규칙을 Flyway + MySQL 기준으로 갱신 - infra.md: 로컬 기본 H2 문장을 compose MySQL + Flyway(validate)로 교체, 스키마 변경은 새 V{n} 마이그레이션 추가 원칙 명시 - 테스트는 H2 유지 + test 설정 shadowing 주의사항 추가 - AGENTS.md 기술 스택 요약 동기화 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: 로컬 DB 기본 계정이 compose 로컬 전용임을 주석으로 명시 CodeRabbit 리뷰 반영: fallback 제거 대신 로컬 전용 기본값임을 문서화. (제거 시 zero-config 로컬 실행이 깨지고, 같은 값이 compose fallback에도 있어 제거 실익이 없음) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: application.yml flyway 주석을 변경된 전략에 맞게 갱신 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refac: 리뷰 반영 — 테이블명 2건 변경 - application_checklists -> policy_application_checklists (부모 policy_applications와 prefix 정렬, 기존 단수 prefix 컨벤션 유지) - search_logs -> search_histories - V1 직접 수정: 아직 dev merge 전이라 rename 마이그레이션 대신 V1 정정 (적용된 로컬 DB는 볼륨 리셋 필요 — docker compose down -v) - 엔티티 @Table/@Index, docs/schema.sql 동기화 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: spotlessApply 포맷 정리 (CI spotlessJavaCheck 통과용) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: 손정원 <sjungwon03@gmail.com>
* chore: IntelliJ 코드 스타일 XML 추가 (google-java-format AOSP) - docs/ide/intellij-java-google-aosp-style.xml: 공식 GoogleStyle 스킴을 AOSP(4-space, continuation 8-space)로 변형한 IntelliJ import용 코드 스타일 - docs/ide/README.md: import 방법과 저장 시 자동 포맷 안내 - docs/README.md: 개발 환경 섹션에 IDE 가이드 링크 추가 .idea/는 gitignore 대상이라 커밋하지 않고 import용 XML만 문서 경로에 둔다. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: dev 잔여 포맷 위반 정리 (spotlessApply) Spotless(AOSP) 설정 이전에 머지된 온보딩 관련 9개 파일이 포맷을 위반해 dev의 spotlessCheck가 실패하던 것을 spotlessApply로 정리했다. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* refac: ApiResponse meta를 Map에서 PageMeta record로 변경 * chore: spotlessApply 포맷 적용
* docs: 프론트 연동 기준 API 명세 문서(api-spec.md) 추가 Closes #31 * docs: API 경로를 엔티티 리소스 중심으로 재설계 /me, /users/{userId} 없이 JWT 인증 정보로 사용자를 식별하고 user-profiles, policy-applications, policy-batch-histories 등 엔티티 리소스명을 경로에 직접 노출하는 방식으로 변경. Closes #31 * docs: OAuth 인가 URL/콜백 파라미터를 실제 구현에 맞게 정정 authorization-url은 redirect_uri/state를 프론트가 넘기는 게 아니라 서버가 고정값(youthpick.oauth.frontend-callback-uri)과 자체 생성 state(Redis TTL 저장)로 처리한다. 콜백도 redirectUri 대신 provider가 돌려준 state를 그대로 받아 CSRF 방어에 쓴다. feat/20-social-login(PR #22) 구현 기준으로 문서만 정정.
* feat: 카카오, 구글, 네이버 소셜 로그인 인증 기능 추가 authorization-url 발급 -> provider 콜백 -> access token 교환/유저정보 조회 -> 회원 findOrCreate -> Redis 세션(Spring Session) 저장 흐름으로 세션 기반 로그인을 구현한다. /api/v1/auth/me, /api/v1/auth/logout 추가, SecurityConfig는 STATELESS를 걷어내고 세션(IF_REQUIRED) + SecurityContextRepository로 정리한다. Closes #20 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * refac: 소셜 로그인 인증 방식을 세션에서 JWT(access/refresh)로 전환 Redis 세션 대신 JWT access/refresh token으로 인증한다. refresh token은 사용자별로 Redis에 TTL과 함께 저장하고, /api/v1/auth/token/refresh에서 서명·만료 검증 후 Redis 저장값과 일치할 때만 rotate한다. OAuth state도 세션이 아니라 Redis(OAuthStateStore)로 옮겨 완전한 stateless 구조로 만든다. - SecurityConfig: STATELESS 유지, JwtAuthenticationFilter로 Authorization Bearer 헤더를 해석해 SecurityContext를 채운다. - AuthController: 콜백을 POST(JSON body)로 변경해 토큰을 직접 반환하고, token/refresh 엔드포인트를 추가한다. - OAuth redirect_uri를 프론트 콜백 라우트로 통일(youthpick.oauth.frontend-callback-uri). - build.gradle: spring-session-data-redis 제거, jjwt 추가. - AGENTS.md / auth-security.md / security-review 스킬 문서를 JWT 구조에 맞게 갱신. - 부수 수정: UserRepositoryTest가 @DataJpaTest 슬라이스에서 JpaAuditingConfig를 못 읽어 간헐적으로 실패하던 문제를 @import로 고침. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat: 로그인 사용자 조회용 @currentuser 파라미터 리졸버 추가 컨트롤러에서 @AuthenticationPrincipal + authService.getCurrentUser 수동 호출을 반복하는 대신 @currentuser User user로 바로 주입받도록 한다. SecurityContext의 AuthPrincipal에서 userId를 읽어 조회하고, 인증되지 않았으면 AuthException(A001)을 던진다. 인가 규칙은 여전히 SecurityConfig의 경로 매처로 관리한다(어노테이션 기반 인가 데코레이터는 시도했다가 경로기반으로 되돌렸다). WebMvcConfigurer로 전역 등록되어 모든 @WebMvcTest 슬라이스에 로드되므로, @currentuser를 쓰지 않는 OnboardingControllerTest에도 슬라이스 부팅을 위해 @MockitoBean AuthService를 추가했다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * refac: @currentuser가 User 엔티티 대신 userId(Long)를 반환하도록 변경 CurrentUserArgumentResolver가 DB 조회 없이 SecurityContext의 AuthPrincipal.userId()를 그대로 주입한다. User 엔티티가 필요한 곳(AuthController.me)은 Controller에서 authService.getCurrentUser(userId)를 직접 호출한다. 리졸버가 AuthService 의존을 잃어서 OnboardingControllerTest의 불필요한 @MockitoBean AuthService도 제거했다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: CodeRabbit 리뷰 반영 — 패키지 구조/보안/트랜잭션 이슈 수정 - 패키지 구조: auth.client/config/jwt를 도메인 우선 구조(controller/service/ repository/dto/entity/exception)에 맞춰 auth.service로 통합 - OAuthClient: 전역 RestClient connect/read timeout(spring.http.client.*) 추가, provider 응답의 providerId가 blank면 예외로 거부(계정 오귀속 방지) - OAuthProperties.Registration: toString에서 clientSecret 마스킹 - RefreshTokenStore: Redis에 refresh token 원문 대신 SHA-256 해시만 저장, 재발급을 Lua 스크립트 기반 compare-and-rotate로 원자화해 동시 refresh 요청의 중복 발급을 막음 - AuthService: login()에서 provider 통신을 트랜잭션 밖으로 분리(DB 커넥션 점유 방지), find-or-create 유니크 제약 충돌 시 재조회로 복구, refresh()를 RefreshTokenStore.rotate 기반으로 재작성, logout()이 Redis만 다뤄 @transactional 대상이 아님을 문서화 - application-local.yml: JWT_SECRET 하드코딩 fallback 제거(다른 DB 계정 fallback과 달리 토큰 위조 위험과 직결되는 값이라 환경변수 필수로 변경) - CSRF 관련 지적(logout/SecurityConfig)은 세션 기반 코드 리뷰 시점의 지적으로, 현재는 STATELESS + Bearer 토큰 인증이라 CSRF 대상이 아니어서 반영하지 않음 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat: API 명세 기준 인가 규칙 및 관리자(403) 처리 추가 docs/api-spec-db.md의 권한 컬럼(관리자/회원/비회원/공통)을 SecurityConfig의 authorizeHttpRequests에 반영한다. 관리자 전용 경로는 hasRole("ADMIN")으로 막고, 아직 컨트롤러가 없는 명세상 경로도 미리 규칙을 걸어 컨트롤러가 추가돼도 기본이 열려 있지 않게 한다. - AuthErrorCode.FORBIDDEN(A008, 403) + RestAccessDeniedHandler 추가: 권한 부족 시 RestAuthenticationEntryPoint(401)와 동일한 JSON 포맷으로 응답 - SecurityConfigTest: 관리자/회원/공개 경로의 401·403·인가통과 케이스 검증 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: GitHub 리뷰 반영 — JWT 파싱 방어/성능, 테스트 보강 - JwtTokenProvider.getUserId: subject가 숫자가 아니면 NumberFormatException이 그대로 전파되던 것을 AuthException(INVALID_TOKEN)으로 감싼다 - validateAccessToken/validateRefreshToken이 검증된 Claims를 반환하도록 바꿔 JwtAuthenticationFilter/AuthService.refresh()가 토큰을 요청당 1회만 파싱 하도록 정리(기존엔 검증+userId+role 조회에서 최대 3번 재파싱) - JwtAuthenticationFilter: role이 null인 토큰은 "ROLE_null" 권한을 만들지 않고 인증을 채우지 않은 채 통과시킨다 - OAuthClientTest: Google userinfo 정규화 테스트 추가(Kakao/Naver만 있었음) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs: API 명세 DB에 OAuth 콜백/토큰 재발급 엔드포인트 추가 이번 PR에서 새로 구현한 POST /api/v1/auth/oauth/{provider}/callback, POST /api/v1/auth/token/refresh가 명세 표에 빠져 있어 SecurityConfig의 permitAll() 대상과 문서가 불일치했다. 두 엔드포인트 모두 비회원(로그인 전) 호출이라 권한은 비회원으로 기재한다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat: 회원 탈퇴(DELETE /api/v1/users) 경로/권한 선점 docs/31-api-spec의 새 명세는 회원 탈퇴 경로를 /api/v1/auth/me가 아니라 엔티티 중심의 DELETE /api/v1/users로 정의한다. 실제 탈퇴 로직(soft delete, confirmText 검증 등)은 아직 구현하지 않고, SecurityConfig에 인증 필수 경로만 먼저 등록해 컨트롤러가 없는 지금도 인가 규칙이 어긋나지 않게 한다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs: Spring 요청 처리(Filter/Interceptor)와 JWT 인증/인가 학습 노트 추가 서블릿 Filter 체인 vs HandlerInterceptor vs HandlerMethodArgumentResolver의 차이, Spring Security FilterChainProxy 내부 필터 순서, 이 프로젝트의 JwtAuthenticationFilter/JwtTokenProvider/RefreshTokenStore/@currentuser가 실제로 어떻게 연결되는지, 인증 실패(401)와 인가 실패(403)가 왜 각각 RestAuthenticationEntryPoint/RestAccessDeniedHandler에서 처리되고 GlobalExceptionHandler로는 못 잡는지를 정리한 학습 노트. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * refac: refresh token을 HttpOnly 쿠키로 전달 XSS로 JS가 refresh token을 읽지 못하도록 access token은 그대로 body로 내려주되 refresh token은 HttpOnly 쿠키(refresh_token, path=/api/v1/auth)로 전달하도록 바꿨다. 배포 시 프론트/백엔드가 크로스사이트라 SameSite=None; Secure가 필요하고, 로컬은 SameSite=Lax로 완화한다(RefreshCookieProperties, 프로필별 override). - AuthController: callback/refresh가 Set-Cookie를 실어야 해서 ResponseEntity<ApiResponse<AccessTokenResponse>>로 응답을 감쌌다. refresh는 @RequestBody 대신 @CookieValue로 refresh token을 받고, logout은 clear-cookie 헤더를 추가한다. - refresh 엔드포인트는 쿠키만으로 preflight 없이도 도달 가능해져 CSRF 여지가 생기므로, Origin 헤더가 있는데 허용 목록에 없으면 거부한다 (SecurityConfig.ALLOWED_ORIGINS 공유, logout은 Bearer 헤더 인증이라 영향 없음). - CORS allowCredentials를 true로 켜야 브라우저가 쿠키를 실어 보낸다. - TokenResponse(accessToken+refreshToken)는 AuthService 내부 반환 타입으로만 남기고, 공개 응답 DTO는 refreshToken이 없는 AccessTokenResponse로 분리했다. part of #20 --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* docs: Claude Code 코드 포맷(Google AOSP Style) 규칙 문서화 Closes #33 * docs: spotlessCheck를 test/build와 분리된 필수 단계로 명시 CodeRabbit 리뷰 반영: CI가 spotlessCheck를 별도 실행하고 build에서는 제외하므로, test/build로 대체 가능하다는 뉘앙스를 제거한다.
* feat: 앱 로그를 Log4j2 + JDBC로 app_logs 테이블에 적재 Spring Boot 기본 Logback을 Log4j2로 교체하고, app_logs 전용 커넥션 풀과 커스텀 Appender로 WARN 이상 로그를 DB에 적재한다. 요청 컨텍스트(traceId/method/uri/ip/ userId)는 MDC 필터가 채우고, GlobalExceptionHandler의 미처리 예외를 로깅해 실제 에러가 쌓이도록 연결했다. 테스트는 log4j2-test.xml로 JDBC Appender 없이 콘솔만 사용해 Docker 없이 돈다. Closes #41 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * refac: app_logs 테이블/엔티티를 ERD 물리명(application_logs)에 맞춰 개명 Flyway V3로 app_logs → application_logs 테이블/인덱스명을 변경하고, AppLog 엔티티와 log4j2 커스텀 Appender/DataSource를 ApplicationLog*로 함께 개명해 SQL·클래스명·설정을 일치시켰다. Refs #41 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: 로그 테이블 개명 마이그레이션을 V3에서 V4로 변경 V3은 #38(regions 시드) PR에서 이미 사용 중이라 번호가 겹친다. Refs #41 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: 리뷰 반영 — userId 파싱 실패/프록시 뒤 IP 로깅 문제 수정 - ApplicationLogAppender: user_id MDC 값이 숫자가 아니면 NumberFormatException이 Log4j2까지 전파되어 ignoreExceptions=true로 로그 행 전체가 조용히 유실됐다. NumberFormatException을 잡아 NULL로 적재하도록 수정. - RequestLogContextFilter: request.getRemoteAddr()는 리버스 프록시/로드밸런서 뒤에서 프록시 IP만 반환한다. X-Forwarded-For 헤더의 첫 값을 우선 사용하도록 수정. Refs #41 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
목록 조회(role/accountStatus/provider 필터 + Pageable), 온보딩 프로필 상세, role 변경, 회원 탈퇴(soft delete) 엔드포인트를 /api/v1/admin/users 아래 추가한다. SecurityConfig에 이미 예약된 /api/v1/admin/** -> hasRole(ADMIN) 규칙을 그대로 사용한다. Closes #43
목록 조회(category/visibilityStatus/신청기간 겹침 필터 + Pageable), 전체 수정, 노출상태 변경, soft delete 엔드포인트를 /api/v1/admin/policies 아래 추가하고 /api/v1/admin/regions에 지역 전체 목록 조회를 추가한다. Policy에는 관리자 soft delete용 deletedAt을 신규 추가한다(Flyway V4) — 배치가 쓰는 visibility(원본 소실 정책 재노출용 상태 전환)와는 독립적인 필드다. PolicyRepository/PolicyRegionRepository도 이번 작업에서 함께 만든다. Closes #45
LoginHistory 엔티티(BaseEntity 상속)와 login_histories 테이블(Flyway V5)을 신규 추가하고, AuthService.login() 성공 시 로그인 이력을 기록하도록 훅을 건다. userId/기간(createdAt) 필터를 지원하는 읽기 전용 목록 조회를 /api/v1/admin/login-histories에 추가한다. 기존 AuthServiceTest를 새 LoginHistoryRepository 의존성에 맞춰 갱신하고, 로그인 성공 시 이력이 기록되는지(및 state 검증 실패 시 기록되지 않는지) 검증을 추가한다. Closes #58
keyword(query/normalized 부분일치)와 기간(createdAt) 필터를 지원하는 읽기 전용 목록 조회를 /api/v1/admin/search-logs에 추가한다. SearchLogRepository도 이번 작업에서 함께 만든다. Closes #52
* feat: 관리자 - 정책 신청 관리 API 추가 목록 조회(userId/policyName/status/deadline 범위 필터 + Pageable), 체크리스트 읽기 전용 조회, 상태 변경 엔드포인트를 /api/v1/admin/policy-applications 아래 추가한다. PolicyApplicationRepository/ApplicationChecklistRepository도 이번 작업에서 함께 만든다. 프론트 스펙은 상태를 INTERESTED/PREPARING/SUBMITTED/CLOSED 4단계로 보지만 실제 ApplicationStatus enum은 INTERESTED/APPLIED/COMPLETED 3단계다. 기존 enum을 그대로 쓰고 PR에 차이를 명시한다. Closes #48 * fix: 신청 관리 API 상태값/성능/일관성 보완 - 상태값을 프론트 계약(INTERESTED/PREPARING/SUBMITTED/CLOSED)에 맞춰 변경 (기존 APPLIED/COMPLETED, V3 마이그레이션으로 기존 데이터 재매핑) - id/userId/policyId를 프론트가 기대하는 문자열로 JSON 직렬화 - 목록 조회에서 policy/user를 fetch join하여 N+1 쿼리 제거 - 관리 해제(soft delete)된 신청을 목록/상세/체크리스트 조회에서 제외 - 체크리스트 조회 순서를 id 오름차순으로 명시 * Revert "fix: 신청 관리 API 상태값/성능/일관성 보완" This reverts commit c83a32e. * fix: 신청 관리 API 리뷰 코멘트(N+1, soft-delete, 상태값 방어) 반영 - PolicyApplication/ApplicationChecklist에 @SQLRestriction(deleted_at IS NULL)을 적용해 관리 해제(soft delete)된 행이 모든 조회에서 누락 없이 제외되도록 함 - 목록 조회에서 policy/user를 LEFT fetch join(count 쿼리는 제외)해 N+1 제거, policyName 필터의 join과 fetch join이 같은 policy 경로를 재사용하도록 정리 - ApplicationStatus.valueOf 실패 시 500 대신 P005(INVALID_APPLICATION_STATUS)로 매핑 - 체크리스트 조회 정렬을 id 오름차순으로 명시(findByApplicationIdOrderByIdAsc) - deadline 비교의 타임존 가정(JVM 기본 타임존=KST 고정 운영)을 주석으로 명시 * fix: policyName 필터 없이도 policy를 fetch join하도록 수정 policyJoin()이 기존 join/fetch가 없을 때 항상 일반 join(root.join)만 만들어, policyName 필터가 없는 목록 조회에서는 policy가 실제로 fetch되지 않고 N+1이 남아있었다(kilo-code-bot 재리뷰 지적). count 쿼리에서는 fetch가 허용되지 않으므로 일반 join, content 쿼리에서는 fetch join을 생성하도록 분기하고, fetch 여부를 Hibernate.isInitialized로 검증하는 테스트를 추가. --------- Co-authored-by: canfactor <jyk47133@gmail.com>
* feat: 관리자 - 커뮤니티 관리 API 추가 게시글 목록 조회(category/authorId/createdAt 범위 필터 + Pageable), 댓글/첨부파일 조회, 게시글/댓글 soft delete 엔드포인트를 /api/v1/admin/community 아래 추가한다. board 패키지에 없던 PostRepository/CommentRepository/AttachmentRepository와 BoardErrorCode(신규 접두어 B)도 함께 만든다. 게시글 createdAt은 스펙의 날짜만 있는 mock과 달리 실제 시:분초 타임스탬프를 그대로 내려준다(스펙 문서 자체가 이 방향을 제안). category(QUESTION/REVIEW/FREE)와 첨부파일 필드명(fileKey ← fileUrl)의 프론트 표시값 차이는 PR에 명시한다. Closes #50 * refac: 관리자 커뮤니티 API 리뷰 반영 (N+1 방지, 에러코드 접두어 문서화) - 게시글 목록: user를 fetch join으로 함께 로딩해 페이지당 N+1 쿼리 제거 (count 쿼리에는 fetch를 적용하지 않는다) - 댓글 조회(findByPostId): @EntityGraph(user, parent)로 N+1 제거 - error-handling.md 접두어 표에 board 도메인의 B 접두어 추가 리뷰: kilo-code-bot (PR #51)
* feat: 관리자 - 애플리케이션 로그 조회 API 추가 logLevel, keyword(message/traceId/requestUri 부분일치), 기간(createdAt) 필터를 지원하는 읽기 전용 목록 조회를 /api/v1/admin/application-logs에 추가한다. AppLogRepository도 이번 작업에서 함께 만든다. Log4j2 JDBC Appender(#41, 오픈)가 아직 없어 실제 데이터는 비어 있을 수 있다. 프론트 스펙의 로그 레벨 4단계(ERROR/WARN/INFO/DEBUG)를 그대로 허용하되, DEBUG는 현재 문서화된 실제 적재 레벨(INFO/WARN/ERROR)에 없어 당분간 빈 결과만 나올 수 있음을 PR에 명시한다. Closes #54 * refac: 관리자 애플리케이션 로그 API 리뷰 반영 (타임존 고정, 대소문자, 빈값 일관성) - JVM 기본 타임존을 기동 시점에 Asia/Seoul로 고정한다(YouthpickApplication.main). KST 기간 필터와 @CreatedDate가 공유하는 LocalDateTime.now() 기준이 서버 배포 환경의 기본 타임존에 좌우되지 않게 한다. - level 필터를 대소문자 무시 비교로 변경(자유 문자열 컬럼이라 적재값의 대소문자를 보장할 수 없음) - logLevel 빈 문자열을 keyword와 동일하게 "필터 없음"으로 처리(기존엔 400) 선행 와일드카드 LIKE 검색의 인덱스 미사용 지적은 현재 app_logs가 비어 있고(#41 미구현) 스키마 변경(FULLTEXT 인덱스 등)이 필요해 이번에는 반영하지 않는다 — 실제 로그 볼륨이 생기면 별도로 다룬다. 리뷰: kilo-code-bot (PR #55)
status(BatchStatus 실제 enum 값), 기간(requestedAt) 필터를 지원하는 읽기 전용 목록 조회를 /api/v1/admin/batch-job-logs에 추가한다. PolicyBatchHistoryRepository도 이번 작업에서 함께 만든다. POST .../sync(수동 강제 동기화)는 이번 이슈 범위에서 제외한다 — 실제 온통청년 API 연동/정책 upsert 로직(#15, 오픈)이 없어 만들 수 없다. PolicyBatchHistory에는 프론트 스펙이 요구하는 userId/inputPayload 컬럼도 없고, status는 스펙의 SUCCESS/PARTIAL/FAILED 3단계가 아니라 REQUESTED/RUNNING/SUCCEEDED/FAILED 4단계(요청 생명주기) 모델이다 — PR에 명시한다. Closes #56
* chore: regions 지역 마스터 시드 마이그레이션 추가 (V3)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: Flyway 폴더를 migration(DDL)/seed(데이터)로 분리
리뷰 피드백 반영 — 스키마 변경(db/migration)과 데이터 적재(db/seed)를
폴더로 분리하고 spring.flyway.locations에 두 경로를 명시한다.
버전(V{n})은 두 폴더에 걸쳐 하나의 순서를 공유한다.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NGhwM7GG2Zacc8DYr5PqA
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
#42에서 로그 엔티티를 AppLog에서 ApplicationLog로 리네임했지만, 이후 #55에서 추가된 관리자 애플리케이션 로그 조회 API 파일들이 옛 이름을 계속 참조해 컴파일이 실패했다. 타입 참조를 ApplicationLog로 맞추고, 테스트의 네이티브 쿼리가 참조하던 옛 테이블명(app_logs)도 실제 테이블명(application_logs)으로 수정했다. Closes #61 Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix: Flyway V4 마이그레이션 버전 충돌 해소 (정책 soft delete → V6) #42(로그 rename)와 #47(정책/지역 관리 API)가 거의 동시에 merge되며 두 PR이 각각 V4 버전의 마이그레이션을 추가해, 로컬 MySQL(Flyway)로 앱을 기동하면 "Found more than one migration with version 4"로 시작이 실패했다. 테스트는 H2 + create-drop으로 Flyway 자체가 꺼져 있어 잡히지 않는 문제였다. 로컬 flyway_schema_history를 확인해 V4__rename_app_logs_to_application_logs만 이미 적용된 것을 확인하고, V4__add_policies_deleted_at을 다음 빈 버전인 V6으로 재넘버링했다. Closes #63 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * refac: 마이그레이션 버전 재정렬로 V3 공백 제거 V3가 비어있던 이유는 로그 rename 마이그레이션이 애초 V3로 계획됐다가 V4로 바뀐 이력 때문이었다. V4(rename)→V3, V5(login_histories)→V4, V6(policies.deleted_at)→V5로 한 칸씩 당겨 공백 없이 연속되게 정리했다. 로컬 MySQL 볼륨은 이미 옛 버전 번호(V4/V5/V6)로 마이그레이션 이력이 쌓여있어 파일명 변경과 충돌하므로 docker compose down -v로 초기화했다. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
#64에서 V3 공백을 메운다고 재넘버링했으나, db/migration과 db/seed가 버전 번호를 공유한다는 걸 확인하지 않았다. db/seed/V3__seed_regions.sql (#38)이 이미 V3를 쓰고 있어 재넘버링이 실제로는 새로운 버전 충돌이었다. V3__rename_app_logs_to_application_logs.sql → V4, V4__create_login_histories.sql → V5, V5__add_policies_deleted_at.sql → V6으로 원복해 db/seed/V3와의 충돌을 없앴다. Closes #65 Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
프론트가 호출하는 GET /api/v1/admin/policy-sync-jobs/summary에 매핑된 컨트롤러가 없어 NoResourceFoundException(500)이 발생했다. 정책 수집 배치 파이프라인(POST /policy-sync-jobs 실행, PolicyBatchHistory 적재)은 별도 PR에서 다루므로, 이번엔 지금 실데이터로 계산 가능한 부분만 구현한다. - activeCount: Policy.visibility=VISIBLE AND deletedAt IS NULL 카운트 - missingCount: Policy.visibility=HIDDEN AND deletedAt IS NULL 카운트 (배치가 원본 소실 정책을 HIDDEN 처리하는 기존 컨벤션 그대로 활용) - parseErrorCount/dbFailCount: 배치 파이프라인이 아직 없어 항상 0. 파이프라인 PR에서 실제 집계로 교체 예정. PolicyRepository에 countByVisibilityAndDeletedAtIsNull 쿼리 메서드를 추가하고, sync 도메인에 AdminPolicySyncJobController/Service/DTO를 새로 만들었다. Closes #71 Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
AdminCommunityPostController/AdminCommunityCommentController가
/api/v1/admin/community/posts, /api/v1/admin/community/comments처럼
중첩 경로를 썼는데, login-histories/application-logs/search-logs/
batch-job-logs/policy-applications 등 다른 관리자 API는 전부
/api/v1/admin/{kebab-case} 평평한 구조다.
프론트가 관례대로 /api/v1/admin/community-posts를 호출했다가 정적
리소스로 오인되어 500(NoResourceFoundException)을 만났다. 경로를
/api/v1/admin/community-posts, /api/v1/admin/community-comments로
맞췄다.
Closes #69
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
/api/v1/auth/token/refresh가 access token뿐 아니라 refresh token도 매번 재발급(rotate)해 Redis 값과 쿠키를 함께 교체하던 것을, refresh token은 최초 로그인 시점에 발급된 값을 설정된 TTL이 다할 때까지 그대로 재사용하도록 바꿨다. - RefreshTokenStore: 원자적 교체용 rotate()(Lua 스크립트)를 제거하고 TTL을 건드리지 않는 matches() 조회 메서드로 교체. - AuthService.refresh(): 새 refresh token을 생성하지 않고 matches()로 검증 후 access token만 재발급. - AuthController.refresh(): refresh token이 바뀌지 않으므로 Set-Cookie를 다시 내려주지 않음. - 관련 테스트와 문서(auth-security.md, security-review/checklist.md, api-spec-db.md, spring-request-processing-and-jwt-auth.md)를 실제 동작에 맞게 갱신. Closes #67 Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* chore: Flyway 도입 및 V1 초기 스키마 마이그레이션 추가 - flyway-core, flyway-mysql 의존성 추가 - docs/schema.sql(PR #10) 기반 V1__init_schema.sql 생성 - 기본 프로필 Flyway 비활성(H2는 ddl-auto 유지), prod(MySQL)에서만 활성 + ddl-auto=validate Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: 로컬 MySQL 프로파일에 Flyway 적용 및 V2 마이그레이션 추가 - local 프로파일을 H2 -> docker compose MySQL + Flyway(validate)로 전환 - V2: policies.missing_count 추가 (3회 연속 누락 시 HIDDEN 처리용) - 테스트는 H2 유지: test 전용 application.yml이 main 설정을 가리도록 파일명 정렬 - docker-compose.yml MySQL 서비스 활성화 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: 로컬 DB 규칙을 Flyway + MySQL 기준으로 갱신 - infra.md: 로컬 기본 H2 문장을 compose MySQL + Flyway(validate)로 교체, 스키마 변경은 새 V{n} 마이그레이션 추가 원칙 명시 - 테스트는 H2 유지 + test 설정 shadowing 주의사항 추가 - AGENTS.md 기술 스택 요약 동기화 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: 빈 DB일 때 앱 시작 시 초기 정책 수집 트리거 추가 - PolicySyncStartupRunner: policies 0건이면 전량 수집 1회 실행 - youthpick.sync.startup-on-empty 프로퍼티로 on/off (기본 off, local만 on) — 테스트/CI의 빈 H2에서 외부 API 호출 사고 방지 - PolicySyncService: 수집 진입점 (본 구현은 #15에서) - PolicyRepository 추가 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: 로컬 DB 기본 계정이 compose 로컬 전용임을 주석으로 명시 CodeRabbit 리뷰 반영: fallback 제거 대신 로컬 전용 기본값임을 문서화. (제거 시 zero-config 로컬 실행이 깨지고, 같은 값이 compose fallback에도 있어 제거 실익이 없음) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: application.yml flyway 주석을 변경된 전략에 맞게 갱신 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: application.yml flyway 주석을 변경된 전략에 맞게 갱신 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refac: 리뷰 반영 — 테이블명 2건 변경 - application_checklists -> policy_application_checklists (부모 policy_applications와 prefix 정렬, 기존 단수 prefix 컨벤션 유지) - search_logs -> search_histories - V1 직접 수정: 아직 dev merge 전이라 rename 마이그레이션 대신 V1 정정 (적용된 로컬 DB는 볼륨 리셋 필요 — docker compose down -v) - 엔티티 @Table/@Index, docs/schema.sql 동기화 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: 온통청년 API 응답 DTO 추가 (plan 1-1) - YouthPolicyApiResponse/YouthPolicyItem record — 전 필드 @JsonProperty 명시 - 실제 응답 형태 픽스처(JSON) + 역직렬화 테스트 (sbizCd 소문자, 빈 문자열 케이스) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: 온통청년 정책 API 클라이언트 추가 (plan 1-2, 1-3) - PolicyApiClient: pageSize=1000 페이지 순회 + 누적==totCount 검증, 성공 3중 체크(HTTP/JSON파싱/resultCode), 페이지당 3회 재시도 후 회차 중단 - PolicySyncException: 배치 내부 실패 전용 (Job이 잡아 FAILED 기록) - 타임아웃 spring.http.client.*, 키는 YOUTH_API_KEY 환경변수 주입(.env.example) - MockRestServiceServer 테스트 6종: 성공/403 errorCode/HTML/resultCode!=200/재시도/키 미설정 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: spotless 재적용 — 기존 파일 포맷 정규화 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: 정책 전처리 PolicyMapper 추가 (plan 2-1) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: zipCd를 PolicyRegion으로 정규화 (plan 2-2) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: zipCd 코드 체계 주석 정정 — 온통청년 자체 코드가 아니라 현행 법정시군구코드 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: 배치 비교용 정책 스냅샷 조회 및 배치 이력 Repository 추가 (3-1) - PolicyRepository.findSyncSnapshots(): 경량 projection(policyNo, lastMdfcnDt, visibility)으로 전체 정책 스냅샷 조회. HIDDEN 포함 — VISIBLE만 조회하면 재등장 정책이 신규로 분류돼 policy_no 유니크 위반이 나므로 전체 조회로 설계 변경 - PolicyBatchHistoryRepository 추가 (조회 메서드는 Phase 5에서) - @DataJpaTest: 스냅샷 필드 검증 + lastMdfcnDt null 정책 + 빈 테이블 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: 정책 청크 업서트 Writer 및 누락 3회 숨김 규칙 추가 (3-2) - Policy: missingCount 필드 + markMissing(3회 도달 시 HIDDEN), updateFrom(전 필드 갱신 + 누락 상태 리셋), copyOf(재시도용 비영속 복사본) - PolicyUpsertWriter: 청크(100) 단위 트랜잭션 커밋, policyNo 조회로 신규 INSERT/변경 UPDATE 분기, 지역 링크 교체(delete 선-flush로 유니크 충돌 방지), 청크 실패 시 건별 재시도로 범인 격리(error_count), hideMissing으로 사라진 정책 누락 집계 - TDD(Red-Green)로 진행. 잡은 버그: 청크 롤백 후 IDENTITY id가 엔티티에 남아 재시도 저장이 실패 → copyOf 복사본으로 해결 - 테스트: PolicyTest(단위 3), PolicyUpsertWriterTest(@DataJpaTest 4 — 신규/변경/범인격리/숨김), 스냅샷 HIDDEN 포함 검증, PolicyFixture 공용화 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: 정책 전량 수집 오케스트레이션 구현 (4-1) - PolicySyncService.runFullSync 본 구현: 이력 REQUESTED→RUNNING → 전량 fetch → 스냅샷/지역 각 1쿼리 로드 → 메모리 비교(신규/변경/SKIP) → 청크 upsert → 누락 처리 → 이력 SUCCEEDED. fetch 실패 시 DB 무손상 FAILED 기록 후 예외 재던짐 - SKIP 조건은 수정일 동일 + VISIBLE — HIDDEN이 같은 수정일로 재등장하면 변경으로 취급해 updateFrom이 복구(3-1 스냅샷 visibility 활용) - PolicyBatchHistory: request/start/succeed/fail 상태 전이 메서드 (fail 메시지 1000자 절단), 단위 테스트 5건 - PolicyMapper.toRegions → resolveRegions(List<Region>) 리팩토링 — PolicyRegion 링크 생성 책임을 Writer로 일원화 - 통합 테스트 3건(@DataJpaTest + 목 클라이언트): 성공 4분기+이력, 회차 실패(DB 무손상+FAILED), HIDDEN 재등장 복구 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: 정책 수집 배치 중복 실행 가드(Redis lock) 추가 - PolicySyncLock: SETNX+TTL(10분)+UUID 소유 토큰, 해제는 Lua 스크립트로 소유 확인+삭제 원자 처리 (RefreshTokenStore와 동일 패턴) - runFullSync: 락 획득 실패 시 이력 없이 PolicySyncException, finally 해제 - 테스트: 락 획득/거부/해제 단위 3건 + 서비스 통합(거부 시 무동작, 실패 회차에도 해제) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: 정책 수집 일일 스케줄러 추가 (설정 on/off, 기본 off) - PolicySyncScheduler: @scheduled cron(기본 새벽 4시), 예외는 로그만 남기고 삼킴 - @ConditionalOnProperty(youthpick.sync.scheduler.enabled) — 로컬 기본 off - @EnableScheduling을 스케줄러 클래스에 부착해 off면 스케줄링 인프라도 미기동 - 테스트: 조건부 빈 등록 2건 + 예외 무전파 1건 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: 정책 수집 수동 트리거 관리자 API 추가 (202/409) - POST /api/v1/admin/batch/policy-sync: 락 동기 획득 후 백그라운드 실행, 즉시 202 - 실행 중이면 SyncErrorCode.SYNC_ALREADY_RUNNING(SY001) 409 - 백그라운드 실패는 이력(FAILED)+로그만, 예외 무전파 - GET status는 미구현 - dev의 GET /admin/batch-job-logs(#57)가 상위호환으로 대체 - 테스트: 컨트롤러 202/409 + 서비스 락거부/성공/백그라운드실패 5건 (TDD) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # src/main/java/com/bop/youthpick/global/error/GlobalExceptionHandler.java
# Conflicts: # src/main/java/com/bop/youthpick/post/controller/PostController.java
* fix: 정책 키워드 검색에 지역명 부분일치 포함 검색창에 "서울"처럼 지역명을 입력해도 title/keywords/description 등 텍스트 컬럼에 그 문자열이 없으면 결과에서 빠졌다. keyword 조건에 PolicyRegion.sidoName LIKE 매칭을 추가해 지역명 검색도 함께 걸리게 한다. Closes #199 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: 키워드 지역명 매칭 시에도 전국 정책을 뒤로 미는 정렬 적용 기존 정렬은 sidoName(명시적 지역 필터) 파라미터일 때만 전국 정책을 뒤로 밀었다. 이번에 keyword로 지역명 매칭을 추가하면서, 전국 정책은 모든 시도에 지역 행이 걸려 있어 keyword="서울" 같은 검색에도 항상 걸리는데 이 demote 규칙이 빠져 있어 지역 특화 정책보다 먼저 뜨는지 여부가 순전히 최신순(id desc) 우연에 달려 있었다. 같은 demote 규칙을 keyword 지역 매칭 케이스에도 적용한다. Refs #199 --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
doRunFullSync()의 실패율 초과 메시지에서 "10%"의 %가 이스케이프 안 돼 있어 String.formatted()가 UnknownFormatConversionException을 던졌다. 정확히 실패율이 10%를 넘는 순간에만 터지는 버그라, policy_batch_history에 남아야 할 "실패율 10% 초과 — 에러 건수: X/Y" 메시지가 의미 없는 "Conversion = ' '"로 덮여 실제 원인 파악이 안 됐다. "10%%"로 이스케이프. 회귀 테스트: title 컬럼 길이(300) 초과로 일부 저장이 실패해 실패율이 10%를 넘는 시나리오에서, 수정 전에는 동일한 UnknownFormatConversionException으로 실패하고 수정 후에는 FAILED 이력에 의도한 메시지가 정상 기록됨을 확인. Closes #203 Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
- 댓글 생성/목록조회/수정/삭제, 대댓글(1단) 지원 - CommentErrorCode를 B006~B008로 재배번(BoardErrorCode.COMMENT_NOT_FOUND의 B005와 충돌 방지) - 정상/예외/경계 케이스 테스트 추가 Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
business_period_etc(bizPrdEtcCn)는 자유 서술 텍스트인데 V1에서 날짜 컬럼 옆에 묶이며 VARCHAR(64)로 잡혀, 64자 넘는 사업기간 설명을 가진 정책 11건이 매 회차 Data truncation으로 저장 실패했다. 같은 성격의 earnEtcCn/addAplyQlfcCndCn/ptcpPrpTrgtCn과 동일하게 TEXT로 맞춘다. 함께 넓히는 컬럼: - reference_url1 등 URL 3종 VARCHAR(500) -> VARCHAR(1000) (원본에 500자 넘는 URL 1건 존재 — 나머지 1건의 실패 원인) - application_period_raw VARCHAR(64) -> VARCHAR(255) (aplyYmd 원문은 신청기간 2개만 돼도 43자라 3개째에서 같은 방식으로 터진다) Closes #207 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes #210 Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
springdoc-openapi를 추가하고 전체 REST 컨트롤러에 @Tag/@operation을 붙여 /swagger-ui에서 실제 코드 기준 API 문서를 바로 확인할 수 있게 한다. Closes #213
성공 응답의 실제 타입(ApiResponse<T>)과 요청 DTO는 springdoc이 이미 정확히 추론하지만, 예외 핸들러가 던지는 ErrorResponse는 컨트롤러 시그니처에 드러나지 않아 문서에서 빠져 있었다. OpenApiConfig에서 ErrorResponse 스키마를 명시적으로 등록하고 OperationCustomizer로 모든 오퍼레이션에 4XX/5XX 공통 에러 응답을 전역으로 붙인다. Closes #215
외부 API 원본이 VARCHAR 컬럼 상한을 넘으면 그대로 INSERT를 시도하다 Data truncation으로 정책 1건 전체가 저장 실패하던 문제를 고쳤다(#207 사례). PolicyMapper 클래스 javadoc의 실패 격리 원칙(필드 하나 파싱 실패는 그 필드만 null 처리)을 길이 초과에도 동일하게 적용한다 — 상한 초과 시 경고 로그 (plcyNo·필드명·실제 길이·상한)를 남기고 잘라서 저장하며 예외를 던지지 않는다. - policy_no, title, description, keywords, category, middle_category, organization_name, job_codes, school_codes, 각종 code, URL 3종, application_period_raw, operating_institution_name 등 Policy 엔티티의 @column(length = ...) VARCHAR 필드에 적용. TEXT/LONGTEXT 컬럼은 제외. - PolicyMapperLengthLimitsTest: 매퍼의 길이 상한 상수가 Policy @column(length)와 어긋나면 리플렉션으로 잡아내 드리프트를 방지. - PolicyMapperTest: 길이 초과 시 실제로 잘려서 저장되는지, 경계값(정확히 상한)은 잘리지 않는지, 경고 로그에 plcyNo·필드·길이·상한이 담기는지 검증. - PolicySyncServiceTest: 기존 실패율 테스트가 title 길이 초과를 DB 저장 실패 트리거로 썼는데 이제는 절단되어 성공하므로, plcyNm 누락(title NOT NULL 위반)으로 트리거를 바꿔 동일한 실패율 계산·기록 시나리오를 유지했다. Closes #208
프론트 추천 하드필터(buildRecommendations)가 결혼·전공·취업·학력·특화조건으로 부적격 정책을 제외하려면 정책 응답에 해당 코드가 있어야 하는데, PolicyCardResponse/ PolicyDetailResponse에는 이 필드가 없어 프론트 매퍼가 전부 '제한없음'으로 고정되고 있었다. Policy 엔티티는 이미 해당 필드를 갖고 있어 엔티티 변경 없이 두 응답 DTO에 jobCodes/schoolCodes/maritalStatusCode/majorCodes/specializationCodes를 추가하고 from(Policy) 매핑을 갱신했다. PolicyComparisonItemResponse는 여전히 비교표용 원문 필드만 노출하므로 관련 javadoc/문서만 최신화했다. Closes #92 Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
PolicyBatchHistory.fail(String)이 카운트를 건드리지 않아 실패율 초과로 FAILED 처리된 회차가 관리자 배치 로그 화면에 항상 0/0/0/0으로 보였다. 카운트를 함께 받는 fail(...) 오버로드를 추가하고, PolicySyncService의 실패율 초과 분기에서 실제 집계값을 넘기도록 수정한다. Closes #205
#219와 #221이 독립적으로 작업되며 PolicySyncServiceTest에서 같은 "길이 초과로 DB 저장 실패" 트리거를 썼는데, #221이 그 실패 자체를 없애버려(초과분을 잘라 저장) #219가 추가한 전량실패 테스트가 dev에서 깨졌다. plcyNm 누락(title NOT NULL 위반) 방식으로 트리거를 통일한다. Closes #223
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
변경 내용
dev에 쌓인 변경사항을main에 반영한다(105 commits). main 전용으로 존재하던.github/템플릿 커밋 2건은 현재 dev의 템플릿과 내용이 동일해 병합 시 충돌이 없음을 로컬에서 dry-run으로 확인했다.주요 항목(최근 순):
검증
git merge --no-commit --no-ff origin/dev로컬 dry-run — 충돌 없음spotlessCheck,build/test) 통과 후 dev에 머지된 상태연결 이슈
없음(릴리즈 반영)