확장 안전 공고수집 배치 FE 웹 설계
Background
근거 문서는 타깃 공고 알림 및 지원 히스토리 PRD와 확장 안전 공고수집 배치 TDD다. TDD는 수집을 짧은 slice로 분할하고, 완전 수집 전에는 마감 판정을 하지 않으며, 운영자가 09:00 전 미완료·예산 소진 상태를 확인할 수 있어야 한다(NFR-2).
Overview
기존 /operations의 수집 이력 탭을 확장한다. 새 화면·라우트·쓰기 API는 만들지 않는다. 기존 목록의 각 실행 행에 cycle 진행 상태와 제한된 요청량을 보이고, 상단에는 미완료와 예산 소진을 먼저 보이는 읽기 전용 요약을 둔다. 이는 사용자가 실패 소스와 정상적인 안전 중단(BUDGET_EXHAUSTED)을 혼동하지 않게 하는 최소 변경이다.
Terminology
| 용어 | FE 표기/의미 |
|---|---|
| 완전 수집 | isComplete=true; 이 경우에만 마감 판정이 반영될 수 있다 |
| 미완료 | PENDING, RUNNING, FAILED, BUDGET_EXHAUSTED 등 isComplete!==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.tsx와collectionRunPresentation.ts#describeCollectionRun은SUCCESS/FAILED와abnormal만 해석한다. 진행 중·예산 소진·완전성은 구별할 수 없다.web/src/types/api.ts#CollectionRun과CollectionRunsResponse에 새 cycle 필드와 top-levelsummary가 없다.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는 v2isComplete/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_STARTED | neutral 칩 시작 전, 아직 수집을 시작하지 않았어요 텍스트; 서버가 아직 cycle을 시작하지 않은 상태이며 실패/지연으로 표현하지 않는다 |
| v2 pending/running | neutral/accent 칩, 요청량이 있으면 표시, 마감 판정 제외 중 표시 |
| v2 budget exhausted | warning 칩 예산 상한 도달, 안전 상한으로 중단됨과 마감 제외를 표시; abnormal/소스 고장으로 표시하지 않는다 |
| v2 success/failed | complete 성공은 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 | 완료 | positive | fetchedCount건 + counts |
cycleStatus=FAILED | 실패 | danger | errorMessage 우선, 없으면 수집을 완료하지 못했어요 |
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 처리 |
|---|---|---|
PENDING | neutral 09:00 KST 점검 예정 / 점검 시각 전입니다 | lateBeforeNotificationCount=0, items 없음은 위반 0건이 아니라 아직 capture 전이라는 뜻. scheduledAt은 보조 시각으로만 표시 |
CAPTURED | positive 09:00 KST 점검 완료; count 0이면 미완료 없음, 양수면 warning 09:00 KST 시점 미완료 N개 | capturedAt을 기록 시각으로 보이며, immutable items는 양수일 때 09:00 KST 당시 미완료 소스 목록으로 표시 |
MISSED | danger 09:00 KST 점검 누락 / 당시 상태를 기록하지 못했어요 | count/items를 현재 cycle로 대체하지 않는다. missReason이 있으면 보조 문구로 표시 |
NOT_APPLICABLE | neutral SLA 점검 적용 전 | count/items 없음. v2 활성화 전·적용 대상이 아닌 날짜라는 서버 판정을 그대로 보인다 |
UNAVAILABLE | warning 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 useCollectionRuns | GET 응답이 SSOT이며 다른 저장소 복사는 stale 상태를 만든다 |
| 지역 상태 | 기존 활성 탭 | useState | 운영 페이지 수명 안에서만 필요하다 |
| 전역 상태 | 해당 없음 | 추가하지 않음 | summary를 다른 화면에 공유할 요구가 없다 |
useCollectionRuns의 queryKey와 GET URL은 유지한다. 운영 화면이 열려 있는 동안의 별도 polling은 채택하지 않는다. 1일 1회 배치의 관측 화면에 고정 polling을 더하면 외부 요청이 늘고, 사용자는 브라우저 새로고침 또는 기존 오류 재시도로 최신 조회를 할 수 있다.
API 연동
| 화면/컴포넌트 | endpoint | 훅 | 소비 필드 | 오류 처리 |
|---|---|---|---|---|
| 수집 이력 탭 | GET /api/operations/collection-runs?days=30 | useCollectionRuns | 기존 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 |
CollectionRunSummary | pendingCount, 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.css와 Chip의 시맨틱 토큰을 그대로 쓴다.
| 시맨틱 토큰 | 라이트 | 다크 | 용도 |
|---|---|---|---|
surface / border | #ffffff / #e5e8eb | #191c22 / #2c313a | summary 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는
section과aria-labelledby를 사용해수집 운영 요약을 제목으로 연결한다. - counts는 색만으로 의미를 전달하지 않고
진행 중 2개처럼 텍스트를 함께 제공한다. - Chip은 기존 텍스트 레이블을 유지하고, 마감 제외 문구는 본문 텍스트로 노출한다.
Testing Plan
| 레벨 | 테스트 |
|---|---|
| API/type | additive summary와 nullable v2 item을 MSW 응답으로 소비하고 기존 응답도 파싱·렌더되는지 확인 |
| presentation unit | NOT_STARTED/PENDING/RUNNING/BUDGET_EXHAUSTED/SUCCESS/FAILED의 칩/상세와, v2 closeGuarded 직접 소비 및 legacy SUCCESS/0건/FAILED 폴백을 검증 |
| component | slaSnapshot의 PENDING/CAPTURED/MISSED/NOT_APPLICABLE/UNAVAILABLE 각각의 문구, live request counter를 섞지 않은 CAPTURED item 목록과 missReason, budget warning의 비장애 문구, isComplete=true + abnormal=true + closeGuarded=true를 포함한 close-guard 문구를 사용자 텍스트로 검증 |
| page integration | v2 current summary + immutable SLA snapshot + mixed row 목록, slaSnapshot 없는 legacy 목록, loading/empty/error 및 탭 전환을 MSW로 검증 |
| theme | 신규 summary와 모든 신규 상태를 .dark에서 시맨틱 Tailwind class만으로 렌더하고 하드코딩 색이 없는지 검증 |
Release Scenario — 점진 공개
- BE expand/dark deploy 단계에서는
summary와closeGuarded를 포함한 item additive 필드가 없거나 null일 수 있다. FE는closeGuarded부재에만 legacy 가드를 폴백하므로 어느 배포 순서도 화면을 깨지 않는다. - FE 배포 후 BE-40이 새 필드를 보내면 동일 GET 요청에서 summary와 v2 상태가 자동 노출된다. feature flag를 FE가 별도로 보유하지 않는다.
- 롤백 시 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-30 | PM 정합 반영 — v2 closeGuarded 서버 소유, legacy 전용 가드 폴백으로 변경 |
| 2026-07-30 | 09:00 KST 영속 snapshot·NOT_STARTED 계약 반영 |
| 2026-07-30 | PM 정합 반영 — full slaSnapshot capture 상태 계약으로 전환 |
| 2026-07-30 | TPM G0 C-01 반영 — snapshot item에서 live 요청 카운터 제거 |