Merge remote-tracking branch 'origin/sub_laptop_3' into sub_laptop_2
This commit is contained in:
@@ -138,6 +138,8 @@ export interface StructureCombo extends StructureNumbers {
|
||||
분류3: string | null;
|
||||
단위: string | null;
|
||||
일위대가: string | null;
|
||||
/** 자재 조합 키 `UM` */
|
||||
자재조합: string | null;
|
||||
바인딩: unknown;
|
||||
펼침: unknown;
|
||||
}
|
||||
@@ -153,6 +155,59 @@ export interface StructureCombos {
|
||||
/** 조합 표 — 서버가 집계표 [저장] 때 맞춤(읽기만) */
|
||||
export const fetchStructureCombos = (): Promise<StructureCombos> => call("/structure-combos");
|
||||
|
||||
/** 조합 갈라 내기 결과 — `새로만듦` false = 같은 담은 로직의 조합이 이미 있어 그 키를 가리킴 */
|
||||
export interface BranchedCombo {
|
||||
키: string;
|
||||
이름: string;
|
||||
새로만듦: boolean;
|
||||
집계표판: string;
|
||||
}
|
||||
|
||||
/** 이 열이 가리키는 일위대가 조합을 고친 담은 로직으로 갈라 냄 — 새 UA · 집계표 그 열만 새 키 ·
|
||||
* `판`(집계표 판)을 주면 어긋날 때 StaleError · 원래 조합은 그대로 */
|
||||
export const branchCombo = (
|
||||
column: string,
|
||||
body: {
|
||||
본뜬키: string;
|
||||
담은로직: { 로직: string; 메모?: string | null }[];
|
||||
이름: string;
|
||||
판?: string;
|
||||
},
|
||||
): Promise<BranchedCombo> =>
|
||||
call(`/structures/${enc(column)}/combo`, { method: "POST", body: JSON.stringify(body) });
|
||||
|
||||
/** 자재 조합(키 `UM`) 담은 자재 한 줄 */
|
||||
export interface MaterialItem {
|
||||
코드: string;
|
||||
이름: string | null;
|
||||
규격: string | null;
|
||||
단위: string | null;
|
||||
}
|
||||
|
||||
/** 자재 조합 한 줄 — 본뜬키 = 갈라 낸 원래 키(없으면 null) */
|
||||
export interface MaterialCombo {
|
||||
키: string;
|
||||
이름: string;
|
||||
담은자재: MaterialItem[];
|
||||
본뜬키: string | null;
|
||||
소유: string;
|
||||
비고: string | null;
|
||||
}
|
||||
|
||||
export const fetchMaterialCombo = (key: string): Promise<MaterialCombo> =>
|
||||
call(`/material-combos/${enc(key)}`);
|
||||
|
||||
/** 이 열의 자재 조합을 고친 담은 자재로 갈라 냄 — 규칙은 `branchCombo` 와 같음 ·
|
||||
* `본뜬키` 빈 글 = 이 열의 첫 자재 조합 */
|
||||
export const branchMaterialCombo = (
|
||||
column: string,
|
||||
body: { 본뜬키?: string; 담은자재: MaterialItem[]; 이름: string; 판?: string },
|
||||
): Promise<BranchedCombo> =>
|
||||
call(`/structures/${enc(column)}/material-combo`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
/** 다른 열(`from`) 구조물 도면 내용을 받는 열(`column`)로 가져옴 — 도번 · 열은 받는 쪽 그대로 */
|
||||
export const copyStructure = async (column: string, from: string): Promise<void> => {
|
||||
await call(`/structures/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" });
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
"""조합 갈라 내기 — 구조물 열 하나가 가리키는 조합을 고친 내용으로 새로 만들어 그 열만 새 키를
|
||||
가리키게 함(PLAN 15장 · 검토 `14_조합편집검토.md` 1장).
|
||||
· 일위대가 조합(UA · `일위대가조합.json`) — 새 줄은 M01 조합 저장소가 만듦(부르기만)
|
||||
· 자재 조합(UM · `자재조합.json`) — M01 마스터 저장(`save`)으로 더함(키 = 대장 다음 번호)
|
||||
|
||||
조합은 여러 열 · 모든 프로젝트가 같이 씀 — 제자리 고치지 않음. 담은 것의 집합(차례 · 메모 무시)이
|
||||
이미 있는 조합과 같으면 새로 안 만들고 그 키 · 새 줄의 본뜬키 = 원래 키 · 소유 현장.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
_OWNER = "현장"
|
||||
MATERIAL_FILE = "자재조합.json"
|
||||
|
||||
|
||||
def _m01(fn, *args):
|
||||
"""M01 막힘은 M02 막힘으로 — 상태 · 까닭 그대로."""
|
||||
try:
|
||||
return fn(*args)
|
||||
except m01.StoreError as e:
|
||||
raise store.StoreError(e.status, e.detail) from e
|
||||
|
||||
|
||||
def _free_name(name: str, rows: list[dict]) -> str:
|
||||
taken = {str(r.get("이름") or "") for r in rows}
|
||||
n, out = 2, name
|
||||
while out in taken:
|
||||
out, n = f"{name} ({n})", n + 1
|
||||
return out
|
||||
|
||||
|
||||
def _branch(
|
||||
column: str,
|
||||
version: str,
|
||||
field: str,
|
||||
file: str,
|
||||
slot: str,
|
||||
ident: str,
|
||||
items: list[dict],
|
||||
source: str,
|
||||
make: Callable[[str, list[dict]], dict],
|
||||
name: str,
|
||||
) -> dict[str, Any]:
|
||||
"""집계표 판 검사 → 같은 내용 찾기 · 없으면 `make(이름, 줄들)` 로 새 줄 → 집계표 그 열 `field`.
|
||||
`slot` = 담은 칸(담은로직 · 담은자재) · `ident` = 담은 줄의 알맹이 칸(로직 · 코드)."""
|
||||
table = store.read("table", store.STRUCTURE_TABLE)
|
||||
if version and table["판"] != version:
|
||||
raise store.StoreError(409, {"stale": [store.STRUCTURE_TABLE], "판": table["판"]})
|
||||
col = next((c for c in store._structure_columns() if c["id"] == column), None)
|
||||
if col is None:
|
||||
raise store.StoreError(404, f"{store.STRUCTURE_TABLE}에 없는 구조물 열 「{column}」")
|
||||
if not isinstance(items, list) or not items:
|
||||
raise store.StoreError(400, "담은 것이 비었음")
|
||||
|
||||
def body(got) -> frozenset[str]:
|
||||
return frozenset(str(x.get(ident) or "") for x in got or [] if isinstance(x, dict))
|
||||
|
||||
rows = _m01(m01.read, file)[0].get("줄") or []
|
||||
same = [r for r in rows if body(r.get(slot)) == body(items)]
|
||||
hit = next((r for r in same if source and r.get("키") == source), same[0] if same else None)
|
||||
if hit:
|
||||
key, made_name, made = str(hit["키"]), str(hit.get("이름") or ""), False
|
||||
else:
|
||||
made_name = _free_name(name, rows)
|
||||
key, made = make(made_name, rows), True
|
||||
# ponytail: 조합 쓰기와 집계표 쓰기는 한 묶음 아님 — 사이에 집계표가 바뀌면 409 뒤 주인 없는
|
||||
# 조합 하나가 남음(다시 하면 같은 내용이라 그 키를 씀)
|
||||
doc = table["문서"]
|
||||
for c in doc.get("열", []):
|
||||
if isinstance(c, dict) and c.get("id") == column:
|
||||
c[field] = key
|
||||
info = store.write("table", store.STRUCTURE_TABLE, table["판"], doc) # SS 도 여기서 맞춤
|
||||
return {"키": key, "이름": made_name, "새로만듦": made, "집계표판": info["판"]}
|
||||
|
||||
|
||||
def branch_combo(
|
||||
column: str, source: str, items: list[dict], name: str, version: str = ""
|
||||
) -> dict[str, Any]:
|
||||
"""일위대가 조합 갈라 내기 — 구분 · 상세구분 · 단위 · 출처 = 본뜬 것."""
|
||||
was = _m01(combo_store.combo, source)["combo"]
|
||||
items = [
|
||||
{"로직": str(x.get("로직") or ""), "메모": x.get("메모")}
|
||||
for x in items or []
|
||||
if isinstance(x, dict)
|
||||
]
|
||||
|
||||
def make(made_name: str, _rows) -> str:
|
||||
row = {
|
||||
"이름": made_name,
|
||||
**{k: was.get(k) for k in ("구분", "상세구분", "단위")},
|
||||
"담은로직": items,
|
||||
"본뜬키": source,
|
||||
"출처": was.get("출처"),
|
||||
}
|
||||
return str(_m01(combo_store.combo_new, row, _OWNER)["key"])
|
||||
|
||||
name = name.strip() or str(was.get("이름") or source)
|
||||
return _branch(
|
||||
column, version, "일위대가", combo_store.FILE, "담은로직", "로직", items, source, make, name
|
||||
)
|
||||
|
||||
|
||||
def _material_rows() -> list[dict]:
|
||||
return _m01(m01.read, MATERIAL_FILE)[0].get("줄") or []
|
||||
|
||||
|
||||
def material_combo(key: str) -> dict[str, Any]:
|
||||
row = next((r for r in _material_rows() if r.get("키") == key), None)
|
||||
if row is None:
|
||||
raise store.StoreError(404, f"없는 자재 조합 「{key}」")
|
||||
return row
|
||||
|
||||
|
||||
def branch_material_combo(
|
||||
column: str, source: str, items: list[dict], name: str, version: str = ""
|
||||
) -> dict[str, Any]:
|
||||
"""자재 조합 갈라 내기 — `source` 가 빈 글이면 이 열의 첫 자재 조합(본뜬키 null)."""
|
||||
was = material_combo(source) if source else {}
|
||||
items = [
|
||||
{k: x.get(k) for k in ("코드", "이름", "규격", "단위")}
|
||||
for x in items or []
|
||||
if isinstance(x, dict)
|
||||
]
|
||||
|
||||
def make(made_name: str, rows: list[dict]) -> str:
|
||||
row = {"이름": made_name, "담은자재": items, "본뜬키": source or None, "소유": _OWNER}
|
||||
before = {r.get("키") for r in rows}
|
||||
_m01(
|
||||
m01.save,
|
||||
[
|
||||
{
|
||||
"file": MATERIAL_FILE,
|
||||
"version": _m01(m01.read, MATERIAL_FILE)[1],
|
||||
"changes": [{"op": "add", "row": {**row, "비고": None}}],
|
||||
}
|
||||
],
|
||||
)
|
||||
return str(next(r["키"] for r in _material_rows() if r.get("키") not in before))
|
||||
|
||||
heads = next((c for c in store._structure_columns() if c["id"] == column), {})
|
||||
default = " ".join(h for h in store._heads(heads) if h) if heads else column
|
||||
name = name.strip() or str(was.get("이름") or default or column)
|
||||
return _branch(
|
||||
column, version, "자재조합", MATERIAL_FILE, "담은자재", "코드", items, source, make, name
|
||||
)
|
||||
@@ -10,6 +10,7 @@ from typing import Any
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Branch as branch
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
router = APIRouter(prefix="/api/m02", tags=["M02 MasterTemplete"])
|
||||
@@ -20,6 +21,20 @@ class SaveBody(BaseModel):
|
||||
문서: Any
|
||||
|
||||
|
||||
class BranchBody(BaseModel):
|
||||
본뜬키: str
|
||||
담은로직: list[dict[str, Any]]
|
||||
이름: str = ""
|
||||
판: str = "" # 집계표 판 — 주면 어긋날 때 409
|
||||
|
||||
|
||||
class MaterialBranchBody(BaseModel):
|
||||
본뜬키: str = "" # 빈 글 = 이 열의 첫 자재 조합
|
||||
담은자재: list[dict[str, Any]]
|
||||
이름: str = ""
|
||||
판: str = ""
|
||||
|
||||
|
||||
def _call(fn, *args):
|
||||
try:
|
||||
return fn(*args)
|
||||
@@ -49,6 +64,26 @@ def post_structure(column: str) -> dict:
|
||||
return _call(store.create_structure, column)
|
||||
|
||||
|
||||
@router.post("/structures/{column}/combo")
|
||||
def post_structure_combo(column: str, body: BranchBody) -> dict:
|
||||
"""조합 갈라 내기 — 고친 담은 로직으로 새 UA(같은 로직 집합이면 있는 키) · 이 열만 가리킴."""
|
||||
return _call(branch.branch_combo, column, body.본뜬키, body.담은로직, body.이름, body.판)
|
||||
|
||||
|
||||
@router.post("/structures/{column}/material-combo")
|
||||
def post_structure_material_combo(column: str, body: MaterialBranchBody) -> dict:
|
||||
"""자재 조합 갈라 내기 — 같은 규칙(같은 자재 집합이면 있는 키 · 아니면 새 UM)."""
|
||||
return _call(
|
||||
branch.branch_material_combo, column, body.본뜬키, body.담은자재, body.이름, body.판
|
||||
)
|
||||
|
||||
|
||||
@router.get("/material-combos/{key}")
|
||||
def get_material_combo(key: str) -> dict:
|
||||
"""자재 조합 한 줄(키 UM) — 키 · 이름 · 담은자재 · 본뜬키 · 소유 · 비고."""
|
||||
return _call(branch.material_combo, key)
|
||||
|
||||
|
||||
# 복사 가져오기 — 다른 열(`from`) 내용을 받는 열로 · 번호 · 열은 받는 쪽 그대로
|
||||
@router.post("/structures/{column}/copy")
|
||||
def post_structure_copy(column: str, source: str = Query(alias="from")) -> dict:
|
||||
|
||||
@@ -109,9 +109,12 @@ def write(kind: str, name: str, version: str, doc: Any) -> dict[str, Any]:
|
||||
만들고 지운 열(또는 구조물이 아니게 된 열)의 세 파일을 지움(잠금 밖에서 · 안에서 부르면 죽음).
|
||||
통합문서(area · material)는 서버가 같은 TS 엔진을 Node 로 돌려 `계산값` 을 새로 적음
|
||||
(브라우저 값 버림 · 5장 ②) · 알약 `연결` 은 문서와 같이 그대로 적힘.
|
||||
번호 칸(도번 · 면적번 · 재료번)은 서버 것 — 있던 파일 번호를 그대로 둠(화면 값을 안 받음)."""
|
||||
번호 칸(도번 · 면적번 · 재료번)은 서버 것 — 있던 파일 번호를 그대로 둠(화면 값을 안 받음).
|
||||
표 열의 `본뜬열`(분류 더하기로 복사한 새 열)은 적지 않음 — 집계표면 아직 파일이 없는 새 열이
|
||||
원본 열의 일위대가 · 자재조합 키와 세 파일 내용을 받음(새 번호 · 그 뒤로는 열마다 따로)."""
|
||||
path = _path(kind, name)
|
||||
_check_skeleton(kind, doc)
|
||||
origins = _take_origins(doc) if kind == "table" else {}
|
||||
if kind in SHEETS:
|
||||
doc = _server_values(doc)
|
||||
with _LOCK:
|
||||
@@ -124,6 +127,9 @@ def write(kind: str, name: str, version: str, doc: Any) -> dict[str, Any]:
|
||||
atomic_write_json(path, doc)
|
||||
info = _info(kind, path)
|
||||
if kind == "table" and name == STRUCTURE_TABLE:
|
||||
for column, source in origins.items(): # 빈 파일 채우기 앞 — 원본 내용으로 만듦
|
||||
for part in _KEY:
|
||||
_ensure(part, column, source)
|
||||
backfill_missing_structures() # 지우기 앞 — 같은 저장의 새 열이 지운 번호를 안 받게
|
||||
prune_removed_structures()
|
||||
sync_combos() # 번호가 다 선 뒤 — 조합 표 도번 · 면적번 · 재료번 칸
|
||||
@@ -256,17 +262,46 @@ def _blank(kind: str, column: str) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _ensure(kind: str, column: str) -> bool:
|
||||
"""그 열의 파일이 없으면 빈 문서를 번호(세 파일이 같은 8자리)와 같이 만듦 · 있으면 그대로."""
|
||||
def _ensure(kind: str, column: str, source: str | None = None) -> bool:
|
||||
"""그 열의 파일이 없으면 빈 문서를 번호(세 파일이 같은 8자리)와 같이 만듦 · 있으면 그대로.
|
||||
`source` 열 파일이 있으면 빈 문서 대신 그 내용(알약 `연결` 포함 · 계산값은 서버가 다시 풂)."""
|
||||
path = _path(kind, column)
|
||||
if path.is_file():
|
||||
return False
|
||||
head = next(h for k, _, h, _ in PARTS if k == kind)
|
||||
base = _blank(kind, column)
|
||||
if source and _path(kind, source).is_file():
|
||||
got = json.loads(_path(kind, source).read_bytes())
|
||||
base = {**{k: v for k, v in got.items() if k != "계산값"}, "열": column}
|
||||
if kind in SHEETS:
|
||||
base = _server_values(base)
|
||||
with _LOCK:
|
||||
if path.is_file():
|
||||
return False
|
||||
atomic_write_json(path, {**_blank(kind, column), _KEY[kind]: head + _new_code(column)})
|
||||
atomic_write_json(path, {**base, _KEY[kind]: head + _new_code(column)})
|
||||
return True
|
||||
|
||||
|
||||
def _take_origins(doc: Any) -> dict[str, str]:
|
||||
"""표 열의 `본뜬열` 을 떼어 냄 → `{새 열: 원본 열}` (구조물 파일이 아직 없는 열만) ·
|
||||
그 열에 일위대가 · 자재조합 키가 비었으면 원본 열 것을 받음."""
|
||||
cols = (
|
||||
[c for c in (doc.get("열") or []) if isinstance(c, dict)] if isinstance(doc, dict) else []
|
||||
)
|
||||
by_id = {c.get("id"): c for c in cols}
|
||||
out: dict[str, str] = {}
|
||||
for col in cols:
|
||||
source = col.pop("본뜬열", None)
|
||||
if not source or source not in by_id or not col.get("id"):
|
||||
continue
|
||||
if (FOLDER / "structure" / f"{col['id']}.json").is_file():
|
||||
continue
|
||||
out[col["id"]] = source
|
||||
for field in ("일위대가", "자재조합"):
|
||||
col[field] = col.get(field) or by_id[source].get(field)
|
||||
return out
|
||||
|
||||
|
||||
def create_structure(column: str) -> dict[str, Any]:
|
||||
"""구조물 열 하나의 구조물 도면 새로 — 도번 SD + 8자리 · 이미 있으면 409.
|
||||
면적산출 · 재료산출 파일이 없으면 빈 통합문서도 같이 만듦(같은 8자리)."""
|
||||
@@ -400,6 +435,7 @@ def _combo_row(key: str, col: dict[str, Any], numbers: dict[str, str]) -> dict[s
|
||||
"분류3": level3,
|
||||
"단위": col.get("단위"),
|
||||
"일위대가": col.get("일위대가"),
|
||||
"자재조합": col.get("자재조합"),
|
||||
**{field: numbers.get(field, "") for _, field, _, _ in PARTS},
|
||||
"바인딩": col.get("바인딩"),
|
||||
"펼침": col.get("펼침"),
|
||||
|
||||
Reference in New Issue
Block a user