[BE-54] 운영 조회 확장(웹훅 수신·터널 상태) · platform/dedup_key 백필 API

작업 내용 (설계 의도)

근거 TDD: 20260808-지원관리-확장-tdd.md — “Observability”, “Release Scenario 1단계 데이터 마이그레이션 계획”

변경 사항

  1. job_postings.platform·MANUAL dedup_key 백필 APIPOST /api/operations/backfills/posting-platform.
    • Flyway 인라인 백필 DML 금지가 전제입니다. Flyway는 DDL만 하고 값 채우기는 이 배치가 합니다. 대용량 테이블에 단일 UPDATE를 걸면 테이블 전체 락으로 배포가 곧 장애입니다.
    • Spring Batch를 도입하지 않습니다 — 기존 EvaluateJobPostingsUseCase.kt:83-88의 500건 페이지 순회 + 페이지마다 커밋 패턴을 재사용합니다. 청크 커밋·락 범위 축소·멱등이라는 5단계 절차의 취지를 이 패턴이 이미 충족하고, 일생 1회 백필에 메타 테이블 6개는 과합니다.
    • 대상은 platform IS NULL AND posting_origin='COLLECTED' 또는 dedup_key IS NULL AND posting_origin='MANUAL'입니다. 채워진 행은 대상에서 빠지므로 재실행 멱등입니다.
    • platformjob_source_id로 company DomainService에서 배치 조회해 채웁니다(크로스 컨텍스트 조합은 application 레이어).
  2. 웹훅 수신 이력 조회GET /api/operations/webhook-receipts (PRD Operations “수신 건수·서명 검증 실패 건수·사유”).
  3. 터널 상태 확인GET /api/operations/tunnel-status (PRD Operations “터널이 끊기면 웹훅 수신 자체가 불가능하므로 별도 확인 수단”). 설정된 공개 호스트명으로 GET /api/health-probe를 1회 호출(타임아웃 3초)해 왕복 가능 여부를 판정합니다. 호스트명 미설정이면 reachable=false.
  4. /api/health-probe — 인증 제외 경로에 이미 포함된 웹훅 경로와 별개로, 이 경로만 추가로 인증 제외합니다. 응답은 204이며 어떤 정보도 노출하지 않습니다.

파일 소유: OperationApiController.kt에 엔드포인트 3개를 추가합니다. 같은 wave에 이 파일을 건드리는 티켓이 없습니다.

롤백: 백필은 되돌릴 필요가 없습니다(신규 nullable 컬럼을 채울 뿐이며 아직 아무도 읽지 않습니다). 조회 API는 읽기 전용입니다.

의존

  • BE-47 (웹훅 수신 이력 테이블)
  • BE-48 (platform 컬럼·createManual dedupKey 계산)

다이어그램

처리 흐름

sequenceDiagram
    participant Op as 운영자
    participant C as OperationApiController
    participant U as BackfillPostingPlatformUseCase
    participant P as PostingDomainService
    participant Co as CompanyDomainService
    Op->>C: POST /api/operations/backfills/posting-platform
    C->>U: execute()
    loop 500건 페이지
        U->>P: findAllBackfillTargets(page, 500)
        alt 빈 페이지
            P-->>U: [] → 순회 종료
        else 대상 존재
            U->>Co: findAllJobSourcesBy(ids) 배치 1회
            U->>P: saveAll(platform·dedupKey 채운 공고)
        end
    end
    U-->>C: processedCount / remainingNullCount

클래스 의존

flowchart LR
    subgraph Presentation["presentation/operation"]
        Api[OperationApiController]
        Probe[HealthProbeApiController]
    end
    subgraph Application["application/operation"]
        Backfill[BackfillPostingPlatformUseCase]
        Receipts[ListWebhookReceiptsUseCase]
        Tunnel[CheckTunnelStatusUseCase]
    end
    subgraph Domain["domain"]
        PD[PostingDomainService]
        CD[CompanyDomainService]
        WD[WebhookReceiptQueryDomainService]
        TG[TunnelProbeGateway]
    end
    Api --> Backfill
    Api --> Receipts
    Api --> Tunnel
    Backfill --> PD
    Backfill --> CD
    Receipts --> WD
    Tunnel --> TG

테스트 케이스

  • 백필 실행 후 platform IS NULL AND posting_origin='COLLECTED' 건수가 0이 된다
  • 백필 실행 후 MANUAL 공고의 dedup_key가 정규화 회사명+제목으로 채워진다
  • 백필을 두 번 실행하면 두 번째는 처리 건수가 0이다 (멱등)
  • 백필 도중 중단 후 재실행하면 남은 대상만 처리한다
  • 백필이 페이지마다 커밋해 단일 트랜잭션으로 전체를 잠그지 않는다
  • 대상이 0건이면 즉시 종료하고 200을 반환한다 (0건 경계)
  • 백필이 job_sources 조회를 페이지당 1회만 수행한다 (N+1 방지)
  • 웹훅 수신 이력을 result=SIGNATURE_MISMATCH로 필터해 조회할 수 있다
  • 웹훅 수신 이력이 days 범위 밖이면 조회되지 않는다
  • days=31이면 400 BAD_REQUEST다 (경계값)
  • 터널 호스트명이 설정되지 않으면 reachable=false, publicHostname=null이다
  • 터널 호스트가 응답하지 않으면 3초 타임아웃 후 reachable=false를 반환하고 예외를 던지지 않는다
  • /api/health-probe가 인증 없이 204를 반환하고 본문이 비어 있다