Skip to content

Comments

[SRLT-133] 백오피스 전문가 수정기능을 구현한다#80

Merged
2ghrms merged 12 commits intodevelopfrom
SRLT-133-백오피스-전문가-수정
Feb 16, 2026

Hidden character warning

The head ref may contain hidden characters: "SRLT-133-\ubc31\uc624\ud53c\uc2a4-\uc804\ubb38\uac00-\uc218\uc815"
Merged

[SRLT-133] 백오피스 전문가 수정기능을 구현한다#80
2ghrms merged 12 commits intodevelopfrom
SRLT-133-백오피스-전문가-수정

Conversation

@SeongHo5356
Copy link
Member

@SeongHo5356 SeongHo5356 commented Jan 25, 2026

🚀 Why - 해결하려는 문제가 무엇인가요?

  • 백오피스(관리자)에서 전문가 정보를 수정/관리(활성상태, 프로필 이미지, 기본 정보, 경력/태그/카테고리 등) 할 수 있는 API가 필요했습니다.
  • 백오피스에서 전문가 조회/상세 확인 후 바로 수정/삭제까지 이어지는 관리 플로우를 지원하려는 배경이 있습니다.

✅ What - 무엇이 변경됐나요?

  • 백오피스 전문가 API 추가 (/v1/backoffice/experts)

    • GET /v1/backoffice/experts : 전체 조회
    • POST /v1/backoffice/experts : 생성
    • GET /v1/backoffice/experts/{expertId} : 상세 조회
    • PATCH /v1/backoffice/experts/{expertId} : 전문가 정보 수정
    • PATCH /v1/backoffice/experts/{expertId}/active-status : 활성 상태 변경
    • PATCH /v1/backoffice/experts/{expertId}/profile-image : 프로필 이미지 변경
    • DELETE /v1/backoffice/experts/{expertId} : 삭제
  • Request DTO 추가/정의

    • 활성상태 변경 요청: BackofficeExpertActiveStatusUpdateRequest(activeStatus)
    • 프로필 이미지 변경 요청: BackofficeExpertProfileImageUpdateRequest(profileImageUrl)
    • 생성 요청: BackofficeExpertCreateRequest(name, email, oneLineIntroduction, tags, categories)
    • 수정 요청: BackofficeExpertUpdateRequest(name, email, oneLineIntroduction, detailedIntroduction, workedPeriod, mentoringPriceWon, tags, categories, careers)
    • 경력 수정 요청: BackofficeExpertCareerUpdateRequest(id, orderIndex, careerTitle, careerExplanation, careerStartedAt, careerEndedAt) (+기간 검증)
  • Response DTO 추가/정의

    • 상세 응답: BackofficeExpertDetailResponse (applicationCount, activeStatus, careers/tags/categories 포함)
    • 리스트 응답: BackofficeExpertListResponse (경력 요약 최대 3개)
  • (PR 포함 커밋 기준) 부가 변경: 활성상태 필드 추가, Swagger 최신화, 메일 관련 리팩터링/오류 처리, 테스트 수정 등

🛠️ How - 어떻게 해결했나요?

  • Controller 계층

    • BackofficeExpertController에서 BackofficeExpertQueryUseCase, BackofficeExpertCommandUseCase를 사용해 조회/생성/수정/삭제를 제공했습니다.
    • 백오피스 인증 전제: @SecurityRequirement(name = "backofficeSession") 적용
  • 활성상태 변경

    • 요청 바디의 activeStatus(ExpertActiveStatus)를 받아 BackofficeExpertActiveStatusUpdateInput.of(expertId, activeStatus) 형태로 전달합니다.
  • 프로필 이미지 변경

    • profileImageUrl@NotBlank 검증 후 BackofficeExpertProfileImageUpdateInput.of(expertId, profileImageUrl)로 전달합니다.
  • 전문가 정보 수정(경력 포함)

    • BackofficeExpertUpdateRequest에서 careers 리스트를 input 모델(BackofficeExpertCareerUpdateInput)로 매핑하여 전달합니다.
    • 경력 기간 검증: careerStartedAtcareerEndedAt보다 늦으면 안 되도록 @AssertTrue로 검증합니다.

🖼️ Attachment

  • 화면 이미지, 결과 캡처 등 첨부

💬 기타 코멘트

  • 리뷰어에게 전하고 싶은 말, 테스트 방법, 주의할 점 등

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 백오피스 전문가 관리 API 추가 (조회, 생성, 수정, 삭제)
    • 전문가 활동 상태 및 프로필 이미지 업데이트 기능
    • 백오피스 이미지 업로드 URL 생성 기능
    • 백오피스 메일 템플릿 관리 기능
  • Improvements

    • 이메일 로그에 개인정보 마스킹 처리 적용
    • API 보안 설정 강화
    • 전문가 경력 정보 동기화 기능 추가

@github-actions
Copy link

github-actions bot commented Jan 25, 2026

Test Results

246 tests   246 ✅  9s ⏱️
 45 suites    0 💤
 45 files      0 ❌

Results for commit a9cacfe.

♻️ This comment has been updated with latest results.

@coderabbitai
Copy link

coderabbitai bot commented Jan 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'tools'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
📝 Walkthrough

워크스루

백오피스 전문가 관리 시스템(CRUD, 활동 상태/프로필 이미지 업데이트 포함), 이미지 업로드 기능 통합, 메일 시스템 개선(콘텐츠 타입 파서 및 이메일 마스킹), 전문가 도메인 확장(활동 상태 및 경력 관리), 백오피스 경로에 대한 보안 구성 업데이트를 포함한 대규모 기능 추가입니다.

변경사항

Cohort / File(s) Summary
백오피스 전문가 API
src/main/java/starlight/adapter/backoffice/expert/webapi/BackofficeExpertController.java, src/main/java/starlight/adapter/backoffice/expert/webapi/swagger/BackofficeExpertApiDoc.java
새로운 Spring REST 컨트롤러로 /v1/backoffice/experts 엔드포인트(검색, 생성, 상세, 수정, 삭제, 활동상태 업데이트, 프로필이미지 업데이트) 구현. OpenAPI 문서화 인터페이스 제공.
백오피스 전문가 요청/응답 DTO
src/main/java/starlight/adapter/backoffice/expert/webapi/dto/request/BackofficeExpert*Request.java, src/main/java/starlight/adapter/backoffice/expert/webapi/dto/response/BackofficeExpert*Response.java
8개의 요청 DTO(생성, 수정, 활동상태, 프로필이미지, 경력) 및 4개의 응답 DTO(생성, 상세, 목록, 경력)로 전문가 데이터 검증 및 변환 처리. 경력 목록은 최대 3개로 제한.
백오피스 전문가 비즈니스 로직
src/main/java/starlight/application/backoffice/expert/BackofficeExpertQueryService.java, src/main/java/starlight/application/backoffice/expert/BackofficeExpertCommandService.java, src/main/java/starlight/application/backoffice/expert/provided/*
쿼리/커맨드 서비스 구현 및 유스케이스 인터페이스 정의. 입출력 DTO 모음으로 포트 기반 오케스트레이션 제공.
백오피스 전문가 포트
src/main/java/starlight/application/backoffice/expert/required/BackofficeExpertQueryPort.java, src/main/java/starlight/application/backoffice/expert/required/BackofficeExpertCommandPort.java, src/main/java/starlight/application/backoffice/expert/required/BackofficeExpertApplicationCountLookupPort.java
3개의 포트 인터페이스로 전문가 조회/저장/삭제/지원자 수 조회 기능 정의.
전문가 도메인 확장
src/main/java/starlight/domain/expert/entity/Expert.java, src/main/java/starlight/domain/expert/entity/ExpertCareer.java, src/main/java/starlight/domain/expert/enumerate/ExpertActiveStatus.java, src/main/java/starlight/domain/expert/dto/ExpertCareerUpdate.java, src/main/java/starlight/domain/expert/exception/ExpertErrorType.java
활동상태(ACTIVE/INACTIVE) 열거형 추가, 백오피스 생성 팩토리 메서드, 활동상태/프로필이미지/기본정보 뮤테이터, 복잡한 경력 동기화 로직(검증 포함). 경력 주문/시간 범위 유효성 검사 강화.
백오피스 이미지 업로드
src/main/java/starlight/adapter/backoffice/image/webapi/BackofficeImageController.java, src/main/java/starlight/adapter/backoffice/image/webapi/swagger/BackofficeImageApiDoc.java, src/main/java/starlight/adapter/backoffice/image/webapi/dto/request/BackofficeImagePublicRequest.java, src/main/java/starlight/adapter/backoffice/image/webapi/validation/*
사전 서명된 URL 생성 및 공개 URL 확정 엔드포인트. 이미지 파일명 커스텀 검증(확장자: png/jpg/jpeg/webp)
백오피스 메일 개선
src/main/java/starlight/application/backoffice/mail/util/BackofficeMailContentTypeParser.java, src/main/java/starlight/application/backoffice/mail/util/EmailMaskingUtils.java, src/main/java/starlight/application/backoffice/mail/BackofficeMailSendService.java, src/main/java/starlight/application/backoffice/mail/BackofficeMailTemplateService.java, src/main/java/starlight/application/backoffice/mail/BackofficeMailSendLogEventHandler.java, src/main/java/starlight/adapter/backoffice/mail/webapi/swagger/BackofficeMailApiDoc.java
콘텐츠 타입 파싱 로직 통합, 이메일 주소 마스킹 유틸리티 추가, 비동기 로그 처리 및 데이터 접근 예외 처리 강화.
보안 및 설정 업데이트
src/main/java/starlight/bootstrap/SecurityConfig.java, src/main/java/starlight/bootstrap/SwaggerConfig.java
CSRF 쿠키 도메인 설정 추가, 백오피스 경로 확대(/v1/backoffice/mail/** → /v1/backoffice/**), 백오피스 세션 보안 스킴 추가(쿠키 기반 API KEY)
전문가 쿼리 리팩토링
src/main/java/starlight/application/expert/ExpertDetailQueryService.java, src/main/java/starlight/application/expert/provided/ExpertDetailQueryUseCase.java, src/main/java/starlight/adapter/expert/webapi/ExpertController.java, src/test/java/starlight/adapter/expert/webapi/ExpertControllerTest.java
searchAll() → searchAllActive() 메서드명 변경으로 활성 전문가만 반환하도록 리팩토링. 테스트 업데이트.
전문가 퍼시스턴스 업데이트
src/main/java/starlight/adapter/expert/persistence/ExpertJpa.java, src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationJpa.java
새로운 포트 구현(BackofficeExpertQueryPort, BackofficeExpertCommandPort, BackofficeExpertApplicationCountLookupPort). 경력/태그/카테고리 포함 조회 메서드 추가.
공유 인프라 재위치
src/main/java/starlight/adapter/shared/infrastructure/storage/NcpPresignedUrlProvider.java, src/test/java/starlight/adapter/aireport/infrastructure/storage/NcpPresignedUrlProviderUnitTest.java
NcpPresignedUrlProvider 패키지 변경(aireport → shared). 테스트 임포트 경로 업데이트.
설정 파일
pom.xml, 개발가이드.md
의존성 추가(384줄), 개발가이드 문서 섹션 중복 추가.

시퀀스 다이어그램

sequenceDiagram
    participant Client as HTTP Client
    participant Controller as BackofficeExpertController
    participant QueryService as BackofficeExpertQueryService
    participant CommandService as BackofficeExpertCommandService
    participant QueryPort as BackofficeExpertQueryPort
    participant CommandPort as BackofficeExpertCommandPort
    participant CountPort as BackofficeExpertApplicationCountLookupPort
    participant Expert as Expert Domain

    Note over Client,Expert: 전문가 목록 조회
    Client->>Controller: GET /v1/backoffice/experts
    Controller->>QueryService: searchAll()
    QueryService->>QueryPort: findAllWithCareersTagsCategories()
    QueryPort->>Expert: 쿼리 실행
    Expert-->>QueryPort: Expert 목록
    QueryPort-->>QueryService: Expert 목록
    QueryService->>CountPort: countByExpertIds(expertIds)
    CountPort-->>QueryService: 지원자 수 맵
    QueryService-->>Controller: BackofficeExpertDetailResult 목록
    Controller-->>Client: ApiResponse<List<BackofficeExpertListResponse>>

    Note over Client,Expert: 전문가 생성
    Client->>Controller: POST /v1/backoffice/experts
    Controller->>CommandService: createExpert(input)
    CommandService->>Expert: Expert.createBackoffice()
    Expert-->>CommandService: Expert 인스턴스
    CommandService->>CommandPort: save(expert)
    CommandPort->>Expert: JPA 저장
    Expert-->>CommandPort: 저장된 Expert
    CommandPort-->>CommandService: 저장된 Expert
    CommandService-->>Controller: BackofficeExpertCreateResult
    Controller-->>Client: ApiResponse<BackofficeExpertCreateResponse>

    Note over Client,Expert: 전문가 상세 조회 및 수정
    Client->>Controller: GET /v1/backoffice/experts/{id}
    Controller->>QueryService: findById(id)
    QueryService->>QueryPort: findByIdWithCareersTagsCategories(id)
    QueryPort->>Expert: 상세 쿼리
    Expert-->>QueryPort: Expert
    QueryPort-->>QueryService: Expert
    QueryService->>CountPort: countByExpertIds([id])
    CountPort-->>QueryService: 지원자 수
    QueryService-->>Controller: BackofficeExpertDetailResult
    Controller-->>Client: ApiResponse<BackofficeExpertDetailResponse>
Loading

예상 코드 리뷰 난이도

🎯 4 (Complex) | ⏱️ ~60 minutes

분석 근거:

  • 규모: 30개 이상의 파일 수정, 8개의 새로운 API 엔드포인트
  • 복잡도: 경력 동기화 검증 로직, 다중 조건 유효성 검사(순서 인덱스 중복, 시간 범위)
  • 다양성: 컨트롤러, DTO, 서비스, 도메인 엔티티, 포트, 보안 설정, 유틸리티 등 다양한 레이어
  • 상호작용: 백오피스 쿼리/커맨드 서비스 간 복잡한 포트 의존성, 지원자 수 조회 통합
  • 도메인 로직: Expert 엔티티의 경력 동기화(syncCareers)는 상태 변경 감지, 유효성 검사, CRUD 통합으로 구성
  • 보안: CSRF, CORS, 세션 기반 인증 설정 변경으로 신중한 검토 필요

관련 PR

제안 레이블

✨ FEAT, 🧵 REFACTOR, :memo: DOCS


🔍 리뷰어 주목 사항

1. 경력 동기화 검증 로직 (Expert.java)

  • syncCareers() 메서드의 유효성 검사 규칙 정확성 확인 필요
  • 기존 경력과 새로운 경력의 매핑 로직에서 ID 누락 시 삭제되는 동작 검증

2. 활동 상태 필터링 (ExpertDetailQueryService.java)

  • searchAllActive()로의 변경이 기존 워크플로우에서 활성/비활성 전문가를 올바르게 구분하는지 확인
  • 통합 테스트 커버리지 필요

3. 메일 콘텐츠 타입 처리 (BackofficeMailContentTypeParser.java)

  • 예외 변환 로직에서 IllegalArgumentException → BackofficeException 변환 정확성 확인
  • 기존 코드와의 호환성 검증

4. 보안 설정 (SecurityConfig.java, SwaggerConfig.java)

  • CSRF 쿠키 도메인 설정이 개발/프로덕션 환경에서 올바르게 작동하는지 검증
  • 백오피스 세션 기반 인증이 모든 /v1/backoffice/** 경로에 적용되었는지 확인

5. 인증/권한 검증

  • BackofficeExpertController의 @SecurityRequirement 적용이 올바른지 확인
  • 엔드포인트별 권한 체크 로직 존재 확인
🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ⚠️ Unable to check for merge conflicts: Invalid branch name format
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 PR의 주요 변경사항을 명확하게 반영합니다. 백오피스 전문가 수정기능 구현이라는 핵심 의도가 잘 드러나 있습니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch SRLT-133-백오피스-전문가-수정
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch SRLT-133-백오피스-전문가-수정
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

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

@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: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/main/java/starlight/bootstrap/SecurityConfig.java (1)

69-77: 백오피스 CSRF 쿠키 도메인 공백 설정 시 로그인 불가 위험이 있습니다.
프로덕션에서 값이 공백으로 들어오면 잘못된 도메인 쿠키가 설정되어 CSRF 동작이 실패할 수 있습니다. 비어있을 때는 즉시 예외를 던지거나 도메인 설정을 생략하는 가드가 필요합니다.

🐛 제안 변경
 boolean isDevProfile = List.of(environment.getActiveProfiles()).contains("dev");
 if (!isDevProfile) {
+    if (backofficeCsrfCookieDomain == null || backofficeCsrfCookieDomain.isBlank()) {
+        throw new IllegalStateException("backoffice.csrf.cookie-domain must be configured");
+    }
     csrfTokenRepository.setCookieCustomizer(cookie -> cookie
             .domain(backofficeCsrfCookieDomain)
             .sameSite("None")
             .secure(true)
     );
 }
src/main/java/starlight/application/businessplan/required/SpellCheckerPort.java (1)

3-3: 어댑터 레이어의 DTO를 포트 인터페이스에서 노출하지 마세요

SpellCheckerPort(application 레이어의 driven port)가 starlight.adapter.businessplan.spellcheck.dto.Finding을 import하고 있습니다. 헥사고날 아키텍처에서 application 레이어는 adapter 레이어에 의존하면 안 됩니다.

Finding은 스펠 체크 도메인 개념(오류 타입, 심각도, 제안 목록 등)을 담고 있으므로, domain 레이어로 이동하여 포트가 domain 계층의 모델을 사용하도록 정정해 주세요. 어댑터 계층의 DaumSpellChecker는 도메인 Finding을 어댑터용 데이터로 변환(또는 그 반대)하는 책임을 가져야 합니다.

src/main/java/starlight/adapter/aireport/infrastructure/storage/NcpPresignedUrlProvider.java (1)

72-91: makePublic 메서드의 버킷 검증 부재

현재 makePublic은 전달된 objectUrl이 실제로 이 서비스의 버킷에 속하는 객체인지 검증하지 않습니다. 악의적인 사용자가 다른 버킷의 URL을 전달하면 예상치 못한 동작이 발생할 수 있습니다.

🔒 제안된 수정
     `@Override`
     public String makePublic(String objectUrl) {
+        // 버킷 소속 검증
+        String expectedHost = bucket + "." + endpoint.replaceFirst("^https?://", "").replaceAll("/$", "");
+        if (!objectUrl.contains(expectedHost)) {
+            throw new IllegalArgumentException("잘못된 버킷의 객체 URL입니다: " + objectUrl);
+        }
+        
         String key = extractKeyFromUrl(objectUrl);

         try {
src/main/java/starlight/adapter/aireport/webapi/ImageController.java (1)

28-31: objectUrl 파라미터 검증 누락

finalizePublic 엔드포인트의 objectUrl 파라미터에 검증이 없습니다. 빈 문자열이나 null이 전달되면 NcpPresignedUrlProvider.extractKeyFromUrl()에서 예외가 발생합니다. @NotBlank 검증을 추가하여 더 명확한 에러 응답을 제공하세요.

✅ 제안된 수정
+import jakarta.validation.constraints.NotBlank;
+import org.springframework.validation.annotation.Validated;
 
 `@RestController`
 `@RequestMapping`("/v1/images")
 `@RequiredArgsConstructor`
+@Validated
 public class ImageController implements ImageApiDoc {
 
     private final PresignedUrlProviderPort presignedUrlReader;
 
     // ...
 
     `@PostMapping`("/upload-url/public")
-    public ApiResponse<String> finalizePublic(`@RequestParam` String objectUrl) {
+    public ApiResponse<String> finalizePublic(`@RequestParam` `@NotBlank` String objectUrl) {
         return ApiResponse.success(presignedUrlReader.makePublic(objectUrl));
     }
 }
src/main/java/starlight/application/member/MemberService.java (1)

27-43: 인터페이스 메서드 구현에 @Override 어노테이션 누락

MemberUseCase 인터페이스의 모든 메서드 구현에 @Override 어노테이션을 일관되게 적용해야 합니다. 현재 getUserById에는 있지만, createUsergetUserByEmail에는 없습니다.

헥사고날 아키텍처에서 포트(인터페이스) 구현의 명확성을 위해 @Override로 명시하면, 시그니처 변경 시 컴파일 에러를 조기에 감지할 수 있으며 코드 의도를 더욱 명확히 드러낼 수 있습니다.

createUsergetUserByEmail 메서드에 @Override 어노테이션을 추가해주세요.

🤖 Fix all issues with AI agents
In `@config`:
- Line 1: config 서브모듈(.gitmodules의 URL에 설정된
`https://github.com/StartUpLight/config.git`)에 접근할 수 없어 초기화/빌드가 실패하므로
.gitmodules의 config 서브모듈 URL을 확인하고 올바른 리포지토 URL로 수정하거나 접근 권한을 부여하세요; 만약 리포지토가
이동·삭제됐다면 .gitmodules의 해당 `config` 엔트리를 업데이트하고 변경사항을 커밋(git add .gitmodules &&
git commit)한 후 푸시하고, 필요하면 서브모듈을 reinit/reupdate하여 CI가 성공하도록 권한 및 URL이 유효한지
검증하세요.

In
`@src/main/java/starlight/adapter/aireport/infrastructure/storage/NcpPresignedUrlProvider.java`:
- Around line 85-88: Replace the generic RuntimeException thrown in the
NcpPresignedUrlProvider catch block for S3Exception with a project-specific
exception so the global handler can process it; either throw AiReportException
(e.g., new AiReportException("객체 공개 처리 실패", e)) or create a StorageException
extending GlobalException (and use a StorageErrorType like ACL_UPDATE_FAILED)
and throw that from the catch (S3Exception e) block in NcpPresignedUrlProvider
to mirror the OcrException pattern used elsewhere.

In
`@src/main/java/starlight/adapter/aireport/report/agent/impl/SpringAiSectionGradeAgent.java`:
- Around line 100-107: After parsing the LLM response with
responseParser.parseSectionResponse(llmResponse), explicitly check
AiReportResult.sectionScores() for null or empty before streaming: if
sectionScores() is null or empty, log or throw a specific parsing error that
includes the raw llmResponse or a clear message (e.g., "sectionScores missing in
parsed AiReportResult") to differentiate from general parsing failures; then
proceed to locate the section by comparing getSectionType().name() against each
AiReportResult.SectionScoreDetailResponse.sectionType() and assign to
sectionScore only after the null/empty guard.

In
`@src/main/java/starlight/adapter/aireport/report/circuitbreaker/SectionGradingCircuitBreaker.java`:
- Around line 7-12: In SectionGradingCircuitBreaker, the allowRequest logic
currently returns true for STATE.HALF_OPEN allowing unlimited concurrent test
requests; change allowRequest (and any per-key logic using stateMap/half-open
handling) to gate a single test request at a time—e.g., add an AtomicBoolean or
AtomicInteger token (per key via stateMap/halfOpenToken or reuse
AtomicReference/AtomicInteger structures) that does a compareAndSet to grant
exactly one caller when state == HALF_OPEN and rejects others until that test
request completes and transitions state; ensure the token is released/cleared
when transitionToClosed/transitionToOpen/after the test completes so subsequent
HALF_OPEN cycles can permit another single tester.

In
`@src/main/java/starlight/adapter/aireport/report/dto/SectionGradingResult.java`:
- Around line 13-23: The extractScore method is missing a success filter so
failed SectionGradingResult entries with score=0 can be picked up; update the
extractScore(List<SectionGradingResult> results, SectionType type)
implementation to first filter by SectionGradingResult::success and then by r ->
r.sectionType() == type before mapping to SectionGradingResult::score and
defaulting to 0, ensuring only successful results contribute to the returned
score.

In
`@src/main/java/starlight/adapter/backoffice/image/webapi/swagger/BackofficeImageApiDoc.java`:
- Around line 27-33: The OpenAPI annotation in BackofficeImageApiDoc currently
documents the 200 response as PreSignedUrlResponse but the controller returns
ApiResponse<PreSignedUrlResponse>; update the ApiResponse annotation so the
response schema reflects the ApiResponse wrapper (use `@Content`(schema =
`@Schema`(implementation = ApiResponse.class)) or an equivalent allOf composition
that references ApiResponse plus the PreSignedUrlResponse payload) ensuring the
documented type is ApiResponse<PreSignedUrlResponse> instead of raw
PreSignedUrlResponse; adjust imports/annotations in BackofficeImageApiDoc to
reference ApiResponse and PreSignedUrlResponse accordingly.

In
`@src/main/java/starlight/adapter/businessplan/checklist/provider/ChecklistPromptProvider.java`:
- Around line 101-114: The prompt currently builds formattedCriteria from pairs
of criteria and detailedCriteria but sets requestLength to criteria.size(),
which can mismatch when lengths differ; in ChecklistPromptProvider change the
logic that computes requestLength to use the actual number of included pairs
(e.g., compute int includedCount = Math.min(criteria.size(),
detailedCriteria.size()) or derive it from the loop that builds criteriaBuilder)
and put that includedCount into variables.put("requestLength", ... ) so
requestLength accurately reflects the number of criteria lines included in
formattedCriteria.

In `@src/main/java/starlight/adapter/member/persistence/MemberJpa.java`:
- Line 5: MemberLookUpPort duplicates MemberQueryPort's findByIdOrThrow(Long
id); update BusinessPlanService to depend on MemberQueryPort instead of
MemberLookUpPort, remove the MemberLookUpPort import and any usages from
MemberJpa, and delete the MemberLookUpPort interface; specifically, replace
constructor/field types and method parameters in BusinessPlanService to use
MemberQueryPort, remove import
starlight.application.businessplan.required.MemberLookUpPort from MemberJpa and
ensure MemberJpa implements the methods declared in MemberQueryPort
(findByIdOrThrow), then remove the now-unused MemberLookUpPort definition from
the codebase.

In
`@src/main/java/starlight/application/aireport/provided/dto/AiReportResult.java`:
- Around line 32-35: Javadoc references the wrong type name; update the comment
above the static factory method fromGradingResult to mention AiReportResult
instead of AiReportResponse (and scan the surrounding Javadocs in the
AiReportResult class for any other lingering AiReportResponse mentions) so the
documentation matches the actual return type.

In
`@src/main/java/starlight/application/aireport/required/BusinessPlanCommandLookUpPort.java`:
- Around line 5-8: The interface name BusinessPlanCommandLookUpPort is
misleading because its methods save(...) and createBusinessPlanWithPdf(...) are
command (write) operations; rename the interface to BusinessPlanCommandPort and
update all references/usages (implementations, imports, DI/bean configs, tests)
to use BusinessPlanCommandPort while leaving the method signatures (save and
createBusinessPlanWithPdf) unchanged so compilation and behavior remain intact.

In
`@src/main/java/starlight/application/aireport/required/BusinessPlanQueryLookUpPort.java`:
- Around line 5-6: Rename the interface BusinessPlanQueryLookUpPort to
BusinessPlanLookupPort and update all references/usages (including
implementations and imports) to the new name; also keep the method signature
BusinessPlan findByIdOrThrow(Long id) unchanged. Ensure other similarly named
ports follow the same convention (e.g., BusinessPlanCommandLookUpPort ->
BusinessPlanCommandLookupPort, MemberLookUpPort -> MemberLookupPort) so naming
is consistently using "Lookup" (lowercase 'u') across the codebase. Verify
compilation and update any test or DI configuration that references
BusinessPlanQueryLookUpPort.

In
`@src/main/java/starlight/application/backoffice/expert/BackofficeExpertCommandService.java`:
- Around line 44-60: The updateExpert method currently always calls
expert.syncCareers(toCareerUpdates(input.careers())), which will delete all
careers when input.careers() is null/empty; change updateExpert to only call
expert.syncCareers(...) when the input explicitly contains careers (i.e.,
input.careers() != null or input.hasCareers flag), and adjust the request
mapping/DTO so BackofficeExpertUpdateInput preserves null for careers (do not
convert missing careers to an empty list) so PATCH can distinguish "no change"
vs "clear all". Locate updateExpert, syncCareers, and toCareerUpdates to
implement the conditional guard and update the BackofficeExpertUpdateInput
mapping to allow nullable careers.

In `@src/main/java/starlight/application/expert/ExpertDetailQueryService.java`:
- Around line 48-50: Add a new enum constant EXPERT_NOT_ACTIVE (e.g.
EXPERT_NOT_ACTIVE(HttpStatus.FORBIDDEN, "비활성 전문가입니다.")) to ExpertErrorType, then
update ExpertDetailQueryService to throw new
ExpertException(ExpertErrorType.EXPERT_NOT_ACTIVE) when expert.getActiveStatus()
!= ExpertActiveStatus.ACTIVE instead of using ExpertErrorType.EXPERT_NOT_FOUND;
ensure the new enum follows the same constructor/fields as other error types and
that any tests or handlers expecting EXPERT_NOT_FOUND for non-existent experts
remain unchanged.

In `@src/main/java/starlight/domain/aireport/exception/AiReportException.java`:
- Around line 12-14: The AiReportException(String message, Throwable cause)
constructor hardcodes AiReportErrorType.AI_RESPONSE_PARSING_FAILED causing
misclassification (e.g., JsonProcessingException from serialization). Add an
overloaded constructor public AiReportException(String message, Throwable cause,
AiReportErrorType type) that sets the provided type, then refactor the existing
two-arg constructor to delegate to it (preserving current default). Update
callers such as AiReportService (the location that currently uses the two-arg
constructor for serialization failures) to pass the correct AiReportErrorType
for each failure scenario so exceptions are classified correctly.

In `@src/main/java/starlight/domain/expert/entity/Expert.java`:
- Around line 139-187: In syncCareers, when a career lookup by id returns null
you currently throw new ExpertException(ExpertErrorType.EXPERT_NOT_FOUND);
change this to throw a career-specific error (e.g.
ExpertException(ExpertErrorType.EXPERT_CAREER_INVALID) or add a new
ExpertErrorType like EXPERT_CAREER_NOT_FOUND) so missing/invalid career IDs
produce a career-specific error; update the ExpertErrorType enum accordingly and
adjust any callers/tests expecting EXPERT_NOT_FOUND from syncCareers; keep all
other logic in syncCareers, ExpertCareerUpdate, and ExpertCareer.update
unchanged.

In `@src/main/java/starlight/shared/apiPayload/exception/GlobalException.java`:
- Around line 15-18: The new GlobalException(String message, Throwable cause,
ErrorType errorType) constructor currently allows a null errorType which delays
NPEs; update that constructor (GlobalException(..., ErrorType errorType)) to
perform a fail-fast null check on errorType (e.g., Objects.requireNonNull or
explicit check) and throw a clear exception (NPE or IllegalArgumentException) if
null so behavior matches the original constructor that relied on
errorType.getMessage().

In
`@src/test/java/starlight/adapter/aireport/report/SpringAiReportGraderTest.java`:
- Around line 16-18: Tests create thread pools with Executors.newFixedThreadPool
but declare them as Executor and never stop them, which can leave non-daemon
threads running; change the fields/local variables to type ExecutorService where
newFixedThreadPool is used (e.g., the Executor declared in
SpringAiReportGraderTest), store the returned ExecutorService, and guarantee
termination by calling shutdownNow() in a teardown (e.g., `@AfterEach` or a
finally block) for each created pool (also fix the other occurrence around the
130-168 region); ensure the teardown references the same ExecutorService
variable names so the pools are always shut down.

In
`@src/test/java/starlight/application/aireport/AiReportServiceIntegrationTest.java`:
- Around line 175-186: Update the test Bean ocrProvider() so its
ocrPdfByUrl(String pdfUrl) implementation returns a valid
starlight.shared.dto.infrastructure.OcrResponse dummy (not throw
UnsupportedOperationException); construct an OcrResponse whose extractedText
matches the string returned by ocrPdfTextByUrl and populate any required fields
(e.g., status/metadata) expected by ClovaOcrProvider, so calls from
ClovaOcrProvider -> ocrPdfByUrl succeed during the PDF flow integration test.
♻️ Duplicate comments (1)
src/main/java/starlight/adapter/backoffice/expert/webapi/dto/request/BackofficeExpertUpdateRequest.java (1)

23-48: careers null 처리 관련은 다른 코멘트에서 다뤘습니다.
동일 이슈의 중복 설명은 생략합니다.

🧹 Nitpick comments (45)
src/main/java/starlight/bootstrap/AsyncConfig.java (1)

35-47: sectionGradingExecutor 설정이 적절합니다.

AI 섹션 그레이딩을 위한 전용 스레드풀 설정이 잘 되어 있습니다. CallerRunsPolicy는 과부하 시 태스크 손실을 방지하고, 120초 종료 대기 시간은 AI 작업의 특성을 고려한 적절한 값입니다.

다만, emailTaskExecutor와 설정 코드가 중복되고 있어 추후 유지보수를 위해 공통 설정을 추출하는 것을 고려해 볼 수 있습니다.

♻️ 선택적 리팩토링 제안
private ThreadPoolTaskExecutor createExecutor(int coreSize, int maxSize, int queueCapacity, 
                                               String threadPrefix, int terminationSeconds) {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(coreSize);
    executor.setMaxPoolSize(maxSize);
    executor.setQueueCapacity(queueCapacity);
    executor.setThreadNamePrefix(threadPrefix);
    executor.setWaitForTasksToCompleteOnShutdown(true);
    executor.setAwaitTerminationSeconds(terminationSeconds);
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    executor.initialize();
    return executor;
}
src/main/java/starlight/adapter/aireport/webapi/dto/AiReportCreateWithPdfRequest.java (1)

5-10: pdfUrl에 URL 형식 검증 추가를 고려해 주세요.

현재는 공백만 막고 있어 잘못된 URL 형식이 들어와도 통과됩니다. @URL 어노테이션을 추가하면 http, https, ftp 등 표준 프로토콜에 대한 형식 검증이 자동으로 수행되어 더욱 안정적입니다.

♻️ 제안 변경
 import jakarta.validation.constraints.NotBlank;
+import org.hibernate.validator.constraints.URL;

 public record AiReportCreateWithPdfRequest(
     `@NotBlank`(message = "제목은 필수입니다.")
     String title,

     `@NotBlank`(message = "PDF URL은 필수입니다.")
+    `@URL`(message = "PDF URL 형식이 올바르지 않습니다.")
     String pdfUrl
 ) {}
src/main/java/starlight/application/backoffice/mail/util/EmailMaskingUtils.java (1)

20-33: 이메일 마스킹 로직이 잘 구현되었습니다.

널/빈 값 처리, @ 기호 위치 검증, 로컬 파트 마스킹 로직이 올바릅니다.

도메인(@ 뒤)은 마스킹되지 않고 그대로 노출됩니다. 운영 로그에서 도메인 정보가 필요한 경우 현재 구현이 적절하지만, GDPR/개인정보 보호 요구사항에 따라 도메인도 부분 마스킹이 필요할 수 있습니다. 현재 요구사항에 맞다면 무시해도 됩니다.

src/main/java/starlight/application/backoffice/mail/BackofficeMailSendLogEventHandler.java (1)

5-5: 사용되지 않는 import가 있습니다.

@EventListener@TransactionalEventListener로 대체되었으므로 line 5의 org.springframework.context.event.EventListener import가 더 이상 필요하지 않습니다.

♻️ 미사용 import 제거
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.context.event.EventListener;
 import org.springframework.dao.DataAccessException;
 import org.springframework.scheduling.annotation.Async;
src/main/java/starlight/application/expertReport/ExpertReportService.java (1)

75-85: Switch 문 간소화를 고려해 보세요.

Java 21에서는 switch expression을 더 간결하게 작성할 수 있습니다. 단일 문장인 경우 중괄호 생략이 가능합니다.

♻️ 제안된 리팩토링
         switch (saveType) {
-            case TEMPORARY -> {
-                report.temporarySave();
-            }
-            case FINAL -> {
+            case TEMPORARY -> report.temporarySave();
+            case FINAL -> {
                 report.submit();
                 BusinessPlan plan = businessPlanQuery.findByIdOrThrow(report.getBusinessPlanId());
                 plan.updateStatus(PlanStatus.FINALIZED);
             }
-
         }
src/main/java/starlight/application/backoffice/mail/util/BackofficeMailContentTypeParser.java (1)

11-17: 원본 예외를 cause로 보존하는 것을 권장합니다.

현재 IllegalArgumentException을 새로운 BackofficeException으로 변환할 때 원본 예외가 손실됩니다. 디버깅 및 로그 분석 시 유용할 수 있도록 원본 예외를 cause로 전달하는 것이 좋습니다.

BackofficeExceptionThrowable cause를 받는 생성자를 지원합니다 (relevant_code_snippets 참조).

♻️ 제안된 수정
     public static BackofficeMailContentType parse(String contentType) {
         try {
             return BackofficeMailContentType.from(contentType);
         } catch (IllegalArgumentException exception) {
-            throw new BackofficeException(BackofficeErrorType.INVALID_MAIL_CONTENT_TYPE);
+            throw new BackofficeException(BackofficeErrorType.INVALID_MAIL_CONTENT_TYPE, exception);
         }
     }
src/main/java/starlight/application/backoffice/mail/BackofficeMailSendService.java (1)

50-67: 유효성 검사 실패 시 더 구체적인 오류 정보 제공을 고려해 보세요.

현재 모든 유효성 검사 실패가 동일한 INVALID_MAIL_REQUEST 에러 타입을 사용합니다. 운영 환경에서 문제 원인 파악을 용이하게 하려면, 로깅 시 어떤 필드가 유효하지 않은지 포함하거나 별도의 에러 타입을 정의하는 것을 고려해 볼 수 있습니다.

💡 선택적 개선 방안
// 옵션 1: 로그에 상세 정보 포함
if (input.to() == null || input.to().isEmpty()) {
    log.warn("Mail validation failed: recipient is empty");
    throw new BackofficeException(BackofficeErrorType.INVALID_MAIL_REQUEST);
}

// 옵션 2: 별도 에러 타입 정의 (BackofficeErrorType에 추가)
// INVALID_MAIL_RECIPIENT, INVALID_MAIL_SUBJECT, INVALID_MAIL_BODY 등
src/main/java/starlight/adapter/businessplan/persistence/BusinessPlanQueryJpa.java (1)

17-20: 클래스명과 책임 불일치 검토 필요

BusinessPlanQueryJpa라는 클래스명이 Query 전용임을 암시하지만, 실제로는 BusinessPlanCommandPortBusinessPlanCommandLookUpPort를 포함한 Command 관련 포트도 구현하고 있습니다.

헥사고날 아키텍처의 명확한 책임 분리를 위해 클래스명을 BusinessPlanPersistenceAdapter 또는 BusinessPlanRepositoryAdapter로 변경하거나, Query/Command 어댑터를 분리하는 것을 고려해 주세요.

src/main/java/starlight/adapter/backoffice/mail/webapi/swagger/BackofficeMailApiDoc.java (1)

116-134: deleteTemplate 엔드포인트에 에러 응답 문서 누락

다른 엔드포인트들(send, createTemplate)은 400 에러 응답을 문서화하고 있지만, deleteTemplate은 200 응답만 정의되어 있습니다. 존재하지 않는 템플릿 ID로 삭제 요청 시의 에러 응답(404 또는 400)도 문서화하면 API 사용자에게 더 명확한 가이드가 됩니다.

📝 에러 응답 추가 제안
     `@ApiResponses`({
             `@io.swagger.v3.oas.annotations.responses.ApiResponse`(
                     responseCode = "200",
                     description = "성공",
                     content = `@Content`(examples = `@ExampleObject`(
                             value = """
                             {
                               "result": "SUCCESS",
                               "data": "템플릿이 삭제되었습니다.",
                               "error": null
                             }
                             """
                     ))
-            )
+            ),
+            `@io.swagger.v3.oas.annotations.responses.ApiResponse`(
+                    responseCode = "404",
+                    description = "템플릿을 찾을 수 없음",
+                    content = `@Content`(examples = `@ExampleObject`(
+                            value = """
+                            {
+                              "result": "ERROR",
+                              "data": null,
+                              "error": {
+                                "code": "TEMPLATE_NOT_FOUND",
+                                "message": "해당 템플릿을 찾을 수 없습니다."
+                              }
+                            }
+                            """
+                    ))
+            )
     })
src/main/java/starlight/adapter/backoffice/mail/webapi/BackofficeMailController.java (2)

19-20: 클래스 레벨 @SecurityRequirement 중복 검토

@SecurityRequirement(name = "backofficeSession")가 컨트롤러 클래스 레벨(Line 19)과 BackofficeMailApiDoc 인터페이스의 각 메서드에 모두 선언되어 있습니다. 인터페이스에 이미 정의되어 있으므로 클래스 레벨 어노테이션은 중복일 수 있습니다.

다만, 명시적인 보안 요구사항 표현을 위해 의도적으로 추가한 것이라면 현재 상태를 유지해도 무방합니다.


48-54: templateId 유효성 검증 고려

@PathVariable Long templateId@Positive 어노테이션을 추가하면 음수나 0 같은 유효하지 않은 ID를 컨트롤러 레벨에서 조기에 거부할 수 있습니다.

✨ 유효성 검증 추가 제안
+import jakarta.validation.constraints.Positive;
+
     `@DeleteMapping`("/v1/backoffice/mail/templates/{templateId}")
     public ApiResponse<String> deleteTemplate(
-            `@PathVariable` Long templateId
+            `@PathVariable` `@Positive` Long templateId
     ) {
src/main/java/starlight/adapter/aireport/report/provider/SpringAiAdvisorProvider.java (1)

36-38: 연산자 주변 공백 추가 권장

가독성을 위해 연산자 주변에 공백을 추가하는 것이 좋습니다.

♻️ 제안된 수정
     public SimpleLoggerAdvisor getSimpleLoggerAdvisor(){
-        return new SimpleLoggerAdvisor(Ordered.LOWEST_PRECEDENCE-1);
+        return new SimpleLoggerAdvisor(Ordered.LOWEST_PRECEDENCE - 1);
     }
src/main/java/starlight/adapter/backoffice/image/webapi/dto/request/BackofficeImagePublicRequest.java (1)

5-7: URL 형식 검증 추가 권장

@NotBlank만으로는 유효한 URL 형식인지 검증되지 않습니다. 잘못된 형식의 URL이 전달되면 NcpPresignedUrlProvider.extractKeyFromUrl()에서 IllegalArgumentException이 발생할 수 있습니다. URL 형식 검증을 추가하면 더 명확한 에러 메시지를 제공할 수 있습니다.

♻️ 제안된 수정
 package starlight.adapter.backoffice.image.webapi.dto.request;

 import jakarta.validation.constraints.NotBlank;
+import org.hibernate.validator.constraints.URL;

 public record BackofficeImagePublicRequest(
-        `@NotBlank` String objectUrl
+        `@NotBlank` `@URL` String objectUrl
 ) { }
src/main/java/starlight/adapter/aireport/webapi/ImageController.java (1)

8-18: 포트 인터페이스로 의존성 변경 - LGTM!

PresignedUrlProviderPort로 의존성 타입을 변경하여 헥사고날 아키텍처의 포트 기반 설계를 따릅니다.

필드명 presignedUrlReader는 읽기 작업만 수행하는 것처럼 보이지만, makePublic은 쓰기 작업입니다. presignedUrlProvider 또는 presignedUrlPort로 변경하면 의미가 더 명확해집니다.

♻️ 제안된 수정
-    private final PresignedUrlProviderPort presignedUrlReader;
+    private final PresignedUrlProviderPort presignedUrlProvider;
src/main/java/starlight/application/businessplan/required/ChecklistGraderPort.java (1)

7-9: 포트 인터페이스에 Javadoc 추가 권장

기존 ChecklistGrader 인터페이스에는 메서드 설명이 포함된 Javadoc이 있습니다. 일관성을 위해 ChecklistGraderPort에도 동일한 수준의 문서화를 추가하는 것을 권장합니다.

📝 Javadoc 추가 제안
 public interface ChecklistGraderPort {

+    /**
+     * 서브섹션 내용을 체크리스트 기준에 따라 체크합니다.
+     *
+     * `@param` subSectionType 서브섹션 타입
+     * `@param` content 서브섹션 내용
+     * `@return` 체크리스트 결과
+     */
     List<Boolean> check(SubSectionType subSectionType, String content);
 }
src/test/java/starlight/adapter/aireport/infrastructure/webapi/ImageControllerIntegrationTest.java (1)

59-84: 주석 처리된 테스트 코드 정리 필요

getPresignedUrl_Success 테스트가 주석 처리되어 있습니다. 주석 처리된 테스트 코드는 코드베이스 품질을 저하시킵니다. 테스트가 현재 작동하지 않는 이유가 있다면 이슈로 추적하고 제거하거나, 수정하여 활성화하는 것이 좋습니다.

테스트 활성화를 위한 수정이 필요하시면 도와드릴 수 있습니다. 또는 이 테스트를 추적하기 위한 이슈를 생성해 드릴까요?

src/main/java/starlight/adapter/member/webapi/MemberController.java (1)

21-21: 필드명과 타입명 불일치 확인

타입이 MemberUseCase로 변경되었지만 필드명은 여전히 memberQueryUseCase입니다. 일관성을 위해 필드명도 memberUseCase로 변경하는 것을 권장합니다.

♻️ 필드명 일치 제안
-    private final MemberUseCase memberQueryUseCase;
+    private final MemberUseCase memberUseCase;

그리고 사용처도 함께 변경:

     public ApiResponse<MemberDetailResponse> getMemberDetail(
             `@AuthenticationPrincipal` AuthenticatedMember authenticatedMember
     ) {
         return ApiResponse.success(MemberDetailResponse.fromMember(
-                memberQueryUseCase.getUserById(authenticatedMember.getMemberId())
+                memberUseCase.getUserById(authenticatedMember.getMemberId())
         ));
     }
src/main/java/starlight/application/businessplan/util/BusinessPlanContentExtractor.java (3)

19-19: 미사용 @Slf4j 어노테이션

@Slf4j가 추가되었지만 파일 내에서 log를 사용하는 곳이 없습니다. 실제로 로깅이 필요한 경우가 아니라면 제거하는 것이 좋습니다.

♻️ 제안된 수정
-@Slf4j
 `@Component`
 public class BusinessPlanContentExtractor {

99-123: 중복 로직 리팩토링 기회

extractSectionContents 메서드와 extractContent 메서드(lines 26-70)가 동일한 섹션 추출 로직을 반복합니다. 유지보수성을 위해 내부적으로 extractSectionContents를 호출하도록 extractContent를 리팩토링하는 것을 고려해 주세요.

♻️ 리팩토링 제안
public String extractContent(BusinessPlan businessPlan) {
    StringBuilder promptBuilder = new StringBuilder();
    promptBuilder.append("다음 사업계획서 내용을 채점해주세요:\n\n");
    
    Map<SectionType, String> sectionContents = extractSectionContents(businessPlan);
    List<String> sections = sectionContents.values().stream()
        .filter(content -> !content.isBlank())
        .toList();
    
    promptBuilder.append(String.join("\n\n", sections));
    return promptBuilder.toString();
}

125-148: 주석 처리된 코드 정리 필요

주석 처리된 extractSectionContentsFromText 메서드가 있습니다. TODO 주석에 따르면 향후 구현이 필요한 기능입니다. 코드 정리를 위해 이 주석 코드를 제거하고 별도의 이슈로 트래킹하는 것을 권장합니다.

이 기능에 대한 이슈를 생성해 드릴까요?

src/main/java/starlight/bootstrap/AiReportSectionAdvisorConfig.java (1)

32-42: Java 21에서는 .toList() 사용 권장

Java 16+ 에서는 Collectors.toList() 대신 Stream.toList()를 직접 사용할 수 있습니다. 불변 리스트를 반환하며 더 간결합니다.

♻️ 제안된 수정
         return Arrays.stream(SectionType.values())
             .filter(sectionType -> sectionType.getTag() != null)  // OVERVIEW 제외
             .map(sectionType -> new SpringAiSectionGradeAgent(
                 sectionType,
                 chatClientBuilder,
                 reportPromptProvider,
                 advisorProvider,
                 responseParser,
                 circuitBreaker
             ))
-            .collect(Collectors.toList());
+            .toList();
src/test/java/starlight/application/member/auth/AuthServiceImplIntegrationTest.java (1)

31-34: 필드 이름과 타입 간 불일치

헥사고날 아키텍처 네이밍 컨벤션에 따르면:

  • MemberUseCase 타입의 필드 이름이 memberQueryUseCase로 되어 있어 불일치합니다.
  • CredentialUseCase 타입의 필드 이름이 credentialService로 되어 있어 불일치합니다.

일관성을 위해 필드 이름을 타입에 맞게 수정하는 것을 권장합니다. Based on learnings, UseCase 인터페이스와 Service 구현체 간의 명확한 구분이 필요합니다.

♻️ 제안된 수정
     `@MockitoBean`
-    MemberUseCase memberQueryUseCase;
+    MemberUseCase memberUseCase;
     `@MockitoBean`
-    CredentialUseCase credentialService;
+    CredentialUseCase credentialUseCase;

필드 이름 변경 시 테스트 메서드 내 참조도 함께 업데이트해야 합니다:

  • memberQueryUseCasememberUseCase
  • credentialServicecredentialUseCase
src/test/java/starlight/application/member/auth/AuthServiceImplUnitTest.java (1)

27-31: 변수명과 타입명 간 일관성 개선을 권장합니다.

CredentialUseCase 타입의 변수명이 credentialService로 되어 있어 혼란을 줄 수 있습니다. 타입에 맞게 credentialUseCase로 변경하면 가독성이 향상됩니다.

♻️ 제안된 변경
 `@Mock`
 MemberUseCase memberQueryUseCase;
 `@Mock`
-CredentialUseCase credentialService;
+CredentialUseCase credentialUseCase;

테스트 내 참조도 함께 업데이트 필요:

-verify(credentialService).checkPassword(member, "pw");
+verify(credentialUseCase).checkPassword(member, "pw");
src/main/java/starlight/application/aireport/util/SectionScoreExtractor.java (2)

12-19: 불변 Map 사용을 권장합니다.

상수 맵이므로 HashMap 대신 Map.of()를 사용하면 불변성이 보장되고 코드도 간결해집니다.

♻️ 제안된 변경
-private static final Map<SectionType, Function<AiReportResult, Integer>> SCORE_EXTRACTORS = new HashMap<>();
-
-static {
-    SCORE_EXTRACTORS.put(SectionType.PROBLEM_RECOGNITION, AiReportResult::problemRecognitionScore);
-    SCORE_EXTRACTORS.put(SectionType.FEASIBILITY, AiReportResult::feasibilityScore);
-    SCORE_EXTRACTORS.put(SectionType.GROWTH_STRATEGY, AiReportResult::growthStrategyScore);
-    SCORE_EXTRACTORS.put(SectionType.TEAM_COMPETENCE, AiReportResult::teamCompetenceScore);
-}
+private static final Map<SectionType, Function<AiReportResult, Integer>> SCORE_EXTRACTORS = Map.of(
+    SectionType.PROBLEM_RECOGNITION, AiReportResult::problemRecognitionScore,
+    SectionType.FEASIBILITY, AiReportResult::feasibilityScore,
+    SectionType.GROWTH_STRATEGY, AiReportResult::growthStrategyScore,
+    SectionType.TEAM_COMPETENCE, AiReportResult::teamCompetenceScore
+);

21-33: 새로운 SectionType 추가 시 매핑 누락을 감지할 수 있도록 경고 로그 추가를 권장합니다.

현재 SectionType은 5개(OVERVIEW, PROBLEM_RECOGNITION, FEASIBILITY, GROWTH_STRATEGY, TEAM_COMPETENCE)이지만, 이 중 OVERVIEW는 의도적으로 점수 매핑이 제외되어 있습니다. 다만, 향후 새로운 섹션이 추가되고 해당 매핑이 누락되는 경우, 조용히 0을 반환하므로 개발자가 이를 감지하기 어렵습니다.

♻️ 제안된 변경
 Function<AiReportResult, Integer> extractor = SCORE_EXTRACTORS.get(sectionType);
 if (extractor == null) {
+    log.warn("Unknown SectionType for score extraction: {}", sectionType);
     return 0;
 }

참고: 매핑이 의도적으로 제외된 SectionType의 경우, 로그 레벨을 debug로 변경하거나 enum 자체에서 점수 추출 여부를 마크하는 방식도 고려할 수 있습니다.

src/test/java/starlight/adapter/businessplan/webapi/SpellControllerTest.java (1)

53-75: 주석 처리된 테스트 코드 정리가 필요합니다.

테스트가 주석 처리된 상태로 남아있습니다. 테스트가 더 이상 필요하지 않다면 삭제하고, 필요하다면 활성화해주세요. 주석 처리된 테스트 코드는 코드베이스의 가독성을 저해하고 유지보수 부담을 증가시킵니다.

src/main/java/starlight/application/member/auth/AuthServiceImpl.java (1)

23-29: 구현체 네이밍 규칙(AuthServiceImpl) 정렬을 고려해주세요.

AuthUseCase 구현체는 Impl 접미사를 제거하고 AuthService로 맞추는 편이 일관됩니다. 필드명도 MemberUseCase/CredentialUseCase와 맞춰 memberUseCase/credentialUseCase로 정렬하면 가독성이 좋아집니다. Based on learnings, 네이밍 규칙 정렬이 필요합니다.

src/main/java/starlight/adapter/aireport/webapi/dto/AiReportResponse.java (2)

1-12: 동일 이름 DTO 중복으로 인한 import 혼동 가능성

application 레이어에도 AiReportResponse가 있어 패키지 오타/자동 임포트 실수가 나기 쉽습니다. Web 전용임을 드러내는 명칭(AiReportWebResponse 등)으로 구분을 고려해 주세요.


33-55: result 리스트 null 대비가 있으면 안정성이 올라갑니다.

AiReportResult의 sectionScores/strengths/weaknesses가 null일 가능성이 있으면 현재 스트림에서 NPE가 납니다. 도메인에서 non-null 보장이 없다면 빈 리스트로 방어하는 편이 안전합니다.

♻️ 제안 변경
-import java.util.List;
+import java.util.List;
+import java.util.Optional;
@@
-        List<SectionScoreDetailResponse> sectionScores = result.sectionScores().stream()
+        List<SectionScoreDetailResponse> sectionScores = Optional.ofNullable(result.sectionScores())
+                .orElse(List.of())
+                .stream()
                 .map(s -> new SectionScoreDetailResponse(s.sectionType(), s.gradingListScores()))
                 .toList();
-        List<StrengthWeakness> strengths = result.strengths().stream()
+        List<StrengthWeakness> strengths = Optional.ofNullable(result.strengths())
+                .orElse(List.of())
+                .stream()
                 .map(s -> new StrengthWeakness(s.title(), s.content()))
                 .toList();
-        List<StrengthWeakness> weaknesses = result.weaknesses().stream()
+        List<StrengthWeakness> weaknesses = Optional.ofNullable(result.weaknesses())
+                .orElse(List.of())
+                .stream()
                 .map(w -> new StrengthWeakness(w.title(), w.content()))
                 .toList();
src/main/java/starlight/adapter/aireport/report/agent/impl/SpringAiFullReportGradeAgent.java (1)

35-63: 예외 래핑 시 원인 로그를 남겨주세요.

현재 일반 Exception이 로그 없이 AiReportException으로 치환되어 원인 분석이 어렵습니다. 최소한 error 로그로 남기는 편이 안전합니다.

♻️ 제안 변경
-                } catch (Exception e) {
-                        throw new AiReportException(AiReportErrorType.AI_GRADING_FAILED);
-                }
+                } catch (Exception e) {
+                        log.error("AI full report grading failed", e);
+                        throw new AiReportException(AiReportErrorType.AI_GRADING_FAILED);
+                }
src/main/java/starlight/adapter/aireport/report/supervisor/SpringAiReportSupervisor.java (1)

49-75: 빈 입력/응답을 먼저 걸러주면 불필요한 예외 로그를 줄일 수 있습니다.

현재 llmResponse가 비어 있어도 바로 parse를 호출해 예외로 흐르고, 입력 값이 null이면 for-loop에서 NPE가 납니다. 간단한 가드로 빠르게 빈 리스트 반환하면 의도가 더 명확해집니다.

♻️ 제안 변경
-        try {
+        try {
+            if (businessPlanContent == null || businessPlanContent.trim().isEmpty()
+                    || sectionResults == null || sectionResults.isEmpty()) {
+                log.warn("Supervisor skipped {} generation due to empty input", type);
+                return List.of();
+            }
             String prompt = buildSupervisorPrompt(businessPlanContent, sectionResults, type);
@@
-            String llmResponse = chatClient
+            String llmResponse = chatClient
                 .prompt(new Prompt(List.of(
                     new SystemMessage(supervisorSystemPrompt),
                     new UserMessage(prompt)
                 )))
                 .options(ChatOptions.builder()
                     .temperature(0.0)
                     .topP(0.1)
                     .build())
                 .call()
                 .content();
+
+            if (llmResponse == null || llmResponse.trim().isEmpty()) {
+                log.warn("Supervisor returned empty {} response", type);
+                return List.of();
+            }
             
             return responseParser.parseStrengthWeakness(llmResponse, type);
src/main/java/starlight/adapter/businessplan/checklist/SpringAiChecklistGrader.java (1)

27-35: 체크리스트 기준이 없을 때 LLM 호출 스킵 권장

criteria/detailedCriteria가 비어 있으면 호출 결과가 의미 없고 비용만 증가합니다. 빈 경우 기본값 반환으로 바로 종료하는 편이 안전합니다.

✅ 제안 수정
         List<String> criteria = checklistCatalog.getCriteriaBySubSectionType(subSectionType);
         List<String> detailedCriteria = checklistCatalog.getDetailedCriteriaBySubSectionType(subSectionType);

+        if (criteria.isEmpty() || detailedCriteria.isEmpty()) {
+            return normalizeToFive(List.of());
+        }
+
         // 2) LLM 호출 → Boolean 배열 파싱
         List<Boolean> result = generator.generateChecklistArray(subSectionType, content, criteria, detailedCriteria);
src/main/java/starlight/adapter/businessplan/checklist/agent/SpringAiChecklistAgent.java (1)

52-57: JSON 파싱 실패 시 로그가 없어 원인 파악이 어렵습니다
실패 시 바로 기본값만 반환하면 문제 원인을 놓칠 수 있어 최소한의 경고 로그를 남기는 것을 권장합니다.

🛠️ 수정 제안
         try {
             return objectMapper.readValue(output, new TypeReference<List<Boolean>>() {
             });
         } catch (Exception e) {
+            String snippet = output == null ? "null"
+                    : (output.length() > 500 ? output.substring(0, 500) + "..." : output);
+            log.warn("체크리스트 응답 파싱 실패: {}", snippet, e);
             return List.of(false, false, false, false, false);
         }
src/main/java/starlight/adapter/aireport/report/SpringAiReportGrader.java (3)

34-34: contentExtractor 필드가 사용되지 않습니다.

BusinessPlanContentExtractor contentExtractor 필드가 주입되지만 이 클래스 내에서 사용되는 곳이 없습니다. 불필요한 의존성이라면 제거하는 것이 좋습니다.

♻️ 미사용 필드 제거 제안
 public class SpringAiReportGrader implements ReportGraderPort {

     private final Map<SectionType, SectionGradeAgent> sectionGradeAgentMap;
     private final FullReportGradeAgent fullReportGradeAgent;
     private final SpringAiReportSupervisor supervisor;
-    private final BusinessPlanContentExtractor contentExtractor;
     private final Executor sectionGradingExecutor;

     public SpringAiReportGrader(
             List<SectionGradeAgent> sectionGradeAgentList,
             FullReportGradeAgent fullReportGradeAgent,
             SpringAiReportSupervisor supervisor,
-            BusinessPlanContentExtractor contentExtractor,
             `@Qualifier`("sectionGradingExecutor") Executor sectionGradingExecutor) {

85-93: FQN(Fully Qualified Name) 사용이 일관성이 없습니다.

Lines 13-14에서 AiReportErrorTypeAiReportException을 import 했지만, 이 코드 블록에서는 FQN을 사용하고 있습니다. 코드 가독성을 위해 import된 클래스명을 직접 사용하는 것이 좋습니다.

♻️ import된 클래스명 사용 제안
         if (sectionContents == null || sectionContents.isEmpty()) {
             log.error("섹션별 내용이 비어있습니다");
-            throw new starlight.domain.aireport.exception.AiReportException(
-                    starlight.domain.aireport.exception.AiReportErrorType.AI_GRADING_FAILED);
+            throw new AiReportException(AiReportErrorType.AI_GRADING_FAILED);
         }

         if (fullContent == null || fullContent.trim().isEmpty()) {
             log.error("전체 내용이 비어있습니다");
-            throw new starlight.domain.aireport.exception.AiReportException(
-                    starlight.domain.aireport.exception.AiReportErrorType.AI_GRADING_FAILED);
+            throw new AiReportException(AiReportErrorType.AI_GRADING_FAILED);
         }

162-173: future.get(0, TimeUnit.SECONDS) 타임아웃 값이 의도된 것인지 확인이 필요합니다.

allFutures.get(2, TimeUnit.MINUTES) 이후에 개별 future에서 0초 타임아웃으로 결과를 가져오는 것은 이미 완료된 future를 즉시 반환받기 위한 것으로 보입니다. 이 패턴은 동작하지만, 코드 의도를 명확히 하기 위해 주석을 추가하는 것이 좋겠습니다.

src/test/java/starlight/application/aireport/AiReportServiceUnitTest.java (1)

46-46: sut 초기화를 @BeforeEach로 리팩터링하면 테스트 코드가 더 깔끔해집니다.

현재 각 테스트 메서드에서 sut를 개별적으로 초기화하고 있습니다. @BeforeEach를 사용하면 중복 코드를 줄일 수 있습니다.

♻️ `@BeforeEach` 사용 제안
+    `@BeforeEach`
+    void setUp() {
+        sut = new AiReportService(
+            businessPlanCommandLookUpPort, 
+            businessPlanQueryLookUpPort, 
+            aiReportQuery, 
+            aiReportCommand, 
+            aiReportGrader, 
+            objectMapper, 
+            ocrProvider, 
+            responseParser, 
+            contentExtractor
+        );
+    }
+
     private AiReportService sut;

각 테스트 메서드에서 sut = new AiReportService(...) 호출을 제거하세요.

src/main/java/starlight/application/aireport/util/AiReportResponseParser.java (3)

276-278: 빈 catch 블록은 디버깅을 어렵게 만듭니다.

예외가 발생해도 무시되어 문제 추적이 어려워집니다. 최소한 debug 레벨 로깅을 추가하는 것이 좋습니다.

♻️ 로깅 추가 제안
                 } catch (Exception e2) {
+                    log.debug("Regex extraction failed for 'text' field", e2);
                 }

293-364: repairIncompleteJson 메서드의 복구 로직에 한계가 있습니다.

현재 구현은 누락된 ]를 모두 추가한 후 }를 추가하는 방식입니다. 이는 {[}] 같은 복잡한 중첩 구조에서 올바른 순서를 보장하지 못할 수 있습니다.

예: {"arr": [1, 2 → 현재: {"arr": [1, 2]} (정상)
예: {"a": {"b": [1 → 현재: {"a": {"b": [1]}} (정상)

다만, 대부분의 LLM 응답이 정형화된 구조이므로 현재 구현도 실용적입니다. Edge case가 발생하면 스택 기반 복구로 개선을 고려하세요.


159-162: 예외 발생 시 원본 예외 정보가 손실됩니다.

catch (Exception e) 블록에서 원본 예외 e의 정보를 로깅하지 않고 새로운 예외를 던지고 있습니다. 디버깅을 위해 원본 예외를 로깅하거나 cause로 전달하는 것이 좋습니다.

♻️ 예외 정보 보존 제안
         } catch (Exception e) {
+            log.error("Failed to parse LLM response", e);
             throw new AiReportException(AiReportErrorType.AI_RESPONSE_PARSING_FAILED);
-        }
+        }

또는 cause 체인 유지:

throw new AiReportException("Failed to parse LLM response", e);
src/main/java/starlight/adapter/backoffice/expert/webapi/dto/request/BackofficeExpertProfileImageUpdateRequest.java (1)

5-7: URL 형식 검증 추가를 권장합니다.

@NotBlank는 빈 문자열 여부만 검증하며 URL 형식은 검증하지 않습니다. 잘못된 형식의 URL이 입력될 경우 downstream에서 예외가 발생하거나 데이터 정합성 문제가 생길 수 있습니다.

♻️ URL 형식 검증 추가 제안
 package starlight.adapter.backoffice.expert.webapi.dto.request;
 
 import jakarta.validation.constraints.NotBlank;
+import org.hibernate.validator.constraints.URL;
 
 public record BackofficeExpertProfileImageUpdateRequest(
-        `@NotBlank` String profileImageUrl
+        `@NotBlank` `@URL` String profileImageUrl
 ) { }

또는 presigned URL 등 특정 도메인만 허용해야 한다면 @Pattern 어노테이션으로 도메인을 제한하는 것도 고려해 주세요.

src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationJpa.java (1)

21-24: 동일한 이름의 인터페이스가 여러 패키지에 존재하며, 서로 다른 계약(contract)을 가지고 있습니다.

ExpertApplicationCountLookupPort가 두 개의 다른 패키지에 정의되어 있고, 메서드 시그니처가 다릅니다:

  • starlight.application.expert.required.ExpertApplicationCountLookupPort (2개 메서드)
  • starlight.application.expertReport.required.ExpertApplicationCountLookupPort (1개 메서드)

현재 ExpertApplicationJpa가 두 인터페이스를 모두 구현하고 있는데, 동일한 이름으로 인해 코드 가독성과 유지보수성이 저하될 수 있습니다. 두 인터페이스가 서로 다른 바운디드 컨텍스트(expert vs. expertReport)를 나타낸다면, 의도를 명확히 드러내기 위해 다음을 검토하시기 바랍니다:

  • 더 명확한 이름으로 변경 (예: ExpertApplicationCountLookupPortExpertReportApplicationCountLookupPort)
  • 또는 이 클래스가 정말 두 계약을 모두 구현해야 하는지 아키텍처 관점에서 재검토
src/main/java/starlight/application/expert/ExpertDetailQueryService.java (1)

27-42: DB 레벨에서 ACTIVE 필터링을 권장합니다.

현재 구현은 모든 Expert를 조회한 후 메모리에서 ACTIVE 상태를 필터링합니다. 데이터가 증가하면 불필요한 데이터 전송과 메모리 사용이 발생할 수 있습니다.

ExpertQueryPort에 ACTIVE 상태만 조회하는 메서드를 추가하는 것을 고려해 보세요.

♻️ 제안하는 개선 방안
// ExpertQueryPort에 추가
List<Expert> findAllActiveWithCareersTagsCategories();

// ExpertDetailQueryService에서 사용
`@Override`
public List<ExpertDetailResult> searchAllActive() {
    List<Expert> activeExperts = expertQueryPort.findAllActiveWithCareersTagsCategories();
    
    List<Long> expertIds = activeExperts.stream()
            .map(Expert::getId)
            .toList();
    // ... 나머지 로직
}
src/main/java/starlight/domain/expert/enumerate/ExpertActiveStatus.java (1)

8-13: API 계약에 enum 노출값(“ACTIVE/INACTIVE”)을 명시하는 걸 고려해 주세요.
description은 내부 설명용이고, 기본 직렬화 값은 enum 이름이라 클라이언트 혼동을 줄이려면 Swagger 예시/설명을 보강하는 편이 안전합니다.

src/main/java/starlight/adapter/backoffice/expert/webapi/dto/request/BackofficeExpertCreateRequest.java (1)

10-24: tags/categories의 null 정규화 고려를 추천합니다.
생성 요청에서는 null보다 빈 리스트가 보통이라, downstream NPE 방지를 위해 기본값/복사 처리로 정규화하면 안전합니다.

🛠️ 제안 수정안
     public BackofficeExpertCreateInput toInput() {
         return BackofficeExpertCreateInput.of(
                 name,
                 email,
                 oneLineIntroduction,
-                tags,
-                categories
+                tags == null ? List.of() : List.copyOf(tags),
+                categories == null ? List.of() : List.copyOf(categories)
         );
     }

Copy link

@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: 2

🤖 Fix all issues with AI agents
In
`@src/main/java/starlight/adapter/backoffice/image/webapi/BackofficeImageController.java`:
- Around line 3-15: The controller implementation BackofficeImageController is
missing validation annotations that live on the interface BackofficeImageApiDoc,
so Spring won’t run parameter validation; add `@Validated` at the class level (if
not already present) and explicitly annotate the controller method parameters
with `@Valid` and the custom `@ValidImageFileName` (the same annotations used in
BackofficeImageApiDoc) for the BackofficeImagePublicRequest / filename
parameters so runtime validation is executed; ensure import of javax/
jakarta.validation.Valid and the `@ValidImageFileName` annotation and that method
signatures in BackofficeImageController mirror the annotated parameter types
from BackofficeImageApiDoc.

In
`@src/main/java/starlight/adapter/backoffice/image/webapi/swagger/BackofficeImageApiDoc.java`:
- Around line 95-98: The 200 ApiResponse for finalizePublic doesn't declare a
response content/schema so Swagger won't show the response body; update the
ApiResponse on finalizePublic to include content with MediaType
"application/json" (or "text/plain" if appropriate) and a schema for String
(e.g., `@Content`(schema = `@Schema`(implementation = String.class)) or an example
like in BackofficeMailApiDoc), following the pattern used by
getPresignedUrl/BackofficeMailApiDoc to ensure ApiResponse<String> is documented
correctly.
♻️ Duplicate comments (1)
src/main/java/starlight/adapter/backoffice/image/webapi/swagger/BackofficeImageApiDoc.java (1)

29-34: 200 응답 스키마에 ApiResponse 래퍼 반영 필요
컨트롤러 반환이 ApiResponse<PreSignedUrlResponse>인데 문서에는 PreSignedUrlResponse만 표기되어 포맷 오해 위험이 있습니다. 이전 리뷰와 동일한 이슈로, ApiResponse 래퍼를 명시해주세요.

🔧 제안 수정
                     responseCode = "200",
                     description = "성공",
                     content = `@Content`(
-                            schema = `@Schema`(implementation = PreSignedUrlResponse.class)
+                            schema = `@Schema`(implementation = ApiResponse.class),
+                            examples = `@ExampleObject`(value = """
+                            {
+                              "result": "SUCCESS",
+                              "data": {
+                                "url": "https://...",
+                                "objectUrl": "s3://..."
+                              },
+                              "error": null
+                            }
+                            """)
                     )
             ),
🧹 Nitpick comments (4)
src/main/java/starlight/adapter/backoffice/mail/webapi/swagger/BackofficeMailApiDoc.java (1)

205-243: 삭제 API에 404 응답 문서 추가를 권장합니다.

존재하지 않는 templateId로 삭제 요청 시 반환될 404 응답이 문서화되어 있지 않습니다. API 소비자가 에러 핸들링을 할 때 유용한 정보가 됩니다.

♻️ 404 응답 추가 제안
     `@ApiResponses`({
             `@io.swagger.v3.oas.annotations.responses.ApiResponse`(
                     responseCode = "200",
                     description = "성공",
                     content = `@Content`(examples = `@ExampleObject`(
                             value = """
                             {
                               "result": "SUCCESS",
                               "data": "템플릿이 삭제되었습니다.",
                               "error": null
                             }
                             """
                     ))
             ),
+            `@io.swagger.v3.oas.annotations.responses.ApiResponse`(
+                    responseCode = "404",
+                    description = "템플릿 미존재",
+                    content = `@Content`(examples = `@ExampleObject`(
+                            value = """
+                            {
+                              "result": "ERROR",
+                              "data": null,
+                              "error": {
+                                "code": "MAIL_TEMPLATE_NOT_FOUND",
+                                "message": "해당 메일 템플릿을 찾을 수 없습니다."
+                              }
+                            }
+                            """
+                    ))
+            ),
             `@io.swagger.v3.oas.annotations.responses.ApiResponse`(
                     responseCode = "500",
                     description = "템플릿 삭제 실패",
src/main/java/starlight/adapter/backoffice/expert/webapi/swagger/BackofficeExpertApiDoc.java (2)

171-174: @Parameter 어노테이션 추가를 고려해 보세요.

Swagger UI에서 경로 변수에 대한 설명을 제공하려면 @Parameter 어노테이션을 추가하는 것이 좋습니다. 이는 API 문서의 가독성을 높여줍니다.

♻️ 개선 제안
+import io.swagger.v3.oas.annotations.Parameter;
 `@GetMapping`("/{expertId}")
 ApiResponse<BackofficeExpertDetailResponse> detail(
-        `@PathVariable` Long expertId
+        `@Parameter`(description = "전문가 ID") `@PathVariable` Long expertId
 );

다른 메서드들의 @PathVariable expertId에도 동일하게 적용할 수 있습니다.


252-256: ApiResponse<?> 대신 ApiResponse<Void> 사용을 고려해 보세요.

응답 본문이 없는 경우 ApiResponse<?>보다 ApiResponse<Void>를 사용하면 API 계약이 더 명확해집니다. 이는 update, updateActiveStatus, updateProfileImage, delete 메서드 모두에 해당됩니다.

♻️ 개선 제안
-ApiResponse<?> update(
+ApiResponse<Void> update(
         `@PathVariable` Long expertId,
         `@RequestBody` BackofficeExpertUpdateRequest request
 );

동일하게 다른 메서드들에도 적용:

  • updateActiveStatusApiResponse<Void>
  • updateProfileImageApiResponse<Void>
  • deleteApiResponse<Void>
src/main/java/starlight/adapter/backoffice/image/webapi/BackofficeImageController.java (1)

28-35: 인프라 구현체 직접 주입은 결합도를 높입니다
웹 어댑터가 NcpPresignedUrlProvider에 직접 의존하면 교체/테스트가 어려워집니다. 가능하면 포트 인터페이스(예: PresignedUrlProviderPort)로 의존성을 역전해 주세요.

Comment on lines 3 to 15
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import starlight.adapter.aireport.infrastructure.storage.NcpPresignedUrlProvider;
import starlight.adapter.backoffice.image.webapi.dto.request.BackofficeImagePublicRequest;
import starlight.adapter.backoffice.image.webapi.swagger.BackofficeImageApiDoc;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "BackofficeImageController.java" -type f

Repository: StartUpLight/STARLIGHT_BE

Length of output: 157


🏁 Script executed:

find . -name "*BackofficeImage*" -type f | head -20

Repository: StartUpLight/STARLIGHT_BE

Length of output: 354


🏁 Script executed:

find . -path "*backoffice/image*" -name "*.java" -type f

Repository: StartUpLight/STARLIGHT_BE

Length of output: 549


🏁 Script executed:

cat -n ./src/main/java/starlight/adapter/backoffice/image/webapi/BackofficeImageController.java

Repository: StartUpLight/STARLIGHT_BE

Length of output: 2324


🏁 Script executed:

cat -n ./src/main/java/starlight/adapter/backoffice/image/webapi/dto/request/BackofficeImagePublicRequest.java

Repository: StartUpLight/STARLIGHT_BE

Length of output: 315


🏁 Script executed:

cat -n ./src/main/java/starlight/adapter/backoffice/image/webapi/validation/ValidImageFileName.java

Repository: StartUpLight/STARLIGHT_BE

Length of output: 915


🏁 Script executed:

cat -n ./src/main/java/starlight/adapter/backoffice/image/webapi/swagger/BackofficeImageApiDoc.java

Repository: StartUpLight/STARLIGHT_BE

Length of output: 7963


컨트롤러 구현에 검증 애노테이션 추가 필요

인터페이스(BackofficeImageApiDoc)에만 @ValidImageFileName@Valid 애노테이션이 정의되어 있고, 실제 구현체(BackofficeImageController)에는 누락되어 있습니다. Spring은 인터페이스의 애노테이션을 구현체에 자동으로 상속하지 않으므로, 컨트롤러 메서드의 파라미터에 명시적으로 검증 애노테이션을 추가해야 @Validated와 함께 런타임 검증이 정상 동작합니다.

🔧 제안 수정
+import jakarta.validation.Valid;
 import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import lombok.RequiredArgsConstructor;
 import org.springframework.http.MediaType;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import starlight.adapter.aireport.infrastructure.storage.NcpPresignedUrlProvider;
 import starlight.adapter.backoffice.image.webapi.dto.request.BackofficeImagePublicRequest;
+import starlight.adapter.backoffice.image.webapi.validation.ValidImageFileName;
 import starlight.adapter.backoffice.image.webapi.swagger.BackofficeImageApiDoc;
 import starlight.shared.apiPayload.response.ApiResponse;
 import starlight.shared.dto.infrastructure.PreSignedUrlResponse;
@@
     public ApiResponse<PreSignedUrlResponse> getPresignedUrl(
-            `@RequestParam` String fileName
+            `@RequestParam` `@ValidImageFileName` String fileName
     ) {
         return ApiResponse.success(presignedUrlProvider.getPreSignedUrl(BACKOFFICE_USER_ID, fileName));
     }
@@
     public ApiResponse<String> finalizePublic(
-            `@RequestBody` BackofficeImagePublicRequest request
+            `@Valid` `@RequestBody` BackofficeImagePublicRequest request
     ) {
         return ApiResponse.success(presignedUrlProvider.makePublic(request.objectUrl()));
     }
🤖 Prompt for AI Agents
In
`@src/main/java/starlight/adapter/backoffice/image/webapi/BackofficeImageController.java`
around lines 3 - 15, The controller implementation BackofficeImageController is
missing validation annotations that live on the interface BackofficeImageApiDoc,
so Spring won’t run parameter validation; add `@Validated` at the class level (if
not already present) and explicitly annotate the controller method parameters
with `@Valid` and the custom `@ValidImageFileName` (the same annotations used in
BackofficeImageApiDoc) for the BackofficeImagePublicRequest / filename
parameters so runtime validation is executed; ensure import of javax/
jakarta.validation.Valid and the `@ValidImageFileName` annotation and that method
signatures in BackofficeImageController mirror the annotated parameter types
from BackofficeImageApiDoc.

Comment on lines 95 to 98
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "성공"
),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and read the file in question
fd "BackofficeImageApiDoc.java" --type f

Repository: StartUpLight/STARLIGHT_BE

Length of output: 159


🏁 Script executed:

# Read the file to see lines 95-98 and surrounding context
cat -n src/main/java/starlight/adapter/backoffice/image/webapi/swagger/BackofficeImageApiDoc.java | head -120 | tail -50

Repository: StartUpLight/STARLIGHT_BE

Length of output: 2576


🏁 Script executed:

# Let's see the method name and more context around lines 95-98
cat -n src/main/java/starlight/adapter/backoffice/image/webapi/swagger/BackofficeImageApiDoc.java | sed -n '85,160p'

Repository: StartUpLight/STARLIGHT_BE

Length of output: 3523


🏁 Script executed:

# Let's check other success responses in this file to see if there's a pattern
cat -n src/main/java/starlight/adapter/backoffice/image/webapi/swagger/BackofficeImageApiDoc.java | sed -n '1,100p'

Repository: StartUpLight/STARLIGHT_BE

Length of output: 5275


🏁 Script executed:

# Let's check if there are other ApiResponse annotations in the file to see the pattern
cat -n src/main/java/starlight/adapter/backoffice/image/webapi/swagger/BackofficeImageApiDoc.java

Repository: StartUpLight/STARLIGHT_BE

Length of output: 7963


🏁 Script executed:

# Let's find the ApiResponse class to understand its structure
fd "ApiResponse.java" --type f

Repository: StartUpLight/STARLIGHT_BE

Length of output: 136


🏁 Script executed:

# Read the ApiResponse class to understand its structure
cat -n src/main/java/starlight/shared/apiPayload/response/ApiResponse.java

Repository: StartUpLight/STARLIGHT_BE

Length of output: 1039


🏁 Script executed:

# Let's search for other Swagger documentation files to see the pattern
fd "ApiDoc.java" --type f | head -10

Repository: StartUpLight/STARLIGHT_BE

Length of output: 906


🏁 Script executed:

# Let's check another ApiDoc file to see if there's a pattern for success responses with String return
cat -n src/main/java/starlight/adapter/backoffice/mail/webapi/swagger/BackofficeMailApiDoc.java | grep -A 10 "responseCode = \"200\"" | head -30

Repository: StartUpLight/STARLIGHT_BE

Length of output: 1625


🏁 Script executed:

# Let's check the actual endpoint implementation to see what it returns
fd "BackofficeImageController.java" --type f

Repository: StartUpLight/STARLIGHT_BE

Length of output: 155


🏁 Script executed:

# Read the controller file to see the actual implementation
cat -n src/main/java/starlight/adapter/backoffice/image/webapi/BackofficeImageController.java

Repository: StartUpLight/STARLIGHT_BE

Length of output: 2324


🏁 Script executed:

# Let's also check how ResultType is defined to better understand the response structure
fd "ResultType.java" --type f

Repository: StartUpLight/STARLIGHT_BE

Length of output: 135


🏁 Script executed:

# Read the ResultType file
cat -n src/main/java/starlight/shared/apiPayload/response/ResultType.java

Repository: StartUpLight/STARLIGHT_BE

Length of output: 196


200 성공 응답 스키마를 문서에 추가하세요

finalizePublicApiResponse<String>을 반환하지만 현재 200 응답 주석에 content/schema가 없어 Swagger 문서에서 응답 바디가 표시되지 않습니다. 같은 파일의 getPresignedUrl 메서드처럼 content를 추가하거나, BackofficeMailApiDocApiResponse<String> 사례처럼 예시를 통해 스키마를 명시해야 합니다.

🔧 제안 수정
             `@io.swagger.v3.oas.annotations.responses.ApiResponse`(
                     responseCode = "200",
-                    description = "성공"
+                    description = "성공",
+                    content = `@Content`(examples = `@ExampleObject`(
+                            value = """
+                            {
+                              "result": "SUCCESS",
+                              "data": "PUBLIC_OK",
+                              "error": null
+                            }
+                            """
+                    ))
             ),
🤖 Prompt for AI Agents
In
`@src/main/java/starlight/adapter/backoffice/image/webapi/swagger/BackofficeImageApiDoc.java`
around lines 95 - 98, The 200 ApiResponse for finalizePublic doesn't declare a
response content/schema so Swagger won't show the response body; update the
ApiResponse on finalizePublic to include content with MediaType
"application/json" (or "text/plain" if appropriate) and a schema for String
(e.g., `@Content`(schema = `@Schema`(implementation = String.class)) or an example
like in BackofficeMailApiDoc), following the pattern used by
getPresignedUrl/BackofficeMailApiDoc to ensure ApiResponse<String> is documented
correctly.

# Conflicts:
#	src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationJpa.java
- 전문가 관리 API: 생성, 조회, 수정, 삭제, 활성 상태 변경 및 Swagger 문서 추가
- 이미지 업로드 API: Presigned URL 발급, 이미지 공개 전환 요청 및 Swagger 문서 추가
- 메일 관리 API: 템플릿 생성, 조회, 발송 및 Swagger 문서 추가
- DTO, validation 어노테이션, 요청/응답 매핑 구현
- API 문서화 및 요청-응답 형식 정의
- EmailMaskingUtils 추가로 수신자 이메일 마스킹 로직 구현
- BackofficeMailContentTypeParser 도입으로 ContentType 파싱 로직 정리/재사용
- 로그 개선: 이메일 수신자의 수량만 로깅하도록 변경
- 로깅 및 데이터 저장 시 예외 처리 개선 (BackofficeException 활용)
- CSRF 쿠키 도메인 설정 외부화 (application-{env}.yaml 수정)
@SeongHo5356 SeongHo5356 force-pushed the SRLT-133-백오피스-전문가-수정 branch from 0379e29 to 67fafed Compare January 26, 2026 06:10
Copy link

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/starlight/bootstrap/SwaggerConfig.java (1)

25-40: 쿠키명을 프로퍼티로 관리하여 설정 유연성을 확보하세요.

현재 코드에서 backofficeSession 스킴의 쿠키명이 "JSESSIONID"로 하드코딩되어 있습니다. 향후 서버 환경설정이나 보안 정책 변경으로 인해 server.servlet.session.cookie.name이 변경되면 Swagger 문서와 실제 쿠키명이 불일치할 수 있습니다. 프로퍼티 주입을 통해 설정의 일관성을 유지하는 것을 권장합니다.

♻️ 쿠키명 프로퍼티 주입 리팩터링
 import io.swagger.v3.oas.models.OpenAPI;
 import io.swagger.v3.oas.models.security.SecurityScheme;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
 `@Configuration`
 public class SwaggerConfig {
+
+    `@Value`("${server.servlet.session.cookie.name:JSESSIONID}")
+    private String sessionCookieName;
 
     `@Bean`
     public OpenAPI openAPI() {
         SecurityScheme securityScheme = new SecurityScheme()
                 .type(SecurityScheme.Type.HTTP)
                 .scheme("bearer")
                 .bearerFormat("JWT")
                 .in(SecurityScheme.In.HEADER)
                 .name("Authorization");
         SecurityScheme backofficeSessionScheme = new SecurityScheme()
                 .type(SecurityScheme.Type.APIKEY)
                 .in(SecurityScheme.In.COOKIE)
-                .name("JSESSIONID");
+                .name(sessionCookieName);
🤖 Fix all issues with AI agents
In
`@src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationJpa.java`:
- Around line 19-22: The class declaration currently defines public class
ExpertApplicationJpaPort while the file is named ExpertApplicationJpa.java,
causing Java compilation error; fix by making the public class name match the
file (either rename the class to ExpertApplicationJpa or rename the file to
ExpertApplicationJpaPort.java), ensuring the public class
ExpertApplicationJpaPort (or ExpertApplicationJpa) and the file name are
consistent and updating any references/imports to the class accordingly.

In
`@src/main/java/starlight/application/backoffice/mail/BackofficeMailSendService.java`:
- Around line 41-47: The failure event published by publishFailureEvent(...) may
never reach AFTER_COMMIT listeners when the surrounding transaction is rolled
back; update the handling so failure events are recorded regardless of rollback
by either (A) moving the listener in BackofficeMailSendLogEventHandler to use
`@TransactionalEventListener`(phase = TransactionPhase.AFTER_COMPLETION) or
TransactionPhase.AFTER_ROLLBACK and add a handler that checks event.success() to
persist failure logs, or (B) publish the failure event outside the transactional
boundary (e.g., from the catch block in BackofficeMailSendService where
publishFailureEvent is called) so the event is emitted after rollback; locate
publishFailureEvent, BackofficeMailSendEvent, and
BackofficeMailSendLogEventHandler to implement one of these fixes.

In `@src/main/java/starlight/domain/expert/enumerate/ExpertActiveStatus.java`:
- Around line 3-13: The enum ExpertActiveStatus declares constants ACTIVE and
INACTIVE with String arguments but lacks an explicit constructor and relies on
Lombok; remove the Lombok annotations (or at least
`@RequiredArgsConstructor/`@Getter) and add an explicit private constructor that
accepts the String description and assigns it to the field, and provide a public
getter for description so the constants (ACTIVE/INACTIVE) compile correctly.
♻️ Duplicate comments (4)
src/main/java/starlight/adapter/backoffice/image/webapi/swagger/BackofficeImageApiDoc.java (2)

29-35: 200 응답 스키마가 실제 ApiResponse 래퍼와 불일치합니다.

컨트롤러는 ApiResponse<PreSignedUrlResponse>를 반환하지만, 문서에는 PreSignedUrlResponse만 명시되어 있습니다. 클라이언트가 응답 형식을 정확히 이해할 수 있도록 ApiResponse 래퍼를 포함한 스키마로 수정해주세요.


95-98: 200 성공 응답에 content/schema가 누락되었습니다.

finalizePublic 메서드는 ApiResponse<String>을 반환하지만, 현재 200 응답에 content가 정의되어 있지 않아 Swagger UI에서 응답 본문이 표시되지 않습니다. getPresignedUrl처럼 예시를 추가해주세요.

src/main/java/starlight/application/expert/ExpertDetailQueryService.java (1)

48-50: 비활성 전문가에 대해 EXPERT_NOT_FOUND를 사용하는 것은 의미적으로 부정확합니다.

전문가가 존재하지만 비활성 상태인 경우와 실제로 존재하지 않는 경우를 구분할 수 없어 디버깅 및 API 명세 측면에서 혼란을 야기할 수 있습니다. EXPERT_NOT_ACTIVE 또는 유사한 에러 타입 추가를 권장합니다.

src/main/java/starlight/domain/expert/entity/Expert.java (1)

174-177: 경력 ID 미존재 시 오류 타입이 모호합니다.

경력 ID가 없을 때 EXPERT_NOT_FOUND를 던지면 전문가가 실제로 존재해도 404로 보일 수 있습니다. 경력 관련 오류 코드(예: EXPERT_CAREER_NOT_FOUND)를 사용해주세요.

🧹 Nitpick comments (9)
src/main/java/starlight/application/backoffice/mail/util/EmailMaskingUtils.java (1)

20-33: 도메인 마스킹 검토를 권장합니다.

현재 구현은 로컬 파트만 마스킹하고 도메인은 그대로 노출합니다. 로그 분석 목적으로는 충분하지만, 더 엄격한 PII 보호가 필요한 경우 도메인 일부도 마스킹하는 것을 고려해볼 수 있습니다.

예: j***@example.comj***@e***.com

현재 접근 방식이 로그 추적성과 개인정보 보호 사이의 적절한 균형을 제공한다면, 현재 구현을 유지해도 무방합니다.

src/main/java/starlight/domain/expert/exception/ExpertErrorType.java (1)

12-14: 에러 타입 네이밍 컨벤션 정합성 확인 필요.

프로젝트 컨벤션이 “도메인명+4자리 숫자”라면 신규 상수도 동일 패턴으로 정리하거나, 별도 errorCode 필드를 두는 방식을 고려해주세요. Based on learnings, 컨벤션 확인 부탁드립니다.

src/main/java/starlight/adapter/backoffice/expert/webapi/swagger/BackofficeExpertApiDoc.java (2)

84-109: 전문가 생성 API의 응답 코드를 201로 변경하는 것을 고려해 주세요.

RESTful API 관례상 리소스 생성 성공 시 200 OK 대신 201 Created를 반환하는 것이 더 명확합니다.

♻️ 제안하는 변경
     `@ApiResponses`({
             `@io.swagger.v3.oas.annotations.responses.ApiResponse`(
-                    responseCode = "200",
+                    responseCode = "201",
                     description = "성공",
                     content = `@Content`(schema = `@Schema`(implementation = BackofficeExpertCreateResponse.class))
             ),

253-256: ApiResponse<?> 대신 구체적인 응답 타입 사용을 권장합니다.

update, updateActiveStatus, updateProfileImage, delete 메서드가 ApiResponse<?>를 반환하고 있습니다. 타입 안정성을 위해 ApiResponse<Void> 또는 구체적인 응답 DTO를 사용하는 것이 좋습니다.

♻️ 제안하는 변경
-    ApiResponse<?> update(
+    ApiResponse<Void> update(
             `@PathVariable` Long expertId,
             `@RequestBody` BackofficeExpertUpdateRequest request
     );

-    ApiResponse<?> updateActiveStatus(
+    ApiResponse<Void> updateActiveStatus(
             `@PathVariable` Long expertId,
             `@RequestBody` BackofficeExpertActiveStatusUpdateRequest request
     );

-    ApiResponse<?> updateProfileImage(
+    ApiResponse<Void> updateProfileImage(
             `@PathVariable` Long expertId,
             `@RequestBody` BackofficeExpertProfileImageUpdateRequest request
     );

-    ApiResponse<?> delete(
+    ApiResponse<Void> delete(
             `@PathVariable` Long expertId
     );

Also applies to: 301-304, 349-352, 381-383

src/main/java/starlight/application/backoffice/expert/provided/dto/input/BackofficeExpertUpdateInput.java (1)

19-43: 정적 팩토리 메서드 of()가 생성자와 동일한 역할을 합니다.

현재 of() 메서드는 단순히 생성자를 호출하는 것 외에 추가 로직이 없습니다. Java record의 canonical constructor를 직접 사용하거나, 향후 검증 로직 추가 계획이 있다면 유지해도 됩니다.

src/main/java/starlight/domain/expert/entity/Expert.java (4)

88-96: 새 객체에 대한 불필요한 clear() 호출

createBackoffice에서 새로 생성된 Expert 객체의 tagscategories는 이미 빈 LinkedHashSet으로 초기화되어 있으므로 clear() 호출이 불필요합니다.

♻️ 제안하는 수정
         if (tags != null && !tags.isEmpty()) {
-            expert.tags.clear();
             expert.tags.addAll(tags);
         }

         if (categories != null && !categories.isEmpty()) {
-            expert.categories.clear();
             expert.categories.addAll(categories);
         }

111-123: workedPeriodmentoringPriceWon에 대한 유효성 검증 고려

nameemailAssert로 검증하고 있지만, workedPeriodmentoringPriceWon에 대한 비즈니스 규칙 검증이 없습니다. 필드 레벨에 @Min(0) 어노테이션이 있지만, 이는 Bean Validation이 활성화된 경우에만 동작합니다. 도메인 로직에서 음수 값을 방지하려면 명시적 검증을 추가하는 것이 좋습니다.

♻️ 제안하는 수정
     public void updateBasicInfo(
             String name, String email, String oneLineIntroduction,
             String detailedIntroduction, Long workedPeriod, Integer mentoringPriceWon
     ) {
         Assert.hasText(name, "name must not be blank");
         Assert.hasText(email, "email must not be blank");
+        Assert.isTrue(workedPeriod == null || workedPeriod >= 0, "workedPeriod must not be negative");
+        Assert.isTrue(mentoringPriceWon == null || mentoringPriceWon >= 0, "mentoringPriceWon must not be negative");
         this.name = name;
         this.email = email;
         this.oneLineIntroduction = oneLineIntroduction;
         this.detailedIntroduction = detailedIntroduction;
         this.workedPeriod = workedPeriod;
         this.mentoringPriceWon = mentoringPriceWon;
     }

152-155: requestedIds 중복 계산

requestedIdssyncCareers 메서드(lines 152-155)와 validateCareerUpdates 메서드(lines 194-197)에서 동일하게 계산됩니다. 검증 메서드가 먼저 호출되므로, validateCareerUpdates에서 계산한 결과를 반환하여 재사용하는 방식을 고려해 볼 수 있습니다.

Also applies to: 194-197


206-212: 변수명 hasInvalidPeriod가 실제 검증 내용과 불일치

hasInvalidPeriod 변수가 orderIndex 검증(null 또는 음수)까지 포함하고 있어 변수명이 실제 검증 내용을 정확히 반영하지 않습니다. 가독성을 위해 변수명을 hasInvalidCareerData로 변경하거나, 검증을 분리하는 것을 고려해주세요.

♻️ 제안하는 수정
-        boolean hasInvalidPeriod = careerUpdates.stream().anyMatch(update ->
+        boolean hasInvalidCareerData = careerUpdates.stream().anyMatch(update ->
                 update.orderIndex() == null
                         || update.orderIndex() < 0
                         || update.careerStartedAt() == null
                         || update.careerEndedAt() == null
                         || update.careerStartedAt().isAfter(update.careerEndedAt())
         );

-        if (hasDuplicateOrderIndex || hasDuplicateIds || hasInvalidPeriod) {
+        if (hasDuplicateOrderIndex || hasDuplicateIds || hasInvalidCareerData) {
             throw new ExpertException(ExpertErrorType.EXPERT_CAREER_INVALID);
         }

Comment on lines +41 to 47
} catch (BackofficeException exception) {
publishFailureEvent(input, contentType, exception.getMessage());
throw new BackofficeException(BackofficeErrorType.INVALID_MAIL_REQUEST);
throw exception;
} catch (Exception exception) {
publishFailureEvent(input, contentType, exception.getMessage());
throw new BackofficeException(BackofficeErrorType.MAIL_SEND_FAILED);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

실패 이벤트가 처리되지 않을 수 있습니다.

publishFailureEvent()가 예외를 다시 던지기 전에 호출되지만, 트랜잭션이 롤백되면 @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) 리스너는 실행되지 않습니다.

현재 흐름:

  1. 예외 발생 → publishFailureEvent() 호출
  2. 예외 재발생 → 트랜잭션 롤백
  3. AFTER_COMMIT 리스너 실행 안됨 → 실패 로그 미저장

다음 해결 방안을 고려해주세요:

🔧 제안하는 수정 방안

방안 1: 실패 이벤트 리스너에 AFTER_ROLLBACK 또는 AFTER_COMPLETION 페이즈 사용

// BackofficeMailSendLogEventHandler에 별도 핸들러 추가
`@TransactionalEventListener`(phase = TransactionPhase.AFTER_COMPLETION)
public void handleFailure(BackofficeMailSendEvent event) {
    if (!event.success()) {
        // 실패 로그 저장
    }
}

방안 2: 실패 이벤트는 트랜잭션 외부에서 발행

- } catch (BackofficeException exception) {
-     publishFailureEvent(input, contentType, exception.getMessage());
-     throw exception;
+ } catch (BackofficeException exception) {
+     throw exception;
  }

그리고 컨트롤러나 AOP에서 실패 이벤트를 발행

🤖 Prompt for AI Agents
In
`@src/main/java/starlight/application/backoffice/mail/BackofficeMailSendService.java`
around lines 41 - 47, The failure event published by publishFailureEvent(...)
may never reach AFTER_COMMIT listeners when the surrounding transaction is
rolled back; update the handling so failure events are recorded regardless of
rollback by either (A) moving the listener in BackofficeMailSendLogEventHandler
to use `@TransactionalEventListener`(phase = TransactionPhase.AFTER_COMPLETION) or
TransactionPhase.AFTER_ROLLBACK and add a handler that checks event.success() to
persist failure logs, or (B) publish the failure event outside the transactional
boundary (e.g., from the catch block in BackofficeMailSendService where
publishFailureEvent is called) so the event is emitted after rollback; locate
publishFailureEvent, BackofficeMailSendEvent, and
BackofficeMailSendLogEventHandler to implement one of these fixes.

Copy link

@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: 1

🤖 Fix all issues with AI agents
In
`@src/main/java/starlight/adapter/backoffice/image/webapi/BackofficeImageController.java`:
- Line 13: BackofficeImageController currently depends on the concrete
NcpPresignedUrlProvider; change it to depend on the port/interface used by image
presigned URL functionality (replace NcpPresignedUrlProvider with the
PresignedUrlProvider interface used by other controllers) in the controller's
constructor/field and imports, mirroring how BackofficeExpertController uses
BackofficeExpertQueryUseCase and BackofficeMailController uses
BackofficeMailSendUseCase; also ensure the existing NcpPresignedUrlProvider
implements that interface and that the Spring bean wiring remains valid after
you update the import and type.
🧹 Nitpick comments (3)
src/main/java/starlight/adapter/shared/infrastructure/storage/NcpPresignedUrlProvider.java (1)

1-1: 공유 패키지로 이동했지만, Port 인터페이스는 여전히 aireport 도메인에 위치합니다.

NcpPresignedUrlProvideradapter.shared로 옮긴 의도는 여러 모듈(backoffice, aireport 등)에서 공통으로 사용하기 위함으로 보입니다. 그런데 구현체가 의존하는 PresignedUrlProviderPort는 아직 starlight.application.aireport.required 패키지에 남아 있습니다.

헥사고날 아키텍처 관점에서, shared adapter가 특정 도메인(aireport)의 application port에 직접 의존하면 모듈 간 결합도가 높아집니다. Port 인터페이스도 shared 또는 공용 application 패키지로 이동하는 것을 검토해 주세요.

#!/bin/bash
# PresignedUrlProviderPort의 현재 위치와 사용처 확인
echo "=== PresignedUrlProviderPort 위치 ==="
fd "PresignedUrlProviderPort.java"

echo ""
echo "=== PresignedUrlProviderPort 참조처 ==="
rg -n "PresignedUrlProviderPort" --type=java

Also applies to: 15-15

src/test/java/starlight/adapter/aireport/infrastructure/storage/NcpPresignedUrlProviderUnitTest.java (1)

1-1: 테스트 파일의 패키지가 이동된 프로덕션 클래스와 불일치합니다.

NcpPresignedUrlProvideradapter.shared.infrastructure.storage로 이동했지만, 이 테스트 파일은 여전히 starlight.adapter.aireport.infrastructure.storage 패키지에 남아 있습니다. import는 Line 17에서 올바르게 업데이트되었으나, 테스트 파일 자체의 패키지 및 디렉토리도 shared로 이동하는 것이 관례적으로 일관됩니다.

현재 상태에서도 컴파일·실행에는 문제가 없지만, 패키지 구조의 일관성을 위해 이동을 권장합니다.

Also applies to: 17-17

src/main/java/starlight/adapter/backoffice/image/webapi/BackofficeImageController.java (1)

26-26: BACKOFFICE_USER_ID = 0L 매직 넘버의 의도를 명확히 해 주세요.

0L이 "백오피스 전용 사용자"를 뜻하는 규칙이라면, 간단한 주석이나 공용 상수로 분리하면 다른 개발자가 의도를 파악하기 쉽습니다. 실제 사용자 ID와 충돌 가능성도 확인이 필요합니다.

@SeongHo5356 SeongHo5356 changed the title Srlt 133 백오피스 전문가 수정 [SRLT-133] 백오피스 전문가 수정기능을 구현한다 Feb 15, 2026
Signed-off-by: seongho5356 <kjeng7897@gmail.com>
Signed-off-by: seongho5356 <kjeng7897@gmail.com>
@SeongHo5356 SeongHo5356 requested a review from 2ghrms February 15, 2026 15:41
@SeongHo5356 SeongHo5356 self-assigned this Feb 15, 2026
@SeongHo5356 SeongHo5356 added 🐞 FIX Something isn't working ✨ FEAT 새로운 기능 구현 labels Feb 15, 2026
Copy link
Member

@2ghrms 2ghrms left a comment

Choose a reason for hiding this comment

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

고생하셨습니다!! 개발가이드 추가된 것 확인했습니다~


public void updateBasicInfo(
String name, String email, String oneLineIntroduction,
String detailedIntroduction, Long workedPeriod, Integer mentoringPriceWon
Copy link
Member

Choose a reason for hiding this comment

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

여기도 가이드에 따라 인자를 멀티라인으로 해주시면 좋을 것 같습니다

@2ghrms 2ghrms merged commit 31848a8 into develop Feb 16, 2026
4 checks passed
@2ghrms 2ghrms deleted the SRLT-133-백오피스-전문가-수정 branch February 16, 2026 07:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ FEAT 새로운 기능 구현 🐞 FIX Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants