[BE-73] 변경 공고 알림 — 24시간 1회 제한 (FR-93)

작업 내용 (설계 의도)

근거 TDD: 20260808-지원관리-확장-tdd.md — “방안 11 변경 공고 알림의 24시간 제한”

변경 사항

  1. “변경 공고” 알림을 신설합니다 — 공고 내용이 변경됐다는 사실을 알립니다. FR-36이 제외하는 것은 “공고가 CLOSED로 전환됐다는 사실”이고, 내용 변경 통보는 그 범위 밖입니다(PRD 관계 정리).
  2. 24시간 1회 제한을 멱등 키로 표현합니다 — JOB_POSTING:{id}:JOB_POSTING_CHANGED:{kstDayOrdinal(changedAt)}. DAILY_DIGEST가 이미 같은 패턴을 씁니다(IdempotencyKey.kt:37-38, NotificationDispatchDomainService.kt:91-103).
    • 롤링 24시간 판정 대신 KST 일자 서수를 쓰는 근거: 알림 배치가 09:00 하루 1회이므로(application.yml:46) 같은 공고에 두 번 발송될 창이 애초에 없고, 롤링 판정은 매번 발송 이력 조회를 요구하며 기존 멱등 키 규칙을 벗어납니다. 명시적 키는 수동 트리거·재실행 시의 중복을 방어합니다.
  3. 대상 산출JobPostingRepository.findAllChangedSince(baseTime) 신설(대표 && changed_at >= baseTime). 여기에 매칭 성립 && 미지원 필터를 application 레이어가 얹습니다(크로스 컨텍스트 조합).
  4. 메시지ChangedJobPostingTarget.toMessageContent()가 만들고 300자 이내로 조립합니다 — notification_dispatches.message_summary가 VARCHAR(300)입니다(baseline...sql:319).
  5. 피처 플래그 notification.job-posting-changed가 OFF면 대상 산출이 0건입니다.
  6. NotificationDispatchDomainService.planDispatches() 시그니처에 changedJobPostingTargets를 추가합니다. 기존 2종 대상 산출은 변경하지 않습니다.

롤백: 플래그 OFF → 대상 0건.

의존

  • BE-68 (NotificationType.JOB_POSTING_CHANGED enum·플래그 시드)

다이어그램

처리 흐름

sequenceDiagram
    participant S as NotificationDispatchScheduler
    participant U as DispatchPendingNotificationsUseCase
    participant P as PostingDomainService
    participant M as MatchResultQueryDomainService
    participant A as ApplicationQueryDomainService
    participant D as NotificationDispatchDomainService
    S->>U: execute()
    U->>P: findAllChangedSince(baseTime)
    U->>M: findAllBy(ids) — 매칭 성립만
    U->>A: findAllByJobPostingIn(ids) — 미지원만
    U->>D: planDispatches(신규·고장·변경 대상)
    D->>D: 멱등 키 JOB_POSTING:{id}:JOB_POSTING_CHANGED:{일자서수}
    D->>D: 이미 발송이면 SKIPPED
    D-->>U: DispatchOutcome

클래스 의존

flowchart LR
    subgraph Application["application/notification"]
        UC[DispatchPendingNotificationsUseCase]
        Mapper[NotificationTargetMapper]
    end
    subgraph Domain["domain/notification"]
        DS[NotificationDispatchDomainService]
        Target[ChangedJobPostingTarget]
        Key[IdempotencyKey]
        Type[NotificationType]
        Flag[FeatureFlagGateway]
    end
    UC --> Mapper
    UC --> DS
    Mapper --> Target
    DS --> Key
    DS --> Flag
    Key --> Type

테스트 케이스

  • 변경 감지된 매칭 공고에 변경 알림이 1건 발송된다
  • 같은 공고가 하루에 2회 변경돼도 알림이 1건이다 (KST 일자 서수 멱등)
  • 다음 날 다시 변경되면 알림이 1건 더 발송된다
  • 매칭되지 않은 공고는 변경 알림 대상이 아니다
  • 이미 지원한 공고는 변경 알림 대상이 아니다
  • 비대표(중복) 공고는 변경 알림 대상이 아니다
  • 변경되지 않은 공고는 대상이 아니다 (0건 경계)
  • 메시지 요약이 300자를 넘지 않는다 (컬럼 상한)
  • 피처 플래그 OFF면 대상이 0건이다
  • 발송 실패 시 idempotency_key가 NULL로 남아 다음 배치가 재시도한다
  • 기존 신규 공고·소스 고장 알림 동작이 변경되지 않는다 (회귀)
  • 마감(CLOSED) 전환 자체로는 알림이 발송되지 않는다 (FR-36 유지)