[BE-74] 매칭 소급 재평가 백필 · 신규 매칭 공고 알림 억제 (FR-87)

작업 내용 (설계 의도)

근거 TDD: 20260808-지원관리-확장-tdd.md — “방안 6 Spring Batch 미채택”, “Release Scenario 3-5·3-7”

변경 사항

FR-87은 매칭 범위 확대 적용 시 저장된 전체 공고를 새 기준으로 1회 소급 재평가하되 신규 매칭 공고에 알림을 보내지 않을 것을 요구합니다.

  1. POST /api/matching/field-scope-backfills — 실행 시 ① match_criteria_revisionsMATCH_FIELD_SCOPE 1행 삽입(새 revision) ② 500건 페이지 순회로 전 공고 재평가 ③ 이전 결과 미매칭 && 새 결과 매칭인 공고의 알림 억제.
  2. revision 삽입이 트리거입니다 — 기존 재평가 로직이 criteria_revision 비교로 대상을 판별하므로(JobPostingEvaluationDomainService.kt:97-98), 새 revision을 만들면 전 공고가 자동으로 재평가 대상이 됩니다. 별도 대상 목록을 만들지 않습니다.
  3. Spring Batch를 도입하지 않습니다 — 기존 EvaluateJobPostingsUseCase.kt:83-88(500건 페이지)·:48-50·:109-118(대상별 REQUIRES_NEW 격리) 패턴이 청크 커밋·락 범위 축소·멱등이라는 5단계 절차의 취지를 이미 충족합니다. 일생 1회 백필에 메타 테이블 6개는 과합니다. 이 예외 근거를 코드 주석에 남깁니다.
  4. 알림 억제(FR-87) — JobPosting.suppressNotification()으로 notification_eligible=0을 세웁니다. 대상은 “이전 평가에서 미매칭 && 새 평가에서 매칭”인 공고만입니다 — 이미 매칭이던 공고는 건드리지 않습니다.
    • 크로스 컨텍스트(matching 결과로 posting을 수정)라 application 레이어 UseCase가 조합합니다.
    • 이 조치가 없으면 최근 7일 내 발견된 공고가 소급 재평가로 새로 매칭되며 09:00 배치가 알림을 쏟습니다(findAllNotificationCandidates(firstSeenAfter)).
  5. dryRun=true — revision을 만들지 않고 재평가 결과만 계산해 통계를 반환합니다. Release Scenario 3-5에서 오탐 규모를 사전 점검하는 수단입니다. unmatchedNowCount(기존 매칭이 새 기준에서 풀린 건)가 0이어야 안전합니다.
  6. 멱등 — 이미 새 revision으로 평가된 공고는 skip되므로 중단 후 재실행이 안전합니다.
  7. 근거 쓰기량 증가는 실측상 미미합니다 (A-2, dba 실측 정정) — BE-69가 match_score > 0인 전부를 저장하도록 확대됐지만, 전량 재평가 시 추가되는 자식 행은 공고당 평균 +0.09행(6,633건 기준 총 +602행)입니다. 본문 보유 공고가 13.8%뿐이라 확대분 자체가 작습니다. 최초 판정 “자식 쓰기 최대 6.7배”는 과대 추정이었고, 락 범위·소요 시간 모두 사실상 불변입니다. dryRun 사전 점검은 그대로 수행합니다.

롤백 지점: 플래그 matching.field-weighted-scope OFF로 매칭 규칙은 즉시 제목 단독으로 복귀합니다. 단 억제된 notification_eligible=0은 되돌리지 않습니다 — 이는 “알림 과다 발송”이 아니라 “알림 누락” 방향이고, 그 공고들은 이미 목록에 노출되므로 사용자 손실이 없습니다. 이 판단을 티켓·코드 주석에 명시합니다.

의존

  • BE-69 (필드별 가중치 매칭)

다이어그램

처리 흐름

sequenceDiagram
    participant Op as 운영자
    participant C as JobPostingEvaluationApiController
    participant U as BackfillFieldScopeMatchingUseCase
    participant M as MatchCriteriaRepository
    participant E as JobPostingEvaluationDomainService
    participant P as PostingDomainService
    Op->>C: POST /api/matching/field-scope-backfills
    C->>U: execute(dryRun)
    alt dryRun=false
        U->>M: bumpRevision(MATCH_FIELD_SCOPE)
    end
    loop 500건 페이지
        U->>P: 평가 대상 페이지 조회
        U->>E: 이전 결과 조회 → evaluateOne (REQUIRES_NEW)
        alt 미매칭 → 매칭 전환
            U->>P: suppressNotification()
        end
    end
    U-->>C: processed / newlyMatched / suppressed / unmatchedNow

클래스 의존

flowchart LR
    subgraph Presentation["presentation/matching"]
        Api[JobPostingEvaluationApiController]
    end
    subgraph Application["application/matching"]
        UC[BackfillFieldScopeMatchingUseCase]
    end
    subgraph Domain["domain"]
        EDS[JobPostingEvaluationDomainService]
        MRepo[MatchCriteriaRepository]
        PDS[PostingDomainService]
        Posting[JobPosting]
    end
    Api --> UC
    UC --> EDS
    UC --> MRepo
    UC --> PDS
    PDS --> Posting

테스트 케이스

  • 백필 실행 시 MATCH_FIELD_SCOPE revision이 1건 생성된다
  • 전 공고가 새 revision으로 재평가되고 processedCount가 대상 수와 같다
  • 이전 미매칭 → 새 매칭인 공고의 notification_eligible이 0이 된다
  • 이미 매칭이던 공고의 notification_eligible은 변경되지 않는다
  • 억제된 공고가 09:00 알림 배치의 후보에서 제외된다
  • 백필 후 신규 매칭 공고에 대한 알림이 0건이다 (FR-87 핵심)
  • 백필을 재실행하면 이미 새 revision으로 평가된 공고가 skip된다 (멱등)
  • 중단 후 재실행하면 남은 공고만 처리한다
  • dryRun=true면 revision이 생성되지 않고 통계만 반환된다
  • dryRun=truenotification_eligible이 변경되지 않는다
  • unmatchedNowCount가 기존 매칭이 새 기준에서 풀린 건수를 정확히 센다
  • 대상 1건이 실패해도 나머지가 계속 처리된다 (REQUIRES_NEW 격리)
  • 대상이 0건이면 즉시 200을 반환한다 (0건 경계)
  • 페이지마다 커밋해 단일 트랜잭션으로 전체를 잠그지 않는다
  • 미매칭 공고에도 근거가 저장되어 백필 후 자식 행 수가 매칭 성립분만 저장할 때보다 많다 (A-2)
  • 본문이 없는 애그리게이터 공고에는 DESCRIPTION_BODY 근거가 생성되지 않는다 (확대분이 작은 구조적 이유)
  • 근거 쓰기량이 늘어도 페이지 단위 커밋이 유지되어 락 범위가 행 단위다