feat(807-1): 엑셀 불러오기 = 수식 문서 · 단가산출 · 일위대가 하나씩 개인 층으로 · 원문 패널 기초 찾기 + 「어디서 왔나」 카드

- 임도품셈 판 대응표 CSV 새 자리(STmate/old/30_원자료) — 불러오기 시험 26 실패 → 0
- 가져온 식 SK = 수식 문서(멈춘 wip/802-4b-1 M02_Doc_Import 를 메인 판에 다시 얹음 · 빈 산근 = 엑셀 0 원 줄) —
  줄 쌓기 「수식」 탭 길 열림 · 셈 = 단가기준(엑셀 값 기억 | 연결 값) · 화면 캐시 기록 · 수식 길 저장 `단가기준`
- 1,021 블록 전수(실무 여덟 + 표본 밖 셋) 엑셀 검산 · 엑셀 전후 · 우리 전후 모두 같음 · 셈됨 줄지 않음
- 옮기기 도구 「가져온식」 상태 걷음(가져온 줄 문서로 대조)
- [내 일위대가로] = 호표 하나 + 하위 → 개인 층 SC · 부르는 단산 = 가져온 식 SK(같은 내용 = 있는 것 · 못 이은 줄 = 고정)
- [내 단가산출로] = 단산 블록 판 → 개인 층 가져온 식 SK(개인 층이면 [가져온 식으로 열기] 숨김)
- 원문 패널(M01 로직 · M02 단가산출) 맨 위 「단가산출 기초 찾기」 · 가져온 식 SK 첫 화면 = 「어디서 왔나」 카드
  (프로그램 · 파일 · 블록 · 행 · 가져온 때 · 표지 · 엑셀 글의 품셈 절 글만)
- 수식 탭 [엑셀 단가 그대로 | 우리 단가로](단가산출 화면 · 저장 뒤 페이지 문서 단가기준 맞춤)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-08 17:11:09 +09:00
co-authored by Claude Opus 5.5
parent 5a0144004d
commit 9c803024e0
25 changed files with 1465 additions and 58 deletions
@@ -66,3 +66,8 @@
html:not([data-size="compact"], [data-size="medium"]) .m01-master__panel .m01-logic .m01lab-side {
margin-inline-end: calc(-1 * (var(--spacing-12) + var(--spacing-24)));
}
/* 원문 패널 맨 위 기초 찾기(807-1) — 결과는 패널 몸을 다 먹지 않게 */
.m01lab-src__find .ui-find__list {
max-height: 40vh;
}
@@ -3,12 +3,14 @@
* 로직 상세 — 왼쪽 지금 내용 · 오른쪽 접는 패널 「원문」(품셈 본문 절)
* 공용 오른쪽 접는 패널(ui_template_side_panels) — 접힘 됨 · 접힌 패널 = 세로 글 띠
* 열림 · 폭은 취향 키(`m01-logic-side`) · 원문 = 공용 원문 보기(`LogicLab_Source`)
* 맨 위 「단가산출 기초 찾기」(807-1 · `LogicLab_SourceFind`) — 고르면 그 기초 원문 · 지우면 연 로직 원문
* ========================================================================== */
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { createSidePanels } from "@ui/ui_template_side_panels";
import { el } from "@ui/ui_template_elements";
import { createSourceView } from "./M01_MasterData_UI_LogicLab_Source";
import { createSourceFind } from "./M01_MasterData_UI_LogicLab_SourceFind";
import "./M01_MasterData_UI_LogicLab_Side.css";
const TEXT = {
@@ -27,20 +29,41 @@ export interface LogicSide {
export function buildLogicSide(left: HTMLElement): LogicSide {
const source = createSourceView();
let want = "";
let found: string | null = null; // 찾기로 고른 기초 출처(지우면 연 로직으로)
/** 원문 패널이 열려 있을 때만 찾음 */
const load = (): void => {
if (sp.current() === "source") source.show(want);
if (sp.current() === "source") source.show(found ?? want);
};
const find = createSourceFind({
onPick: (x) => {
found = x.출처 ?? "";
load();
},
onClear: () => {
found = null;
load();
},
});
const sp = createSidePanels({
prefKey: "m01-logic-side",
defaultWidth: 520,
minWidth: 320,
panels: [{ id: "source", title: ts("Source"), body: source.root }],
panels: [
{
id: "source",
title: ts("Source"),
body: el("div", { className: "m01lab-src", children: [find.root, source.root] }),
},
],
onChange: load,
});
return {
root: el("div", { className: "m01lab-side ui-side-panels-row", children: [left, sp.root] }),
showSource: (src) => {
if (src !== want) {
find.clear();
found = null;
}
want = src;
load();
},
@@ -0,0 +1,95 @@
/* =============================================================================
* M01_MasterData_UI_LogicLab_SourceFind.ts
* 원문 패널 맨 위 「단가산출 기초 찾기」(PLAN 807-1 · 요구 699) — M01 로직 상세 · M02 단가산출 원문 패널이 같이 씀.
* M01 로직 목록(로그인 길 `/m02/logic-lib/logics` · 처음 칠 때 한 번 읽음) · 낱말 맞춤 = 공용 `matchLogic`
* (키 · 원문번호 · 이름 · 구분 · 별칭) · 30 줄까지 · 고르면 `onPick`(부르는 쪽이 그 출처 원문을 보임 · 기억 안 함)
* 빈 칸 = 목록 비움 + `onClear`(부르는 쪽이 처음 화면으로)
* ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend";
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { createFindList } from "@ui/ui_template_find_list";
import { matchLogic } from "@ui/ui_template_logic_find";
const TEXT = {
Find: ["단가산출 기초 찾기 — 이름 · 원문번호", "Find base build-up — name · clause"],
Many: ["{n} 개 중 30 개 — 낱말을 더 침", "Showing 30 of {n} — type more"],
None: ["맞는 기초 없음", "No match"],
Failed: ["기초 목록을 못 읽음", "Failed to load"],
} as const;
const ts = (key: keyof typeof TEXT, n = 0): string =>
TEXT[key][currentLanguageIndex === 1 ? 1 : 0].replace("{n}", String(n));
export interface FoundLogic {
키: string;
원문번호?: string;
이름: string;
구분?: string;
상세구분?: string;
별칭?: string | null;
결과단위?: string;
출처?: string;
}
const MAX = 30;
let all: Promise<FoundLogic[]> | null = null; // 한 화면 수명 동안 한 번(M01 로직 목록)
const logics = (): Promise<FoundLogic[]> =>
(all ??= fetch(`${API_BASE_URL}/m02/logic-lib/logics`, { credentials: "include" })
.then((r) => (r.ok ? (r.json() as Promise<{ logics: FoundLogic[] }>) : Promise.reject()))
.then((d) => d.logics)
.catch((e: unknown) => {
all = null;
throw e;
}));
export function createSourceFind(opts: {
onPick: (logic: FoundLogic) => void;
onClear: () => void;
}): { root: HTMLElement; clear: () => void } {
const find = createFindList({ placeholder: ts("Find") });
find.root.classList.add("m01lab-src__find");
find.root.setAttribute("data-source-find", "");
let seq = 0;
const run = async (): Promise<void> => {
const q = find.search.value.trim();
const mine = ++seq;
if (!q) {
find.setRows([]);
find.hint.textContent = "";
return opts.onClear();
}
let rows: FoundLogic[];
try {
rows = await logics();
} catch {
find.hint.textContent = ts("Failed");
return;
}
if (mine !== seq) return;
const hit = rows.filter((x) => matchLogic(q, x).ok);
find.hint.textContent = hit.length > MAX ? ts("Many", hit.length) : hit.length ? "" : ts("None"); // prettier-ignore
find.setRows(
hit.slice(0, MAX).map((x) => ({
key: x.키,
tag: x.원문번호 || x.키,
name: x.이름,
sub: [x.구분, x.상세구분].filter(Boolean).join(" · "),
unit: x.결과단위 ?? "",
onPick: () => {
find.setRows([]);
find.hint.textContent = `${x.원문번호 || x.키} ${x.이름}`;
opts.onPick(x);
},
})),
);
};
find.search.addEventListener("input", () => void run());
return {
root: find.root,
clear: () => {
find.search.value = "";
find.setRows([]);
find.hint.textContent = "";
},
};
}