Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Side.ts
T
eomsangdonandClaude Opus 5.5 bed35e3bc3 feat(814-1 3·4·6·7·8·9·10): 불러오기 결정 반영 — 세 줄 묶기 · 꼬리 뗀 맞대기 · 유가 · 재료 · 까닭 · 가벼움
- 3 연결 찾기 = 엑셀 이름 규격 · 단위 꼬리 뗀 이름으로 맞댐(`M02_Doc_Match_Note.bare` · 보이는 이름 그대로) · STmate 블록 규격도 가져온 칸에
- 4 기계 세 줄 = 기계 한 줄 — STmate X 코드 노 · 재 · 경 세 참조 줄(같은 수량 · 번지) 묶음(묶어서 셈 못 하는 세 줄만 그대로) ·
  문서 단계 묶기 넓힘(차례 아무렇게 · 시간당 경비 이음 없으면 엑셀 값 묶음 · 번지 같을 때) · 조합 다음 줄 = 세 줄 뒤 · 같은 기계 한 항목
- 6 공구손료 율 비목 차이(재료비 ↔ 경비) = 참고(`M02_Doc_Match_Value.tool_cost`) — 확정 안 막음
- 7 못 이은 줄 까닭(`M02_San_Link.why` — 자재 없음 · 운임표 없음 · 기계 표에 없음 · 별칭 대기 · 후보 여럿 · 시간당 경비 없음 · 조합) =
  문서 설명 · 이음 없는 자원 띠 · 연결 「없음」 띠 = 「우리 기초 없음 — 우리 정보로 셈 n/m 줄」 · 형제만 = 「조합」
- 8 유가 = 전국평균 줄 + 지역 옵션(`MO:경유:{S_지역}`) · 전력 = 일반용(갑)Ⅰ 저압 + 계약종별 옵션 · 엑셀 값 = 검산 칸
- 9 재료 = 지역 꼬리 뗀 이름 · 규격 정확 일치면 이음(지역 여럿 = 줄 지역 › 서울)
- 10 조합 첫 줄 규격 = 그 기계 · 얼린 줄도 소계 식에(북부청임도 33) · 기계 줄 찾기 판정 늘 붙음 · 올린 직후 409(HP 보이기 전 산근 보관) ·
  엑셀 블록 「엑셀 읽기 확정」 글 · 전수 도우미 str() 뗌 · 줄 쌓기 이름에 규격이 들면 한 번만
- M02_San_Link 새 파일(San_Emit 700 줄 막음 — 요소 잇기 · 시간당 · 조합 · 까닭)
- 시험: 새 test_814_1 · 812-2 · 810-4 거푸집 41 = 근접 → 확정(공구손료 비목 = 값 · 요구 744 C1) · 바꾸기 거절 견본 = 19 ·
  812-9 조합 첫 줄 규격 「0.7+브레이카」 → 「0.7」(칩 글 · 요구 738 흠 12)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:47:08 +09:00

175 lines
7.8 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_Sangeun_Side.ts
* 단가산출(SK) 상세 오른쪽 원문 패널(PLAN 752-1 · 요구 588-3) — 일위대가 상세(47-4 · 85-4)와 같은 부품:
* 오른쪽 서랍(`createSidePanels`) + 원문 보기(`createSourceView` · M02 로그인 길 `/m02/logic-lib/source`)
* 기초마다 탭 — 바탕 로직 · 둘째 기초(더한줄 로직) 각각의 출처 · 기초가 하나면 탭 없음 · 없으면 안내
* 처음 = 넓은 화면만 열림(좁으면 세로 띠로 접힘) · 열림 · 폭 기억 = `m02-sangeun-side`
* 맨 위 「단가산출 기초 찾기」(807-1 · M01 원문 패널과 같은 `LogicLab_SourceFind`) — 고르면 그 기초 원문(보기만 · 기억 안 함) ·
* 지우면 처음 화면 · 처음 화면 = 우리 SK 는 기초 원문 · 가져온 식 SK 는 「어디서 왔나」 카드
* (프로그램 · 파일 · 블록 · 행 · 가져온 때 · 표지 요약 + 엑셀 글의 품셈 절 글만 — 찾기 · 잇기 없음)
* 카드 맨 위 = 우리 기초 판정 띠(810-4 · `Sangeun_MatchBand`) · 우리 기초 사본으로 바꾼 SK = 보관한 가져온 식 카드
* 일위대가 줄 모달 안 상세는 안 만듦(부르는 쪽이 `source` 를 안 켬).
* ========================================================================== */
import { createSegment } from "@ui/ui_template_segment";
import { el } from "@ui/ui_template_elements";
import { currentLanguageIndex, t as L } from "@ui/ui_template_locale";
import { createSidePanels } from "@ui/ui_template_side_panels";
import { createText } from "@ui/ui_template_text";
import { createButton } from "@ui/ui_template_button";
import { createCard } from "@ui/ui_template_card";
import { createKv } from "@ui/ui_template_kv";
import { createSourceView } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Source";
import { createSourceFind } from "../M01_MasterData/M01_MasterData_UI_LogicLab_SourceFind";
import { originOf, type Origin, type OriginMeta } from "./logic/M02_Sangeun_Origin";
import { tc } from "./M02_MasterTemplete_Labor_Text";
import { createMatchBand, type MatchCtx } from "./M02_MasterTemplete_Sangeun_MatchBand";
export interface SourceTab {
label: string;
src: string;
}
/** 기초 로직 → 원문 탭(「원문번호 이름」 · 출처 글) */
export const tabOf = (logic: {
키: string;
원문번호?: string;
이름: string;
출처?: string;
}): SourceTab => ({
label: `${logic.원문번호 || logic.키} ${logic.이름}`,
src: logic.출처 ?? "",
});
/** 이 폭 이상일 때만 처음부터 열림 — 상세 표 여섯 칸이 좁아지지 않게 */
const OPEN_FROM = 1500;
const TEXT = {
Origin: ["가져온 단가산출 — 어디서 왔나", "Imported build-up — where from"],
Program: ["프로그램", "Program"],
File: ["파일", "File"],
Block: ["블록", "Block"],
Rows: ["행", "Rows"],
When: ["가져온 때", "Imported"],
Marks: ["표지", "Flags"],
MarksN: ["{n} 개", "{n}"],
Clauses: ["엑셀 글의 품셈 정보", "Standard clauses in the Excel text"],
NoClause: ["엑셀 글에 품셈 절 글 없음", "No standard clause in the Excel text"],
Hint: ["우리 기초 원문은 위 찾기로", "Find our base above to see its source"],
More: ["표지 모두 보기", "Show all flags"],
Sure: ["원파일 확정", "Source-file read"],
} as const;
const tx = (k: keyof typeof TEXT, n = 0): string =>
TEXT[k][currentLanguageIndex === 1 ? 1 : 0].replace("{n}", String(n));
/** 「어디서 왔나」 카드 — 표지는 셋까지(더 = 단추로 펼침) */
function originCard(o: Origin, band: HTMLElement | null): HTMLElement {
const marks = el("div");
const showMarks = (all: boolean): void =>
marks.replaceChildren(
...(all ? o.표지 : o.표지.slice(0, 3)).map((m) => createText(m, "muted", 12)),
...(!all && o.표지.length > 3
? [createButton({ label: tx("More"), variant: "ghost", size: 24, onClick: () => showMarks(true) })] // prettier-ignore
: []),
);
showMarks(false);
const clauses = o.품셈.length
? o.품셈.map((c) =>
createKv([{ label: `${c.책} ${c.절}`, value: c.행 !== undefined ? `${c.행}행 · ${c.글}` : c.글 }]),
) // prettier-ignore
: [createText(tx("NoClause"), "muted", 12)];
const card = createCard({
title: tx("Origin"),
pad: 12,
body: [
...(band ? [band] : []),
...(o.요약 ? [el("strong", { text: o.요약, attrs: { "data-origin-native": "" } })] : []),
createKv([
{ label: tx("Program"), value: o.프로그램 },
{ label: tx("File"), value: o.파일 },
{ label: tx("Block"), value: o.블록 },
{ label: tx("Rows"), value: o.행 },
{ label: tx("When"), value: o.때 },
{ label: tx("Marks"), value: tx("MarksN", o.표지.length) },
...(o.확정 ? [{ label: o.확정이름 ?? tx("Sure"), value: o.확정 }] : []),
]),
marks,
el("strong", { text: tx("Clauses") }),
...clauses,
createText(tx("Hint"), "muted", 12),
],
});
card.setAttribute("data-sangeun-origin", "");
return card;
}
export function createSangeunSide(match?: MatchCtx): {
wrap: (main: HTMLElement) => HTMLElement;
/** 원문 탭 · SK 문서(`가져온` · 바꾼 SK `가져온보관` — 있으면 첫 화면 = 카드 + 판정 띠) */
set: (tabs: SourceTab[], sk?: unknown) => void;
} {
const view = createSourceView({
path: "/m02/logic-lib/source",
pdf: "/m02/logic-lib/source-pdf",
});
const content = el("div");
let found: string | null = null; // 찾기로 고른 기초 출처(지우면 처음 화면)
const find = createSourceFind({
onPick: (x) => ((found = x.출처 ?? ""), paint()),
onClear: () => ((found = null), paint()),
});
const host = el("div", {
className: "m01lab-src",
attrs: { "data-sangeun-source": "" },
children: [find.root, content],
});
let tabs: SourceTab[] = [];
let origin: Origin | null = null;
let skDoc: unknown = null;
let at = 0;
const side = createSidePanels({
prefKey: "m02-sangeun-side",
follow: false, // 일위대가와 같음 — 상세 고정 높이 칸을 채움 · 본문과 따로 스크롤
defaultOpen: window.innerWidth >= OPEN_FROM ? "source" : null,
panels: [{ id: "source", title: tc("Labor_Source"), body: host }],
onChange: () => paint(),
});
function paint(): void {
if (side.current() !== "source") return; // 접힘 = 찾지 않음
if (found !== null) {
view.show(found);
return void content.replaceChildren(view.root);
}
if (origin)
return void content.replaceChildren(originCard(origin, createMatchBand(skDoc, match)));
if (!tabs.length)
return void content.replaceChildren(createText(L("M02_SangeunSourceNone"), "muted"));
at = Math.min(at, tabs.length - 1);
view.show(tabs[at].src);
const bar =
tabs.length > 1
? [
createSegment({
options: tabs.map((x, i) => ({ value: String(i), label: x.label })),
value: String(at),
size: 28,
ariaLabel: tc("Labor_Source"),
onChange: (v) => ((at = Number(v)), paint()),
}).root,
]
: [];
content.replaceChildren(...bar, view.root);
}
return {
wrap: (main) => el("div", { className: "ui-side-panels-row", children: [main, side.root] }),
set: (next, sk) => {
const one = sk as { 가져온?: unknown; 가져온보관?: { 가져온?: unknown } } | null;
const card = originOf((one?.가져온 ?? one?.가져온보관?.가져온) as OriginMeta | null);
const same = JSON.stringify([next, card, sk]) === JSON.stringify([tabs, origin, skDoc]);
if (same) return;
[tabs, origin, skDoc] = [next, card, sk ?? null];
paint();
},
};
}