Skip to content

[deploy] 배포 - #122

Merged
You-Hyuk merged 58 commits into
mainfrom
develop
Sep 18, 2026
Merged

You-Hyuk merged 58 commits into
mainfrom
develop

Conversation

@You-Hyuk

@You-Hyuk You-Hyuk commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

게시판 · 댓글

  • 게시글 CRUD, 추천/추천취소, 통합검색, 엔티티별 백링크 조회 API
  • 엔티티 멘션(자동완성) 검색 API, TRACK 타입 및 트랙명 매칭 지원
  • 이번 주 인기 게시글 / 트렌딩 태그 조회 API
  • 댓글·답글 CRUD, 좋아요, 500자 제한, 연쇄 삭제 처리
  • 입력값 검증 강화 (본문 크기, 태그 개수, 검색어 와일드카드 이스케이프 등)

정책 변경 고지 메일

  • PolicyDocument / UserPolicyAgreement 이력 도메인 및 레거시 동의 데이터 백필
  • 정책 알림 대상 생성 · 메일 발송 Batch Job (Step1/Step2) 및 실패 재시도 스케줄러
  • 메일 발신자 표시 이름 · 로고 삽입, 템플릿 스타일 수정

기타

  • SecurityConfig, ErrorCode 정리 및 prod 환경변수 추가
  • CI 워크플로 권한 최소화

🤖 Generated with Claude Code

You-Hyuk and others added 30 commits September 10, 2026 16:01
post, post_entity_tag, post_recommend 테이블을 신설한다. entity_type은
Inquiry.targetId 패턴을 따라 FK 없이 enum+id 조합으로 폴리모픽 참조를
표현하며, content는 Hibernate 6.6 네이티브 JSON 매핑으로 jsonb 컬럼에
저장한다.

- V30 마이그레이션: post/post_entity_tag/post_recommend 테이블 및 인덱스
- Post/PostEntityTag/PostRecommend 엔티티, PostCategory/EntityType enum
- ErrorCode: POST_NOT_FOUND, ALREADY_RECOMMENDED, NOT_RECOMMENDED 추가

이슈 #116

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
게시글 entityTags(공연/아티스트/음악 멘션)를 화면에 표시하려면 FE가
엔티티 ID만으로는 제목·부제·썸네일을 알 수 없다. BE가 응답 시점에
Concert/Artist/ReleaseGroup을 조인해 EntityCardResponse로 채워주는
EntityLookupService를 도입한다. 상세·목록·백링크·통합검색 응답에서
공통으로 재사용한다.

- EntityCardResponse(type, id, title, subtitle, thumbnailUrl) DTO
- EntityLookupService.findCards(): (type, id) 키 목록을 배치 조회,
  삭제된 참조는 결과에서 조용히 제외
- EntityLookupServiceTest: 타입별 매핑·혼합 조회·삭제된 참조 제외 검증

이슈 #116

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
게시글 작성 중 공연/아티스트/음악 발매를 본문에 인라인 멘션하기 위한
자동완성 API를 추가한다. type별로 기존 검색 로직(Concert 제목,
Artist name/alias, ReleaseGroup 검색 쿼리)을 재사용하고 결과를
EntityLookupService의 카드 변환 로직으로 통일한다.

- GET /api/mentions/search?type=&q=&limit= (인증 불필요)
- ConcertRepository.searchByTitleForMention 추가
- EntityLookupService.toCard(...)를 패키지 접근으로 열어 MentionService에서 재사용
- SecurityConfig: GET /api/mentions/search permitAll 추가
- MentionService/MentionControllerTest: 타입별 위임·빈 결과 처리 검증
  (컨트롤러 테스트는 프로젝트 컨벤션인 standaloneSetup + LocalValidatorFactoryBean 사용)

이슈 #116

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
게시글 작성·조회·수정·삭제 API를 구현한다. Tiptap JSON 본문은 Spring
Boot 4의 기본 HTTP 컨버터가 Jackson 3(tools.jackson) 기반이라 Jackson 2
전용 타입(JsonNode/@JsonRawValue)을 요청·응답 DTO에 직접 쓸 수 없어
Object(런타임에는 Map/List)로 받고, 저장·응답 시 컨트롤러 계층과
무관한 별도의 Jackson 2 ObjectMapper로 문자열 변환한다.

- POST /api/posts, GET /api/posts/{id}, GET /api/posts,
  PATCH /api/posts/{id}, DELETE /api/posts/{id}
- REVIEW/INFO 카테고리는 entityTags 1개 이상 필수(생성·수정 모두 최종
  category 기준으로 검증), entityTags는 주어지면 전체 교체
- PostDetailResponse.isAuthor(비인증 시 false), isRecommended(비인증
  시 null) — 인증 여부에 따라 다른 널 규약을 의도적으로 적용
- 조회 시 viewCount +1 (Concert.viewCount와 동일한 벌크 UPDATE 패턴)
- TiptapTextExtractor: Tiptap 문서 트리에서 텍스트 노드만 추출해
  content_text(검색용 파생 컬럼)를 만든다
- SecurityConfig: GET /api/posts, GET /api/posts/** permitAll 추가
- PostService/PostController/TiptapTextExtractor 테스트 추가
  (컨트롤러 테스트는 standaloneSetup + AuthenticationPrincipalArgumentResolver로
  인증 사용자 시뮬레이션, Jackson 3 기본 컨버터가 테스트에서 쓰는
  JsonNode/Object 필드를 못 읽어 MappingJackson2HttpMessageConverter를 명시 등록)

이슈 #116

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
ArtistService.follow/unfollow와 동일한 패턴(존재 확인 -> 중복 확인 ->
DataIntegrityViolationException으로 UNIQUE 제약 race condition 방어)을
따르고, recommend_count는 비정규화 카운터로 @Modifying 쿼리를 통해 증감한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
GET /api/entities/{type}/{id}/posts로 특정 공연·아티스트·발매에 태그된
게시글을 추천순/최신순으로 조회한다. PostEntityTag를 서브쿼리로 조인해
Post를 찾고, 기존 PostService.toSummaryResponses를 재사용해 응답을
구성한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
GET /api/search로 게시글 제목·본문·태그된 엔티티명(아티스트/공연/발매)을
단일 JPQL 쿼리로 통합 검색한다. 최신순 고정.

Spring Boot 4.0.6부터 @DataJpaTest가 spring-boot-starter-data-jpa-test로
분리돼 있어 테스트 의존성을 추가했다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
FE에서 후기/정보 카테고리의 엔티티 태그 필수 정책을 제거함에 따라
BE의 동일 서버사이드 검증도 제거해 FE-BE 불일치를 해소한다.
entityTags 저장/조회 기능 자체는 유지한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
커뮤니티 목록 페이지 사이드바 위젯(이번 주 인기글, 지금 많이 언급된 태그)을 위한 백엔드 지원.
- GET /api/posts/popular: 최근 N일 게시글을 추천수 내림차순으로 상위 K개 조회
- GET /api/posts/trending-tags: 최근 N일 게시글에 태그된 엔티티를 언급 빈도순으로 상위 K개 조회
  (태그 재삽입 이슈를 피하기 위해 post.createdAt 기준으로 필터링)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
작업 브랜치 → develop → main 순으로 병합하는 흐름으로 전환하면서
CI가 develop으로의 PR도 검증하도록 대상 브랜치를 추가하고,
README의 CI/CD 설명을 새 흐름에 맞게 갱신

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
Tiptap content에서 추출한 순수 텍스트(contentText) 기준으로 길이를 검증해
비정상적으로 큰 payload를 DB에 저장하지 못하도록 막는다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
entityTags에 @SiZe(max=10) 검증을 추가하고, 이전에 추가한 본문 10000자
상한 검증과 함께 경계값·초과값 테스트를 작성한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
POST-07 게시글 댓글 기능을 위한 comment·comment_like 테이블과
post.comment_count 컬럼을 추가한다. 답글 삭제는 하드 삭제 대신
is_deleted 플래그를 통한 소프트 삭제로 처리한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
Comment·CommentLike 엔티티를 추가하고, 답글 달린 최상위 댓글
삭제 시 하드 삭제 대신 사용할 softDelete() 도메인 메서드를 둔다.
Post에는 commentCount 필드를 추가한다(답글 포함 전체 댓글 수,
생성 시에만 증가하고 소프트 삭제로는 감소하지 않음).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
CommentRepository(최상위 댓글 페이지네이션, 답글 배치 조회, 좋아요
수 증감), CommentLikeRepository(좋아요 여부·배치 조회)를 추가하고
PostRepository에 incrementCommentCount를 추가한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
CommentCreateRequest/Response, CommentResponse(답글 중첩),
CommentLikeCountResponse와 CommentNotFoundException·
CommentForbiddenException·InvalidReplyDepthException·
AlreadyLikedException·NotLikedException을 추가한다.
PostDetailResponse에 commentCount 필드를 추가한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
CommentService(getComments/create/delete/like/unlike)를 구현한다.
답글은 최상위 댓글 조회 시 배치 조회해 중첩하고, 소프트 삭제된
댓글은 응답 매핑 단계에서 content·authorNickname 등을 치환한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
PostCommentController(GET·POST /api/posts/{id}/comments),
CommentController(DELETE /api/comments/{id}, POST·DELETE
/api/comments/{id}/like)를 추가한다. 목록 조회는 비인증도 허용한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
be-review 중 CommentServiceTest가 Repository를 전부 mock 처리해
findByParentCommentIdInOrderByCreatedAtAsc·findLikedCommentIds의
실제 DB 동작(특히 빈 컬렉션 IN절 — 댓글 없는 게시글 조회 시 항상
거치는 경로)이 한 번도 실제 PostgreSQL로 검증되지 않은 커버리지
공백을 발견했다. @DataJpaTest로 8개 케이스를 검증한 결과 Hibernate
7.2.12에서는 예외 없이 정상 동작함을 확인했다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
/simplify 4개 관점(reuse·simplification·efficiency·altitude) 병렬
리뷰에서 나온 발견 중 아래 항목을 반영했다:
- 소프트 삭제 마스킹 정책(content·likeCount·author 노출 여부)을
  Comment 엔티티의 도메인 메서드(getDisplayContent 등)로 이동
- getComments/create의 게시글 존재 확인을 findById 대신
  existsById로 변경해 불필요한 전체 컬럼 로딩 제거
- 최상위 댓글이 없을 때 답글 조회 쿼리를 생략하는 가드 추가
- 소프트 삭제된 댓글은 닉네임·좋아요 배치 조회 대상에서 제외
- commentCount가 삭제 시 감소하지 않는 이유를 주석으로 명시

UserRepository에 닉네임 배치 조회 default 메서드를 추가해
PostService/CommentService 중복을 제거하는 안은 시도했으나,
Mockito @mock이 인터페이스 default 메서드를 실제로 호출하지 않아
기존 PostServiceTest 다수가 깨지는 것을 확인하고 되돌렸다(의도한
동작 변경 없이 적용 가능한 범위를 벗어남).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
FE 클라이언트 검증 우회 방지를 위한 서버 측 유효성 검사.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
본문만 플레이스홀더로 대체하고, 닉네임과 좋아요 수는 삭제 여부와 무관하게 실제 값을 반환한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
게시글이 삭제돼도 딸린 comment·comment_like·post_recommend 행이 정리되지 않아
orphan 데이터로 남는 문제를 수정한다. 이 저장소의 다른 마이그레이션은 전부
ON DELETE CASCADE 없는 FK만 사용하므로, DB 제약 대신 PostService.delete()에서
comment_like → comment → post_recommend → post_entity_tag → post 순으로
명시적으로 삭제한다.

같은 CommentRepository 변경에 묶여 댓글 목록 조회 정렬에 id 보조 정렬을 추가해
createdAt이 동일한 댓글이 페이지 경계에서 뒤섞이지 않도록 한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
- content_text 글자수(1만자) 제한은 Tiptap JSON에서 추출한 텍스트만 검증해,
  텍스트는 짧고 구조(attrs 등)만 방대한 요청으로 우회할 수 있었다. 직렬화된
  content 원본 문자열 크기(5만자)에도 별도 상한을 둔다.
- 게시글 통합 검색·멘션 자동완성에서 검색어에 포함된 %, _가 LIKE 와일드카드로
  해석돼 결과가 부정확해지는 문제를 백슬래시 이스케이프로 수정한다.
  PostgreSQL의 LIKE 기본 이스케이프 문자가 \이므로 JPQL 쿼리 자체는 건드리지
  않는다.
- 같은 (entityType, entityId) 태그를 중복 요청하면 post_entity_tag에 중복
  행이 쌓이던 문제를 생성 시 distinct 처리로 방지한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
- entityTags 리스트에 null 원소가 오면 @Valid가 건너뛰어 PostService가 그대로
  tag.entityType()을 호출해 NPE가 날 수 있었다. 원소 타입에 @NotNull을 추가해
  요청 단계에서 거부한다.
- PostUpdateRequest.title은 @SiZe만 있어 공백 문자열로 덮어쓸 수 있었다(생성
  시엔 @notblank로 막혀 있음). null(미변경)은 허용하되 공백만 거부하도록
  @pattern을 추가한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
- CommentService.like/unlike가 삭제 여부를 확인하지 않아 소프트 삭제된
  댓글에도 좋아요가 계속 쌓일 수 있었다. 삭제된 댓글이면 CommentNotFoundException을
  던지도록 한다.
- 좋아요·추천 카운트는 @Modifying 벌크 UPDATE로 DB 값만 갱신하고, 응답은
  이미 로드해둔 stale 엔티티 값에 +1/-1 해서 계산했다. 동시 요청 시 실제 DB
  값과 응답이 어긋날 수 있어, 갱신 직후 현재 값을 다시 조회해 응답한다
  (CommentRepository.findLikeCountById, PostRepository.findRecommendCountById).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
GET /api/search(통합 검색)가 permitAll 목록에 없어 비로그인 사용자가 401을
받고 있었다. 게시글 목록·상세 조회가 공개인 것과 일관되게 permitAll에
추가한다. 겸사겸사 이미 "/api/posts/**"에 포함되는 "/api/posts" 중복
나열도 정리한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
permissions: 선언이 없어 PR에서 체크아웃한 코드가 조직/저장소 기본 토큰
권한(쓰기 권한일 수 있음)으로 실행되고 있었다. contents: read로 최소
권한을 명시한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
기존 로직은 텍스트 노드가 나올 때마다 무조건 공백을 끼워 넣어, 같은 문단
안에서 굵게 등 서식으로 나뉜 인접 텍스트("안녕"+"하세요")까지 "안녕
하세요"로 바꿔버렸다. 이는 검색용 content_text와 1만자 글자수 검증 결과를
원문과 다르게 왜곡시킨다. 문단 등 블록(텍스트가 아닌 컨테이너) 경계에서만
구분자를 넣도록 순회 로직을 바꾸고, 관련 테스트를 갱신한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
- 게시글 삭제 시 댓글 좋아요를 postId 서브쿼리로 일괄 삭제해 IN 파라미터 무제한 확장 방지
- 추천 취소를 postId+userId 조건부 delete로 변경해 동시 취소 시 StaleStateException 대신 NotRecommendedException 반환

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
You-Hyuk and others added 26 commits September 15, 2026 16:37
FE 무한스크롤 전환에 맞춰 GET /api/mentions/search에 page 파라미터를 추가하고
limit 기본값을 10 -> 20으로 변경. 응답을 프로젝트 표준 PageResponse
({content, page, size, totalElements, totalPages}) 형식으로 변경.

- ConcertRepository.searchByTitleForMention: List -> Page 반환, countQuery 추가,
  startDate DESC + id DESC tie-breaker로 페이지 간 정렬 안정성 확보
- MentionService: ARTIST/RELEASE 검색에 id 오름차순 정렬을 적용해 동일한 이유로 안정성 확보
- MentionController/Service 테스트를 새 시그니처에 맞게 갱신, 페이지 파라미터 전달과
  정렬 적용을 검증하는 케이스 추가

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
트랙 멘션 도입을 위한 준비 작업. 트랙이 속한 앨범 id를 응답에 실어
FE가 앨범 상세 페이지로 링크를 구성할 수 있게 한다. TRACK 외 타입은
당분간 null을 반환한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
ConcertRepository.searchByTitleForMention과 동일한 패턴으로 트랙명
부분 일치 검색을 추가한다. 기존 LIKE 쿼리들과 달리 ESCAPE '\' 절을
명시해 서비스 레이어의 와일드카드 이스케이프가 실제로 동작하도록
했다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
앨범(RELEASE) 단위로만 지원되던 음악 멘션에 트랙 단위를 추가한다.
MentionService.searchTracks()가 트랙명으로 검색하고,
EntityLookupService.toTrackCardsById()가 트랙 → 앨범 → 아티스트를
배치 조회해 카드(제목=트랙명, 부제=아티스트·앨범명, releaseGroupId=
FE 링크 구성용)로 변환한다. 앨범 참조가 끊긴 트랙(FK 제약 없음)은
제목만 채운 카드로 안전하게 대체한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
PostRepository.searchPosts()가 ARTIST/CONCERT/RELEASE 태그된
게시글만 이름으로 찾을 수 있었다. TRACK 태그 조건을 동일한 형태로
추가해, 트랙이 멘션된 게시글도 트랙명으로 검색되게 한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
TRACK 멘션 카드(EntityCardResponse)에는 releaseGroupId가 채워지지만,
게시글 상세·목록의 entityTags(PostEntityTagResponse)에는 반영되지 않아
게시글 하단 태그 칩에서 트랙 앨범으로 딥링크할 수 없었다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
트랙 앵커가 앨범 상세 페이지로 귀결되는 구조라, RELEASE 백링크 조회 시
해당 릴리즈에 속한 트랙이 태그된 게시글도 함께 노출되도록 findByEntityTag 확장

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
TRACK이 별도 멘션 타입으로 분리된 이후에도 RELEASE 멘션 검색이 내부
트랙명까지 매칭해, 검색어와 무관해 보이는 앨범이 노출되는 문제가 있었다.
멘션 전용 쿼리(searchByTitleForMention)를 추가해 릴리즈 제목만 매칭하도록
분리하고, 기존 searchReleases(트랙/아티스트명 포함 매칭)는 일반 릴리즈
목록 API에서 그대로 사용한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
[feat] 엔티티 멘션 검색 API 무한스크롤 페이지네이션 지원
이슈 #118: 약관 변경 이메일 고지 시스템의 기반이 되는 정책 버전
메타데이터(PolicyDocument)를 도입하고, 관리자가 새 정책 버전을 등록하는
POST /api/admin/policies API를 추가한다. 원문 전체는 프론트엔드가
SSOT로 관리하므로 detailUrl로만 연결한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
이슈 #118: User의 agreed_terms/agreed_privacy boolean 필드를 대체할
UserPolicyAgreement(userId, policyId, agreedAt) 이력 테이블을 추가한다.
마이그레이션에서 레거시 PolicyDocument(TERMS/PRIVACY, version=legacy)를
시드하고, 기존에 agreed_terms/agreed_privacy=true였던 유저의 동의
이력을 agreed_at 기준으로 백필한다.

로컬 DB에 다른 브랜치의 V30/V31(post/comment 테이블)이 이미 적용되어
있어 정책 관련 마이그레이션을 V32/V33으로 재넘버링했다(V30__create_policy_document.sql
커밋 포함).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
이슈 #118: User.agreedTerms/agreedPrivacy/agreedAt 필드를 제거하고,
회원가입 완료 시 현재 시행 중인 TERMS/PRIVACY PolicyDocument에 대한
UserPolicyAgreement를 기록하도록 AuthService.register()를 리팩터링한다.
이미 동의 이력이 있으면 건너뛰어 탈퇴 후 재가입 시 유니크 제약 위반을
방지한다. 레거시 컬럼은 V34 마이그레이션으로 제거한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
이슈 #118: 정책 변경 이메일 발송 대상과 발송 상태(PENDING/SENT/FAILED),
재시도 횟수를 추적하는 PolicyNotificationTarget을 추가한다. 배치 Job이
이 테이블을 기준으로 대상 생성과 발송 상태 갱신을 수행한다(후속 커밋).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
이슈 #118: Gmail SMTP 기반 JavaMailSender 설정과 Thymeleaf 이메일
템플릿(policy-change-notice.html)을 추가한다. 법적 고지 목적상
수신거부 링크는 넣지 않고, 발신자 정보·변경사항 요약(changeSummary)·
시행일자를 본문에 필수로 노출한다. requiresReconsent인 경우 재동의
필요 안내를 추가로 표시한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
이슈 #118: Spring Batch를 도입하고, 정책 발행 시 ACTIVE 유저 중
이메일이 있고 아직 대상이 아닌 유저를 PENDING 상태의
PolicyNotificationTarget으로 등록하는 Tasklet을 추가한다. 배치
스키마는 Flyway(V36)로 관리하고, spring.batch.job.enabled=false로
앱 기동 시 자동 실행되지 않도록 한다(트리거는 후속 커밋).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
이슈 #118: PENDING 상태 PolicyNotificationTarget을 청크 단위로 읽어
메일을 발송하고 상태(SENT/FAILED)를 갱신하는 Step2를 추가하고,
Step1(대상 생성)과 Step2(메일 발송)를 policyNotificationJob으로
조립한다. 발송 실패(SMTP 예외 포함)는 FAILED로 마킹되어 재시도
대상이 된다(재시도 스케줄러는 후속 커밋).

임시 DB에 V1~V36 전체 마이그레이션을 적용해 실제 앱을 기동, Job/Step/
Reader/Processor/Writer 빈 그래프가 정상 등록되는지 확인했다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
이슈 #118: 정책 등록 트랜잭션 커밋 후(@TransactionalEventListener
AFTER_COMMIT) policyNotificationJob을 동기로 실행하는 트리거를
추가한다. 발송 실패(FAILED) 대상은 별도 스케줄러가 1시간 간격으로
최대 3회까지 재시도한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
임시 DB에 실제 앱을 기동해 관리자 API로 정책 등록 → 배치 실행까지
end-to-end로 검증한 결과 두 가지 실행 시점 버그를 발견해 수정했다.

- JobOperator.start(Job, JobParameters)는 인크리멘터가 설정된 Job에서
  run.id 외의 커스텀 파라미터(policyId)를 무시하고 버림 → run(...)으로 전환
- @TransactionalEventListener(AFTER_COMMIT) 콜백과 같은 스레드에서
  자체 트랜잭션을 가진 배치를 실행하면 트랜잭션 동기화 상태 충돌로
  JobInterruptedException 발생 → 별도 스레드에서 실행 후 join으로 대기

수정 후 Step1(대상 생성) → Step2(메일 발송, 실패 시 FAILED 처리) →
Job COMPLETED까지 실제 동작을 확인했다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
/simplify 4개 관점(Reuse/Simplification/Efficiency/Altitude) 병렬
리뷰 결과를 반영한다.

- PolicyNotificationMailProcessor와 PolicyNotificationRetryScheduler에
  중복되던 "유저 조회 → 이메일 확인 → 발송 시도 → SENT/FAILED 마킹"
  로직을 PolicyNotificationSender로 추출
- User.hasNoEmail()을 추가해 3곳에 흩어져 있던 이메일 공백 체크를 통합
- PolicyService.registerPolicy()에 기존 코드베이스 관례(AuthService,
  CalendarService와 동일한 존재 체크 + DataIntegrityViolationException
  catch)를 적용해 동시 등록 시 유니크 제약 위반을 도메인 예외로 변환

리팩터링 후 임시 DB에 실제 앱을 재기동해 정책 등록 → 배치 실행까지
동일하게 정상 동작함을 재검증했다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
이슈 #118: application-prod.yaml에서 spring.mail.username/password를
필수 환경변수(MAIL_USERNAME/MAIL_PASSWORD)로 요구하도록 설정했으나
cd.yml의 배포 스크립트가 이를 be.env에 반영하지 않고 있었다.
기존 DISCORD_WEBHOOK_* 패턴과 동일하게 GitHub Secrets → be.env 전달
과정에 추가한다.

배포 전 GitHub 저장소 Secrets에 MAIL_USERNAME/MAIL_PASSWORD 등록 필요.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
- 발신자가 이메일 주소로만 노출되던 것을 "커밍" 표시 이름으로 변경
- 정책 변경 안내 메일 템플릿에 커밍 로고를 인라인(CID) 첨부로 삽입
- 본문 표기를 "Coming"에서 "커밍"으로, 안내 링크 도메인을 comingg.com으로 통일

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
- 오프셋 페이징으로 인해 PENDING 대상이 누락되던 문제를 id 커서 기반 리더로 교체
- 최초 발송 실패 시 재시도 횟수가 먼저 소진되던 markFailed 로직 수정
- 동시 가입 시 약관 동의 저장 실패가 예외 변환 범위 밖에 있던 문제 수정
- SMTP 연결/응답 타임아웃 및 STARTTLS 필수화 설정 추가
- PolicyController 등록 API 테스트 보강

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
기존 약관에 명시된 변경사항 묵시적 동의 조항으로 충분하다는 PM 판단에 따라
재동의 필요 여부를 별도로 관리하지 않기로 결정. 등록 API 요청/응답, 엔티티,
메일 발송 로직·템플릿에서 관련 필드를 제거하고 컬럼을 삭제하는 마이그레이션을 추가.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
- 원문 링크 안내 문구와 링크 사이에 빈 줄 추가
- 발신자 표시명을 "커밍"으로 통일

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNjhau1bE58LegkwKtPBq2
[feat] 정책 변경 이메일 고지 시스템 구현
@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: c32663a8-ca90-43cd-9c7e-495750cdab40

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@You-Hyuk You-Hyuk self-assigned this Sep 18, 2026
@You-Hyuk You-Hyuk changed the title 게시판/댓글 기능 및 정책 변경 고지 메일 발송 인프라 추가 [deploy] 배포 Sep 18, 2026
@You-Hyuk
You-Hyuk merged commit 8cd7750 into main Sep 18, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant