[BE-70] 소스 레지스트리 상태 6종 · 전이 기록 (FR-88)

작업 내용 (설계 의도)

근거 TDD: 20260808-지원관리-확장-tdd.md — “방안 5 (소스 상태 → company 합류)”, “상태 전이 표 소스 레지스트리 상태”

변경 사항

현재 소스 상태는 2축 + 별도 테이블입니다 — job_sources.seeded_at·disabled_at(baseline...sql:59-60)와 job_source_health(:152-164). 6종 상태와 “마지막 전이 시각”(PRD Operations 요구)을 담을 자리가 없습니다.

  1. company 컨텍스트에 합류합니다 — job_sources가 company 소유이므로 상태는 그 테이블의 컬럼 확장입니다. 신규 컨텍스트를 만들지 않습니다.
  2. 파생 계산이 아니라 영속 상태로 둡니다 — registry_status·registry_status_changed_at. 파생 계산으로는 “마지막 전이 시각”을 만들 수 없습니다.
  3. 전이 규칙을 JobSourceRegistryStatus.canTransitTo()에 캡슐화합니다 — 등록 → DISCOVERED / 어댑터 없음 → UNSUPPORTED / 수집 성공 → ACTIVE / 연속 실패 3일 미만 → TRANSIENT_FAILURE / 401·403·robots 차단 → ACCESS_RESTRICTED / 사용자 비활성 → DISABLED / DISABLED 해제 → DISCOVERED(바로 ACTIVE로 보내지 않고 재검증부터).
  4. 별도 BROKEN 상태를 만들지 않습니다 — PRD가 6종을 확정했고, 3일 연속 고장 여부는 job_source_health.broken_since가 이미 표현합니다. TRANSIENT_FAILURE를 유지하며 기존 소스 고장 알림(FR-19)은 그대로 동작합니다.
  5. 전이 트리거는 posting(수집 결과)이 알고 전이 실행은 company가 합니다 — 크로스 컨텍스트라 ApplyJobSourceRegistryOutcomeUseCase(application 레이어)가 조합합니다. posting DomainService가 회차 결과를 도메인 값 객체(SourceCollectionOutcome)로 넘기고, application 매퍼가 company 입력으로 변환합니다.
  6. 초기값 산출(데이터 마이그레이션 2단계) — recalculateAll()seeded_at·disabled_at·job_source_health·어댑터 보유 여부로 6종 초기값을 계산합니다. 대상 30행 미만(NFR-1)이라 페이지 순회·청크가 불필요합니다. registry_status IS NULL인 행만 대상이라 재실행 멱등입니다. 이 근거를 코드 주석에 남깁니다.
  7. 피처 플래그 source.registry-status가 OFF면 전이를 기록하지 않습니다(컬럼은 유지).

롤백: 플래그 OFF. 컬럼을 다시 NULL로 되돌리는 UPDATE는 30행이라 즉시 가능합니다.

의존

  • BE-68 (플래그 시드)
  • DB-04 (job_sources 컬럼 2개)

다이어그램

처리 흐름

sequenceDiagram
    participant U as ApplyJobSourceRegistryOutcomeUseCase
    participant P as PostingDomainService
    participant M as SourceOutcomeMapper
    participant C as JobSourceRegistryDomainService
    participant S as JobSource
    U->>P: 수집 회차 결과 조회
    P-->>U: SourceCollectionOutcome (posting 값 객체)
    U->>M: company 입력 값 객체로 변환
    U->>C: applyCollectionOutcome(jobSourceId, outcome)
    C->>S: 현재 상태 × 트리거 → 다음 상태 판정
    alt 전이 없음
        S-->>C: false
    else 전이
        S->>S: registryStatus 변경 + changedAt 기록
        C-->>U: true
    end

클래스 의존

flowchart LR
    subgraph Application["application/company"]
        UC[ApplyJobSourceRegistryOutcomeUseCase]
        Mapper[SourceOutcomeMapper]
        Backfill[BackfillSourceRegistryUseCase]
    end
    subgraph Domain["domain/company"]
        DS[JobSourceRegistryDomainService]
        Status[JobSourceRegistryStatus]
        Source[JobSource]
        Repo[JobSourceRepository]
        Flag[FeatureFlagGateway]
    end
    UC --> Mapper
    UC --> DS
    Backfill --> DS
    DS --> Source
    DS --> Repo
    DS --> Flag
    Source --> Status

테스트 케이스

  • 등록만 되고 수집되지 않은 소스는 DISCOVERED
  • 수집이 성공(fetched_count > 0)하면 ACTIVE로 전이하고 전이 시각이 기록된다
  • ACTIVE 소스가 수집 실패하면 TRANSIENT_FAILURE로 전이한다
  • TRANSIENT_FAILURE 소스가 수집 성공하면 ACTIVE로 복귀한다
  • 403 응답이면 ACCESS_RESTRICTED로 전이한다
  • ACCESS_RESTRICTED 소스가 수집 성공하면 ACTIVE로 복귀한다
  • 어댑터가 없는 플랫폼은 UNSUPPORTED
  • 사용자가 소스를 끄면(disabled_at 설정) DISABLED
  • DISABLED 소스를 다시 켜면 ACTIVE가 아니라 DISCOVERED로 복귀한다
  • 같은 상태로의 전이는 false를 반환하고 전이 시각을 갱신하지 않는다
  • 3일 연속 실패해도 상태는 TRANSIENT_FAILURE를 유지하고 별도 BROKEN 상태가 생기지 않는다
  • 3일 연속 실패 시 기존 소스 고장 알림(FR-19)이 그대로 발송된다 (회귀)
  • 백필을 실행하면 registry_status IS NULL인 소스가 0건이 된다
  • 백필을 두 번 실행하면 두 번째는 처리 건수가 0이다 (멱등)
  • 피처 플래그 OFF면 전이가 기록되지 않고 컬럼 값이 유지된다