Merge remote-tracking branch 'origin/sub_laptop_3' into sub_laptop_2

This commit is contained in:
2026-10-04 18:28:31 +09:00
10 changed files with 729 additions and 18 deletions
+22
View File
@@ -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}"
+90
View File
@@ -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)));
}
}
+340
View File
@@ -0,0 +1,340 @@
{
"종류": "일위대가",
"판": 1,
"열": "item1",
"단위": "개소",
"본뜬": {
"층": "system",
"주인": null,
"키": "UA000008"
},
"담은로직": [
{
"로직": "GC000198",
"메모": "레미콘타설(장비) 무근,소형",
"입력값": {},
"수량": null
},
{
"로직": "GC000214",
"메모": "유로폼 설치 및 해체 보통",
"입력값": {},
"수량": null
},
{
"로직": "GC000206",
"메모": "철근가공 및 조립 Type-Ⅰ-1,현장가공조립",
"입력값": {},
"수량": null
},
{
"로직": "GC000207",
"메모": "철근가공 및 조립 Type-Ⅰ-1,현장가공조립",
"입력값": {},
"수량": null
},
{
"로직": "GF000218",
"메모": "기초다짐 및 뒤채움 소형 장비사용 시공 (실무 수는 옛 판 값)",
"입력값": {},
"수량": null
},
{
"로직": "GF000225",
"메모": "깬잡석찰붙임 L3=30cm내외",
"입력값": {},
"수량": null
},
{
"로직": "GF000156",
"메모": "콘크리트(기계비빔) 무근구조물 (실무 수는 옛 판 값)",
"입력값": {},
"수량": null
},
{
"로직": "GF000226",
"메모": "큰돌메쌓기 뒷길이 0.6~0.8m",
"입력값": {},
"수량": null
},
{
"로직": "GF000214",
"메모": "야면석채집 L3=45cm내외 (실무 수는 옛 판 값)",
"입력값": {},
"수량": null
},
{
"로직": "GF000222",
"메모": "찰쌓기(켜쌓기) L3=55cm이하 (실무 수는 옛 판 값)",
"입력값": {},
"수량": null
},
{
"로직": "GC000677",
"메모": "모르터바르기 1:3",
"입력값": {},
"수량": null
},
{
"로직": "GF000221",
"메모": "돌쌓기 찰(켜)쌓기,L3=55cm이하 (실무 수는 옛 판 값)",
"입력값": {},
"수량": null
},
{
"로직": "GC000088",
"메모": "구조물터파기(토사) 굴착기0.6m3",
"입력값": {},
"수량": null
},
{
"로직": "GF000113",
"메모": "구조물되메우기 굴착기0.6m3 (실무 수는 옛 판 값)",
"입력값": {},
"수량": null
}
],
"자재": [],
"일위대가번": "SU-000001",
"계산값": {
"줄": [
{
"로직": "GC000198",
"알약": "레디믹스트콘크리트 타설|㎥",
"분모": "㎥",
"배": 1,
"기준": {
"노무비": 58122.782608695656,
"재료비": 0,
"경비": 1162.455652173913,
"계": 59285.23826086956
},
"까닭": null,
"수량": null,
"수량출처": "없음",
"반영": null
},
{
"로직": "GC000214",
"알약": "유로폼 설치 및 해체|㎡",
"분모": "㎡",
"배": 1,
"기준": {
"노무비": 51524.36363636364,
"재료비": 0,
"경비": 1545.730909090909,
"계": 53070.09454545454
},
"까닭": null,
"수량": null,
"수량출처": "없음",
"반영": null
},
{
"로직": "GC000206",
"알약": "철근 현장가공|ton",
"분모": "ton",
"배": 1,
"기준": {
"노무비": 217028.66666666666,
"재료비": 0,
"경비": 19532.58,
"계": 236561.24666666667
},
"까닭": null,
"수량": null,
"수량출처": "없음",
"반영": null
},
{
"로직": "GC000207",
"알약": "철근 현장조립|ton",
"분모": "ton",
"배": 1,
"기준": {
"노무비": 574487.6470588235,
"재료비": 11765,
"경비": 11489.75294117647,
"계": 597742.4
},
"까닭": null,
"수량": null,
"수량출처": "없음",
"반영": null
},
{
"로직": "GF000218",
"알약": "기초다짐 및 뒷채움|㎥",
"분모": "㎥",
"배": 1,
"기준": {
"노무비": 12869.246375,
"재료비": 1530.2328689,
"경비": 1315.382,
"계": 15714.8612439
},
"까닭": null,
"수량": null,
"수량출처": "없음",
"반영": null
},
{
"로직": "GF000225",
"알약": "돌붙임 찰붙임|㎡",
"분모": "㎡",
"배": 1,
"기준": {
"노무비": 49447.04583333333,
"재료비": 4806.2270256,
"경비": 6425.98,
"계": 60679.25285893333
},
"까닭": null,
"수량": null,
"수량출처": "없음",
"반영": null
},
{
"로직": "GF000156",
"알약": "기계비빔타설|㎥",
"분모": "㎥",
"배": 1,
"기준": {
"노무비": 144348.25222222222,
"재료비": 1647.113,
"경비": 378,
"계": 146373.36522222223
},
"까닭": null,
"수량": null,
"수량출처": "없음",
"반영": null
},
{
"로직": "GF000226",
"알약": "큰돌쌓기 메쌓기|㎡",
"분모": "㎡",
"배": 10,
"기준": {
"노무비": 819958.01,
"재료비": 132344.5084416,
"경비": 111948.7001,
"계": 1064251.2185416
},
"까닭": null,
"수량": null,
"수량출처": "없음",
"반영": null
},
{
"로직": "GF000214",
"알약": "야면석 채집|㎡ · ㎥",
"분모": "㎡ · ㎥",
"배": 1,
"기준": {
"노무비": 18927.48,
"재료비": 0,
"경비": 0,
"계": 18927.48
},
"까닭": null,
"수량": null,
"수량출처": "없음",
"반영": null
},
{
"로직": "GF000222",
"알약": "돌쌓기 찰쌓기|㎡",
"분모": "㎡",
"배": 1,
"기준": {
"노무비": 51101.38458333333,
"재료비": 6772.4108088,
"경비": 9054.79,
"계": 66928.58539213333
},
"까닭": null,
"수량": null,
"수량출처": "없음",
"반영": null
},
{
"로직": "GC000677",
"알약": "모르타르 배합|㎥",
"분모": "㎥",
"배": 1,
"기준": {
"노무비": 113564.88,
"재료비": 166136,
"경비": 0,
"계": 279700.88
},
"까닭": null,
"수량": null,
"수량출처": "없음",
"반영": null
},
{
"로직": "GF000221",
"알약": "돌쌓기 찰쌓기|㎡",
"분모": "㎡",
"배": 1,
"기준": {
"노무비": 52917.12,
"재료비": 1469.88,
"경비": 0,
"계": 54387
},
"까닭": null,
"수량": null,
"수량출처": "없음",
"반영": null
},
{
"로직": "GC000088",
"알약": "터파기(기계) 보통토사|㎥",
"분모": "㎥",
"배": 1,
"기준": {
"노무비": 843.1458333333334,
"재료비": 627.50883,
"경비": 423.15714285714284,
"계": 1893.811806190476
},
"까닭": null,
"수량": null,
"수량출처": "없음",
"반영": null
},
{
"로직": "GF000113",
"알약": "되메우기|㎥",
"분모": "㎥",
"배": 1,
"기준": {
"노무비": 2660.6419100706057,
"재료비": 416.1508405797101,
"경비": 391.04953017996496,
"계": 3467.8422808302807
},
"까닭": null,
"수량": null,
"수량출처": "없음",
"반영": null
}
],
"자재": [],
"기준합": {
"노무비": 2167800.6667278423,
"재료비": 327515.0318154797,
"경비": 163667.5782754784,
"계": 2658983.2768188003
},
"반영합": {
"노무비": 0,
"재료비": 0,
"경비": 0,
"계": 0
}
}
}
@@ -0,0 +1,11 @@
{
"종류": "일위대가",
"판": 1,
"열": "item2",
"단위": null,
"본뜬": null,
"담은로직": [],
"자재": [],
"계산값": {},
"일위대가번": "SU-000002"
}
@@ -0,0 +1,98 @@
{
"종류": "일위대가",
"판": 1,
"열": "item3",
"단위": "식",
"본뜬": {
"층": "system",
"주인": null,
"키": "UA000001"
},
"담은로직": [
{
"로직": "GF000219",
"메모": "메쌓기 — 돌쌓기 인력",
"입력값": {},
"수량": null
},
{
"로직": "GF000119",
"메모": "노체포설 — 굴착기 0.7㎥",
"입력값": {},
"수량": null
},
{
"로직": "GF000137",
"메모": "운반 — 중기운반 트레일러(20ton)",
"입력값": {},
"수량": null
}
],
"자재": [],
"일위대가번": "SU-000003",
"계산값": {
"줄": [
{
"로직": "GF000219",
"알약": "돌쌓기 메쌓기|㎡",
"분모": "㎡",
"배": 1,
"기준": {
"노무비": 79279.668,
"재료비": 0,
"경비": 0,
"계": 79279.668
},
"까닭": null,
"수량": null,
"수량출처": "없음",
"반영": null
},
{
"로직": "GF000119",
"알약": "노체포설|㎥",
"분모": "㎥",
"배": 1,
"기준": {
"노무비": 2433.321308321308,
"재료비": 1077.3082366522367,
"경비": 1012.3273551844981,
"계": 4522.956900158043
},
"까닭": null,
"수량": null,
"수량출처": "없음",
"반영": null
},
{
"로직": "GF000137",
"알약": "중기운반 트레일러|회",
"분모": "회",
"배": 1,
"기준": {
"노무비": 38181.82530864197,
"재료비": 32481.682622277778,
"경비": 13280.479259259258,
"계": 83943.98719017902
},
"까닭": null,
"수량": null,
"수량출처": "없음",
"반영": null
}
],
"자재": [],
"기준합": {
"노무비": 119894.81461696328,
"재료비": 33558.99085893002,
"경비": 14292.806614443756,
"계": 167746.61209033706
},
"반영합": {
"노무비": 0,
"재료비": 0,
"경비": 0,
"계": 0
}
}
}
@@ -89,3 +89,15 @@ def test_면적산출_카드_알약_줄_켬끔_기본_숨김():
assert '"m02-card-pills": { bucket: "pref", scope: "global" },' in state
# 상세(전체 편집기)는 켬끔 없음 — 늘 보임
assert "is-pills-off" not in _text(M02 / "M02_MasterTemplete_UI_Main.ts")
def test_면적산출_알약은_일위대가_pills_길():
"""39-4 D7 — 로직 물량 「이름|단위」 + 자재 「자재|MT키」 · 다 고정 · 선택 입력 알약 빠짐."""
struct = _text(M02 / "M02_MasterTemplete_Structure.ts")
pills = struct.split("export async function pillsFor")[1].split("async function cardLinks")[0]
area = pills.split('if (kind === "material")')[1].split("\n }\n", 1)[1]
assert "await fetchLaborPills(column)" in area
assert "고정: true" in area and "...(p.자재 ? { 자재: p.자재 } : {})" in area
assert "inputPills" not in area and "fetchComboInputs" not in struct
api = _text(M02 / "M02_MasterTemplete_Api_Fetch.ts")
assert "/layers/${activeLayer}/labor/${enc(column)}/pills" in api
+119
View File
@@ -0,0 +1,119 @@
"""옛 일위대가 조합 → 열마다 일위대가 문서 옮기기(`M02_Labor_Migrate` · PLAN 39-1b).
시스템 층 = 견본 표(임시 폴더) · 조합은 M01 마스터(읽기만) · 개인 층 = 임시 storage.
"""
import json
from pathlib import Path
import pytest
from config import config_system
from M02_MasterTemplete import M02_Labor_Migrate as migrate
from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos
from M02_MasterTemplete import M02_MasterTemplete_Items as items_store
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Template_Layers as layers
from resources.tester.test_m02_structure import write_sample_table
SHARED = "UP42-000001"
def _point(folder: Path, keys: dict[str, str]) -> None:
path = folder / "table" / f"{store.STRUCTURE_TABLE}.json"
doc = json.loads(path.read_bytes())
for col in doc["열"]:
if col["id"] in keys:
col["일위대가"] = keys[col["id"]]
path.write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8")
@pytest.fixture()
def world(tmp_path, monkeypatch):
system = tmp_path / "system"
system.mkdir()
write_sample_table(system)
monkeypatch.setattr(store, "FOLDER", system)
monkeypatch.setattr(layers, "SYSTEM_ROOT", system)
storage = tmp_path / "storage"
storage.mkdir()
monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(storage))
_point(system, {"rv": "UA000008", "c1": "UA000008", "pv_b": "UA999999"})
personal = layers.personal_dir(7, 42)
items_store.ensure(personal)
with store.at(personal):
got = store.read("table", store.STRUCTURE_TABLE)
doc = got["문서"]
doc["열"] = [
{"id": "a", "머리": ["돌", "가"], "꼴": "수", "일위대가": SHARED},
{"id": "b", "머리": ["돌", "나"], "꼴": "수", "일위대가": SHARED},
]
store.write("table", store.STRUCTURE_TABLE, got["판"], doc)
combo = {
"그룹": "일위대가조합",
"다음": 2,
"줄": [
{
"키": SHARED,
"이름": "개인 조합",
"단위": "개소",
"담은로직": [{"로직": "GF000219", "메모": "메쌓기"}],
}
],
}
(personal / combos.FILE).write_text(json.dumps(combo, ensure_ascii=False), encoding="utf-8")
return {"system": system, "personal": personal, "storage": storage}
def _labor(folder: Path, cid: str) -> dict:
return json.loads((folder / "labor" / f"{cid}.json").read_bytes())
def test_시스템_층_열마다_복사_같은_조합도_따로_없는_조합은_못함(world):
system = world["system"]
got = migrate.migrate(system)
assert got["옮김"] == ["rv ← UA000008", "c1 ← UA000008"]
assert got["못함"] == ["pv_b · 없는 일위대가 조합 「UA999999」"]
rv, c1 = _labor(system, "rv"), _labor(system, "c1")
assert rv["본뜬"] == {"층": "system", "주인": None, "키": "UA000008"}
assert rv["담은로직"] == c1["담은로직"] and len(rv["담은로직"]) == 14
assert all(one["입력값"] == {} and one["수량"] is None for one in rv["담은로직"])
assert rv["일위대가번"] != c1["일위대가번"] # 열마다 자기 번호
with store.at(system): # 나중에 만든 도면도 같은 뒷자리
store.create_structure("rv")
doc = json.loads((system / "structure" / "rv.json").read_bytes())
assert doc["도번"][2:] == rv["일위대가번"][2:]
assert rv["계산값"]["줄"] # 서버가 계산값까지
assert _labor(system, "ms")["담은로직"] == [] # 키 없는 열 = 빈 일위대가
def test_개인_층_여러_열이_쓰던_조합은_열마다_본뜬은_그_층(world):
personal = world["personal"]
assert migrate.layer_folders(True) == [personal]
got = migrate.migrate(personal)
assert got["옮김"] == [f"a ← {SHARED}", f"b ← {SHARED}"]
a, b = _labor(personal, "a"), _labor(personal, "b")
assert a["본뜬"] == b["본뜬"] == {"층": "personal", "주인": "7/42", "키": SHARED}
assert (a["단위"], a["담은로직"][0]["메모"]) == ("개소", "메쌓기")
assert a["일위대가번"] != b["일위대가번"]
def test_다시_돌려도_같음_사람이_고친_문서는_그대로(world):
system = world["system"]
migrate.migrate(system)
before = {p.name: p.read_bytes() for p in (system / "labor").glob("*.json")}
assert migrate.migrate(system)["옮김"] == []
after = {p.name: p.read_bytes() for p in (system / "labor").glob("*.json")}
assert before == after
with store.at(system): # 사람이 비운 문서도 다시 안 채움
got = store.read("labor", "rv")
store.write("labor", "rv", got["판"], {**got["문서"], "담은로직": []})
assert "rv" in migrate.migrate(system)["그대로"]
assert _labor(system, "rv")["담은로직"] == []
def test_미리_보기는_파일을_안_씀(world):
system = world["system"]
got = migrate.migrate(system, dry=True)
assert "rv ← UA000008" in got["옮김"]
assert not list((system / "labor").glob("*.json"))