Merge remote-tracking branch 'origin/sub_laptop_3' into sub_laptop_2
This commit is contained in:
@@ -125,6 +125,28 @@ def clean(doc: Any) -> dict[str, Any]:
|
||||
return out
|
||||
|
||||
|
||||
def from_combo(key: str) -> dict[str, Any]:
|
||||
"""옛 일위대가 조합(마스터 UA = 실무문서 · 회사 UC · 개인 UP) → 일위대가 문서 몸 —
|
||||
담은로직 · 단위 복사 · 입력값 {} · 수량 null · 자재 [] · `본뜬` = 그 조합(층 · 주인 · 키).
|
||||
없는 조합은 404."""
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos
|
||||
|
||||
row = combos.find(key)
|
||||
if row is None:
|
||||
raise store.StoreError(404, f"없는 일위대가 조합 「{key}」")
|
||||
layer, owner = combos.where(key) or (None, None)
|
||||
return {
|
||||
"단위": row.get("단위"),
|
||||
"본뜬": {"층": layer, "주인": owner, "키": key},
|
||||
"담은로직": [
|
||||
{"로직": one.get("로직"), "메모": one.get("메모"), "입력값": {}, "수량": None}
|
||||
for one in row.get("담은로직") or []
|
||||
if isinstance(one, dict)
|
||||
],
|
||||
"자재": [],
|
||||
}
|
||||
|
||||
|
||||
# ── 알약 · 면적 값 ────────────────────────────────────────────────────
|
||||
def material_key(code: str) -> str:
|
||||
return f"{MATERIAL_HEAD}{code}"
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""옛 일위대가 조합 → 구조물 열마다 일위대가 문서 — 한 번 돌리는 도구(PLAN 39-1b · 설계_정함 1장).
|
||||
|
||||
층마다 구조물 열마다 `labor/<열id>.json`:
|
||||
- 열 `일위대가` 키가 있으면 그 조합의 담은로직 · 단위 복사(`본뜬.키` = 옛 키 · 입력값 {} ·
|
||||
수량 null · 자재 []) — 여러 열이 쓰던 조합도 열마다 따로 복사
|
||||
- 키 없는 열 = 빈 일위대가 · 번호 SU 는 저장소가 줌(도번과 같은 뒷자리)
|
||||
- 몇 번 돌려도 같음 — 이미 그 키를 본뜬 문서 · 사람이 채운 문서(담은로직 · 자재가 있음)는 그대로
|
||||
옛 조합 파일 · 집계표 열 `일위대가` 칸은 손대지 않음(실무문서 · 은퇴는 39-1b B9).
|
||||
|
||||
./venv/Scripts/python.exe M02_MasterTemplete/M02_Labor_Migrate.py # 시스템 층
|
||||
./venv/Scripts/python.exe M02_MasterTemplete/M02_Labor_Migrate.py --storage # 회사 · 개인 층
|
||||
[--dry-run]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor # noqa: E402
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store # noqa: E402
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers # noqa: E402
|
||||
|
||||
|
||||
def _filled(doc: dict) -> bool:
|
||||
return bool(doc.get("담은로직") or doc.get("자재"))
|
||||
|
||||
|
||||
def migrate(folder: Path, dry: bool = False) -> dict[str, list[str]]:
|
||||
"""`folder` 층 하나 — `{만듦, 옮김, 그대로, 못함}`(열 id · 못함은 까닭 붙임)."""
|
||||
out: dict[str, list[str]] = {"만듦": [], "옮김": [], "그대로": [], "못함": []}
|
||||
with store.at(folder):
|
||||
for col in store._structure_columns():
|
||||
cid, key = col["id"], str(col.get("일위대가") or "")
|
||||
path = store._path("labor", cid)
|
||||
have = json.loads(path.read_bytes()) if path.is_file() else None
|
||||
done = have is not None and (have.get("본뜬") or {}).get("키") == key
|
||||
if have is not None and (_filled(have) or not key or done):
|
||||
out["그대로"].append(cid)
|
||||
continue
|
||||
if not key:
|
||||
out["만듦"].append(cid)
|
||||
if not dry:
|
||||
store._ensure("labor", cid)
|
||||
continue
|
||||
try:
|
||||
body = labor.from_combo(key)
|
||||
except store.StoreError as e:
|
||||
out["못함"].append(f"{cid} · {e.detail}")
|
||||
continue
|
||||
out["옮김"].append(f"{cid} ← {key}")
|
||||
if dry:
|
||||
continue
|
||||
store._ensure("labor", cid) # 번호(SU · 같은 뒷자리)
|
||||
got = store.read("labor", cid)
|
||||
store.write("labor", cid, got["판"], {**got["문서"], **body})
|
||||
return out
|
||||
|
||||
|
||||
def layer_folders(storage: bool) -> list[Path]:
|
||||
"""시스템 층 하나 · 또는 storage 회사 · 개인 층(구조물집계표가 있는 것 · 프로젝트 빼고)."""
|
||||
if not storage:
|
||||
return [layers.system_dir()]
|
||||
root = layers.storage_root()
|
||||
table = Path("table") / f"{store.STRUCTURE_TABLE}.json"
|
||||
found = [
|
||||
*root.glob(f"*/{layers.TEMPLATES_DIRNAME}"),
|
||||
*root.glob(f"*/*/{layers.TEMPLATES_DIRNAME}"),
|
||||
]
|
||||
return sorted(p for p in found if (p / table).is_file())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--storage", action="store_true", help="회사 · 개인 층(storage)")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
args = parser.parse_args()
|
||||
report: dict[str, Any] = {}
|
||||
for folder in layer_folders(args.storage):
|
||||
report[str(folder)] = migrate(folder, args.dry_run)
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -301,6 +301,20 @@ export const copyStructure = async (column: string, from: string): Promise<void>
|
||||
await scoped(`/structures/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" });
|
||||
};
|
||||
|
||||
/** 면적산출 알약 하나 — 로직 물량 「이름|단위」(`로직` · `배`) · 자재 「자재|MT키」(`자재`) */
|
||||
export interface LaborPill {
|
||||
키: string;
|
||||
이름: string;
|
||||
단위: string;
|
||||
로직?: string[];
|
||||
배?: number;
|
||||
자재?: { 코드: string; 이름: string; 규격: string; 단위: string };
|
||||
}
|
||||
|
||||
/** 그 열 일위대가의 면적산출 알약(저장본 · PLAN 39-1a pills 길) — 지금 층 */
|
||||
export const fetchLaborPills = (column: string): Promise<{ 열: string; 알약: LaborPill[] }> =>
|
||||
call(owned(activeLayer, `/layers/${activeLayer}/labor/${enc(column)}/pills`));
|
||||
|
||||
/** 다른 열(`from`) 면적산출 내용(알약 연결 포함)을 받는 열(`column`)로 가져옴 — 면적번 · 열은 그대로 */
|
||||
export const copyArea = async (column: string, from: string): Promise<void> => {
|
||||
await scoped(`/area/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" });
|
||||
|
||||
@@ -252,3 +252,9 @@
|
||||
.m02-struct__sheet.is-pills-off .ss-links {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* [알약] 켬 — 눌린 단추를 강조색으로(밝음 · 어두움 같은 토큰) */
|
||||
.m02-struct__card-tools .ui-btn[aria-pressed="true"] {
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
@@ -24,14 +24,14 @@ import {
|
||||
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 { fetchUsableCombos } from "./M02_MasterTemplete_Labor_Api";
|
||||
import {
|
||||
copyArea,
|
||||
copyMaterial,
|
||||
copyStructure,
|
||||
fetchLaborPills,
|
||||
fetchStructureNumbers,
|
||||
getActiveLayer,
|
||||
readTemplate,
|
||||
saveTemplate,
|
||||
type StructureNumbers,
|
||||
type TreeEdit,
|
||||
@@ -67,9 +67,8 @@ const TEXT = {
|
||||
pickCopy: "{value} 복사해 가져오기",
|
||||
copied: "복사해 가져옴",
|
||||
copyFailed: "복사 실패 — {value}",
|
||||
noPrice: "이 열에 일위대가가 없어 알약 없음 — 구조물 조합 일위대가 줄에서 고름",
|
||||
noInputs: "일위대가 {value} 에 입력값이 없어 알약 없음",
|
||||
inputsFailed: "일위대가 입력값을 못 받음 — {value}",
|
||||
noPills: "이 열 일위대가에 담은 로직 · 자재가 없어 알약 없음 — 일위대가 상세에서 담음",
|
||||
pillsFailed: "일위대가 알약을 못 받음 — {value}",
|
||||
discardCards: "카드(도면 · 면적산출 · 재료산출)에 저장 안 한 고침이 있음 — 버리고 넘어갈까?",
|
||||
};
|
||||
|
||||
@@ -275,7 +274,8 @@ export const columnHead = (col: StructureColumn): string =>
|
||||
export const structureLabel = (number: string, col: StructureColumn): string =>
|
||||
[number, columnHead(col)].filter(Boolean).join(" ");
|
||||
|
||||
/** 면적산출 · 재료산출 편집 화면 위 알약 막대 — 면적 = 그 열 일위대가 입력값(물량) · 재료 = 자재DB 찾기.
|
||||
/** 면적산출 · 재료산출 편집 화면 아래 알약 막대 — 면적 = 그 열 일위대가의 로직 물량 「이름|단위」 + 자재 「자재|MT키」
|
||||
* (서버 pills 길 · 39-4 · 다 `고정` — 더하기 · 빼기는 일위대가 쪽) · 재료 = 자재DB 찾기.
|
||||
* 알약이 없으면 까닭 글(`notice`)을 같이 돌려줌 */
|
||||
export async function pillsFor(
|
||||
kind: SheetKind,
|
||||
@@ -285,20 +285,19 @@ export async function pillsFor(
|
||||
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(
|
||||
() => null,
|
||||
);
|
||||
const cols = (table?.문서 as { 열?: { id: string; 일위대가?: string | null }[] } | null)?.열;
|
||||
const combo = cols?.find((c) => c.id === column)?.일위대가;
|
||||
if (!combo) return none(TEXT.noPrice);
|
||||
const none = (notice: string) => ({ links: { 항목: [], 모드: "입력" as const }, notice });
|
||||
try {
|
||||
const got = await fetchComboInputs(combo);
|
||||
const 항목 = [...got.물량.map((q) => ({ 키: q.키, 이름: q.이름, 단위: q.단위 })), ...own];
|
||||
return 항목.length ? { links: { 항목, 모드: "입력" } } : none(fill(TEXT.noInputs, combo));
|
||||
const { 알약 } = await fetchLaborPills(column);
|
||||
const 항목 = 알약.map((p) => ({
|
||||
키: p.키,
|
||||
이름: p.이름,
|
||||
단위: p.단위,
|
||||
...(p.자재 ? { 자재: p.자재 } : {}),
|
||||
고정: true,
|
||||
}));
|
||||
return 항목.length ? { links: { 항목, 모드: "입력" } } : none(TEXT.noPills);
|
||||
} catch (error) {
|
||||
return none(fill(TEXT.inputsFailed, why(error)));
|
||||
return none(fill(TEXT.pillsFailed, why(error)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user