feat(797-2): 엑셀 불러오기 소요량 · 계수 이음 — 값 역색인 후보 + 사람 확인 · 고름 = 표 칸 참조 기록

- 새 `M02_Sangeun_QtyLinks` — 가져온 식 줄(호표 수량 · 덧줄 식)의 수를 M01 소요량(QC/QF) · 계수(CC/CF) 칸과 값 역색인(`San_Basis_Index.lookup`)으로 맞춤 · 같은 값 + 같은 단위 성격 + 공종 문맥(인력 = 직종 · 재료 = 칸 이름 추가) · 일반 수 · 비교 글자 · 중간 변수는 자리 아님 · 유일해도 자동 확정 없음
- 고름 = `가져온.연결` 기록(층 M01 · 표 · 줄 · 칸 · 값 · 꼴) · 엑셀 수 = 칸 값일 때만(다르면 400 까닭) · 버림 · 되돌림 · 기록만(셈 · 금액 · 단가 갱신 안 건드림) · 저장 거름(`clean`) = 자리 · 수 맞는 기록만
- 길 `…/sangeun/imported/{qty|qty-pick|qty-drop}` · 가져온 식 만들 때 표지 「참고 · 소요량 후보 N · 사람 확인」 · 「표 칸 없음」(못 옮김 표지 아님)
- 화면 — 가져온 식 모달 머리 판 한 줄(집계 · [이음 보기]) + 모달(줄마다 후보 · [고르기] · [풀기] · [버리기] · [되돌리기] · 부품 상자만)
- 시험 test_797_2_qty_coef_links 9 · 낡은 시험 없음

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 15:23:12 +09:00
co-authored by Claude Sonnet 5.5
parent ceb62d65c5
commit e171dda4e3
8 changed files with 930 additions and 4 deletions
@@ -4,6 +4,7 @@
* · 줄 1 = 「새」 · 원본(파일 · 시트 · 행) · 검산 표지(같음 / 다름 n · 줄마다 까닭) · 단가 기준 [우리 단가로] / [엑셀 단가로] ·
* 바꾸기 전후 금액 · [원본 글 보기]
* · 줄 2 = 이음 없는 자원 = 표지 + [이음 고르기]
* · 줄 2-2 = 소요량 · 계수 이음(797-2 · 후보 집계 + [이음 보기] → `Hopyo_QtyLinks` 모달)
* · 줄 3 = 근거 제안 세 단(확정 · 근접(차이 · 까닭) · 사람 고름 · 793-3a) — 후보마다 [우리 기초로 바꾸기] · [기초 고르기]
* · 우리 기초로 바꾼 뒤 = 전후 차이 띠 · 「입력 필요」 붉은 칸(채우기 전엔 그 줄 확인 · 옮김 막힘)
* 부품 = 단추 · 배지 · 글 · 입력 · 띠 · 표 · 모달(날 단추 · 입력 없음) · 서버 길은 부르는 쪽.
@@ -21,6 +22,7 @@ import { createText } from "@ui/ui_template_text";
import { createToolbar } from "@ui/ui_template_toolbar";
import { formatMoney } from "../common_util/common_util_money";
import { newBadge } from "./M02_MasterTemplete_Hopyo_CheckRows";
import { tallyBadges, type QtyTally } from "./M02_MasterTemplete_Hopyo_QtyLinks";
import {
gapText,
isImported,
@@ -66,6 +68,8 @@ const TEXT = {
Need: ["입력 필요", "Input needed"],
Swap: ["환산 계수(기초 1 단위 = 이 단가산출 몇 단위)", "Unit factor (1 basis unit = n of this)"],
NeedHint: ["채워야 이 줄을 확인 · 옮김", "Fill to confirm and move this line"],
QtyLinks: ["소요량 · 계수 이음 {n}", "Quantity / coefficient links {n}"],
QtyOpen: ["이음 보기", "Show links"],
Close: ["닫기", "Close"],
} as const satisfies Record<string, readonly [string, string]>;
export const tx = (k: keyof typeof TEXT): string =>
@@ -75,6 +79,8 @@ export interface PanelState {
doc: ImportedDoc;
/** 이음 없는 자원(서버 price 답) */
unlinked: { 자리: string; 이름: string }[];
/** 소요량 · 계수 이음 집계(서버 qty 답 · 797-2) */
qty: QtyTally | null;
/** 마지막 단가 바꿈 전후 */
price: { 전: Money4 | null; 후: Money4 | null } | null;
/** 우리 기초로 바꾼 뒤 입력 필요 칸 */
@@ -89,6 +95,7 @@ export interface PanelActs {
toBase: (code: string, cand: BasisCand | null) => void;
pickBase: () => void;
fill: (name: string, value: string) => void;
qty: () => void;
}
const won = (m: Money4 | null | undefined): string => (m ? formatMoney(m.계) : "—");
@@ -224,6 +231,12 @@ export function importedPanel(s: PanelState, a: PanelActs): HTMLElement {
const tag = createBadge(tx("Unlinked").replace("{n}", String(s.unlinked.length)), "warning");
kids.push(createToolbar({ items: [tag, ...items] }));
}
if (here && s.qty?.자리) {
const open = createButton({ label: tx("QtyOpen"), size: 24, variant: "ghost", onClick: a.qty });
open.dataset.importedQty = String(s.qty.자리);
const tag = createBadge(tx("QtyLinks").replace("{n}", String(s.qty.자리)), "info");
kids.push(createToolbar({ items: [tag, ...tallyBadges(s.qty), open] }));
}
const gone = meta.바꿈;
if (!here && gone) {
const g = tx("Gap").replace("{a}", won(gone.전)).replace("{b}", won(gone.후));