2단계에서 저장한 sources/partial 을 운영자가 볼 수 있게 한다. 운영자 목적은 **진단**이라
상태를 접지 않는다 — blocked(IP 회전으로 자동 회복)와 env_blocked(사람이 환경·설정을 고쳐야 함)를
뭉뚱그리면 회복될 일에 매달리거나 손봐야 할 설정을 방치하게 된다.
API
- /v1/lps/products, /v1/lps/products/{code}/history 둘 다 sources·partial 을 싣는다.
- 이력은 **시점마다** 싣는다. 최신 상태를 과거 시점의 몰별 표 옆에 붙이면 '그때도 막혔던 것처럼'
보여 오해를 부른다 — 그래서 ProductItem 이 아니라 PricePoint 에 담았다.
화면
- lib/sourceState.ts: 상태별 라벨·색·설명·confirmed 를 한곳에. 미지의 상태가 와도 화면이 깨지지
않는다(값 그대로 표시 + '모름' 취급). 색은 전부 @theme 토큰 참조(raw hex 금지).
- 상품 목록: partial 이면 '일부 확인 못함' 배지 + 툴팁에 어느 몰인지.
- 몰별 비교 카드 위: 몰별 상태·수집 건수·실패 사유 원문(툴팁). 가격표에 없는 몰이 **왜** 없는지를
여기서 답한다 — by_mall 은 가격이 있는 몰만 담으므로 그 답이 여기밖에 없다.
검증: ASGI 직접 호출로 두 엔드포인트 응답 확인(한글 사유 포함), tsc 오류 없음.
테스트 3건 추가(목록 노출 / 시점별 상태가 각각 다르게 / 컬럼 추가 이전 옛 행 호환).
전체 292 passed. 진행 상황은 docs/result-states.md 4절.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
63 lines
2.8 KiB
TypeScript
63 lines
2.8 KiB
TypeScript
/**
|
|
* 몰별 확인 상태의 표기 — **운영자용**(상세). 정의는 lps/docs/result-states.md.
|
|
*
|
|
* 실사용자 화면(negodata)은 이걸 3가지로 접어서 보여준다. 여기서 접지 않는 이유는
|
|
* 운영자의 목적이 **진단**이기 때문이다: `blocked`(자동 회복)와 `env_blocked`(사람이 고쳐야 함)를
|
|
* 뭉뚱그리면 회복될 일에 매달리거나 손봐야 할 설정을 방치하게 된다.
|
|
*
|
|
* 색은 전부 index.css @theme 토큰을 참조한다(raw hex 금지 — 토큰을 바꾸면 여기도 함께 바뀌어야 함).
|
|
*/
|
|
import type { SourceState } from "../api/types";
|
|
|
|
type Meta = {
|
|
label: string;
|
|
color: string;
|
|
/** 한 줄 설명 — '이게 무슨 뜻이고 내가 뭘 해야 하나' */
|
|
desc: string;
|
|
/** 그 몰을 실제로 확인했는가. false 면 '없다'고 말하면 안 된다 */
|
|
confirmed: boolean;
|
|
};
|
|
|
|
export const SOURCE_STATES: Record<SourceState, Meta> = {
|
|
matched: {
|
|
label: "매칭", color: "var(--color-ok-600)", confirmed: true,
|
|
desc: "수집·매칭 성공 — 가격 확보",
|
|
},
|
|
no_match: {
|
|
label: "같은 상품 없음", color: "var(--color-neutral-400)", confirmed: true,
|
|
desc: "수집은 됐으나 같은 상품이 아님 — 검색어·규격을 의심할 것",
|
|
},
|
|
empty: {
|
|
label: "결과 0건", color: "var(--color-neutral-400)", confirmed: true,
|
|
desc: "그 몰의 검색 결과 자체가 0건 — 확인했고 정말 없다",
|
|
},
|
|
blocked: {
|
|
label: "차단", color: "var(--color-warn-600)", confirmed: false,
|
|
desc: "안티봇 차단 — IP 회전으로 자동 회복된다. 반복되면 IP 풀·예산 확인",
|
|
},
|
|
env_blocked: {
|
|
label: "환경 차단", color: "var(--color-dead-600)", confirmed: false,
|
|
desc: "회전해도 회복 불가 — 사람이 환경/게이트웨이 설정을 고쳐야 한다",
|
|
},
|
|
unavailable: {
|
|
label: "확인 못함", color: "var(--color-warn-600)", confirmed: false,
|
|
desc: "전송 실패·가용 IP 없음 등 일시적 — 잠시 후 재시도로 회복",
|
|
},
|
|
skipped: {
|
|
label: "미사용", color: "var(--color-neutral-300)", confirmed: true,
|
|
desc: "그 소스를 쓰지 않음(폴백 OFF 등)",
|
|
},
|
|
};
|
|
|
|
/** 미지의 상태도 화면을 깨뜨리지 않는다 — 값 그대로 보여주고 '모름'으로 취급한다. */
|
|
export const stateMeta = (s: string | undefined): Meta =>
|
|
SOURCE_STATES[s as SourceState] ?? {
|
|
label: s || "-", color: "var(--color-ink-400)", desc: "알 수 없는 상태", confirmed: false,
|
|
};
|
|
|
|
/** 못 본 몰 목록 — 결과가 왜 최종이 아닌지 한 줄로 설명할 때 쓴다. */
|
|
export const unconfirmedMalls = (sources?: Record<string, { state: string }>): string[] =>
|
|
Object.entries(sources ?? {})
|
|
.filter(([, v]) => !stateMeta(v?.state).confirmed)
|
|
.map(([mall]) => mall);
|