feat(M02): 좌측 구조물 도면 · 면적산출 · 재료산출 줄 · Api_Fetch 새 이름
- copyArea · copyMaterial · fetchStructureNumbers → {도번, 면적번, 재료번} · Kind area · material
- 옛 이름 copyBasis · 표번 · Kind basis 는 sub2 가 옮길 때까지 별칭(@deprecated)
- 좌측 목록 셋 · 줄 글 = 번호 맨 앞 + 이름 + 변수값
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUypcnp5d1gU2aeKh9F2H7
This commit is contained in:
@@ -8,8 +8,9 @@ import { API_BASE_URL } from "@config/config_frontend";
|
||||
/** 층 이름 — 서버와 같은 글 */
|
||||
export type Layer = "system" | "company" | "personal" | "project";
|
||||
/** 서버 종류 — 화면의 「표 양식」 = table · 「도면 양식」 = drawing · 「구조물 도면」 = structure ·
|
||||
* 「상세 산출근거」 = basis(구조물집계표 열마다 한 통합문서) */
|
||||
export type Kind = "table" | "drawing" | "structure" | "basis";
|
||||
* 「면적산출」 = area · 「재료산출」 = material(둘 다 구조물집계표 열마다 한 통합문서) ·
|
||||
* @deprecated "basis" — 옛 이름(서버 길은 area 로 보냄) · sub2 가 새 이름으로 옮기면 지움 */
|
||||
export type Kind = "table" | "drawing" | "structure" | "area" | "material" | "basis";
|
||||
|
||||
export interface TemplateInfo {
|
||||
종류: Kind;
|
||||
@@ -49,8 +50,10 @@ const enc = encodeURIComponent;
|
||||
const q = (projectId: string | null): string => (projectId ? `?project_id=${enc(projectId)}` : "");
|
||||
const head = (layer: Layer): string =>
|
||||
layer === "system" ? "/templates" : `/layers/${layer}/templates`;
|
||||
/** 서버 길의 종류 — 옛 "basis" 는 "area" 로 */
|
||||
const kindPath = (kind: Kind): string => (kind === "basis" ? "area" : kind);
|
||||
const item = (layer: Layer, kind: Kind, name: string, projectId: string | null): string =>
|
||||
`${head(layer)}/${kind}/${enc(name)}${layer === "system" ? "" : q(projectId)}`;
|
||||
`${head(layer)}/${kindPath(kind)}/${enc(name)}${layer === "system" ? "" : q(projectId)}`;
|
||||
|
||||
/** 시스템 길은 배열 · 층 길은 `{층, 양식: [...]}` — 둘 다 목록으로 */
|
||||
export const listTemplates = async (
|
||||
@@ -82,7 +85,7 @@ export const saveTemplate = (
|
||||
|
||||
/** 시스템 층만 지움 */
|
||||
export const deleteTemplate = (kind: Kind, name: string, 판: string): Promise<{ ok: boolean }> =>
|
||||
call(`/templates/${kind}/${enc(name)}?판=${enc(판)}`, { method: "DELETE" });
|
||||
call(`/templates/${kindPath(kind)}/${enc(name)}?판=${enc(판)}`, { method: "DELETE" });
|
||||
|
||||
/** 개인·회사 층에서 지움 — 판이 맞을 때만(다르면 409) */
|
||||
export const deleteLayerTemplate = (
|
||||
@@ -91,12 +94,14 @@ export const deleteLayerTemplate = (
|
||||
name: string,
|
||||
판: string,
|
||||
): Promise<{ ok: boolean }> =>
|
||||
call(`/layers/${layer}/templates/${kind}/${enc(name)}?판=${enc(판)}`, { method: "DELETE" });
|
||||
call(`/layers/${layer}/templates/${kindPath(kind)}/${enc(name)}?판=${enc(판)}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
/* --- 구조물 도면(종류 structure · 이름 = 구조물집계표 열 id) ---
|
||||
* 읽기 · 저장 · 지우기는 위 readTemplate · saveTemplate · deleteTemplate 에 "structure" */
|
||||
|
||||
/** 구조물 도면 문서 — `도면` = 도면 양식과 같은 CAD 문서 · 산출근거는 따로(`basis` · 같은 열 id) */
|
||||
/** 구조물 도면 문서 — `도면` = 도면 양식과 같은 CAD 문서 · 면적산출 · 재료산출은 따로(같은 열 id) */
|
||||
export interface StructureDoc {
|
||||
양식: "구조물도면";
|
||||
종류: "structure";
|
||||
@@ -110,30 +115,45 @@ export interface StructureDoc {
|
||||
export const listStructures = async (): Promise<TemplateInfo[]> =>
|
||||
(await listTemplates("system", null)).filter((t) => t.종류 === "structure");
|
||||
|
||||
/** 새로 — 도번 자동 · 빈 도면 (+ 없으면 빈 산출근거 통합문서) · 이미 있으면 StaleError */
|
||||
/** 새로 — 도번 자동 · 빈 도면 (+ 없으면 빈 면적산출 · 재료산출 통합문서) · 이미 있으면 StaleError */
|
||||
export const createStructure = (column: string): Promise<TemplateDoc> =>
|
||||
call(`/structures/${enc(column)}`, { method: "POST" });
|
||||
|
||||
/** 한 열의 번호 — 도번 `SD` · 표번 `SC` + 같은 6자리 [종류][분류1][분류2](없는 쪽은 빈 글) */
|
||||
/** 한 열의 번호 — 도번 `SD` · 면적번 `SA` · 재료번 `SM` + 같은 8자리
|
||||
* [종류][분류1][분류2][분류3](없는 쪽은 빈 글) */
|
||||
export interface StructureNumbers {
|
||||
도번: string;
|
||||
표번: string;
|
||||
면적번: string;
|
||||
재료번: string;
|
||||
/** @deprecated 옛 이름 — 면적번과 같은 값 · sub2 가 옮기면 지움 */
|
||||
표번?: string;
|
||||
}
|
||||
|
||||
/** `{열 id: {도번, 표번}}` — 구조물 열만(글 열 · 없는 열은 키 없음 = 「없음」) */
|
||||
export const fetchStructureNumbers = (): Promise<Record<string, StructureNumbers>> =>
|
||||
call("/structures/numbers");
|
||||
/** `{열 id: {도번, 면적번, 재료번}}` — 구조물 열만(글 열 · 없는 열은 키 없음 = 「없음」) */
|
||||
export const fetchStructureNumbers = async (): Promise<Record<string, StructureNumbers>> => {
|
||||
const got = await call<Record<string, StructureNumbers>>("/structures/numbers");
|
||||
for (const row of Object.values(got)) row.표번 = row.면적번;
|
||||
return got;
|
||||
};
|
||||
|
||||
/** 다른 열(`from`) 구조물 도면 내용을 받는 열(`column`)로 가져옴 — 도번 · 열은 받는 쪽 그대로 */
|
||||
export const copyStructure = async (column: string, from: string): Promise<void> => {
|
||||
await call(`/structures/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" });
|
||||
};
|
||||
|
||||
/** 다른 열(`from`) 산출근거 내용을 받는 열(`column`)로 가져옴 — 표번 · 열은 받는 쪽 그대로 */
|
||||
export const copyBasis = async (column: string, from: string): Promise<void> => {
|
||||
await call(`/basis/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" });
|
||||
/** 다른 열(`from`) 면적산출 내용(알약 연결 포함)을 받는 열(`column`)로 가져옴 — 면적번 · 열은 그대로 */
|
||||
export const copyArea = async (column: string, from: string): Promise<void> => {
|
||||
await call(`/area/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" });
|
||||
};
|
||||
|
||||
/** 다른 열(`from`) 재료산출 내용(알약 연결 포함)을 받는 열(`column`)로 가져옴 — 재료번 · 열은 그대로 */
|
||||
export const copyMaterial = async (column: string, from: string): Promise<void> => {
|
||||
await call(`/material/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" });
|
||||
};
|
||||
|
||||
/** @deprecated 옛 이름 — copyArea · sub2 가 옮기면 지움 */
|
||||
export const copyBasis = copyArea;
|
||||
|
||||
/* --- 프로젝트 층 단추 다섯 --- */
|
||||
const project = (id: string, tail: string): string => `/projects/${enc(id)}/templates/${tail}`;
|
||||
const post = (path: string, body: object = {}): Promise<unknown> =>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_UI_Side.ts
|
||||
* 마스터 템플릿 좌측 패널 — 시스템 양식 목록(표/도면/구조물 도면/산출근거) · 새로 · 본떠 · 지우기.
|
||||
* 서버 길은 계약 `6_계약.md`. 구조물 도면 · 산출근거 줄은 집계표 구조물 열(꼴=수)마다 하나씩 —
|
||||
* 서버가 미리 만들어 둠(도번 SD · 표번 SC + 같은 6자리) · 줄 글은 번호가 맨 앞(「SD010101 돌기슭막이 …」).
|
||||
* 마스터 템플릿 좌측 패널 — 시스템 양식 목록(표/도면/구조물 도면/면적산출/재료산출) · 새로 · 본떠 · 지우기.
|
||||
* 서버 길은 계약 `6_계약.md`. 구조물 도면 · 면적산출 · 재료산출 줄은 집계표 구조물 열(꼴=수)마다 하나씩 —
|
||||
* 서버가 미리 만들어 둠(도번 SD · 면적번 SA · 재료번 SM + 같은 8자리) ·
|
||||
* 줄 글은 번호가 맨 앞(「SD01010100 돌기슭막이 …」).
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
@@ -40,6 +41,13 @@ const KINDS: { kind: Kind; label: () => string }[] = [
|
||||
{ kind: "table", label: () => L("M02_KindTable") },
|
||||
{ kind: "drawing", label: () => L("M02_KindDrawing") },
|
||||
];
|
||||
/** 구조물 열마다 한 줄씩인 목록 셋 — 종류 · 번호 칸 · 제목 */
|
||||
type ColumnKind = "structure" | "area" | "material";
|
||||
const COLUMN_KINDS: { kind: ColumnKind; key: keyof StructureNumbers; label: () => string }[] = [
|
||||
{ kind: "structure", key: "도번", label: () => L("M02_KindStructure") },
|
||||
{ kind: "area", key: "면적번", label: () => L("M02_KindArea") },
|
||||
{ kind: "material", key: "재료번", label: () => L("M02_KindMaterial") },
|
||||
];
|
||||
/** 새 양식의 첫 문서 — 표는 `3_표양식.md` 4장 틀 · 도면은 빈 웹캐드 문서 */
|
||||
function stubDoc(kind: Kind, name: string): unknown {
|
||||
return kind === "table"
|
||||
@@ -102,15 +110,15 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
const bodies = new Map<Kind, HTMLElement>(
|
||||
KINDS.map((k) => [k.kind, el("div", { className: "m02-side__items ui-sidebar-list" })]),
|
||||
);
|
||||
const structureBody = el("div", { className: "m02-side__items ui-sidebar-list" });
|
||||
const basisBody = el("div", { className: "m02-side__items ui-sidebar-list" });
|
||||
const columnBodies = COLUMN_KINDS.map(() =>
|
||||
el("div", { className: "m02-side__items ui-sidebar-list" }),
|
||||
);
|
||||
const listHost = el("div", {
|
||||
className: "m02-side__lists ui-sidebar-panel",
|
||||
children: [
|
||||
section(L("M02_KindTable"), bodies.get("table")!),
|
||||
section(L("M02_KindDrawing"), bodies.get("drawing")!),
|
||||
section(L("M02_KindStructure"), structureBody),
|
||||
section(L("M02_KindBasis"), basisBody),
|
||||
...COLUMN_KINDS.map((k, i) => section(k.label(), columnBodies[i])),
|
||||
],
|
||||
});
|
||||
attachCollapsible(listHost);
|
||||
@@ -158,14 +166,12 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/** 구조물 도면 · 산출근거 줄 — 구조물 열마다 하나 · 「번호 이름 변수값」(번호가 없으면 「없음」) */
|
||||
function paintRows(body: HTMLElement, kind: "structure" | "basis"): void {
|
||||
/** 구조물 도면 · 면적산출 · 재료산출 줄 — 구조물 열마다 하나 · 「번호 이름 변수값」(번호가 없으면 「없음」) */
|
||||
function paintRows(body: HTMLElement, kind: ColumnKind, key: keyof StructureNumbers): void {
|
||||
body.replaceChildren(
|
||||
...(columns.length
|
||||
? columns.map((col) => {
|
||||
const num =
|
||||
(kind === "structure" ? numbers[col.id]?.도번 : numbers[col.id]?.표번) ||
|
||||
L("M02_StructureNone");
|
||||
const num = numbers[col.id]?.[key] || L("M02_StructureNone");
|
||||
const b = el("button", {
|
||||
className: `m02-side__item ui-sidebar-row${isPicked(kind, col.id) ? " is-active" : ""}`,
|
||||
text: structureLabel(num, col),
|
||||
@@ -177,8 +183,8 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
: [el("p", { className: "m02-side__empty", text: L("M02_NoTemplates") })]),
|
||||
);
|
||||
}
|
||||
const paintStructure = (): void => paintRows(structureBody, "structure");
|
||||
const paintBasis = (): void => paintRows(basisBody, "basis");
|
||||
const paintColumns = (): void =>
|
||||
COLUMN_KINDS.forEach((k, i) => paintRows(columnBodies[i], k.kind, k.key));
|
||||
|
||||
function paintButtons(): void {
|
||||
const editable = canEdit(layer, opt.isAdmin);
|
||||
@@ -195,13 +201,12 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
paintButtons();
|
||||
}
|
||||
|
||||
async function selectColumn(col: StructureColumn, kind: "structure" | "basis"): Promise<void> {
|
||||
async function selectColumn(col: StructureColumn, kind: ColumnKind): Promise<void> {
|
||||
if (!isPicked(kind, col.id) && !(await opt.confirmLeave())) return;
|
||||
picked = null;
|
||||
// 제목은 편집 화면이 문서 번호를 앞에 붙임(「SD010101 돌기슭막이 …」)
|
||||
opt.onOpen({ layer, projectId: null, kind, name: col.id, label: columnHead(col) });
|
||||
paintStructure();
|
||||
paintBasis();
|
||||
paintColumns();
|
||||
paintButtons();
|
||||
}
|
||||
|
||||
@@ -219,8 +224,7 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
numbers = {};
|
||||
showToast(L("M02_LoadFailed").replace("{value}", why(error)), "error");
|
||||
}
|
||||
paintStructure();
|
||||
paintBasis();
|
||||
paintColumns();
|
||||
}
|
||||
|
||||
async function refresh(): Promise<void> {
|
||||
@@ -320,8 +324,7 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
select: (kind, name) => {
|
||||
picked = { kind, name };
|
||||
paintList();
|
||||
paintStructure();
|
||||
paintBasis();
|
||||
paintColumns();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -49,7 +49,8 @@ def test_소스가_700줄을_넘지_않음():
|
||||
),
|
||||
# 산출근거 저장은 컨테이너가 안 함 — [산출근거 수정] 이 여는 메인 칸 전체 편집이 함
|
||||
("M02_MasterTemplete_Api_Fetch.ts", ["saveTemplate"], MAIN),
|
||||
# 좌측 구조물 도면 · 산출근거 줄 번호는 한 번에 받음(`{열 id: {도번, 표번}}`)
|
||||
# 좌측 구조물 도면 · 면적산출 · 재료산출 줄 번호는 한 번에 받음
|
||||
# (`{열 id: {도번, 면적번, 재료번}}`)
|
||||
("M02_MasterTemplete_Api_Fetch.ts", ["fetchStructureNumbers"], SIDE),
|
||||
("M02_MasterTemplete_Structure.ts", ["structureLabel", "isStructureColumn"], SIDE),
|
||||
("M02_MasterTemplete_Drawing.ts", ["mountDrawingTemplate"], PANEL),
|
||||
@@ -68,12 +69,15 @@ def test_컨테이너가_쓰는_이름이_내보내진_것(source: str, names: l
|
||||
def test_sub2_가_부르는_이름과_모양():
|
||||
"""브레인 확정 이름 · 모양 — sub2 가 이대로 부름(바꾸면 브레인에게)."""
|
||||
body = _text(M02 / "M02_MasterTemplete_Api_Fetch.ts")
|
||||
for name in ("copyStructure", "copyBasis"):
|
||||
for name in ("copyStructure", "copyArea", "copyMaterial"):
|
||||
assert re.search(
|
||||
rf"export const {name} = async \(column: string, from: string\): Promise<void>", body
|
||||
), name
|
||||
assert "fetchStructureNumbers = (): Promise<Record<string, StructureNumbers>>" in body
|
||||
assert re.search(r"interface StructureNumbers \{\s*도번: string;\s*표번: string;\s*\}", body)
|
||||
assert "fetchStructureNumbers = async (): Promise<Record<string, StructureNumbers>>" in body
|
||||
assert re.search(
|
||||
r"interface StructureNumbers \{\s*도번: string;\s*면적번: string;\s*재료번: string;", body
|
||||
)
|
||||
assert re.search(r'export type Kind = [^;]*"area" \| "material"', body)
|
||||
|
||||
|
||||
def test_집계표만_컨테이너를_붙임():
|
||||
|
||||
@@ -9,7 +9,8 @@ export const ui_locales_m2 = {
|
||||
M02_KindTable: ["표 양식", "Table templates"],
|
||||
M02_KindDrawing: ["도면 양식", "Drawing templates"],
|
||||
M02_KindStructure: ["구조물 도면", "Structure drawings"],
|
||||
M02_KindBasis: ["산출근거", "Cost basis"],
|
||||
M02_KindArea: ["면적산출", "Area takeoff"],
|
||||
M02_KindMaterial: ["재료산출", "Material takeoff"],
|
||||
M02_NoTemplates: ["양식 없음", "None"],
|
||||
M02_New: ["새로", "New"],
|
||||
M02_Copy: ["본떠 만들기", "Copy as new"],
|
||||
|
||||
Reference in New Issue
Block a user