feat(M02): 서고 구분 · 상세구분 · 연결 화면(51-12)
- 서고 목록표에 구분 · 상세구분 칸(키 뒤) - 단가승격 상세 정보 칸 = 구분 · 상세구분 · 연결(원 로직 키 · 출처) 읽기만 - 사전조합 상세 정보 칸 = 구분 · 상세구분 고르기(로직 구분 목록 logic-lib/subs · 구분 바꾸면 상세구분 = 그 첫 줄) - 새 사전조합 처음 값 = 로직 구분 목록 첫 줄 - 로직 고르기 창 구분 · 상세구분 거름에 승격 · 사전조합 줄도 걸림 - 화면 시나리오 51-12 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
This commit is contained in:
@@ -233,6 +233,9 @@ export interface LibraryRow {
|
||||
이름: string;
|
||||
제목?: string | null;
|
||||
단위?: string | null;
|
||||
/** 51-12 — 승격 = 원 로직 것 · 사전조합 = 고른 것 */
|
||||
구분?: string | null;
|
||||
상세구분?: string | null;
|
||||
}
|
||||
/** 서고 목록 — 그 층(주인) 단가승격 `promote` · 사전조합 `precombo` 문서 */
|
||||
export const fetchLibrary = async (
|
||||
|
||||
@@ -9,15 +9,23 @@
|
||||
* · 같은 로직 두 번 · 같은 자재 두 번 막음 · 빈 일위대가도 저장 됨
|
||||
* · 오른쪽 「원문」 접는 패널(47-4 · 상세만 · 공용 `ui_template_side_panels`) = 고른 줄 로직의 출처 원문
|
||||
* · 서고 모드(50-3 · `mode`) = 단가승격 · 사전조합 — 이름 칸 · 단가산출 로직 표 · 원문만(자재 · 면적 · 다이어그램 없음) ·
|
||||
* 정보 칸 구분 · 상세구분(51-12 · 승격 = 읽기만 + 연결 · 사전조합 = 고름) ·
|
||||
* 승격은 로직 한 줄(입력값 고정 · 더하기 · 순서 · 수량 없음) · 사전조합 = 일위대가 상세와 같은 [로직 추가] · 표 · 동작(51-11) · 수량 손 입력(기본 1)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
createSelectField,
|
||||
el,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createSidePanels } from "@ui/ui_template_side_panels";
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
import { setPendingLogicKey } from "../M01_MasterData/M01_MasterData_UI_Side";
|
||||
import { createSourceView } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Source";
|
||||
import type { SubBrief } from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet";
|
||||
import { recalcWorkbook } from "../A00_Common/spreadsheet/spreadsheet_recalc";
|
||||
import {
|
||||
@@ -33,6 +41,7 @@ import { lineId, settle, type LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import {
|
||||
areaLinks,
|
||||
calcLabor,
|
||||
fetchLibLogicSubs,
|
||||
fetchLogicInputs,
|
||||
normalizeLabor,
|
||||
type LaborCalc,
|
||||
@@ -457,6 +466,69 @@ export function mountLaborDetail(
|
||||
opt.onChanged(dirty());
|
||||
});
|
||||
|
||||
/** 서고 구분 · 상세구분(51-12) — 승격 = 원 로직 것 읽기만 + 연결(원 로직 키 · 출처) ·
|
||||
* 사전조합 = 로직 구분 목록(logic-lib/subs)에서 고름(구분 바꾸면 상세구분 = 그 첫 줄) */
|
||||
const meta = cur as { 구분?: string | null; 상세구분?: string | null; 연결?: unknown };
|
||||
const readField = (label: string, field: string, value: string): HTMLElement => {
|
||||
const f = createInputField({ type: "text", label });
|
||||
f.root.dataset.laborField = field;
|
||||
f.input.value = value;
|
||||
f.input.readOnly = true;
|
||||
return f.root;
|
||||
};
|
||||
const libMeta = (): HTMLElement[] => {
|
||||
if (promote) {
|
||||
const link = (meta.연결 ?? {}) as { 로직?: string; 키?: string; 출처?: string };
|
||||
return [
|
||||
readField(tc("Sub"), "구분", meta.구분 ?? ""),
|
||||
readField(tc("Detail"), "상세구분", meta.상세구분 ?? ""),
|
||||
readField(
|
||||
tc("Lib_Link"),
|
||||
"연결",
|
||||
[link.로직 ?? link.키, link.출처].filter(Boolean).join(" · "),
|
||||
),
|
||||
];
|
||||
}
|
||||
const sub = createSelectField({ options: [], label: tc("Sub"), disabled: !edit });
|
||||
const detail = createSelectField({ options: [], label: tc("Detail"), disabled: !edit });
|
||||
sub.root.dataset.laborField = "구분";
|
||||
detail.root.dataset.laborField = "상세구분";
|
||||
let subs: SubBrief[] = [];
|
||||
// 목록에 없는 지금 값도 선택지로 둠
|
||||
const opts = (names: string[], now: string | null | undefined) =>
|
||||
[...new Set([...(now ? [now] : []), ...names])].map((v) => ({ value: v, text: v }));
|
||||
const fill = (): void => {
|
||||
sub.setOptions(
|
||||
opts(
|
||||
subs.map((s) => s.name),
|
||||
meta.구분,
|
||||
),
|
||||
meta.구분 ?? "",
|
||||
);
|
||||
const details = subs.find((s) => s.name === meta.구분)?.details ?? [];
|
||||
detail.setOptions(opts(details, meta.상세구분), meta.상세구분 ?? "");
|
||||
};
|
||||
sub.select.addEventListener("change", () => {
|
||||
meta.구분 = sub.select.value;
|
||||
meta.상세구분 = subs.find((s) => s.name === meta.구분)?.details[0] ?? null;
|
||||
fill();
|
||||
opt.onChanged(dirty());
|
||||
});
|
||||
detail.select.addEventListener("change", () => {
|
||||
meta.상세구분 = detail.select.value;
|
||||
opt.onChanged(dirty());
|
||||
});
|
||||
fill();
|
||||
void fetchLibLogicSubs().then(
|
||||
(got) => {
|
||||
subs = got;
|
||||
fill();
|
||||
},
|
||||
() => undefined,
|
||||
);
|
||||
return [sub.root, detail.root];
|
||||
};
|
||||
|
||||
const editor = (body: HTMLElement): HTMLElement =>
|
||||
side ? el("div", { className: "ui-side-panels-row", children: [body, side.root] }) : body;
|
||||
host.replaceChildren(
|
||||
@@ -473,7 +545,11 @@ export function mountLaborDetail(
|
||||
children: [
|
||||
el("div", {
|
||||
className: "m01lab__info",
|
||||
children: [...(lib ? [nameField.root] : []), unit.root, origin.root],
|
||||
children: [
|
||||
...(lib ? [nameField.root, ...libMeta()] : []),
|
||||
unit.root,
|
||||
origin.root,
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -82,9 +82,13 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra
|
||||
(!detail.select.value || x.상세구분 === detail.select.value) &&
|
||||
(!q || `${x.키} ${x.이름}`.toLowerCase().includes(q)),
|
||||
);
|
||||
const libFound = sub.select.value
|
||||
? []
|
||||
: libRows.filter((x) => !q || `${x.row.이름} ${x.row.제목 ?? ""}`.toLowerCase().includes(q));
|
||||
// 서고 줄도 구분 · 상세구분으로 거름(51-12)
|
||||
const libFound = libRows.filter(
|
||||
(x) =>
|
||||
(!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 total = all.length + libRows.length;
|
||||
const shown = found.length + libFound.length;
|
||||
finder.hint.textContent = loading
|
||||
@@ -110,7 +114,7 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra
|
||||
key: x.row.이름,
|
||||
tag: tc(x.kind),
|
||||
name: x.row.제목 || x.row.이름,
|
||||
sub: x.row.이름,
|
||||
sub: `${x.row.이름} · ${x.row.구분 ?? ""} ${x.row.상세구분 ?? ""}`.trim(),
|
||||
unit: x.row.단위 ?? "",
|
||||
onPick: () => {
|
||||
close();
|
||||
|
||||
@@ -124,6 +124,8 @@ const TEXT = {
|
||||
Lib_Unit: ["단위", "Unit"],
|
||||
Lib_Base: ["기준 금액", "Base amount"],
|
||||
Lib_Date: ["고친 날", "Modified"],
|
||||
// 서고 구분 · 연결(51-12)
|
||||
Lib_Link: ["연결", "Linked logic"],
|
||||
} as const satisfies Record<string, readonly [string, string]>;
|
||||
|
||||
export type ComboTextKey = keyof typeof TEXT;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Library_List.ts
|
||||
* 일위대가 조합 서고 목록표(51-4) — 좌측 항목(단가산출 승격 · 일위대가 사전조합)을 누르면 상세 칸에 뜸.
|
||||
* 표 = 고름 · 키 · 이름 · 단위 · 기준 금액(서버 1 단위 계) · 고친 날 · 줄 누름 = 그 문서 상세.
|
||||
* 표 = 고름 · 키 · 구분 · 상세구분(51-12) · 이름 · 단위 · 기준 금액(서버 1 단위 계) · 고친 날 · 줄 누름 = 그 문서 상세.
|
||||
* 머리 [생성] · [복사](고른 줄) · [삭제](고른 줄 · 쓰는 곳 있으면 서버 409 까닭 그대로).
|
||||
* 꼴 = M01 단가산출 로직 목록 — 찾기 줄(`buildHeadFilters`) · 쪽(`buildPager`) · 표(`m01-master__grid`) 를 M01 에서 import.
|
||||
* ========================================================================== */
|
||||
@@ -30,12 +30,19 @@ import {
|
||||
type LibraryBody,
|
||||
type TemplateInfo,
|
||||
} from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { fetchLibLogicSubs } from "./M02_MasterTemplete_Labor_Api";
|
||||
import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import type { PrecomboKind } from "./M02_MasterTemplete_UI_Side_Precombo";
|
||||
|
||||
/** 목록 줄 — 서버가 문서 `이름`(제목) · 단위 · 1 단위 기준 계를 실어 보냄 */
|
||||
type Row = TemplateInfo & { 제목?: string | null; 단위?: string | null; 기준금액?: number | null };
|
||||
type Row = TemplateInfo & {
|
||||
제목?: string | null;
|
||||
단위?: string | null;
|
||||
기준금액?: number | null;
|
||||
구분?: string | null;
|
||||
상세구분?: string | null;
|
||||
};
|
||||
|
||||
export interface LibraryListOptions {
|
||||
layer: Layer;
|
||||
@@ -99,9 +106,15 @@ export function mountLibraryList(
|
||||
attrs: { "data-lib-key": row.이름 },
|
||||
children: [
|
||||
pick,
|
||||
...[row.이름, row.제목 ?? "", row.단위 ?? "", money(row.기준금액), row.수정일].map(
|
||||
(v) => el("td", { text: v, attrs: { title: v } }),
|
||||
),
|
||||
...[
|
||||
row.이름,
|
||||
row.구분 ?? "",
|
||||
row.상세구분 ?? "",
|
||||
row.제목 ?? "",
|
||||
row.단위 ?? "",
|
||||
money(row.기준금액),
|
||||
row.수정일,
|
||||
].map((v) => el("td", { text: v, attrs: { title: v } })),
|
||||
],
|
||||
});
|
||||
tr.addEventListener("click", () => opt.open(row.이름, libraryLabel(row)));
|
||||
@@ -109,7 +122,7 @@ export function mountLibraryList(
|
||||
})
|
||||
: [
|
||||
el("tr", {
|
||||
children: [el("td", { text: L("M02_NoTemplates"), attrs: { colspan: "6" } })],
|
||||
children: [el("td", { text: L("M02_NoTemplates"), attrs: { colspan: "8" } })],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
@@ -164,7 +177,8 @@ export function mountLibraryList(
|
||||
const create = async (body: LibraryBody, label: string): Promise<void> =>
|
||||
made((await newLibrary(opt.layer, opt.kind, body)).이름, label);
|
||||
|
||||
/** 단가승격 = 단가산출 로직 고르기 → 이름 · 단위는 로직 것 · 사전조합 = 이름만 · 빈 묶음 */
|
||||
/** 단가승격 = 단가산출 로직 고르기 → 이름 · 단위 · 구분 · 연결은 로직 것(서버가 채움) ·
|
||||
* 사전조합 = 이름 + 구분 · 상세구분 처음 값 = 로직 구분 목록 첫 줄(51-12 · 상세에서 바꿈) · 빈 묶음 */
|
||||
btnNew.addEventListener("click", () => {
|
||||
if (opt.kind === "promote")
|
||||
return openLogicPicker((x) => {
|
||||
@@ -177,9 +191,17 @@ export function mountLibraryList(
|
||||
showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error"),
|
||||
);
|
||||
});
|
||||
nameDialog(L("M02_NewTitle"), "", (name) =>
|
||||
create({ 문서: { 이름: name, 단위: null, 담은로직: [] } }, name),
|
||||
);
|
||||
nameDialog(L("M02_NewTitle"), "", async (name) => {
|
||||
const first = (await fetchLibLogicSubs())[0];
|
||||
const 문서 = {
|
||||
이름: name,
|
||||
구분: first?.name ?? null,
|
||||
상세구분: first?.details[0] ?? null,
|
||||
단위: null,
|
||||
담은로직: [],
|
||||
};
|
||||
await create({ 문서 }, name);
|
||||
});
|
||||
});
|
||||
|
||||
/** 복사 = 같은 층 새 키(서버 · 본뜬 = 원래 키) → 고른 이름으로 고쳐 씀 */
|
||||
@@ -221,7 +243,16 @@ export function mountLibraryList(
|
||||
page = 1;
|
||||
draw();
|
||||
});
|
||||
const heads = ["", "Lib_Key", "Lib_Name", "Lib_Unit", "Lib_Base", "Lib_Date"] as const;
|
||||
const heads = [
|
||||
"",
|
||||
"Lib_Key",
|
||||
"Sub",
|
||||
"Detail",
|
||||
"Lib_Name",
|
||||
"Lib_Unit",
|
||||
"Lib_Base",
|
||||
"Lib_Date",
|
||||
] as const;
|
||||
host.replaceChildren(
|
||||
buildHeadFilters([searchField.root, pager]),
|
||||
el("div", {
|
||||
|
||||
Reference in New Issue
Block a user