- [수정] 모달 = Labor_Table(읽기 · 덧칸 표지 · 엑셀 금액) · 줄 = 로직(단가산출 · 일위대가) / 요소(연결됨 · 후보 · 못 이음) · 단가산출 줄 = 가져온 식 SK 수식 탭 모달(Labor_SkEdit) · 서버 hopyo-view(M02_Hopyo_View) - 엑셀 보기 목록 · 칠 = 줄 표지(View.listing) · 단가산출 탭 = 블록 목록 + [가져온 식으로 열기] · [내 단가산출로] - 걷음(요구 703): 판정 체계 785(Judge · Solve · Bind · San_Basis 다섯 · Make.Move · Row 1차 판정 · 화면 Check · CheckRows · Rows · Block · JudgeApi · Picks · Judge.css) · [우리 기초로 바꾸기](to-base) · 근거 기초 제안 · Clause 기초 판정 - 시험 지움: 642_2(Bind · Solve) · 642_3(Row 1차 판정) · 642_5(옮기기 Move) · 642_6(판정 화면) · 786_2(옛 [수정] 모달) · 793_3a(근거 → 기초 제안) — 모두 걷은 기초 판정 길 몫 - 시험 고침: 642_1 · 794_2a2 · 794_2b(근거 제안 부분만 뺌) · 793_2 · 793_3b(기초 바꾸기 기대 → 없음) · 627 · 779_2 · 785_4 · 807_1 · m02_hopyo_excel · grid · stmate(judge 길 → view 길) - 새 시험 test_809_1_import_labor(기초 길 0 · 표지 1,350 줄 · 가져온 식 SK · 모달 = Labor_Table · 금액 = 엑셀) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
236 lines
11 KiB
TypeScript
236 lines
11 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Hopyo_View.ts
|
|
* 엑셀 불러오기 [수정] 모달(PLAN 809-1 · 요구 702 · 703) — 엑셀 보기 오른쪽 호표 목록 [수정](`Hopyo_Sheet`)이 엶.
|
|
* 경쟁사 로직 인정 · 요소만 연결 — 몸 = 일위대가 컨테이너 부품(`Labor_Table` · 806-1 조합과 한 벌 · 읽기만) + 덧칸 둘:
|
|
* · 표지 = 로직 줄 「단가산출 N」 · 「일위대가 N」 칩 / 요소 줄 「✓ 연결됨」 · 「후보 n」 · 「못 이음」(엑셀 값 그대로)
|
|
* · 엑셀 금액 = 그 줄 엑셀 금액 계
|
|
* 줄 = 서버 `GET …/hopyo-view/{키}/{번}`(`M02_Hopyo_View` · 엑셀 숫자 그대로 고정 줄 · 저장 없음 · 금액 = 엑셀).
|
|
* · 단가산출 줄 누름 = 그 블록 가져온 식 SK(서버 `…/sangeun/from-hopyo` · 만들 층 · 같은 내용 = 있는 것) →
|
|
* 단가산출 「수식」 탭 모달(`Labor_SkEdit` 부르기만 · [엑셀 단가 그대로 | 우리 단가로])
|
|
* · 일위대가 줄 누름 = 그 하위 호표 [수정] 모달 · 후보 줄 누름 = 후보 목록(고르기 = [내 일위대가로] 뒤 일위대가 [연결])
|
|
* · 머리 = 표지 수 · 엑셀 단가 · [내 일위대가로](807-1 `Hopyo_Mine`) · [구조물 조합으로](만든 SC 있을 때 · `Hopyo_ToItem`) · [엑셀 보기]
|
|
* 기초 판정 · 「우리 기초로 바꾸기」 없음(요구 703).
|
|
* ========================================================================== */
|
|
|
|
import { createBadge } from "@ui/ui_template_badge";
|
|
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
|
import { loadingError, showLoading } from "@ui/ui_template_loading";
|
|
import { currentLanguageIndex, t as L } from "@ui/ui_template_locale";
|
|
import { openModal } from "@ui/ui_template_modal";
|
|
import { createTable } from "@ui/ui_template_table";
|
|
import { createText } from "@ui/ui_template_text";
|
|
import { createToolbar } from "@ui/ui_template_toolbar";
|
|
import { formatPrice } from "../common_util/common_util_money";
|
|
import { call } from "./M02_MasterTemplete_Api_Fetch";
|
|
import { at, type HopyoBrief, type HopyoScope } from "./M02_MasterTemplete_Hopyo_Api";
|
|
import { mineButton } from "./M02_MasterTemplete_Hopyo_Mine";
|
|
import { toItem } from "./M02_MasterTemplete_Hopyo_ToItem";
|
|
import type { LaborDetailHandle } from "./M02_MasterTemplete_Labor_Detail";
|
|
import { openSkEdit } from "./M02_MasterTemplete_Labor_SkEdit";
|
|
import { mountLaborTable } from "./M02_MasterTemplete_Labor_Table";
|
|
|
|
const TEXT = {
|
|
Title: ["제{n}호표 — 엑셀 불러오기", "Unit cost #{n} — Excel import"],
|
|
Mark: ["표지", "Status"],
|
|
Excel: ["엑셀 금액", "Excel amount"],
|
|
Linked: ["✓ 연결됨", "✓ Linked"],
|
|
Near: ["후보 {n}", "{n} candidates"],
|
|
None: ["못 이음", "Not linked"],
|
|
Logic: ["로직 {n}", "Logic {n}"],
|
|
ExcelPrice: ["엑셀 단가", "Excel price"],
|
|
Hint: [
|
|
"경쟁사 로직 그대로 · 요소만 연결 — 단가산출 줄을 누르면 수식 탭 · 금액 = 엑셀",
|
|
"Competitor logic kept · elements linked — click a build-up line for its formula",
|
|
],
|
|
Sheet: ["엑셀 보기", "Show in Excel"],
|
|
NearTitle: ["이음 후보 — {name}", "Link candidates — {name}"],
|
|
NearHint: [
|
|
"고르기 = [내 일위대가로] 뒤 내 일위대가의 [연결]",
|
|
"Pick after [To my unit costs] with [Link]",
|
|
],
|
|
Key: ["키", "Key"],
|
|
Name: ["이름", "Name"],
|
|
Close: ["닫기", "Close"],
|
|
} as const satisfies Record<string, readonly [string, string]>;
|
|
const t = (k: keyof typeof TEXT): string => TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0];
|
|
const why = (e: unknown): string => (e instanceof Error ? e.message : String(e));
|
|
const enc = encodeURIComponent;
|
|
|
|
type Money = Record<"노무비" | "재료비" | "경비" | "계", number | null>;
|
|
/** 줄 표지(서버 `M02_Hopyo_View.mark`) */
|
|
export interface ViewMark {
|
|
차례: number;
|
|
갈래: "로직" | "요소";
|
|
표지: "단가산출" | "일위대가" | "연결됨" | "후보" | "못 이음";
|
|
번?: number | null;
|
|
이음?: Record<string, string>;
|
|
후보?: { 키: string; 이름: string }[];
|
|
엑셀: Money;
|
|
}
|
|
export interface ViewReply {
|
|
번: number;
|
|
이름: string;
|
|
규격: string | null;
|
|
단위: string | null;
|
|
금액: Money | null;
|
|
문서: unknown;
|
|
줄: ViewMark[];
|
|
}
|
|
|
|
export const readHopyoView = (file: HopyoScope, key: string, no: number): Promise<ViewReply> =>
|
|
call(at(file, `/hopyo-view/${enc(key)}/${no}`));
|
|
|
|
/** 표지 칸 글 */
|
|
export function markText(m: ViewMark): string {
|
|
if (m.갈래 === "로직") return `${m.표지}${m.번 ? ` ${m.번}` : ""}`;
|
|
if (m.표지 === "연결됨") return t("Linked");
|
|
if (m.표지 === "후보") return t("Near").replace("{n}", String(m.후보?.length ?? 0));
|
|
return t("None");
|
|
}
|
|
|
|
/** 표지 칸 꼴 — 로직 = 칩 · 연결됨 초록 · 후보 주황 · 못 이음 빨강 */
|
|
const markTone = (m: ViewMark): string =>
|
|
m.갈래 === "로직"
|
|
? "ui-hopyo-grid__tag is-pum"
|
|
: m.표지 === "연결됨"
|
|
? "ui-text--success"
|
|
: m.표지 === "후보"
|
|
? "ui-text--warning"
|
|
: "ui-text--error";
|
|
|
|
export interface ViewOptions {
|
|
/** 실무자료 파일 층 */
|
|
file: HopyoScope;
|
|
key: string;
|
|
/** 가져온 식 SK 를 만들 층 */
|
|
target: HopyoScope;
|
|
no: number;
|
|
/** 목록 한 줄([내 일위대가로] · 엑셀 단가) */
|
|
brief?: HopyoBrief;
|
|
canWrite: boolean;
|
|
/** 모달 닫고 엑셀 보기 그 호표 */
|
|
onSheet?: () => void;
|
|
}
|
|
|
|
/** 후보 줄 — 후보 목록 */
|
|
function showNear(name: string, m: ViewMark): void {
|
|
openModal({
|
|
title: t("NearTitle").replace("{name}", name),
|
|
closeLabel: t("Close"),
|
|
dialogClass: "m02-modal",
|
|
mount: (body) => {
|
|
const rows = (m.후보 ?? []).map((c) => [c.키, c.이름]);
|
|
body.append(createTable([{ label: t("Key") }, { label: t("Name") }], rows, { density: "data" })); // prettier-ignore
|
|
body.append(createText(t("NearHint"), "hint"));
|
|
},
|
|
});
|
|
}
|
|
|
|
/** [구조물 조합으로] — 만든 SC(이 파일 층에서 옮긴 것)로 구조물 조합 만들기 */
|
|
const itemButton = (target: HopyoScope, sc: string, name: string, unit: string): HTMLElement =>
|
|
createButton({ label: L("M02_CheckToItem"), size: 28, variant: "ghost", onClick: () => toItem({ target, sc, name, unit }) }); // prettier-ignore
|
|
|
|
/** [수정] 모달 */
|
|
export function openHopyoView(o: ViewOptions): void {
|
|
const root = el("div", { className: "m02-hopyo", attrs: { "data-hopyo-view": `${o.key}|${o.no}` } }); // prettier-ignore
|
|
const host = el("div", { attrs: { "data-hopyo-view-body": "" } });
|
|
let table: LaborDetailHandle | null = null;
|
|
let close = (): void => {};
|
|
|
|
/** 단가산출 줄 — 가져온 식 SK(만들 층) → 수식 탭 모달 */
|
|
async function openSk(m: ViewMark, name: string, head: string): Promise<void> {
|
|
try {
|
|
const made = await call<{ 이름: string }>(at(o.target, "/sangeun/from-hopyo"), {
|
|
method: "POST",
|
|
body: JSON.stringify({ 층: o.file.layer, ...(o.file.owner ? { 주인: o.file.owner } : {}), 키: o.key, 번: m.번 }), // prettier-ignore
|
|
});
|
|
const ref = { 층: o.target.layer, 주인: o.target.owner, 키: made.이름 };
|
|
void openSkEdit({ ref, title: `${name} — ${made.이름}`, head, readOnly: !o.canWrite, closed: () => {} }); // prettier-ignore
|
|
} catch (e) {
|
|
showToast(why(e), "error");
|
|
}
|
|
}
|
|
|
|
function draw(r: ViewReply): void {
|
|
const count = (k: ViewMark["표지"]): number => r.줄.filter((m) => m.표지 === k).length;
|
|
const logic = r.줄.filter((m) => m.갈래 === "로직").length;
|
|
const name = [r.이름, r.규격].filter(Boolean).join(" ");
|
|
const head = [name, r.단위 ? `1 ${r.단위} 당` : ""].filter(Boolean).join(" · ");
|
|
const badges = [
|
|
createBadge(t("Logic").replace("{n}", String(logic)), "accent"),
|
|
createBadge(`${t("Linked")} ${count("연결됨")}`, "success"),
|
|
createBadge(t("Near").replace("{n}", String(count("후보"))), "warning"),
|
|
createBadge(`${t("None")} ${count("못 이음")}`, "danger"),
|
|
];
|
|
badges.forEach((b, i) => (b.dataset.viewCount = ["로직", "연결됨", "후보", "못 이음"][i]));
|
|
const sheet = createButton({ label: t("Sheet"), size: 28, variant: "ghost", onClick: () => { close(); o.onSheet?.(); } }); // prettier-ignore
|
|
const items: (HTMLElement | "divider")[] = [
|
|
el("strong", { text: `제${r.번}호표 ${name}` }),
|
|
...badges,
|
|
"divider",
|
|
createText(`${t("ExcelPrice")} ${r.금액?.계 == null ? "—" : formatPrice(r.금액.계)}`, "muted", 14), // prettier-ignore
|
|
...(o.brief ? [mineButton(o.file, o.key, o.brief)] : []),
|
|
...(o.brief?.만든 && o.canWrite
|
|
? [itemButton(o.target, o.brief.만든, name, r.단위 ?? "")]
|
|
: []),
|
|
...(o.onSheet ? [sheet] : []),
|
|
];
|
|
root.replaceChildren(createToolbar({ items }), createText(t("Hint"), "hint"), host);
|
|
table?.destroy();
|
|
table = mountLaborTable(host, r.문서, {
|
|
layer: o.target.layer,
|
|
owner: o.target.owner,
|
|
column: `제${r.번}호표`,
|
|
mode: "precombo",
|
|
readOnly: true,
|
|
onChanged: () => {},
|
|
view: {
|
|
cols: [
|
|
{ key: "x_mark", label: t("Mark"), width: 96 },
|
|
{ key: "x_excel", label: t("Excel"), kind: "number", decimals: 1, trim: true, width: 96 },
|
|
],
|
|
cell: (n, key) => {
|
|
const m = r.줄[n];
|
|
if (!m) return null;
|
|
return key === "x_mark" ? markText(m) : m.엑셀.계;
|
|
},
|
|
tone: (n, key) => (key === "x_mark" && r.줄[n] ? markTone(r.줄[n]) : null),
|
|
onRow: (n) => {
|
|
const m = r.줄[n];
|
|
const line = (r.문서 as { 담은로직?: { 별칭?: string | null }[] }).담은로직?.[n];
|
|
const label = line?.별칭 ?? name;
|
|
if (m?.표지 === "단가산출" && m.번) return (void openSk(m, label, head), true);
|
|
if (m?.표지 === "일위대가" && m.번) return void openHopyoView({ ...o, no: m.번, brief: undefined }), true; // prettier-ignore
|
|
if (m?.표지 === "후보") return (void showNear(label, m), true);
|
|
return true; // 요소 줄 = 엑셀 숫자 그대로(모달 없음)
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
async function load(): Promise<void> {
|
|
const stop = showLoading(root);
|
|
try {
|
|
draw(await readHopyoView(o.file, o.key, o.no));
|
|
} catch (e) {
|
|
root.replaceChildren(loadingError(why(e), () => void load()));
|
|
} finally {
|
|
stop();
|
|
}
|
|
}
|
|
|
|
({ close } = openModal({
|
|
title: t("Title").replace("{n}", String(o.no)),
|
|
closeLabel: t("Close"),
|
|
wide: true,
|
|
width: "1100px",
|
|
dialogClass: "m02-modal m02-hopyo__view-modal",
|
|
beforeClose: () => {
|
|
table?.destroy();
|
|
return true;
|
|
},
|
|
mount: (body) => body.append(root),
|
|
}));
|
|
void load();
|
|
}
|