Skip to content

chore: develop 변경사항 main 릴리즈 배포#69

Merged
Hminkyung merged 43 commits into
mainfrom
develop
May 25, 2026
Merged

chore: develop 변경사항 main 릴리즈 배포#69
Hminkyung merged 43 commits into
mainfrom
develop

Conversation

@Hminkyung
Copy link
Copy Markdown
Collaborator

@Hminkyung Hminkyung commented May 25, 2026

📌 작업 요약

🌿 브랜치 정보

  • Source: develop (기본)
  • Target: main (릴리즈)

✅ 체크리스트

  • 브랜치 컨벤션 준수 (feat/refac/hotfix/chore/design/bugfix)
  • 커밋 컨벤션 준수 (feat/fix/refactor/docs/style/chore)
  • self-review 완료
  • 테스트 및 로컬 실행 확인 완료

🧪 테스트 결과

  • GitHub Actions deploy-ec2 실행 확인 (workflow_dispatch, ref: main)

    • 결과:
    • 스크린샷: ssm-send-step
  • 원격 배포 순서/재기동 확인

    • 결과:
    • 스크린샷: ssm-order
  • 배포 후 컨테이너 상태 확인

    • 결과:
    • 스크린샷: compose-ps

Summary by CodeRabbit

Release Notes

  • New Features

    • 회원가입 및 사용자 프로필에서 언어 선택 기능 추가 (한국어, 영어, 중국어, 베트남어)
    • 사용자 언어 설정 변경 기능 추가
    • 실패한 가정통신문 분석 재시도 기능 추가
    • 분석 실패 원인 추적 및 오류 메시지 개선
  • Bug Fixes

    • 분석 실패 시 추출된 데이터 보존 개선
  • Documentation

    • AI 분석 연동 및 실패 정책 문서 추가

Review Change Stack

deli-minju and others added 30 commits May 19, 2026 20:54
refactor: 가정통신문 분석을 AI 서버 client로 전환
…lure-policy

refactor: AI 서버 장애 시 분석 실패 정책 반영
…-save

feat: AI 가정통신문 전체 분석 응답 저장 연동
@Hminkyung Hminkyung self-assigned this May 25, 2026
@Hminkyung Hminkyung added the chore 빌드, 설정, 의존성, 운영성 작업 label May 25, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

📝 Walkthrough

Walkthrough

이 PR은 뉴스레터 분석 파이프라인을 OpenAI 직접 호출에서 외부 AI 서버로 전환하고, 사용자 언어 코드 관리를 도입하며, 분석 실패 추적 및 재시도 기능을 구현합니다. AiNewsletterClient를 통한 AI 서버 통합, Newsletter 엔티티의 실패 필드 추가, 그리고 단계별 실패 처리를 포함합니다.

Changes

AI 서버 통합 및 뉴스레터 실패 처리 재구성

Layer / File(s) Summary
사용자 언어 코드 및 알림 설정 기능
src/main/java/com/gachi/be/domain/auth/dto/request/SignupRequest.java, src/main/java/com/gachi/be/domain/auth/service/impl/AuthServiceImpl.java, src/main/java/com/gachi/be/domain/user/entity/User.java, src/main/java/com/gachi/be/domain/user/dto/response/UserMeResponse.java, src/main/java/com/gachi/be/domain/user/dto/request/ChangeLanguageRequest.java, src/main/java/com/gachi/be/domain/user/api/controller/UserController.java, src/main/resources/db/migration/V12__user_add_language_code.sql, src/main/resources/db/migration/V13__user_add_notification_enabled.sql, src/test/java/com/gachi/be/domain/auth/api/controller/AuthControllerIntegrationTest.java
User 엔티티에 languageCode(기본값 KO)와 notificationEnabled(기본값 true) 필드를 추가하고, 회원가입 시 언어 코드를 필수로 받습니다. 사용자 내 정보 조회 응답에 언어 코드와 알림 설정을 포함하며, PATCH /api/v1/users/me/language 엔드포인트로 언어 변경 시 진행 중인 뉴스레터를 FAILED로 전환합니다.
AI 서버 클라이언트 및 설정
src/main/java/com/gachi/be/global/config/external/AiServerProperties.java, src/main/java/com/gachi/be/global/config/external/ExternalApiConfig.java, src/main/java/com/gachi/be/global/exception/ExternalApiException.java, src/main/java/com/gachi/be/domain/newsletter/pipeline/AiNewsletterClient.java, deploy/docker-compose.yml, deploy/.env.example, src/main/resources/application.yml
AiServerProperties로 AI 서버 연결 설정(baseUrl, 타임아웃)을 관리하고, AiNewsletterClient에서 /ai/newsletters/analyze 엔드포인트를 호출합니다. OpenAI 설정은 제거되고, 외부 API 예외 처리 시 cause 파라미터를 추가합니다.
뉴스레터 실패 추적 및 재시도
src/main/java/com/gachi/be/domain/newsletter/entity/Newsletter.java, src/main/java/com/gachi/be/domain/newsletter/dto/response/NewsletterStatusResponse.java, src/main/java/com/gachi/be/domain/newsletter/repository/NewsletterRepository.java, src/main/java/com/gachi/be/domain/newsletter/api/controller/NewsletterController.java, src/main/resources/db/migration/V11__newsletter_failure_reason.sql, src/test/java/com/gachi/be/domain/newsletter/entity/NewsletterTest.java, src/test/java/com/gachi/be/domain/newsletter/dto/response/NewsletterStatusResponseTest.java
Newsletter 엔티티에 failureStage(최대 50자)와 failureReason(TEXT) 필드를 추가하고, 상태 조회 시 canRetry 플래그를 반환합니다. 실패 처리 메서드를 fail(stage, reason) 형태로 변경하고, failWithSnapshot(...) 메서드로 OCR/번역 결과를 보존합니다. POST /api/v1/newsletters/{newsletterId}/analysis/retry 엔드포인트를 추가하여 FAILED 상태에서만 재시도를 허용합니다.
뉴스레터 AI 분석 파이프라인 재구성
src/main/java/com/gachi/be/domain/newsletter/pipeline/NewsletterAiAnalyzer.java, src/test/java/com/gachi/be/domain/newsletter/pipeline/NewsletterAiAnalyzerTest.java
NewsletterAiAnalyzer가 OpenAI 직접 호출을 제거하고, AiNewsletterClient의 응답(title, summary, items)을 받아 체크리스트와 캘린더 미리보기를 생성합니다. AI 응답을 정규화하고, 공백 시 원문/번역문 기반 폴백 로직을 추가합니다. dateStatus가 confirmed인 항목만 캘린더 미리보기를 저장합니다.
뉴스레터 파이프라인 실패 처리
src/main/java/com/gachi/be/domain/newsletter/pipeline/NewsletterPipelineService.java
runPipeline에서 단계별 failureStage 추적을 도입하고, AI 분석 실패(ExternalApiException)를 별도로 처리하여 markFailedWithSnapshot(...)으로 실패 저장합니다. PDF 페이지 변환 로직을 제거하고 원본 파일 키를 OCR 입력으로 사용합니다.
뉴스레터 서비스 업로드 및 재시도
src/main/java/com/gachi/be/domain/newsletter/service/NewsletterService.java, src/main/java/com/gachi/be/domain/newsletter/service/impl/NewsletterServiceImpl.java
upload 메서드에서 userLanguage 파라미터를 제거하고, userId로부터 저장된 언어 코드를 조회하여 사용합니다. retryAnalysis(userId, newsletterId) 메서드를 추가하여 체크리스트/캘린더 이벤트를 삭제하고 파이프라인을 재실행합니다.
설정, 마이그레이션 및 전역 코드
src/main/java/com/gachi/be/global/code/ErrorCode.java, src/main/java/com/gachi/be/global/code/SuccessCode.java, src/main/java/com/gachi/be/domain/user/entity/enums/UserStatus.java, src/main/java/com/gachi/be/domain/user/repository/UserRepository.java, src/main/java/com/gachi/be/domain/auth/service/AuthenticatedUserResolver.java, src/main/java/com/gachi/be/global/security/JwtAuthenticationFilter.java, deploy/nginx/nginx.conf, deploy/nginx/nginx.https.template.conf, docs/error-code.md, docs/newsletter-ai-analyze-integration.md, docs/newsletter-ai-failure-policy.md, build.gradle
UserStatus enum을 user.entity.enums 패키지로 이동하고, 관련 import들을 갱신합니다. ErrorCode와 SuccessCode에 재시도/언어 변경 관련 상수를 추가합니다. 뉴스레터와 사용자 테이블에 새 컬럼을 추가하는 마이그레이션을 제공합니다. nginx 설정에서 /ai/docs, /ai/openapi.json, /ai/redoc에 기본 인증을 추가합니다. PDF 의존성을 제거하고, AI 통합 및 실패 정책 문서를 추가합니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


Possibly related PRs

  • GACHI-Project/GACHI-BE#61: 뉴스레터 실패 추적(failureStage/failureReason), 스냅샷 저장, 재시도 엔드포인트가 메인 PR과 동일 코드 단위로 함께 반영되어 직접 연관됩니다.
  • GACHI-Project/GACHI-BE#37: 메인 PR은 PR #37의 가정통신문 업로드/상태/AI 분석 파이프라인을 실패 추적과 재시도 중심으로 직접 리팩터링하므로 코드 수준에서 밀접하게 연관됩니다.
  • GACHI-Project/GACHI-BE#66: 사용자 언어 설정 변경 및 진행 중 뉴스레터 취소 구현이 메인 PR과 동일한 코드 변경 흐름을 포함하므로 직접 관련됩니다.

Suggested reviewers

  • deli-minju
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 develop 브랜치의 누적 변경사항을 main으로 릴리즈 배포하는 작업의 주요 목적을 명확하게 반영하고 있습니다.
Description check ✅ Passed PR 설명에서 필수 섹션인 작업 요약, 브랜치 정보, 체크리스트가 포함되었으며, 관련 이슈(#68)가 명시되어 있습니다.
Linked Issues check ✅ Passed PR이 #68의 릴리즈 배포 목적을 충족하며, 포함된 모든 이슈(#56, #57, #60, #62, #63, #65)와 PR(#58, #59, #61, #64, #66, #67)의 변경사항이 develop에서 main으로 전달되고 있습니다.
Out of Scope Changes check ✅ Passed 변경사항이 모두 develop 브랜치의 누적된 변경들(AI 서버 연동, 사용자 언어/알림 설정, 마이그레이션, 테스트 추가 등)로 구성되어 있으며 릴리즈 범위 내에 포함됩니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@deploy/docker-compose.yml`:
- Around line 109-110: 현재 docker-compose에 backend가 ai의 service_healthy에 하드 의존하고
있어 AI 헬스체크 실패 시 backend 재기동이 차단됩니다; docker-compose의 backend 서비스 설정에서 ai에 대한
"condition: service_healthy"를 제거하거나 대체하여 컨테이너 시작 의존성을 분리하세요 (예: 제거하거나
"condition: service_started"로 바꿔 AI 헬스 상태에 따른 재시작 제어를 애플리케이션 레이어로 위임). 변경 대상
식별자: 서비스 이름 "backend", "ai"와 조건 키 "condition: service_healthy".

In
`@src/main/java/com/gachi/be/domain/newsletter/pipeline/AiNewsletterClient.java`:
- Line 59: The current AiNewsletterClient logging writes full request/response
bodies (e.g., requestBody and the response variables logged around lines 75-77),
which may leak sensitive OCR text; change the logging in AiNewsletterClient so
log.info/log.error no longer prints full payloads but instead logs a masked or
truncated summary: include payload length, HTTP status code, and a trace
identifier (generate or pass a traceId), and if needed mask content (e.g., show
first N chars + "...[masked]") rather than the full requestBody/responseBody;
update all places where requestBody and responseBody are logged to use this
summarized format and ensure any error logs include the traceId and status but
not raw body content.

In
`@src/main/java/com/gachi/be/domain/newsletter/pipeline/NewsletterAiAnalyzer.java`:
- Around line 63-65: The summary is not being translated before storage: update
the pipeline so that when analysisResponse.summary() is present you first
normalize it (using normalizeSummary or similar) and then call translateIfNeeded
on that raw summary (pass language, "summary", and newsletterId) falling back to
translatedText when AI summary is absent; ensure the code paths around
normalizeSummary(analysisResponse.summary(), translatedText, originalText) and
translateIfNeeded(...) mirror what you did for title so
NewsletterServiceImpl.getSummary() returns the translated summary for non-KO
users.
- Around line 194-200: The preview title is using the original item.title()
instead of the stored, translated Checklist content; update the
CalendarPreviewEvent creation in saveExtractedItems() to use the saved
checklist's content (e.g., savedItem.checklist().content() or the
Checklist#getContent accessor) instead of item.title(), still applying
trimToMax(..., CHECKLIST_TEXT_MAX_LENGTH) and .trim(), and keep the rest of the
constructor (extractedDate, true, checklistIdList(savedItem.checklist()))
unchanged so CalendarRegisterServiceImpl.getPreview()/register() shows the
translated KO content consistently.

In
`@src/main/java/com/gachi/be/domain/newsletter/repository/NewsletterRepository.java`:
- Around line 104-114: The cancelInProgressByUserId update currently only sets
status and language, leaving failure metadata empty; modify the JPQL update in
NewsletterRepository (method cancelInProgressByUserId) to also set
n.failureStage = :failureStage and n.failureReason = :failureReason, add
corresponding parameters to the method signature (e.g., `@Param`("failureStage")
String failureStage, `@Param`("failureReason") String failureReason), and ensure
callers pass appropriate failureStage/failureReason values so failed documents
retain their failure metadata.

In
`@src/main/java/com/gachi/be/domain/newsletter/service/impl/NewsletterServiceImpl.java`:
- Around line 232-243: The transaction currently deletes DB-derived data
(checklistRepository.deleteByNewsletterId and
calendarEventRepository.deleteByNewsletterIdAndUserId) but leaves the Redis
preview key intact; inject a CalendarPreviewRedisService (add private final
CalendarPreviewRedisService calendarPreviewRedisService) into
NewsletterServiceImpl and, inside the same transactional flow before calling
TransactionSynchronizationManager.registerSynchronization(...), call the
calendarPreviewRedisService method that clears the preview for the
newsletter/user (e.g., deleteByNewsletterIdAndUserId(newsletterId, userId) or
deletePreview(newsletterId, userId)) so Redis preview is removed within the
retry transaction and cannot be consumed by
CalendarRegisterServiceImpl.getPreview()/register() from a stale state.

In
`@src/main/java/com/gachi/be/domain/user/dto/request/ChangeNotificationRequest.java`:
- Around line 6-7: ChangeNotificationRequest's notificationEnabled is declared
as primitive boolean with `@NotNull`, which doesn't enforce presence because
primitives can't be null; change the type to the boxed Boolean (or
Optional<Boolean>) so `@NotNull` can validate missing JSON values, update the
record signature in ChangeNotificationRequest accordingly, and adjust any
callers/consumers (constructors, mappers, tests) that instantiate or read
notificationEnabled to handle the boxed type (and its potential null) or unwrap
Optional as needed.

In `@src/main/java/com/gachi/be/domain/user/entity/User.java`:
- Around line 116-121: In User.updateLanguage, besides the existing null/blank
check, enforce the allowed language-code invariant by normalizing languageCode
(trim + toUpperCase) then validating it against the allowed set { "KO", "US",
"ZH", "VI" } (or an equivalent static Set/enum defined on the User class); if
the normalized code is not in that set, throw an IllegalArgumentException with a
clear message. Ensure the method stores only the validated normalized value in
this.languageCode and consider extracting the allowed codes to a private static
constant (e.g., ALLOWED_LANGUAGE_CODES) for reuse.

In `@src/main/java/com/gachi/be/global/config/external/AiServerProperties.java`:
- Around line 12-14: AiServerProperties currently allows connectTimeoutSeconds
and readTimeoutSeconds to be set to 0 or negative which will cause runtime
failures; add input validation in the AiServerProperties binding so these fields
are validated at startup (e.g., annotate the class with `@Validated` and add
`@Min`(1) to the connectTimeoutSeconds and readTimeoutSeconds fields, or perform
checks in their setters/constructor and throw IllegalArgumentException when
value <= 0) to fail fast and provide a clear error message referencing the
offending property names.

In `@src/main/resources/db/migration/V12__user_add_language_code.sql`:
- Around line 1-2: Add a DB-level CHECK constraint for the newly added
users.language_code column in V12__user_add_language_code.sql so only the
application-supported language codes are allowed; modify the ALTER TABLE users
... ADD COLUMN IF NOT EXISTS language_code VARCHAR(10) NOT NULL DEFAULT 'KO' to
include a CHECK (language_code IN (...)) (or add a separate ALTER TABLE ... ADD
CONSTRAINT ... CHECK) using the exact list of allowed codes your app expects so
invalid codes cannot be inserted even if app-side validation is bypassed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: af95cd3d-b03f-4e9f-ba84-35346816bd62

📥 Commits

Reviewing files that changed from the base of the PR and between 0d2281d and ecd502f.

📒 Files selected for processing (46)
  • build.gradle
  • deploy/.env.example
  • deploy/docker-compose.yml
  • deploy/nginx/nginx.conf
  • deploy/nginx/nginx.https.template.conf
  • docs/error-code.md
  • docs/newsletter-ai-analyze-integration.md
  • docs/newsletter-ai-failure-policy.md
  • src/main/java/com/gachi/be/domain/auth/dto/request/SignupRequest.java
  • src/main/java/com/gachi/be/domain/auth/service/AuthenticatedUserResolver.java
  • src/main/java/com/gachi/be/domain/auth/service/impl/AuthServiceImpl.java
  • src/main/java/com/gachi/be/domain/calendar/service/CalendarPreviewRedisService.java
  • src/main/java/com/gachi/be/domain/newsletter/api/controller/NewsletterController.java
  • src/main/java/com/gachi/be/domain/newsletter/dto/response/NewsletterStatusResponse.java
  • src/main/java/com/gachi/be/domain/newsletter/entity/Newsletter.java
  • src/main/java/com/gachi/be/domain/newsletter/pipeline/AiNewsletterClient.java
  • src/main/java/com/gachi/be/domain/newsletter/pipeline/NewsletterAiAnalyzer.java
  • src/main/java/com/gachi/be/domain/newsletter/pipeline/NewsletterPipelineService.java
  • src/main/java/com/gachi/be/domain/newsletter/repository/NewsletterRepository.java
  • src/main/java/com/gachi/be/domain/newsletter/service/NewsletterService.java
  • src/main/java/com/gachi/be/domain/newsletter/service/impl/NewsletterServiceImpl.java
  • src/main/java/com/gachi/be/domain/user/api/controller/UserController.java
  • src/main/java/com/gachi/be/domain/user/dto/request/ChangeLanguageRequest.java
  • src/main/java/com/gachi/be/domain/user/dto/request/ChangeNotificationRequest.java
  • src/main/java/com/gachi/be/domain/user/dto/response/UserMeResponse.java
  • src/main/java/com/gachi/be/domain/user/entity/User.java
  • src/main/java/com/gachi/be/domain/user/entity/enums/UserStatus.java
  • src/main/java/com/gachi/be/domain/user/repository/UserRepository.java
  • src/main/java/com/gachi/be/global/code/ErrorCode.java
  • src/main/java/com/gachi/be/global/code/SuccessCode.java
  • src/main/java/com/gachi/be/global/config/external/AiServerProperties.java
  • src/main/java/com/gachi/be/global/config/external/ExternalApiConfig.java
  • src/main/java/com/gachi/be/global/config/external/OpenAiProperties.java
  • src/main/java/com/gachi/be/global/exception/ExternalApiException.java
  • src/main/java/com/gachi/be/global/security/JwtAuthenticationFilter.java
  • src/main/resources/application.yml
  • src/main/resources/db/migration/V11__newsletter_failure_reason.sql
  • src/main/resources/db/migration/V12__user_add_language_code.sql
  • src/main/resources/db/migration/V13__user_add_notification_enabled.sql
  • src/test/java/com/gachi/be/domain/auth/api/controller/AuthControllerIntegrationTest.java
  • src/test/java/com/gachi/be/domain/auth/api/controller/AuthRateLimitIntegrationTest.java
  • src/test/java/com/gachi/be/domain/child/api/controller/ChildControllerIntegrationTest.java
  • src/test/java/com/gachi/be/domain/newsletter/dto/response/NewsletterStatusResponseTest.java
  • src/test/java/com/gachi/be/domain/newsletter/entity/NewsletterTest.java
  • src/test/java/com/gachi/be/domain/newsletter/pipeline/AiNewsletterClientTest.java
  • src/test/java/com/gachi/be/domain/newsletter/pipeline/NewsletterAiAnalyzerTest.java
💤 Files with no reviewable changes (2)
  • build.gradle
  • src/main/java/com/gachi/be/global/config/external/OpenAiProperties.java

Comment thread deploy/docker-compose.yml
Comment on lines +109 to +110
ai:
condition: service_healthy
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

백엔드 기동이 AI 헬스체크에 하드 의존합니다.

Line 109-110에서 backendaiservice_healthy를 필수로 기다리면, AI 장애 시 백엔드 재기동이 막혀 비-AI API까지 함께 중단될 수 있습니다. AI 실패는 앱 레이어에서 처리하고 컨테이너 기동 의존성은 분리하는 게 안전합니다.

수정 제안
     depends_on:
       db:
         condition: service_healthy
       redis:
         condition: service_healthy
-      ai:
-        condition: service_healthy
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ai:
condition: service_healthy
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/docker-compose.yml` around lines 109 - 110, 현재 docker-compose에
backend가 ai의 service_healthy에 하드 의존하고 있어 AI 헬스체크 실패 시 backend 재기동이 차단됩니다;
docker-compose의 backend 서비스 설정에서 ai에 대한 "condition: service_healthy"를 제거하거나 대체하여
컨테이너 시작 의존성을 분리하세요 (예: 제거하거나 "condition: service_started"로 바꿔 AI 헬스 상태에 따른 재시작
제어를 애플리케이션 레이어로 위임). 변경 대상 식별자: 서비스 이름 "backend", "ai"와 조건 키 "condition:
service_healthy".

LocalDate.now(DEFAULT_ZONE),
DEFAULT_ZONE.getId(),
toDateCandidateRequests(dateCandidates)));
log.info("[AiNewsletterClient] 요청 body: {}", requestBody);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

요청/응답 본문 원문 로그는 민감정보 유출 위험이 큽니다.

현재 로그에 OCR 원문/번역문 및 실패 응답 본문이 그대로 남습니다. 본문 전체 대신 길이·상태코드·추적 식별자만 남기도록 축약/마스킹해 주세요.

🔧 제안 수정안
-      log.info("[AiNewsletterClient] 요청 body: {}", requestBody);
+      log.debug(
+          "[AiNewsletterClient] 분석 요청 전송. language={}, dateCandidatesCount={}, originalTextLength={}, translatedTextLength={}",
+          language != null ? language : "KO",
+          dateCandidates != null ? dateCandidates.size() : 0,
+          originalText != null ? originalText.length() : 0,
+          translatedText != null ? translatedText.length() : 0);
@@
-        log.error(
-            "[AiNewsletterClient] AI 서버 분석 실패. status={}, body={}",
-            response.statusCode(),
-            response.body());
+        log.error(
+            "[AiNewsletterClient] AI 서버 분석 실패. status={}, responseBodyLength={}",
+            response.statusCode(),
+            response.body() != null ? response.body().length() : 0);

Also applies to: 75-77

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/gachi/be/domain/newsletter/pipeline/AiNewsletterClient.java`
at line 59, The current AiNewsletterClient logging writes full request/response
bodies (e.g., requestBody and the response variables logged around lines 75-77),
which may leak sensitive OCR text; change the logging in AiNewsletterClient so
log.info/log.error no longer prints full payloads but instead logs a masked or
truncated summary: include payload length, HTTP status code, and a trace
identifier (generate or pass a traceId), and if needed mask content (e.g., show
first N chars + "...[masked]") rather than the full requestBody/responseBody;
update all places where requestBody and responseBody are logged to use this
summarized format and ensure any error logs include the traceId and status but
not raw body content.

Comment on lines +63 to +65
String rawTitle = normalizeTitle(analysisResponse.title(), originalText);
String title = translateIfNeeded(rawTitle, language, "title", newsletterId);
String summary = normalizeSummary(analysisResponse.summary(), translatedText, originalText);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

요약문 번역 경로가 빠져 있습니다.

여기서는 titletranslateIfNeeded(...)를 타고, summary는 그대로 저장됩니다. NewsletterServiceImpl.getSummary()는 저장된 요약을 그대로 내려주므로 비KO 사용자는 제목/체크리스트는 번역되고 요약만 한국어로 남게 됩니다. AI 응답의 summary가 있을 때만 별도로 번역하고, 폴백 경로(translatedText)는 그대로 쓰는 쪽이 안전합니다.

가능한 수정 예시
     String rawTitle = normalizeTitle(analysisResponse.title(), originalText);
     String title = translateIfNeeded(rawTitle, language, "title", newsletterId);
-    String summary = normalizeSummary(analysisResponse.summary(), translatedText, originalText);
+    String summary =
+        analysisResponse.summary() != null && !analysisResponse.summary().isBlank()
+            ? translateIfNeeded(
+                trimToMax(compact(analysisResponse.summary()), SUMMARY_MAX_LENGTH),
+                language,
+                "summary",
+                newsletterId)
+            : buildBaselineSummary(firstNonBlank(translatedText, originalText));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/gachi/be/domain/newsletter/pipeline/NewsletterAiAnalyzer.java`
around lines 63 - 65, The summary is not being translated before storage: update
the pipeline so that when analysisResponse.summary() is present you first
normalize it (using normalizeSummary or similar) and then call translateIfNeeded
on that raw summary (pass language, "summary", and newsletterId) falling back to
translatedText when AI summary is absent; ensure the code paths around
normalizeSummary(analysisResponse.summary(), translatedText, originalText) and
translateIfNeeded(...) mirror what you did for title so
NewsletterServiceImpl.getSummary() returns the translated summary for non-KO
users.

Comment on lines +194 to +200
previewEvents.add(
new CalendarPreviewEvent(
"ai_evt_" + (previewEvents.size() + 1),
trimToMax(item.title().trim(), CHECKLIST_TEXT_MAX_LENGTH),
extractedDate,
true,
checklistIdList(savedItem.checklist())));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

캘린더 preview 제목이 번역 결과를 버리고 있습니다.

saveExtractedItems()에서 Checklist.content는 사용자 언어로 번역해 저장했는데, 여기서는 다시 item.title() 원문을 Redis preview에 넣고 있습니다. CalendarRegisterServiceImpl.getPreview()/register()는 이 preview를 그대로 UI와 일정 등록에 사용하므로 비KO 사용자는 preview만 한국어로 보게 됩니다. preview 제목은 이미 저장한 Checklist.content를 재사용하는 편이 일관됩니다.

가능한 수정 예시
       previewEvents.add(
           new CalendarPreviewEvent(
               "ai_evt_" + (previewEvents.size() + 1),
-              trimToMax(item.title().trim(), CHECKLIST_TEXT_MAX_LENGTH),
+              savedItem.checklist().getContent(),
               extractedDate,
               true,
               checklistIdList(savedItem.checklist())));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/gachi/be/domain/newsletter/pipeline/NewsletterAiAnalyzer.java`
around lines 194 - 200, The preview title is using the original item.title()
instead of the stored, translated Checklist content; update the
CalendarPreviewEvent creation in saveExtractedItems() to use the saved
checklist's content (e.g., savedItem.checklist().content() or the
Checklist#getContent accessor) instead of item.title(), still applying
trimToMax(..., CHECKLIST_TEXT_MAX_LENGTH) and .trim(), and keep the rest of the
constructor (extractedDate, true, checklistIdList(savedItem.checklist()))
unchanged so CalendarRegisterServiceImpl.getPreview()/register() shows the
translated KO content consistently.

Comment on lines +104 to +114
UPDATE Newsletter n
SET n.status = :failedStatus,
n.language = :newLanguage
WHERE n.userId = :userId
AND n.status IN :targetStatuses
""")
int cancelInProgressByUserId(
@Param("userId") Long userId,
@Param("targetStatuses") List<NewsletterStatus> targetStatuses,
@Param("failedStatus") NewsletterStatus failedStatus,
@Param("newLanguage") String newLanguage);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

언어 변경 취소 경로에서도 실패 메타데이터를 채워야 합니다

cancelInProgressByUserId는 상태를 FAILED로 바꾸지만 failureStage/failureReason을 비워둡니다. 이 경로로 실패한 문서는 상태 조회 시 실패 원인 추적 정보가 누락됩니다.

수정 예시
   `@Query`(
       """
         UPDATE Newsletter n
         SET n.status = :failedStatus,
-                n.language = :newLanguage
+                n.language = :newLanguage,
+                n.failureStage = :failureStage,
+                n.failureReason = :failureReason
         WHERE n.userId = :userId
           AND n.status IN :targetStatuses
         """)
   int cancelInProgressByUserId(
       `@Param`("userId") Long userId,
       `@Param`("targetStatuses") List<NewsletterStatus> targetStatuses,
       `@Param`("failedStatus") NewsletterStatus failedStatus,
-      `@Param`("newLanguage") String newLanguage);
+      `@Param`("newLanguage") String newLanguage,
+      `@Param`("failureStage") String failureStage,
+      `@Param`("failureReason") String failureReason);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/gachi/be/domain/newsletter/repository/NewsletterRepository.java`
around lines 104 - 114, The cancelInProgressByUserId update currently only sets
status and language, leaving failure metadata empty; modify the JPQL update in
NewsletterRepository (method cancelInProgressByUserId) to also set
n.failureStage = :failureStage and n.failureReason = :failureReason, add
corresponding parameters to the method signature (e.g., `@Param`("failureStage")
String failureStage, `@Param`("failureReason") String failureReason), and ensure
callers pass appropriate failureStage/failureReason values so failed documents
retain their failure metadata.

Comment on lines +232 to +243
checklistRepository.deleteByNewsletterId(newsletterId);
calendarEventRepository.deleteByNewsletterIdAndUserId(newsletterId, userId);
Newsletter saved = findNewsletterById(newsletterId);

TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
log.info("[Newsletter] 분석 재시도 파이프라인 트리거. newsletterId={}", newsletterId);
newsletterPipelineService.runPipeline(newsletterId);
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

재시도 전에 Redis preview도 같이 비워야 합니다.

여기서는 DB 파생 데이터만 지우고 이전 preview 키는 그대로 남깁니다. 그런데 preview는 CalendarRegisterServiceImpl.getPreview()/register()에서 상태 검증 없이 그대로 소비되므로, 새 재시도가 saveCalendarPreview()까지 못 가고 다시 실패하면 사용자가 이전 분석의 일정을 그대로 열거나 등록할 수 있습니다. 재시도 트랜잭션 안에서 preview까지 같이 지워 두는 게 안전합니다.

가능한 수정 예시
     checklistRepository.deleteByNewsletterId(newsletterId);
     calendarEventRepository.deleteByNewsletterIdAndUserId(newsletterId, userId);
+    calendarPreviewRedisService.deletePreview(newsletterId);
     Newsletter saved = findNewsletterById(newsletterId);

추가로 이 필드 주입이 필요합니다.

private final CalendarPreviewRedisService calendarPreviewRedisService;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/gachi/be/domain/newsletter/service/impl/NewsletterServiceImpl.java`
around lines 232 - 243, The transaction currently deletes DB-derived data
(checklistRepository.deleteByNewsletterId and
calendarEventRepository.deleteByNewsletterIdAndUserId) but leaves the Redis
preview key intact; inject a CalendarPreviewRedisService (add private final
CalendarPreviewRedisService calendarPreviewRedisService) into
NewsletterServiceImpl and, inside the same transactional flow before calling
TransactionSynchronizationManager.registerSynchronization(...), call the
calendarPreviewRedisService method that clears the preview for the
newsletter/user (e.g., deleteByNewsletterIdAndUserId(newsletterId, userId) or
deletePreview(newsletterId, userId)) so Redis preview is removed within the
retry transaction and cannot be consumed by
CalendarRegisterServiceImpl.getPreview()/register() from a stale state.

Comment on lines +6 to +7
public record ChangeNotificationRequest(
@NotNull(message = "notificationEnabled는 필수입니다.") boolean notificationEnabled) {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Jakarta Bean Validation에서 @NotNull primitive boolean 필드에 실제로 검증되는지, 그리고 JSON 필드 누락 시 boolean vs Boolean 바인딩 차이가 무엇인지 공식 문서 기준으로 확인해줘.

💡 Result:

결론부터 말하면, Jakarta Bean Validation의 @NotNull이 primitive boolean(boolean 타입) 필드에 대해 “실제로 검증되는가?”에 대한 답은 “null 여부를 기준으로 한 @NotNull 검증은 의미가 없다(=검증 대상이 null이 될 수 없다)”입니다. 또한 JSON에서 필드가 누락되면, boolean(primitive)과 Boolean(래퍼) 바인딩 차이 때문에 “누락을 검증으로 잡을 수 있느냐”가 달라집니다. 1) @NotNull이 primitive boolean에 대해 검증되는가? - Jakarta Bean Validation의 @NotNull은 “annotated element must not be null(해당 엘리먼트는 null이 아니어야 함)”을 요구합니다.[1][2] - 그런데 Java의 primitive boolean 타입은 null 값을 가질 수 없습니다(primitive 타입은 null을 수용하지 않음). JSON-B 명시에서도 primitive 타입은 null을 받아들일 수 없어서(“Primitive types cannot accept null values…”) 필수/기본값 로직이 따로 존재함을 밝힙니다.[3][4] - 따라서 primitive boolean 필드에 @NotNull을 붙여도, 바인딩/프로퍼티 값이 애초에 null이 될 수 없으므로 @NotNull이 “누락/NULL” 케이스를 잡아주는 검증으로 동작하지 않습니다(실질적으로는 항상 null이 아닌 값이 들어오는 형태). 2) JSON 필드 누락 시 boolean vs Boolean 바인딩 차이 A. JSON-B 관점(공식 사양 기준) - JSON 문서에서 property가 “absent(누락)”인 경우: deserialization은 그 필드 값을 세팅하지 않으며(setter 호출도 하지 않음), thus original value of the field(필드의 기존 값)을 보존해야 한다고 명시돼 있습니다.[3][4] - 또한 primitive 타입은 null을 받아들일 수 없으므로, absent/nullable 처리와 별개로 primitive boolean에는 “필요한 값”이 들어가야 합니다. JSON-B에서는 primitive 타입 대신 래퍼/Optional 등이 null/부재를 표현할 수 있는 쪽으로 설계돼 있습니다.[3][4] - 요약하면: - boolean(primitive) 타입: JSON에서 필드가 누락되면 “null로 들어오는 게 아니라” 자바의 primitive 기본값(false) 또는 초기값 유지 형태로 귀결됩니다(결과적으로 @NotNull이 잡을 ‘null’ 상태가 생기지 않음).[3][4] - Boolean(래퍼) 타입: JSON에서 필드가 누락되면 필드에 null이 들어가거나(또는 기존 값이 유지) ‘null 상태’를 만들 수 있어, 그 다음 Bean Validation의 @NotNull이 누락을 잡는 형태가 됩니다(“must not be null”이 실제로 위반 가능).[1][2][3][4] B. @AssertTrue/@assertfalse와의 관련(공식 문맥: primitive/Boolean 모두 지원) - Hibernate Validator 문서에는 @AssertTrue/@assertfalse가 Boolean과 boolean 모두 지원된다고 나와 있으며, @NotNull은 “annotated value is not null” 체크로 정의됩니다.[5] - 다만 “누락”을 잡으려면 결국 null 여부를 판별할 수 있어야 하는데, primitive boolean은 null 표현이 불가능하므로 @NotNull로 ‘누락’을 검출하는 목적에는 맞지 않습니다. 3) 실무적으로 의미있는 방법(공식 문서가 직접 제시하는 방향에 근거한 결론) - “클라이언트가 반드시 JSON에서 필드를 제공해야 한다(누락이면 400)” 같은 요구사항을 Bean Validation @NotNull로 처리하려면, 해당 필드를 반드시 nullable을 표현할 수 있게 해야 합니다. - 즉 boolean primitive가 아니라 Boolean(또는 Optional 계열)로 모델링한 뒤 @NotNull을 거는 것이 @NotNull의 의미(‘null이 아니어야 함’)와 정확히 맞습니다.[1][2][3][4] - primitive boolean에 @NotNull을 붙이는 것만으로는 “누락”을 null 위반으로 잡지 못합니다. 원문 확인용 핵심 근거(요약) - @NotNull: “must not be null(해당 엘리먼트는 null이 아니어야)”[1][2] - JSON-B: JSON property absent 시 setter 미호출 + original value 보존[3][4] - JSON-B: primitive types는 null을 받아들일 수 없음 + primitive의 기본값 로직 표기[3][4]

Citations:


@NotNullboolean(primitive) 필드에 적용해 “필수”가 보장되지 않습니다.
@NotNull은 “null이 아니어야 함”을 검증하는데, boolean은 null을 가질 수 없습니다. 따라서 JSON에서 해당 프로퍼티가 누락되면(예: JSON-B 명세상 absent 시 setter 미호출/기본값 유지) @NotNull로 누락을 위반으로 잡기 어렵고 기본값(false)으로 처리될 수 있어 “필수 입력 검증” 의도와 어긋납니다.

  • 해결: booleanBoolean(또는 Optional<Boolean>)로 바꾼 뒤 @NotNull을 적용
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/gachi/be/domain/user/dto/request/ChangeNotificationRequest.java`
around lines 6 - 7, ChangeNotificationRequest's notificationEnabled is declared
as primitive boolean with `@NotNull`, which doesn't enforce presence because
primitives can't be null; change the type to the boxed Boolean (or
Optional<Boolean>) so `@NotNull` can validate missing JSON values, update the
record signature in ChangeNotificationRequest accordingly, and adjust any
callers/consumers (constructors, mappers, tests) that instantiate or read
notificationEnabled to handle the boxed type (and its potential null) or unwrap
Optional as needed.

Comment on lines +116 to +121
public void updateLanguage(String languageCode) {
if (languageCode == null || languageCode.isBlank()) {
throw new IllegalArgumentException("languageCode는 비어 있을 수 없습니다.");
}
this.languageCode = languageCode.trim().toUpperCase();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

updateLanguage에서 허용 언어 코드 불변식을 함께 강제해 주세요.

Line 120 기준으로 현재는 공백만 막고 임의 문자열이 저장될 수 있습니다. DTO 검증을 우회하는 경로를 대비해 엔티티에서도 KO|US|ZH|VI를 검증하는 게 안전합니다.

제안 수정안
+import java.util.Set;
+
 public class User {
+  private static final Set<String> SUPPORTED_LANGUAGE_CODES = Set.of("KO", "US", "ZH", "VI");
+
   public void updateLanguage(String languageCode) {
     if (languageCode == null || languageCode.isBlank()) {
       throw new IllegalArgumentException("languageCode는 비어 있을 수 없습니다.");
     }
-    this.languageCode = languageCode.trim().toUpperCase();
+    String normalized = languageCode.trim().toUpperCase();
+    if (!SUPPORTED_LANGUAGE_CODES.contains(normalized)) {
+      throw new IllegalArgumentException("지원하지 않는 languageCode입니다.");
+    }
+    this.languageCode = normalized;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void updateLanguage(String languageCode) {
if (languageCode == null || languageCode.isBlank()) {
throw new IllegalArgumentException("languageCode는 비어 있을 수 없습니다.");
}
this.languageCode = languageCode.trim().toUpperCase();
}
public void updateLanguage(String languageCode) {
if (languageCode == null || languageCode.isBlank()) {
throw new IllegalArgumentException("languageCode는 비어 있을 수 없습니다.");
}
String normalized = languageCode.trim().toUpperCase();
if (!SUPPORTED_LANGUAGE_CODES.contains(normalized)) {
throw new IllegalArgumentException("지원하지 않는 languageCode입니다.");
}
this.languageCode = normalized;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/gachi/be/domain/user/entity/User.java` around lines 116 -
121, In User.updateLanguage, besides the existing null/blank check, enforce the
allowed language-code invariant by normalizing languageCode (trim + toUpperCase)
then validating it against the allowed set { "KO", "US", "ZH", "VI" } (or an
equivalent static Set/enum defined on the User class); if the normalized code is
not in that set, throw an IllegalArgumentException with a clear message. Ensure
the method stores only the validated normalized value in this.languageCode and
consider extracting the allowed codes to a private static constant (e.g.,
ALLOWED_LANGUAGE_CODES) for reuse.

Comment on lines +12 to +14
private String baseUrl = "http://localhost:8000";
private int connectTimeoutSeconds = 10;
private int readTimeoutSeconds = 120;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

타임아웃 설정값 하한 검증이 없어 0/음수 설정 시 런타임 실패가 발생할 수 있습니다.

connectTimeoutSeconds/readTimeoutSeconds가 0 이하로 주입되면 HttpClient/HttpRequest 타임아웃 설정에서 예외가 발생합니다. 프로퍼티 바인딩 단계에서 검증해 조기 실패시키는 게 안전합니다.

🔧 제안 수정안
 import lombok.Getter;
-import lombok.Setter;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 
 `@Getter`
-@Setter
 `@ConfigurationProperties`(prefix = "app.ai-server")
 public class AiServerProperties {
 
   private String baseUrl = "http://localhost:8000";
   private int connectTimeoutSeconds = 10;
   private int readTimeoutSeconds = 120;
+
+  public void setConnectTimeoutSeconds(int connectTimeoutSeconds) {
+    if (connectTimeoutSeconds < 1) {
+      throw new IllegalArgumentException(
+          "app.ai-server.connect-timeout-seconds must be >= 1");
+    }
+    this.connectTimeoutSeconds = connectTimeoutSeconds;
+  }
+
+  public void setReadTimeoutSeconds(int readTimeoutSeconds) {
+    if (readTimeoutSeconds < 1) {
+      throw new IllegalArgumentException(
+          "app.ai-server.read-timeout-seconds must be >= 1");
+    }
+    this.readTimeoutSeconds = readTimeoutSeconds;
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private String baseUrl = "http://localhost:8000";
private int connectTimeoutSeconds = 10;
private int readTimeoutSeconds = 120;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
`@Getter`
`@ConfigurationProperties`(prefix = "app.ai-server")
public class AiServerProperties {
private String baseUrl = "http://localhost:8000";
private int connectTimeoutSeconds = 10;
private int readTimeoutSeconds = 120;
public void setConnectTimeoutSeconds(int connectTimeoutSeconds) {
if (connectTimeoutSeconds < 1) {
throw new IllegalArgumentException(
"app.ai-server.connect-timeout-seconds must be >= 1");
}
this.connectTimeoutSeconds = connectTimeoutSeconds;
}
public void setReadTimeoutSeconds(int readTimeoutSeconds) {
if (readTimeoutSeconds < 1) {
throw new IllegalArgumentException(
"app.ai-server.read-timeout-seconds must be >= 1");
}
this.readTimeoutSeconds = readTimeoutSeconds;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/gachi/be/global/config/external/AiServerProperties.java`
around lines 12 - 14, AiServerProperties currently allows connectTimeoutSeconds
and readTimeoutSeconds to be set to 0 or negative which will cause runtime
failures; add input validation in the AiServerProperties binding so these fields
are validated at startup (e.g., annotate the class with `@Validated` and add
`@Min`(1) to the connectTimeoutSeconds and readTimeoutSeconds fields, or perform
checks in their setters/constructor and throw IllegalArgumentException when
value <= 0) to fail fast and provide a clear error message referencing the
offending property names.

Comment on lines +1 to +2
ALTER TABLE users
ADD COLUMN IF NOT EXISTS language_code VARCHAR(10) NOT NULL DEFAULT 'KO';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

language_code 허용값을 DB 제약으로도 고정해 주세요.

현재는 컬럼만 추가되어 DB에 임의 코드가 들어갈 수 있습니다. 애플리케이션 검증 우회 상황을 대비해 CHECK 제약을 함께 두는 편이 안전합니다.

제안 수정안
 ALTER TABLE users
     ADD COLUMN IF NOT EXISTS language_code VARCHAR(10) NOT NULL DEFAULT 'KO';
+
+ALTER TABLE users
+    ADD CONSTRAINT chk_users_language_code
+    CHECK (language_code IN ('KO', 'US', 'ZH', 'VI'));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ALTER TABLE users
ADD COLUMN IF NOT EXISTS language_code VARCHAR(10) NOT NULL DEFAULT 'KO';
ALTER TABLE users
ADD COLUMN IF NOT EXISTS language_code VARCHAR(10) NOT NULL DEFAULT 'KO';
ALTER TABLE users
ADD CONSTRAINT chk_users_language_code
CHECK (language_code IN ('KO', 'US', 'ZH', 'VI'));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/resources/db/migration/V12__user_add_language_code.sql` around lines
1 - 2, Add a DB-level CHECK constraint for the newly added users.language_code
column in V12__user_add_language_code.sql so only the application-supported
language codes are allowed; modify the ALTER TABLE users ... ADD COLUMN IF NOT
EXISTS language_code VARCHAR(10) NOT NULL DEFAULT 'KO' to include a CHECK
(language_code IN (...)) (or add a separate ALTER TABLE ... ADD CONSTRAINT ...
CHECK) using the exact list of allowed codes your app expects so invalid codes
cannot be inserted even if app-side validation is bypassed.

@Hminkyung Hminkyung requested a review from deli-minju May 25, 2026 16:56
Copy link
Copy Markdown
Contributor

@deli-minju deli-minju left a comment

Choose a reason for hiding this comment

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

확인했습니다. 감사합니다!

@Hminkyung Hminkyung merged commit b926d78 into main May 25, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

chore 빌드, 설정, 의존성, 운영성 작업

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants