feat(717-2a): M02 왼쪽 맨 위 「단가산출」 칸 — 층 단추 따로(기본 개인) · 폴더 나무 · 목록표 · 상세 자리 · 대시보드 줄
- 새 UI_Side_Sangeun.ts — 서고 칸(716-4) kind sangeun · 목록표 고름 sangeunList - 새 Sangeun_View.ts — 이름 · 단위 · 바탕 · 덮개 · 더한 줄 · 뺀 줄 · 열린 입력 요약 + 상세 자리표(717-2b 가 갈음) - UI_Main 종류 sangeun 갈래 · 목록표 조건에 sangeun · Library_List 열 표(단가산출 = 바탕 · 고친 줄 · 쓰는 곳) · sangeunBlank(단위 식 · 바탕 없음) - UI_Page 단가산출 층 상태 sk · UI_Side 맨 위 칸 · reveal 「단가산출」 · Layer open 「단가산출」 · Api Kind sangeun - m_settings_nav 대시보드 마스터 템플릿 묶음 「단가산출」 줄(?open=단가산출) · locale_m2 키 - 낡은 글 대조 시험 넷 고침(197_1 · b01_settings_cards · m02_layer_screen · m02_precombo_scope) — 대시보드 줄 하나 늘고 OPENS · reveal · 목록 열 표 · 칸 차례가 바뀜 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,7 @@ export function settingLinks(user: SettingUser, companyId: number | string | nul
|
||||
m01("로직", L("B01_Dashboard_CardLogic"), read, "로직"),
|
||||
m01("옵션", L("B01_Dashboard_CardStructOptionDef"), desc, "옵션"),
|
||||
m01("별칭", L("B01_Dashboard_CardAliasLedger"), desc, "별칭"),
|
||||
m02(L("B01_Dashboard_CardSangeun"), "단가산출", comboRoot("master"), desc),
|
||||
m02(L("B01_Dashboard_CardPrecombo"), "일위대가조합", comboRoot("master"), desc),
|
||||
m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", comboRoot("master"), desc),
|
||||
m02(L("B01_Dashboard_CardDrawing"), "도면", "master", desc),
|
||||
|
||||
@@ -10,8 +10,9 @@ export type Layer = "system" | "company" | "personal" | "project";
|
||||
/** 서버 종류 — 화면의 「표 양식」 = table · 「도면 양식」 = drawing · 「구조물 도면」 = structure ·
|
||||
* 「면적산출」 = area(구조물집계표 열마다 한 통합문서) ·
|
||||
* 「일위대가」 = labor(열마다 문서 · 39장) ·
|
||||
* 「사전조합」 = precombo(일위대가 사전조합 서고 · 50-3 · 키가 이름 · 옛 단가승격 promote 는 134-2 에서 뺌) */
|
||||
export type Kind = "table" | "drawing" | "structure" | "area" | "labor" | "precombo";
|
||||
* 「사전조합」 = precombo(일위대가 사전조합 서고 · 50-3 · 키가 이름 · 옛 단가승격 promote 는 134-2 에서 뺌) ·
|
||||
* 「단가산출」 = sangeun(SK- 서고 · 715-2 · 717-2a) */
|
||||
export type Kind = "table" | "drawing" | "structure" | "area" | "labor" | "precombo" | "sangeun";
|
||||
|
||||
export interface TemplateInfo {
|
||||
종류: Kind;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Layer.ts
|
||||
* M02 한 화면의 뿌리 셋(마스터 · 회사 · 개인 · PLAN 27-1) — 진입 해시
|
||||
* `#/m02-master-template?open=구조물조합|옵션|도면&root=master|company|personal&owner=…&company=…&pre=…`
|
||||
* `#/m02-master-template?open=구조물조합|옵션|도면|일위대가조합|단가산출&root=master|company|personal&owner=…&company=…&pre=…`
|
||||
* (옛 `?layer=` 도 root 로 읽음 · `pre` = 일위대가 사전조합 층 · 134-3) · 고칠 권한(`accessFor` — 서버 `M02_Layer_Access` 와 짝 · 27-1c) ·
|
||||
* 회사 없음 안내 · 회사용 읽기만일 때 [개인용 복사] 막대.
|
||||
* ========================================================================== */
|
||||
@@ -16,7 +16,7 @@ import { copyItemToPersonal, fetchStructureCombos } from "./M02_MasterTemplete_A
|
||||
export type ManageLayer = "system" | "company" | "personal";
|
||||
/** 해시 · 화면의 뿌리 이름 — 서버 층 이름은 `layerOfRoot` */
|
||||
export type Root = "master" | "company" | "personal";
|
||||
export type OpenTarget = "구조물조합" | "옵션" | "도면" | "일위대가조합";
|
||||
export type OpenTarget = "구조물조합" | "옵션" | "도면" | "일위대가조합" | "단가산출";
|
||||
|
||||
export interface Entry {
|
||||
root: Root;
|
||||
@@ -29,7 +29,7 @@ export interface Entry {
|
||||
pre: Root | null;
|
||||
}
|
||||
|
||||
const OPENS: OpenTarget[] = ["구조물조합", "옵션", "도면", "일위대가조합"];
|
||||
const OPENS: OpenTarget[] = ["구조물조합", "옵션", "도면", "일위대가조합", "단가산출"];
|
||||
const ROOTS: Root[] = ["master", "company", "personal"];
|
||||
|
||||
export const layerOfRoot = (root: Root): ManageLayer => (root === "master" ? "system" : root);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Library_List.ts
|
||||
* 일위대가 사전조합 서고 목록표(51-4) — 좌측 사전조합 층 단추를 누르면 상세 칸에 뜸.
|
||||
* 표 = 고름 · 키 · 구분 · 상세구분(51-12) · 이름 · 단위 · 기준 금액(서버 1 단위 계) · 고친 날 · 줄 누름 = 그 문서 상세.
|
||||
* 서고 목록표(51-4 · 717-2a) — 좌측 일위대가 · 단가산출 층 단추를 누르면 상세 칸에 뜸.
|
||||
* 표 = 고름 · 키 · 구분 · 상세구분(51-12) · 이름 · 단위 · 기준 금액(서버 1 단위 계) · 고친 날 · 줄 누름 = 그 문서 상세 ·
|
||||
* 단가산출 = 구분 둘 대신 바탕(M01 로직 · 없으면 「바탕 없음」) · 고친 줄 수 · 쓰는 곳 수(서버 `summary`).
|
||||
* 머리 [생성] · [복사](고른 줄) · [삭제](고른 줄 · 쓰는 곳 있으면 서버 409 까닭 그대로).
|
||||
* 꼴 = M01 단가산출 로직 목록 — 찾기 줄(`buildHeadFilters`) · 쪽(`buildPager`) · 표(`m01-master__grid`) 를 M01 에서 import.
|
||||
* ========================================================================== */
|
||||
@@ -28,10 +29,10 @@ import {
|
||||
saveTemplate,
|
||||
type Layer,
|
||||
type LibraryBody,
|
||||
type LibraryKind,
|
||||
type TemplateInfo,
|
||||
} from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import type { PrecomboKind } from "./M02_MasterTemplete_UI_Side_Precombo";
|
||||
|
||||
/** 목록 줄 — 서버가 문서 `이름`(제목) · 단위 · 1 단위 기준 계를 실어 보냄 */
|
||||
type Row = TemplateInfo & {
|
||||
@@ -40,13 +41,16 @@ type Row = TemplateInfo & {
|
||||
기준금액?: number | null;
|
||||
구분?: string | null;
|
||||
상세구분?: string | null;
|
||||
바탕?: string | null;
|
||||
고친줄수?: number;
|
||||
쓰는곳수?: number;
|
||||
};
|
||||
|
||||
export interface LibraryListOptions {
|
||||
layer: Layer;
|
||||
/** 그 층 주인(134-3 · 사전조합 층 · null = 자기 층) */
|
||||
owner: string | null;
|
||||
kind: PrecomboKind;
|
||||
kind: LibraryKind;
|
||||
canEdit: boolean;
|
||||
/** 줄 누름 · 생성 · 복사 뒤 — 그 문서 상세 */
|
||||
open: (key: string, label: string) => void;
|
||||
@@ -60,6 +64,36 @@ const money = (v: number | null | undefined): string =>
|
||||
v == null ? "" : v.toLocaleString("ko-KR", { maximumFractionDigits: 2 });
|
||||
export const libraryLabel = (row: Row): string => (row.제목 ? `${row.이름} ${row.제목}` : row.이름);
|
||||
|
||||
/** 새 단가산출 몸(717-2a) — 바탕 없음 · 빈 더한줄 · 단위 「식」(서버 필수 · 상세에서 고침 · 717-2b) */
|
||||
export const sangeunBlank = (name: string): Record<string, unknown> => ({
|
||||
이름: name,
|
||||
단위: "식",
|
||||
바탕: null,
|
||||
더한줄: [],
|
||||
});
|
||||
/** 표 열 — 머리 글 · 줄 값(일위대가 = 구분 둘 · 단가산출 = 바탕 · 고친 줄 · 쓰는 곳) */
|
||||
const COLUMNS: Record<LibraryKind, () => [string, (row: Row) => string][]> = {
|
||||
precombo: () => [
|
||||
[tc("Lib_Key"), (r) => r.이름],
|
||||
[tc("Sub"), (r) => r.구분 || tc("Lib_Unsorted")], // 빈 구분 = 미분류(149-2 · 나무와 같은 말)
|
||||
[tc("Detail"), (r) => r.상세구분 ?? ""],
|
||||
[tc("Lib_Name"), (r) => r.제목 ?? ""],
|
||||
[tc("Lib_Unit"), (r) => r.단위 ?? ""],
|
||||
[tc("Lib_Base"), (r) => money(r.기준금액)],
|
||||
[tc("Lib_Date"), (r) => r.수정일],
|
||||
],
|
||||
sangeun: () => [
|
||||
[tc("Lib_Key"), (r) => r.이름],
|
||||
[tc("Lib_Name"), (r) => r.제목 ?? ""],
|
||||
[tc("Lib_Unit"), (r) => r.단위 ?? ""],
|
||||
[L("M02_SangeunBase"), (r) => r.바탕 || L("M02_SangeunNoBase")],
|
||||
[L("M02_SangeunEdited"), (r) => String(r.고친줄수 ?? 0)],
|
||||
[L("M02_SangeunUsers"), (r) => String(r.쓰는곳수 ?? 0)],
|
||||
[tc("Lib_Base"), (r) => money(r.기준금액)],
|
||||
[tc("Lib_Date"), (r) => r.수정일],
|
||||
],
|
||||
};
|
||||
|
||||
/** 이름 창(생성 · 복사 · 사전조합 나무 폴더 메뉴 149-1) — [생성] = `onOk` 성공하면 닫힘 */
|
||||
export const nameDialog = (
|
||||
title: string,
|
||||
@@ -101,6 +135,7 @@ export function mountLibraryList(
|
||||
tools: HTMLElement,
|
||||
opt: LibraryListOptions,
|
||||
): { getDoc: () => null; destroy: () => void } {
|
||||
const columns = COLUMNS[opt.kind]();
|
||||
let rows: Row[] = [];
|
||||
let picked: string | null = null;
|
||||
let page = 1;
|
||||
@@ -144,15 +179,9 @@ export function mountLibraryList(
|
||||
attrs: { "data-lib-key": row.이름 },
|
||||
children: [
|
||||
pick,
|
||||
...[
|
||||
row.이름,
|
||||
row.구분 || tc("Lib_Unsorted"), // 빈 구분 = 미분류(149-2 · 나무와 같은 말)
|
||||
row.상세구분 ?? "",
|
||||
row.제목 ?? "",
|
||||
row.단위 ?? "",
|
||||
money(row.기준금액),
|
||||
row.수정일,
|
||||
].map((v) => el("td", { text: v, attrs: { title: v } })),
|
||||
...columns
|
||||
.map(([, value]) => value(row))
|
||||
.map((v) => el("td", { text: v, attrs: { title: v } })),
|
||||
],
|
||||
});
|
||||
tr.addEventListener("click", () => opt.open(row.이름, libraryLabel(row)));
|
||||
@@ -160,7 +189,12 @@ export function mountLibraryList(
|
||||
})
|
||||
: [
|
||||
el("tr", {
|
||||
children: [el("td", { text: L("M02_NoTemplates"), attrs: { colspan: "8" } })],
|
||||
children: [
|
||||
el("td", {
|
||||
text: L("M02_NoTemplates"),
|
||||
attrs: { colspan: String(columns.length + 1) },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
@@ -191,7 +225,10 @@ export function mountLibraryList(
|
||||
/** 사전조합 = 이름 · 구분 · 상세구분 빈 값(미분류 · 134-5 · 상세에서 고름) · 빈 묶음 */
|
||||
btnNew.addEventListener("click", () => {
|
||||
nameDialog(L("M02_NewTitle"), "", async (name) => {
|
||||
const 문서 = { 이름: name, 구분: "", 상세구분: "", 단위: null, 담은로직: [] };
|
||||
const 문서 =
|
||||
opt.kind === "sangeun"
|
||||
? sangeunBlank(name)
|
||||
: { 이름: name, 구분: "", 상세구분: "", 단위: null, 담은로직: [] };
|
||||
await create({ 문서 }, name);
|
||||
});
|
||||
});
|
||||
@@ -244,16 +281,7 @@ export function mountLibraryList(
|
||||
page = 1;
|
||||
draw();
|
||||
});
|
||||
const heads = [
|
||||
"",
|
||||
"Lib_Key",
|
||||
"Sub",
|
||||
"Detail",
|
||||
"Lib_Name",
|
||||
"Lib_Unit",
|
||||
"Lib_Base",
|
||||
"Lib_Date",
|
||||
] as const;
|
||||
const heads = ["", ...columns.map(([head]) => head)];
|
||||
const gridBox = el("div", {
|
||||
className: "ui-table-wrap m01-master__grid-wrap",
|
||||
attrs: { "data-lib-list": opt.kind },
|
||||
@@ -262,9 +290,7 @@ export function mountLibraryList(
|
||||
className: "ui-table m01-master__grid",
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [
|
||||
el("tr", { children: heads.map((h) => el("th", { text: h ? tc(h) : "" })) }),
|
||||
],
|
||||
children: [el("tr", { children: heads.map((h) => el("th", { text: h })) })],
|
||||
}),
|
||||
body,
|
||||
],
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Sangeun_View.ts
|
||||
* 단가산출(SK) 상세 자리(717-2a) — 편집 칸 종류 sangeun 이 여는 곳(UI_Main 갈래 한 줄).
|
||||
* 지금 = 이름 · 단위 · 바탕 · 고친 줄 요약(덮개 · 더한 줄 · 뺀 줄 · 열린 입력) + 상세 자리표.
|
||||
* 717-2b(sub2) = 자리표 줄을 `mountSangeunDetail(slot, doc, opt)`(`Sangeun_Detail.ts`)로 갈음 ·
|
||||
* [저장]은 그 손잡이 `getDoc` · 고침 알림은 `opt.onChanged`. 지금 [저장] = 읽은 문서 그대로.
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
import { createKv } from "@ui/ui_template_kv";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import type { Selection } from "./M02_MasterTemplete_UI_Main";
|
||||
|
||||
/** SK 문서 중 이 자리가 읽는 칸(꼴 = 서버 `M02_Sangeun_Doc`) */
|
||||
export interface SangeunDoc {
|
||||
키?: string;
|
||||
이름?: string;
|
||||
단위?: string;
|
||||
바탕?: { 로직?: string; 참조?: { 키?: string } } | null;
|
||||
덮개?: Record<string, unknown>;
|
||||
더한줄?: unknown[];
|
||||
뺀줄?: unknown[];
|
||||
열린입력?: string[];
|
||||
}
|
||||
|
||||
/** 고름(층 · 주인 · SK 키 = name) + 읽기만 · 고침 알림 */
|
||||
export type SangeunViewOptions = Pick<Selection, "layer" | "owner" | "name"> & {
|
||||
readOnly: boolean;
|
||||
onChanged: (dirty: boolean) => void;
|
||||
};
|
||||
|
||||
/** 바탕 한 줄 — 로직 키 · 참조 SK 키 · 없으면 「바탕 없음」 */
|
||||
export const baseText = (doc: SangeunDoc): string =>
|
||||
doc.바탕?.로직 ?? doc.바탕?.참조?.키 ?? L("M02_SangeunNoBase");
|
||||
|
||||
export function mountSangeunView(
|
||||
host: HTMLElement,
|
||||
doc: unknown,
|
||||
_opt: SangeunViewOptions,
|
||||
): { getDoc: () => unknown; destroy: () => void } {
|
||||
const sk = (doc ?? {}) as SangeunDoc;
|
||||
const count = (v: unknown[] | Record<string, unknown> | undefined): number =>
|
||||
Array.isArray(v) ? v.length : Object.keys(v ?? {}).length;
|
||||
const head = createKv([
|
||||
{ label: L("M02_Name"), value: sk.이름 ?? "" },
|
||||
{ label: tc("Lib_Unit"), value: sk.단위 ?? "" },
|
||||
{ label: L("M02_SangeunBase"), value: baseText(sk) },
|
||||
{ label: L("M02_SangeunCover"), value: count(sk.덮개) },
|
||||
{ label: L("M02_SangeunAdded"), value: count(sk.더한줄) },
|
||||
{ label: L("M02_SangeunRemoved"), value: count(sk.뺀줄) },
|
||||
{ label: L("M02_SangeunOpened"), value: (sk.열린입력 ?? []).join(" · ") || null },
|
||||
]);
|
||||
const slot = el("div", {
|
||||
attrs: { "data-sangeun-detail": sk.키 ?? "" },
|
||||
children: [createEmpty({ text: L("M02_SangeunDetailSoon") })],
|
||||
});
|
||||
host.replaceChildren(el("div", { attrs: { "data-sangeun-view": "" }, children: [head, slot] }));
|
||||
return { getDoc: () => doc, destroy: () => host.replaceChildren() };
|
||||
}
|
||||
@@ -357,6 +357,12 @@ export function createMain(
|
||||
const readOnly = !writable(at);
|
||||
const mine = seq; // 모듈 · 집계표 번호를 기다리는 사이 다른 화면(옵션 정의 등)이 열렸으면 그만둠
|
||||
host.replaceChildren();
|
||||
if (at.kind === "sangeun") {
|
||||
const { mountSangeunView } = await import("./M02_MasterTemplete_Sangeun_View"); // 717-2a
|
||||
const onChanged = (d: boolean): boolean => (dirty = d && !readOnly);
|
||||
if (mine === seq) editor = mountSangeunView(host, doc, { ...at, readOnly, onChanged });
|
||||
return;
|
||||
}
|
||||
if (at.kind === "table") {
|
||||
const create = await findExport<(h: HTMLElement, d: unknown, o: object) => EditorHandle>(
|
||||
SHEET_MODS,
|
||||
@@ -503,7 +509,7 @@ export function createMain(
|
||||
return empty("");
|
||||
}
|
||||
title.textContent = isCombo(next) ? L("M02_KindTable") : (next.label ?? next.name); // 26-7a
|
||||
if (isPrecombo(next.kind) && !next.name) {
|
||||
if ((isPrecombo(next.kind) || next.kind === "sangeun") && !next.name) {
|
||||
// 서고 목록표(51-4) — 이름 "" · 저장할 것 없음 · 줄 누름 = 그 문서 상세
|
||||
save.hidden = true;
|
||||
const { mountLibraryList } = await import("./M02_MasterTemplete_Library_List");
|
||||
|
||||
@@ -53,6 +53,7 @@ import { canEdit, createMain, newItemButton, type Selection } from "./M02_Master
|
||||
import { buildSide } from "./M02_MasterTemplete_UI_Side";
|
||||
import { ITEM_BUILT } from "./M02_MasterTemplete_Hopyo_ToItem";
|
||||
import { precomboList, type PrecomboScope } from "./M02_MasterTemplete_UI_Side_Precombo";
|
||||
import { sangeunList } from "./M02_MasterTemplete_UI_Side_Sangeun";
|
||||
import { buildTwoWay, pickedName } from "./M02_MasterTemplete_ItemPick";
|
||||
import "./M02_MasterTemplete_UI_Style.css";
|
||||
|
||||
@@ -248,6 +249,16 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise<Mounte
|
||||
reselect: () =>
|
||||
void main.confirmLeave().then((ok) => ok && void main.open(precomboList(pre))),
|
||||
});
|
||||
// 단가산출 층(717-2a) — 일위대가 층과 따로 · 기본 개인(못 가면 마스터) · 주소에 안 실음
|
||||
let sk = preScope("personal");
|
||||
const skBar = (): HTMLElement =>
|
||||
createLayerBar({
|
||||
user,
|
||||
active: { root: sk.root, owner: sk.owner },
|
||||
company,
|
||||
go: (next) => ((sk = preScope(next)), void side.refreshSangeun()),
|
||||
reselect: () => void main.confirmLeave().then((ok) => ok && void main.open(sangeunList(sk))),
|
||||
});
|
||||
const side = buildSide({
|
||||
layer,
|
||||
isAdmin,
|
||||
@@ -264,6 +275,7 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise<Mounte
|
||||
bar: preBar,
|
||||
more: (key, name) => share.precomboMenu(pre.root === "personal" && !pre.owner, key, name),
|
||||
},
|
||||
sangeun: { scope: () => sk, bar: skBar },
|
||||
});
|
||||
|
||||
const layout = el("div", { className: "ui-workflow-layout m02-master" });
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_UI_Side.ts
|
||||
* 마스터 템플릿 좌측 패널 차례(716-3) — 「일위대가」(층 단추 따로 · 2단 폴더 나무 · `UI_Side_Precombo` ·
|
||||
* 마스터 템플릿 좌측 패널 차례(716-3 · 717-2a) — 「단가산출」(층 단추 따로 · 기본 개인 · `UI_Side_Sangeun`) › 「일위대가」(층 단추 따로 · 2단 폴더 나무 · `UI_Side_Precombo` ·
|
||||
* 50-3 · 134-3) › 「구조물 조합」(층 단추 · 나무) › 수평 구분선 › 「일위대가 불러오기」(구조물 조합 층 ·
|
||||
* `Hopyo_Side` · 324-4) › 「도면 양식」(새로 · 본떠 · 지우기) › 실무문서. 「단가산출」 칸은 맨 위(717-2a).
|
||||
* `Hopyo_Side` · 324-4) › 「도면 양식」(새로 · 본떠 · 지우기) › 실무문서.
|
||||
* 구조물 옵션 정의는 M01 로 옮김(716-1).
|
||||
* 서버 길은 계약 `6_계약.md`.
|
||||
* 구조물 도면 · 일위대가 · 면적산출 상세는 좌측 줄이 아니라 항목 카드 [수정]으로(PLAN 45-2).
|
||||
@@ -36,6 +36,7 @@ import { HOPYO_BUILT } from "./M02_MasterTemplete_Hopyo_Check";
|
||||
import { buildHopyoSection } from "./M02_MasterTemplete_Hopyo_Side";
|
||||
import { canEdit, type Selection } from "./M02_MasterTemplete_UI_Main";
|
||||
import { buildPrecomboSection, type PrecomboOptions } from "./M02_MasterTemplete_UI_Side_Precombo";
|
||||
import { buildSangeunSection, type SangeunOptions } from "./M02_MasterTemplete_UI_Side_Sangeun";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
import { createDivider } from "@ui/ui_template_divider";
|
||||
|
||||
@@ -67,6 +68,8 @@ export interface SideOptions {
|
||||
tree?: HTMLElement[];
|
||||
/** 「일위대가」 층(134-3 · 페이지 층과 따로 · 층 단추는 페이지가 만듦) */
|
||||
precombo: Pick<PrecomboOptions, "scope" | "bar" | "more">;
|
||||
/** 「단가산출」 층(717-2a · 일위대가 층과 따로 · 층 단추는 페이지가 만듦) */
|
||||
sangeun: Pick<SangeunOptions, "scope" | "bar">;
|
||||
}
|
||||
|
||||
export interface SideHandle {
|
||||
@@ -75,9 +78,11 @@ export interface SideHandle {
|
||||
/** 고름 표시를 지금 열린 것 한 줄로 — 편집 칸이 무엇을 열든(카드 [도면 수정] 등) 부름 */
|
||||
paint: () => void;
|
||||
/** 진입 해시 `open`(PLAN 27-1d) — 그 컨테이너를 펼침 · 도면은 시스템 층에만 · 옵션은 M01 로 옮겨 아무 일 없음 */
|
||||
reveal: (target: "옵션" | "도면" | "일위대가조합") => void;
|
||||
reveal: (target: "옵션" | "도면" | "일위대가조합" | "단가산출") => void;
|
||||
/** 사전조합 층이 바뀜(134-3) — 그 컨테이너만 다시 그림 */
|
||||
refreshPrecombo: () => Promise<void>;
|
||||
/** 단가산출 층이 바뀜(717-2a) — 그 컨테이너만 다시 그림 */
|
||||
refreshSangeun: () => Promise<void>;
|
||||
}
|
||||
|
||||
/** 서버 이름 규칙과 같게 — 화면에서 먼저 막음 */
|
||||
@@ -133,7 +138,7 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
openOnExpand(summary, async () => {
|
||||
if (!isPicked("table", STRUCTURE_TABLE) && (await opt.confirmLeave())) opt.onOpen(null);
|
||||
});
|
||||
// 일위대가 서고(50-3 · 134-3) — 맨 위 칸 · 층은 페이지 층과 따로
|
||||
// 일위대가 서고(50-3 · 134-3) — 단가산출 다음 칸 · 층은 페이지 층과 따로
|
||||
const precombo = buildPrecomboSection({
|
||||
...opt.precombo,
|
||||
confirmLeave: opt.confirmLeave,
|
||||
@@ -141,10 +146,18 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
getOpen: opt.getOpen,
|
||||
});
|
||||
const precomboSec = section(L("M02_PrecomboTitle"), precombo.body);
|
||||
// 단가산출 서고(717-2a) — 맨 위 칸 · 층은 일위대가 층과 따로
|
||||
const sangeun = buildSangeunSection({
|
||||
...opt.sangeun,
|
||||
confirmLeave: opt.confirmLeave,
|
||||
onOpen: opt.onOpen,
|
||||
getOpen: opt.getOpen,
|
||||
});
|
||||
const sangeunSec = section(L("M02_SangeunTitle"), sangeun.body);
|
||||
// 실무자료로 일위대가를 만들었음(확인 창 · 엑셀 보기 창) → 목록 다시 · 이 패널이 내려갔으면 듣기 끝
|
||||
const onBuilt = (): void =>
|
||||
precombo.body.isConnected
|
||||
? void precombo.refresh()
|
||||
? void Promise.all([precombo.refresh(), sangeun.refresh()])
|
||||
: document.removeEventListener(HOPYO_BUILT, onBuilt);
|
||||
document.addEventListener(HOPYO_BUILT, onBuilt);
|
||||
// 도면 양식은 시스템 층(마스터 템플릿)에만
|
||||
@@ -171,6 +184,7 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
const listHost = el("div", {
|
||||
className: "m02-side__lists ui-sidebar-panel",
|
||||
children: [
|
||||
sangeunSec,
|
||||
precomboSec,
|
||||
summary,
|
||||
createDivider(),
|
||||
@@ -229,6 +243,7 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
* (양식이 열려 있으면 실무문서 고름은 지움) */
|
||||
function paint(): void {
|
||||
paintList();
|
||||
sangeun.paint();
|
||||
precombo.paint();
|
||||
hopyo.paint();
|
||||
paintButtons();
|
||||
@@ -253,7 +268,7 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
if (mine !== seq) return;
|
||||
paintList();
|
||||
paintButtons();
|
||||
await precombo.refresh();
|
||||
await Promise.all([precombo.refresh(), sangeun.refresh()]);
|
||||
}
|
||||
|
||||
/* --- 시스템 층 새로 · 본떠 · 지우기(도면 양식만 — 구조물 도면은 집계표 열이 정함) --- */
|
||||
@@ -333,11 +348,25 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
}
|
||||
}
|
||||
|
||||
const reveal = (target: "옵션" | "도면" | "일위대가조합"): void => {
|
||||
const sec = target === "일위대가조합" ? precomboSec : target === "도면" ? drawing : null;
|
||||
const reveal = (target: "옵션" | "도면" | "일위대가조합" | "단가산출"): void => {
|
||||
const sec =
|
||||
target === "단가산출"
|
||||
? sangeunSec
|
||||
: target === "일위대가조합"
|
||||
? precomboSec
|
||||
: target === "도면"
|
||||
? drawing
|
||||
: null;
|
||||
sec?.classList.remove("is-collapsed");
|
||||
};
|
||||
|
||||
void refresh();
|
||||
return { root, refresh, paint, reveal, refreshPrecombo: precombo.refresh };
|
||||
return {
|
||||
root,
|
||||
refresh,
|
||||
paint,
|
||||
reveal,
|
||||
refreshPrecombo: precombo.refresh,
|
||||
refreshSangeun: sangeun.refresh,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_UI_Side_Sangeun.ts
|
||||
* 좌측 맨 위 「단가산출」 칸(717-2a · 요구서 4장 12) — 서고 칸(`UI_Side_Library` · 716-4)에 단가산출 몫만 줌:
|
||||
* 종류 sangeun(SK-) · 그 층 목록표 고름 · 새 문서 몸 · 글. 층 단추는 페이지가 만듦(칸마다 따로 · 기본 개인).
|
||||
* 문서 누름 = 편집 칸(UI_Main 종류 sangeun = `Sangeun_View`) · 켜진 층 단추 다시 누름 = 목록표(`Library_List`).
|
||||
* ========================================================================== */
|
||||
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { sangeunBlank } from "./M02_MasterTemplete_Library_List";
|
||||
import type { Selection } from "./M02_MasterTemplete_UI_Main";
|
||||
import {
|
||||
buildLibrarySection,
|
||||
type LibraryHandle,
|
||||
type LibraryOptions,
|
||||
type LibraryScope,
|
||||
} from "./M02_MasterTemplete_UI_Side_Library";
|
||||
|
||||
/** 그 층 목록표 고름(켜진 층 단추 다시 누름) */
|
||||
export const sangeunList = (scope: LibraryScope): Selection => ({
|
||||
layer: scope.layer,
|
||||
owner: scope.owner,
|
||||
canWrite: scope.canWrite,
|
||||
projectId: null,
|
||||
kind: "sangeun",
|
||||
name: "",
|
||||
label: L("M02_SangeunTitle"),
|
||||
});
|
||||
|
||||
export type SangeunOptions = Pick<
|
||||
LibraryOptions,
|
||||
"scope" | "bar" | "confirmLeave" | "onOpen" | "getOpen"
|
||||
>;
|
||||
|
||||
export const buildSangeunSection = (opt: SangeunOptions): LibraryHandle =>
|
||||
buildLibrarySection({
|
||||
...opt,
|
||||
kind: "sangeun",
|
||||
list: sangeunList,
|
||||
blank: sangeunBlank,
|
||||
text: { none: L("M02_SangeunNone"), newHere: L("M02_SangeunNewHere") },
|
||||
});
|
||||
@@ -49,16 +49,17 @@ def test_줄_해시_옵션은_M01():
|
||||
from test_b01_settings_cards import _cards
|
||||
|
||||
cards = _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3)
|
||||
heads = [c[2].split("?")[0] + "?" + c[2].split("?")[1].split("&")[0] for c in cards[1:7]]
|
||||
heads = [c[2].split("?")[0] + "?" + c[2].split("?")[1].split("&")[0] for c in cards[1:8]]
|
||||
assert heads == [
|
||||
"m01-master-data?open=로직",
|
||||
"m01-master-data?open=옵션",
|
||||
"m01-master-data?open=별칭", # 718-3
|
||||
"m02-master-template?open=단가산출", # 717-2a
|
||||
"m02-master-template?open=일위대가조합",
|
||||
"m02-master-template?open=구조물조합",
|
||||
"m02-master-template?open=도면",
|
||||
]
|
||||
assert all("&root=master" in c[2] for c in cards[4:7])
|
||||
assert all("&root=master" in c[2] for c in cards[4:8])
|
||||
|
||||
|
||||
@needs_node
|
||||
@@ -77,7 +78,7 @@ def test_해시_진입_셋():
|
||||
+ fn.group(0).replace("export function", "function")
|
||||
+ "\nconsole.log(JSON.stringify(entryFromHash(null)));"
|
||||
)
|
||||
for want in ("구조물조합", "옵션", "도면", "일위대가조합"):
|
||||
for want in ("구조물조합", "옵션", "도면", "일위대가조합", "단가산출"):
|
||||
got = _run(script, f"open={want}&root=master&company=3")
|
||||
assert got["open"] == want and got["root"] == "master" and got["company"] == "3"
|
||||
|
||||
@@ -85,8 +86,12 @@ def test_해시_진입_셋():
|
||||
def test_reveal_옵션은_M01_로():
|
||||
side = _text(M02 / "M02_MasterTemplete_UI_Side.ts")
|
||||
body = side.split("const reveal = ")[1].split("\n };")[0]
|
||||
# 716-1 · 716-3 — 옵션 정의는 M01 로 옮김 · M02 reveal 은 일위대가 · 도면만 펼침
|
||||
assert 'target === "일위대가조합" ? precomboSec : target === "도면" ? drawing : null' in body
|
||||
# 716-1 · 716-3 · 717-2a — 옵션 정의는 M01 로 옮김 · M02 reveal 은 단가산출 · 일위대가 · 도면만 펼침
|
||||
flat = " ".join(body.split())
|
||||
assert (
|
||||
'target === "단가산출" ? sangeunSec : target === "일위대가조합" ? precomboSec'
|
||||
' : target === "도면" ? drawing : null'
|
||||
) in flat
|
||||
assert 'sec?.classList.remove("is-collapsed");' in body
|
||||
assert "options" not in body and "옵션: section(" not in side
|
||||
page = _text(M02 / "M02_MasterTemplete_UI_Page.ts")
|
||||
|
||||
@@ -97,22 +97,23 @@ WRITE_CO = READ_CO.replace("CompanyRead", "CompanyWrite")
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
def test_시스템_관리자_줄_칸별_여덟_고른_회사_해시():
|
||||
# 716-2 · 718-3 — 대시보드 설계 기본값 = 칸별 줄: M01 4(마스터 요소 · 단가산출 로직 · 구조물 옵션 정의 ·
|
||||
# 별칭 장부) +
|
||||
# M02 3(일위대가 · 구조물 조합 · 도면 양식) + 계산 자리
|
||||
def test_시스템_관리자_줄_칸별_아홉_고른_회사_해시():
|
||||
# 716-2 · 718-3 · 717-2a — 대시보드 설계 기본값 = 칸별 줄: M01 4(마스터 요소 · 단가산출 로직 ·
|
||||
# 구조물 옵션 정의 · 별칭 장부) +
|
||||
# M02 4(단가산출 · 일위대가 · 구조물 조합 · 도면 양식) + 계산 자리
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3) == [
|
||||
["M01_Title", READ_SRC, M01],
|
||||
["B01_Dashboard_CardLogic", READ_SRC, f"{M01}?open=로직"],
|
||||
["B01_Dashboard_CardStructOptionDef", WRITE_M, f"{M01}?open=옵션"],
|
||||
["B01_Dashboard_CardAliasLedger", WRITE_M, f"{M01}?open=별칭"],
|
||||
["B01_Dashboard_CardSangeun", WRITE_M, f"{M02}?open=단가산출&root=master&company=3"],
|
||||
["B01_Dashboard_CardPrecombo", WRITE_M, f"{M02}?open=일위대가조합&root=master&company=3"],
|
||||
["B01_Dashboard_CardStructCombo", WRITE_M, f"{M02}?open=구조물조합&root=master&company=3"],
|
||||
["B01_Dashboard_CardDrawing", WRITE_M, f"{M02}?open=도면&root=master&company=3"],
|
||||
CALC_SITES,
|
||||
]
|
||||
# 회사가 없으면 해시에 안 실음
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False})[5][2] == (
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False})[6][2] == (
|
||||
f"{M02}?open=구조물조합&root=master"
|
||||
)
|
||||
|
||||
@@ -243,7 +244,8 @@ def test_구조물_조합_기본_층_개인():
|
||||
134-4 — 층 없는 주소도 개인(회사 없으면 마스터) · 사전조합 기본 층도 같은 규칙."""
|
||||
combo = f"{M02}?open=구조물조합&root=personal"
|
||||
rows = _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": 1}, 3)
|
||||
assert rows[4:7] == [
|
||||
assert rows[4:8] == [
|
||||
["B01_Dashboard_CardSangeun", WRITE_M, f"{M02}?open=단가산출&root=personal&company=3"],
|
||||
["B01_Dashboard_CardPrecombo", WRITE_M, f"{M02}?open=일위대가조합&root=personal&company=3"],
|
||||
["B01_Dashboard_CardStructCombo", WRITE_M, f"{combo}&company=3"],
|
||||
["B01_Dashboard_CardDrawing", WRITE_M, f"{M02}?open=도면&root=master&company=3"],
|
||||
@@ -254,7 +256,7 @@ def test_구조물_조합_기본_층_개인():
|
||||
assert _cards({"role": "USER", "is_master": True, "company_id": 1})[0][2] == combo
|
||||
assert _cards({"role": "USER", "is_master": False, "company_id": 1})[0][2] == combo
|
||||
# 개인 층 없음(회사 없음) = 지금 규칙
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": None})[5][2] == (
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": None})[6][2] == (
|
||||
f"{M02}?open=구조물조합&root=master"
|
||||
)
|
||||
assert _cards({"role": "ADMIN", "is_master": False, "company_id": None})[0][2] == (
|
||||
|
||||
@@ -89,7 +89,8 @@ def test_주소_인자를_라우트에서_뗌():
|
||||
assert (
|
||||
'raw === "master" || raw === "company" || raw === "personal"' in layer
|
||||
) # 없음 = 기본(134-4)
|
||||
assert '["구조물조합", "옵션", "도면", "일위대가조합"]' in layer # 일위대가 조합 서고 = 50-3
|
||||
# 일위대가 조합 서고 = 50-3 · 단가산출 = 717-2a
|
||||
assert '["구조물조합", "옵션", "도면", "일위대가조합", "단가산출"]' in layer
|
||||
picker = _text(ROOT / "ui_template" / "ui_template_item_picker.ts")
|
||||
assert 'open: "구조물조합", root: "personal"' in picker and "layer=personal" not in picker
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ def test_분류_고정_폴더_지움_자유_폴더_메뉴_끌어_옮기기():
|
||||
def test_목록표_빈_구분_미분류_고르기_창_새_분류_거름():
|
||||
"""149-2 — 목록표 빈 구분 = 미분류 · 고르기 창 거름 = 품셈 구분 + 사전조합 새 분류"""
|
||||
lib = _text("M02_MasterTemplete_Library_List.ts")
|
||||
assert 'row.구분 || tc("Lib_Unsorted"),' in lib
|
||||
assert '(r) => r.구분 || tc("Lib_Unsorted")],' in lib # 717-2a 열 표(COLUMNS)
|
||||
pick = _text("M02_MasterTemplete_Labor_Pick.ts")
|
||||
assert "new Map([...subs, ...classes].map((s) => [s.name, new Set(s.details)]))" in pick
|
||||
assert "classes = precomboClasses(def);" in pick
|
||||
@@ -143,6 +143,7 @@ def test_왼쪽_칸_차례_일위대가_구조물조합_구분선_불러오기_7
|
||||
side = _text("M02_MasterTemplete_UI_Side.ts")
|
||||
order = side[side.index("m02-side__lists") :]
|
||||
marks = [
|
||||
"sangeunSec,", # 717-2a 맨 위
|
||||
"precomboSec,",
|
||||
"summary,",
|
||||
"createDivider()",
|
||||
|
||||
@@ -191,4 +191,20 @@ export const ui_locales_m2 = {
|
||||
M02_HopyoImport: ["일위대가 불러오기", "Import unit-price sheets"],
|
||||
B01_Dashboard_CardPrecombo: ["일위대가", "Unit-cost items"],
|
||||
M02_PrecomboTitle: ["일위대가", "Unit-cost items"],
|
||||
B01_Dashboard_CardSangeun: ["단가산출", "Unit-price analyses"],
|
||||
M02_SangeunTitle: ["단가산출", "Unit-price analyses"],
|
||||
M02_SangeunNone: ["단가산출 없음", "No unit-price analyses"],
|
||||
M02_SangeunNewHere: ["여기에 새 단가산출", "New unit-price analysis here"],
|
||||
M02_SangeunBase: ["바탕", "Base"],
|
||||
M02_SangeunNoBase: ["바탕 없음", "No base"],
|
||||
M02_SangeunEdited: ["고친 줄", "Edited rows"],
|
||||
M02_SangeunUsers: ["쓰는 곳", "Used by"],
|
||||
M02_SangeunCover: ["덮개", "Overrides"],
|
||||
M02_SangeunAdded: ["더한 줄", "Added rows"],
|
||||
M02_SangeunRemoved: ["뺀 줄", "Removed rows"],
|
||||
M02_SangeunOpened: ["열린 입력", "Open inputs"],
|
||||
M02_SangeunDetailSoon: [
|
||||
"단가산출 상세 표 · 고침 — 준비 중(717-2b)",
|
||||
"Unit-price detail table and editing — coming next (717-2b)",
|
||||
],
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user