[BE-19] 운영 조회 API — 수집 실행 이력 · 알림 발송 실패 이력

작업 내용 (설계 의도)

근거 TDD: 20260722-타깃-공고-알림-및-지원-히스토리-tdd.md — “Observability”

변경 사항

별도 모니터링 스택(Prometheus·Grafana)을 도입하지 않는 대신 DB 테이블 자체를 관측 대상으로 삼고 조회 API로 노출합니다. 1인용 로컬 Docker 환경(NFR-7)에서 모니터링 스택의 운영 비용이 가치를 넘습니다.

핵심 설계 의도:

  • 알림 발송 실패 이력 조회는 알림에 의존하지 않는 확인 수단입니다(시나리오 7). 알림 자체가 실패하는 상황을 알림으로 알릴 수 없으므로 조회 화면이 필수입니다. 재시도(다음 배치 자동 재발송)와 조회를 함께 제공하는 것이 시나리오 7의 요구입니다.
  • 수집 실행 이력은 최소 30일 유지합니다(Operations). 소스별 성공/실패·수집 건수·실행 시각·오류 메시지를 반환하며, Success Metrics의 “수집 배치 성공률 30일 롤링 95%“를 이 데이터로 계산합니다.
  • 소스 고장 상태(brokenSince·failureEpisode)와 소스 가드로 마감 판정이 제외된 상태를 함께 노출합니다(Operations).
  • 크로스 컨텍스트 조합은 application 레이어에서 합니다 (no-crosscontext-raw-read). 조회 전용이라도 operation 컨텍스트가 posting·notification 테이블을 직접 읽는 read model(QueryDSL이든 JdbcTemplate이든)을 만들지 않습니다 — 타 컨텍스트 테이블을 직접 읽으면 스키마 결합이 남습니다. 대신 각 소유 컨텍스트가 자기 테이블을 읽는 조회 메서드를 노출하고 operation application UseCase가 조합합니다: ① 수집 이력은 posting 컨텍스트(job_posting_collection_runs 소유)의 조회 서비스가 자기 테이블을 QueryDSL로 읽어 반환, ② 발송 이력은 notification 컨텍스트(notification_dispatches 소유)의 조회 서비스가 자기 테이블을 읽어 반환, ③ sourceLabel(회사명+플랫폼)에 필요한 회사명은 company 컨텍스트 조회로 받아 application 매퍼가 조합, ④ operation application이 각 조각을 operation 응답 DTO로 매핑. operation 도메인/application이 posting·notification 도메인 타입을 시그니처에 두지 않도록 값 객체/응답으로 받습니다. abnormal은 조회 시 계산(파생 값 미저장)합니다.

응답 필드 확정(FE 요청 #9)

  • 수집 이력 아이템: jobSourceId, sourceLabel(회사명 + 플랫폼), runDate, runStatus, fetchedCount, newCount, closedCount, errorMessage, abnormal + 상단 successRate.
  • abnormal은 저장 컬럼이 아니라 조회 시 계산합니다(runStatus=FAILED || fetchedCount=0). DB 설계가 “파생 값을 저장하면 판정 규칙이 코드와 컬럼 두 곳에 생겨 드리프트가 발생한다”고 명시했으므로 이 원칙을 따릅니다.
  • guardApplied는 두지 않습니다(부분 수용) — 소스 가드는 “비정상 회차이면 마감 판정을 건너뛴다”는 규칙이라 abnormal항상 같은 값입니다. 동일 값을 두 필드로 내리면 한쪽 계산만 바뀌었을 때 모순이 생깁니다. FE는 abnormal=true일 때 “마감 판정 제외됨” 문구를 표시합니다. 가드 조건이 비정상 회차 외 요인으로 확장되면 그때 필드를 분리합니다.
  • 알림 이력 아이템: targetType, targetId, notificationType, dispatchSequence, dispatchStatus, attemptCount, lastError, messageSummary, attemptedAt.
  • 삭제·정리 배치를 만들지 않습니다 — 수집 이력을 포함한 모든 데이터를 보존합니다(NFR-5).

범위: OperationApiController(presentation), ListCollectionRunsUseCase·ListNotificationDispatchesUseCase(application — 크로스 컨텍스트 조합), 조회는 각 소유 컨텍스트(posting·notification)의 조회 서비스에 자기 테이블 QueryDSL 조회 메서드를 추가(없으면). operation 컨텍스트 자체의 타 테이블 read model은 두지 않습니다.

의존

  • BE-10 (수집 이력 생성), BE-16 (발송 이력 생성)

다이어그램

처리 흐름

sequenceDiagram
    participant U as 사용자
    participant C as OperationApiController
    participant UC1 as ListCollectionRunsUseCase
    participant UC2 as ListNotificationDispatchesUseCase
    participant R as OperationQueryRepository
    U->>C: GET /api/operations/collection-runs?days=30
    C->>UC1: execute(query)
    UC1->>R: findCollectionRuns(sourceId, from)
    R-->>UC1: 소스별 성공/실패 · 건수 · 오류
    UC1-->>C: 이력 + 성공률 집계
    U->>C: GET /api/operations/notification-dispatches?dispatchStatus=FAILED
    C->>UC2: execute(query)
    UC2->>R: findDispatches(status, from)
    R-->>UC2: 실패 이력 (오류 · 시도 횟수)

클래스 의존

flowchart LR
    subgraph Presentation["presentation/operation"]
        Api[OperationApiController]
    end
    subgraph Application["application/operation"]
        UC1[ListCollectionRunsUseCase]
        UC2[ListNotificationDispatchesUseCase]
    end
    subgraph Domain["domain/operation"]
        Repo[OperationQueryRepository]
        RunView[CollectionRunView]
        DispatchView[NotificationDispatchView]
    end
    subgraph Infra["infrastructure/operation"]
        Impl[OperationQueryRepositoryImpl]
    end
    Api --> UC1
    Api --> UC2
    UC1 --> Repo
    UC2 --> Repo
    Repo --> RunView
    Repo --> DispatchView
    Impl -.->|implements| Repo

테스트 케이스

  • 최근 30일 수집 실행 이력이 소스별·일자별로 조회된다
  • 성공/실패 건수로 30일 롤링 성공률이 계산되어 반환된다
  • 실패 회차의 오류 메시지가 응답에 포함된다
  • runStatus=SUCCESS이고 fetchedCount=0인 회차의 abnormal이 true로 계산된다
  • runStatus=SUCCESS이고 fetchedCount>0인 회차의 abnormal이 false다
  • runStatus=FAILED 회차의 abnormal이 true다
  • 수집 이력 아이템에 sourceLabel(회사명+플랫폼)이 포함된다
  • 소스 고장 상태(brokenSince)와 마감 판정 제외 상태가 함께 표시된다
  • 알림 발송 실패 이력이 시도 횟수·마지막 오류와 함께 조회된다
  • 성공 발송 이력도 필터로 조회할 수 있다
  • 같은 대상의 실패 이력이 여러 건이면 모두 조회된다(NULL 멱등 키 중복 허용 확인)
  • 조회 기간을 벗어난 이력은 반환되지 않는다
  • 이력 삭제 엔드포인트가 존재하지 않는다