Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Find_Pick.ts
|
||||
* M02 찾아 넣기 창(PLAN 721-2 · 요구서_내역16 1장 6 · 7 · 2장 1 · 2) — 탭 [M01 전체 | M02 단가산출 | 일위대가] ·
|
||||
* 찾기 칸 하나 · 표 하나(품명 · 규격 · 단위 · 금액 · 구분 · 출처) · 옛 거름 값(종류 · 공종 · 구분 · 상세구분 · 층) = 「구분」 · 「출처」 열
|
||||
* · M01 전체 = 단가산출 로직(기준 목록 `/logic-lib/standard` · 줄 누름 = 계산서 보기 → [담기]) +
|
||||
* 인력(값 있는 노임 직종) · 재료(자재품목) = `/logic-lib/pick` 서버 찾기(친 낱말 · 상한 200) · 기계 뺌 · 실무 호표 뺌
|
||||
* · M02 단가산출 · 일위대가 = 위 층(마스터 › 회사 › 개인) 서고 목록(`/layers/{층}/templates`) · 고름 = 참조 {층 · 주인 · 키}
|
||||
* · 인력 · 재료 고름 = `element` · 일위대가 고정 줄 꼴 = `fixedOf`(705-1 이음 노임 LB · 자재 MT · 수량 1 은 부르는 쪽) ·
|
||||
* `element` 없으면 그 줄 회색 막힘(까닭 = 일위대가 표 개편 뒤 · 723)
|
||||
* · 부르는 곳 = M02 단가산출 [기본 로직 선택](`logicOnly`) · [항목 추가](`Sangeun_Pick`) · 일위대가 [로직 추가]
|
||||
* (`Labor_Pick` 이 `lib.sangeun` 이면 이 창) — B09 · 불러오기는 옛 창(`Labor_Pick`)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createDataTable } from "@ui/ui_template_data_table";
|
||||
import { createInputField, el, showToast } from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { aliasNote, matchLogic } from "@ui/ui_template_logic_find";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { createTabs } from "@ui/ui_template_tabs";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
||||
import type { LogicSummary } from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import { call, type Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { fetchLibLogics, fetchStandard, type StandardRow } from "./M02_MasterTemplete_Labor_Api";
|
||||
import type { LaborFixed, LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import { openStandardSheet } from "./M02_MasterTemplete_Labor_Sheet";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import { ownOwner } from "./M02_MasterTemplete_Layer";
|
||||
import { listLayerDocs, SANGEUN_KIND, type SangeunRow } from "./M02_MasterTemplete_Sangeun_Pick";
|
||||
import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; // 창 겉틀
|
||||
import "./M02_MasterTemplete_Labor.css";
|
||||
|
||||
type Scope = Omit<LaborRef, "키">;
|
||||
type Tab = "m01" | "sk" | "pre";
|
||||
|
||||
/** M01 인력 · 재료 한 줄(`/logic-lib/pick` · 코드 = LB- · MT- 키) */
|
||||
export interface M01Element {
|
||||
종류: "인력" | "재료";
|
||||
코드: string;
|
||||
이름: string;
|
||||
규격: string;
|
||||
단위: string;
|
||||
값: number | null;
|
||||
}
|
||||
|
||||
export interface FindPickOptions {
|
||||
layer: Layer;
|
||||
owner: string | null;
|
||||
title?: string;
|
||||
/** M01 단가산출 로직 고름 */
|
||||
logic: (x: LogicSummary) => void;
|
||||
/** M02 단가산출 · 일위대가 고름(참조) */
|
||||
ref?: (ref: LaborRef, name: string) => void;
|
||||
/** M01 인력 · 재료 고름 — 없으면 그 줄 회색 막힘 */
|
||||
element?: (e: M01Element) => void;
|
||||
/** 로직 줄만(탭 · 인력 · 재료 없음 — [기본 로직 선택]) */
|
||||
logicOnly?: boolean;
|
||||
}
|
||||
|
||||
/** 일위대가 고정 줄(705-1 이음 꼴) — 인력 = 노임 LB · 재료 = 자재 MT · 단가 = 지금 값(셈은 단가 책 값) */
|
||||
export function fixedOf(e: M01Element): LaborFixed {
|
||||
const labor = e.종류 === "인력";
|
||||
return {
|
||||
이름: e.이름,
|
||||
규격: e.규격,
|
||||
단위: e.단위,
|
||||
단가: { 노무비: labor ? (e.값 ?? 0) : 0, 재료비: labor ? 0 : (e.값 ?? 0), 경비: 0 },
|
||||
이음: { 노임: labor ? e.코드 : null, 자재: labor ? null : e.코드, 기계: null },
|
||||
};
|
||||
}
|
||||
|
||||
/** 고를 수 있는 층 — 위 층만(마스터 → 마스터 · 회사 → 그 회사 · 마스터 · 개인 → 그 개인 · 그 회사 · 마스터) */
|
||||
export 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: Record<string, "Lib_System" | "Lib_Company" | "Lib_Personal"> = {
|
||||
system: "Lib_System",
|
||||
company: "Lib_Company",
|
||||
personal: "Lib_Personal",
|
||||
};
|
||||
/** 노임 「원/일」 → 줄 단위 「인」 · 「원/시」 → 「시」 · 재료 단위 그대로 */
|
||||
const unitOf = (u: string): string => (u === "원/일" ? "인" : u.replace(/^원\//, ""));
|
||||
const join = (...xs: Array<string | null | undefined>): string => xs.filter(Boolean).join(" · ");
|
||||
|
||||
interface PickItem {
|
||||
ref: string;
|
||||
구분?: string | null;
|
||||
이름: string;
|
||||
규격?: string | null;
|
||||
단위?: string | null;
|
||||
값: unknown;
|
||||
}
|
||||
/** 표 한 줄 + 누름 */
|
||||
interface Line {
|
||||
품명: string;
|
||||
규격: string;
|
||||
단위: string;
|
||||
금액: number | null;
|
||||
구분: string;
|
||||
출처: string;
|
||||
dim?: boolean;
|
||||
go: () => void;
|
||||
}
|
||||
|
||||
export function openFindPicker(opt: FindPickOptions): void {
|
||||
let tab: Tab = "m01";
|
||||
let close = (): void => {};
|
||||
let logics: LogicSummary[] = [];
|
||||
let std: StandardRow[] | null = null;
|
||||
let people: PickItem[] = [];
|
||||
let goods: PickItem[] = [];
|
||||
const lists: Record<"sk" | "pre", SangeunRow[] | null> = { sk: null, pre: null };
|
||||
let shown: Line[] = [];
|
||||
let seq = 0;
|
||||
let timer = 0;
|
||||
|
||||
const search = createInputField({ type: "search", placeholder: L("M02_FindSearch") });
|
||||
const hint = createText("");
|
||||
const table = createDataTable({
|
||||
caption: L("M02_FindTitle"),
|
||||
columns: [
|
||||
{ key: "품명", label: tc("Std_Name"), width: 200 },
|
||||
{ key: "규격", label: tc("Std_Spec"), width: 110 },
|
||||
{ key: "단위", label: tc("Std_Unit"), width: 44 },
|
||||
{ key: "금액", label: tc("Std_Money"), kind: "number", width: 84 },
|
||||
{ key: "구분", label: L("M02_FindClass"), width: 180 },
|
||||
{ key: "출처", label: tc("Std_Source"), width: 120 },
|
||||
],
|
||||
rows: [],
|
||||
rowClass: (i) => (shown[i]?.dim ? "is-dim" : null),
|
||||
onRowClick: (i) => shown[i]?.go(),
|
||||
});
|
||||
const take = (f: () => void) => (): void => {
|
||||
close();
|
||||
f();
|
||||
};
|
||||
|
||||
const logicLines = (q: string): Line[] =>
|
||||
(std ?? logics.map(plain)).flatMap((x) => {
|
||||
const hit = matchLogic(q, { ...x, 이름: `${x.이름} ${x.규격}` }); // 공용 · 208-1
|
||||
if (!hit.ok) return [];
|
||||
const logic = logics.find((y) => y.키 === x.키);
|
||||
return [
|
||||
{
|
||||
품명: join(x.이름, aliasNote(hit.alias)),
|
||||
규격: x.규격,
|
||||
단위: `${x.단위}${x.막힘 ? ` · ${tc("Pick_Blocked")}` : ""}`,
|
||||
금액: x.금액,
|
||||
구분: join(L("M02_FindLogic"), x.공종, x.구분, x.상세구분),
|
||||
출처: x.출처,
|
||||
go: () =>
|
||||
logic &&
|
||||
openStandardSheet(
|
||||
x,
|
||||
take(() => opt.logic(logic)),
|
||||
),
|
||||
},
|
||||
];
|
||||
});
|
||||
const elementLines = (kind: M01Element["종류"], items: PickItem[]): Line[] =>
|
||||
items.map((m) => {
|
||||
const e: M01Element = {
|
||||
종류: kind,
|
||||
코드: m.ref,
|
||||
이름: m.이름,
|
||||
규격: m.규격 ?? "",
|
||||
단위: unitOf(m.단위 ?? ""),
|
||||
값: Number.isFinite(Number(m.값)) && m.값 !== null ? Number(m.값) : null,
|
||||
};
|
||||
const put = opt.element;
|
||||
return {
|
||||
품명: m.이름,
|
||||
규격: e.규격,
|
||||
단위: e.단위,
|
||||
금액: e.값,
|
||||
구분: join(kind, m.구분, put ? "" : L("M02_FindElementLater")),
|
||||
출처: `M01 ${m.ref}`,
|
||||
dim: !put,
|
||||
go: put ? take(() => put(e)) : () => showToast(L("M02_FindElementLater"), "warning"),
|
||||
};
|
||||
});
|
||||
const docLines = (rows: SangeunRow[], kind: string): Line[] =>
|
||||
rows.map((x) => ({
|
||||
품명: x.제목 || x.이름,
|
||||
규격: "",
|
||||
단위: x.단위 ?? "",
|
||||
금액: x.기준금액,
|
||||
구분: join(kind, x.구분, x.상세구분),
|
||||
출처: LAYER_TEXT[x.ref.층] ? tc(LAYER_TEXT[x.ref.층]) : x.ref.층,
|
||||
go: take(() => opt.ref?.(x.ref, x.제목 || x.이름)),
|
||||
}));
|
||||
|
||||
function draw(): void {
|
||||
const q = search.input.value.trim();
|
||||
if (tab === "m01") {
|
||||
shown = logicLines(q);
|
||||
if (!opt.logicOnly)
|
||||
shown.push(...elementLines("인력", people), ...elementLines("재료", goods));
|
||||
} else {
|
||||
const words = q.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
const kind = tab === "sk" ? L("M02_PickSangeun") : L("M02_PrecomboTitle");
|
||||
shown = docLines(lists[tab] ?? [], kind).filter((x) => {
|
||||
const text = join(x.품명, x.구분).toLowerCase();
|
||||
return words.every((w) => text.includes(w));
|
||||
});
|
||||
}
|
||||
const loading = tab === "m01" ? !logics.length : !lists[tab];
|
||||
hint.textContent = loading ? tc("Loading") : shown.length ? `${shown.length}` : tc("Pick_None");
|
||||
table.setRows(shown.map(({ go: _go, dim: _dim, ...row }) => row));
|
||||
}
|
||||
|
||||
/** 인력 · 재료 = 서버 찾기(친 낱말) — 재료는 낱말 있을 때만 · 늦게 온 답은 버림 */
|
||||
async function findElements(): Promise<void> {
|
||||
if (opt.logicOnly) return;
|
||||
const mine = ++seq;
|
||||
const q = search.input.value.trim();
|
||||
const get = (kind: "job" | "price"): Promise<PickItem[]> =>
|
||||
call<{ items: PickItem[] }>(
|
||||
`/logic-lib/pick?kind=${kind}&q=${encodeURIComponent(q)}&limit=200`,
|
||||
).then((d) => d.items);
|
||||
try {
|
||||
const [p, g] = await Promise.all([get("job"), q ? get("price") : Promise.resolve([])]);
|
||||
if (mine !== seq) return;
|
||||
[people, goods] = [p, g];
|
||||
} catch (error) {
|
||||
if (mine !== seq) return;
|
||||
showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error");
|
||||
}
|
||||
if (tab === "m01") draw();
|
||||
}
|
||||
|
||||
search.input.addEventListener("input", () => {
|
||||
draw();
|
||||
window.clearTimeout(timer);
|
||||
timer = window.setTimeout(() => void findElements(), 250);
|
||||
});
|
||||
|
||||
const tabs = opt.logicOnly
|
||||
? null
|
||||
: createTabs({
|
||||
tabs: [
|
||||
{ key: "m01", label: L("M02_FindM01") },
|
||||
{ key: "sk", label: L("M02_PickSangeun") },
|
||||
{ key: "pre", label: L("M02_PrecomboTitle") },
|
||||
],
|
||||
value: tab,
|
||||
kind: "line",
|
||||
onChange: (key) => {
|
||||
tab = key as Tab;
|
||||
draw();
|
||||
},
|
||||
});
|
||||
|
||||
({ close } = openModal({
|
||||
title: opt.title ?? L("M02_FindTitle"),
|
||||
closeLabel: tc("Pick_Close"),
|
||||
dialogClass: "m01-logic__pick m01-combo",
|
||||
width: "960px", // 표 여섯 칸
|
||||
backdropClass: "m01-logic__backdrop",
|
||||
mount: (body) =>
|
||||
body.append(
|
||||
el("div", {
|
||||
className: "m02-std-pick",
|
||||
attrs: { "data-find-pick": "" },
|
||||
children: [...(tabs ? [tabs.root] : []), search.root, hint, table.root],
|
||||
}),
|
||||
),
|
||||
}));
|
||||
search.input.focus();
|
||||
draw();
|
||||
|
||||
const fail = (error: unknown): void =>
|
||||
showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error");
|
||||
void Promise.all([fetchLibLogics(), fetchStandard().catch(() => null)])
|
||||
.then(([l, s]) => {
|
||||
logics = l;
|
||||
std = s?.줄.length ? s.줄 : null; // 빈 줄 = 서버가 아직 셈 — 로직 줄 그대로
|
||||
draw();
|
||||
})
|
||||
.catch(fail);
|
||||
void findElements();
|
||||
if (tabs)
|
||||
void upper(opt.layer, opt.owner)
|
||||
.then((scopes) =>
|
||||
Promise.all(
|
||||
(["sk", "pre"] as const).map((t) =>
|
||||
listLayerDocs(scopes, t === "sk" ? SANGEUN_KIND : "precombo").then((rows) => {
|
||||
lists[t] = rows;
|
||||
tabs.setBadge(t, rows.length);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.catch((error) => {
|
||||
lists.sk ??= [];
|
||||
lists.pre ??= [];
|
||||
fail(error);
|
||||
})
|
||||
.finally(draw);
|
||||
}
|
||||
|
||||
/** 기준 목록이 오기 전 · 못 받으면 — 로직 하나 = 줄 하나(금액 빈칸) */
|
||||
const plain = (x: LogicSummary): StandardRow => ({
|
||||
키: x.키,
|
||||
이름: x.이름,
|
||||
규격: "",
|
||||
단위: x.결과단위.replace(/^원\/?/, ""),
|
||||
금액: null,
|
||||
출처: x.출처 ?? "",
|
||||
공종: "",
|
||||
별칭: x.별칭 ?? "",
|
||||
구분: x.구분,
|
||||
상세구분: x.상세구분,
|
||||
막힘: x.blocked,
|
||||
바꿈: {},
|
||||
});
|
||||
@@ -9,8 +9,9 @@
|
||||
* 704-1 단가산출 줄 = 기준 목록 표(품명 · 규격 · 단위 · 금액 · 출처 · 대표 규격 펼침 · 공종 거름 ·
|
||||
* 실무 낱말 = 서버가 별칭에 덧붙임) · 금액 = 서버 M01 엔진(`/logic-lib/standard`) · 오기 전엔 로직 줄만 ·
|
||||
* 줄 누름 = 계산서 보기(704-2 · `Labor_Sheet`) → [담기] = 지금 담기 동작(`done(로직)`)
|
||||
* 717-4 `lib.sangeun` = 탭 [기준 목록 | 단가산출] · 단가산출 = 위 층 SK(마스터 › 회사 › 개인 · `Sangeun_Pick`)를
|
||||
* 같은 표 꼴(출처 = 단가산출 · 층) · 줄 누름 = `lib.pick` 참조(SK 키)
|
||||
* 721-2 `lib.sangeun`(M02 일위대가 [로직 추가] · `withHopyo`) = 찾아 넣기 창(`Find_Pick` · 탭 M01 전체 · 단가산출 ·
|
||||
* 일위대가 · 실무 호표 줄 없음) · M01 인력 · 재료 = `lib.fixed`(일위대가 고정 줄 · 없으면 회색 막힘) ·
|
||||
* B09 · 불러오기는 이 창 그대로
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements";
|
||||
@@ -19,7 +20,6 @@ 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,
|
||||
@@ -30,8 +30,7 @@ import {
|
||||
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 type { LaborFixed, LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import { precomboClasses } from "./M02_MasterTemplete_UI_Side_Precombo";
|
||||
import { fetchStructureDef } from "./StructOptions/M02_SO_Api";
|
||||
@@ -39,8 +38,7 @@ 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";
|
||||
import { createTabs } from "@ui/ui_template_tabs";
|
||||
import { listSangeun, type SangeunRow } from "./M02_MasterTemplete_Sangeun_Pick";
|
||||
import { fixedOf, openFindPicker, upper } from "./M02_MasterTemplete_Find_Pick";
|
||||
|
||||
export { withHopyo } from "./M02_MasterTemplete_Hopyo_Pick";
|
||||
|
||||
@@ -75,22 +73,12 @@ export interface LibraryPick {
|
||||
more?: () => Promise<PickExtra[]>;
|
||||
/** 종류 고르기 칸(251 · 값 logic · precombo · 덧붙인 줄 kind) */
|
||||
kindFilter?: Array<{ value: string; text: string }>;
|
||||
/** 717-4 「단가산출」 탭(M02 SK · 위 층 모두) — 고르면 `pick` 참조 */
|
||||
/** 721-2 찾아 넣기 창(`Find_Pick`)으로 — M02 일위대가 [로직 추가] */
|
||||
sangeun?: boolean;
|
||||
/** 721-2 M01 인력 · 재료 → 일위대가 고정 줄(705-1 이음 꼴 · 담은로직 `{로직: "", 고정, 수량}`) */
|
||||
fixed?: (fx: LaborFixed, 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",
|
||||
@@ -103,6 +91,17 @@ export function openLogicPicker(
|
||||
lib?: LibraryPick,
|
||||
pre?: { query?: string },
|
||||
): void {
|
||||
if (lib?.sangeun && lib.layer !== "project") {
|
||||
const fixed = lib.fixed;
|
||||
return openFindPicker({
|
||||
layer: lib.layer,
|
||||
owner: lib.owner,
|
||||
title: lib.title,
|
||||
logic: done,
|
||||
ref: lib.pick,
|
||||
element: fixed && ((e) => fixed(fixedOf(e), e.이름)),
|
||||
});
|
||||
}
|
||||
const subs: SubBrief[] = [];
|
||||
let classes: ReturnType<typeof precomboClasses> = []; // 사전조합 새 분류(149-2) — 서고 줄이 있을 때만
|
||||
let all: LogicSummary[] = [];
|
||||
@@ -114,8 +113,6 @@ export function openLogicPicker(
|
||||
detail.select.disabled = true;
|
||||
let close = (): void => {};
|
||||
let rowPick: Array<() => void> = []; // 표 줄 누름 — 탭마다 다시 셈
|
||||
let tab: "std" | "sk" = "std";
|
||||
let skRows: SangeunRow[] | null = null; // 717-4 — 오기 전 = null
|
||||
|
||||
/* 한 목록 — 구분(단가산출 · 일위조합) 칸 · 층 단추는 서고 줄(사전조합)의 층 */
|
||||
const layerBar = el("div", { className: "ui-btn-group", attrs: { "data-pick-layers": "" } });
|
||||
@@ -171,7 +168,6 @@ export function openLogicPicker(
|
||||
|
||||
const find = (): void => {
|
||||
const q = finder.search.value.trim().toLowerCase();
|
||||
if (tab === "sk") return findSangeun(q);
|
||||
const rows = std ?? all.map(plain);
|
||||
const found = rows.filter(
|
||||
(x) =>
|
||||
@@ -250,32 +246,6 @@ export function openLogicPicker(
|
||||
})),
|
||||
]);
|
||||
};
|
||||
/** 717-4 단가산출 탭 — 같은 표 꼴 · 찾기 = 이름 · 제목 · 구분 */
|
||||
const findSangeun = (q: string): void => {
|
||||
const rows = (skRows ?? []).filter(
|
||||
(x) => !q || [x.이름, x.제목, x.구분, x.상세구분].join(" ").toLowerCase().includes(q),
|
||||
);
|
||||
finder.hint.textContent = !skRows
|
||||
? tc("Loading")
|
||||
: rows.length
|
||||
? `${rows.length} / ${skRows.length}`
|
||||
: tc("Pick_None");
|
||||
rowPick = rows.map((x) => () => {
|
||||
close();
|
||||
lib?.pick(x.ref, x.제목 || x.이름);
|
||||
});
|
||||
table.root.hidden = false;
|
||||
table.setRows(
|
||||
rows.map((x) => ({
|
||||
품명: x.제목 || x.이름,
|
||||
규격: [x.구분, x.상세구분].filter(Boolean).join(" "),
|
||||
단위: x.단위 ?? "",
|
||||
금액: x.기준금액,
|
||||
출처: `${L("M02_PickSangeun")} · ${tc(LAYER_TEXT[x.ref.층 as keyof typeof LAYER_TEXT])}`,
|
||||
})),
|
||||
);
|
||||
finder.setRows([]);
|
||||
};
|
||||
const loadLib = async (): Promise<void> => {
|
||||
if (!lib || !scope) return;
|
||||
const mine = ++seq;
|
||||
@@ -340,45 +310,12 @@ export function openLogicPicker(
|
||||
|
||||
layerBar.style.display = "none";
|
||||
const hint = createText(tc("Std_Hint"));
|
||||
/** 탭 바꿈 — 단가산출 탭은 찾기 칸만(거름 · 층 단추 · 계산서 안내 숨김) */
|
||||
const tabs = lib?.sangeun
|
||||
? createTabs({
|
||||
tabs: [
|
||||
{ key: "std", label: L("M02_PickStd") },
|
||||
{ key: "sk", label: L("M02_PickSangeun") },
|
||||
],
|
||||
value: tab,
|
||||
kind: "line",
|
||||
onChange: (key) => {
|
||||
tab = key === "sk" ? "sk" : "std";
|
||||
const std = tab === "std";
|
||||
for (const f of [kindSel?.root, work.root, sub.root, detail.root])
|
||||
if (f) f.style.display = std ? "" : "none";
|
||||
layerBar.style.display = std && scopes.length ? "" : "none";
|
||||
hint.hidden = !std;
|
||||
find();
|
||||
},
|
||||
})
|
||||
: null;
|
||||
if (lib)
|
||||
void upper(lib.layer, lib.owner).then((got) => {
|
||||
scopes = got;
|
||||
if (tabs)
|
||||
listSangeun(got).then(
|
||||
(rows) => {
|
||||
skRows = rows;
|
||||
tabs.setBadge("sk", rows.length);
|
||||
find();
|
||||
},
|
||||
(error) => {
|
||||
skRows = [];
|
||||
find();
|
||||
showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error");
|
||||
},
|
||||
);
|
||||
scope = got[got.length - 1] ?? null; // 기본 = 가장 가까운 층
|
||||
if (!got.length) return;
|
||||
layerBar.style.display = tab === "std" ? "" : "none";
|
||||
layerBar.style.display = "";
|
||||
drawLayers();
|
||||
void loadLib();
|
||||
void fetchStructureDef().then(
|
||||
@@ -401,7 +338,7 @@ export function openLogicPicker(
|
||||
body.append(
|
||||
el("div", {
|
||||
className: "m02-std-pick",
|
||||
children: [...(tabs ? [tabs.root] : []), finder.root, hint, table.root],
|
||||
children: [finder.root, hint, table.root],
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
import { buildLabDetail } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Detail";
|
||||
import { createViewPicker } from "../M01_MasterData/M01_MasterData_UI_LogicLab_View";
|
||||
import type { Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick";
|
||||
import { openFindPicker } from "./M02_MasterTemplete_Find_Pick";
|
||||
import { calcSangeun, skCoverTable } from "./M02_MasterTemplete_Sangeun_Cover";
|
||||
import { openSangeunLinePicker } from "./M02_MasterTemplete_Sangeun_Pick";
|
||||
import {
|
||||
@@ -101,12 +101,17 @@ export function mountSangeunDetail(
|
||||
variant: "filled",
|
||||
size: 28,
|
||||
onClick: () =>
|
||||
openLogicPicker((x) => {
|
||||
doc.바탕 = { 로직: x.키 };
|
||||
delete doc.바탕판; // 새 바탕 — 개정 견줄 판 없음
|
||||
if (!doc.단위 || doc.단위 === "식") doc.단위 = unitOf(x.결과단위) || doc.단위;
|
||||
touch(false);
|
||||
void loadBase();
|
||||
openFindPicker({
|
||||
...scope,
|
||||
logicOnly: true,
|
||||
title: L("M02_SangeunPickBase"),
|
||||
logic: (x) => {
|
||||
doc.바탕 = { 로직: x.키 };
|
||||
delete doc.바탕판; // 새 바탕 — 개정 견줄 판 없음
|
||||
if (!doc.단위 || doc.단위 === "식") doc.단위 = unitOf(x.결과단위) || doc.단위;
|
||||
touch(false);
|
||||
void loadBase();
|
||||
},
|
||||
}),
|
||||
});
|
||||
pick.hidden = true; // 보임은 drawHead 가 정함
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Sangeun_Pick.ts
|
||||
* M02 단가산출(SK) 고르기(PLAN 717-4) — 위 층 SK 목록 한 벌 · 단가산출 더한줄 고르기.
|
||||
* · `listSangeun` = 층 목록 길(`/layers/{층}/templates` · 종류 `sangeun`)을 마스터 › 회사 › 개인 차례로 이음 ·
|
||||
* 금액 = 서버가 붙인 1 단위 `기준금액` · 일위대가 [로직 추가] 「단가산출」 탭 · B09 재료 창고 출처가 씀
|
||||
* · `listLayerDocs` = 층 목록 길(`/layers/{층}/templates` · 종류 하나)을 마스터 › 회사 › 개인 차례로 이음 ·
|
||||
* 금액 = 서버가 붙인 1 단위 `기준금액` · `listSangeun` = 종류 `sangeun` · 찾아 넣기 창(`Find_Pick`) 탭 · B09 재료 창고 출처가 씀
|
||||
* · 고른 SK = 참조 `{층, 주인, 키: SK-…}` — 일위대가 줄은 옛 참조 줄 꼴(서버 `M02_Labor_Expand` 가 펼침)
|
||||
* · `openSangeunLinePicker` = 단가산출 더한줄 고르기 — 같은 [로직 추가] 창(M01 로직 · 일위대가 · 단가산출 ·
|
||||
* M01 재료) → SK 문서 `더한줄` 한 줄(PLAN 715 이음 꼴)
|
||||
* · `openSangeunLinePicker` = 단가산출 [항목 추가] — 찾아 넣기 창(721-2 · M01 로직 · 인력 · 재료 · 단가산출 · 일위대가)
|
||||
* → SK 문서 `더한줄` 한 줄(PLAN 715 이음 꼴 · 인력 · 재료 = 요소 키 LB · MT · 수량 1)
|
||||
* ========================================================================== */
|
||||
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { call, type Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { openFindPicker } from "./M02_MasterTemplete_Find_Pick";
|
||||
import type { LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import { pickMaterial } from "./M02_Material_Pick";
|
||||
|
||||
export const SANGEUN_KIND = "sangeun";
|
||||
/** 층 차례 — 위에서 아래 */
|
||||
@@ -41,7 +38,14 @@ interface ListRow {
|
||||
}
|
||||
|
||||
/** 위 층 SK — 층 차례(마스터 › 회사 › 개인) · 층 안은 서버 차례 */
|
||||
export async function listSangeun(scopes: Omit<LaborRef, "키">[]): Promise<SangeunRow[]> {
|
||||
export const listSangeun = (scopes: Omit<LaborRef, "키">[]): Promise<SangeunRow[]> =>
|
||||
listLayerDocs(scopes, SANGEUN_KIND);
|
||||
|
||||
/** 위 층 서고 문서 한 종류(sangeun · precombo) — 층 차례 · 층 안은 서버 차례 */
|
||||
export async function listLayerDocs(
|
||||
scopes: Omit<LaborRef, "키">[],
|
||||
kind: string,
|
||||
): Promise<SangeunRow[]> {
|
||||
const rank = (x: Omit<LaborRef, "키">): number =>
|
||||
LAYER_ORDER.indexOf(x.층 as (typeof LAYER_ORDER)[number]);
|
||||
const sorted = [...scopes].sort((a, b) => rank(a) - rank(b));
|
||||
@@ -50,7 +54,7 @@ export async function listSangeun(scopes: Omit<LaborRef, "키">[]): Promise<Sang
|
||||
const q = at.주인 && at.층 !== "system" ? `?owner=${encodeURIComponent(at.주인)}` : "";
|
||||
const got = await call<{ 양식: ListRow[] }>(`/layers/${at.층}/templates${q}`);
|
||||
return got.양식
|
||||
.filter((r) => r.종류 === SANGEUN_KIND)
|
||||
.filter((r) => r.종류 === kind)
|
||||
.map<SangeunRow>((r) => ({
|
||||
ref: { 층: at.층, 주인: at.층 === "system" ? null : at.주인, 키: r.이름 },
|
||||
이름: r.이름,
|
||||
@@ -69,36 +73,17 @@ export async function listSangeun(scopes: Omit<LaborRef, "키">[]): Promise<Sang
|
||||
export type SangeunLine =
|
||||
| { 종류: "로직"; 로직: string; 입력값: Record<string, never>; 수량: number }
|
||||
| { 참조: LaborRef; 수량: number }
|
||||
| { 종류: "재료"; 요소: string; 이름: string | null; 단위: string | null; 수량: number };
|
||||
| { 종류: "인력" | "재료"; 요소: string; 이름: string | null; 단위: string | null; 수량: number };
|
||||
|
||||
/** 단가산출 더한줄 고르기 — `scope` = 그 SK 문서 층(고를 수 있는 서고 = 위 층만) */
|
||||
/** 단가산출 [항목 추가] — `scope` = 그 SK 문서 층(고를 수 있는 서고 = 위 층만) */
|
||||
export function openSangeunLinePicker(
|
||||
scope: { layer: Layer; owner: string | null },
|
||||
done: (line: SangeunLine) => void,
|
||||
): void {
|
||||
const element = L("M02_PickElement");
|
||||
openLogicPicker((x) => done({ 종류: "로직", 로직: x.키, 입력값: {}, 수량: 1 }), {
|
||||
openFindPicker({
|
||||
...scope,
|
||||
sangeun: true,
|
||||
pick: (ref) => done({ 참조: ref, 수량: 1 }),
|
||||
kindFilter: [
|
||||
{ value: "logic", text: tc("Kind_Logic") },
|
||||
{ value: "precombo", text: L("M02_PrecomboTitle") },
|
||||
{ value: "element", text: element },
|
||||
],
|
||||
extra: [
|
||||
{
|
||||
kind: "element",
|
||||
key: "element:material",
|
||||
tag: element,
|
||||
name: L("M02_PickMaterial"),
|
||||
sub: "",
|
||||
unit: "",
|
||||
onPick: () =>
|
||||
pickMaterial((m) =>
|
||||
done({ 종류: "재료", 요소: m.코드, 이름: m.이름, 단위: m.단위, 수량: 1 }),
|
||||
),
|
||||
},
|
||||
],
|
||||
logic: (x) => done({ 종류: "로직", 로직: x.키, 입력값: {}, 수량: 1 }),
|
||||
ref: (ref) => done({ 참조: ref, 수량: 1 }),
|
||||
element: (e) => done({ 종류: e.종류, 요소: e.코드, 이름: e.이름, 단위: e.단위, 수량: 1 }),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user