Merge remote-tracking branches 'origin/sub_laptop_2', 'origin/sub_laptop_3', 'origin/sub_laptop_7' and 'origin/sub_laptop_8' into sub_laptop_5

This commit is contained in:
2026-10-04 22:45:48 +09:00
27 changed files with 814 additions and 16 deletions
+84
View File
@@ -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
+1 -1
View File
@@ -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 "")
+1 -1
View File
@@ -127,7 +127,7 @@ const VALUE_TEXT = {
title: "넣을 값",
missing: "빠진 로직 {value}",
edit: "넣을 값 수정",
none: "넣을 값이 없음 — 담은 로직에 입력이 없음",
none: "넣을 값이 없음 — 단가산출 로직에 입력이 없음",
auto: "자동값",
unconfirmed: "확인 안 함",
confirmed: "✓ 확인함",
@@ -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: "카드(도면 · 면적산출)에 저장 안 한 고침이 있음 — 버리고 넘어갈까?",
};