[BE-11] 마감일 경과 판정 경량 배치

작업 내용 (설계 의도)

근거 TDD: 20260722-타깃-공고-알림-및-지원-히스토리-tdd.md — “방안 3 마감 판정”, “상태 전이 표 / JobPosting”

변경 사항

마감일이 이미 지난 공고를 DB 데이터만으로 CLOSED 전환하는 경량 배치입니다(FR-17). 수집 배치(BE-10)와 의도적으로 분리합니다 — 이 판정은 외부 소스 호출이 필요 없으므로 수집이 며칠 깨져 있어도 계속 정확히 동작합니다. 수집 실패에 마감 판정이 종속되지 않게 하는 것이 분리의 목적입니다.

핵심 설계 의도:

  • 상시채용(deadlineAt == null)은 판정 대상에서 제외합니다(FR-17). 도메인의 isPermanentPosting()으로 판정하고, 조회 쿼리에서도 deadline_at IS NOT NULL 조건을 걸어 이중 방어합니다. 어댑터의 마감일 정규화(FR-13)가 제대로 동작하지 않아 9999-12-31이 들어오면 이 배치가 영원히 동작하지 않으므로, 정규화와 이 배치는 짝을 이룹니다.
  • origin과 무관하게 동작합니다 — 수동 등록 공고(FR-20)도 마감일을 입력했으면 경과 시 CLOSED됩니다. 델타 비교와 달리 이 판정은 소스 존재 여부에 의존하지 않으므로 수동 공고를 포함해도 안전합니다.
  • 이미 CLOSED인 공고는 no-op입니다. 물리 삭제는 하지 않습니다(FR-18, NFR-5).
  • 피처 플래그 posting.auto-close를 동일하게 존중합니다 — 마감 판정을 멈춰야 할 때 두 경로(델타·마감일)가 함께 멈춰야 합니다.
  • 실행 시각은 00:30 KST로, 자정 수집(00:00) 이후에 배치합니다. TaskScheduler 풀 크기가 1이라 수집이 길어져도 직렬 실행됩니다.

범위: CloseDeadlinePassedJobPostingsUseCase, JobPostingDeadlineDomainService, JobPostingDeadlineSweepScheduler(cron 0 30 0 * * *).

롤백: 피처 플래그 OFF. 오전환된 공고는 소프트 삭제라 상태 복구로 되돌립니다.

의존

  • BE-02

다이어그램

처리 흐름

sequenceDiagram
    participant S as DeadlineSweepScheduler
    participant U as CloseDeadlinePassedUseCase
    participant D as DeadlineDomainService
    participant F as FeatureFlagGateway
    participant R as JobPostingRepository
    participant P as JobPosting
    S->>U: execute()
    U->>D: closePassed(now)
    D->>F: isEnabled("posting.auto-close")
    alt 플래그 OFF
        F-->>D: false → 0건 처리 후 종료
    else 플래그 ON
        D->>R: findAllOpenWithDeadlineBefore(now)
        loop 대상 공고
            D->>P: closeByDeadline()
            P-->>D: DEADLINE_PASSED 전이 (이미 CLOSED면 no-op)
        end
        D->>R: saveAll(전환 공고)
    end
    D-->>U: 전환 건수

클래스 의존

flowchart LR
    subgraph Presentation["presentation/posting"]
        Sched[DeadlineSweepScheduler]
    end
    subgraph Application["application/posting"]
        UC[CloseDeadlinePassedJobPostingsUseCase]
    end
    subgraph Domain["domain/posting"]
        DS[JobPostingDeadlineDomainService]
        Posting[JobPosting]
        Repo[JobPostingRepository]
        Flag[FeatureFlagGateway]
    end
    Sched --> UC
    UC --> DS
    DS --> Posting
    DS --> Repo
    DS --> Flag

테스트 케이스

  • 마감일이 어제인 OPEN 공고가 CLOSED + closedReason=DEADLINE_PASSED로 전이한다
  • 마감일이 내일인 공고는 전이하지 않는다
  • deadlineAt == null(상시채용) 공고는 판정 대상에서 제외된다
  • 이미 CLOSED인 공고는 다시 처리되지 않고 closedAt이 덮어써지지 않는다
  • 수동 등록 공고도 마감일이 지났으면 CLOSED로 전이한다
  • 소스 수집이 전부 실패한 상태에서도 이 배치는 정상 동작한다
  • 피처 플래그 posting.auto-close=false면 전이 건수가 0이다
  • 마감일 경계값(현재 시각과 동일)에서는 아직 전이하지 않는다
  • 전환 건수가 반환값으로 집계된다