Skip to content

[FEAT] 포트폴리오 수합 요청 공개 알림 Producer 추가 #331

Description

@d1ng1724

Sub-issue of #192

기능 배경 또는 문제

#192(PORTFOLIO_REQUEST·MEMBER_APPROVAL 알림 대상 접근 판정 추가)의 실제 블로커 중 하나다. #192 코멘트(2026-09-02)에서 정리한 대로, 현재 저장소에는 targetType = PORTFOLIO_REQUEST인 알림을 만드는 producer가 하나도 없다.

develop(9286c7b) 기준 확인 결과:

  • NotificationType의 15개 값 중 포트폴리오에 대응하는 값이 없다 (domain/notification/entity/type/NotificationType.kt)
  • domain/notification/event의 Listener 12개에 포트폴리오 관련 producer가 없다
  • domain/portfolioevent Package가 없고, PortfolioRequestServiceImpl은 Domain Event를 발행하지 않는다

#191이 확정한 Producer/수신자 정책은 JOB_*·PROGRAM_* 계열이라 포트폴리오는 범위 밖이었다. producer가 없는 상태에서 #192의 Resolver 분기만 추가하면 실행되지 않는 코드가 된다.

사용자 관점의 요구사항

As a 학생
I want 나를 대상으로 하는 포트폴리오 수합 요청이 공개되면 알림을 받고
So that 마감(dueAt) 전에 제출해야 할 요청을 놓치지 않는다

제안하는 해결 방법

InquiryAnsweredNotificationListener / ProgramDeletedNotificationListener와 같은 구조로 포트폴리오 수합 요청 공개 알림을 추가한다.

  1. domain/portfolio/eventPortfolioRequestPublishedEvent 신설 (@NamedInterface)
  2. PortfolioRequestServiceImpl.changeStatusDRAFT -> PUBLISHED 전이에서 발행
  3. NotificationTypePORTFOLIO_REQUEST_PUBLISHED 추가
  4. domain/notification/eventPortfolioRequestPublishedNotificationListener 추가
  5. 수신자(대상 학생 id) 조회용 Query Port를 domain/portfolio/query에 신설

상세 요구사항

  • 수신자: 해당 요청의 portfolio_request_targets.student_member_id 전원. DRAFT는 학생에게 노출되지 않으므로(PortfolioRequestStatus KDoc) 공개 시점이 첫 알림 시점이다.
  • 수신자 조회 방식: 대상 인원 수가 정해져 있지 않으므로 Event Payload에 담지 않고, ProgramDeletedEvent -> ProgramApplicantQueryPort 선례대로 domain/portfolio/query에 Query Port를 두고 notification이 조회한다. Event에는 문구에 필요한 requestId, title, dueAt 정도만 담는다.
  • 의존 방향: Query Port는 반드시 소유 Domain(portfolio)에 둔다. 반대로 두면 portfolio -> notification 의존이 생겨 Event 구독 방향과 겹치고 ModularityTest가 순환 의존으로 실패한다(JobNotificationTargetQueryPort KDoc).
  • 실행 방식: @TransactionalEventListener(AFTER_COMMIT) + notificationTaskExecutor. 수신자가 여러 명이라 공개 API 응답을 막지 않아야 한다(MemberApprovalProcessedNotificationListener 선례).
  • Idempotency: sourceEventType = "PortfolioRequestPublishedEvent", sourceEventId = requestId. UNIQUE 제약이 (recipient_member_id, source_event_type, source_event_id)라 수신자별 1건씩 생성되고 같은 Event 중복 수신은 dedup된다(V34__add_notification_source_event_idempotency.sql).
  • Target: targetType = NotificationTargetType.PORTFOLIO_REQUEST, targetId = requestId. Deep Link 및 접근 판정은 이 Issue 범위가 아니라 후속 Issue에서 붙인다.
  • Push 대상 여부 결정 필요: PushEligibleNotificationTypes.TYPES에 포함할지 결정한다. #190의 "사용자 행동/상태 변화와 직접 관련된 알림" 기준으로는 포함이 타당해 보이나 확정이 필요하다.
  • Discord 발송 여부 결정 필요: 현재 Discord Event Listener는 job/program/inquiry만 있다. 포함 여부를 결정하고, 제외한다면 근거를 KDoc에 남긴다.
  • Migration 불필요: notifications.typetarget_typeVARCHAR(100)이고 CHECK 제약이 없다(V16__align_notifications_for_in_app_api.sql). Enum 값 추가만으로 충분하다.

제외 범위

  • 마감 임박(dueAt) 리마인드 알림, 미제출자 독촉 알림 — 스케줄링 정책 미확정
  • CLOSED 전이 알림, 제출 완료 알림 — 필요 여부 미확정
  • PORTFOLIO_REQUEST 접근 판정(Query Port + Resolver + Deep Link) — 후속 Issue

완료 조건

  • PortfolioRequestPublishedEvent 신설 및 changeStatusDRAFT -> PUBLISHED 전이에서 발행
  • NotificationType.PORTFOLIO_REQUEST_PUBLISHED 추가
  • domain/portfolio/query에 대상 학생 조회 Query Port 신설
  • PortfolioRequestPublishedNotificationListener 구현 (대상 학생 전원 수신, AFTER_COMMIT, Executor 실행, 실패 시 로그만)
  • Push 대상 포함 여부와 Discord 발송 여부를 결정하고 근거를 KDoc에 남김
  • Test: 대상 학생 수만큼 알림 생성 / 같은 Event 중복 수신 시 dedup / 알림 생성 실패가 상태 변경 Transaction에 영향 없음
  • ./gradlew clean test build 통과 (ModularityTest, PackageArchitectureTest 포함)

추가 정보

이 Issue가 닫히기 전까지 #192의 PORTFOLIO_REQUEST 부분은 진행할 수 없다.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ✨ feature새로운 기능을 구현하거나 기존 기능을 확장하는 작업🌿 size: m일반적인 크기의 기능 또는 개선 작업👀 review구현이 완료되어 리뷰 또는 검증을 기다리는 상태🔄 area: eventKafka 및 비동기 이벤트 처리 영역🔔 area: notification앱, 이메일 및 Discord 알림 관련 영역

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions