Merge remote-tracking branches 'origin/sub_laptop_3', 'origin/sub_laptop_5' and 'origin/sub_laptop_8' into sub_laptop_2
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""면적산출 범위 → 도면 표(PLAN 41-1) — 변환은 Node 번들 한 벌(`common_util_spreadsheet_node.ts`
|
||||
`areaTable` · 화면 표시 글과 같은 코드) · 여기는 부르기 · 도면 표 맞춤만(파이썬 이식 없음).
|
||||
|
||||
도면 문서(`structure/<열>.json`) 맨 위 `연결표` = `[{연결, 시트, 범위, 글자크기, 값}]` — 도면 표
|
||||
`shapeData.style.연결` 이 `연결` 과 같은 표를 면적 [저장] 때 칸 글로 다시 적음(자리 · 크기 그대로 ·
|
||||
칸 수가 바뀌면 열 너비 · 행 높이도 새로) · `값` = 마지막 칸 글(`{시트id!A1: 글}` · 다름 알림용).
|
||||
도면에 그 표가 없으면 기록은 그대로 둠. 층 = `store.at(층 폴더)` · 권한은 부르는 쪽.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from common_util import common_util_spreadsheet as sheet_node
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
LINKS = "연결표"
|
||||
|
||||
|
||||
def to_table(column: str, sheet: str, rng: str, font: float | None = None) -> dict[str, Any]:
|
||||
"""그 열 면적산출(저장본) 시트 · 범위 → `{표, 시트, 범위, 값}` — 저장 안 함 · 못 바꾸면 400."""
|
||||
book = store.read("area", column)["문서"]
|
||||
try:
|
||||
got = sheet_node.area_tables([{"book": book, "시트": sheet, "범위": rng, "글자크기": font}])
|
||||
except sheet_node.RecalcError as e:
|
||||
raise store.StoreError(503, str(e)) from e
|
||||
if not got[0].get("ok"):
|
||||
raise store.StoreError(400, got[0].get("까닭"))
|
||||
return {k: got[0][k] for k in ("표", "시트", "범위", "값")}
|
||||
|
||||
|
||||
def _style_link(entity: Any) -> Any:
|
||||
if not isinstance(entity, dict) or entity.get("type") != "Table":
|
||||
return None
|
||||
return ((entity.get("shapeData") or {}).get("style") or {}).get("연결")
|
||||
|
||||
|
||||
def refresh_drawing(column: str) -> list[str]:
|
||||
"""지금 층 그 열 도면의 연결표마다 칸 글 다시 · 마지막 값 갱신 → 칸을 간 표의 `연결` 목록."""
|
||||
try:
|
||||
drawing, area = store._path("structure", column), store._path("area", column)
|
||||
except store.StoreError:
|
||||
return []
|
||||
if not drawing.is_file() or not area.is_file():
|
||||
return []
|
||||
doc = json.loads(drawing.read_bytes())
|
||||
links = [x for x in doc.get(LINKS) or [] if isinstance(x, dict) and x.get("연결")]
|
||||
if not links:
|
||||
return []
|
||||
book = json.loads(area.read_bytes())
|
||||
asks = [
|
||||
{"book": book, "시트": x.get("시트"), "범위": x.get("범위"), "글자크기": x.get("글자크기")}
|
||||
for x in links
|
||||
]
|
||||
try:
|
||||
results = sheet_node.area_tables(asks)
|
||||
except sheet_node.RecalcError as e:
|
||||
logger.warning("도면 표 맞춤 건너뜀 %s — %s", column, e)
|
||||
return []
|
||||
entities = (doc.get("도면") or {}).get("entities") or []
|
||||
done: list[str] = []
|
||||
for link, got in zip(links, results):
|
||||
if not got.get("ok"):
|
||||
continue
|
||||
link["값"] = got["값"]
|
||||
new = got["표"]["shapeData"]
|
||||
for entity in entities:
|
||||
if _style_link(entity) != link["연결"]:
|
||||
continue
|
||||
shape = entity["shapeData"]
|
||||
if (len(shape.get("rowHeights") or []), len(shape.get("columnWidths") or [])) != (
|
||||
len(new["rowHeights"]),
|
||||
len(new["columnWidths"]),
|
||||
):
|
||||
shape["rowHeights"], shape["columnWidths"] = new["rowHeights"], new["columnWidths"]
|
||||
shape["cells"] = new["cells"]
|
||||
done.append(link["연결"])
|
||||
with store._LOCK:
|
||||
atomic_write_json(drawing, doc)
|
||||
return done
|
||||
@@ -67,7 +67,7 @@ def clean(doc: Any) -> dict[str, Any]:
|
||||
whole = m01.loaded()[1]
|
||||
lines, seen = [], set()
|
||||
for at, one in enumerate(doc.get("담은로직") or [], start=1):
|
||||
where = f"담은 로직 {at}째"
|
||||
where = f"단가산출 로직 {at}째"
|
||||
if not isinstance(one, dict):
|
||||
raise _bad(f"{where} · 묶음 아님")
|
||||
code = str(one.get("로직") or "")
|
||||
|
||||
@@ -127,7 +127,7 @@ const VALUE_TEXT = {
|
||||
title: "넣을 값",
|
||||
missing: "빠진 로직 {value}",
|
||||
edit: "넣을 값 수정",
|
||||
none: "넣을 값이 없음 — 담은 로직에 입력이 없음",
|
||||
none: "넣을 값이 없음 — 단가산출 로직에 입력이 없음",
|
||||
auto: "자동값",
|
||||
unconfirmed: "확인 안 함",
|
||||
confirmed: "✓ 확인함",
|
||||
|
||||
@@ -135,6 +135,14 @@ export interface StructureDoc {
|
||||
열: string;
|
||||
도번: string;
|
||||
도면: { entities: unknown[] } & Record<string, unknown>;
|
||||
/** 면적산출 → 도면 표 연결 기록(41 · `AreaToDrawing` · `M02_Area_Table`) — 표 style.연결 = 연결 · 값 = 마지막 칸 글 */
|
||||
연결표?: {
|
||||
연결: string;
|
||||
시트: string;
|
||||
범위: string;
|
||||
글자크기: number;
|
||||
값: Record<string, string>;
|
||||
}[];
|
||||
}
|
||||
|
||||
/** 시스템 층 구조물 도면 목록 */
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_AreaToDrawing.ts
|
||||
* 면적산출 범위 → 도면 표(PLAN 41-3) — 편집 칸(`UI_Main`)은 갈고리만 · 몸은 여기.
|
||||
* ① 면적 상세 머리 [도면으로 보내기] — 고른 범위 · 저장 안 한 고침은 먼저 저장 물음 · 작은 창 글자 크기(기본 2.5)
|
||||
* → 서버 표 변환 길(`POST /layers/{층}/area/{열}/table` · 41-1)로 표 객체 받음 → 같은 페이지에서 그 열
|
||||
* 도면 상세로 옮기며 넘김(닫힘 변수 · 캐시 · 서버 안 씀) → 도면 손잡이 `placeTable`(놓기 도구 · 41-2)
|
||||
* ② 도면 [저장] — 문서 맨 위 `연결표[]` = {연결(T1…), 시트, 범위, 글자크기, 값(마지막 칸 글 `{시트!A1: 글}`)} ·
|
||||
* 표 style.연결 = 연결 · 도면에서 지운 표는 기록에서 뺌(면적 [저장] 때 서버가 칸 글 · 값을 다시 적음 · 41-1)
|
||||
* ③ 도면을 열 때 — 연결표마다 지금 면적 값과 마지막 값이 다르면 알림 한 줄 + [업데이트](`updateTable`)
|
||||
* ④ 표 여러 개 — id 마다 한 줄
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
el,
|
||||
showConfirmDialog,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { getActiveOwner, type Layer, type StructureDoc } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import type { Selection } from "./M02_MasterTemplete_UI_Main";
|
||||
|
||||
/** 표 칸 하나(CAD `TableCell` 꼴에서 쓰는 것만) */
|
||||
interface TableCell {
|
||||
text?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
/** CAD 표 객체 JSON(`TableEntity.toJson` 꼴에서 쓰는 것만) */
|
||||
interface TableEntityJson {
|
||||
type: string;
|
||||
shapeData: { cells: (TableCell | null)[][]; style?: Record<string, unknown> } & Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
/** 연결 기록 한 줄 — 도면 문서 맨 위 `연결표[]`(값 = 마지막 칸 글 · 다름 알림 기준) */
|
||||
export type TableLink = NonNullable<StructureDoc["연결표"]>[number];
|
||||
type LinkSpec = Pick<TableLink, "시트" | "범위" | "글자크기">;
|
||||
/** 표 변환 길 답(41-1) — 시트 = 시트 id */
|
||||
interface AreaTable {
|
||||
표: TableEntityJson;
|
||||
시트: string;
|
||||
범위: string;
|
||||
값: Record<string, string>;
|
||||
}
|
||||
|
||||
/** 도면 손잡이에 더해지는 표 함수(41-2 · `DrawingTemplateHandle`) — 없으면 놓기 · 알림이 빠짐 */
|
||||
interface TableHandle {
|
||||
placeTable?: (entity: Record<string, unknown>, link: string) => void;
|
||||
updateTable?: (link: string, cells: unknown[][]) => Promise<number>;
|
||||
}
|
||||
|
||||
const TEXT = {
|
||||
send: "도면으로 보내기",
|
||||
sendTip: "고른 범위를 이 열 도면에 표로 넣음 — 놓을 자리는 도면에서 누름",
|
||||
title: "도면으로 보내기",
|
||||
range: "범위",
|
||||
size: "글자 크기",
|
||||
ok: "보내기",
|
||||
close: "닫기",
|
||||
badSize: "글자 크기는 0 보다 큰 수",
|
||||
saveFirst: "저장 안 한 고침이 있음 — 저장하고 보낼까요?",
|
||||
saveLabel: "저장하고 보내기",
|
||||
failed: "표를 못 만듦 — {value}",
|
||||
place: "도면에서 표를 놓을 자리를 누름(Esc = 그만)",
|
||||
noPlace: "이 도면 화면은 표 놓기를 아직 못 함",
|
||||
stale: "면적산출 값이 바뀐 표 {n}개",
|
||||
update: "업데이트",
|
||||
updated: "표 {n}개를 지금 면적 값으로 바꿈 — [저장]하면 남음",
|
||||
} as const;
|
||||
|
||||
const SIZE = 2.5; // 표 글자 크기 기본(정함 41)
|
||||
const LINK = "연결"; // 표 style 안 표지 칸
|
||||
|
||||
const fill = (text: string, key: string, value: string | number): string =>
|
||||
text.replace(`{${key}}`, String(value));
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
||||
const enc = encodeURIComponent;
|
||||
|
||||
/** 범위 → 표 객체 · 칸 글(41-1 서버 · 저장본 면적 · 저장 안 함) — 지금 보는 층 · 주인 */
|
||||
async function fetchTable(layer: Layer, column: string, spec: LinkSpec): Promise<AreaTable> {
|
||||
const owner = getActiveOwner();
|
||||
const q = owner && layer !== "system" && layer !== "project" ? `?owner=${enc(owner)}` : "";
|
||||
const res = await fetch(`${API_BASE_URL}/m02/layers/${layer}/area/${enc(column)}/table${q}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ 시트: spec.시트, 범위: spec.범위, 글자크기: spec.글자크기 }),
|
||||
});
|
||||
const data = (await res.json().catch(() => ({}))) as { detail?: unknown } & Partial<AreaTable>;
|
||||
if (!res.ok || !data.표) {
|
||||
const d = data.detail;
|
||||
throw new Error(typeof d === "string" ? d : `HTTP ${res.status}`);
|
||||
}
|
||||
return data as AreaTable;
|
||||
}
|
||||
|
||||
const isTable = (e: unknown): e is TableEntityJson =>
|
||||
!!e && typeof e === "object" && (e as { type?: unknown }).type === "Table";
|
||||
const linkOf = (e: TableEntityJson): string | null => {
|
||||
const id = e.shapeData?.style?.[LINK];
|
||||
return typeof id === "string" && id ? id : null;
|
||||
};
|
||||
/** 칸 글 묶음 견줌 — 열쇠 차례와 무관 */
|
||||
const sameValues = (a: Record<string, string> = {}, b: Record<string, string> = {}): boolean => {
|
||||
const ka = Object.keys(a);
|
||||
return ka.length === Object.keys(b).length && ka.every((k) => a[k] === b[k]);
|
||||
};
|
||||
|
||||
export interface AreaToDrawingOptions {
|
||||
/** 같은 페이지에서 그 열 도면 상세를 엶(`UI_Main.open`) */
|
||||
open: (sel: Selection) => Promise<void>;
|
||||
/** 지금 화면 [저장] */
|
||||
save: () => Promise<void>;
|
||||
/** 저장 안 한 고침이 남았나 */
|
||||
isDirty: () => boolean;
|
||||
/** 편집 칸 알림 줄 */
|
||||
showNotice: (nodes: (HTMLElement | string)[]) => void;
|
||||
}
|
||||
|
||||
export interface AreaToDrawing {
|
||||
/** 면적 시트 고름(`onSelect`) — 범위를 들고 있음 */
|
||||
select: (s: { 시트: string; 범위: string }) => void;
|
||||
/** 면적 상세가 섰음 — 머리 칸에 [도면으로 보내기](고칠 수 있는 층 · 프로젝트 층 아님) */
|
||||
onArea: (at: Selection, readOnly: boolean, slot: HTMLElement) => void;
|
||||
/** 도면 상세가 섰음 — 넘겨받은 표 놓기 · 연결표 다름 알림 */
|
||||
onDrawing: (handle: unknown, at: Selection, doc: StructureDoc) => void;
|
||||
/** 도면 저장 문서 — 맨 위 `연결표` 를 도면의 표에 맞춤 */
|
||||
withLinks: (doc: StructureDoc) => StructureDoc;
|
||||
}
|
||||
|
||||
export function createAreaToDrawing(opt: AreaToDrawingOptions): AreaToDrawing {
|
||||
let range: { 시트: string; 범위: string } | null = null;
|
||||
/** 면적 상세 → 도면 상세로 넘길 표(같은 열 · 같은 층일 때만 놓음) */
|
||||
let pending: { layer: Layer; column: string; got: AreaTable; 글자크기: number } | null = null;
|
||||
/** 이 도면에서 새로 놓은 표 · [업데이트]로 바뀐 값(저장 때 기록) · 열어 둔 도면 */
|
||||
let fresh = new Map<string, TableLink>();
|
||||
let latest = new Map<string, Record<string, string>>();
|
||||
let shown: string | null = null;
|
||||
|
||||
/** 글자 크기 작은 창 — 보내기면 크기 · 닫으면 null */
|
||||
const askSize = (spec: { 범위: string }): Promise<number | null> =>
|
||||
new Promise((resolve) => {
|
||||
let done = false;
|
||||
const finish = (v: number | null): void => {
|
||||
if (!done) resolve(v);
|
||||
done = true;
|
||||
};
|
||||
const size = createInputField({ label: TEXT.size, type: "number", value: String(SIZE) });
|
||||
size.input.step = "0.5";
|
||||
size.input.min = "0.1";
|
||||
const { close } = openModal({
|
||||
title: TEXT.title,
|
||||
closeLabel: TEXT.close,
|
||||
dialogClass: "m02-modal",
|
||||
beforeClose: async () => (finish(null), true),
|
||||
mount: (body) => {
|
||||
const ok = createButton({ label: TEXT.ok, variant: "filled" });
|
||||
ok.addEventListener("click", () => {
|
||||
const v = Number(size.input.value);
|
||||
if (!(v > 0)) return showToast(TEXT.badSize, "error");
|
||||
finish(v);
|
||||
close();
|
||||
});
|
||||
size.input.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" && !event.isComposing) ok.click();
|
||||
});
|
||||
body.append(el("p", { text: `${TEXT.range} ${spec.범위}` }), size.root, ok);
|
||||
size.input.select();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
async function send(at: Selection): Promise<void> {
|
||||
if (!range) return;
|
||||
if (opt.isDirty()) {
|
||||
if (!(await showConfirmDialog(TEXT.saveFirst, TEXT.saveLabel))) return;
|
||||
await opt.save();
|
||||
if (opt.isDirty()) return; // 저장 실패 — 알림은 저장 길이 띄움
|
||||
}
|
||||
const picked = range;
|
||||
const size = await askSize(picked);
|
||||
if (size === null) return;
|
||||
const spec: LinkSpec = { 시트: picked.시트, 범위: picked.범위, 글자크기: size };
|
||||
let got: AreaTable;
|
||||
try {
|
||||
got = await fetchTable(at.layer, at.name, spec);
|
||||
} catch (error) {
|
||||
return showToast(fill(TEXT.failed, "value", why(error)), "error");
|
||||
}
|
||||
pending = { layer: at.layer, column: at.name, got, 글자크기: size };
|
||||
await opt.open({
|
||||
layer: at.layer,
|
||||
projectId: null,
|
||||
kind: "structure",
|
||||
name: at.name,
|
||||
label: at.label,
|
||||
});
|
||||
}
|
||||
|
||||
/** 다음 연결 id — 기록 · 도면 표지에 없는 가장 작은 T번호 */
|
||||
const nextId = (doc: StructureDoc, taken: Set<string>): string => {
|
||||
for (const row of doc.연결표 ?? []) taken.add(row.연결);
|
||||
for (const e of doc.도면?.entities ?? []) if (isTable(e)) taken.add(linkOf(e) ?? "");
|
||||
let n = 1;
|
||||
while (taken.has(`T${n}`)) n += 1;
|
||||
return `T${n}`;
|
||||
};
|
||||
|
||||
/** 연결표마다 지금 면적 값 → 마지막 값과 다르면 알림 + [업데이트] */
|
||||
async function checkStale(handle: TableHandle, at: Selection, doc: StructureDoc): Promise<void> {
|
||||
const rows = doc.연결표 ?? [];
|
||||
if (!rows.length || !handle.updateTable) return;
|
||||
const key = `${at.layer}:${at.name}`;
|
||||
const got = await Promise.all(
|
||||
rows.map((row) =>
|
||||
fetchTable(at.layer, at.name, row).then(
|
||||
(e) => ({ row, e }),
|
||||
() => null,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (shown !== key) return; // 그 사이 다른 화면
|
||||
const stale = got.filter(
|
||||
(x): x is { row: TableLink; e: AreaTable } => !!x && !sameValues(x.row.값, x.e.값),
|
||||
);
|
||||
if (!stale.length) return;
|
||||
const update = createButton({ label: TEXT.update, variant: "filled", size: "sm" });
|
||||
update.addEventListener("click", async () => {
|
||||
update.disabled = true;
|
||||
let n = 0;
|
||||
for (const { row, e } of stale) {
|
||||
const changed = (await handle.updateTable?.(row.연결, e.표.shapeData.cells)) ?? 0;
|
||||
if (changed) latest.set(row.연결, e.값);
|
||||
n += changed ? 1 : 0;
|
||||
}
|
||||
if (shown !== key) return;
|
||||
opt.showNotice([]);
|
||||
showToast(fill(TEXT.updated, "n", n), "success");
|
||||
});
|
||||
opt.showNotice([fill(TEXT.stale, "n", stale.length), update]);
|
||||
}
|
||||
|
||||
return {
|
||||
select: (s) => (range = { 시트: s.시트, 범위: s.범위 }),
|
||||
onArea: (at, readOnly, slot) => {
|
||||
range = null;
|
||||
if (readOnly || at.layer === "project") return;
|
||||
const button = createButton({ label: TEXT.send, variant: "ghost", size: "sm" });
|
||||
button.title = TEXT.sendTip;
|
||||
button.dataset.areaToDrawing = "";
|
||||
button.addEventListener("click", () => void send(at));
|
||||
slot.append(button);
|
||||
},
|
||||
onDrawing: (raw, at, doc) => {
|
||||
const handle = raw as TableHandle;
|
||||
shown = `${at.layer}:${at.name}`;
|
||||
fresh = new Map();
|
||||
latest = new Map();
|
||||
const give =
|
||||
pending && pending.layer === at.layer && pending.column === at.name ? pending : null;
|
||||
pending = null;
|
||||
if (give) {
|
||||
if (!handle.placeTable) showToast(TEXT.noPlace, "error");
|
||||
else {
|
||||
const id = nextId(doc, new Set());
|
||||
const { 표, 시트, 범위, 값 } = give.got;
|
||||
const style = { ...(표.shapeData.style ?? {}), [LINK]: id };
|
||||
fresh.set(id, { 연결: id, 시트, 범위, 글자크기: give.글자크기, 값 });
|
||||
handle.placeTable({ ...표, shapeData: { ...표.shapeData, style } }, id);
|
||||
showToast(TEXT.place, "info");
|
||||
}
|
||||
}
|
||||
void checkStale(handle, at, doc);
|
||||
},
|
||||
withLinks: (doc) => {
|
||||
const old = new Map((doc.연결표 ?? []).map((row) => [row.연결, row]));
|
||||
const rows: TableLink[] = [];
|
||||
for (const e of doc.도면?.entities ?? []) {
|
||||
if (!isTable(e)) continue;
|
||||
const id = linkOf(e);
|
||||
const row = id ? (old.get(id) ?? fresh.get(id)) : undefined;
|
||||
if (!id || !row || rows.some((r) => r.연결 === id)) continue;
|
||||
rows.push({ ...row, 값: latest.get(id) ?? row.값 });
|
||||
}
|
||||
// 기록이 없던 도면은 칸을 안 만듦(옛 문서 그대로)
|
||||
return rows.length || doc.연결표 ? { ...doc, 연결표: rows } : doc;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -77,6 +77,9 @@ function migrateOldRecovery(options: DrawingTemplateOptions, scope: string): voi
|
||||
|
||||
export interface DrawingTemplateHandle {
|
||||
getDoc: () => Promise<DrawingTemplateDoc>;
|
||||
/** 표 놓기 · 칸 갈기(41장) — `cad_host` 그대로 */
|
||||
placeTable: (entity: Record<string, unknown>, link: string) => void;
|
||||
updateTable: (link: string, cells: unknown[][]) => Promise<number>;
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
@@ -277,6 +280,8 @@ export function mountDrawingTemplate(
|
||||
|
||||
return {
|
||||
getDoc,
|
||||
placeTable: cad.placeTable,
|
||||
updateTable: cad.updateTable,
|
||||
destroy: () => {
|
||||
areaModal?.close();
|
||||
cad.destroy();
|
||||
|
||||
@@ -94,7 +94,7 @@ export const ITEM_COLS: Record<ListKind, Col<ItemRow>[]> = {
|
||||
NAME,
|
||||
FOLDER,
|
||||
{ head: "단위", get: (r) => r.labor?.단위 ?? "" }, // 일위대가 문서 단위(39-8 B3)
|
||||
{ head: "담은 로직 수", get: (r) => (r.labor ? String(r.labor.로직수) : DASH) },
|
||||
{ head: "단가산출 로직 수", get: (r) => (r.labor ? String(r.labor.로직수) : DASH) },
|
||||
{ head: "합계", get: (r) => laborSum(r).text, note: (r) => laborSum(r).note },
|
||||
changed("labor"),
|
||||
],
|
||||
|
||||
@@ -62,7 +62,7 @@ const PRACTICE_COLS: Col<ComboSummary>[] = [
|
||||
{ head: "구분", get: (c) => c.구분 ?? "" },
|
||||
{ head: "상세구분", get: (c) => c.상세구분 ?? "" },
|
||||
{ head: "단위", get: (c) => c.단위 ?? "" },
|
||||
{ head: "담은 로직 수", get: (c) => String(c.count) },
|
||||
{ head: "단가산출 로직 수", get: (c) => String(c.count) },
|
||||
];
|
||||
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
||||
|
||||
@@ -381,10 +381,11 @@ export function compactTable(data: CompactData): HTMLElement {
|
||||
text: wait ? "…" : v === null || v === undefined ? "—" : formatMoney(v),
|
||||
attrs: { "data-label": label, ...(base ? { title: tc("Labor_Base") } : {}) },
|
||||
});
|
||||
// 머리 글 = 사용자 글 그대로(40-1 · 항목 · 수량산출 · 단위 · 노무비 · 재료비 · 경비 · 합계)
|
||||
const heads = [
|
||||
tc("Col_Name"),
|
||||
tc("Labor_Item"),
|
||||
tc("Labor_QtyCol"),
|
||||
tc("Col_Unit"),
|
||||
tc("Labor_Unit"),
|
||||
...COSTS.map((c) => tc(HEAD[c])),
|
||||
];
|
||||
const lines = data.rows.map((row) => {
|
||||
|
||||
@@ -57,6 +57,7 @@ const TEXT = {
|
||||
Labor_AddMat: ["자재 추가", "Add material"],
|
||||
Labor_Qty: ["수량", "Quantity"],
|
||||
Labor_QtyCol: ["수량산출", "Quantity"],
|
||||
Labor_Item: ["항목", "Item"],
|
||||
Labor_Hand: ["손", "Manual"],
|
||||
Labor_QtyPick: ["누르면 손 입력 · 엑셀에서 고르기", "Manual or pick a cell"],
|
||||
In_QtyPick: ["엑셀에서 고르기", "Pick in sheet"],
|
||||
|
||||
@@ -317,6 +317,29 @@ def _copy_practice(column: str, key: str) -> dict[str, Any]:
|
||||
return store.copy_from("labor", column, key, body=labor.from_combo(key))
|
||||
|
||||
|
||||
class AreaTableBody(BaseModel):
|
||||
"""면적산출 범위 → 도면 표(41-1) — `시트` = 시트 id · 이름 · `글자크기` 기본 2.5."""
|
||||
|
||||
시트: str
|
||||
범위: str
|
||||
글자크기: float | None = None
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/area/{column}/table")
|
||||
def post_area_table(
|
||||
layer: Layer,
|
||||
column: str,
|
||||
body: AreaTableBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""그 열 면적산출(저장본) 범위 → CAD 표 `{표, 시트, 범위, 값}` — 저장 안 함 · 층 읽기 권한."""
|
||||
from M02_MasterTemplete import M02_Area_Table as area_table
|
||||
|
||||
folder = _root(session, layer, owner)
|
||||
return _call(folder, area_table.to_table, column, body.시트, body.범위, body.글자크기)
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/{kind}/{column}/clear")
|
||||
def post_clear(
|
||||
layer: Layer,
|
||||
|
||||
@@ -361,9 +361,18 @@ async def save_layer_template(
|
||||
) from error
|
||||
except ValueError as error:
|
||||
raise _bad(error) from error
|
||||
if kind == "area": # 그 열 도면 연결표 칸 글도(41-1 · 시스템 · 회사 · 개인 층은 Store.write 가)
|
||||
await asyncio.to_thread(_refresh_drawing, folder, name)
|
||||
return {"종류": kind, "이름": name, "판": version, "층": layer}
|
||||
|
||||
|
||||
def _refresh_drawing(folder: Path, column: str) -> None:
|
||||
from M02_MasterTemplete import M02_Area_Table as area_table
|
||||
|
||||
with store.at(folder):
|
||||
area_table.refresh_drawing(column)
|
||||
|
||||
|
||||
def _store_save(folder: Path, kind: str, name: str, version: str, doc: Any, layer: str) -> dict:
|
||||
with store.at(folder):
|
||||
try:
|
||||
|
||||
@@ -170,6 +170,7 @@ def write(kind: str, name: str, version: str, doc: Any, screen: bool = False) ->
|
||||
info = _info(kind, path)
|
||||
if kind == "area":
|
||||
_refresh_labor(name) # 면적 칸 값이 바뀌면 일위대가 계산값(목록 합계)도
|
||||
_refresh_drawing(name) # 도면 연결표 칸 글도(41-1)
|
||||
if kind == "table" and name == STRUCTURE_TABLE:
|
||||
for column, source in origins.items(): # 빈 파일 채우기 앞 — 원본 내용으로 만듦
|
||||
for part in _KEY:
|
||||
@@ -362,6 +363,7 @@ def clear(kind: str, column: str) -> dict[str, Any]:
|
||||
info = _info(kind, _path(kind, column))
|
||||
if kind == "area":
|
||||
_refresh_labor(column)
|
||||
_refresh_drawing(column)
|
||||
sync_combos()
|
||||
return info
|
||||
|
||||
@@ -391,6 +393,7 @@ def copy_from(
|
||||
info = _info(kind, _path(kind, column))
|
||||
if kind == "area":
|
||||
_refresh_labor(column)
|
||||
_refresh_drawing(column)
|
||||
return info
|
||||
|
||||
|
||||
@@ -482,6 +485,13 @@ def _server(kind: str, doc: Any) -> Any:
|
||||
return doc
|
||||
|
||||
|
||||
def _refresh_drawing(column: str) -> None:
|
||||
"""그 열 도면의 면적 연결표 칸 글을 지금 면적 값으로(`M02_Area_Table` · 41-1)."""
|
||||
from M02_MasterTemplete import M02_Area_Table as area_table
|
||||
|
||||
area_table.refresh_drawing(column)
|
||||
|
||||
|
||||
def _refresh_labor(column: str) -> None:
|
||||
"""그 열 일위대가 `계산값` 을 지금 면적 값으로 다시 적음 — 문서가 틀어졌으면 그대로 둠."""
|
||||
path = _path("labor", column)
|
||||
|
||||
@@ -51,7 +51,7 @@ const TEXT = {
|
||||
pickCopy: "{value} 복사해 가져오기",
|
||||
copied: "복사해 가져옴",
|
||||
copyFailed: "복사 실패 — {value}",
|
||||
noPills: "이 열 일위대가에 담은 로직 · 자재가 없어 알약 없음 — 일위대가 상세에서 담음",
|
||||
noPills: "이 열 일위대가에 단가산출 로직 · 자재가 없어 알약 없음 — 일위대가 상세에서 담음",
|
||||
pillsFailed: "일위대가 알약을 못 받음 — {value}",
|
||||
discardCards: "카드(도면 · 면적산출)에 저장 안 한 고침이 있음 — 버리고 넘어갈까?",
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from "./M02_MasterTemplete_Structure";
|
||||
import { askSettle } from "./M02_MasterTemplete_TreePanel_Dialogs";
|
||||
import { buildAreaPanel, type AreaPanel } from "./M02_MasterTemplete_AreaPanel";
|
||||
import { createAreaToDrawing } from "./M02_MasterTemplete_AreaToDrawing";
|
||||
import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet";
|
||||
import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links";
|
||||
export interface Selection {
|
||||
@@ -220,6 +221,9 @@ export function createMain(
|
||||
back.hidden = !sel || !(sel.kind in NUMBER_FIELD);
|
||||
};
|
||||
|
||||
// 면적산출 범위 → 도면 표(41-3) — 몸은 `AreaToDrawing` · 여기는 갈고리만
|
||||
const toDrawing = createAreaToDrawing({ open, save: doSave, isDirty: busy, showNotice });
|
||||
|
||||
const drop = (): void => {
|
||||
editor?.destroy();
|
||||
editor = null;
|
||||
@@ -275,12 +279,13 @@ export function createMain(
|
||||
dirty = !readOnly;
|
||||
apanel?.refresh(); // 칸 값 · 연결이 바뀜 — 반영 금액을 바로 다시 곱함
|
||||
},
|
||||
onSelect: (s) => (picked = s.칸),
|
||||
onSelect: (s) => ((picked = s.칸), toDrawing.select(s)),
|
||||
links,
|
||||
},
|
||||
);
|
||||
editor = handle;
|
||||
sheetHandle = handle;
|
||||
toDrawing.onArea(at, readOnly, slot); // 머리 [도면으로 보내기]
|
||||
if (at.layer === "project") return;
|
||||
if (kind === "area") {
|
||||
// 오른쪽 일위대가 패널 — 담은 로직 + 자재목록(기준 · 반영) · 줄 누름 = 칸 지정 모달
|
||||
@@ -411,6 +416,7 @@ export function createMain(
|
||||
originMarker: at.kind === "structure",
|
||||
noArea: at.kind === "structure",
|
||||
});
|
||||
if (at.kind === "structure") toDrawing.onDrawing(editor, at, doc as StructureDoc);
|
||||
return;
|
||||
}
|
||||
empty(L("M02_NoDrawing"));
|
||||
@@ -470,7 +476,10 @@ export function createMain(
|
||||
// 면적산출은 스프레드시트 문서로 통째 갈아 끼우되 번호는 계약 밖(엔진이 모름)이라 옮겨 붙임.
|
||||
const doc =
|
||||
at.kind === "structure" && loaded
|
||||
? { ...(loaded as StructureDoc), 도면: edited as StructureDoc["도면"] }
|
||||
? toDrawing.withLinks({
|
||||
...(loaded as StructureDoc),
|
||||
도면: edited as StructureDoc["도면"],
|
||||
})
|
||||
: at.kind === "area" && loaded
|
||||
? { ...(loaded as Record<string, unknown>), ...(edited as Record<string, unknown>) }
|
||||
: edited;
|
||||
|
||||
Reference in New Issue
Block a user