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/portfolio에 event Package가 없고, PortfolioRequestServiceImpl은 Domain Event를 발행하지 않는다
#191이 확정한 Producer/수신자 정책은 JOB_*·PROGRAM_* 계열이라 포트폴리오는 범위 밖이었다. producer가 없는 상태에서 #192의 Resolver 분기만 추가하면 실행되지 않는 코드가 된다.
사용자 관점의 요구사항
As a 학생
I want 나를 대상으로 하는 포트폴리오 수합 요청이 공개되면 알림을 받고
So that 마감(dueAt) 전에 제출해야 할 요청을 놓치지 않는다
제안하는 해결 방법
InquiryAnsweredNotificationListener / ProgramDeletedNotificationListener와 같은 구조로 포트폴리오 수합 요청 공개 알림을 추가한다.
domain/portfolio/event에 PortfolioRequestPublishedEvent 신설 (@NamedInterface)
PortfolioRequestServiceImpl.changeStatus의 DRAFT -> PUBLISHED 전이에서 발행
NotificationType에 PORTFOLIO_REQUEST_PUBLISHED 추가
domain/notification/event에 PortfolioRequestPublishedNotificationListener 추가
- 수신자(대상 학생 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.type과 target_type은 VARCHAR(100)이고 CHECK 제약이 없다(V16__align_notifications_for_in_app_api.sql). Enum 값 추가만으로 충분하다.
제외 범위
- 마감 임박(
dueAt) 리마인드 알림, 미제출자 독촉 알림 — 스케줄링 정책 미확정
CLOSED 전이 알림, 제출 완료 알림 — 필요 여부 미확정
PORTFOLIO_REQUEST 접근 판정(Query Port + Resolver + Deep Link) — 후속 Issue
완료 조건
추가 정보
이 Issue가 닫히기 전까지 #192의 PORTFOLIO_REQUEST 부분은 진행할 수 없다.
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/portfolio에eventPackage가 없고,PortfolioRequestServiceImpl은 Domain Event를 발행하지 않는다#191이 확정한 Producer/수신자 정책은
JOB_*·PROGRAM_*계열이라 포트폴리오는 범위 밖이었다. producer가 없는 상태에서 #192의 Resolver 분기만 추가하면 실행되지 않는 코드가 된다.사용자 관점의 요구사항
As a 학생
I want 나를 대상으로 하는 포트폴리오 수합 요청이 공개되면 알림을 받고
So that 마감(
dueAt) 전에 제출해야 할 요청을 놓치지 않는다제안하는 해결 방법
InquiryAnsweredNotificationListener/ProgramDeletedNotificationListener와 같은 구조로 포트폴리오 수합 요청 공개 알림을 추가한다.domain/portfolio/event에PortfolioRequestPublishedEvent신설 (@NamedInterface)PortfolioRequestServiceImpl.changeStatus의DRAFT -> PUBLISHED전이에서 발행NotificationType에PORTFOLIO_REQUEST_PUBLISHED추가domain/notification/event에PortfolioRequestPublishedNotificationListener추가domain/portfolio/query에 신설상세 요구사항
portfolio_request_targets.student_member_id전원.DRAFT는 학생에게 노출되지 않으므로(PortfolioRequestStatusKDoc) 공개 시점이 첫 알림 시점이다.ProgramDeletedEvent->ProgramApplicantQueryPort선례대로domain/portfolio/query에 Query Port를 두고notification이 조회한다. Event에는 문구에 필요한requestId,title,dueAt정도만 담는다.portfolio)에 둔다. 반대로 두면portfolio -> notification의존이 생겨 Event 구독 방향과 겹치고ModularityTest가 순환 의존으로 실패한다(JobNotificationTargetQueryPortKDoc).@TransactionalEventListener(AFTER_COMMIT)+notificationTaskExecutor. 수신자가 여러 명이라 공개 API 응답을 막지 않아야 한다(MemberApprovalProcessedNotificationListener선례).sourceEventType = "PortfolioRequestPublishedEvent",sourceEventId = requestId. UNIQUE 제약이(recipient_member_id, source_event_type, source_event_id)라 수신자별 1건씩 생성되고 같은 Event 중복 수신은 dedup된다(V34__add_notification_source_event_idempotency.sql).targetType = NotificationTargetType.PORTFOLIO_REQUEST,targetId = requestId. Deep Link 및 접근 판정은 이 Issue 범위가 아니라 후속 Issue에서 붙인다.PushEligibleNotificationTypes.TYPES에 포함할지 결정한다. #190의 "사용자 행동/상태 변화와 직접 관련된 알림" 기준으로는 포함이 타당해 보이나 확정이 필요하다.job/program/inquiry만 있다. 포함 여부를 결정하고, 제외한다면 근거를 KDoc에 남긴다.notifications.type과target_type은VARCHAR(100)이고 CHECK 제약이 없다(V16__align_notifications_for_in_app_api.sql). Enum 값 추가만으로 충분하다.제외 범위
dueAt) 리마인드 알림, 미제출자 독촉 알림 — 스케줄링 정책 미확정CLOSED전이 알림, 제출 완료 알림 — 필요 여부 미확정PORTFOLIO_REQUEST접근 판정(Query Port + Resolver + Deep Link) — 후속 Issue완료 조건
PortfolioRequestPublishedEvent신설 및changeStatus의DRAFT -> PUBLISHED전이에서 발행NotificationType.PORTFOLIO_REQUEST_PUBLISHED추가domain/portfolio/query에 대상 학생 조회 Query Port 신설PortfolioRequestPublishedNotificationListener구현 (대상 학생 전원 수신,AFTER_COMMIT, Executor 실행, 실패 시 로그만)./gradlew clean test build통과 (ModularityTest,PackageArchitectureTest포함)추가 정보
이 Issue가 닫히기 전까지 #192의
PORTFOLIO_REQUEST부분은 진행할 수 없다.