Skip to content

[FEATURE] [Notification] AI-Slack 이벤트 연동 및 Slack API 개선 #30 - #48

Merged
250ghghghgh merged 31 commits into
developfrom
feature/#30-ai-slack-event
Apr 7, 2026
Merged

250ghghghgh merged 31 commits into
developfrom
feature/#30-ai-slack-event

Conversation

@250ghghghgh

@250ghghghgh 250ghghghgh commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

📌 PR 제목

[FEATURE] [Notification] AI-Slack 이벤트 연동 및 Slack API 개선 #30

✨ 작업 내용

Notification 서비스의 AI/Slack 핵심 로직 구현 및 Gateway 연동, 트랜잭션 경계 개선


🔍 상세 내용

  • RabbitMQ 이벤트(ShipmentCreatedEvent) 수신 후 AI 발송 시한 계산 및 Slack 알림 발송 플로우 구현
  • NotificationOrchestratorService 클래스 레벨 @Transactional 제거 → 외부 API 호출(Gemini, Slack) 중 DB 커넥션 점유 방지
  • GeminiApiConfig WebClient 빈 이름 geminiWebClient로 명시 → 빈 충돌 방지
  • JPAConfig AuditorAware를 X-User-Id 헤더 기반으로 수정 → createdBy, updatedBy 자동 처리
  • OrderInternalClient FeignClient name orderservice로 수정 (Eureka 등록명 일치)
  • Swagger 설정 추가

🔗 관련 이슈

Closes #30


⚠️ 리뷰 포인트 (선택)

  • docker-compose.yml notificationservice에 RabbitMQ 환경변수(RABBITMQ_HOST, RABBITMQ_PORT, RABBITMQ_USERNAME, RABBITMQ_PASSWORD) 명시적 추가로 수정 했습니다. 확인 부탁드립니다.

✅ 체크리스트

  • 코드가 정상적으로 동작합니다.
  • 테스트를 완료했습니다.
  • 코드 스타일을 준수했습니다.

Summary by CodeRabbit

릴리스 노트

  • New Features

    • Slack 메시지 발송 및 AI 기반 데드라인 생성 워크플로우 추가
    • 메시지·AI 로그에 페이징 및 상세 검색 필터 도입
    • 외부 주문 연동 및 Swagger/OpenAPI 문서화 추가
    • Redis 기반 멱등성 처리 및 메시지 소비 핸들러 도입
  • Refactor

    • 역할 기반 접근 제어(권한 검증) 강화
    • 저장소 및 조회 API를 명령형 검색(검색 커맨드 + 페이징)으로 전환
  • Chores

    • 설정·환경 변수, 도커·컴포즈 및 서비스 설정 정비 (Eureka, Redis, RabbitMQ 등)
  • Tests

    • 오케스트레이션 서비스 및 단위 테스트 추가/갱신

250ghghghgh and others added 26 commits April 4, 2026 13:52
…rvice/infrastructure/messaging/dto/ShipmentCreatedEvent.java

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@250ghghghgh 250ghghghgh self-assigned this Apr 7, 2026
@250ghghghgh 250ghghghgh added ✨ Feature 기능 추가 🔨 Refactor 코드 리팩토링 labels Apr 7, 2026
@coderabbitai

coderabbitai Bot commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3fd8145a-3f69-43c5-9a0d-8dc42ee6e0be

📥 Commits

Reviewing files that changed from the base of the PR and between e6dbacd and bc40b87.

📒 Files selected for processing (1)
  • notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderReadModelResponse.java
✅ Files skipped from review due to trivial changes (1)
  • notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderReadModelResponse.java

📝 Walkthrough

Walkthrough

이 변경은 shipment.created 이벤트 수신부터 AI 발송 시한 생성, Slack 자동 발송 및 상태 기록까지의 이벤트 기반 오케스트레이션을 구현합니다. Redis 기반 멱등성 검사와 RabbitMQ 리스너 연동, Order 서비스 조회(Feign), NotificationOrchestratorService를 통한 AiAppService/SlackAppService 연동, 헤더 기반 userId/userRole 권한 검증 추가 및 AI/Slack 로그의 페이징 검색(QueryDSL)·검증·리포지토리 변경이 포함됩니다. 또한 Redis/RedisTemplate, Feign, Swagger/OpenAPI, Eureka 설정 및 docker-compose·application.yaml 관련 인프라/구성 변경이 포함됩니다.

Sequence Diagram(s)

sequenceDiagram
    participant RabbitMQ as RabbitMQ
    participant Handler as ShipmentCreatedHandler
    participant Redis as Redis (idempotency)
    participant Orchestrator as NotificationOrchestratorService
    participant OrderClient as OrderInternalClient (Feign)
    participant AiService as AiAppService
    participant AiRepo as AiLogRepository
    participant SlackService as SlackAppService
    participant SlackRepo as SlackMessageRepository

    RabbitMQ->>Handler: ShipmentCreatedEvent
    Handler->>Redis: setIfAbsent(saga:processed:{eventId})
    alt already processed
        Redis-->>Handler: false
        Handler->>Handler: log warning & return
    else first time
        Redis-->>Handler: true
        Handler->>Orchestrator: handleShipmentCreated(event)
        Orchestrator->>OrderClient: getOrderReadModel(orderId)
        OrderClient-->>Orchestrator: OrderReadModelResponse / (error -> null)
        Orchestrator->>AiService: generateAiLog(GenerateDeadlineCommand)
        AiService->>AiRepo: save(aiLog)
        AiRepo-->>AiService: saved AiLog (id)
        Orchestrator->>SlackService: sendSlackMessage(SendSlackMessageCommand)
        SlackService->>SlackRepo: save(slackMessage)
        SlackRepo-->>SlackService: saved SlackMessage
        alt send success
            SlackService-->>Orchestrator: success
            Orchestrator->>AiRepo: markSlackSendSuccess(aiLogId)
        else send failure
            SlackService-->>Orchestrator: exception
            Orchestrator->>AiRepo: markSlackSendFail(aiLogId)
            Orchestrator-->>Handler: rethrow
            Handler->>Redis: delete(saga:processed:{eventId})
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

✅ Test

Suggested reviewers

  • afterrest
  • soo96
  • zlonce
  • kim-jun-won
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항(AI-Slack 이벤트 연동 및 Slack API 개선)을 명확하게 나타내고 있으며, 관련 이슈 번호(#30)도 포함되어 있습니다.
Description check ✅ Passed PR 설명이 템플릿의 모든 필수 섹션(제목, 작업 내용, 상세 내용, 관련 이슈, 리뷰 포인트, 체크리스트)을 포함하고 있으며 충분히 상세하게 작성되었습니다.
Linked Issues check ✅ Passed PR의 코드 변경사항이 이슈 #30의 모든 주요 요구사항을 충족합니다: (1) ShipmentCreatedEvent 수신 Consumer 구현 [ShipmentCreatedHandler], (2) NotificationOrchestratorService를 통한 AI+Slack 오케스트레이션, (3) AI 발송 시한 생성 및 Slack 메시지 자동 발송, (4) 페이징/검증/리팩토링 완료.
Out of Scope Changes check ✅ Passed 모든 변경사항이 이슈 #30의 범위 내에 있습니다. 이벤트 기반 자동화, 트랜잭션 경계 개선, 빈 충돌 방지, AuditorAware 개선, Swagger 설정 등이 모두 명시된 목표와 일치합니다.

✏️ 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 feature/#30-ai-slack-event

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Notification 서비스에서 shipment.created 이벤트를 수신해 AI 발송 시한을 계산하고 Slack 알림까지 자동 발송하는 오케스트레이션 흐름을 추가하며, Slack/AI 조회 API의 페이징·검색 및 설정(Feign/Eureka/Swagger/Auditing/Redis 등)을 보강합니다.

Changes:

  • RabbitMQ ShipmentCreatedEvent 소비 → AI 생성 → Slack 발송까지 이어지는 NotificationOrchestratorService 및 소비 핸들러(멱등성 처리) 추가
  • Slack/AI 외부 API에 검색/페이징, 요청 검증(@Valid) 및 역할 기반 접근 제어(헤더 기반) 적용
  • Feign/Eureka/Swagger/Gemini WebClient/JPA Auditing/Redis 등 인프라 설정 및 의존성 보강

Reviewed changes

Copilot reviewed 44 out of 47 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
notification-service/src/test/java/com/shipflow/notificationservice/SlackAppServiceTest.java SlackAppService API 시그니처 변경(유저/역할, 페이징 검색) 반영 테스트 수정
notification-service/src/test/java/com/shipflow/notificationservice/NotificationOrchestratorServiceTest.java shipment.created 처리 오케스트레이션 단위 테스트 추가
notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java AiAppService 조회 시그니처 변경(유저/역할) 반영
notification-service/src/main/resources/application.yaml 서비스명/DB/RabbitMQ/Redis/Eureka/Swagger 등 런타임 설정 조정
notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java Slack API에 헤더 기반 사용자정보, 검색/페이징 파라미터, 검증 적용
notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/response/SlackMessageResponse.java 목록 변환 유틸 제거(페이지 기반 응답으로 전환)
notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/UpdateSlackMessageRequest.java 수정 요청 DTO 검증 및 command 변환 시 user/role 포함
notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java 발송 요청 DTO 검증 및 command 변환 시 user/role 포함
notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/NotificationExceptionHandler.java IllegalArgumentException → 공통 API 응답으로 변환하는 예외 처리 추가
notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageRequest.java page/size 기본값 처리 및 기본 정렬 pageable 생성 추가
notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java AI 조회 검색/페이징 파라미터 및 헤더 기반 사용자정보 처리 추가, debug 엔드포인트 분리
notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java AI 생성 요청 스키마 확장(orderId 등) 및 command 변환 변경
notification-service/src/main/java/com/shipflow/notificationservice/NotificationserviceApplication.java Feign 활성화(@EnableFeignClients)
notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java QueryDSL 기반 Slack 메시지 검색/페이징 구현 추가
notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageJpaRepository.java 목록 조회 메서드 제거(검색/페이징 구현으로 대체)
notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogRepositoryImpl.java QueryDSL 기반 AI 로그 검색/페이징 구현 추가
notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogJpaRepository.java 목록 조회 메서드 제거(검색/페이징 구현으로 대체)
notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java shipment.created 이벤트 DTO 확장 및 SagaEvent 상속
notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedListener.java 이벤트 핸들러 위임용 컴포넌트 추가
notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedHandler.java RabbitMQ 리스너 + Redis 멱등성 처리 + 오케스트레이터 호출 구현
notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedConsumer.java 기존 단순 로깅 Consumer 제거
notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderReadModelResponse.java Order read-model 응답 DTO 추가
notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderInternalClient.java orderservice FeignClient 추가(내부 read-model 조회)
notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java WebClient 빈 충돌 방지 위해 Qualifier 기반 주입으로 변경
notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiApiConfig.java gemini 전용 WebClient 빈 명시 및 타임아웃 설정 추가
notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/SlackMessage.java 메시지 업데이트 메서드 위치/주석 정리
notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/repository/SlackMessageRepository.java Slack 메시지 검색/페이징 repository API로 변경
notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/exception/SlackErrorCode.java Slack 접근 거부 에러 코드 추가
notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/repository/AiLogRepository.java AI 로그 검색/페이징 repository API로 변경
notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java AI 접근 거부 에러 코드 추가
notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java 공백/포맷팅 정리
notification-service/src/main/java/com/shipflow/notificationservice/config/SwaggerConfig.java OpenAPI(Swagger) 설정 추가
notification-service/src/main/java/com/shipflow/notificationservice/config/RedisConfig.java RedisTemplate(String,String) 설정 추가(멱등성 키 저장용)
notification-service/src/main/java/com/shipflow/notificationservice/config/JPAConfig.java AuditorAware를 X-User-Id 헤더 기반으로 변경
notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java Slack CRUD에 역할 검증 추가 및 목록 조회를 검색/페이징으로 변경
notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/UpdateSlackMessageCommand.java update command에 userId/userRole 추가
notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SendSlackMessageCommand.java send command에 userId/userRole 추가
notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SearchSlackMessageCommand.java Slack 메시지 검색 command 추가
notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java 이벤트 기반 AI+Slack 오케스트레이션 서비스 추가 및 발송 상태 업데이트 로직 추가
notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/SearchAiLogCommand.java AI 로그 검색 command 추가
notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/GenerateDeadlineCommand.java AI deadline 생성 command 스키마 확장(orderId, quantity 등)
notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java AI 조회/검색에 역할 검증 및 repository search 기반 페이징 적용
notification-service/build.gradle Feign/Eureka/Swagger/Redis 의존성 추가
keycloak/shipflow-export.json Keycloak realm export 업데이트(포맷 포함)
docker-compose.yml notificationservice RabbitMQ 환경변수 추가 및 포트/헬스체크 포맷 정리
common/src/main/java/com/shipflow/common/domain/BaseEntity.java 공백/포맷팅 정리
.gitignore 파일 끝 공백 라인 추가
Comments suppressed due to low confidence (1)

notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java:68

  • getSlackMessage() 시그니처에 userId가 포함되어 있지만 메서드 본문에서 전혀 사용되지 않습니다. 호출부/Command와의 일관성을 위해 제거하거나, 추후 감사/권한 체크에 사용할 계획이면 TODO와 함께 실제 사용(예: 접근 범위 제한, 로깅 등)을 추가해 주세요.
	// 단건 조회
	public SlackMessageResult getSlackMessage(UUID userId, String userRole, UUID slackId) {
		//권한 확인
		validateMasterRole(userRole);
		SlackMessage slackMessage = slackMessageRepository.findByIdAndDeletedAtIsNull(slackId)
			.orElseThrow(() -> new BusinessException(SlackErrorCode.SLACK_MESSAGE_NOT_FOUND));

		return SlackMessageResult.from(slackMessage);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread notification-service/src/main/resources/application.yaml

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (4)
notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/repository/AiLogRepository.java (1)

9-18: 🛠️ Refactor suggestion | 🟠 Major

저장소 인터페이스가 application 계층 DTO에 의존하고 있습니다.
SearchAiLogCommand에는 userId/userRole까지 포함돼 있어서, 현재 시그니처는 조회 필터뿐 아니라 권한 문맥도 domain 저장소 API로 끌고 내려옵니다. 검색 조건 전용 타입을 domain 쪽으로 분리하고 역할 검사는 서비스에서 끝내는 편이 계층 경계를 지키기 좋습니다.

리팩터링 방향 예시
-import com.shipflow.notificationservice.application.ai.dto.command.SearchAiLogCommand;
+import com.shipflow.notificationservice.domain.ai.repository.criteria.AiLogSearchCriteria;
 ...
-	Page<AiLog> search(SearchAiLogCommand command, Pageable pageable);
+	Page<AiLog> search(AiLogSearchCriteria criteria, Pageable pageable);

AiLogSearchCriteria에는 실제 검색 조건만 두고, userId/userRole 기반 권한 검사는 application service에서 끝내는 구성이 더 자연스럽습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/repository/AiLogRepository.java`
around lines 9 - 18, AiLogRepository currently depends on the application DTO
SearchAiLogCommand, which leaks authorization context into the domain layer;
create a domain-specific search type (e.g., AiLogSearchCriteria) containing only
query/filter fields and change the repository signature Page<AiLog>
search(SearchAiLogCriteria criteria, Pageable pageable) in AiLogRepository, then
update the application service to map SearchAiLogCommand → AiLogSearchCriteria
and perform userId/userRole authorization checks there before calling the
repository.
notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/repository/SlackMessageRepository.java (1)

9-18: 🛠️ Refactor suggestion | 🟠 Major

도메인 레이어의 애플리케이션 DTO 의존을 분리해 주세요.

Line 9, Line 18에서 domain의 Repository가 application Command를 참조하고 있어 계층 의존 방향이 역전됩니다. 검색 조건 타입은 domain(또는 infrastructure 전용 포트)으로 이동하는 편이 안전합니다.

리팩터링 예시
- import com.shipflow.notificationservice.application.slack.dto.command.SearchSlackMessageCommand;
+ import com.shipflow.notificationservice.domain.slack.repository.criteria.SlackMessageSearchCriteria;

- Page<SlackMessage> search(SearchSlackMessageCommand command, Pageable pageable);
+ Page<SlackMessage> search(SlackMessageSearchCriteria criteria, Pageable pageable);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/repository/SlackMessageRepository.java`
around lines 9 - 18, The repository SlackMessageRepository currently depends on
application DTO SearchSlackMessageCommand which inverts layer dependencies;
introduce a domain-level search criteria (e.g., SearchSlackMessageCriteria or
SlackMessageSearchSpec) inside the domain (or a domain-facing port) and change
the repository method signature Page<SlackMessage> search(...) to accept that
new type instead of SearchSlackMessageCommand; update any mappers or adapters in
the application layer to convert SearchSlackMessageCommand →
SearchSlackMessageCriteria and adjust implementations of SlackMessageRepository
to consume the domain criteria so the domain layer no longer imports application
DTOs.
notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java (1)

33-64: ⚠️ Potential issue | 🟠 Major

트랜잭션 내에서 외부 API 호출로 인한 실패 상태 미저장 및 연결 점유 문제

Gemini 호출이 @Transactional 경계 내에 있어 예외 발생 시 전체 트랜잭션이 롤백됩니다. aiLog.markFail()로 상태를 변경해도 DB 플러시 없이 예외가 던져지면 저장되지 않으므로 실패 로그가 기록되지 않습니다. 또한 외부 API 호출 중 DB 커넥션이 점유된 상태로 유지되어 커넥션 풀 효율이 저하됩니다.

pending 상태 저장 → 외부 호출 → 상태 업데이트를 분리된 트랜잭션으로 나누고, 외부 호출을 @Transactional 외부로 빼주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java`
around lines 33 - 64, The generateAiLog method currently performs the external
aiGenerator.generate(prompt) call inside a single `@Transactional` method which
causes rollbacks to erase aiLog.markFail() and holds DB connections during the
external call; fix by persisting the initial AiLog in pending state first (use
aiLogRepository.save(aiLog)), then perform the external call outside the
transaction (move the aiGenerator.generate(prompt) call into a non-transactional
context or a separate method without `@Transactional`), and finally update the
AiLog status in a separate transaction (e.g., a method annotated with
`@Transactional` or `@Transactional`(propagation = REQUIRES_NEW) that calls
aiLog.markSuccess(...) or aiLog.markFail() and saves via
aiLogRepository.save(aiLog)); ensure methods referenced (generateAiLog,
aiGenerator.generate, aiLogRepository.save, aiLog.markSuccess, aiLog.markFail)
are reorganized accordingly so pending gets flushed before the external call and
status updates run in their own transactions to release DB connections during
the external API request.
notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java (1)

33-58: ⚠️ Potential issue | 🔴 Critical

Slack API 호출 실패가 성공으로 반환되고 있습니다.

SlackMessage를 먼저 저장한 뒤 외부 Slack API를 호출하는데, BusinessException은 삼켜서 반환값의 sendStatus 필드가 FAIL인데도 호출부(SlackController)에서 확인하지 않아 HTTP 200으로 응답됩니다. 그 외 예외는 전체 트랜잭션이 롤백되어 저장된 기록마저 사라집니다. 또한 markFail()sentAt 타임스탐프를 설정하지 않아 markSuccess()와 불일치합니다.

pending 저장 → 외부 호출 → 성공/실패 마킹을 분리하고, 실패는 호출자에게 명시적으로 반환하거나 예외를 발생시켜 실패 감지를 강제해주세요.

🧹 Nitpick comments (9)
notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java (1)

156-172: 권한 실패 케이스 테스트를 추가해 주세요.

현재는 MASTER 성공 경로만 검증해서 권한 회귀를 잡기 어렵습니다. userRole != MASTER일 때 BusinessException이 발생하는 테스트 1개를 추가하는 것을 권장합니다.

테스트 추가 예시
+	`@Test`
+	void get_fail_non_master_role() {
+		UUID id = UUID.randomUUID();
+		UUID userId = UUID.randomUUID();
+		String userRole = "HUB_MANAGER";
+
+		assertThatThrownBy(() -> aiAppService.getAiLog(userId, userRole, id))
+			.isInstanceOf(BusinessException.class);
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java`
around lines 156 - 172, Add a unit test in AiAppServiceTest that verifies
getAiLog throws BusinessException when the caller role is not MASTER: mock
aiLogRepository.findByIdAndDeletedAtIsNull(id) to return the existing AiLog (as
in the successful test), call aiAppService.getAiLog(userId, "SOME_OTHER_ROLE",
id) and assert that a BusinessException is thrown; ensure the test targets the
getAiLog method and uses the same id/AiLog setup but with userRole != "MASTER"
to validate the permission failure path.
notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SearchSlackMessageCommand.java (1)

15-16: 검색 기간 역전 입력을 조기 차단하는 검증을 권장합니다.

Line 15~16은 createdAtFrom > createdAtTo 케이스를 허용하므로, Command 단계에서 불변식을 잡아두면 하위 쿼리/응답 해석이 더 안정적입니다.

개선 예시
 public record SearchSlackMessageCommand(
 	UUID userId,
 	String userRole,
 	String receiverSlackId,
 	SlackSendStatus sendStatus,
 	SlackMessageType messageType,
 	LocalDateTime createdAtFrom,
 	LocalDateTime createdAtTo
 ) {
+	public SearchSlackMessageCommand {
+		if (createdAtFrom != null && createdAtTo != null && createdAtFrom.isAfter(createdAtTo)) {
+			throw new IllegalArgumentException("createdAtFrom은 createdAtTo보다 이후일 수 없습니다.");
+		}
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SearchSlackMessageCommand.java`
around lines 15 - 16, SearchSlackMessageCommand currently allows createdAtFrom >
createdAtTo which can lead to invalid queries; add a validation in the command
(e.g., in the constructor or a `@PostConstruct/validate` method of
SearchSlackMessageCommand) that checks if createdAtFrom and createdAtTo are both
non-null and throws IllegalArgumentException (or a custom ValidationException)
when createdAtFrom.isAfter(createdAtTo), ensuring the invariant is enforced at
the command level before any query/handler logic runs; reference the
createdAtFrom and createdAtTo fields and the SearchSlackMessageCommand
constructor/factory to locate where to add this check.
notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderInternalClient.java (1)

9-13: Feign 클라이언트에 resilience 패턴 적용 권장

외부 서비스(orderservice) 호출 시 장애 전파를 방지하기 위해 fallback 또는 circuit breaker 설정을 고려해 주세요. 현재 구현은 order-service 장애 시 notification-service로 장애가 전파될 수 있습니다.

♻️ Fallback 적용 예시
-@FeignClient(name = "orderservice")
+@FeignClient(name = "orderservice", fallback = OrderInternalClientFallback.class)
 public interface OrderInternalClient {
 
 	`@GetMapping`("/internal/orders/{orderId}/read-model")
 	OrderReadModelResponse getOrderReadModel(`@PathVariable` UUID orderId);
 }

별도의 OrderInternalClientFallback 클래스를 생성하여 fallback 로직을 구현하거나, application.yml에 Feign timeout 및 retry 설정을 추가하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderInternalClient.java`
around lines 9 - 13, Add a resilience layer to the Feign client to avoid
cascading failures: create a fallback implementation class (e.g.,
OrderInternalClientFallback) that implements OrderInternalClient and provides a
safe default for getOrderReadModel(UUID orderId), then register it with the
FeignClient annotation (or with a Feign builder) and wire any needed bean;
alternatively, configure circuit breaker/timeout/retry policies (e.g.,
Resilience4j or Spring Cloud Circuit Breaker) around the OrderInternalClient
bean and tune Feign timeouts/retries in application.yml so orderservice failures
don’t propagate to notification-service.
notification-service/src/test/java/com/shipflow/notificationservice/NotificationOrchestratorServiceTest.java (2)

75-81: 구체적인 검증 추가 권장

현재 테스트는 assertThatNoException()만 사용하여 예외가 발생하지 않는지만 검증합니다. 더 구체적인 assertion을 추가하면 테스트 품질이 향상됩니다.

♻️ 구체적인 검증 예시
 		// when & then
 		assertThatNoException()
 			.isThrownBy(() -> notificationOrchestratorService.handleShipmentCreated(event));
 
 		verify(orderInternalClient).getOrderReadModel(event.getOrderId());
 		verify(aiAppService).generateAiLog(any());
-		verify(slackAppService).sendSlackMessage(any());
+		verify(slackAppService).sendSlackMessage(argThat(cmd ->
+			cmd.receiverSlackId().equals("U123SLACK") &&
+			cmd.messageType() == SlackMessageType.DEADLINE_ALERT
+		));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@notification-service/src/test/java/com/shipflow/notificationservice/NotificationOrchestratorServiceTest.java`
around lines 75 - 81, The test currently only asserts no exception; enhance it
by adding specific verifications and assertions: assert the expected
interactions on NotificationOrchestratorServiceTest by verifying
notificationOrchestratorService.handleShipmentCreated invoked the expected
collaborators (retain
verify(orderInternalClient).getOrderReadModel(event.getOrderId()),
verify(aiAppService).generateAiLog(any()),
verify(slackAppService).sendSlackMessage(any())) and add stricter checks—e.g.,
verify generateAiLog and sendSlackMessage are called with payloads derived from
event/order (use argument captors to assert fields on the captured objects) and
optionally verify invocation counts or order if important (verify(..., times(1))
or inOrder on aiAppService and slackAppService).

45-104: AI 생성 실패 시나리오 테스트 추가 고려

현재 테스트는 order 조회 실패 시나리오는 있지만, aiAppService.generateAiLog() 호출 실패 시 동작을 검증하는 테스트가 없습니다. 오케스트레이션 서비스의 resilience를 보장하려면 AI 실패 시나리오도 테스트하는 것이 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@notification-service/src/test/java/com/shipflow/notificationservice/NotificationOrchestratorServiceTest.java`
around lines 45 - 104, Add a unit test for
notificationOrchestratorService.handleShipmentCreated that simulates
aiAppService.generateAiLog throwing an exception: mock
orderInternalClient.getOrderReadModel(...) to return a valid
OrderReadModelResponse, mock aiAppService.generateAiLog(any()) to throw(new
RuntimeException("AI failure")), keep
aiLogRepository.findByIdAndDeletedAtIsNull(...) behavior as needed, then
assertThatNoException().isThrownBy(() ->
notificationOrchestratorService.handleShipmentCreated(event)) and verify
orderInternalClient.getOrderReadModel(...) was called and that
slackAppService.sendSlackMessage(...) was still invoked (ensuring the
orchestrator falls back and continues despite AI failure).
notification-service/src/main/java/com/shipflow/notificationservice/config/JPAConfig.java (1)

43-58: 불필요한 예외 throw-catch 패턴 개선 권장

Line 53에서 IllegalStateException을 던지고 Line 56에서 바로 catch하여 SYSTEM_UUID를 반환하는 패턴은 비효율적입니다. 또한 catch-all Exception으로 UUID 파싱 오류 등 모든 예외가 무시되어 디버깅이 어려워질 수 있습니다.

♻️ 개선된 구현
 	`@Bean`
 	public AuditorAware<UUID> auditorAware() {
 		return () -> {
-			try {
-				ServletRequestAttributes attrs =
-					(ServletRequestHolder.getRequestAttributes();
-
-				// 요청 컨텍스트 없는 경우만 SYSTEM_UUID (RabbitMQ 등)
-				if (attrs == null)
-					return Optional.of(SYSTEM_UUID);
-
-				String userId = attrs.getRequest().getHeader("X-User-Id");
-				if (userId == null || userId.isBlank())
-					throw new IllegalStateException("X-User-Id 헤더가 없습니다.");
-
-				return Optional.of(UUID.fromString(userId));
-			} catch (Exception e) {
-				return Optional.of(SYSTEM_UUID);
-			}
+			ServletRequestAttributes attrs =
+				(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
+
+			if (attrs == null) {
+				return Optional.of(SYSTEM_UUID);
+			}
+
+			String userId = attrs.getRequest().getHeader("X-User-Id");
+			if (userId == null || userId.isBlank()) {
+				return Optional.of(SYSTEM_UUID);
+			}
+
+			try {
+				return Optional.of(UUID.fromString(userId));
+			} catch (IllegalArgumentException e) {
+				// 잘못된 UUID 형식 로깅 권장
+				return Optional.of(SYSTEM_UUID);
+			}
 		};
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/config/JPAConfig.java`
around lines 43 - 58, The current code in JPAConfig uses an unnecessary
throw-then-catch and a broad catch(Exception); instead, remove the
IllegalStateException and handle cases explicitly: after obtaining
ServletRequestAttributes from RequestContextHolder, if attrs is null return
Optional.of(SYSTEM_UUID); read the "X-User-Id" header and if it is null or blank
return Optional.of(SYSTEM_UUID) (do not throw); attempt to parse
UUID.fromString(userId) inside a small try-catch that only catches
IllegalArgumentException (or DateTimeParseException if used) and on parse
failure return Optional.of(SYSTEM_UUID); avoid catching Exception broadly and
keep references to ServletRequestAttributes, RequestContextHolder,
UUID.fromString, and SYSTEM_UUID so the change is localized.
notification-service/src/test/java/com/shipflow/notificationservice/SlackAppServiceTest.java (1)

59-61: 다양한 역할에 대한 테스트 케이스 추가 고려

현재 모든 테스트가 "MASTER" 역할만 사용합니다. 서비스 레이어에 validateMasterRole 검증이 있다면, 권한이 없는 역할로 호출 시 예외가 발생하는 케이스도 테스트하면 좋겠습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@notification-service/src/test/java/com/shipflow/notificationservice/SlackAppServiceTest.java`
around lines 59 - 61, Tests in SlackAppServiceTest currently only use the
"MASTER" role; add cases to assert behavior for non-MASTER roles and the
authorization check in validateMasterRole. Add at least one test that sets
userRole to a non-master value (e.g., "USER" or "GUEST") and verifies the
service method under test (the same method(s) currently exercised in
SlackAppServiceTest) throws the expected exception or returns the expected
error, and another test that ensures validateMasterRole enforces access by
calling it directly or via the public service method and asserting the exception
type/message; reference SlackAppServiceTest and the validateMasterRole logic
when locating where to add these tests.
notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java (1)

20-20: quantity는 DTO에서 @Positive로 막는 편이 낫습니다.

지금은 0이나 음수가 컨트롤러 validation을 통과한 뒤 서비스에서 AI_EVENT_INVALID로 떨어집니다. 요청 스키마에서 바로 거르는 쪽이 API 계약이 더 명확합니다.

🔧 수정 예시
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
@@
-	`@NotNull` Integer quantity,
+	`@NotNull` `@Positive` Integer quantity,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java`
at line 20, 요약: GenerateDeadlineRequest의 quantity 필드는 컨트롤러 단에서 0 또는 음수를 차단하도록
`@Positive` 검증을 추가해야 합니다 — 수정: GenerateDeadlineRequest 클래스에서 quantity 선언에
`@Positive` 애노테이션을 추가하거나 `@NotNull` 대신 `@NotNull` `@Positive` 조합을 사용하고
javax.validation.constraints.Positive를 임포트하여 요청 스키마 레벨에서 1 이상의 값만 허용되게 만드세요.
notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java (1)

53-58: Pageablesort가 지금은 전혀 반영되지 않습니다.

현재는 offset/limit만 쓰고 orderBy(createdAt.desc())를 고정해서 호출자의 정렬 조건이 모두 버려집니다. 정렬을 지원할 계획이면 pageable.getSort()를 QueryDSL orderBy로 풀고, 고정 정렬만 허용할 거면 API에서 sort를 막는 편이 계약이 명확합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java`
around lines 53 - 58, The current query always uses
orderBy(slackMessage.createdAt.desc()) and ignores Pageable.getSort(); update
SlackMessageRepositoryImpl so the QueryDSL orderBy honors pageable.getSort() by
converting Pageable.getSort() into QueryDSL OrderSpecifier(s) (mapping property
names to slackMessage fields, e.g., createdAt) and pass them to
queryFactory.selectFrom(slackMessage).where(builder).orderBy(...).offset(...).limit(...);
if implementing sort mapping is not desired, explicitly reject or strip incoming
Sort in the API/Service layer (or validate Pageable) so callers cannot provide
custom sorts—choose one approach and implement it around the query construction
that currently references slackMessage, builder, pageable and createdAt.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java`:
- Around line 43-57: The code currently uses resolveSlackId(event) and will send
a message even when slackId is missing by substituting "확인 필요"; instead, check
the slackId returned by resolveSlackId (and similarly where used around lines
116-119) and if it is null/empty, do not call slackAppService.sendSlackMessage
or construct a SendSlackMessageCommand — instead skip sending and update/leave
the notification state to FAIL or PENDING as appropriate (invoke whatever status
update path you use in NotificationOrchestratorService), ensuring no placeholder
string like "확인 필요" is used as a Slack identifier.
- Around line 63-67: The calls to markSlackSendSuccess and markSlackSendFail
from handleShipmentCreated are self-invocations so their `@Transactional` (e.g.,
`@Transactional`(propagation = Propagation.REQUIRES_NEW)) is not applied; fix by
moving those methods into a separate Spring bean (e.g., SlackSendAuditService)
and inject that bean into NotificationOrchestratorService and call
slackSendAuditService.markSlackSendSuccess(aiId)/markSlackSendFail(aiId), or
alternatively wrap the calls with TransactionTemplate inside
handleShipmentCreated or obtain the proxied instance
(applicationContext.getBean(NotificationOrchestratorService.class)) and call the
methods via that proxy so the REQUIRES_NEW transaction semantics on
markSlackSendSuccess/Fail are actually honored.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedListener.java`:
- Around line 15-17: ShipmentCreatedListener.onShipmentCreated currently lacks
the `@RabbitListener` annotation so it never receives messages; either add
`@RabbitListener`(queues =
NotificationRabbitConfig.QUEUE_NOTIFICATION_SHIPMENT_CREATED) to the
onShipmentCreated(ShipmentCreatedEvent event) method so this class actually
consumes messages and delegates to shipmentCreatedHandler.handle(event), or
remove the dead ShipmentCreatedListener class and keep the existing
`@RabbitListener` on ShipmentCreatedHandler (ensure only one listener remains and
tests/configs reference the retained class).

In
`@notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java`:
- Around line 33-36: The publisher (OrderMessageFlowTest / the OrderMessage
publisher) is not populating the new ShipmentCreatedEvent fields
shipmentManagerSlackId and routes, so notification-service's auto-alert routing
cannot work; update the code that constructs ShipmentCreatedEvent (the
constructor call in OrderMessageFlowTest and any publisher factory methods) to
pass a valid shipmentManagerSlackId string and a non-empty List<RouteInfo> (or
realistic test RouteInfo instances), and if the ShipmentCreatedEvent
constructors/signatures do not accept these new params update the
constructor/overloads accordingly so the publisher actually sets
shipmentManagerSlackId and routes rather than relying on notification-service
defaults.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java`:
- Around line 45-56: Change the X-User-Id handler parameters in AiController
methods (e.g., getAiLog, getAiLogs, and the shown method using
aiAppService.generateAiLog) from String to UUID by declaring
`@RequestHeader`("X-User-Id") UUID userId so Spring will validate/convert headers
and return 400 for bad UUIDs; remove manual UUID.fromString(userId) calls and
pass userId directly to request.toCommand(...) (or other methods) and adjust any
imports/signatures accordingly.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/NotificationExceptionHandler.java`:
- Around line 19-26: The current handler method handleIllegalArgument that
catches IllegalArgumentException is too broad and can mask internal bugs; change
it to handle a dedicated validation exception (e.g., create and throw
ValidationException) or narrow the handler to only controller-layer validation
errors so genuine programming errors are not mapped to
CommonErrorCode.VALIDATION_ERROR; update the `@ExceptionHandler` to reference
ValidationException (or add an additional handler for ValidationException and
remove/limit the IllegalArgumentException handler) and ensure
controllers/services throw ValidationException for input validation failures
rather than IllegalArgumentException.
- Around line 22-25: NotificationExceptionHandler currently returns
e.getMessage() to the client and only logs a short warn line; change the
IllegalArgumentException handler to return a generic client-facing message
(e.g., "Invalid request parameters") via
ApiResponse.fail(CommonErrorCode.VALIDATION_ERROR, "<generic message>",
request.getRequestURI()) instead of e.getMessage(), and log the full exception
(including stack trace) on the server side using the logger (e.g.,
logger.warn("IllegalArgumentException while handling request {}",
request.getRequestURI(), e)) so debugging retains details but sensitive
internals are not exposed to clients.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java`:
- Around line 14-16: Update the Slack ID validation in SendSlackMessageRequest
for the receiverSlackId field to accept Enterprise user IDs by adding the 'W'
prefix to the pattern; specifically modify the `@Pattern` regexp from
"^[UCDG][A-Z0-9]+$" to include W (e.g., "^[UCDGW][A-Z0-9]+$") so Enterprise IDs
starting with 'W' are validated correctly.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java`:
- Around line 44-50: Replace manual UUID parsing in SlackController with
Spring's native conversion by changing method parameters from
`@RequestHeader`("X-User-Id") String userId to `@RequestHeader`("X-User-Id") UUID
userId and remove all UUID.fromString(userId) usages (e.g., in the
sendSlackMessage call where request.toCommand(UUID.fromString(userId), userRole)
is used); apply the same change to the other endpoints in SlackController (the
handlers at the locations corresponding to lines 64, 87, 112, 125) so each
handler accepts UUID userId directly and passes that UUID into toCommand /
service methods (keep X-User-Role as String).

In `@notification-service/src/main/resources/application.yaml`:
- Around line 24-27: Replace the hard-coded Redis host/port in application.yaml
with environment-variable placeholders so the container can connect to an
external Redis; specifically change data.redis.host and data.redis.port to read
from e.g. REDIS_HOST and REDIS_PORT using Spring-style placeholders (with
sensible defaults if desired) so runtime environment controls the Redis endpoint
used by the idempotency logic.

In
`@notification-service/src/test/java/com/shipflow/notificationservice/NotificationOrchestratorServiceTest.java`:
- Around line 182-197: The comment above createEvent currently says
"reflection으로 필드 세팅" but the method actually instantiates a ShipmentCreatedEvent
via its constructor; update or remove the misleading comment so it matches the
implementation (modify the comment above the createEvent method or delete it),
referencing the createEvent method and the ShipmentCreatedEvent constructor
usage to keep the code and comment consistent.

---

Outside diff comments:
In
`@notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java`:
- Around line 33-64: The generateAiLog method currently performs the external
aiGenerator.generate(prompt) call inside a single `@Transactional` method which
causes rollbacks to erase aiLog.markFail() and holds DB connections during the
external call; fix by persisting the initial AiLog in pending state first (use
aiLogRepository.save(aiLog)), then perform the external call outside the
transaction (move the aiGenerator.generate(prompt) call into a non-transactional
context or a separate method without `@Transactional`), and finally update the
AiLog status in a separate transaction (e.g., a method annotated with
`@Transactional` or `@Transactional`(propagation = REQUIRES_NEW) that calls
aiLog.markSuccess(...) or aiLog.markFail() and saves via
aiLogRepository.save(aiLog)); ensure methods referenced (generateAiLog,
aiGenerator.generate, aiLogRepository.save, aiLog.markSuccess, aiLog.markFail)
are reorganized accordingly so pending gets flushed before the external call and
status updates run in their own transactions to release DB connections during
the external API request.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/repository/AiLogRepository.java`:
- Around line 9-18: AiLogRepository currently depends on the application DTO
SearchAiLogCommand, which leaks authorization context into the domain layer;
create a domain-specific search type (e.g., AiLogSearchCriteria) containing only
query/filter fields and change the repository signature Page<AiLog>
search(SearchAiLogCriteria criteria, Pageable pageable) in AiLogRepository, then
update the application service to map SearchAiLogCommand → AiLogSearchCriteria
and perform userId/userRole authorization checks there before calling the
repository.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/repository/SlackMessageRepository.java`:
- Around line 9-18: The repository SlackMessageRepository currently depends on
application DTO SearchSlackMessageCommand which inverts layer dependencies;
introduce a domain-level search criteria (e.g., SearchSlackMessageCriteria or
SlackMessageSearchSpec) inside the domain (or a domain-facing port) and change
the repository method signature Page<SlackMessage> search(...) to accept that
new type instead of SearchSlackMessageCommand; update any mappers or adapters in
the application layer to convert SearchSlackMessageCommand →
SearchSlackMessageCriteria and adjust implementations of SlackMessageRepository
to consume the domain criteria so the domain layer no longer imports application
DTOs.

---

Nitpick comments:
In
`@notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SearchSlackMessageCommand.java`:
- Around line 15-16: SearchSlackMessageCommand currently allows createdAtFrom >
createdAtTo which can lead to invalid queries; add a validation in the command
(e.g., in the constructor or a `@PostConstruct/validate` method of
SearchSlackMessageCommand) that checks if createdAtFrom and createdAtTo are both
non-null and throws IllegalArgumentException (or a custom ValidationException)
when createdAtFrom.isAfter(createdAtTo), ensuring the invariant is enforced at
the command level before any query/handler logic runs; reference the
createdAtFrom and createdAtTo fields and the SearchSlackMessageCommand
constructor/factory to locate where to add this check.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/config/JPAConfig.java`:
- Around line 43-58: The current code in JPAConfig uses an unnecessary
throw-then-catch and a broad catch(Exception); instead, remove the
IllegalStateException and handle cases explicitly: after obtaining
ServletRequestAttributes from RequestContextHolder, if attrs is null return
Optional.of(SYSTEM_UUID); read the "X-User-Id" header and if it is null or blank
return Optional.of(SYSTEM_UUID) (do not throw); attempt to parse
UUID.fromString(userId) inside a small try-catch that only catches
IllegalArgumentException (or DateTimeParseException if used) and on parse
failure return Optional.of(SYSTEM_UUID); avoid catching Exception broadly and
keep references to ServletRequestAttributes, RequestContextHolder,
UUID.fromString, and SYSTEM_UUID so the change is localized.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderInternalClient.java`:
- Around line 9-13: Add a resilience layer to the Feign client to avoid
cascading failures: create a fallback implementation class (e.g.,
OrderInternalClientFallback) that implements OrderInternalClient and provides a
safe default for getOrderReadModel(UUID orderId), then register it with the
FeignClient annotation (or with a Feign builder) and wire any needed bean;
alternatively, configure circuit breaker/timeout/retry policies (e.g.,
Resilience4j or Spring Cloud Circuit Breaker) around the OrderInternalClient
bean and tune Feign timeouts/retries in application.yml so orderservice failures
don’t propagate to notification-service.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java`:
- Around line 53-58: The current query always uses
orderBy(slackMessage.createdAt.desc()) and ignores Pageable.getSort(); update
SlackMessageRepositoryImpl so the QueryDSL orderBy honors pageable.getSort() by
converting Pageable.getSort() into QueryDSL OrderSpecifier(s) (mapping property
names to slackMessage fields, e.g., createdAt) and pass them to
queryFactory.selectFrom(slackMessage).where(builder).orderBy(...).offset(...).limit(...);
if implementing sort mapping is not desired, explicitly reject or strip incoming
Sort in the API/Service layer (or validate Pageable) so callers cannot provide
custom sorts—choose one approach and implement it around the query construction
that currently references slackMessage, builder, pageable and createdAt.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java`:
- Line 20: 요약: GenerateDeadlineRequest의 quantity 필드는 컨트롤러 단에서 0 또는 음수를 차단하도록
`@Positive` 검증을 추가해야 합니다 — 수정: GenerateDeadlineRequest 클래스에서 quantity 선언에
`@Positive` 애노테이션을 추가하거나 `@NotNull` 대신 `@NotNull` `@Positive` 조합을 사용하고
javax.validation.constraints.Positive를 임포트하여 요청 스키마 레벨에서 1 이상의 값만 허용되게 만드세요.

In
`@notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java`:
- Around line 156-172: Add a unit test in AiAppServiceTest that verifies
getAiLog throws BusinessException when the caller role is not MASTER: mock
aiLogRepository.findByIdAndDeletedAtIsNull(id) to return the existing AiLog (as
in the successful test), call aiAppService.getAiLog(userId, "SOME_OTHER_ROLE",
id) and assert that a BusinessException is thrown; ensure the test targets the
getAiLog method and uses the same id/AiLog setup but with userRole != "MASTER"
to validate the permission failure path.

In
`@notification-service/src/test/java/com/shipflow/notificationservice/NotificationOrchestratorServiceTest.java`:
- Around line 75-81: The test currently only asserts no exception; enhance it by
adding specific verifications and assertions: assert the expected interactions
on NotificationOrchestratorServiceTest by verifying
notificationOrchestratorService.handleShipmentCreated invoked the expected
collaborators (retain
verify(orderInternalClient).getOrderReadModel(event.getOrderId()),
verify(aiAppService).generateAiLog(any()),
verify(slackAppService).sendSlackMessage(any())) and add stricter checks—e.g.,
verify generateAiLog and sendSlackMessage are called with payloads derived from
event/order (use argument captors to assert fields on the captured objects) and
optionally verify invocation counts or order if important (verify(..., times(1))
or inOrder on aiAppService and slackAppService).
- Around line 45-104: Add a unit test for
notificationOrchestratorService.handleShipmentCreated that simulates
aiAppService.generateAiLog throwing an exception: mock
orderInternalClient.getOrderReadModel(...) to return a valid
OrderReadModelResponse, mock aiAppService.generateAiLog(any()) to throw(new
RuntimeException("AI failure")), keep
aiLogRepository.findByIdAndDeletedAtIsNull(...) behavior as needed, then
assertThatNoException().isThrownBy(() ->
notificationOrchestratorService.handleShipmentCreated(event)) and verify
orderInternalClient.getOrderReadModel(...) was called and that
slackAppService.sendSlackMessage(...) was still invoked (ensuring the
orchestrator falls back and continues despite AI failure).

In
`@notification-service/src/test/java/com/shipflow/notificationservice/SlackAppServiceTest.java`:
- Around line 59-61: Tests in SlackAppServiceTest currently only use the
"MASTER" role; add cases to assert behavior for non-MASTER roles and the
authorization check in validateMasterRole. Add at least one test that sets
userRole to a non-master value (e.g., "USER" or "GUEST") and verifies the
service method under test (the same method(s) currently exercised in
SlackAppServiceTest) throws the expected exception or returns the expected
error, and another test that ensures validateMasterRole enforces access by
calling it directly or via the public service method and asserting the exception
type/message; reference SlackAppServiceTest and the validateMasterRole logic
when locating where to add these tests.
🪄 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: CHILL

Plan: Pro

Run ID: 1b6f5086-12a1-4309-8a06-908f4ddce377

📥 Commits

Reviewing files that changed from the base of the PR and between ae3056d and 32e3648.

📒 Files selected for processing (47)
  • .gitignore
  • common/src/main/java/com/shipflow/common/domain/BaseEntity.java
  • docker-compose.yml
  • keycloak/shipflow-export.json
  • notification-service/build.gradle
  • notification-service/src/main/java/com/shipflow/notificationservice/NotificationserviceApplication.java
  • notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java
  • notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java
  • notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/GenerateDeadlineCommand.java
  • notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/SearchAiLogCommand.java
  • notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java
  • notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SearchSlackMessageCommand.java
  • notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SendSlackMessageCommand.java
  • notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/UpdateSlackMessageCommand.java
  • notification-service/src/main/java/com/shipflow/notificationservice/config/JPAConfig.java
  • notification-service/src/main/java/com/shipflow/notificationservice/config/RedisConfig.java
  • notification-service/src/main/java/com/shipflow/notificationservice/config/SwaggerConfig.java
  • notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java
  • notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java
  • notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/repository/AiLogRepository.java
  • notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/SlackMessage.java
  • notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/exception/SlackErrorCode.java
  • notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/repository/SlackMessageRepository.java
  • notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java
  • notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiApiConfig.java
  • notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderInternalClient.java
  • notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderReadModelResponse.java
  • notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedConsumer.java
  • notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedHandler.java
  • notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedListener.java
  • notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java
  • notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogJpaRepository.java
  • notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogRepositoryImpl.java
  • notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageJpaRepository.java
  • notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java
  • notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java
  • notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java
  • notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageRequest.java
  • notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/NotificationExceptionHandler.java
  • notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java
  • notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/UpdateSlackMessageRequest.java
  • notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/response/SlackMessageResponse.java
  • notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java
  • notification-service/src/main/resources/application.yaml
  • notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java
  • notification-service/src/test/java/com/shipflow/notificationservice/NotificationOrchestratorServiceTest.java
  • notification-service/src/test/java/com/shipflow/notificationservice/SlackAppServiceTest.java
💤 Files with no reviewable changes (3)
  • notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogJpaRepository.java
  • notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedConsumer.java
  • notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/response/SlackMessageResponse.java

Comment thread notification-service/src/main/resources/application.yaml Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java (1)

50-50: ⚠️ Potential issue | 🔴 Critical

@Transactional이 self-invocation으로 적용되지 않습니다.

Line 50/73/76의 동일 클래스 내부 호출은 프록시를 우회합니다. 그래서 Line 208/215의 트랜잭션 경계가 보장되지 않아 AiLog 상태 변경이 의도대로 커밋되지 않을 수 있습니다. 상태 업데이트 메서드를 별도 빈으로 분리해 호출하세요.

수정 예시 (별도 트랜잭션 빈 분리)
+@Service
+@RequiredArgsConstructor
+public class AiLogStatusService {
+  private final AiLogRepository aiLogRepository;
+
+  `@Transactional`
+  public void markSlackSendSuccess(UUID aiLogId) {
+    AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiLogId)
+      .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND));
+    aiLog.markSendSuccess();
+  }
+
+  `@Transactional`
+  public void markSlackSendFail(UUID aiLogId) {
+    AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiLogId)
+      .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND));
+    aiLog.markSendFail();
+  }
+}
 public class NotificationOrchestratorService {
+  private final AiLogStatusService aiLogStatusService;
 ...
-      markSlackSendFail(aiResult.aiId());
+      aiLogStatusService.markSlackSendFail(aiResult.aiId());
 ...
-      markSlackSendSuccess(aiResult.aiId());
+      aiLogStatusService.markSlackSendSuccess(aiResult.aiId());
 ...
-      markSlackSendFail(aiResult.aiId());
+      aiLogStatusService.markSlackSendFail(aiResult.aiId());
#!/bin/bash
set -euo pipefail

FILE=$(fd -i "NotificationOrchestratorService.java" | head -n1)
echo "Target file: $FILE"

echo "== Calls to status update methods inside orchestrator =="
rg -n "markSlackSend(Success|Fail)\(" "$FILE"

echo
echo "== Transactional annotations on those methods =="
rg -n "@Transactional|void markSlackSend(Success|Fail)\(" "$FILE"

echo
echo "== Relevant code window =="
nl -ba "$FILE" | sed -n '40,225p'

Also applies to: 73-77, 208-220

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java`
at line 50, NotificationOrchestratorService is self-invoking transactional
methods (e.g., markSlackSendFail and markSlackSendSuccess) so `@Transactional` is
bypassed; extract these state-update routines into a separate Spring bean (e.g.,
NotificationStatusUpdater) with `@Component/`@Service and `@Transactional` on its
methods (retain method names like markSlackSendFail(AiId) /
markSlackSendSuccess(...)), inject that bean into
NotificationOrchestratorService and replace internal calls (the current
self-calls to markSlackSendFail/markSlackSendSuccess) with calls to the injected
updater to ensure transaction boundaries are applied and AiLog state changes
commit as intended.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java`:
- Around line 118-122: In NotificationOrchestratorService, do not swallow all
exceptions around orderInternalClient.getOrderReadModel(orderId); replace the
broad try-catch that returns null with a clear policy: catch only expected
exceptions (e.g., transient network errors) and perform a bounded retry with
exponential backoff, log a WARN or ERROR with context (orderId) and the
exception stack for every failure, and for non-recoverable errors rethrow or
return an explicit failure type (e.g., Optional/Result) instead of null so
callers can distinguish UNKNOWN vs hard failure; update the method surrounding
getOrderReadModel to use these specific exception types, retry logic, and
detailed logging.

---

Duplicate comments:
In
`@notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java`:
- Line 50: NotificationOrchestratorService is self-invoking transactional
methods (e.g., markSlackSendFail and markSlackSendSuccess) so `@Transactional` is
bypassed; extract these state-update routines into a separate Spring bean (e.g.,
NotificationStatusUpdater) with `@Component/`@Service and `@Transactional` on its
methods (retain method names like markSlackSendFail(AiId) /
markSlackSendSuccess(...)), inject that bean into
NotificationOrchestratorService and replace internal calls (the current
self-calls to markSlackSendFail/markSlackSendSuccess) with calls to the injected
updater to ensure transaction boundaries are applied and AiLog state changes
commit as intended.
🪄 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: CHILL

Plan: Pro

Run ID: 5c95bcc9-882f-4714-9c35-4fbb71475228

📥 Commits

Reviewing files that changed from the base of the PR and between 32e3648 and 801b19f.

📒 Files selected for processing (5)
  • notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java
  • notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java
  • notification-service/src/main/resources/application.yaml
  • notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java
  • notification-service/src/test/java/com/shipflow/notificationservice/NotificationserviceApplicationTests.java
💤 Files with no reviewable changes (1)
  • notification-service/src/test/java/com/shipflow/notificationservice/NotificationserviceApplicationTests.java
✅ Files skipped from review due to trivial changes (1)
  • notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java
  • notification-service/src/main/resources/application.yaml

@kim-jun-won kim-jun-won left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

고생하셨습니다~

@Jin4041 Jin4041 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

고생하셨습니다!

@250ghghghgh
250ghghghgh merged commit 5781e34 into develop Apr 7, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 추가 🔨 Refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] [Notification] AI-Slack 이벤트 연동 및 Slack API 개선

4 participants