Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts
T
eomsangdonandClaude Opus 5.5 90d27128e1 feat(M02): 50-3 ④ 로직 고르기 창 서고 탭 · 참조 줄 표 · 줄 상세
- [로직 추가] 창 탭 단가산출 · 단가승격 · 사전조합 — 서고 탭은 층 단추(위 층만 · 기본 = 가장 가까운 층) + 그 층 목록 · 고르면 참조 줄 더함(같은 참조 두 번 막음)
- 담은 로직 표 참조 줄 — 줄 가름 lineId · 키 · 이름 · 단위 = 가리킨 문서 것 · 구분 = 참조 키 머리 · [옵션] 없음(넣을 값은 승격 때 고정)
- 줄 상세 — 단가승격 = 원 로직 키 + 원 로직 표 + 기준(1 단위) · 사전조합 = 안 줄 목록 한 층(구분 · 이름 · 수량 · 금액 넷) + 기준 · 아래 반영 합
- 문서 정리 · 보기 패널 · 다이어그램이 참조 줄을 받음(다이어그램은 단가산출 줄만)
- 시험: 승격 줄 원 로직 · 표 · 사전조합 안 줄 · ui_checks 50-3-4 시나리오(목록 · calc 는 탭 안 가짜 · 공유 마스터 안 씀)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
2026-10-05 18:59:40 +09:00

250 lines
9.1 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_Labor_Pick.ts
* [로직 더하기] 창 — 구분 → 상세구분 → 찾기로 사용자가 로직을 하나씩 고름(자동 추천 없음) ·
* 탭 단가산출 · 단가승격 · 사전조합(50-3 ④) — 서고 탭은 층 단추(위 층만 · 짝 `M02_Labor_Ref.readable`) +
* 그 층 목록 · 고르면 참조 {층 · 주인 · 키} · 프로젝트 · 카드엔 서고 탭 없음
* ========================================================================== */
import {
createButton,
createInputField,
createSelectField,
el,
showToast,
} from "@ui/ui_template_elements";
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,
type LibraryRow,
} from "./M02_MasterTemplete_Labor_Api";
import type { LaborRef } from "./M02_MasterTemplete_Labor_Calc";
import { ownOwner } from "./M02_MasterTemplete_Layer";
import { tc } from "./M02_MasterTemplete_Labor_Text";
import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; // 창 겉틀
import "./M02_MasterTemplete_Labor.css";
const opts = (values: string[]): { value: string; text: string }[] =>
[L("M01_All"), ...values].map((v, i) => ({ value: i ? v : "", text: v }));
type Scope = Omit<LaborRef, "키">;
/** 서고 탭 — 일위대가가 있는 층 · 주인(null = 자기 층) · 고르면 참조 */
export interface LibraryPick {
layer: Layer;
owner: string | null;
pick: (ref: LaborRef, name: string) => void;
}
/** 고를 수 있는 층 — 위 층만(마스터 → 마스터 · 회사 → 그 회사 · 마스터 · 개인 → 그 개인 · 그 회사 · 마스터) */
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;
export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: LibraryPick): void {
const subs: SubBrief[] = [];
let all: LogicSummary[] = [];
const sub = createSelectField({ options: opts([]), compact: true, label: tc("Sub") });
const detail = createSelectField({ options: opts([]), compact: true, label: tc("Detail") });
detail.select.disabled = true;
const searchField = createInputField({ type: "search", placeholder: tc("Pick_Search") });
const list = el("div", { className: "m01-logic__pick-list" });
const hint = el("p", { className: "m01-logic__muted", text: tc("Pick_Hint") });
const close = (): void => backdrop.remove();
const find = (): void => {
const q = searchField.input.value.trim().toLowerCase();
const found = all.filter(
(x) =>
(!sub.select.value || x.구분 === sub.select.value) &&
(!detail.select.value || x.상세구분 === detail.select.value) &&
(!q || `${x.키} ${x.이름}`.toLowerCase().includes(q)),
);
hint.textContent = found.length ? `${found.length} / ${all.length}` : tc("Pick_None");
list.replaceChildren(
...found.slice(0, 100).map((x) => {
const row = el("button", {
className: "m01-logic__pick-row m01-logic__pick-row--button",
attrs: { type: "button", "data-pick": x.키 },
children: [
el("div", {
className: "m01-logic__stack",
children: [
el("strong", { text: x.이름 }),
el("span", {
className: "m01-logic__muted",
text: `${x.키} · ${x.구분} ${x.상세구분}`.trim(),
}),
],
}),
el("span", { text: `${x.결과단위}${x.blocked ? ` · ${tc("Pick_Blocked")}` : ""}` }),
],
});
row.addEventListener("click", () => {
close();
done(x);
});
return row;
}),
);
};
sub.select.addEventListener("change", () => {
const d = subs.find((s) => s.name === sub.select.value)?.details ?? [];
detail.setOptions(opts(d), "");
detail.select.disabled = d.length === 0;
});
/* 서고 탭(50-3 ④) — 탭 · 층 단추 · 목록 */
const filters = el("div", {
className: "m01-logic__filters",
children: [
sub.root,
detail.root,
searchField.root,
createButton({ label: tc("Pick_Find"), variant: "filled", onClick: find }),
],
});
const tabs = el("div", { className: "ui-btn-group", attrs: { "data-pick-tabs": "" } });
const layerBar = el("div", { className: "ui-btn-group", attrs: { "data-pick-layers": "" } });
let tab: "logic" | "promote" | "precombo" = "logic";
let scopes: Scope[] = [];
let scope: Scope | null = null;
let seq = 0;
const libList = async (): Promise<void> => {
if (!lib || !scope || tab === "logic") return;
const mine = ++seq;
const at = scope;
list.replaceChildren();
hint.textContent = tc("Loading");
let rows: LibraryRow[] = [];
try {
rows = await fetchLibrary(at, tab);
} catch (error) {
if (mine === seq)
hint.textContent = error instanceof Error ? error.message : tc("Load_Failed");
return;
}
if (mine !== seq) return;
hint.textContent = rows.length ? `${rows.length}` : tc("Pick_None");
list.replaceChildren(
...rows.map((x) => {
const row = el("button", {
className: "m01-logic__pick-row m01-logic__pick-row--button",
attrs: { type: "button", "data-pick": x.이름 },
children: [
el("div", {
className: "m01-logic__stack",
children: [
el("strong", { text: x.제목 || x.이름 }),
el("span", { className: "m01-logic__muted", text: x.이름 }),
],
}),
el("span", { text: x.단위 ?? "" }),
],
});
row.addEventListener("click", () => {
close();
lib.pick({ ...at, 키: x.이름 }, x.제목 || x.이름);
});
return row;
}),
);
};
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 libList();
},
}),
),
);
const drawTabs = (): void => {
tabs.replaceChildren(
...(["logic", "promote", "precombo"] as const).map((k) =>
createButton({
label: tc(
k === "logic" ? "Kind_Logic" : k === "promote" ? "Kind_Promote" : "Lib_Precombo",
),
variant: k === tab ? "filled" : "ghost",
onClick: () => {
tab = k;
drawTabs();
filters.style.display = k === "logic" ? "" : "none"; // 공용 꼴 display 가 hidden 을 이김
layerBar.style.display = k === "logic" ? "none" : "";
if (k === "logic") {
list.replaceChildren();
hint.textContent = tc("Pick_Hint");
} else void libList();
},
}),
),
);
};
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;
drawTabs();
drawLayers();
});
const dialog = el("div", {
className: "m01-logic__pick m01-combo",
attrs: { role: "dialog", "aria-label": tc("Pick_Title") },
children: [
el("div", {
className: "m01-logic__section-head",
children: [
el("h3", { text: tc("Pick_Title") }),
createButton({ label: tc("Pick_Close"), variant: "ghost", onClick: close }),
],
}),
tabs,
filters,
layerBar,
hint,
list,
],
});
const backdrop = el("div", { className: "m01-logic__backdrop", children: [dialog] });
backdrop.addEventListener("click", (ev) => ev.target === backdrop && close());
backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close());
document.body.append(backdrop);
searchField.input.addEventListener("keydown", (ev) => ev.key === "Enter" && find());
searchField.input.focus();
void Promise.all([fetchLogicSubs(), fetchLogics()])
.then(([s, l]) => {
subs.push(...s);
all = l;
sub.setOptions(opts(s.map((x) => x.name)), "");
})
.catch((error) =>
showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error"),
);
}