refactor(M02): 재료산출 시트 지움 — 종류 material · 재료번 SM · 좌측 줄 · 집계표 「재료산출」 줄 · 카드 · 전체 편집 · 복사 길 · 프로젝트 복사본 「재료」 · 고르기 모달 · 시스템 층 · fixture · 시험
- 새 프로젝트 씨앗(KINDS)에서도 빠짐 · 새 열은 구조물 도면 · 면적산출만 만듦 - storage 층(회사 · 개인 · 프로젝트) material/ 파일은 지우지 않음(읽기만 끊음) - 장부 재료번 칸은 다음 집계표 [저장] 때 빠짐 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
This commit is contained in:
@@ -50,13 +50,12 @@ from M02_MasterTemplete.M02_Item_Source import (
|
||||
KINDS = {
|
||||
"도면": "도면.json",
|
||||
"면적": "면적.json",
|
||||
"재료": "재료.json",
|
||||
"입력연결": LINK_FILE,
|
||||
"입력값": VALUES_FILE,
|
||||
"고른자재": PICKS_FILE,
|
||||
}
|
||||
#: 뼈대 검사에 쓰는 층 양식 종류(도면 · 면적산출 · 재료산출)
|
||||
SKELETON = {"도면": "structure", "면적": "area", "재료": "material"}
|
||||
#: 뼈대 검사에 쓰는 층 양식 종류(도면 · 면적산출)
|
||||
SKELETON = {"도면": "structure", "면적": "area"}
|
||||
SEATS = ("면적", "재료", "선택", None)
|
||||
_MT = re.compile(r"^MT\d{6}$")
|
||||
|
||||
@@ -190,13 +189,13 @@ def write_file(
|
||||
content: Any,
|
||||
recalc: Callable[[dict], dict] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""판이 같을 때만 씀 — 면적 · 재료는 `recalc`(서버 계산값)를 거친 것을 씀."""
|
||||
"""판이 같을 때만 씀 — 면적은 `recalc`(서버 계산값)를 거친 것을 씀."""
|
||||
target = folder_of(project_root, folder)
|
||||
check(kind, content)
|
||||
current = version_of(read_json(target / KINDS[kind]))
|
||||
if version != current:
|
||||
raise Stale(current)
|
||||
if recalc is not None and kind in ("면적", "재료"):
|
||||
if recalc is not None and kind == "면적":
|
||||
content = recalc(content)
|
||||
atomic_write_json(target / KINDS[kind], content)
|
||||
manifest = read_json(target / MANIFEST_NAME) or {}
|
||||
@@ -244,7 +243,7 @@ def _kept_picks(picks: Any, labor: dict[str, Any]) -> dict[str, Any]:
|
||||
def reapply(project_root: str | Path, folder: str) -> dict[str, Any]:
|
||||
"""재적용 — 원본(층 · 마스터)을 다시 가져옴 · 입력값 · 고른자재는 같은 키면 살림.
|
||||
|
||||
원본과 달라 덮인 파일(도면 · 면적 · 재료 · 입력연결)은 `덮임` 에 알림."""
|
||||
원본과 달라 덮인 파일(도면 · 면적 · 입력연결)은 `덮임` 에 알림."""
|
||||
target = folder_of(project_root, folder)
|
||||
manifest = read_json(target / MANIFEST_NAME) or {}
|
||||
values = read_json(target / VALUES_FILE)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
붙일 폴더(`dest`) 아래 새 항목 — 대상 층 새 키(SC · SP) · 열 `본뜬` = 원본(층 · 주인 · 키) ·
|
||||
같은 이름은 「이름 (2)」 · 폴더는 안의 항목 · 빈 폴더까지(폴더 이름이 겹치면 「폴더 (2)」).
|
||||
파일(도면 · 면적 · 재료 · 입력 연결)은 `Items.copy_item` 이 옮김. 권한은 부르는 쪽 몫.
|
||||
파일(도면 · 면적 · 입력 연결)은 `Items.copy_item` 이 옮김. 권한은 부르는 쪽 몫.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""프로젝트에서 개인 층으로 — [개인에 저장](save-as) · 프로젝트에서 새 항목(new) (PLAN 26-2b).
|
||||
|
||||
개인 층 새 항목 = 구조물집계표 새 열 하나 + 빈 도면 · 면적산출 · 재료산출(번호 · SS 키는 저장소가
|
||||
줌). [개인에 저장]은 구조물 복사본의 열 정의 · 도면 · 면적 · 재료 · 입력연결을 담음 —
|
||||
개인 층 새 항목 = 구조물집계표 새 열 하나 + 빈 도면 · 면적산출(번호 · SS 키는 저장소가
|
||||
줌). [개인에 저장]은 구조물 복사본의 열 정의 · 도면 · 면적 · 입력연결을 담음 —
|
||||
단가 묶음 · 입력값 · 고른자재는 프로젝트 몫이라 안 가져감. 쓰기는 지금 층 저장소 함수(`store.at` ·
|
||||
`store.write` · 항목 연결 `write_file`) — 나무 고침(`M02_MasterTemplete_Tree`)과
|
||||
같은 집계표 저장 한 줄기(경로 · 머리 줄 수 맞춤 · 번호 · SS 장부)를 탐(PLAN 26-2a).
|
||||
@@ -23,7 +23,7 @@ from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
from M02_MasterTemplete.M02_Item_Source import LINK_FILE, read_json
|
||||
|
||||
#: 복사본 파일 → 층 양식 종류
|
||||
PART_KINDS = (("도면.json", "structure"), ("면적.json", "area"), ("재료.json", "material"))
|
||||
PART_KINDS = (("도면.json", "structure"), ("면적.json", "area"))
|
||||
|
||||
|
||||
def heads_of(path: list[str], name: str | None) -> list[str]:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""구조물 집계 항목 복사본 — 프로젝트 `templates/structures/{구조물 id}/` (PLAN 25-2b · 26-2b).
|
||||
|
||||
복사본은 **구조물마다 하나** — 같은 항목을 고른 구조물 둘도 각자 도면 · 면적 · 재료 ·
|
||||
복사본은 **구조물마다 하나** — 같은 항목을 고른 구조물 둘도 각자 도면 · 면적 ·
|
||||
일위대가 · 입력연결 · 입력값 · 고른자재 · 단가를 가짐(길이 · 소재 · 높이 · 옵션이 구조물마다 다름).
|
||||
폴더 이름 = 구조물 id(`structure_id`) · 관 지점은 `pipe@측점`.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""집계 항목 원본 — 층(시스템 · 회사 · 개인)에서 항목 내용을 모음 (PLAN 21-1 · 25-1b · 25-2b).
|
||||
|
||||
모으는 것 = `{파일 이름: 내용}`:
|
||||
`집계.json` SS 줄 + 열 정의 · `도면.json` SD · `면적.json` SA · `재료.json` SM ·
|
||||
`집계.json` SS 줄 + 열 정의 · `도면.json` SD · `면적.json` SA ·
|
||||
`일위대가.json` UA 줄 + 담은 로직 줄(M01 시스템) ·
|
||||
`입력연결.json` 층의 `inputs/<열id>.json`(없으면 빈 연결) ·
|
||||
`단가.json` 계산에 닿는 마스터 줄 전부
|
||||
@@ -20,8 +20,8 @@ from typing import Any
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
TABLE_NAME = "구조물집계표"
|
||||
#: 열 id → 스냅샷 파일(구조물 도면 · 면적산출 · 재료산출)
|
||||
PART_FILES = (("structure", "도면.json"), ("area", "면적.json"), ("material", "재료.json"))
|
||||
#: 열 id → 스냅샷 파일(구조물 도면 · 면적산출)
|
||||
PART_FILES = (("structure", "도면.json"), ("area", "면적.json"))
|
||||
LINK_FILE = "입력연결.json"
|
||||
PRICE_FILE = "단가.json"
|
||||
LABOR_FILE = "일위대가.json"
|
||||
|
||||
@@ -8,8 +8,8 @@ import { API_BASE_URL } from "@config/config_frontend";
|
||||
/** 층 이름 — 서버와 같은 글 */
|
||||
export type Layer = "system" | "company" | "personal" | "project";
|
||||
/** 서버 종류 — 화면의 「표 양식」 = table · 「도면 양식」 = drawing · 「구조물 도면」 = structure ·
|
||||
* 「면적산출」 = area · 「재료산출」 = material(둘 다 구조물집계표 열마다 한 통합문서) */
|
||||
export type Kind = "table" | "drawing" | "structure" | "area" | "material";
|
||||
* 「면적산출」 = area(구조물집계표 열마다 한 통합문서) */
|
||||
export type Kind = "table" | "drawing" | "structure" | "area";
|
||||
|
||||
export interface TemplateInfo {
|
||||
종류: Kind;
|
||||
@@ -129,7 +129,7 @@ export const deleteLayerTemplate = (
|
||||
/* --- 구조물 도면(종류 structure · 이름 = 구조물집계표 열 id) ---
|
||||
* 읽기 · 저장 · 지우기는 위 readTemplate · saveTemplate · deleteTemplate 에 "structure" */
|
||||
|
||||
/** 구조물 도면 문서 — `도면` = 도면 양식과 같은 CAD 문서 · 면적산출 · 재료산출은 따로(같은 열 id) */
|
||||
/** 구조물 도면 문서 — `도면` = 도면 양식과 같은 CAD 문서 · 면적산출은 따로(같은 열 id) */
|
||||
export interface StructureDoc {
|
||||
양식: "구조물도면";
|
||||
종류: "structure";
|
||||
@@ -143,19 +143,18 @@ 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> =>
|
||||
scoped(`/structures/${enc(column)}`, { method: "POST" });
|
||||
|
||||
/** 한 열의 번호 — 도번 `SD` · 면적번 `SA` · 재료번 `SM` + 같은 8자리
|
||||
/** 한 열의 번호 — 도번 `SD` · 면적번 `SA` + 같은 8자리
|
||||
* [종류][분류1][분류2][분류3](없는 쪽은 빈 글) */
|
||||
export interface StructureNumbers {
|
||||
도번: string;
|
||||
면적번: string;
|
||||
재료번: string;
|
||||
}
|
||||
|
||||
/** `{열 id: {도번, 면적번, 재료번}}` — 구조물 열만(글 열 · 없는 열은 키 없음 = 「없음」) */
|
||||
/** `{열 id: {도번, 면적번}}` — 구조물 열만(글 열 · 없는 열은 키 없음 = 「없음」) */
|
||||
export const fetchStructureNumbers = (): Promise<Record<string, StructureNumbers>> =>
|
||||
scoped("/structures/numbers");
|
||||
|
||||
@@ -247,12 +246,7 @@ export const copyArea = async (column: string, from: string): Promise<void> => {
|
||||
await scoped(`/area/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" });
|
||||
};
|
||||
|
||||
/** 다른 열(`from`) 재료산출 내용(알약 연결 포함)을 받는 열(`column`)로 가져옴 — 재료번 · 열은 그대로 */
|
||||
export const copyMaterial = async (column: string, from: string): Promise<void> => {
|
||||
await scoped(`/material/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" });
|
||||
};
|
||||
|
||||
/** 회사 · 개인 층의 집계표 항목(열 하나 + 도면 · 면적 · 재료 파일 + 자재조합 줄)을 내 개인 층으로 복사 —
|
||||
/** 회사 · 개인 층의 집계표 항목(열 하나 + 도면 · 면적 파일)을 내 개인 층으로 복사 —
|
||||
* 새 SS 키로 만들고 `본뜬` 표시 · `fromOwner` = 회사 층 회사 · 개인 층 「회사/사용자」 */
|
||||
export const copyItemToPersonal = (body: {
|
||||
fromLayer: Layer;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
옛 `SS` 키는 맞출 때 새 모양으로 옮기고(숫자 여섯 그대로 · 겹치면 다음 번호) 옛 키는 줄 `옛키` 에
|
||||
남김 — 프로젝트 복사본이 들고 있는 옛 키로도 찾음(`find`).
|
||||
줄 = `{키, 열, 경로, 이름, 종류, 분류1~3(경로 앞 넷 — 옛 코드가 읽음), 단위, 일위대가,
|
||||
도번, 면적번, 재료번, 바인딩, 펼침, 옛키?}`. `M02_MasterTemplete_Store.py` 가 커서 떼어 냄 —
|
||||
도번, 면적번, 바인딩, 펼침, 옛키?}`. `M02_MasterTemplete_Store.py` 가 커서 떼어 냄 —
|
||||
저장소는 `store.sync_combos` · `store.read_combos` · `store.find_combo` 이름으로 이 파일을 부름.
|
||||
"""
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_CardShell.ts
|
||||
* 항목 카드 공용 틀(PLAN 31-7) — 도면 정보 · 면적산출 · 재료산출 · 선택 입력(관리 화면 · 고르기 모달)이
|
||||
* 항목 카드 공용 틀(PLAN 31-7) — 도면 정보 · 면적산출 · 선택 입력(관리 화면 · 고르기 모달)이
|
||||
* 내용만 넣음. 모양 · 높이 · 스크롤은 `M02_MasterTemplete_Structure.css`.
|
||||
* 머리 = 제목 줄(h3 · 제목 · 저장 안 한 점 ● · 오른쪽 단추 자리) + 한 줄 부제(번호 · 이름 · 넘치면 말줄임)
|
||||
* 본문 = 카드 높이를 채움 · 넘치면 카드 안 스크롤 · 상태 = 불러오는 중 · 실패 + [다시 불러오기] · 비었음
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_ItemCards.ts
|
||||
* 항목 카드 — 도면 정보 · 면적산출 · 재료산출(보기만 + [수정]) + 덧붙는 카드(선택 입력 …).
|
||||
* 항목 카드 — 도면 정보 · 면적산출(보기만 + [수정]) + 덧붙는 카드(선택 입력 …).
|
||||
* M02 구조물 설계 컨테이너(`M02_MasterTemplete_Structure.ts`)에서 떼어 냄(계획서 25-2d) — 항목 고르기
|
||||
* 모달(`ui_template_item_picker_detail.ts`)이 **같은 카드**를 쓰려고. 문서를 어디서 읽는지는
|
||||
* 자료 자리(`ItemSource`)가 정함 · 카드는 몰라도 됨.
|
||||
* 카드 겉 틀 · 제목 줄 · 부제 · [저장] · ? · 점(●) · 상태 글은 공용 틀(`M02_MasterTemplete_CardShell` · 31-7).
|
||||
* 면적산출 · 재료산출 = 스프레드시트 · 부품은 카드를 열 때만 `import()`(다른 페이지 번들에 안 섞임).
|
||||
* 면적산출 = 스프레드시트 · 부품은 카드를 열 때만 `import()`(다른 페이지 번들에 안 섞임).
|
||||
* 덧붙는 카드는 `extraCards` 로 끼움 — 선택 입력 카드(`M02_MasterTemplete_Inputs.ts`)가 그 자리.
|
||||
* `sheetEdit` 가 있으면 면적산출 · 재료산출 카드를 미리보기에서 고침(31-3) — 스프레드시트 `preview`(메뉴 없이
|
||||
* `sheetEdit` 가 있으면 면적산출 카드를 미리보기에서 고침(31-3) — 스프레드시트 `preview`(메뉴 없이
|
||||
* 수식 입력 줄 · 칸 고침 · 단축키) + 아래 알약 줄 + 카드 [저장] · 없으면 보기만(고르기 모달).
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -42,12 +42,10 @@ export type SheetKind = Exclude<EditKind, "structure">;
|
||||
export const PART_FIELD: Record<EditKind, keyof StructureNumbers> = {
|
||||
structure: "도번",
|
||||
area: "면적번",
|
||||
material: "재료번",
|
||||
};
|
||||
export const PART_WORD: Record<EditKind, string> = {
|
||||
structure: "도면",
|
||||
area: "면적산출",
|
||||
material: "재료산출",
|
||||
};
|
||||
|
||||
const TEXT = {
|
||||
@@ -79,7 +77,7 @@ const SHEET_KEYS: [string, string][] = [
|
||||
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : "");
|
||||
const fill = (text: string, value: string): string => text.replace("{value}", value);
|
||||
/** 스프레드시트는 면적산출 · 재료산출 카드를 열 때만 받음 */
|
||||
/** 스프레드시트는 면적산출 카드를 열 때만 받음 */
|
||||
const loadSpreadsheet = () => import("../A00_Common/spreadsheet/spreadsheet");
|
||||
|
||||
export interface ItemCardsOptions {
|
||||
@@ -87,7 +85,7 @@ export interface ItemCardsOptions {
|
||||
source: () => ItemSource;
|
||||
/** [수정] 단추를 보일까 — 부를 때마다 다시 봄 */
|
||||
editable: () => boolean;
|
||||
/** [도면 수정] · [면적산출 수정] · [재료산출 수정] */
|
||||
/** [도면 수정] · [면적산출 수정] */
|
||||
onEdit: (kind: EditKind) => void;
|
||||
/** 문서에 번호가 없을 때 쓸 번호(집계표 번호 줄) */
|
||||
numberOf?: (kind: EditKind, column: string) => string | undefined;
|
||||
@@ -97,11 +95,11 @@ export interface ItemCardsOptions {
|
||||
extraCards?: HTMLElement[];
|
||||
/** 카드 가운데 하나라도 저장 안 한 고침이 생김 · 없어짐(PLAN 31-4 · 31-7) */
|
||||
onDirty?: (dirty: boolean) => void;
|
||||
/** 면적산출 · 재료산출 카드를 미리보기에서 고침(31-3) — 없으면 보기만 */
|
||||
/** 면적산출 카드를 미리보기에서 고침(31-3) — 없으면 보기만 */
|
||||
sheetEdit?: SheetEditOptions;
|
||||
}
|
||||
|
||||
/** 카드 스프레드시트를 다루는 손잡이 — 알약 · 자재 조합 판이 씀 */
|
||||
/** 카드 스프레드시트를 다루는 손잡이 — 알약이 씀 */
|
||||
export interface SheetCardControl {
|
||||
column: string;
|
||||
label: string;
|
||||
@@ -116,7 +114,7 @@ export interface SheetCardControl {
|
||||
export interface SheetEditOptions {
|
||||
/** 지금 고칠 수 있나 — 부를 때마다 다시 봄 */
|
||||
can: () => boolean;
|
||||
/** 아래 알약 줄(입력 연결) · 카드 아래 덧붙는 판(재료산출 = 자재 조합) — 전체 편집기와 같은 부품 */
|
||||
/** 아래 알약 줄(입력 연결) · 카드 아래 덧붙는 판 — 전체 편집기와 같은 부품 */
|
||||
links: (
|
||||
kind: SheetKind,
|
||||
control: SheetCardControl,
|
||||
@@ -135,7 +133,7 @@ export interface ItemCardsHandle {
|
||||
relabel: (labels: Partial<Record<EditKind, string | null>>) => void;
|
||||
/** 지금 보이는 열 id */
|
||||
column: () => string | null;
|
||||
/** 카드(도면 · 면적산출 · 재료산출)에 저장 안 한 고침이 있나 — 페이지가 떠날 때 물음 */
|
||||
/** 카드(도면 · 면적산출)에 저장 안 한 고침이 있나 — 페이지가 떠날 때 물음 */
|
||||
dirty: () => boolean;
|
||||
/** 고친 카드를 모두 저장(도면 · 엑셀) — 실패하면 던짐 */
|
||||
saveAll: () => Promise<void>;
|
||||
@@ -198,7 +196,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
preview ? preview.write() : Promise.resolve(),
|
||||
);
|
||||
|
||||
/** 면적산출 · 재료산출 카드 — 스프레드시트 미리보기 · 카드 고침(31-3) */
|
||||
/** 면적산출 카드 — 스프레드시트 미리보기 · 카드 고침(31-3) */
|
||||
interface SheetCard extends Card {
|
||||
kind: SheetKind;
|
||||
host: HTMLElement;
|
||||
@@ -220,12 +218,11 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
} as SheetCard;
|
||||
return card;
|
||||
};
|
||||
const sheets = [sheetCard("area"), sheetCard("material")];
|
||||
const sheets = [sheetCard("area")];
|
||||
const cards: Card[] = [info, ...sheets];
|
||||
const defaults: Record<EditKind, string> = {
|
||||
structure: TEXT.edit,
|
||||
area: fill(TEXT.editSheet, PART_WORD.area),
|
||||
material: fill(TEXT.editSheet, PART_WORD.material),
|
||||
};
|
||||
|
||||
const root = el("div", {
|
||||
@@ -265,7 +262,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
c.doc = next as Workbook;
|
||||
}
|
||||
|
||||
/** 미리보기 수정 — 메뉴 없이 수식 입력 줄 · 칸 고침 · 아래 알약 줄 · 덧붙는 판(재료산출 = 자재 조합) */
|
||||
/** 미리보기 수정 — 메뉴 없이 수식 입력 줄 · 칸 고침 · 아래 알약 줄 · 덧붙는 판 */
|
||||
const paintEditable = async (
|
||||
c: SheetCard,
|
||||
mine: number,
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
type Layer,
|
||||
} from "./M02_MasterTemplete_Api_Fetch";
|
||||
|
||||
/** 집계표 열 하나에 딸린 문서 셋 — 구조물 도면 · 면적산출 · 재료산출 */
|
||||
export type ItemPart = "structure" | "area" | "material";
|
||||
/** 집계표 열 하나에 딸린 문서 — 구조물 도면 · 면적산출 */
|
||||
export type ItemPart = "structure" | "area";
|
||||
|
||||
export interface ItemSource {
|
||||
/** CAD 자동백업 칸을 가르는 층 — 프로젝트 복사본이면 `project` */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""M02 층 집계표 항목 — 회사 · 개인 층 빈 뼈대 · 항목 목록 · 항목 복사(PLAN 21-1 · 21-2).
|
||||
|
||||
항목 = 구조물집계표조합(SS) 한 줄 — 그 열의 일위대가(UA) ·
|
||||
도번 SD · 면적번 SA · 재료번 SM.
|
||||
도번 SD · 면적번 SA.
|
||||
층 폴더(시스템 · 회사 · 개인)는 모양이 같아 저장소 함수를 `store.at(층 폴더)` 안에서 그대로 부름.
|
||||
권한은 부르는 쪽(`M02_MasterTemplete_Router_Items.py`) 몫 — 여기는 폴더와 파일만.
|
||||
"""
|
||||
@@ -30,7 +30,6 @@ ITEM_FIELDS = (
|
||||
"일위대가",
|
||||
"도번",
|
||||
"면적번",
|
||||
"재료번",
|
||||
)
|
||||
|
||||
|
||||
@@ -62,10 +61,10 @@ def ensure(folder: Path) -> bool:
|
||||
|
||||
def items(folder: Path) -> list[dict[str, Any]]:
|
||||
"""그 층 항목 — SS 줄마다 `{key, 열, 경로, 이름, 종류, 분류1~3, 단위, 일위대가,
|
||||
도번, 면적번, 재료번, names}` · 경로 = 폴더 이름 배열 · 이름 = 경로 이음(26-1 · 종류 ·
|
||||
도번, 면적번, names}` · 경로 = 폴더 이름 배열 · 이름 = 경로 이음(26-1 · 종류 ·
|
||||
분류1~3 은 경로 앞 넷) · `names` = 화면 이름(키 대신 · 없으면 null) —
|
||||
일위대가 = 조합 이름 ·
|
||||
도번 · 면적번 · 재료번 = 항목(열) 이름."""
|
||||
도번 · 면적번 = 항목(열) 이름."""
|
||||
with store.at(folder):
|
||||
ua = combos.names() # 마스터 · 이 층이 쓸 수 있는 회사 · 개인 조합(27-2c)
|
||||
rows = store.read_combos().get("줄", [])
|
||||
@@ -74,9 +73,7 @@ def items(folder: Path) -> list[dict[str, Any]]:
|
||||
head = r.get("이름")
|
||||
names = {
|
||||
"일위대가": ua.get(str(r.get("일위대가"))) if r.get("일위대가") else None,
|
||||
**{
|
||||
k: (head or r.get("열")) if r.get(k) else None for k in ("도번", "면적번", "재료번")
|
||||
},
|
||||
**{k: (head or r.get("열")) if r.get(k) else None for k in ("도번", "면적번")},
|
||||
}
|
||||
out.append({"key": r.get("키"), **{k: r.get(k) for k in ITEM_FIELDS}, "names": names})
|
||||
return out
|
||||
@@ -97,7 +94,7 @@ def copy_item(
|
||||
path: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""다른 층 항목(SS `key`) → `target` 층 새 열 하나 — 머리 · 단위 · 일위대가 · 바인딩 그대로 ·
|
||||
도면 · 면적산출 · 재료산출 내용(알약 연결 포함) · 항목 연결 복사 ·
|
||||
도면 · 면적산출 내용(알약 연결 포함) · 항목 연결 복사 ·
|
||||
새 번호 · 새 SS 키 ·
|
||||
열 `본뜬` = `origin`(원래 층 · 주인 · 키) · `path` = 새 자리(폴더 + 이름 · 없으면 원래 경로 ·
|
||||
붙여넣기 PLAN 30-2). 돌려줌 `{key, 열, 이름}`."""
|
||||
|
||||
@@ -59,7 +59,7 @@ def get_structure_combos() -> dict:
|
||||
|
||||
@router.post("/structures/{column}")
|
||||
def post_structure(column: str) -> dict:
|
||||
"""구조물 도면 새로 — 열 id 하나 · 도번 자동 · 빈 면적산출 · 재료산출도 · 이미 있으면 409."""
|
||||
"""구조물 도면 새로 — 열 id 하나 · 도번 자동 · 빈 면적산출도 · 이미 있으면 409."""
|
||||
return _call(store.create_structure, column)
|
||||
|
||||
|
||||
@@ -104,11 +104,6 @@ def post_area_copy(column: str, source: str = Query(alias="from")) -> dict:
|
||||
return _call(store.copy_from, "area", column, source)
|
||||
|
||||
|
||||
@router.post("/material/{column}/copy")
|
||||
def post_material_copy(column: str, source: str = Query(alias="from")) -> dict:
|
||||
return _call(store.copy_from, "material", column, source)
|
||||
|
||||
|
||||
@router.get("/templates/{kind}/{name}")
|
||||
def get_template(kind: str, name: str) -> dict:
|
||||
return _call(store.read, kind, name)
|
||||
|
||||
@@ -110,8 +110,7 @@ def get_items(
|
||||
all: bool = False, # noqa: A002 — 21-1 약속 이름 `?all=1`
|
||||
session: Session = Depends(verify_session),
|
||||
):
|
||||
"""21-1 — 한 층이면 `[{key, 열, 종류, 분류1~3, 단위, 일위대가, 도번, 면적번,
|
||||
재료번}]` ·
|
||||
"""21-1 — 한 층이면 `[{key, 열, 종류, 분류1~3, 단위, 일위대가, 도번, 면적번}]` ·
|
||||
`?all=1` 이면 세 층 `[{layer, owner, items}]`(회사 · 개인은 세션 것 · 회사 없으면 빈 목록)."""
|
||||
if all:
|
||||
return [_listing(session, one, None) for one in ("system", "company", "personal")]
|
||||
@@ -303,7 +302,7 @@ def post_use_combo(
|
||||
@router.post("/layers/{layer}/{kind}/{column}/copy")
|
||||
def post_copy(
|
||||
layer: Layer,
|
||||
kind: Literal["structures", "area", "material"],
|
||||
kind: Literal["structures", "area"],
|
||||
column: str,
|
||||
source: str = Query(alias="from"),
|
||||
owner: str | None = None,
|
||||
|
||||
@@ -315,8 +315,8 @@ async def save_layer_template(
|
||||
return await asyncio.to_thread(
|
||||
_store_save, folder, kind, name, body.판 or "", document, layer
|
||||
)
|
||||
if kind in ("area", "material"):
|
||||
# 면적산출 · 재료산출 계산값은 서버가 같은 TS 엔진을 Node 로 돌려 새로 적음
|
||||
if kind == "area":
|
||||
# 면적산출 계산값은 서버가 같은 TS 엔진을 Node 로 돌려 새로 적음
|
||||
# (브라우저 값 버림 · 5장 ②)
|
||||
try:
|
||||
document = await asyncio.to_thread(spreadsheet.with_server_values, document)
|
||||
|
||||
@@ -26,7 +26,7 @@ FOLDER: Path = Path(__file__).resolve().parent.parent / "resources" / "master_te
|
||||
KINDS = layers.KINDS # 시험은 FOLDER 를 사본으로 바꿈
|
||||
# 지금 다루는 층 폴더 — 층 라우터가 `at(층 폴더)` 로 바꿈 · 안 바꾸면 시스템 층(`FOLDER`).
|
||||
# 회사 · 개인 층도 시스템과 같은 모양(`table/구조물집계표.json` · 조합 장부 ·
|
||||
# structure/area/material — PLAN 21-1)이라 아래 함수는 그대로 씀
|
||||
# structure/area — PLAN 21-1)이라 아래 함수는 그대로 씀
|
||||
_ROOT: ContextVar[Path | None] = ContextVar("m02_layer_root", default=None)
|
||||
|
||||
|
||||
@@ -45,9 +45,8 @@ def at(folder: str | Path) -> Iterator[Path]:
|
||||
|
||||
|
||||
# 구조물집계표 구조물 열(꼴=수) 마다 세 파일(파일 이름 = 열 id) —
|
||||
# 구조물 도면 `structure/` (도각 없는 전용 화면) · 면적산출 `area/` · 재료산출 `material/`
|
||||
# (둘 다 스프레드시트 통합문서)
|
||||
# 번호 = 도번 SD · 면적번 SA · 재료번 SM + 뒷자리 — 한 열의 세 번호는 같은 뒷자리 · 한 번 준 번호는
|
||||
# 구조물 도면 `structure/` (도각 없는 전용 화면) · 면적산출 `area/`(스프레드시트 통합문서)
|
||||
# 번호 = 도번 SD · 면적번 SA + 뒷자리 — 한 열의 번호는 같은 뒷자리 · 한 번 준 번호는
|
||||
# 파일에 남아 안 바뀜(PLAN 13장). 뒷자리 두 모양:
|
||||
# 옛 8자리 `SD01020300` = [종류][분류1][분류2][분류3] 두 자리씩(그대로 유효 · 더는 새로 안 줌)
|
||||
# 새 일련 `SD-000001` = 붙임표 + 6자리 · 자리 뜻 없음(PLAN 26-1 · 26-4a) — 옛 8자리 ·
|
||||
@@ -57,7 +56,7 @@ def at(folder: str | Path) -> Iterator[Path]:
|
||||
STRUCTURE_TABLE = "구조물집계표"
|
||||
# 프로그램이 이름으로 부르는 양식 — 지우면 그 기능이 죽음(B07 이름은 `B07_DesignDetail_Engine_*`)
|
||||
IN_USE = {
|
||||
("table", STRUCTURE_TABLE): "구조물 도면 · 면적산출 · 재료산출 · 조합 장부가 씀",
|
||||
("table", STRUCTURE_TABLE): "구조물 도면 · 면적산출 · 조합 장부가 씀",
|
||||
("drawing", "00_template_A1"): "B07 도면 도각",
|
||||
("drawing", "00_template_compass"): "B07 방위표",
|
||||
("drawing", "00_template_cover"): "B07 표지",
|
||||
@@ -66,11 +65,10 @@ IN_USE = {
|
||||
PARTS = (
|
||||
("structure", "도번", "SD", None),
|
||||
("area", "면적번", "SA", "면적산출"),
|
||||
("material", "재료번", "SM", "재료산출"),
|
||||
)
|
||||
_KEY = {kind: key for kind, key, _, _ in PARTS}
|
||||
SHEETS = ("area", "material") # 통합문서 종류 — [저장] 때 서버가 계산값을 새로 적음
|
||||
_CODE = re.compile(r"^S[DAM](\d{8}|-\d{6})$")
|
||||
SHEETS = ("area",) # 통합문서 종류 — [저장] 때 서버가 계산값을 새로 적음
|
||||
_CODE = re.compile(r"^S[DA](\d{8}|-\d{6})$")
|
||||
_OLD_CODE = re.compile(r"^S[DCA](\d{6})$") # PLAN 12장 6자리 — 끝에 00 을 붙여 읽음
|
||||
_BAD_NAME = re.compile(r'[\\/:*?"<>|\x00-\x1f]|\.\.')
|
||||
# 도각 없는 빈 CAD 문서의 기본 도면층 — 없으면 B07 CAD 가 못 엶("Cannot read properties
|
||||
@@ -152,11 +150,11 @@ def _check_skeleton(kind: str, doc: Any) -> None:
|
||||
|
||||
def write(kind: str, name: str, version: str, doc: Any, screen: bool = False) -> dict[str, Any]:
|
||||
"""`version` 이 빈 글이면 새로 만듦(이미 있으면 409) · 아니면 그 판일 때만 덮어씀.
|
||||
구조물집계표를 저장하면 — 새 구조물 열의 빈 구조물 도면 · 면적산출 · 재료산출을 번호와 같이
|
||||
구조물집계표를 저장하면 — 새 구조물 열의 빈 구조물 도면 · 면적산출을 번호와 같이
|
||||
만들고 지운 열(또는 구조물이 아니게 된 열)의 세 파일을 지움(잠금 밖에서 · 안에서 부르면 죽음).
|
||||
통합문서(area · material)는 서버가 같은 TS 엔진을 Node 로 돌려 `계산값` 을 새로 적음
|
||||
통합문서(area)는 서버가 같은 TS 엔진을 Node 로 돌려 `계산값` 을 새로 적음
|
||||
(브라우저 값 버림 · 5장 ②) · 알약 `연결` 은 문서와 같이 그대로 적힘.
|
||||
번호 칸(도번 · 면적번 · 재료번)은 서버 것 — 있던 파일 번호를 그대로 둠(화면 값을 안 받음).
|
||||
번호 칸(도번 · 면적번)은 서버 것 — 있던 파일 번호를 그대로 둠(화면 값을 안 받음).
|
||||
표 열의 `본뜬열`(분류 더하기로 복사한 새 열)은 적지 않음 — 집계표면 아직 파일이 없는 새 열이
|
||||
원본 열의 일위대가 키와 세 파일 내용을 받음(새 번호 · 그 뒤로는 열마다 따로).
|
||||
`screen` = 화면 표 [저장](PLAN 26-7e) — 있던 항목의 경로 · 층 이름 · 빈 폴더는 저장본 그대로
|
||||
@@ -186,7 +184,7 @@ def write(kind: str, name: str, version: str, doc: Any, screen: bool = False) ->
|
||||
_copy_inputs(source, column)
|
||||
backfill_missing_structures() # 지우기 앞 — 같은 저장의 새 열이 지운 번호를 안 받게
|
||||
prune_removed_structures()
|
||||
sync_combos() # 번호가 다 선 뒤 — 조합 표 도번 · 면적번 · 재료번 칸
|
||||
sync_combos() # 번호가 다 선 뒤 — 조합 표 도번 · 면적번 칸
|
||||
return info
|
||||
|
||||
|
||||
@@ -202,7 +200,7 @@ def delete(kind: str, name: str, version: str | None = None) -> None:
|
||||
path.unlink()
|
||||
|
||||
|
||||
# ── 구조물 도면 · 면적산출 · 재료산출 ─────────────────────
|
||||
# ── 구조물 도면 · 면적산출 ─────────────────────
|
||||
|
||||
|
||||
def _docs(kind: str) -> list[tuple[str, Any]]:
|
||||
@@ -221,7 +219,7 @@ def _code(doc: Any, key: str) -> str | None:
|
||||
|
||||
|
||||
def _codes() -> dict[str, str]:
|
||||
"""`{열 id: 뒷자리}` — 구조물 도면 도번 먼저 · 없으면 면적번 · 재료번."""
|
||||
"""`{열 id: 뒷자리}` — 구조물 도면 도번 먼저 · 없으면 면적번."""
|
||||
codes: dict[str, str] = {}
|
||||
for kind, key, _, _ in reversed(PARTS):
|
||||
codes.update({name: c for name, doc in _docs(kind) if (c := _code(doc, key))})
|
||||
@@ -229,7 +227,7 @@ def _codes() -> dict[str, str]:
|
||||
|
||||
|
||||
def structure_numbers() -> dict[str, dict[str, str]]:
|
||||
"""`{열 id: {도번, 면적번, 재료번}}` — 집계표 줄 · 좌측 줄이 한 번에 받음(없는 쪽은 빈 글)."""
|
||||
"""`{열 id: {도번, 면적번}}` — 집계표 줄 · 좌측 줄이 한 번에 받음(없는 쪽은 빈 글)."""
|
||||
out: dict[str, dict[str, str]] = {}
|
||||
for kind, key, _, _ in PARTS:
|
||||
for name, doc in _docs(kind):
|
||||
@@ -281,7 +279,7 @@ def _retired() -> list[str]:
|
||||
|
||||
def _blank(kind: str, column: str) -> dict[str, Any]:
|
||||
"""빈 문서(번호는 부르는 쪽이 붙임) — 구조물 도면 = 도각 없음 · 기본 도면층 하나 /
|
||||
면적산출 · 재료산출 = 진짜 빈 시트 하나(식 없어 `계산값` 없음 · 첫 [저장] 때 서버가 적음)."""
|
||||
면적산출 = 진짜 빈 시트 하나(식 없어 `계산값` 없음 · 첫 [저장] 때 서버가 적음)."""
|
||||
if kind == "structure":
|
||||
return {
|
||||
"양식": "구조물도면",
|
||||
@@ -342,7 +340,7 @@ def _take_origins(doc: Any) -> dict[str, str]:
|
||||
|
||||
def create_structure(column: str) -> dict[str, Any]:
|
||||
"""구조물 열 하나의 구조물 도면 새로 — 도번 SD + 새 일련(`-` + 6자리) · 이미 있으면 409.
|
||||
면적산출 · 재료산출 파일이 없으면 빈 통합문서도 같이 만듦(같은 뒷자리)."""
|
||||
면적산출 파일이 없으면 빈 통합문서도 같이 만듦(같은 뒷자리)."""
|
||||
if column not in _column_ids():
|
||||
raise StoreError(404, f"{STRUCTURE_TABLE}에 없는 구조물 열 「{column}」")
|
||||
if not _ensure("structure", column):
|
||||
@@ -428,7 +426,7 @@ def _move_basis() -> list[str]:
|
||||
def renumber() -> dict[str, list[str]]:
|
||||
"""한 번 — 옛 산출근거를 면적산출로 옮김 · 옛 번호(6자리 · 구-NN · 표-NN · 없음)를 8자리로
|
||||
(6자리는 끝에 00 · 번호 없는 열은 열 차례로 일련번호) ·
|
||||
구조물 아닌 열 파일 지움 · 빠진 파일(재료산출 등) 만듦. 내용은 그대로 · 두 번 돌려도 같음."""
|
||||
구조물 아닌 열 파일 지움 · 빠진 파일(면적산출 등) 만듦. 내용은 그대로 · 두 번 돌려도 같음."""
|
||||
moved = _move_basis()
|
||||
gone = prune_removed_structures()
|
||||
changed: list[str] = []
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Structure.ts
|
||||
* 구조물집계표 아래 설계 컨테이너 — 고른 열의 도면 정보 · 면적산출 · 재료산출 카드(보기만 + [수정]).
|
||||
* 구조물집계표 아래 설계 컨테이너 — 고른 열의 도면 정보 · 면적산출 카드(보기만 + [수정]).
|
||||
* 카드 셋은 좌측 패널과 같은 공용 ui-sidebar 틀 — 고침은 [수정] 이 여는 M02 메인 칸 전체 편집 화면
|
||||
* (`M02_MasterTemplete_UI_Main.ts`)이 맡음.
|
||||
* 면적산출 · 재료산출 = 스프레드시트(`A00_Common/spreadsheet/`) · 열마다 따로 파일(`area|material/<열 id>.json`) ·
|
||||
* 면적산출 = 스프레드시트(`A00_Common/spreadsheet/`) · 열마다 따로 파일(`area/<열 id>.json`) ·
|
||||
* 부품은 카드를 열 때만 `import()` 로 받음(다른 페이지 번들에 안 섞임).
|
||||
*
|
||||
* 구조물 도면은 집계표 열마다 하나(`resources/master_template/structure/<열 id>.json`) ·
|
||||
* 서버가 열마다 미리 만들어 둠(번호 자동 · 새 열도 집계표 저장 때 서버가 같이 만듦 — `M02_MasterTemplete_Store.py`).
|
||||
* 이 칸은 표 부품 host 밖 형제 자리에 붙음 — 집계표는 그대로 두고 여기만 열마다 다시 그림.
|
||||
* 집계표 일위대가 · 도면 · 면적산출 · 재료산출 줄의 조회 후 선택(`pick`)도 여기 — 일위대가 = M01 일위대가
|
||||
* 집계표 일위대가 · 도면 · 면적산출 줄의 조회 후 선택(`pick`)도 여기 — 일위대가 = M01 일위대가
|
||||
* 목록에서 고름 · 나머지 = 다른 열 것을 골라 내용만 복사해 가져옴(번호 · 열은 그대로 · 서버가 복사).
|
||||
* 글자는 이 파일에 둠(도면 부품 `M02_MasterTemplete_Drawing.ts` 와 같은 방식).
|
||||
* ========================================================================== */
|
||||
@@ -23,11 +23,9 @@ import {
|
||||
} from "@ui/ui_template_elements";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links";
|
||||
import { searchMaterials } from "../M01_MasterData/M01_MasterData_Api_Fetch";
|
||||
import { fetchComboInputs, fetchUsableCombos } from "./M02_MasterTemplete_Labor_Api";
|
||||
import {
|
||||
copyArea,
|
||||
copyMaterial,
|
||||
copyStructure,
|
||||
fetchStructureNumbers,
|
||||
getActiveLayer,
|
||||
@@ -69,26 +67,24 @@ const TEXT = {
|
||||
noPrice: "이 열에 일위대가가 없어 알약 없음 — 구조물 조합 일위대가 줄에서 고름",
|
||||
noInputs: "일위대가 {value} 에 입력값이 없어 알약 없음",
|
||||
inputsFailed: "일위대가 입력값을 못 받음 — {value}",
|
||||
discardCards: "카드(도면 · 면적산출 · 재료산출)에 저장 안 한 고침이 있음 — 버리고 넘어갈까?",
|
||||
discardCards: "카드(도면 · 면적산출)에 저장 안 한 고침이 있음 — 버리고 넘어갈까?",
|
||||
};
|
||||
|
||||
/** 집계표 열 하나에 딸린 문서 셋 — 번호 칸 · 이름 · 복사 */
|
||||
const COPY: Record<EditKind, (to: string, from: string) => Promise<void>> = {
|
||||
structure: copyStructure,
|
||||
area: copyArea,
|
||||
material: copyMaterial,
|
||||
};
|
||||
/** 집계표 줄 열쇠 → 문서 */
|
||||
const ROW_KIND: Record<string, EditKind> = {
|
||||
"s:drawing": "structure",
|
||||
"s:area": "area",
|
||||
"s:material": "material",
|
||||
};
|
||||
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : "");
|
||||
const fill = (text: string, value: string): string => text.replace("{value}", value);
|
||||
|
||||
/** `{열 id: {도번, 면적번, 재료번}}` — 집계표 번호 줄(없는 열은 키 없음 = 「없음」) */
|
||||
/** `{열 id: {도번, 면적번}}` — 집계표 번호 줄(없는 열은 키 없음 = 「없음」) */
|
||||
export type ColumnNumbers = Record<string, StructureNumbers>;
|
||||
|
||||
/** 일위대가 목록 — 이 층이 쓸 수 있는 세 층 조합(27-2c) · 고를 때마다 받음(층을 바꿔 다시 들어와도 맞게) */
|
||||
@@ -206,7 +202,7 @@ export interface StructureColumn {
|
||||
꼴?: string | null;
|
||||
}
|
||||
|
||||
/** 구조물 열 = 꼴=수 — NO · 측점 · 비고 같은 글 열은 도면 · 면적산출 · 재료산출 · 번호가 없음(서버와 같은 규칙) */
|
||||
/** 구조물 열 = 꼴=수 — NO · 측점 · 비고 같은 글 열은 도면 · 면적산출 · 번호가 없음(서버와 같은 규칙) */
|
||||
export const isStructureColumn = (col: StructureColumn): boolean => col.꼴 === "수";
|
||||
|
||||
/** 관리 화면 「구조물 조합」에서 숨기는 것(PLAN 26-7b · 30-7) — NO · 측점 · 비고 열 · 전구간 줄(본문 줄) ·
|
||||
@@ -274,16 +270,12 @@ export const columnHead = (col: StructureColumn): string =>
|
||||
export const structureLabel = (number: string, col: StructureColumn): string =>
|
||||
[number, columnHead(col)].filter(Boolean).join(" ");
|
||||
|
||||
/** 면적산출 · 재료산출 편집 화면 위 알약 막대 — 면적 = 그 열 일위대가 입력값(물량) · 재료 = 자재DB 찾기.
|
||||
/** 면적산출 편집 화면 위 알약 막대 — 그 열 일위대가 입력값(물량).
|
||||
* 알약이 없으면 까닭 글(`notice`)을 같이 돌려줌 */
|
||||
export async function pillsFor(
|
||||
kind: SheetKind,
|
||||
_kind: SheetKind, // 종류는 면적산출 하나 — 부르는 쪽 모양 그대로
|
||||
column: string,
|
||||
): Promise<{ links: LinkOptions; notice?: string }> {
|
||||
if (kind === "material") {
|
||||
const own = await inputPills(column, "재료");
|
||||
return { links: { 항목: own, 모드: "자재", 자재찾기: searchMaterials } };
|
||||
}
|
||||
const own = await inputPills(column, "면적"); // 선택 입력 카드가 면적산출로 고른 입력
|
||||
const none = (notice: string) => ({ links: { 항목: own, 모드: "입력" as const }, notice });
|
||||
const table = await readTemplate(getActiveLayer(), "table", STRUCTURE_TABLE, null).catch(
|
||||
@@ -312,7 +304,7 @@ async function cardLinks(
|
||||
export interface StructurePanelOptions {
|
||||
/** 시스템 관리자만 저장함(구조물 도면은 서버가 미리 만들어 둠) */
|
||||
isAdmin: boolean;
|
||||
/** [도면 수정] · [면적산출 수정] · [재료산출 수정] — 그 열을 CAD · 스프레드시트 전체 편집으로 엶
|
||||
/** [도면 수정] · [면적산출 수정] — 그 열을 CAD · 스프레드시트 전체 편집으로 엶
|
||||
* (페이지의 `open`) · label 은 화면 제목에 씀 */
|
||||
onEdit: (column: string, label: string, kind: EditKind) => void;
|
||||
/** 집계표에 저장 안 한 고침이 있으면 버릴지 물음 — 취소면 false */
|
||||
@@ -325,13 +317,13 @@ export interface StructurePanelOptions {
|
||||
|
||||
export interface StructurePanelHandle {
|
||||
root: HTMLElement;
|
||||
/** `{열 id: {도번, 면적번, 재료번}}` — 집계표 `drawingLabel` · `areaLabel` · `materialLabel` 이 씀 */
|
||||
/** `{열 id: {도번, 면적번}}` — 집계표 `drawingLabel` · `areaLabel` 이 씀 */
|
||||
numbers: () => ColumnNumbers;
|
||||
/** 번호 목록 다시 받기 */
|
||||
reload: () => Promise<void>;
|
||||
/** 「선택 입력」 카드에서 고른 자리를 서버에 저장 — 집계표 [저장] 뒤에 부름(실패하면 던짐) */
|
||||
saveInputs: () => Promise<void>;
|
||||
/** 카드에 저장 안 한 고침(도면 31-4 · 면적산출 · 재료산출 31-3) — 페이지가 떠날 때 함께 물음 */
|
||||
/** 카드에 저장 안 한 고침(도면 31-4 · 면적산출 31-3) — 페이지가 떠날 때 함께 물음 */
|
||||
dirty: () => boolean;
|
||||
/** 고친 카드 저장(도면 · 엑셀) — 집계표 [저장] 이 카드를 다시 읽기 전에 부름(실패하면 던짐) */
|
||||
saveCards: () => Promise<void>;
|
||||
@@ -340,7 +332,7 @@ export interface StructurePanelHandle {
|
||||
/** 보던 열을 다시 읽음 — 집계표 [저장] 뒤(서버가 새 열 파일 · 번호를 만듦) */
|
||||
refresh: (columns: StructureColumn[]) => void;
|
||||
/** 집계표 조회 후 선택(`onPick`) — 일위대가 = 고른 키(「비움」 = 빈 글 · 닫으면 null) ·
|
||||
* 도면 · 면적산출 · 재료산출 = 다른 열 것을 복사해 가져옴(돌려주는 글 없음) */
|
||||
* 도면 · 면적산출 = 다른 열 것을 복사해 가져옴(돌려주는 글 없음) */
|
||||
pick: (row: string, colId: string | null, columns: StructureColumn[]) => Promise<string | null>;
|
||||
destroy: () => void;
|
||||
}
|
||||
@@ -364,7 +356,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
|
||||
// 도번 목록에 있는 열인데 못 읽으면 알림 — 없는 열은 그냥 「도면 없음」
|
||||
warnOnFail: (id) => !!numbers[id],
|
||||
extraCards: [inputs.root],
|
||||
// 면적산출 · 재료산출 카드 미리보기 수정(31-3) — 전체 편집기와 같은 알약 · 저장 길
|
||||
// 면적산출 카드 미리보기 수정(31-3) — 전체 편집기와 같은 알약 · 저장 길
|
||||
sheetEdit: {
|
||||
can: () => opts.isAdmin,
|
||||
links: (kind, ctl) => cardLinks(kind, ctl),
|
||||
@@ -407,7 +399,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
|
||||
return got ? got.key : null;
|
||||
};
|
||||
|
||||
/** 다른 열의 도면 · 면적산출 · 재료산출을 골라 내용만 복사 — 덮어쓰기 확인 · 보고 있던 열이면 다시 읽음 */
|
||||
/** 다른 열의 도면 · 면적산출을 골라 내용만 복사 — 덮어쓰기 확인 · 보고 있던 열이면 다시 읽음 */
|
||||
const pickCopy = async (
|
||||
kind: EditKind,
|
||||
colId: string,
|
||||
|
||||
@@ -57,7 +57,6 @@ type Mods = Record<string, () => Promise<unknown>>;
|
||||
const NUMBER_FIELD: Partial<Record<Kind, string>> = {
|
||||
structure: "도번",
|
||||
area: "면적번",
|
||||
material: "재료번",
|
||||
};
|
||||
// 표 부품(sub3) · 도면 부품(sub2) — 내보낸 이름으로 찾음
|
||||
// 표는 진입 파일만 — 폴더 통째(`*.ts`)면 분수 모듈이 따로 늦게 받는 입구가 되어 번들러 도우미를
|
||||
@@ -130,7 +129,7 @@ export function createMain(
|
||||
const title = el("h2", { className: "m02-main__title", text: L("M02_PickTemplate") });
|
||||
const reload = createButton({ label: L("M02_Reload"), variant: "ghost" });
|
||||
const save = createButton({ label: L("M02_Save"), variant: "filled" });
|
||||
// 구조물 도면 · 면적산출 · 재료산출 상세 화면 → 구조물집계표(그 열 고른 채)
|
||||
// 구조물 도면 · 면적산출 상세 화면 → 구조물집계표(그 열 고른 채)
|
||||
const back = createButton({ label: L("M02_Back"), variant: "ghost", size: "sm" });
|
||||
const slot = el("div", { className: "m02-main__slot" }); // 도면 양식의 작도 영역 칸이 들어옴
|
||||
const notice = el("div", { className: "m02-main__notice", attrs: { hidden: "" } });
|
||||
@@ -161,7 +160,7 @@ export function createMain(
|
||||
title.classList.toggle("m02-main__title--fold", combo);
|
||||
title.title = combo ? L("M02_TableFold") : "";
|
||||
if (!combo) {
|
||||
host.hidden = false; // 다른 상세(도면 · 면적 · 재료)는 늘 펼침
|
||||
host.hidden = false; // 다른 상세(도면 · 면적)는 늘 펼침
|
||||
title.classList.remove("is-folded");
|
||||
}
|
||||
};
|
||||
@@ -182,7 +181,7 @@ export function createMain(
|
||||
let cpanel: ComboPanel | null = null; // 면적산출 오른쪽 조합 패널(프로젝트 층 아님)
|
||||
let seq = 0;
|
||||
let dirty = false;
|
||||
/** 저장 안 한 고침 — 이 화면 + 카드(도면 31-4 · 면적산출 · 재료산출 31-3) */
|
||||
/** 저장 안 한 고침 — 이 화면 + 카드(도면 31-4 · 면적산출 31-3) */
|
||||
const busy = (): boolean => dirty || !!panel?.dirty();
|
||||
|
||||
const guard = (event: BeforeUnloadEvent): void => {
|
||||
@@ -232,7 +231,7 @@ export function createMain(
|
||||
slot.replaceChildren();
|
||||
};
|
||||
|
||||
/** 구조물집계표 아래 설계 컨테이너 — 고른 열의 도면 정보 · 면적산출 · 재료산출 카드 */
|
||||
/** 구조물집계표 아래 설계 컨테이너 — 고른 열의 도면 정보 · 면적산출 카드 */
|
||||
function buildPanel(layer: Layer): StructurePanelHandle {
|
||||
const made = createStructurePanel({
|
||||
isAdmin,
|
||||
@@ -248,11 +247,11 @@ export function createMain(
|
||||
return made;
|
||||
}
|
||||
|
||||
/** 면적산출 · 재료산출 스프레드시트 — 위 알약 막대(sub7 `links`) · 면적 = 그 열 일위대가 입력값 ·
|
||||
* 재료 = 자재DB 찾기(`pillsFor`) · 시스템 층은 면적 = 조합 줄 + [조합 고치기] */
|
||||
/** 면적산출 스프레드시트 — 위 알약 막대(sub7 `links`) = 그 열 일위대가 입력값(`pillsFor`) ·
|
||||
* 시스템 층은 조합 줄 + [조합 고치기] */
|
||||
async function mountSheet(
|
||||
doc: unknown,
|
||||
kind: "area" | "material",
|
||||
kind: "area",
|
||||
at: Selection,
|
||||
readOnly: boolean,
|
||||
): Promise<void> {
|
||||
@@ -279,7 +278,7 @@ export function createMain(
|
||||
);
|
||||
editor = handle;
|
||||
sheetHandle = handle;
|
||||
if (at.layer === "project" || kind !== "area") return;
|
||||
if (at.layer === "project") return;
|
||||
// 오른쪽 조합 패널 — 한 번 만들고 시트를 다시 붙여도 그대로(늘 시트 뒤)
|
||||
cpanel ??= buildComboPanel({
|
||||
kind,
|
||||
@@ -305,11 +304,7 @@ export function createMain(
|
||||
}
|
||||
|
||||
/** 시트만 다시 붙임(알약을 새로 받음) — 저장 안 한 시트 고침은 그대로 옮김 */
|
||||
async function remount(
|
||||
kind: "area" | "material",
|
||||
at: Selection,
|
||||
readOnly: boolean,
|
||||
): Promise<void> {
|
||||
async function remount(kind: "area", at: Selection, readOnly: boolean): Promise<void> {
|
||||
const kept = sheetHandle?.getDoc();
|
||||
if (sel !== at || !kept) return;
|
||||
editor?.destroy();
|
||||
@@ -326,8 +321,8 @@ export function createMain(
|
||||
"createSheet",
|
||||
);
|
||||
if (!create) return empty(L("M02_NoSheet"));
|
||||
// 구조물집계표(프로젝트 층 아님)만 — 도면 · 면적산출 · 재료산출 줄에 번호 · 칸을 고르면 아래 컨테이너 ·
|
||||
// 고칠 수 있으면 일위대가 · 도면 · 면적산출 · 재료산출 칸은 조회 후 선택
|
||||
// 구조물집계표(프로젝트 층 아님)만 — 도면 · 면적산출 줄에 번호 · 칸을 고르면 아래 컨테이너 ·
|
||||
// 고칠 수 있으면 일위대가 · 도면 · 면적산출 칸은 조회 후 선택
|
||||
const struct =
|
||||
at.layer !== "project" && at.name === STRUCTURE_TABLE ? buildPanel(at.layer) : null;
|
||||
if (struct) await struct.reload();
|
||||
@@ -347,7 +342,6 @@ export function createMain(
|
||||
? {
|
||||
drawingLabel: (colId: string) => struct.numbers()[colId]?.도번 ?? "",
|
||||
areaLabel: (colId: string) => struct.numbers()[colId]?.면적번 ?? "",
|
||||
materialLabel: (colId: string) => struct.numbers()[colId]?.재료번 ?? "",
|
||||
combo: {
|
||||
...(readOnly || !extras.onHeadRename
|
||||
? {}
|
||||
@@ -378,8 +372,8 @@ export function createMain(
|
||||
editor = { getDoc: () => comboMerge(loaded, shown.getDoc()), destroy: shown.destroy };
|
||||
return;
|
||||
}
|
||||
if (at.kind === "area" || at.kind === "material") {
|
||||
// 면적산출 · 재료산출 = 구조물집계표 열마다 한 스프레드시트 — 페이지 번들에 안 섞이게 이 화면을
|
||||
if (at.kind === "area") {
|
||||
// 면적산출 = 구조물집계표 열마다 한 스프레드시트 — 페이지 번들에 안 섞이게 이 화면을
|
||||
// 열 때만 `import()` 로 받음(계약 `spreadsheet_types.ts` 머리)
|
||||
return mountSheet(doc, at.kind, at, readOnly);
|
||||
}
|
||||
@@ -435,7 +429,7 @@ export function createMain(
|
||||
)
|
||||
: got.문서;
|
||||
if (mine !== seq) return;
|
||||
// 구조물 도면 · 면적산출 · 재료산출은 열 id 로 열리니 머리 맨 앞에 번호(「SD01010100 돌기슭막이 …」)
|
||||
// 구조물 도면 · 면적산출은 열 id 로 열리니 머리 맨 앞에 번호(「SD01010100 돌기슭막이 …」)
|
||||
const field = NUMBER_FIELD[next.kind];
|
||||
const number = field && (got.문서 as Record<string, unknown> | null)?.[field];
|
||||
if (number) title.textContent = `${String(number)} ${next.label ?? next.name}`;
|
||||
@@ -457,11 +451,11 @@ export function createMain(
|
||||
try {
|
||||
const edited = editor ? await editor.getDoc() : loaded;
|
||||
// 구조물 도면 저장은 `도면` 칸만 갈아 끼움 — 도번은 읽은 그대로.
|
||||
// 면적산출 · 재료산출은 스프레드시트 문서로 통째 갈아 끼우되 번호는 계약 밖(엔진이 모름)이라 옮겨 붙임.
|
||||
// 면적산출은 스프레드시트 문서로 통째 갈아 끼우되 번호는 계약 밖(엔진이 모름)이라 옮겨 붙임.
|
||||
const doc =
|
||||
at.kind === "structure" && loaded
|
||||
? { ...(loaded as StructureDoc), 도면: edited as StructureDoc["도면"] }
|
||||
: (at.kind === "area" || at.kind === "material") && loaded
|
||||
: at.kind === "area" && loaded
|
||||
? { ...(loaded as Record<string, unknown>), ...(edited as Record<string, unknown>) }
|
||||
: edited;
|
||||
const info = await saveTemplate(at.layer, at.kind, at.name, at.projectId, version, doc);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_UI_Side.ts
|
||||
* 마스터 템플릿 좌측 패널 — 「구조물 조합」(구조물집계표 줄 하나 + 하위 구조물 도면 · 면적산출 ·
|
||||
* 재료산출) · 「도면 양식」(새로 · 본떠 · 지우기). 서버 길은 계약 `6_계약.md`.
|
||||
* 구조물 도면 · 면적산출 · 재료산출 줄은 집계표 구조물 열(꼴=수)마다 하나씩 —
|
||||
* 서버가 미리 만들어 둠(도번 SD · 면적번 SA · 재료번 SM + 같은 8자리) ·
|
||||
* 마스터 템플릿 좌측 패널 — 「구조물 조합」(구조물집계표 줄 하나 + 하위 구조물 도면 · 면적산출) ·
|
||||
* 「도면 양식」(새로 · 본떠 · 지우기). 서버 길은 계약 `6_계약.md`.
|
||||
* 구조물 도면 · 면적산출 줄은 집계표 구조물 열(꼴=수)마다 하나씩 —
|
||||
* 서버가 미리 만들어 둠(도번 SD · 면적번 SA + 같은 8자리) ·
|
||||
* 줄 글은 번호가 맨 앞(「SD01010100 돌기슭막이 …」).
|
||||
* 화면 이름 「구조물 조합」(PLAN 26-7a) — 파일 이름 · 양식 키 · API 길은 「구조물집계표」 그대로.
|
||||
* 구조물 라이브러리는 좌측이 아니라 「구조물 조합」 상세 화면 왼쪽(PLAN 26-7c · 페이지가 붙임).
|
||||
@@ -47,11 +47,10 @@ const KINDS: { kind: Kind; label: () => string }[] = [
|
||||
{ kind: "drawing", label: () => L("M02_KindDrawing") },
|
||||
];
|
||||
/** 구조물 열마다 한 줄씩인 목록 셋 — 종류 · 번호 칸 · 제목 */
|
||||
type ColumnKind = "structure" | "area" | "material";
|
||||
type ColumnKind = "structure" | "area";
|
||||
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") },
|
||||
];
|
||||
/** 새 도면 양식의 첫 문서 — 빈 웹캐드 문서 */
|
||||
const STUB_DRAWING = {
|
||||
@@ -215,7 +214,7 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/** 구조물 도면 · 면적산출 · 재료산출 줄 — 구조물 열마다 하나 · 「번호 이름 변수값」(번호가 없으면 「없음」) */
|
||||
/** 구조물 도면 · 면적산출 줄 — 구조물 열마다 하나 · 「번호 이름 변수값」(번호가 없으면 「없음」) */
|
||||
function paintRows(body: HTMLElement, kind: ColumnKind, key: keyof StructureNumbers): void {
|
||||
body.replaceChildren(
|
||||
...(columns.length
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
POST …/items {structure, layer, owner, key} 미리 가져오기 → {폴더, 만듦, 가져온날}
|
||||
POST …/items/new {structure, 경로, 이름, 단위} 새 항목(개인 층에도) · 회사 없음 409
|
||||
GET …/items/{폴더}/{종류} {종류, 판, 내용}
|
||||
PUT …/items/{폴더}/{종류} {판, 내용} — 판이 다르면 409 · 면적 · 재료는 서버 계산값
|
||||
PUT …/items/{폴더}/{종류} {판, 내용} — 판이 다르면 409 · 면적은 서버 계산값
|
||||
POST …/items/{폴더}/reapply 재적용(입력값 · 고른자재는 같은 키면 살림 · `덮임` 알림)
|
||||
POST …/items/{폴더}/save-as {to, 경로, 이름} 개인 층에 새 항목으로(단가 묶음은 안 가져감)
|
||||
POST …/items/{폴더}/calc 복사본만으로 계산(세 비목 · 계 · 빠진 입력)
|
||||
종류 = 도면 · 면적 · 재료 · 입력연결 · 입력값 · 고른자재 · `refresh` = 옛 이름(재적용).
|
||||
종류 = 도면 · 면적 · 입력연결 · 입력값 · 고른자재 · `refresh` = 옛 이름(재적용).
|
||||
⚠ 등록은 `main.py`(로그인만) — 프로젝트 권한(같은 회사)은 여기서 `_project` 로 봄(403 · 404).
|
||||
"""
|
||||
|
||||
@@ -31,7 +31,7 @@ from M02_MasterTemplete.M02_Item_Source import item_name
|
||||
|
||||
router = APIRouter(prefix="/api/projects", tags=["M02 Project Items"])
|
||||
|
||||
Kind = Literal["도면", "면적", "재료", "입력연결", "입력값", "고른자재"]
|
||||
Kind = Literal["도면", "면적", "입력연결", "입력값", "고른자재"]
|
||||
|
||||
|
||||
class WriteBody(BaseModel):
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""M02 양식 층 — 네 층의 자리 · 읽기 · 쓰기 · 복사 · manifest.
|
||||
|
||||
층 넷 (PLAN 10-5):
|
||||
system `resources/master_template/{table,drawing,structure,area,material}/<이름>.json` (git)
|
||||
company `storage/{회사}/templates/{table,drawing,structure,area,material}/`
|
||||
personal `storage/{회사}/{사용자}/templates/{table,drawing,structure,area,material}/`
|
||||
system `resources/master_template/{table,drawing,structure,area}/<이름>.json` (git)
|
||||
company `storage/{회사}/templates/{table,drawing,structure,area}/`
|
||||
personal `storage/{회사}/{사용자}/templates/{table,drawing,structure,area}/`
|
||||
project `storage/{회사}/{사용자}/{프로젝트}/templates/{…같은 다섯}/`
|
||||
+ 초기 사본 `templates/_initial/`
|
||||
|
||||
@@ -26,8 +26,8 @@ from common_util.common_util_json import atomic_write_json
|
||||
from config import config_system
|
||||
|
||||
LAYERS = ("system", "company", "personal", "project")
|
||||
# area = 면적산출 · material = 재료산출 — 통합문서(구조물집계표 열마다 · `A00_Common/spreadsheet/`)
|
||||
KINDS = ("table", "drawing", "structure", "area", "material")
|
||||
# area = 면적산출 — 통합문서(구조물집계표 열마다 · `A00_Common/spreadsheet/`)
|
||||
KINDS = ("table", "drawing", "structure", "area")
|
||||
TEMPLATES_DIRNAME = "templates"
|
||||
INITIAL_DIRNAME = "_initial"
|
||||
MANIFEST_NAME = "manifest.json"
|
||||
@@ -169,7 +169,6 @@ _SKELETON = {
|
||||
"drawing": ("entities",),
|
||||
"structure": ("도면.entities",),
|
||||
"area": ("시트",),
|
||||
"material": ("시트",),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"종류": "통합문서",
|
||||
"판": 1,
|
||||
"열": "item1",
|
||||
"서식": [
|
||||
{}
|
||||
],
|
||||
"시트": [
|
||||
{
|
||||
"id": "s1",
|
||||
"이름": "재료산출",
|
||||
"칸": {}
|
||||
}
|
||||
],
|
||||
"활성": "s1",
|
||||
"재료번": "SM-000001"
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"종류": "통합문서",
|
||||
"판": 1,
|
||||
"열": "item2",
|
||||
"서식": [
|
||||
{}
|
||||
],
|
||||
"시트": [
|
||||
{
|
||||
"id": "s1",
|
||||
"이름": "재료산출",
|
||||
"칸": {}
|
||||
}
|
||||
],
|
||||
"활성": "s1",
|
||||
"재료번": "SM-000002"
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"종류": "통합문서",
|
||||
"판": 1,
|
||||
"열": "item3",
|
||||
"서식": [
|
||||
{}
|
||||
],
|
||||
"시트": [
|
||||
{
|
||||
"id": "s1",
|
||||
"이름": "재료산출",
|
||||
"칸": {}
|
||||
}
|
||||
],
|
||||
"활성": "s1",
|
||||
"재료번": "SM-000003"
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"종류": "통합문서",
|
||||
"판": 1,
|
||||
"열": "c4",
|
||||
"서식": [
|
||||
{}
|
||||
],
|
||||
"시트": [
|
||||
{
|
||||
"id": "s1",
|
||||
"이름": "재료산출",
|
||||
"칸": {
|
||||
"I5": {
|
||||
"값": 6
|
||||
}
|
||||
},
|
||||
"연결": [
|
||||
{
|
||||
"키": "MT003810",
|
||||
"이름": "모 래, 서울, 도착도 강모래",
|
||||
"칸": "I5",
|
||||
"단위": "㎥",
|
||||
"자재": {
|
||||
"코드": "MT003810",
|
||||
"이름": "모 래, 서울, 도착도",
|
||||
"규격": "강모래",
|
||||
"단위": "㎥"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"활성": "s1",
|
||||
"재료번": "SM01010101",
|
||||
"계산값": {}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"종류": "통합문서",
|
||||
"판": 1,
|
||||
"열": "ms",
|
||||
"서식": [
|
||||
{}
|
||||
],
|
||||
"시트": [
|
||||
{
|
||||
"id": "s1",
|
||||
"이름": "재료산출",
|
||||
"칸": {}
|
||||
}
|
||||
],
|
||||
"활성": "s1",
|
||||
"재료번": "SM10010100"
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"종류": "통합문서",
|
||||
"판": 1,
|
||||
"열": "pp_len",
|
||||
"서식": [
|
||||
{}
|
||||
],
|
||||
"시트": [
|
||||
{
|
||||
"id": "s1",
|
||||
"이름": "재료산출",
|
||||
"칸": {}
|
||||
}
|
||||
],
|
||||
"활성": "s1",
|
||||
"재료번": "SM03010100"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
"""시험 안 마스터 견본(PLAN 26-5a) — 초기화 전 마스터 층을 줄여 얼린 것
|
||||
(`fixtures/m02_master_sample`).
|
||||
|
||||
집계표(바인딩 열 54) · SS 장부(51 줄) · 자재조합 · 세 파일은 rv · c4 · pp_len · ms 넷(도면 줄임) —
|
||||
집계표(바인딩 열 54) · SS 장부(51 줄) · 도면 · 면적 파일은 rv · c4 · pp_len · ms 넷(도면 줄임) —
|
||||
시험은 실제 `resources/master_template` 을 안 읽고 이 견본을 임시 폴더에 복사해 씀.
|
||||
"""
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""저장 뒤 식 · 값 유지 — M02 면적산출 · 재료산출 저장본을 서버 엔진으로 다시 풀면 저장된 `계산값` 과 같음.
|
||||
"""저장 뒤 식 · 값 유지 — M02 면적산출 저장본을 서버 엔진으로 다시 풀면 저장된 `계산값` 과 같음.
|
||||
|
||||
시험 안 견본 저장본(`fixtures/m02_master_sample` · PLAN 26-5a)을 읽기만 함.
|
||||
저장 때 서버가 적은 값과 지금 엔진 풀이가 어긋나면 화면을 다시 열 때
|
||||
@@ -18,7 +18,7 @@ pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가
|
||||
|
||||
def _books() -> list[tuple[str, dict]]:
|
||||
out = []
|
||||
for kind in ("area", "material"):
|
||||
for kind in ("area",):
|
||||
for path in sorted((FOLDER / kind).glob("[!.]*.json")):
|
||||
book = json.loads(path.read_text(encoding="utf-8"))
|
||||
if isinstance(book, dict) and book.get("시트"):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""F — 면적산출(area) · 재료산출(material) 저장 · 옛 산출근거(basis) 옮김 · [저장] 때 서버 계산값 ·
|
||||
"""F — 면적산출(area) 저장 · 옛 산출근거(basis) 옮김 · [저장] 때 서버 계산값 ·
|
||||
알약 연결(`연결`)은 저장 · 복사 때 문서와 같이.
|
||||
|
||||
서버 Node 풀이는 `test_spreadsheet_node.py` 가 따로 봄 — 여기는 `with_server_values` 를 바꿔 끼워
|
||||
@@ -42,7 +42,7 @@ def _write(folder, kind, column, doc):
|
||||
|
||||
|
||||
def test_옛_산출근거를_면적산출로_옮기고_번호_8자리(client, tmp_path):
|
||||
"""12장 파일(SD · SC 6자리 · basis/) → SA 로 옮김 · 끝에 00 · 재료산출 새로 · 내용 그대로."""
|
||||
"""12장 파일(SD · SC 6자리 · basis/) → SA 로 옮김 · 끝에 00 · 재료산출은 안 만듦 · 내용 그대로."""
|
||||
structure = {"양식": "구조물도면", "종류": "structure", "판": 1, "열": "rv", "도번": "SD010101"}
|
||||
_write(tmp_path, "structure", "rv", {**structure, "도면": {"entities": [{"type": "line"}]}})
|
||||
old = {**store._blank("area", "rv"), "표번": "SC010101", "연결": LINKS}
|
||||
@@ -56,30 +56,26 @@ def test_옛_산출근거를_면적산출로_옮기고_번호_8자리(client, tm
|
||||
"A2": {"값": "터파기"}
|
||||
}
|
||||
assert store.read("structure", "rv")["문서"]["도번"] == "SD01010100"
|
||||
material = store.read("material", "rv")["문서"]
|
||||
assert (material["재료번"], material["시트"][0]["이름"]) == ("SM01010100", "재료산출")
|
||||
assert material["시트"][0]["칸"] == {} and material["서식"] == [{}] # 진짜 빈 시트
|
||||
assert not (tmp_path / "material").exists() # 재료산출은 없앰
|
||||
before = {p: p.read_bytes() for p in tmp_path.rglob("*.json")}
|
||||
assert store.renumber() == {"옮김": [], "지움": [], "바꿈": [], "새로": []}
|
||||
assert before == {p: p.read_bytes() for p in tmp_path.rglob("*.json")} # 두 번 돌려도 같음
|
||||
|
||||
|
||||
def test_새_구조물_도면은_면적_재료_진짜_빈_시트_같은_8자리(client):
|
||||
def test_새_구조물_도면은_면적_진짜_빈_시트_같은_8자리(client):
|
||||
made1 = client.post("/api/m02/structures/rv").json()["문서"]
|
||||
made2 = client.post("/api/m02/structures/pv_b").json()["문서"]
|
||||
# 새 항목 번호 = 자리 뜻 없는 일련번호(PLAN 26-1)
|
||||
assert made1["도번"] == "SD-000001" and made2["도번"] == "SD-000002"
|
||||
for kind, key, head in (("area", "면적번", "SA"), ("material", "재료번", "SM")):
|
||||
for kind, key, head in (("area", "면적번", "SA"),):
|
||||
doc = client.get(f"/api/m02/templates/{kind}/pv_b").json()["문서"]
|
||||
assert doc[key] == f"{head}-000002"
|
||||
assert doc["시트"][0]["칸"] == {} and doc["서식"] == [{}] # 머리 줄 없음
|
||||
numbers = client.get("/api/m02/structures/numbers").json()
|
||||
assert numbers["pv_b"] == {"도번": "SD-000002", "면적번": "SA-000002", "재료번": "SM-000002"}
|
||||
assert numbers["pv_b"] == {"도번": "SD-000002", "면적번": "SA-000002"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("kind", "key", "head"), [("area", "면적번", "SA"), ("material", "재료번", "SM")]
|
||||
)
|
||||
@pytest.mark.parametrize(("kind", "key", "head"), [("area", "면적번", "SA")])
|
||||
def test_다른_열_내용을_복사해_가져옴_연결도_같이(client, kind, key, head):
|
||||
store.renumber()
|
||||
got = client.get(f"/api/m02/templates/{kind}/rv").json()
|
||||
@@ -117,21 +113,21 @@ def test_저장은_서버_계산값만_적음_연결_두고_번호는_서버_것
|
||||
|
||||
|
||||
def test_못_풀면_저장을_막음(client, monkeypatch):
|
||||
store._ensure("material", "rv")
|
||||
got = client.get("/api/m02/templates/material/rv").json()
|
||||
store._ensure("area", "rv")
|
||||
got = client.get("/api/m02/templates/area/rv").json()
|
||||
|
||||
def broken(_book):
|
||||
raise spreadsheet.RecalcError("스프레드시트 계산 실패 — 시험")
|
||||
|
||||
monkeypatch.setattr(spreadsheet, "with_server_values", broken)
|
||||
r = client.put("/api/m02/templates/material/rv", json={"판": got["판"], "문서": got["문서"]})
|
||||
r = client.put("/api/m02/templates/area/rv", json={"판": got["판"], "문서": got["문서"]})
|
||||
assert r.status_code == 503
|
||||
assert client.get("/api/m02/templates/material/rv").json()["판"] == got["판"]
|
||||
assert client.get("/api/m02/templates/area/rv").json()["판"] == got["판"]
|
||||
|
||||
|
||||
def test_층_규칙과_프로젝트_복사(client, tmp_path, monkeypatch):
|
||||
client.post("/api/m02/structures/rv")
|
||||
for kind in ("area", "material"):
|
||||
for kind in ("area",):
|
||||
doc = store.read(kind, "rv")["문서"]
|
||||
layers.check_skeleton(kind, doc)
|
||||
for bad in ({}, {**doc, "시트": {}}):
|
||||
@@ -140,5 +136,6 @@ def test_층_규칙과_프로젝트_복사(client, tmp_path, monkeypatch):
|
||||
layers.check_skeleton("structure", {"도면": {"entities": []}}) # 산출근거 없어도 됨
|
||||
monkeypatch.setattr(layers, "SYSTEM_ROOT", tmp_path)
|
||||
copied = layers.seed_project(tmp_path / "project")
|
||||
for kind in ("area", "material"):
|
||||
assert not any(k.startswith("material/") for k in copied["작업본"]) # 재료산출 없앰
|
||||
for kind in ("area",):
|
||||
assert f"{kind}/rv" in copied["작업본"] and f"{kind}/rv" in copied["초기"]
|
||||
|
||||
@@ -54,7 +54,7 @@ def world(tmp_path: Path, monkeypatch, master_copy: Path) -> dict[str, Any]:
|
||||
system.mkdir()
|
||||
for name in (layers.COMBO_FILE,):
|
||||
shutil.copy2(REAL / name, system / name)
|
||||
for kind in ("table", "structure", "area", "material"):
|
||||
for kind in ("table", "structure", "area"):
|
||||
shutil.copytree(REAL / kind, system / kind)
|
||||
monkeypatch.setattr(layers, "SYSTEM_ROOT", system)
|
||||
monkeypatch.setattr(m01, "FOLDER", master_copy)
|
||||
|
||||
@@ -196,7 +196,7 @@ def test_분류_더하기로_복사한_열은_원본_키_세_파일_내용을_
|
||||
numbers = client.get("/api/m02/structures/numbers").json()
|
||||
assert numbers["c9"]["도번"] != numbers["rv"]["도번"] # 새 번호
|
||||
assert numbers["c9"]["면적번"][2:] == numbers["c9"]["도번"][2:]
|
||||
for kind in ("area", "material"):
|
||||
for kind in ("area",):
|
||||
assert client.get(f"/api/m02/templates/{kind}/c9").json()["문서"]["열"] == "c9"
|
||||
# 그 뒤로는 열마다 따로 — 원본을 고쳐도 복사본은 그대로
|
||||
drawing = client.get("/api/m02/templates/structure/rv").json()
|
||||
|
||||
@@ -80,7 +80,7 @@ def test_지운_뒤_항목_0_틀은_남음(empty):
|
||||
combos = client.get("/api/m02/structure-combos").json()
|
||||
assert combos["줄"] == [] and combos["다음"] == 52
|
||||
assert len(combos["지운번호"]) == 4 # 견본에 세 파일이 있던 넷(rv · c4 · pp_len · ms)
|
||||
for kind in ("structure", "area", "material", "inputs"):
|
||||
for kind in ("structure", "area", "inputs"):
|
||||
assert not list((system / kind).glob("*.json")), kind
|
||||
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ def test_새_번호는_옛_번호와_모양이_달라_안_겹침(world):
|
||||
doc = json.loads(path.read_bytes())
|
||||
path.write_text(json.dumps({**doc, key: f"{head}{code}"}), encoding="utf-8")
|
||||
old = {cid: _numbers(system, cid) for cid in store._column_ids()}
|
||||
assert old["rv"] == {"SD01010100", "SA01010100", "SM01010100"}
|
||||
assert old["rv"] == {"SD01010100", "SA01010100"}
|
||||
first = max(int(n[3:]) for nums in old.values() for n in nums if NEW.match(n)) + 1
|
||||
with store.at(system):
|
||||
got = store.read("table", store.STRUCTURE_TABLE)
|
||||
@@ -191,7 +191,7 @@ def test_새_번호는_옛_번호와_모양이_달라_안_겹침(world):
|
||||
got["문서"]["열"].append({"id": "y", "머리": ["y"], "꼴": "수"})
|
||||
store.write("table", store.STRUCTURE_TABLE, got["판"], got["문서"])
|
||||
last = f"-{first + 120:06d}" # 지운 x119 번호(first + 119) 다음
|
||||
assert _numbers(system, "y") == {f"SD{last}", f"SA{last}", f"SM{last}"}
|
||||
assert _numbers(system, "y") == {f"SD{last}", f"SA{last}"}
|
||||
|
||||
|
||||
def test_층마다_번호는_따로_파일은_열_id_로(world):
|
||||
|
||||
@@ -68,7 +68,7 @@ def test_시스템_층은_그대로_항목_목록은_SS_줄(world):
|
||||
ss = store.read_combos()["줄"]
|
||||
assert [r["key"] for r in rows] == [r["키"] for r in ss] and rows[0]["key"] == "SS000001"
|
||||
assert set(rows[0]) == {"key", "names", *items_router.items_store.ITEM_FIELDS}
|
||||
assert set(rows[0]["names"]) == {"일위대가", "도번", "면적번", "재료번"}
|
||||
assert set(rows[0]["names"]) == {"일위대가", "도번", "면적번"}
|
||||
assert client.get("/api/m02/layers/system/structure-combos").json() == store.read_combos()
|
||||
# 시스템 층 쓰기는 SYSTEM_ADMIN 만
|
||||
assert client.post("/api/m02/layers/system/structures/rv").status_code == 403
|
||||
@@ -145,7 +145,6 @@ def test_회사_항목을_개인으로_복사(world):
|
||||
"일위대가": ua_name,
|
||||
"도번": "돌기슭막이 찰쌓기 회사형",
|
||||
"면적번": "돌기슭막이 찰쌓기 회사형",
|
||||
"재료번": "돌기슭막이 찰쌓기 회사형",
|
||||
}
|
||||
|
||||
user = _as(world)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""M02 구조물 도면(structure) — 새로 · 도번 SD+8자리 · 409 · 저장 · 지운 뒤 번호 ·
|
||||
번호 매김(새 항목 = 자리 뜻 없는 일련번호 · PLAN 26-1) · 집계표 저장 때 만들기/지우기 ·
|
||||
복사 가져오기 · 프로젝트 복사.
|
||||
면적산출(area) · 재료산출(material)은 따로 파일 —
|
||||
면적산출(area)은 따로 파일(재료산출은 없앰) —
|
||||
`resources/tester/spreadsheet/test_spreadsheet_store.py`."""
|
||||
|
||||
import json
|
||||
@@ -64,8 +64,8 @@ def client(tmp_path, monkeypatch):
|
||||
|
||||
|
||||
def _num(code: str) -> dict:
|
||||
"""세 번호 — 같은 8자리."""
|
||||
return {"도번": f"SD{code}", "면적번": f"SA{code}", "재료번": f"SM{code}"}
|
||||
"""두 번호 — 같은 8자리."""
|
||||
return {"도번": f"SD{code}", "면적번": f"SA{code}"}
|
||||
|
||||
|
||||
def test_새로_도번_자동_409_없는_열(client):
|
||||
@@ -82,10 +82,10 @@ def test_새로_도번_자동_409_없는_열(client):
|
||||
assert doc["도면"]["format"] == 6
|
||||
assert doc["도면"]["entities"] == []
|
||||
assert len(doc["도면"]["layers"]) == 1
|
||||
# 면적산출 · 재료산출은 구조물 문서 밖 — 같은 열 id 로 따로 한 통합문서씩
|
||||
# 면적산출은 구조물 문서 밖 — 같은 열 id 로 따로 한 통합문서 · 재료산출은 안 만듦
|
||||
assert "산출근거" not in doc
|
||||
for kind in ("area", "material"):
|
||||
assert client.get(f"/api/m02/templates/{kind}/pp_len").json()["문서"]["종류"] == "통합문서"
|
||||
assert client.get("/api/m02/templates/area/pp_len").json()["문서"]["종류"] == "통합문서"
|
||||
assert client.get("/api/m02/templates/material/pp_len").status_code == 404
|
||||
|
||||
assert client.post("/api/m02/structures/rv").json()["문서"]["도번"] == "SD-000002"
|
||||
assert client.post("/api/m02/structures/pp_len").status_code == 409
|
||||
@@ -127,7 +127,7 @@ def test_지워도_번호를_당기지_않음_짝은_같은_번호(client):
|
||||
client.post(f"/api/m02/structures/{column}")
|
||||
assert client.delete("/api/m02/templates/structure/rv").status_code == 200
|
||||
numbers = client.get("/api/m02/structures/numbers").json()
|
||||
assert numbers["rv"] == {**_num("-000002"), "도번": ""} # 면적 · 재료는 남아 번호를 쥠
|
||||
assert numbers["rv"] == {**_num("-000002"), "도번": ""} # 면적은 남아 번호를 쥠
|
||||
assert numbers["ms"] == _num("-000003") # 다른 열 번호는 그대로
|
||||
# 새 열 = 다음 일련번호(자리 뜻 없음) · 다시 만든 도면은 짝(면적산출) 번호를 이어 받음
|
||||
assert client.post("/api/m02/structures/pp_cp").json()["문서"]["도번"] == "SD-000004"
|
||||
@@ -181,7 +181,7 @@ def test_집계표_저장_때_새_열은_만들고_지운_열은_같이_지움(c
|
||||
assert saved.status_code == 200
|
||||
numbers = client.get("/api/m02/structures/numbers").json()
|
||||
assert "pv_a" not in numbers and "note2" not in numbers
|
||||
for kind_ in ("structure", "area", "material"):
|
||||
for kind_ in ("structure", "area"):
|
||||
assert client.get(f"/api/m02/templates/{kind_}/pv_a").status_code == 404
|
||||
# 새 열 = 열 차례로 다음 일련번호 · 지운 pv_a(5) 는 비워 둠(다른 번호를 안 당김)
|
||||
assert numbers["pv_x"] == _num("-000014")
|
||||
|
||||
@@ -84,9 +84,9 @@ def test_집계표_저장_때_새_열_지운_열_고친_열을_맞춤_키_유지
|
||||
assert (rv["분류1"], rv["분류3"]) == (old_rv["분류1"], old_rv["분류3"])
|
||||
assert (rv["단위"], rv["일위대가"]) == ("m", "UA000004")
|
||||
assert (rv["바인딩"], rv["펼침"]) == ({"종류": "revetment"}, True)
|
||||
# 번호는 세 파일과 같음 — 새 열도 [저장] 한 번에 SD · SA · SM 이 섬
|
||||
# 번호는 파일과 같음 — 새 열도 [저장] 한 번에 SD · SA 가 섬
|
||||
numbers = client.get("/api/m02/structures/numbers").json()
|
||||
assert {k: rows["new1"][k] for k in ("도번", "면적번", "재료번")} == numbers["new1"]
|
||||
assert {k: rows["new1"][k] for k in ("도번", "면적번")} == numbers["new1"]
|
||||
assert rows["new1"]["도번"].startswith("SD")
|
||||
|
||||
# 또 저장해도(바뀐 것 없음) 키 · 다음 그대로
|
||||
|
||||
@@ -51,8 +51,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),
|
||||
# 도면 카드(31-4)는 카드 파일에서 뗀 `DrawingCard` 가 부름
|
||||
@@ -72,15 +72,13 @@ def test_컨테이너가_쓰는_이름이_내보내진_것(source: str, names: l
|
||||
def test_sub2_가_부르는_이름과_모양():
|
||||
"""브레인 확정 이름 · 모양 — sub2 가 이대로 부름(바꾸면 브레인에게)."""
|
||||
body = _text(M02 / "M02_MasterTemplete_Api_Fetch.ts")
|
||||
for name in ("copyStructure", "copyArea", "copyMaterial"):
|
||||
for name in ("copyStructure", "copyArea"):
|
||||
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*재료번: string;", body
|
||||
)
|
||||
assert re.search(r'export type Kind = [^;]*"area" \| "material"', body)
|
||||
assert re.search(r"interface StructureNumbers \{\s*도번: string;\s*면적번: string;\s*\}", body)
|
||||
assert re.search(r'export type Kind = [^;]*"area";', body)
|
||||
|
||||
|
||||
def test_집계표만_컨테이너를_붙임():
|
||||
@@ -154,7 +152,7 @@ def test_좌측_구조물_집계_양식_아래_하위_넷_새로_본떠_지우
|
||||
assert "createSelectField" not in side # 새로 = 종류 고르기 없이 이름만
|
||||
assert "cur?.종류 !== kind" in side # 본떠 · 지우기 = 그 컨테이너에서 고른 것이 있을 때만
|
||||
assert "i.이름 === STRUCTURE_TABLE" in side # 표는 구조물집계표 한 줄만
|
||||
# 구조물 도면 · 면적산출 · 재료산출 = 구조물 집계 양식 컨테이너 안(같이 접힘) · 자재 조합 없앰
|
||||
# 구조물 도면 · 면적산출 = 구조물 집계 양식 컨테이너 안(같이 접힘) · 자재 조합 없앰
|
||||
summary = side.split("const summary = section(")[1].split(");")[0]
|
||||
assert "COLUMN_KINDS.map" in summary and "matset" not in side
|
||||
assert 'M02_KindTable: ["구조물 조합"' in _text( # 화면 이름(26-7a)
|
||||
@@ -214,7 +212,7 @@ def test_표_도구_막대에_줄_더하기_줄_지우기_손_입력_열_없음(
|
||||
def test_상세_화면_돌아가기는_집계표로_그_열_고른_채():
|
||||
main = _text(MAIN)
|
||||
assert "children: [title, back, slot, reload, save, " in main # 제목 바로 오른쪽
|
||||
assert "!(sel.kind in NUMBER_FIELD)" in main # 구조물 도면 · 면적산출 · 재료산출만
|
||||
assert "!(sel.kind in NUMBER_FIELD)" in main # 구조물 도면 · 면적산출만
|
||||
body = main.split('back.addEventListener("click"')[1]
|
||||
assert "confirmLeave()" in body and "STRUCTURE_TABLE" in body
|
||||
assert "sheet?.selectColumn(column)" in body
|
||||
|
||||
@@ -188,7 +188,7 @@ def test_빈_층에서_폴더_항목_깊이_1_2_5_이름_옮기기_지우기(wor
|
||||
(root / "inputs" / f"{gone['열']}.json").write_text('{"판": 1, "줄": []}', encoding="utf-8")
|
||||
note = t.do(op="delete", 키=gone["key"])["알림"]
|
||||
assert note["지운항목"] == [gone["열"]]
|
||||
for part in ("structure", "area", "material", "inputs"):
|
||||
for part in ("structure", "area", "inputs"):
|
||||
assert not (root / part / f"{gone['열']}.json").is_file()
|
||||
t.do(op="add-item", 경로=["돌쌓기"], 이름="새것") # 지운 번호는 다시 안 줌
|
||||
assert _numbers(root, t.item(["돌쌓기", "새것"])["열"]) == {"-000004"}
|
||||
|
||||
@@ -50,7 +50,7 @@ def test_회사_항목_폴더를_개인에_붙임_새_키_파일_이름2(world):
|
||||
table = json.loads((root / "table/구조물집계표.json").read_bytes())
|
||||
made = next(c for c in table["열"] if c.get("경로") == ["옹벽"])
|
||||
assert made["본뜬"] == {"layer": "company", "owner": "7", "key": wall}
|
||||
for kind in ("structure", "area", "material"):
|
||||
for kind in ("structure", "area"):
|
||||
assert (root / kind / f"{made['id']}.json").is_file()
|
||||
# 다시 붙이면 같은 이름 「(2)」 · 붙일 폴더 안으로
|
||||
again = client.post(PASTE.format("personal"), json={**body, "경로": []}).json()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""표 도면 줄 · 칸 고르기 신호 — `ui_template_sheet_ops.ts` 를 Node 로 바로 돌려 봄.
|
||||
|
||||
마스터 줄 = 일위대가 · 도면 `s:drawing` · 면적산출 `s:area` · 재료산출 `s:material`(식 · 들어갈 것 줄 없음) ·
|
||||
마스터 줄 = 일위대가 · 도면 `s:drawing` · 면적산출 `s:area`(식 · 들어갈 것 줄 없음) ·
|
||||
master 만 · 단위 줄 바로 아래 차례(키보드) · project 에는 없음.
|
||||
`selectionOf` = 격자가 `onSelect` 로 넘기는 모양({row, col, colId} · 모르는 열은 col -1 · colId null).
|
||||
"""
|
||||
@@ -26,7 +26,7 @@ console.log(JSON.stringify({
|
||||
master: navRows(doc, "master"),
|
||||
project: navRows(doc, "project"),
|
||||
pick: [selectionOf(doc, "s:drawing", "c2"), selectionOf(doc, "d:r1", "c1"), selectionOf(doc, "s:unit", "zz")],
|
||||
blank: ["s:price", "s:drawing", "s:area", "s:material", "s:unit", "d:r1"].map((k) => [isBlankPick(k, { id: "t", 머리: [], 꼴: "글" }), isBlankPick(k, { id: "n", 머리: [], 꼴: "수" })]),
|
||||
blank: ["s:price", "s:drawing", "s:area", "s:unit", "d:r1"].map((k) => [isBlankPick(k, { id: "t", 머리: [], 꼴: "글" }), isBlankPick(k, { id: "n", 머리: [], 꼴: "수" })]),
|
||||
}));
|
||||
"""
|
||||
|
||||
@@ -53,7 +53,7 @@ def _run() -> dict:
|
||||
def test_drawing_row_under_price_master_only():
|
||||
got = _run()
|
||||
assert got["master"] == [
|
||||
"s:unit", "s:price", "s:drawing", "s:area", "s:material", "d:r1", "t:t1",
|
||||
"s:unit", "s:price", "s:drawing", "s:area", "d:r1", "t:t1",
|
||||
] # fmt: skip
|
||||
assert got["project"] == ["s:unit", "d:r1", "t:t1"]
|
||||
|
||||
@@ -68,5 +68,5 @@ def test_selection_shape():
|
||||
|
||||
def test_text_column_pick_rows_blank():
|
||||
assert _run()["blank"] == [
|
||||
[True, False], [True, False], [True, False], [True, False], [False, False], [False, False],
|
||||
[True, False], [True, False], [True, False], [False, False], [False, False],
|
||||
] # fmt: skip
|
||||
|
||||
@@ -36,7 +36,7 @@ def _copy_layer(target: Path) -> None:
|
||||
target.mkdir(parents=True)
|
||||
for name in (layers.COMBO_FILE,):
|
||||
shutil.copy2(REAL / name, target / name)
|
||||
for kind in ("table", "structure", "area", "material"):
|
||||
for kind in ("table", "structure", "area"):
|
||||
shutil.copytree(REAL / kind, target / kind)
|
||||
|
||||
|
||||
@@ -131,7 +131,12 @@ def test_지정하면_폴더가_생기고_내용이_원본과_같음(world):
|
||||
assert result == {"만듦": ["pipe@40.0", "st0"], "지움": [], "못만듦": [], "옮겨받음": []}
|
||||
folder = _items(root) / "st0"
|
||||
got = {path.name for path in folder.iterdir()}
|
||||
assert {"집계.json", "도면.json", "면적.json", "재료.json", "manifest.json"} <= got
|
||||
assert {
|
||||
"집계.json",
|
||||
"도면.json",
|
||||
"면적.json",
|
||||
"manifest.json",
|
||||
} <= got and "재료.json" not in got
|
||||
total = json.loads((folder / "집계.json").read_text(encoding="utf-8"))
|
||||
assert total["줄"]["키"] == "SS000022" and total["열"]["id"] == "ms"
|
||||
drawing = json.loads((world["system"] / "structure/ms.json").read_text(encoding="utf-8"))
|
||||
|
||||
@@ -43,7 +43,6 @@ const row = (
|
||||
일위대가: "UA000004",
|
||||
도번: "SD01010100",
|
||||
면적번: "SA01010100",
|
||||
재료번: "SM01010100",
|
||||
layer,
|
||||
owner: layer === "company" ? "샘플회사" : layer === "personal" ? "샘플회사/사용자" : null,
|
||||
...extra,
|
||||
@@ -55,7 +54,6 @@ const rows: PickerRow[] = [
|
||||
일위대가: "돌기슭막이 메쌓기 일위대가",
|
||||
도번: "돌기슭막이 도면",
|
||||
면적번: "돌기슭막이 면적산출",
|
||||
재료번: "돌기슭막이 재료산출",
|
||||
},
|
||||
}),
|
||||
row("SS000002", "rv2", "돌기슭막이", ["메쌓기", "1.5m", "기초유"], "system"),
|
||||
@@ -219,20 +217,20 @@ assert.deepEqual(pathIds(rows[0], labels), [
|
||||
"system|돌기슭막이|메쌓기|1.2m",
|
||||
]);
|
||||
|
||||
// 폴더 모양 — 네 줄 · 이름은 서버 names · 없으면 키만(작은 글) · 둘 다 없으면 빈 키
|
||||
// 폴더 모양 — 세 줄 · 이름은 서버 names · 없으면 키만(작은 글) · 둘 다 없으면 빈 키
|
||||
const lines = folderLines(rows[0]);
|
||||
assert.deepEqual(
|
||||
lines.map((l) => l.kind),
|
||||
["drawing", "area", "material", "unitPrice"],
|
||||
["drawing", "area", "unitPrice"],
|
||||
);
|
||||
assert.deepEqual(lines[0], { kind: "drawing", name: "돌기슭막이 도면", key: "SD01010100" });
|
||||
assert.deepEqual(lines[3], {
|
||||
assert.deepEqual(lines[2], {
|
||||
kind: "unitPrice",
|
||||
name: "돌기슭막이 메쌓기 일위대가",
|
||||
key: "UA000004",
|
||||
});
|
||||
assert.deepEqual(folderLines(rows[1])[0], { kind: "drawing", name: null, key: "SD01010100" }); // names 자체가 없음
|
||||
assert.deepEqual(folderLines({ key: "SS1", 열: "x" })[3], {
|
||||
assert.deepEqual(folderLines({ key: "SS1", 열: "x" })[2], {
|
||||
kind: "unitPrice",
|
||||
name: null,
|
||||
key: "",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/* =============================================================================
|
||||
* ui_template_sheet.ts
|
||||
* 엑셀처럼 도는 표 부품 — `createSheet(칸, 문서, {mode, onChange, drawingLabel, areaLabel, materialLabel, onSelect, onPick})`
|
||||
* 엑셀처럼 도는 표 부품 — `createSheet(칸, 문서, {mode, onChange, drawingLabel, areaLabel, onSelect, onPick})`
|
||||
* → `{getDoc, setDoc, recalc, selectColumn, destroy}`.
|
||||
*
|
||||
* master = 시스템 관리자 양식 고치기 — 머리 · 층 이름 · 단위 · 일위대가 ·
|
||||
* 층마다 열 더하기(「분류1 더하기」 … 층 이름 + 더하기) · 열 지우기.
|
||||
* 도면 · 면적산출 · 재료산출 줄 = `drawingLabel` · `areaLabel` · `materialLabel` 글 ·
|
||||
* 칸을 고르면 `onSelect` · `onPick` 을 주면 일위대가 · 도면 · 면적산출 · 재료산출 칸은
|
||||
* 도면 · 면적산출 줄 = `drawingLabel` · `areaLabel` 글 ·
|
||||
* 칸을 고르면 `onSelect` · `onPick` 을 주면 일위대가 · 도면 · 면적산출 칸은
|
||||
* 치지 않고 조회 후 선택.
|
||||
* `readOnly` = 볼 수만 있음 — 단추 줄 · 안내 글 숨김 · 칸 고치기 · 머리 고치기 · 열 너비 끌기 막음 ·
|
||||
* 열이 하나도 없으면 [열 더하기] 가 켜져 첫 열을 만듦(머리 글 바로 고치기).
|
||||
@@ -56,12 +56,11 @@ export interface SheetOptions {
|
||||
onChange?: (doc: SheetDoc) => void;
|
||||
/** 도면 줄 칸 글(마스터 첫 쪽 · 열 id) — 없거나 빈 글이면 「없음」 · 글이 바뀌면 `recalc()` 로 다시 그림. */
|
||||
drawingLabel?: (colId: string) => string;
|
||||
/** 면적산출 · 재료산출 줄 칸 글(번호) — `drawingLabel` 과 같음. */
|
||||
/** 면적산출 줄 칸 글(번호) — `drawingLabel` 과 같음. */
|
||||
areaLabel?: (colId: string) => string;
|
||||
materialLabel?: (colId: string) => string;
|
||||
/** 칸을 고름 — 누르기 · 방향키 · Tab. */
|
||||
onSelect?: (sel: SheetSelection) => void;
|
||||
/** 조회 후 선택 — master 일위대가 · 도면 · 면적산출 · 재료산출 칸을 두 번 누르기 · Enter · F2 로 부름.
|
||||
/** 조회 후 선택 — master 일위대가 · 도면 · 면적산출 칸을 두 번 누르기 · Enter · F2 로 부름.
|
||||
* 글을 돌려주면 일위대가 칸에 넣음 · 번호 글 줄은 부른 쪽이 가져온 뒤 `recalc()`.
|
||||
* 없으면 일위대가는 글로 고침 · 번호 글 줄은 읽기만. */
|
||||
onPick?: (sel: SheetSelection) => Promise<string | null | undefined> | void;
|
||||
@@ -90,7 +89,6 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio
|
||||
sel: null,
|
||||
drawingLabel: opts.drawingLabel,
|
||||
areaLabel: opts.areaLabel,
|
||||
materialLabel: opts.materialLabel,
|
||||
pick: !!opts.onPick,
|
||||
headLocked: !!opts.lockHead,
|
||||
combo: !!opts.combo,
|
||||
|
||||
@@ -22,16 +22,15 @@ export const KEY_UNIT = "s:unit";
|
||||
export const KEY_PRICE = "s:price";
|
||||
export const KEY_DRAWING = "s:drawing";
|
||||
export const KEY_AREA = "s:area";
|
||||
export const KEY_MATERIAL = "s:material";
|
||||
/** 마스터 전용 줄(첫 쪽 단위 줄 아래 차례) — 도면 · 면적산출 · 재료산출 줄은 손으로 못 침(번호 글). */
|
||||
export const MASTER_ROWS = [KEY_PRICE, KEY_DRAWING, KEY_AREA, KEY_MATERIAL] as const;
|
||||
/** 마스터 전용 줄(첫 쪽 단위 줄 아래 차례) — 도면 · 면적산출 줄은 손으로 못 침(번호 글). */
|
||||
export const MASTER_ROWS = [KEY_PRICE, KEY_DRAWING, KEY_AREA] as const;
|
||||
export type MasterRowKey = (typeof MASTER_ROWS)[number];
|
||||
/** 번호 글 줄 — 부른 쪽이 준 글(`refLabel`)만 보임. */
|
||||
export const REF_ROWS: readonly string[] = [KEY_DRAWING, KEY_AREA, KEY_MATERIAL];
|
||||
export const REF_ROWS: readonly string[] = [KEY_DRAWING, KEY_AREA];
|
||||
/** 조회 후 선택 줄 — `onPick` 을 주면 이 줄 칸은 치지 않고 고름. */
|
||||
export const PICK_ROWS: readonly string[] = MASTER_ROWS;
|
||||
|
||||
/** 글 열(NO · 측점 · 비고 같은 꼴=글)의 일위대가 · 도면 · 면적산출 · 재료산출 칸 — 빈칸 · 잠김. */
|
||||
/** 글 열(NO · 측점 · 비고 같은 꼴=글)의 일위대가 · 도면 · 면적산출 칸 — 빈칸 · 잠김. */
|
||||
export const isBlankPick = (key: string, col: SheetColumn): boolean =>
|
||||
PICK_ROWS.includes(key) && col.꼴 === "글";
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* 표 그리기 — 문서 + 풀이 결과 → 쪽마다 `<table>`(머리 층 · 단위 줄 · 본문 · 합계 줄).
|
||||
*
|
||||
* 칸마다 `data-r`(줄 열쇠) · `data-c`(열 id) — 고치기 · 키보드는 격자(`ui_template_sheet.ts`)가 위임으로 받음.
|
||||
* 줄 열쇠 — 본문 `d:<줄id>` · 합계 `t:<합계id>` · 마스터 줄 `s:unit|price|drawing|area|material`.
|
||||
* master = 본문 위에 열마다 일위대가 · 도면 · 면적산출 · 재료산출 줄 · project = 쪽줄마다 쪽을 나눔(쪽마다 머리 · 합계는 마지막 쪽).
|
||||
* 줄 열쇠 — 본문 `d:<줄id>` · 합계 `t:<합계id>` · 마스터 줄 `s:unit|price|drawing|area`.
|
||||
* master = 본문 위에 열마다 일위대가 · 도면 · 면적산출 줄 · project = 쪽줄마다 쪽을 나눔(쪽마다 머리 · 합계는 마지막 쪽).
|
||||
* 머리 층 줄 머리 칸은 `data-layer`(층 차례) — master 에서 층 이름을 고침.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
isBlankPick,
|
||||
isCalcColumn,
|
||||
KEY_AREA,
|
||||
KEY_MATERIAL,
|
||||
KEY_UNIT,
|
||||
MASTER_ROWS,
|
||||
type MasterRowKey,
|
||||
@@ -39,10 +38,9 @@ export interface RenderState {
|
||||
mode: SheetMode;
|
||||
result: SheetResult;
|
||||
sel: { r: string; c: string } | null;
|
||||
/** 도면 · 면적산출 · 재료산출 줄 칸 글(열 id) — 없거나 빈 글이면 「없음」. */
|
||||
/** 도면 · 면적산출 줄 칸 글(열 id) — 없거나 빈 글이면 「없음」. */
|
||||
drawingLabel?: (colId: string) => string;
|
||||
areaLabel?: (colId: string) => string;
|
||||
materialLabel?: (colId: string) => string;
|
||||
/** 조회 후 선택 줄(`PICK_ROWS`)을 고름으로 받나 — 칸 겉모양만. */
|
||||
pick?: boolean;
|
||||
/** 머리 · 층 이름을 못 고침(`lockHead`) — 칸 겉모양만. */
|
||||
@@ -53,13 +51,9 @@ export interface RenderState {
|
||||
headEditable?: boolean;
|
||||
}
|
||||
|
||||
/** 도면 · 면적산출 · 재료산출 줄 칸 글. */
|
||||
/** 도면 · 면적산출 줄 칸 글. */
|
||||
export const refText = (state: RenderState, key: string, colId: string): string =>
|
||||
(key === KEY_AREA
|
||||
? state.areaLabel
|
||||
: key === KEY_MATERIAL
|
||||
? state.materialLabel
|
||||
: state.drawingLabel)?.(colId)?.trim() || st("Drawing_None");
|
||||
(key === KEY_AREA ? state.areaLabel : state.drawingLabel)?.(colId)?.trim() || st("Drawing_None");
|
||||
|
||||
/** 열 폭 — 저장 폭(없으면 기본) · 머리 글이 안 잘리는 최소 폭보다 좁지 않게. */
|
||||
const columnWidth = (doc: SheetDoc, col: SheetColumn, mins: Map<string, number>): number =>
|
||||
@@ -68,7 +62,7 @@ const columnWidth = (doc: SheetDoc, col: SheetColumn, mins: Map<string, number>)
|
||||
mins.get(col.id) ?? 0,
|
||||
);
|
||||
|
||||
/** 구조물 조합 — 일위대가 · 도면 · 면적산출 · 재료산출 줄 글(키 · 번호)이 줄바꿈 없이 들게 최소 폭을 넓힘
|
||||
/** 구조물 조합 — 일위대가 · 도면 · 면적산출 줄 글(키 · 번호)이 줄바꿈 없이 들게 최소 폭을 넓힘
|
||||
* (`.ui-sheet__meta td` 글꼴과 같게 잼). */
|
||||
function fitMeta(state: RenderState, cols: SheetColumn[], mins: Map<string, number>): void {
|
||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
||||
@@ -115,7 +109,7 @@ export function renderSheet(state: RenderState): HTMLElement {
|
||||
const mins = headMinWidths(cols, depth, font);
|
||||
if (state.combo) fitMeta(state, cols, mins);
|
||||
const widthDoc = state.combo ? { ...doc, 보기: { ...doc.보기, 열너비: {} } } : doc;
|
||||
// 줄 머리 칸 폭 — 층 이름 · 단위 · 일위대가 · 도면 · 면적산출 · 재료산출 · 합계 이름이 안 잘리게
|
||||
// 줄 머리 칸 폭 — 층 이름 · 단위 · 일위대가 · 도면 · 면적산출 · 합계 이름이 안 잘리게
|
||||
const GUTTER = Math.min(
|
||||
GUTTER_MAX,
|
||||
Math.max(
|
||||
@@ -126,7 +120,6 @@ export function renderSheet(state: RenderState): HTMLElement {
|
||||
st("Row_Unit_Price"),
|
||||
st("Row_Drawing"),
|
||||
st("Row_Area"),
|
||||
st("Row_Material"),
|
||||
...(doc.합계줄 ?? []).map((t) => t.이름),
|
||||
].map((label) => Math.ceil(textWidth(label, font)) + 16),
|
||||
),
|
||||
@@ -193,7 +186,6 @@ export function renderSheet(state: RenderState): HTMLElement {
|
||||
"s:price": "Row_Unit_Price",
|
||||
"s:drawing": "Row_Drawing",
|
||||
"s:area": "Row_Area",
|
||||
"s:material": "Row_Material",
|
||||
} as const
|
||||
)[key];
|
||||
const tr = el("tr", { className: "ui-sheet__meta", children: [gutter("th", st(label))] });
|
||||
|
||||
@@ -13,7 +13,6 @@ const TEXT = {
|
||||
Row_Unit_Price: ["일위대가", "Unit price"],
|
||||
Row_Drawing: ["도면", "Drawing"],
|
||||
Row_Area: ["면적산출", "Area"],
|
||||
Row_Material: ["재료산출", "Material"],
|
||||
Add_Suffix: [" 추가", " +"],
|
||||
New_Prefix: ["새 ", "New "],
|
||||
Drawing_None: ["없음", "None"],
|
||||
|
||||
@@ -80,7 +80,7 @@ export interface SheetDoc {
|
||||
보기?: SheetView;
|
||||
}
|
||||
|
||||
/** 고른 칸 — `row` = 줄 열쇠(`d:<줄id>` · `t:<합계id>` · `s:unit|price|drawing|area|material`) ·
|
||||
/** 고른 칸 — `row` = 줄 열쇠(`d:<줄id>` · `t:<합계id>` · `s:unit|price|drawing|area`) ·
|
||||
* `col` = 열 차례(0부터 · 없으면 -1) · `colId` = 열 id(없으면 null). */
|
||||
export interface SheetSelection {
|
||||
row: string;
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface SummaryItem {
|
||||
}
|
||||
|
||||
/** 항목 줄이 가리키는 키 칸 — 서버가 `names` 로 이름을 함께 주면 이름을 보임 */
|
||||
export type NameField = "일위대가" | "도번" | "면적번" | "재료번";
|
||||
export type NameField = "일위대가" | "도번" | "면적번";
|
||||
|
||||
/** 목록 한 줄(21-1) — 서버는 `?all=1` 을 `[{layer, owner, items}]` 로 주고, 줄마다 `layer` · `owner` 를 붙여 폄.
|
||||
* `열` 은 집계표 열 id(rv …) — 보이는 이름은 종류 · 분류를 이은 `itemName`. */
|
||||
@@ -30,7 +30,6 @@ export interface PickerRow {
|
||||
일위대가?: string;
|
||||
도번?: string;
|
||||
면적번?: string;
|
||||
재료번?: string;
|
||||
/** 폴더 이름 배열(깊이 자유 · 26-1) — 없으면 종류 · 분류1~3 중 있는 것으로 이음 */
|
||||
경로?: string[] | null;
|
||||
/** 위 키 칸의 이름(21-9 서버가 더함) — 없으면 키를 작은 글로 보임 */
|
||||
@@ -167,7 +166,7 @@ const ancestorIds = (layer: PickerLayer, path: string[]): string[] => [
|
||||
export const pathIds = (row: PickerRow, _labels?: TreeLabels): string[] =>
|
||||
ancestorIds(layerOf(row), pathOf(row).slice(0, -1));
|
||||
|
||||
export type FolderKind = "drawing" | "area" | "material" | "unitPrice";
|
||||
export type FolderKind = "drawing" | "area" | "unitPrice";
|
||||
|
||||
export interface FolderLine {
|
||||
kind: FolderKind;
|
||||
@@ -180,11 +179,10 @@ export interface FolderLine {
|
||||
const FOLDER_FIELDS: readonly [FolderKind, NameField][] = [
|
||||
["drawing", "도번"],
|
||||
["area", "면적번"],
|
||||
["material", "재료번"],
|
||||
["unitPrice", "일위대가"],
|
||||
];
|
||||
|
||||
/** 오른쪽 폴더 모양 — 네 줄 모두 서버의 키 칸 + `names` 의 이름 */
|
||||
/** 오른쪽 폴더 모양 — 세 줄 모두 서버의 키 칸 + `names` 의 이름 */
|
||||
export function folderLines(row: PickerRow): FolderLine[] {
|
||||
return FOLDER_FIELDS.map(([kind, field]) => ({
|
||||
kind,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* =============================================================================
|
||||
* ui_template_item_picker_detail.ts
|
||||
* 항목 고르기 모달 오른쪽 상세(계획서 25-2d · 26-3b) — **카드 넷**(도면 · 면적산출 · 재료산출 · 선택 입력).
|
||||
* 항목 고르기 모달 오른쪽 상세(계획서 25-2d · 26-3b) — **카드 셋**(도면 · 면적산출 · 선택 입력).
|
||||
* 카드는 M02 와 같은 한 벌(`M02_MasterTemplete_ItemCards` · `_Inputs`)을 늦게 받아 씀
|
||||
* (B05 · B06 번들에 CAD · 스프레드시트가 안 섞이게 `import()`).
|
||||
*
|
||||
@@ -89,7 +89,7 @@ const TEXT = {
|
||||
reapplyTip:
|
||||
"원본을 다시 가져옴 — 이 구조물 복사본에서 고친 것(도면 · 시트 · 선택 입력 자리)이 원본 것으로 덮임",
|
||||
reapplyAsk:
|
||||
"원본을 다시 가져옴 — 이 구조물 복사본에서 고친 도면 · 면적산출 · 재료산출 · 선택 입력 자리가 원본 것으로 덮임(넣은 값은 같은 키면 남음). 재적용할까요?",
|
||||
"원본을 다시 가져옴 — 이 구조물 복사본에서 고친 도면 · 면적산출 · 선택 입력 자리가 원본 것으로 덮임(넣은 값은 같은 키면 남음). 재적용할까요?",
|
||||
reapplied: "재적용했음 — 덮인 것: {value}",
|
||||
reappliedNone: "재적용했음 — 덮인 것 없음(원본과 같았음)",
|
||||
reapplyFailed: "재적용하지 못함 — {value}",
|
||||
@@ -114,7 +114,6 @@ const TEXT = {
|
||||
parts: {
|
||||
도면: "도면",
|
||||
면적: "면적산출",
|
||||
재료: "재료산출",
|
||||
입력연결: "선택 입력 자리",
|
||||
} as Record<string, string>,
|
||||
};
|
||||
@@ -361,7 +360,7 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
||||
await ensureCards();
|
||||
if (mine !== seq || !cards || !inputs) return;
|
||||
const label = !info && canCopy ? TEXT.importEdit : null;
|
||||
cards.relabel({ structure: label, area: label, material: label });
|
||||
cards.relabel({ structure: label, area: label });
|
||||
inputs.setEditLabel(label ?? "선택 입력 수정");
|
||||
cards.root.hidden = false;
|
||||
paintStatus(row, mine);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* =============================================================================
|
||||
* ui_template_item_picker_edit.ts
|
||||
* 항목 고르기 모달의 **편집 화면**(계획서 25-2d) — 카드 [수정] 이 모달 안에서 이 화면으로 바뀜.
|
||||
* 도면 = M02 CAD 부품 · 면적산출 · 재료산출 = M02 스프레드시트 · 선택 입력 = 값 받기 카드.
|
||||
* 도면 = M02 CAD 부품 · 면적산출 = M02 스프레드시트 · 선택 입력 = 값 받기 카드.
|
||||
* 고침은 **프로젝트 복사본**에만 씀(`ProjectItems.write`) — 마스터 · 회사 · 개인 원본은 안 바뀜.
|
||||
* [뒤로] = 선택지(나무 + 카드)로 · 저장 안 한 고침이 있으면 버릴지 물음.
|
||||
* 편집 부품은 이 화면을 열 때만 `import()`(B05 · B06 번들에 안 섞임).
|
||||
@@ -40,7 +40,6 @@ const TEXT = {
|
||||
"면적산출 알약 없음 — 선택 입력에서 자리를 면적산출로 이으면 여기 뜸(마스터 · 회사 · 개인 층에서)",
|
||||
drawing: "도면",
|
||||
area: "면적산출",
|
||||
material: "재료산출",
|
||||
inputs: "선택 입력",
|
||||
suffix: "{value} 수정",
|
||||
};
|
||||
@@ -61,7 +60,6 @@ export function createEditView(req: EditRequest, onBack: (saved: boolean) => voi
|
||||
const word = {
|
||||
structure: TEXT.drawing,
|
||||
area: TEXT.area,
|
||||
material: TEXT.material,
|
||||
inputs: TEXT.inputs,
|
||||
}[kind];
|
||||
const back = createButton({ label: TEXT.back, variant: "ghost", size: "sm" });
|
||||
@@ -96,28 +94,21 @@ export function createEditView(req: EditRequest, onBack: (saved: boolean) => voi
|
||||
body.replaceChildren(el("p", { className: "ui-item-picker__hint", text }));
|
||||
};
|
||||
|
||||
/** 복사본의 항목 연결 — 자리가 면적 · 재료인 입력이 그 시트 알약(25-1b · 키 모양은 `pillsOf`) */
|
||||
/** 복사본의 항목 연결 — 자리가 면적인 입력이 그 시트 알약(25-1b · 키 모양은 `pillsOf`) */
|
||||
const linkOf = async (): Promise<InputLinks | null> =>
|
||||
(await items.read(info.폴더, "입력연결")) as InputLinks | null;
|
||||
|
||||
async function sheetLinks(kindOf: "area" | "material"): Promise<LinkOptions> {
|
||||
const [list, link, { searchMaterials }] = await Promise.all([
|
||||
loadInputs(row),
|
||||
linkOf(),
|
||||
import("../M01_MasterData/M01_MasterData_Api_Fetch"),
|
||||
]);
|
||||
const 항목 = pillsOf(list, link, kindOf === "area" ? "면적" : "재료");
|
||||
if (kindOf === "area") {
|
||||
if (!항목.length) note.textContent = TEXT.noAreaPill;
|
||||
return { 항목, 모드: "입력" };
|
||||
}
|
||||
return { 항목, 모드: "자재", 자재찾기: searchMaterials };
|
||||
async function sheetLinks(): Promise<LinkOptions> {
|
||||
const [list, link] = await Promise.all([loadInputs(row), linkOf()]);
|
||||
const 항목 = pillsOf(list, link, "면적");
|
||||
if (!항목.length) note.textContent = TEXT.noAreaPill;
|
||||
return { 항목, 모드: "입력" };
|
||||
}
|
||||
|
||||
async function mountSheet(kindOf: "area" | "material"): Promise<void> {
|
||||
async function mountSheet(): Promise<void> {
|
||||
const [{ createSpreadsheet }, links] = await Promise.all([
|
||||
import("../A00_Common/spreadsheet/spreadsheet"),
|
||||
sheetLinks(kindOf),
|
||||
sheetLinks(),
|
||||
]);
|
||||
const host = el("div", { className: "ui-item-picker__edit-sheet" });
|
||||
body.replaceChildren(host);
|
||||
@@ -168,7 +159,7 @@ export function createEditView(req: EditRequest, onBack: (saved: boolean) => voi
|
||||
}
|
||||
if (kind === "structure") await mountDrawing();
|
||||
else if (kind === "inputs") await mountInputs();
|
||||
else await mountSheet(kind);
|
||||
else await mountSheet();
|
||||
} catch (error) {
|
||||
fail(fill(TEXT.loadFailed, why(error)));
|
||||
}
|
||||
|
||||
@@ -21,13 +21,12 @@ import {
|
||||
import { layerOf, ownerOf, type PickerRow } from "./ui_template_item_picker_data";
|
||||
|
||||
/** 복사본 폴더 안 부분 — 25-1b 약속 그대로 */
|
||||
export type ProjectPart = "도면" | "면적" | "재료" | "입력연결" | "입력값" | "고른자재";
|
||||
export type ProjectPart = "도면" | "면적" | "입력연결" | "입력값" | "고른자재";
|
||||
|
||||
/** 원본 종류 → 복사본 부분 */
|
||||
export const PART_OF: Record<"structure" | "area" | "material", ProjectPart> = {
|
||||
export const PART_OF: Record<"structure" | "area", ProjectPart> = {
|
||||
structure: "도면",
|
||||
area: "면적",
|
||||
material: "재료",
|
||||
};
|
||||
|
||||
export interface ProjectItemInfo {
|
||||
|
||||
@@ -13,7 +13,6 @@ export const ui_locales_m2 = {
|
||||
M02_KindDrawing: ["도면 양식", "Drawing templates"],
|
||||
M02_KindStructure: ["구조물 도면", "Structure drawings"],
|
||||
M02_KindArea: ["면적산출", "Area takeoff"],
|
||||
M02_KindMaterial: ["재료산출", "Material takeoff"],
|
||||
M02_NoTemplates: ["양식 없음", "None"],
|
||||
M02_New: ["새로", "New"],
|
||||
M02_Copy: ["본떠 만들기", "Copy as new"],
|
||||
@@ -87,7 +86,6 @@ export const ui_locales_m2 = {
|
||||
M02_PickerLoadFailed: ["항목을 못 읽음 — {value}", "Failed to load items — {value}"],
|
||||
M02_PickerDrawing: ["도면", "Drawing"],
|
||||
M02_PickerArea: ["면적산출", "Area takeoff"],
|
||||
M02_PickerMaterial: ["재료산출", "Material takeoff"],
|
||||
M02_PickerUnitPrice: ["일위대가", "Unit price"],
|
||||
M02_PickerUnit: ["단위", "Unit"],
|
||||
M02_PickerNone: ["없음", "None"],
|
||||
|
||||
Reference in New Issue
Block a user