Files
Aislo/M02_MasterTemplete/logic/M02_Sangeun_Match.ts
T
eomsangdon 4e7ebf7f87 feat(812-4): 연결 찾기 율 값 = 참고 — 율 줄 판정 = 구조(대상 · 비목)만 · 율 값 % 다름 = 확정 + 참고(바꾸면 우리 값) · 율 대상 · 비목 다름 = 다른 항 그대로
- M02_Doc_Match_Note.rate_pairs → (다른 항, 참고) · 「율 값」 다른 항 폐지(EXPLAINED 에서도 뺌) · 띠 참고 요약에 「율 값 다름 N 줄」
- 시험: test_812_2 율 값 = 참고로 고침 · test_812_4 새(리퍼 105 · 275 확정 + 참고 · 율 대상 다름 = 다른 항 · 바꾸기 뒤 M01 율 · 되돌리기)
- 전수 2,220: 확정 152 → 163 · 근접 588 → 577 · 없음 1,480 · 확정 다시 셈 다름 0
2026-11-09 13:24:28 +09:00

109 lines
4.6 KiB
TypeScript

/* =============================================================================
* M02_Sangeun_Match.ts
* 가져온 식 ↔ 우리 M01 기초 연결 판정 띠 자료(PLAN 810-4 · 요구 707 · 709 · 703) — 원문 패널 카드 위.
* SK `가져온.원본.연결후보`(서버 `M02_Doc_Match` 가 적음) → 띠 글 · 다른 항 · 단추 하나:
* 확정 = [우리 기초 사본으로 바꾸기] · 근접 · 없음 = 단추 없음(703) · 안 찾음 = [연결 찾기] ·
* 다른 항 = 로직 차이(확정을 막음) · 참고 = 단가 · 끝수 · 율 값 %(값 차이 — 「우리 단가로 셈 ○○ 원」 · 812-2 · 812-4) ·
* 바꾼 SK(`가져온보관` 있음) = 「우리 기초 사본 ○○」 + [가져온 식으로 되돌리기]
* 화면 · DOM · fetch 없음
* ========================================================================== */
export type MatchVerdict = "확정" | "근접" | "없음";
export interface MatchFound {
판정?: MatchVerdict;
키?: string;
이름?: string;
옵션?: Record<string, string>;
다른?: { 항: string; 글: string }[];
/** 값 차이(단가 · 끝수 · 율 값) — 확정을 안 막음 */
참고?: { 항: string; 글: string }[];
금액?: { 가져온?: Record<string, number | null>; 우리?: Record<string, number | null> };
후보수?: number;
가까움?: { 키?: string; 이름?: string; 다른?: { 항: string; 글: string }[] };
}
export type MatchAction = "찾기" | "바꾸기" | "되돌리기" | null;
export interface MatchBand {
tone: "success" | "info" | "warning";
text: string;
/** 다른 항 글(근접 · 없음의 가까운 후보) */
details: string[];
action: MatchAction;
}
interface SkLike {
바탕?: { 로직?: string } | null;
입력값?: Record<string, unknown>;
가져온?: { 로직?: unknown; 원본?: { 연결후보?: MatchFound } } | null;
가져온보관?: { 가져온?: { 원본?: { 연결후보?: MatchFound } } } | null;
}
const opts = (o?: Record<string, unknown>): string => {
const list = Object.entries(o ?? {}).map(([k, v]) => `${k} ${String(v)}`);
return list.length ? `(${list.join(" · ")})` : "(기본 옵션)";
};
const lines = (d?: { 항: string; 글: string }[]): string[] =>
(d ?? []).map((x) => `${x.항} — ${x.글}`);
const won = (n?: number | null): string =>
n == null ? "" : `${Math.round(n).toLocaleString("ko-KR")} 원`;
/** 참고 요약 — 「참고: 단가 다름 3 줄 · 율 값 다름 1 줄 · 우리 단가로 셈 53,218 원」 · 참고 없음 = "" */
const noteText = (f: MatchFound): string => {
const n = f.참고 ?? [];
if (!n.length) return "";
const price = n.filter((x) => x.항 === "단가 차이").length;
const rate = n.filter((x) => x.항 === "율 값").length;
const bits = [
price ? `단가 다름 ${price} 줄` : "",
rate ? `율 값 다름 ${rate} 줄` : "",
n.some((x) => x.항 === "끝수") ? "끝수 다름" : "",
].filter(Boolean);
const ours = won(f.금액?.우리?.계);
return ` · 참고: ${bits.join(" · ")}${ours ? ` · 우리 단가로 셈 ${ours}` : ""}`;
};
/** SK → 띠 자료 · 가져온 식도 · 바꾼 SK 도 아니면 null */
export function matchBand(sk: SkLike | null | undefined): MatchBand | null {
const kept = sk?.가져온보관;
if (kept && sk?.바탕?.로직) {
return {
tone: "info",
text: `우리 기초 사본 ${sk.바탕.로직} ${opts(sk.입력값)} — 원래 가져온 식은 보관`,
details: [],
action: "되돌리기",
};
}
if (!sk?.가져온?.로직) return null;
const f = sk.가져온.원본?.연결후보;
if (!f?.판정) {
return { tone: "info", text: "우리 기초와 연결을 아직 안 찾음", details: [], action: "찾기" };
}
const base = `${f.키 ?? ""} ${f.이름 ?? ""} ${opts(f.옵션)}`.trim();
if (f.판정 === "확정") {
return {
tone: "success",
text: `확정 — 로직 같음 · 우리 기초 ${base}${noteText(f)}`,
details: lines(f.참고),
action: "바꾸기",
};
}
if (f.판정 === "근접") {
const kinds = [...new Set((f.다른 ?? []).map((x) => x.항))].join(" · ");
return {
tone: "warning",
text: `근접 — ${base} · 다른 항: ${kinds}${noteText(f)}`,
details: [...lines(f.다른), ...lines(f.참고).map((x) => `참고 ${x}`)],
action: null,
};
}
const near = f.가까움;
return {
tone: "info",
text: `없음 — 우리 기초 후보 ${f.후보수 ?? 0} 개와 맞지 않음`,
details: near ? [`가장 가까운 ${near.키 ?? ""} ${near.이름 ?? ""}`, ...lines(near.다른)] : [],
action: null,
};
}