- 남김 1 = 분류1~5(구조물집계표 층 이름 = 마스터 템플릿 자료 · 코드 밖) - ui_template 네 파일 글만(한시 브레인 승인) · 키 · 동작 그대로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
361 lines
14 KiB
TypeScript
361 lines
14 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Labor_Pick.ts
|
|
* [로직 더하기] 창 — 한 목록에 구분(단가산출 · 단가승격 · 일위조합) 칸 · 치는 대로 걸러짐(찾기 단추 없음) ·
|
|
* 단가승격 · 일위조합 줄은 층 단추(위 층만 · 짝 `M02_Labor_Ref.readable`) 층의 서고 · 고르면 참조 {층 · 주인 · 키} ·
|
|
* 프로젝트 · 카드엔 서고 줄 없음 · 거름 = 로직 품셈 분류 + 사전조합 새 분류(149-2 · 구조물군 › 공통 + 종류)
|
|
* 단가연결(251 · B08 거리이정표 · B09 설계내역서) = 종류 고르기(단가산출 · 사전조합 · 덧붙인 줄 종류) +
|
|
* 덧붙일 줄(구조물 조합 · B09 호표) — `lib.extra` · `lib.kindFilter` · 늦게 받는 덧줄 `lib.more`
|
|
* (324-4 실무 호표 · `withHopyo`)
|
|
* 704-1 단가산출 줄 = 기준 목록 표(품명 · 규격 · 단위 · 금액 · 출처 · 대표 규격 펼침 · 공종 거름 ·
|
|
* 실무 낱말 = 서버가 별칭에 덧붙임) · 금액 = 서버 M01 엔진(`/logic-lib/standard`) · 오기 전엔 로직 줄만 ·
|
|
* 줄 누름 = 계산서 보기(704-2 · `Labor_Sheet`) → [담기] = 지금 담기 동작(`done(로직)`)
|
|
* ========================================================================== */
|
|
|
|
import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements";
|
|
import { createDataTable } from "@ui/ui_template_data_table";
|
|
import { createFindList } from "@ui/ui_template_find_list";
|
|
import { aliasNote, matchLogic } from "@ui/ui_template_logic_find";
|
|
import { t as L } from "@ui/ui_template_locale";
|
|
import type { LogicSummary, SubBrief } from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
|
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
|
import type { Layer } from "./M02_MasterTemplete_Api_Fetch";
|
|
import {
|
|
fetchLibLogics as fetchLogics,
|
|
fetchLibLogicSubs as fetchLogicSubs,
|
|
fetchLibrary,
|
|
fetchStandard,
|
|
type LibraryRow,
|
|
type StandardRow,
|
|
} from "./M02_MasterTemplete_Labor_Api";
|
|
import { openStandardSheet } from "./M02_MasterTemplete_Labor_Sheet";
|
|
import type { LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
|
import { ownOwner } from "./M02_MasterTemplete_Layer";
|
|
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
|
import { precomboClasses } from "./M02_MasterTemplete_UI_Side_Precombo";
|
|
import { fetchStructureDef } from "./StructOptions/M02_SO_Api";
|
|
import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; // 창 겉틀
|
|
import "./M02_MasterTemplete_Labor.css";
|
|
import { openModal } from "@ui/ui_template_modal";
|
|
import { createText } from "@ui/ui_template_text";
|
|
|
|
export { withHopyo } from "./M02_MasterTemplete_Hopyo_Pick";
|
|
|
|
/** 구분 보이는 글(707 낱말) — 값(저장 글)은 그대로 */
|
|
const CLASS_TEXT: Record<string, string> = { 실무자료: "실무 엑셀 일위대가" };
|
|
const opts = (values: string[]): { value: string; text: string }[] =>
|
|
[L("M01_All"), ...values].map((v, i) => ({ value: i ? v : "", text: CLASS_TEXT[v] ?? v }));
|
|
|
|
type Scope = Omit<LaborRef, "키">;
|
|
/** 덧붙일 줄 하나 — kind = 종류 고르기 값 */
|
|
export interface PickExtra {
|
|
kind: string;
|
|
key: string;
|
|
tag: string;
|
|
name: string;
|
|
sub: string;
|
|
unit: string;
|
|
onPick: () => void;
|
|
}
|
|
/** 서고 탭 — 일위대가가 있는 층 · 주인(null = 자기 층) · 고르면 참조 */
|
|
export interface LibraryPick {
|
|
layer: Layer;
|
|
owner: string | null;
|
|
/** `alias` = 참조 줄 별칭(324-4 실무 호표 품명 · 없으면 빈칸) */
|
|
pick: (ref: LaborRef, name: string, alias?: string | null) => void;
|
|
/** 서고 줄만(로직 목록 안 받음 · B09 설계내역서 연결 204-2) · 창 제목 */
|
|
libOnly?: boolean;
|
|
title?: string;
|
|
/** 목록 끝 덧붙일 줄(251 · 구조물 조합 · B09 호표) — kind = 종류 고르기 값 */
|
|
extra?: PickExtra[];
|
|
/** 창을 연 뒤 받는 덧줄(324-4 실무 호표) — 받으면 `extra` 뒤에 붙음 */
|
|
more?: () => Promise<PickExtra[]>;
|
|
/** 종류 고르기 칸(251 · 값 logic · precombo · 덧붙인 줄 kind) */
|
|
kindFilter?: Array<{ value: string; text: string }>;
|
|
}
|
|
|
|
/** 고를 수 있는 층 — 위 층만(마스터 → 마스터 · 회사 → 그 회사 · 마스터 · 개인 → 그 개인 · 그 회사 · 마스터) */
|
|
async function upper(layer: Layer, owner: string | null): Promise<Scope[]> {
|
|
if (layer === "project") return [];
|
|
const own =
|
|
layer === "system"
|
|
? null
|
|
: (owner ?? ownOwner(layer, await fetchSessionUser().catch(() => null)));
|
|
const out: Scope[] = [{ 층: "system", 주인: null }];
|
|
if (own) out.push({ 층: "company", 주인: own.split("/")[0] });
|
|
if (own && layer === "personal") out.push({ 층: "personal", 주인: own });
|
|
return out;
|
|
}
|
|
const LAYER_TEXT = {
|
|
system: "Lib_System",
|
|
company: "Lib_Company",
|
|
personal: "Lib_Personal",
|
|
} as const;
|
|
|
|
/** `pre.query` = 찾기 칸에 미리 넣을 글(338 실무자료 고르기 창 · 인용 조항 번호 · 품명 낱말) */
|
|
export function openLogicPicker(
|
|
done: (logic: LogicSummary) => void,
|
|
lib?: LibraryPick,
|
|
pre?: { query?: string },
|
|
): void {
|
|
const subs: SubBrief[] = [];
|
|
let classes: ReturnType<typeof precomboClasses> = []; // 사전조합 새 분류(149-2) — 서고 줄이 있을 때만
|
|
let all: LogicSummary[] = [];
|
|
let std: StandardRow[] | null = null; // 기준 목록(서버 셈) — 오기 전 = 로직 줄 그대로
|
|
let listed: StandardRow[] = [];
|
|
const work = createSelectField({ options: opts([]), compact: true, label: tc("Std_Kind") });
|
|
const sub = createSelectField({ options: opts([]), compact: true, label: tc("Sub") });
|
|
const detail = createSelectField({ options: opts([]), compact: true, label: tc("Detail") });
|
|
detail.select.disabled = true;
|
|
let close = (): void => {};
|
|
|
|
/* 한 목록 — 구분(단가산출 · 일위조합) 칸 · 층 단추는 서고 줄(사전조합)의 층 */
|
|
const layerBar = el("div", { className: "ui-btn-group", attrs: { "data-pick-layers": "" } });
|
|
let scopes: Scope[] = [];
|
|
let scope: Scope | null = null;
|
|
let libRows: { kind: "Kind_Combo"; row: LibraryRow }[] = [];
|
|
// 251 종류 고르기 — 값 "" = 전체 · logic · precombo · 덧붙인 줄 kind
|
|
const kindSel = lib?.kindFilter
|
|
? createSelectField({
|
|
options: [{ value: "", text: L("M01_All") }, ...lib.kindFilter],
|
|
compact: true,
|
|
label: "종류",
|
|
})
|
|
: null;
|
|
const kindOk = (k: string): boolean => !kindSel?.select.value || kindSel.select.value === k;
|
|
let loading = false;
|
|
let seq = 0;
|
|
let later: PickExtra[] = []; // lib.more 로 받은 덧줄
|
|
/* 공용 찾기 목록 — 구분 · 상세구분 2열 · 이름 왼쪽 맞춤 · ↑ ↓ Enter */
|
|
const finder = createFindList({
|
|
placeholder: tc("Pick_Search"),
|
|
filters: [...(kindSel ? [kindSel.root] : []), work.root, sub.root, detail.root],
|
|
extra: [layerBar],
|
|
});
|
|
const table = createDataTable({
|
|
caption: tc("Pick_Title"),
|
|
columns: [
|
|
{ key: "품명", label: tc("Std_Name"), width: 220 },
|
|
{ key: "규격", label: tc("Std_Spec"), width: 220 },
|
|
{ key: "단위", label: tc("Std_Unit"), width: 56 },
|
|
{ key: "금액", label: tc("Std_Money"), kind: "number", width: 96 },
|
|
{ key: "출처", label: tc("Std_Source"), width: 150 },
|
|
],
|
|
rows: [],
|
|
onRowClick: (i) => {
|
|
const row = listed[i];
|
|
const logic = all.find((x) => x.키 === row?.키);
|
|
if (row && logic)
|
|
openStandardSheet(row, () => {
|
|
close();
|
|
done(logic);
|
|
});
|
|
},
|
|
});
|
|
/** 기준 목록이 오기 전 · 못 받으면 — 로직 하나 = 줄 하나(금액 빈칸) */
|
|
const plain = (x: LogicSummary): StandardRow => ({
|
|
키: x.키,
|
|
이름: x.이름,
|
|
규격: "",
|
|
단위: x.결과단위.replace(/^원\/?/, ""),
|
|
금액: null,
|
|
출처: x.출처 ?? "",
|
|
공종: "",
|
|
별칭: x.별칭 ?? "",
|
|
구분: x.구분,
|
|
상세구분: x.상세구분,
|
|
막힘: x.blocked,
|
|
바꿈: {},
|
|
});
|
|
if (pre?.query) finder.search.value = pre.query;
|
|
|
|
const find = (): void => {
|
|
const q = finder.search.value.trim().toLowerCase();
|
|
const rows = std ?? all.map(plain);
|
|
const found = rows.filter(
|
|
(x) =>
|
|
kindOk("logic") &&
|
|
(!work.select.value || x.공종 === work.select.value) &&
|
|
(!sub.select.value || x.구분 === sub.select.value) &&
|
|
(!detail.select.value || x.상세구분 === detail.select.value) &&
|
|
matchLogic(finder.search.value, { ...x, 이름: `${x.이름} ${x.규격}` }).ok, // 공용 · 208-1
|
|
);
|
|
const libFound = libRows.filter(
|
|
(x) =>
|
|
kindOk("precombo") &&
|
|
(!sub.select.value || x.row.구분 === sub.select.value) &&
|
|
(!detail.select.value || x.row.상세구분 === detail.select.value) &&
|
|
(!q || `${x.row.이름} ${x.row.제목 ?? ""}`.toLowerCase().includes(q)),
|
|
);
|
|
const extra = [...(lib?.extra ?? []), ...later].filter(
|
|
(x) =>
|
|
kindOk(x.kind) &&
|
|
!sub.select.value &&
|
|
(!q || `${x.tag} ${x.name} ${x.sub}`.toLowerCase().includes(q)),
|
|
);
|
|
const total = rows.length + libRows.length + (lib?.extra?.length ?? 0) + later.length;
|
|
const shown = found.length + libFound.length + extra.length;
|
|
finder.hint.textContent = loading
|
|
? tc("Loading")
|
|
: shown
|
|
? `${shown} / ${total}`
|
|
: tc("Pick_None");
|
|
listed = found;
|
|
table.root.hidden = !!lib?.libOnly;
|
|
table.setRows(
|
|
found.map((x) => ({
|
|
품명: [x.이름, aliasNote(matchLogic(q, x).alias)].filter(Boolean).join(" · "),
|
|
규격: x.규격,
|
|
단위: `${x.단위}${x.막힘 ? ` · ${tc("Pick_Blocked")}` : ""}`,
|
|
금액: x.금액,
|
|
출처: x.출처,
|
|
})),
|
|
);
|
|
finder.setRows([
|
|
...libFound.map((x) => {
|
|
const at = scope as Scope;
|
|
return {
|
|
key: x.row.이름,
|
|
tag: tc(x.kind),
|
|
name: x.row.제목 || x.row.이름,
|
|
// 707 번호(SC · SP 키) 숨김 — 제목이 없을 때만 이름 자리에 씀
|
|
sub: [
|
|
x.row.제목 ? "" : x.row.이름,
|
|
`${CLASS_TEXT[x.row.구분 ?? ""] ?? (x.row.구분 || tc("Lib_Unsorted"))} ${x.row.상세구분 ?? ""}`.trim(),
|
|
]
|
|
.filter(Boolean)
|
|
.join(" · "),
|
|
unit: x.row.단위 ?? "",
|
|
onPick: () => {
|
|
close();
|
|
lib?.pick({ ...at, 키: x.row.이름 }, x.row.제목 || x.row.이름);
|
|
},
|
|
};
|
|
}),
|
|
...extra.map((x) => ({
|
|
...x,
|
|
onPick: () => {
|
|
close();
|
|
x.onPick();
|
|
},
|
|
})),
|
|
]);
|
|
};
|
|
const loadLib = async (): Promise<void> => {
|
|
if (!lib || !scope) return;
|
|
const mine = ++seq;
|
|
const at = scope;
|
|
loading = true;
|
|
find();
|
|
try {
|
|
const combo = await fetchLibrary(at, "precombo");
|
|
if (mine !== seq) return;
|
|
libRows = combo.map((row) => ({ kind: "Kind_Combo" as const, row }));
|
|
} catch (error) {
|
|
if (mine !== seq) return;
|
|
libRows = [];
|
|
showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error");
|
|
}
|
|
loading = false;
|
|
drawSubs();
|
|
find();
|
|
};
|
|
const drawLayers = (): void =>
|
|
layerBar.replaceChildren(
|
|
...scopes.map((x) =>
|
|
createButton({
|
|
label: tc(LAYER_TEXT[x.층 as keyof typeof LAYER_TEXT]),
|
|
variant: x === scope ? "filled" : "ghost",
|
|
onClick: () => {
|
|
scope = x;
|
|
drawLayers();
|
|
void loadLib();
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
/** 구분 → 상세구분 — 로직 품셈 구분 + 사전조합 새 분류 + 서고 줄 지금 값 · 같은 값은 한 칸 */
|
|
const subMap = (): Map<string, Set<string>> => {
|
|
const m = new Map([...subs, ...classes].map((s) => [s.name, new Set(s.details)]));
|
|
for (const { row } of libRows) {
|
|
if (!row.구분) continue;
|
|
if (!m.has(row.구분)) m.set(row.구분, new Set());
|
|
if (row.상세구분) m.get(row.구분)!.add(row.상세구분);
|
|
}
|
|
return m;
|
|
};
|
|
const drawDetails = (keep: string): void => {
|
|
const d = [...(subMap().get(sub.select.value) ?? [])];
|
|
detail.setOptions(opts(d), d.includes(keep) ? keep : "");
|
|
detail.select.disabled = d.length === 0;
|
|
};
|
|
const drawSubs = (): void => {
|
|
const keep = detail.select.value;
|
|
sub.setOptions(opts([...subMap().keys()]), sub.select.value);
|
|
drawDetails(keep);
|
|
};
|
|
sub.select.addEventListener("change", () => {
|
|
drawDetails("");
|
|
find();
|
|
});
|
|
detail.select.addEventListener("change", find);
|
|
work.select.addEventListener("change", find);
|
|
kindSel?.select.addEventListener("change", find);
|
|
finder.search.addEventListener("input", find);
|
|
|
|
layerBar.style.display = "none";
|
|
if (lib)
|
|
void upper(lib.layer, lib.owner).then((got) => {
|
|
scopes = got;
|
|
scope = got[got.length - 1] ?? null; // 기본 = 가장 가까운 층
|
|
if (!got.length) return;
|
|
layerBar.style.display = "";
|
|
drawLayers();
|
|
void loadLib();
|
|
void fetchStructureDef().then(
|
|
(def) => {
|
|
classes = precomboClasses(def);
|
|
drawSubs();
|
|
},
|
|
() => undefined, // 못 받으면 서고 줄 지금 값만
|
|
);
|
|
});
|
|
|
|
// 모달 부품 — 제목줄 [닫기] · 바깥 누르기 · Esc · 위 고정
|
|
({ close } = openModal({
|
|
title: lib?.title ?? tc("Pick_Title"),
|
|
closeLabel: tc("Pick_Close"),
|
|
dialogClass: "m01-logic__pick m01-combo",
|
|
width: "960px", // 기준 목록 표 다섯 칸(704-1)
|
|
backdropClass: "m01-logic__backdrop",
|
|
mount: (body) =>
|
|
body.append(
|
|
el("div", {
|
|
className: "m02-std-pick",
|
|
children: [finder.root, createText(tc("Std_Hint")), table.root],
|
|
}),
|
|
),
|
|
}));
|
|
finder.search.focus();
|
|
|
|
void lib?.more?.().then(
|
|
(got) => ((later = got), find()),
|
|
(error) => showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error"),
|
|
);
|
|
if (!lib?.libOnly)
|
|
void Promise.all([fetchLogicSubs(), fetchLogics()])
|
|
.then(([s, l]) => {
|
|
subs.push(...s);
|
|
all = l;
|
|
drawSubs();
|
|
find();
|
|
return fetchStandard().then((got) => {
|
|
std = got.줄.length ? got.줄 : null; // 빈 줄 = 서버가 아직 셈 — 로직 줄 그대로
|
|
work.setOptions(opts(got.공종), "");
|
|
find();
|
|
});
|
|
})
|
|
.catch((error) =>
|
|
showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error"),
|
|
);
|
|
}
|