확장 안전 공고수집 배치 FE 웹 설계

Background

근거 문서는 타깃 공고 알림 및 지원 히스토리 PRD확장 안전 공고수집 배치 TDD다. TDD는 수집을 짧은 slice로 분할하고, 완전 수집 전에는 마감 판정을 하지 않으며, 운영자가 09:00 전 미완료·예산 소진 상태를 확인할 수 있어야 한다(NFR-2).

Overview

기존 /operations수집 이력 탭을 확장한다. 새 화면·라우트·쓰기 API는 만들지 않는다. 기존 목록의 각 실행 행에 cycle 진행 상태와 제한된 요청량을 보이고, 상단에는 미완료와 예산 소진을 먼저 보이는 읽기 전용 요약을 둔다. 이는 사용자가 실패 소스와 정상적인 안전 중단(BUDGET_EXHAUSTED)을 혼동하지 않게 하는 최소 변경이다.

Terminology

용어FE 표기/의미
완전 수집isComplete=true; 이 경우에만 마감 판정이 반영될 수 있다
미완료PENDING, RUNNING, FAILED, BUDGET_EXHAUSTEDisComplete!==true인 v2 cycle
예산 소진BUDGET_EXHAUSTED; 소스 고장이 아니라 당일 안전 상한에 따른 경고
마감 판정 제외v2 서버 소유 closeGuarded; 서버가 complete-zero/실패/예산 소진을 모두 판정한다. legacy 응답에서만 runStatus !== 'SUCCESS' || abnormal === true로 폴백
SLA snapshot서버가 영속해 반환하는 slaSnapshot 객체. current cycle summary와 독립된 09:00 KST 당시의 사실이다
SLA capture 상태PENDING(09:00 전 점검 예정), CAPTURED, MISSED, NOT_APPLICABLE, UNAVAILABLE; FE는 nullable count로 상태를 만들지 않는다
legacy 실행새 additive 필드가 없는 기존 SUCCESS/FAILED 이력; 기존 runStatus/abnormal 표현을 유지

Define Problem

AS-IS

  • web/src/pages/operations/OperationsPage.tsx#CollectionRunsSection은 최근 30일 성공률과 목록만 렌더한다.
  • web/src/pages/operations/CollectionRunItem.tsxcollectionRunPresentation.ts#describeCollectionRunSUCCESS/FAILEDabnormal만 해석한다. 진행 중·예산 소진·완전성은 구별할 수 없다.
  • web/src/types/api.ts#CollectionRunCollectionRunsResponse에 새 cycle 필드와 top-level summary가 없다.
  • web/src/hooks/operations/useCollectionRuns.ts는 Query를 통한 read-only 조회 경로를 이미 갖고 있어 서버 상태를 별도 전역 스토어로 복사하지 않는다.

TO-BE

  • 같은 GET /api/operations/collection-runs?days=30 응답의 additive fields/summary를 타입으로 소비한다.
  • summary가 있을 때 상단의 읽기 전용 운영 요약을 보이며, 별도 slaSnapshot.captureStatus를 기준으로 09:00 KST SLA 사실을 보인다. 이전 서버 응답에서는 현재 성공률·목록이 그대로 동작한다.
  • v2의 closeGuarded=true인 모든 cycle에는 마감 판정 제외 중을 보인다. FE는 v2 isComplete/abnormal으로 이 값을 추론하지 않는다. BUDGET_EXHAUSTED는 warning으로, FAILED는 danger로 구별한다.
  • 사용자가 원격 상태를 바꾸는 UI, 재실행 버튼, 예산 설정 UI, 새 라우트는 만들지 않는다.

Architecture Benchmarking

제품/사례해결 방식참고할 패턴미참고 사유
Toss Design System짧은 설명과 상태 색으로 중요한 운영 상태를 먼저 구분한 화면 한 과업, 경고는 오류와 분리하고 요약 후 목록 제공차트 중심 대시보드는 개인용 운영 확인에 불필요
GitHub Actions workflow runs실행 이력에서 queued/in-progress/completed 상태를 구분실행 중 상태를 완료/실패로 오인하지 않는 상태 칩실행 상세 드릴다운·취소 액션은 이 API 계약과 범위에 없음

Possible Solutions

방안설명채택 여부사유
기존 성공률·목록만 유지서버가 새 필드를 보내도 FE는 무시미채택예산 소진과 마감 판정 제외를 확인할 수 없어 TDD의 운영 가시성을 충족하지 못한다
별도 운영 대시보드/라우트차트·자동 갱신·제어 UI 추가미채택읽기 전용 additive API만 제공되고, 개인 프로젝트에는 과도하다
기존 운영 탭의 summary + 상태 행 확장현재 Query·컴포넌트 경로에서 타입과 표현만 확장채택기존 진입점과 다크 테마를 재사용하며, 위험 상태를 목록보다 먼저 읽을 수 있다

Detail Design

화면 목록 · 와이어프레임

대상은 기존 S-11 운영 > 수집 이력 한 화면이다. 토스 참고 패턴은 Minimum Features + 명확한 상태 위계다. 경고가 없을 때 summary를 숨겨 평상시 화면의 정보량을 늘리지 않는다.

┌──────────────────────────────────────────────┐
│ 운영                                           │
│ [수집 이력] [알림 실패]                        │
│ 최근 30일 성공률  96.2%                        │
│ ┌ 수집 운영 요약 ───────────────────────────┐ │  ← summary가 있을 때
│ │ 진행 중 2 · 대기 1 · 예산 상한 도달 1      │ │
│ │ 가장 오래 대기 07.30 00:10                 │ │
│ │ 09:00 KST 시점 미완료 3개                   │ │
│ └────────────────────────────────────────────┘ │
│ ┌ 배민 · 사람인                 7.30          ┐ │
│ │ [예산 상한 도달] 목록 5p · 상세 50건         │ │
│ │ 안전 상한으로 중단됨 · 마감 판정 제외 중     │ │
│ └────────────────────────────────────────────┘ │
│ ┌ 당근 · Greenhouse             7.30          ┐ │
│ │ [완료] 12건 · 목록 1p · 상세 12건            │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘

화면 상태

상태UI
loading기존 성공률·summary·행 스켈레톤. 이전 데이터가 있으면 유지하고 별도 전체 로딩 화면으로 전환하지 않는다
empty기존 문구 아직 수집 실행 이력이 없어요 / 첫 수집은 오늘 자정에 진행돼요; summary는 보이지 않는다
error기존 ErrorState다시 시도; 오류 텍스트에 API 내부 세부 정보를 새로 가공하지 않는다
legacy success/failed새 필드가 없으면 현재 [성공], [실패], [0건] 비정상으로 분류됨 표현을 보존한다
v2 NOT_STARTEDneutral 칩 시작 전, 아직 수집을 시작하지 않았어요 텍스트; 서버가 아직 cycle을 시작하지 않은 상태이며 실패/지연으로 표현하지 않는다
v2 pending/runningneutral/accent 칩, 요청량이 있으면 표시, 마감 판정 제외 중 표시
v2 budget exhaustedwarning 칩 예산 상한 도달, 안전 상한으로 중단됨과 마감 제외를 표시; abnormal/소스 고장으로 표시하지 않는다
v2 success/failedcomplete 성공은 positive, 실패는 danger; 서버 closeGuarded=true이면 마감 제외를 표시한다. 따라서 complete된 0건 비정상도 서버 판정대로 제외를 유지한다

상태 표기 규칙

describeCollectionRun을 순수 표현 함수로 확장한다. v2 필드가 존재하면 cycleStatus를 우선하며, 없을 때만 legacy 분기로 폴백한다.

우선 조건tone상세
cycleStatus=PENDING대기neutral다음 수집 대기 및 가능한 경우 nextEligibleAt
cycleStatus=NOT_STARTED시작 전neutral아직 수집을 시작하지 않았어요
cycleStatus=RUNNING진행 중accent목록 Np · 상세 N건
cycleStatus=BUDGET_EXHAUSTED 또는 budgetExhausted=true예산 상한 도달warning안전 상한으로 중단됨 + counts
cycleStatus=SUCCESS완료positivefetchedCount건 + counts
cycleStatus=FAILED실패dangererrorMessage 우선, 없으면 수집을 완료하지 못했어요

v2 closeGuarded서버 소유 nullable 계약이다. 값이 true면 마감 제외를, false 또는 null이면 제외 문구를 숨긴다. JSON에 closeGuarded 키가 존재하는 v2 응답에서는 FE가 isComplete, abnormal, cycleStatus로 가드 여부를 재계산하지 않는다. 이로써 isComplete=true여도 complete-zero를 서버가 closeGuarded=true로 판정하면 반드시 가드한다. closeGuarded 키 자체가 없는 legacy 응답에서만 legacyCloseGuarded = runStatus !== 'SUCCESS' || abnormal === true로 폴백한다. 이 규칙은 BUDGET_EXHAUSTED를 소스 장애로 바꾸지 않으면서 TDD의 complete-only 마감 안전을 정확히 전달한다. continuationPending=true이면 secondary 문구 다음 수집에서 이어서 처리를 추가한다.

09:00 KST SLA 영역은 서버가 영속한 slaSnapshot 객체만 소비한다. FE는 현재 시각 비교, scheduledAt 비교, current cycle 목록 합산을 하지 않는다. captureStatus가 유일한 표시 분기다.

서버 captureStatus표시count/items 처리
PENDINGneutral 09:00 KST 점검 예정 / 점검 시각 전입니다lateBeforeNotificationCount=0, items 없음은 위반 0건이 아니라 아직 capture 전이라는 뜻. scheduledAt은 보조 시각으로만 표시
CAPTUREDpositive 09:00 KST 점검 완료; count 0이면 미완료 없음, 양수면 warning 09:00 KST 시점 미완료 N개capturedAt을 기록 시각으로 보이며, immutable items는 양수일 때 09:00 KST 당시 미완료 소스 목록으로 표시
MISSEDdanger 09:00 KST 점검 누락 / 당시 상태를 기록하지 못했어요count/items를 현재 cycle로 대체하지 않는다. missReason이 있으면 보조 문구로 표시
NOT_APPLICABLEneutral SLA 점검 적용 전count/items 없음. v2 활성화 전·적용 대상이 아닌 날짜라는 서버 판정을 그대로 보인다
UNAVAILABLEwarning SLA snapshot 조회 불가count/items 없음. 데이터가 없음을 0건 또는 누락으로 바꾸지 않는다

BE 최종 계약은 위 다섯 captureStatus를 구별해야 한다. PENDING은 서버가 09:00 KST 전이라고 판정해 준 상태이고, FE가 scheduledAt/브라우저 시간이 앞선다고 추론하는 상태가 아니다.

컴포넌트 트리

OperationsPage
└─ CollectionRunsSection
   ├─ CollectionOperationsSummary (신규, summary가 있을 때만)
   └─ CollectionRunItem
      ├─ Chip (기존)
      └─ CollectionRunProgressDetail (신규 순수 표시 컴포넌트 또는 Item 내부의 짧은 표시)

CollectionOperationsSummary는 표시 전용이다. count를 재계산하거나 source 상태를 추론하지 않고 서버 summary를 그대로 보인다. CollectionRunItem은 카드 표시만 담당하고, 상태 선택·문구 조합은 collectionRunPresentation.ts의 순수 함수로 유지한다. 새 공용 UI primitive나 전역 상태는 필요 없다.

상태 관리

구분대상수단근거
서버 상태collection run items와 summary기존 TanStack Query useCollectionRunsGET 응답이 SSOT이며 다른 저장소 복사는 stale 상태를 만든다
지역 상태기존 활성 탭useState운영 페이지 수명 안에서만 필요하다
전역 상태해당 없음추가하지 않음summary를 다른 화면에 공유할 요구가 없다

useCollectionRuns의 queryKey와 GET URL은 유지한다. 운영 화면이 열려 있는 동안의 별도 polling은 채택하지 않는다. 1일 1회 배치의 관측 화면에 고정 polling을 더하면 외부 요청이 늘고, 사용자는 브라우저 새로고침 또는 기존 오류 재시도로 최신 조회를 할 수 있다.

API 연동

화면/컴포넌트endpoint소비 필드오류 처리
수집 이력 탭GET /api/operations/collection-runs?days=30useCollectionRuns기존 successRate, items + item의 cycleStatus, isComplete, listPageCount, detailRequestCount, attemptCount, nextEligibleAt, leaseUntil, budgetExhausted, continuationPending + top-level summary기존 ErrorState 재시도

계약은 additive로 다음 TypeScript 모델을 확정한다. 신규 item 필드와 summary는 선택적/nullable로 두어 dark deploy 중 구 서버 응답과 기존 이력이 깨지지 않게 한다. 새 write API는 소비하지 않는다.

타입필드
CollectionCycleStatus`‘NOT_STARTED'
CollectionRun additive/nullable 필드`cycleStatus?: CollectionCycleStatus
CollectionRunSummarypendingCount, runningCount, budgetExhaustedCount, oldestPendingSince (각 nullable/additive). SLA count를 이 current summary에 두지 않는다
CollectionSlaCaptureStatus`‘PENDING'
CollectionSlaSnapshot{ snapshotDate: string; scheduledAt: string; captureStatus: CollectionSlaCaptureStatus; capturedAt: string | null; missReason: string | null; lateBeforeNotificationCount: number | null; items: CollectionSlaSnapshotItem[] | null }
CollectionSlaSnapshotItem{ jobSourceId: number; sourceLabel: string; cycleStatus: CollectionCycleStatus; fetchedCount: number; abnormal: boolean; closeGuarded: boolean }; snapshot 당시 값이며 current CollectionRun으로 재구성하지 않는다. listPageCount/detailRequestCount는 immutable snapshot 계약에 요구하지 않으며 live CollectionRun 행에만 표시한다
CollectionRunsResponse 추가`summary?: CollectionRunSummary

테마 토큰

새 원색·토큰은 추가하지 않는다. 기존 tokens.cssChip의 시맨틱 토큰을 그대로 쓴다.

시맨틱 토큰라이트다크용도
surface / border#ffffff / #e5e8eb#191c22 / #2c313asummary card
text-primary / text-secondary#191f28 / #4e5968#edeff2 / #a7aeb8제목·보조 설명
accent / accent-subtle#3182f6 / #eaf2fe#4e92f7 / #17263b실행 중
warning / warning-subtle#c2660a / #fff3e5#ffa23a / #2e2113예산 소진
danger / danger-subtle#e02b39 / #feecee#ff6b76 / #331a1d실패
positive / positive-subtle#00a661 / #e5f7ef#2ecc80 / #10291e완료

라우팅·내비게이션

flowchart LR
    Nav[운영 탭] --> Ops[/operations]
    Ops --> Collection[수집 이력]
    Ops --> Notification[알림 실패]
    Collection --> Summary[읽기 전용 summary]
    Collection --> Runs[수집 cycle 목록]

/operations와 4탭 내비게이션을 유지한다. summary와 row는 button/link가 아니므로 키보드 액션이나 신규 상세 이동은 없다.

접근성

  • summary는 sectionaria-labelledby를 사용해 수집 운영 요약을 제목으로 연결한다.
  • counts는 색만으로 의미를 전달하지 않고 진행 중 2개처럼 텍스트를 함께 제공한다.
  • Chip은 기존 텍스트 레이블을 유지하고, 마감 제외 문구는 본문 텍스트로 노출한다.

Testing Plan

레벨테스트
API/typeadditive summary와 nullable v2 item을 MSW 응답으로 소비하고 기존 응답도 파싱·렌더되는지 확인
presentation unitNOT_STARTED/PENDING/RUNNING/BUDGET_EXHAUSTED/SUCCESS/FAILED의 칩/상세와, v2 closeGuarded 직접 소비 및 legacy SUCCESS/0건/FAILED 폴백을 검증
componentslaSnapshot의 PENDING/CAPTURED/MISSED/NOT_APPLICABLE/UNAVAILABLE 각각의 문구, live request counter를 섞지 않은 CAPTURED item 목록과 missReason, budget warning의 비장애 문구, isComplete=true + abnormal=true + closeGuarded=true를 포함한 close-guard 문구를 사용자 텍스트로 검증
page integrationv2 current summary + immutable SLA snapshot + mixed row 목록, slaSnapshot 없는 legacy 목록, loading/empty/error 및 탭 전환을 MSW로 검증
theme신규 summary와 모든 신규 상태를 .dark에서 시맨틱 Tailwind class만으로 렌더하고 하드코딩 색이 없는지 검증

Release Scenario — 점진 공개

  1. BE expand/dark deploy 단계에서는 summarycloseGuarded를 포함한 item additive 필드가 없거나 null일 수 있다. FE는 closeGuarded 부재에만 legacy 가드를 폴백하므로 어느 배포 순서도 화면을 깨지 않는다.
  2. FE 배포 후 BE-40이 새 필드를 보내면 동일 GET 요청에서 summary와 v2 상태가 자동 노출된다. feature flag를 FE가 별도로 보유하지 않는다.
  3. 롤백 시 FE를 이전 빌드로 되돌려도 additive JSON 필드는 무시되어 안전하다. BE 롤백 시 새 FE는 optional fields 부재를 legacy 표현으로 처리한다.

Open Questions

없음. oldestPendingSince의 시간대는 BE가 ISO offset으로 제공한다는 TDD 계약을 전제로 기존 날짜 포매터와 같은 KST 표기를 사용한다.

Document History

날짜변경 내용
2026-07-30최초 작성 — collection cycle 상태·예산 소진·운영 summary의 기존 운영 화면 확장
2026-07-30PM 정합 반영 — v2 closeGuarded 서버 소유, legacy 전용 가드 폴백으로 변경
2026-07-3009:00 KST 영속 snapshot·NOT_STARTED 계약 반영
2026-07-30PM 정합 반영 — full slaSnapshot capture 상태 계약으로 전환
2026-07-30TPM G0 C-01 반영 — snapshot item에서 live 요청 카운터 제거