- 올림 칸 accept .estx · .stc · 올림 안내 「원파일 · EST/STmate — 호표 N · 같은 공사 엑셀 HP 에 붙음」 - 줄 쌓기 위 옵션 가로 — 가져온 식 값으로 드러난 수 하나 변수(q · K · E · Cm · Q …) = 값 칸 + 꼬리표 「원파일」 · 「엑셀」(`sourceOf` · 공식 묶음 밖) · 규격 칸(원파일) · 품셈 조건 글 = 「추정」 칸(읽기만 · 기초에 안 묶음) - 합 밖 · 비목 없는 소계 줄 = 종류 「소계」 · 비목 칸 빈칸(편집 · 산출근거 표 칸 모두) - 원문 카드 — 「원파일 · EST 산근 D… · 행 …」 한 줄 + 원파일 확정(확정 · 표지 · 추정 · 확정률) - 서버 — 원파일 원본에 원코드 · 규격(EST) · 문서 머리 가져온에 원코드 · 규격 · 확정 · 추정 글줄(금액 · 길 그대로) - 화면 계산기 큰나머지 끝전 — 나머지 비교 소수 흔들림 반올림(파이썬 Decimal 과 같은 갈래 · 동부청영월 블록 1810 재료비 1 원 어긋남 고침) - 시험 test_810_3_native_screen(EST 1,849 · STC 371 블록 화면 셈 = 서버 셈 · 옵션 칸 · 추정 칸 수 = 확정 추정 · 소계 · 카드) · 헬퍼 helper_810_3_screen.cjs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
168 lines
7.2 KiB
TypeScript
168 lines
7.2 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 는 「어디서 왔나」 카드
|
|
* (프로그램 · 파일 · 블록 · 행 · 가져온 때 · 표지 요약 + 엑셀 글의 품셈 절 글만 — 찾기 · 잇기 없음)
|
|
* 일위대가 줄 모달 안 상세는 안 만듦(부르는 쪽이 `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";
|
|
|
|
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): 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: [
|
|
...(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: 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(): {
|
|
wrap: (main: HTMLElement) => HTMLElement;
|
|
/** 원문 탭 · 가져온 식 머리(`가져온` — 있으면 첫 화면 = 카드) */
|
|
set: (tabs: SourceTab[], imported?: 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 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));
|
|
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, imported) => {
|
|
const card = originOf(imported as OriginMeta | null);
|
|
if (JSON.stringify([next, card]) === JSON.stringify([tabs, origin])) return;
|
|
[tabs, origin] = [next, card];
|
|
paint();
|
|
},
|
|
};
|
|
}
|