Merge remote-tracking branch 'origin/sub_laptop_8' into sub_laptop_1
This commit is contained in:
@@ -55,6 +55,7 @@ KINDS = {
|
||||
"입력연결": LINK_FILE,
|
||||
"입력값": VALUES_FILE,
|
||||
"고른자재": PICKS_FILE,
|
||||
"일위대가": LABOR_FILE, # 읽기만 — 일위대가는 원본 층에서 고침(39-1c)
|
||||
}
|
||||
#: 뼈대 검사에 쓰는 층 양식 종류(도면 · 면적산출 · 재료산출)
|
||||
SKELETON = {"도면": "structure", "면적": "area", "재료": "material"}
|
||||
@@ -171,6 +172,8 @@ def _check_links(content: Any) -> None:
|
||||
|
||||
|
||||
def check(kind: str, content: Any) -> None:
|
||||
if kind == "일위대가":
|
||||
raise ValueError("일위대가는 원본(마스터 · 회사 · 개인) 층에서 고침 — 프로젝트는 넣을 값만")
|
||||
if kind == "입력값":
|
||||
_check_values(content)
|
||||
elif kind == "고른자재":
|
||||
@@ -253,7 +256,10 @@ def reapply(project_root: str | Path, folder: str) -> dict[str, Any]:
|
||||
before = {name: read_json(target / file) for name, file in KINDS.items()}
|
||||
files = build_snapshot(manifest["원본"])
|
||||
labor = files.get(LABOR_FILE) or {}
|
||||
files[VALUES_FILE] = _kept_values(values, labor)
|
||||
seeded = files[VALUES_FILE] # 새 원본의 첫 값 — 프로젝트에서 넣은 값이 같은 키면 이김
|
||||
kept_values = _kept_values(values, labor)
|
||||
for logic, inputs in kept_values.items():
|
||||
seeded[logic] = {**seeded.get(logic, {}), **inputs}
|
||||
files[PICKS_FILE] = _kept_picks(picks, labor)
|
||||
for old in target.glob("*.json"): # 원본에서 빠진 파일(도면 없앰 등)은 지움
|
||||
if old.name != MANIFEST_NAME and old.name not in files:
|
||||
@@ -268,7 +274,7 @@ def reapply(project_root: str | Path, folder: str) -> dict[str, Any]:
|
||||
source = version_of(source_files(files))
|
||||
stamp = now_text()
|
||||
# 원본을 다시 가져왔으니 시트 고침은 없음 — 살린 입력값 · 고른 자재는 `값있음` 으로 따로 알림
|
||||
kept = bool(files[VALUES_FILE] or files[PICKS_FILE])
|
||||
kept = bool(kept_values or files[PICKS_FILE])
|
||||
fresh = {**manifest, "판": source, "원본판": source, "가져온날": stamp, "고침": False}
|
||||
fresh["값있음"] = kept
|
||||
fresh.pop("고친날", None)
|
||||
@@ -343,4 +349,5 @@ def calc(project_root: str | Path, folder: str) -> dict[str, Any]:
|
||||
read_json(target / PRICE_FILE) or {},
|
||||
read_json(target / VALUES_FILE) or {},
|
||||
read_json(target / PICKS_FILE) or {},
|
||||
read_json(target / KINDS["면적"]),
|
||||
)
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
원문번호 틀의 `{입력}` 은 그 입력의 고르기 값으로 모두 끼워 모음(고르기가 없으면 그 테이블 전부 ·
|
||||
자재품목만은 너무 커서 뺌). 줄은 마스터 파일 차례 그대로 담음 — 고르기의 「첫 줄」 이 같게.
|
||||
|
||||
일위대가 자재(MT · 39-1c)도 그 줄(+ 준용 · 연결)을 담음.
|
||||
|
||||
`단가.json` = `{파일 이름: {그룹 · 머리 칸 …, 줄|표: [줄]}}` — `master_formula.Master` 가
|
||||
그대로 읽는 꼴.
|
||||
계산(`calc`)은 이 묶음과 `입력값.json` · `고른자재.json` 만 씀 — 마스터 현재값을 읽지 않음.
|
||||
계산(`calc`)은 이 묶음과 `입력값.json` · `고른자재.json` · `면적.json`(알약 칸 값) 만 씀 — 마스터
|
||||
현재값을 읽지 않음. 수량 · 반영 · 합은 일위대가 문서와 같은 함수(`M02_Labor_Doc.settle`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -200,12 +203,14 @@ class _Collector:
|
||||
self.pick(row, item["요소"], item.get("단위"))
|
||||
|
||||
|
||||
def collect(logic_keys: list[str]) -> dict[str, Any]:
|
||||
"""담은 로직의 닫힌 단가 묶음 — `{파일 이름: 파일(줄은 모은 것만 · 차례 그대로)}`."""
|
||||
def collect(logic_keys: list[str], materials: list[str] | None = None) -> dict[str, Any]:
|
||||
"""담은 로직 + 자재(MT)의 닫힌 단가 묶음 — `{파일 이름: 파일(줄은 모은 것만 · 차례 그대로)}`."""
|
||||
files, whole = master()
|
||||
collector = _Collector(whole)
|
||||
for key in logic_keys:
|
||||
collector.logic(key)
|
||||
for code in materials or []:
|
||||
collector.add(code)
|
||||
out: dict[str, Any] = {}
|
||||
for name, data in files.items():
|
||||
slot = m01.items_key(data)
|
||||
@@ -256,9 +261,17 @@ def missing_inputs(snapshot: Any, labor: dict[str, Any], values: dict[str, Any])
|
||||
|
||||
|
||||
def calc(
|
||||
labor: dict[str, Any], prices: dict[str, Any], values: dict[str, Any], picks: dict[str, Any]
|
||||
labor: dict[str, Any],
|
||||
prices: dict[str, Any],
|
||||
values: dict[str, Any],
|
||||
picks: dict[str, Any],
|
||||
area: Any = None,
|
||||
) -> dict[str, Any]:
|
||||
"""복사본만으로 계산 — 입력이 빠진 로직은 안 돌리고 `멈춤` · 세 비목 · 계는 돈 로직 합."""
|
||||
"""복사본만으로 계산 — 입력이 빠진 로직은 안 돌리고 `멈춤` · 세 비목 · 계는 돈 로직 합
|
||||
(수량 1 기준).
|
||||
|
||||
일위대가 문서(39-1c)면 줄마다 수량 · 반영 · `자재` · `기준합` · `반영합` 을 덧붙임 —
|
||||
수량 = 손 · 면적 알약 칸(`area`) · 반영 = `M02_Labor_Doc.settle`(문서 계산과 한 벌)."""
|
||||
snapshot = load_prices(prices)
|
||||
gaps = missing_inputs(snapshot, labor, values)
|
||||
blocked = {gap["로직"] for gap in gaps}
|
||||
@@ -293,4 +306,33 @@ def calc(
|
||||
"계": got["계"],
|
||||
}
|
||||
)
|
||||
return to_json({"줄": lines, **sums, "계": sum(sums.values()), "빠진입력": gaps, "멈춤": stuck})
|
||||
out = {"줄": lines, **sums, "계": sum(sums.values()), "빠진입력": gaps, "멈춤": stuck}
|
||||
return to_json({**out, **_reflected(snapshot, labor, lines, area)})
|
||||
|
||||
|
||||
def _reflected(snapshot: Any, labor: dict[str, Any], lines: list[dict], area: Any) -> dict:
|
||||
"""줄마다 수량 · 반영 + 자재 + 기준합 · 반영합 — 기준은 위 줄 그대로 · 알약은 가져온 때 것."""
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
|
||||
|
||||
doc = labor.get("줄") or {}
|
||||
pill_of = {code: pill for pill in labor.get("알약") or [] for code in pill.get("로직") or []}
|
||||
base = []
|
||||
for line in lines:
|
||||
pill = pill_of.get(line["로직"]) or {}
|
||||
money = {k: line[k] for k in (*mf.COST_ITEMS, "계")} if line.get("갈래") == "돈" else None
|
||||
head = {"로직": line["로직"], "알약": pill.get("키"), "배": pill.get("배") or 1}
|
||||
base.append({**head, "분모": pill.get("단위"), "기준": money})
|
||||
mats = []
|
||||
for one in doc.get("자재") or []:
|
||||
code = str(one.get("코드") or "")
|
||||
try:
|
||||
price, _ = mm.element(snapshot, code, {}, one.get("단위") or None)
|
||||
except mf.FormulaError:
|
||||
price = None
|
||||
mats.append({"코드": code, "알약": labor_doc.material_key(code), "기준단가": price})
|
||||
settled = labor_doc.settle(
|
||||
to_json({"줄": base, "자재": mats}), doc, labor_doc.area_values(area)
|
||||
)
|
||||
for line, done in zip(lines, settled["줄"]):
|
||||
line.update({k: done[k] for k in ("알약", "분모", "배", "수량", "수량출처", "반영")})
|
||||
return {"자재": settled["자재"], "기준합": settled["기준합"], "반영합": settled["반영합"]}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
|
||||
[저장](`PUT /route/structures` · `PUT /drainage/pipe-points`) 뒤 서버가 맞춤:
|
||||
- 항목을 지정한 구조물마다 원본 층에서 내용을 복사(`M02_Item_Source.build_source`) + 프로젝트 값
|
||||
`입력값.json`(처음엔 빈 값 · 자동값은 안 적음) · `고른자재.json` · `manifest.json`
|
||||
`입력값.json`(첫 값 = 일위대가 문서 입력값 · 확인 거짓 · 자동값은 안 적음) · `고른자재.json` ·
|
||||
`manifest.json`
|
||||
- 이미 있는 복사본은 다시 쓰지 않음 — 원본(마스터 · 회사 · 개인)이 바뀌거나 지워져도 그대로
|
||||
([재적용]은 사용자가)
|
||||
- 지정을 다른 항목으로 바꾸면 새로 가져옴 · 구조물을 지우거나 지정을 풀면 복사본을 지움 —
|
||||
@@ -30,6 +31,7 @@ from common_util.common_util_json import atomic_write_json
|
||||
from common_util.common_util_summary_item import snapshot_folder_name
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
from M02_MasterTemplete.M02_Item_Source import (
|
||||
LABOR_FILE,
|
||||
LINK_FILE,
|
||||
PRICE_FILE,
|
||||
build_source,
|
||||
@@ -85,9 +87,22 @@ def origin_of(item: dict[str, Any]) -> dict[str, Any]:
|
||||
return {**item, "경로": path}
|
||||
|
||||
|
||||
def seed_values(labor: Any) -> dict[str, Any]:
|
||||
"""일위대가 문서 입력값 → 프로젝트 입력값 첫 값 `{로직: {입력: {값, 확인: false}}}`(39-1c)."""
|
||||
doc = labor.get("줄") if isinstance(labor, dict) else None
|
||||
out: dict[str, Any] = {}
|
||||
for line in (doc or {}).get("담은로직") or []:
|
||||
given = line.get("입력값") if isinstance(line, dict) else None
|
||||
if isinstance(given, dict) and given and line.get("로직"):
|
||||
out[str(line["로직"])] = {k: {"값": v, "확인": False} for k, v in given.items()}
|
||||
return out
|
||||
|
||||
|
||||
def build_snapshot(item: dict[str, Any]) -> dict[str, Any]:
|
||||
"""원본 내용 + 빈 프로젝트 값 — `{파일 이름: 내용}` · SS 줄이 없으면 LookupError."""
|
||||
return {**build_source(item), VALUES_FILE: {}, PICKS_FILE: {}}
|
||||
"""원본 내용 + 프로젝트 값(입력값 첫 값 · 빈 고른자재) — `{파일 이름: 내용}` · SS 줄이 없으면
|
||||
LookupError."""
|
||||
files = build_source(item)
|
||||
return {**files, VALUES_FILE: seed_values(files.get(LABOR_FILE)), PICKS_FILE: {}}
|
||||
|
||||
|
||||
def source_files(files: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
모으는 것 = `{파일 이름: 내용}`:
|
||||
`집계.json` SS 줄 + 열 정의 · `도면.json` SD · `면적.json` SA · `재료.json` SM ·
|
||||
`자재조합.json` UM 줄 · `일위대가.json` UA 줄 + 담은 로직 줄(M01 시스템) ·
|
||||
`자재조합.json` UM 줄 ·
|
||||
`일위대가.json` = 층 `labor/<열id>.json` 문서(39-1c · 없으면 옛 UA 조합 줄)
|
||||
+ 담은 로직 줄(M01 시스템) + 알약(가져온 때 그대로) ·
|
||||
`입력연결.json` 층의 `inputs/<열id>.json`(없으면 빈 연결) ·
|
||||
`단가.json` 계산에 닿는 마스터 줄 전부
|
||||
`단가.json` 계산에 닿는 마스터 줄 전부(담은 로직 + 일위대가 자재 MT)
|
||||
`원본판` = 모은 내용의 해시 — 지금 층 · 마스터에서 다시 모은 판과 대어 「마스터가 바뀜」 을 앎.
|
||||
프로젝트가 고치는 값(`입력값.json` · `고른자재.json`)은 원본이 아니라 여기서 안 모음.
|
||||
원본 층 자리 = `M02_Template_Layers` 경로 · 일위대가 · 단가는 M01 시스템 것.
|
||||
@@ -23,6 +25,7 @@ TABLE_NAME = "구조물집계표"
|
||||
UM_FILE = "자재조합.json"
|
||||
#: 열 id → 스냅샷 파일(구조물 도면 · 면적산출 · 재료산출)
|
||||
PART_FILES = (("structure", "도면.json"), ("area", "면적.json"), ("material", "재료.json"))
|
||||
LABOR_KIND = "labor" # 열 id → 일위대가 문서(39-1c) — 스냅샷은 `LABOR_FILE` 에 감싸서
|
||||
LINK_FILE = "입력연결.json"
|
||||
PRICE_FILE = "단가.json"
|
||||
LABOR_FILE = "일위대가.json"
|
||||
@@ -80,15 +83,27 @@ def version_of(content: Any) -> str:
|
||||
|
||||
|
||||
def labor_rows(ua_key: str) -> dict[str, Any] | None:
|
||||
"""조합 줄(마스터 UA · 회사 UC · 개인 UP) + 담은 로직 줄(M01 시스템 · 없는 로직은 빼고 키만
|
||||
`빠진로직`)."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
"""옛 조합 줄(마스터 UA · 회사 UC · 개인 UP) + 담은 로직 줄 — labor 문서가 없는 열
|
||||
(옮기기 전)."""
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos
|
||||
|
||||
ua = combos.find(ua_key)
|
||||
if ua is None:
|
||||
return None
|
||||
wanted = [str(entry.get("로직")) for entry in ua.get("담은로직") or [] if entry.get("로직")]
|
||||
return None if ua is None else logic_rows(ua)
|
||||
|
||||
|
||||
def labor_doc_rows(doc: dict[str, Any]) -> dict[str, Any]:
|
||||
"""일위대가 문서(39-1a) + 담은 로직 줄 + 알약(`M02_Labor_Doc.pills` · 가져온 때 그대로 —
|
||||
복사본 계산이 마스터를 안 읽게)."""
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
|
||||
|
||||
return {**logic_rows(doc), "알약": labor_doc.pills(doc)}
|
||||
|
||||
|
||||
def logic_rows(row: dict[str, Any]) -> dict[str, Any]:
|
||||
"""`{줄: row, 로직: 담은 로직 줄(M01 시스템), 빠진로직: 없는 로직 키}`."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
wanted = [str(entry.get("로직")) for entry in row.get("담은로직") or [] if entry.get("로직")]
|
||||
found: dict[str, dict[str, Any]] = {}
|
||||
raw: dict[str, Any] = {} # M01 적재본은 수가 Decimal — 줄은 원본 파일 JSON 그대로 담음
|
||||
for key in wanted:
|
||||
@@ -98,11 +113,11 @@ def labor_rows(ua_key: str) -> dict[str, Any] | None:
|
||||
continue
|
||||
if file not in raw:
|
||||
raw[file] = read_json(Path(m01.FOLDER) / file)
|
||||
row = row_of(raw[file], key)
|
||||
if row is not None:
|
||||
found[key] = row
|
||||
got = row_of(raw[file], key)
|
||||
if got is not None:
|
||||
found[key] = got
|
||||
return {
|
||||
"줄": ua,
|
||||
"줄": row,
|
||||
"로직": [found[key] for key in wanted if key in found],
|
||||
"빠진로직": [key for key in wanted if key not in found],
|
||||
}
|
||||
@@ -142,10 +157,16 @@ def build_source(item: dict[str, Any]) -> dict[str, Any]:
|
||||
um = row_of(read_json(root / UM_FILE), str(row["자재조합"]))
|
||||
if um is not None:
|
||||
files[UM_FILE] = um
|
||||
labor = labor_rows(str(row["일위대가"])) if row.get("일위대가") else None
|
||||
doc = read_json(root / LABOR_KIND / f"{column_id}.json") if column_id else None
|
||||
if isinstance(doc, dict):
|
||||
labor = labor_doc_rows(doc)
|
||||
else:
|
||||
labor = labor_rows(str(row["일위대가"])) if row.get("일위대가") else None
|
||||
if labor is not None:
|
||||
files[LABOR_FILE] = labor
|
||||
files[LINK_FILE] = input_links(root, column_id)
|
||||
logic_keys = [str(one.get("키")) for one in (labor or {}).get("로직", [])]
|
||||
files[PRICE_FILE] = prices.to_json(prices.collect(logic_keys)) if logic_keys else {}
|
||||
materials = [str(m.get("코드")) for m in ((labor or {}).get("줄") or {}).get("자재") or []]
|
||||
wanted = logic_keys + [code for code in materials if code]
|
||||
files[PRICE_FILE] = prices.to_json(prices.collect(logic_keys, materials)) if wanted else {}
|
||||
return files
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
POST …/items/{폴더}/reapply 재적용(입력값 · 고른자재는 같은 키면 살림 · `덮임` 알림)
|
||||
POST …/items/{폴더}/save-as {to, 경로, 이름} 개인 층에 새 항목으로(단가 묶음은 안 가져감)
|
||||
POST …/items/{폴더}/calc 복사본만으로 계산(세 비목 · 계 · 빠진 입력)
|
||||
종류 = 도면 · 면적 · 재료 · 자재조합 · 입력연결 · 입력값 · 고른자재 · `refresh` = 옛 이름(재적용).
|
||||
일위대가 문서면 줄마다 수량 · 반영 + 자재 · 기준합 · 반영합
|
||||
종류 = 도면 · 면적 · 재료 · 자재조합 · 입력연결 · 입력값 · 고른자재 · 일위대가(읽기만 · 쓰면 400) ·
|
||||
`refresh` = 옛 이름(재적용).
|
||||
⚠ 등록은 `main.py`(로그인만) — 프로젝트 권한(같은 회사)은 여기서 `_project` 로 봄(403 · 404).
|
||||
"""
|
||||
|
||||
@@ -31,7 +33,7 @@ from M02_MasterTemplete.M02_Item_Source import item_name
|
||||
|
||||
router = APIRouter(prefix="/api/projects", tags=["M02 Project Items"])
|
||||
|
||||
Kind = Literal["도면", "면적", "재료", "자재조합", "입력연결", "입력값", "고른자재"]
|
||||
Kind = Literal["도면", "면적", "재료", "자재조합", "입력연결", "입력값", "고른자재", "일위대가"]
|
||||
|
||||
|
||||
class WriteBody(BaseModel):
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* ① 폴더 이름 · 목록 {항목} · 가져오기 POST · 읽기 {종류, 판, 내용} · 고침 PUT {판, 내용}(판 · 409 · 403)
|
||||
* ② 새로 가져오기 · 계산 한 줄 · 입력 목록(한 번만 받음)
|
||||
* ③ 카드 셈: 범위 · 고르기 원래 모양 · 자리 「선택」 만 묶음
|
||||
* ④ 일위대가(39-1c): 복사본 요약 · 원본 층 계산 요약 · 넣을 값 = 담은 로직 입력 전부
|
||||
* ⚠ 확장자가 `.cjs` 인 까닭은 `helper_b05_curve_radius_floor.cjs` 머리와 같다.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
@@ -66,6 +67,21 @@ globalThis.fetch = async (url, init = {}) => {
|
||||
const method = init.method ?? "GET";
|
||||
const body = init.body ? JSON.parse(init.body) : undefined;
|
||||
state.calls.push({ method, url, body });
|
||||
if (url.startsWith("/api/m02/logic-lib/inputs?keys=")) {
|
||||
state.logicKeys = decodeURIComponent(url.split("=")[1]).split(",");
|
||||
return reply(200, { 로직: INPUTS.로직, 빠진로직: [] });
|
||||
}
|
||||
if (/^\/api\/m02\/layers\/system\/labor\/rv\/calc$/.test(url)) {
|
||||
return reply(200, {
|
||||
줄: [
|
||||
{ 로직: "GA1", 까닭: null },
|
||||
{ 로직: "GA2", 까닭: "입력 없음" },
|
||||
],
|
||||
자재: [{ 코드: "MT000008" }],
|
||||
기준합: { 계: 500 },
|
||||
반영합: { 계: 1500 },
|
||||
});
|
||||
}
|
||||
if (/^\/api\/m02\/layers\/\w+\/items\/[^/]+\/inputs$/.test(url)) {
|
||||
state.inputsHits += 1;
|
||||
return reply(200, INPUTS);
|
||||
@@ -97,7 +113,13 @@ globalThis.fetch = async (url, init = {}) => {
|
||||
const folder = body.structure.replace("#", "n");
|
||||
state.folders[folder] = {
|
||||
info: { 폴더: folder, 원본: body, 마스터바뀜: false, 가져온날: "2026-09-30T01:00:00" },
|
||||
parts: { 면적: { 시트: [{ id: "s", 칸: { A1: { 값: 1 } } }] }, 입력값: null },
|
||||
parts: {
|
||||
면적: { 시트: [{ id: "s", 칸: { A1: { 값: 1 } } }] },
|
||||
입력값: null,
|
||||
일위대가: {
|
||||
줄: { 일위대가번: "SU-000007", 담은로직: [{ 로직: "GA1" }, { 로직: "GA2" }], 자재: [{}] },
|
||||
},
|
||||
},
|
||||
};
|
||||
return reply(200, { 폴더: folder, 만듦: true, 가져온날: "2026-09-30T01:00:00" });
|
||||
}
|
||||
@@ -132,6 +154,8 @@ globalThis.fetch = async (url, init = {}) => {
|
||||
{ 로직: "GA2", 입력: "지역" },
|
||||
],
|
||||
멈춤: ["GA1"],
|
||||
기준합: { 계: 100 },
|
||||
반영합: { 계: 300 },
|
||||
});
|
||||
if (method === "GET") {
|
||||
const content = item.parts[part] ?? null;
|
||||
@@ -226,6 +250,18 @@ async function main() {
|
||||
out.calc = await items.calc(info.폴더);
|
||||
out.calcMissing = await items.calc("없는폴더");
|
||||
|
||||
// 일위대가 — 복사본 요약(복사본 계산) · 원본 층 요약(층 calc) · 넣을 값 = 담은 로직 입력 전부
|
||||
out.laborCopy = await project.laborOf(row(), items, info.폴더);
|
||||
out.laborOrigin = await project.laborOf(row(), null, null);
|
||||
out.laborNone = await project.laborOf(row({ 열: "없는열" }), null, null);
|
||||
const valueData = await project.laborValueData(["GA1", "GA2"], { GA1: {} });
|
||||
out.laborValues = {
|
||||
keys: state.logicKeys,
|
||||
link: valueData.link.줄.map((l) => `${l.로직}|${l.입력}|${l.자리}`),
|
||||
shown: model.pickedGroups(valueData.list, valueData.link).length,
|
||||
values: valueData.values,
|
||||
};
|
||||
|
||||
// 입력 목록 — 같은 항목은 한 번만 받음
|
||||
const first = await project.loadInputs(row());
|
||||
await project.loadInputs(row());
|
||||
|
||||
@@ -82,7 +82,7 @@ def test_재적용은_덮임을_돌려주고_계산_한_줄(out):
|
||||
assert out["reapplyCall"][0] == "POST /api/projects/P1/items/pipe%4085.05/reapply"
|
||||
assert out["reapply"]["covered"] == ["면적"] # 덮이는 것을 알림으로
|
||||
assert out["reapply"]["info"]["마스터바뀜"] is False
|
||||
assert out["calc"] == {"total": 12345.6, "gaps": 2, "stuck": 1}
|
||||
assert out["calc"] == {"total": 12345.6, "gaps": 2, "stuck": 1, "base": 100, "reflected": 300}
|
||||
assert out["calcMissing"] is None # 없는 복사본 = 계산 줄 없음
|
||||
|
||||
|
||||
@@ -146,8 +146,42 @@ def test_선택_입력_카드는_sub2_파일_한_벌(out):
|
||||
assert "export function createInputsValueCard(" in values # 모달 값 받기(view · value)
|
||||
assert "createInputsValueCard," in inputs and '} from "./M02_Labor_Values";' in inputs
|
||||
for name in ("ui_template_item_picker_detail.ts", "ui_template_item_picker_edit.ts"):
|
||||
text = (UI / name).read_text(encoding="utf-8")
|
||||
assert "M02_MasterTemplete_Inputs" in text and 'InputsCard"' not in text, name
|
||||
text = (UI / name).read_text(encoding="utf-8") # 모달은 값 카드를 바로 받음(39-1c)
|
||||
assert "M02_Labor_Values" in text and 'InputsCard"' not in text, name
|
||||
|
||||
|
||||
def test_일위대가_요약_복사본_원본_넣을_값_전부(out):
|
||||
"""39-1c — 복사본이면 복사본 일위대가 · 계산 · 아니면 원본 층 calc ·
|
||||
넣을 값 = 담은 로직 입력 전부."""
|
||||
assert out["laborCopy"] == {
|
||||
"번호": "SU-000007",
|
||||
"로직": ["GA1", "GA2"],
|
||||
"자재": 1,
|
||||
"base": 100,
|
||||
"reflected": 300,
|
||||
"gaps": 2,
|
||||
"stuck": 1,
|
||||
}
|
||||
assert out["laborOrigin"] == {
|
||||
"번호": None,
|
||||
"로직": ["GA1", "GA2"],
|
||||
"자재": 1,
|
||||
"base": 500,
|
||||
"reflected": 1500,
|
||||
"gaps": 0,
|
||||
"stuck": 1,
|
||||
}
|
||||
assert out["laborNone"] is None # 일위대가 없는 열(층 calc 404)
|
||||
values = out["laborValues"]
|
||||
assert values["keys"] == ["GA1", "GA2"]
|
||||
assert values["link"] == ["GA1|지역|선택", "GA1|소량감률|선택", "GA2|지역|선택"]
|
||||
assert values["shown"] == 2 and values["values"] == {"GA1": {}}
|
||||
|
||||
|
||||
def test_모달_카드_차례_도면_일위대가_면적():
|
||||
detail = (UI / "ui_template_item_picker_detail.ts").read_text(encoding="utf-8")
|
||||
assert 'import("./ui_template_item_picker_labor")' in detail
|
||||
assert "cards.root.insertBefore(labor.root, cards.root.children[1]" in detail
|
||||
|
||||
|
||||
def test_모달은_원본_층에_쓰는_길을_안_부름():
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
"""프로젝트 복사본 · 일위대가 문서(PLAN 39-1c · 임시 저장소 · 임시 마스터 사본).
|
||||
|
||||
- 층 `labor/<열>.json` 이 있으면 스냅샷 `일위대가.json` = {줄: 문서, 로직, 빠진로직, 알약} ·
|
||||
단가 묶음에 자재 MT
|
||||
- `입력값.json` 첫 값 = 문서 입력값(확인 거짓) · 재적용은 프로젝트에서 넣은 값이 이김
|
||||
- 계산 = 수량(손 · 면적 알약 칸) · 반영 = 기준 × 수량 ÷ 배 · 자재 = 기준 단가 × 수량 ·
|
||||
반영합 = 손셈 ·
|
||||
기준 · 반영이 원본 층 문서 계산(`M02_Labor_Doc.calc`)과 같음
|
||||
- 일위대가 쓰기는 400(읽기만)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from config import config_system
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Item_Copy as copies
|
||||
from M02_MasterTemplete import M02_Item_Snapshot as snap
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
from resources.tester.helper_m02_master_sample import SAMPLE
|
||||
|
||||
MASTER = config_system.PROJECT_ROOT / "resources/master_data"
|
||||
PIPE = {"layer": "system", "owner": None, "key": "SS000011", "name": "관공 관매설"}
|
||||
COLUMN = "pp_len"
|
||||
FOLDER = "pipe@40.0"
|
||||
LOGICS = ("GF000176", "GC000088", "GF000113")
|
||||
MATERIAL = "MT000008"
|
||||
HAND = 3 # GC000088 손 수량
|
||||
AREA_QTY = 7.5 # 자재 · GF000113 은 면적 알약 칸 값
|
||||
MAT_QTY = 4
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def master_copy(tmp_path_factory) -> Path:
|
||||
target = tmp_path_factory.mktemp("master_data")
|
||||
for path in MASTER.glob("*.json"):
|
||||
shutil.copy2(path, target / path.name)
|
||||
return target
|
||||
|
||||
|
||||
def _read(path: Path) -> Any:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _write(path: Path, content: Any) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(content, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
|
||||
def _first_input(code: str) -> tuple[str, Any]:
|
||||
whole = m01.loaded()[1]
|
||||
name, value = next(iter(m01.mau.values(whole, whole.logic(code)).items()))
|
||||
return name, int(value) if isinstance(value, Decimal) and value % 1 == 0 else value
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def world(tmp_path: Path, monkeypatch, master_copy: Path) -> dict[str, Any]:
|
||||
system = tmp_path / "master"
|
||||
system.mkdir()
|
||||
for name in (layers.COMBO_FILE, "자재조합.json"):
|
||||
shutil.copy2(SAMPLE / name, system / name)
|
||||
for kind in ("table", "structure", "area", "material"):
|
||||
shutil.copytree(SAMPLE / kind, system / kind)
|
||||
monkeypatch.setattr(layers, "SYSTEM_ROOT", system)
|
||||
monkeypatch.setattr(m01, "FOLDER", master_copy)
|
||||
storage = tmp_path / "storage"
|
||||
monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(storage))
|
||||
root = storage / "3" / "12" / "proj"
|
||||
(root / "B05_Profile/route").mkdir(parents=True)
|
||||
(root / "B04_PreProcess/drainage/edits").mkdir(parents=True)
|
||||
|
||||
name, value = _first_input("GC000088")
|
||||
doc = labor_doc.clean(
|
||||
{
|
||||
"열": COLUMN,
|
||||
"단위": "m",
|
||||
"담은로직": [
|
||||
{"로직": LOGICS[0]},
|
||||
{"로직": LOGICS[1], "입력값": {name: value}, "수량": {"손": HAND}},
|
||||
{"로직": LOGICS[2]},
|
||||
],
|
||||
"자재": [{"코드": MATERIAL}],
|
||||
}
|
||||
)
|
||||
_write(system / "labor" / f"{COLUMN}.json", doc)
|
||||
pills = {p["키"]: p for p in labor_doc.pills(doc)}
|
||||
area_pill = next(k for k, p in pills.items() if LOGICS[2] in p.get("로직", []))
|
||||
area = _read(system / "area" / f"{COLUMN}.json")
|
||||
sheet = area["시트"][0]
|
||||
sheet["칸"] = {"A1": {"값": AREA_QTY}, "A2": {"값": MAT_QTY}}
|
||||
sheet["연결"] = [
|
||||
{"키": area_pill, "칸": "A1"},
|
||||
{"키": labor_doc.material_key(MATERIAL), "칸": "A2"},
|
||||
]
|
||||
_write(system / "area" / f"{COLUMN}.json", area)
|
||||
return {
|
||||
"root": root,
|
||||
"doc": doc,
|
||||
"input": (name, value),
|
||||
"pills": pills,
|
||||
"area_pill": area_pill,
|
||||
}
|
||||
|
||||
|
||||
def _save(root: Path) -> dict[str, list[str]]:
|
||||
_write(root / "B05_Profile/route/structures.json", {"revision": 1, "structures": []})
|
||||
pipe = {"chainage_m": 40.0, "source": "user", "summary_item": PIPE}
|
||||
_write(root / "B04_PreProcess/drainage/edits/pipe_points.json", {"points": [pipe]})
|
||||
return snap.sync_snapshots(root)
|
||||
|
||||
|
||||
def _fill_auto(root: Path) -> None:
|
||||
"""빠진 입력을 자동값으로(확인 참) — 문서 입력값은 그대로 둠."""
|
||||
current = copies.read_file(root, FOLDER, "입력값")
|
||||
values = current["내용"]
|
||||
whole = m01.loaded()[1]
|
||||
for code in LOGICS:
|
||||
for key, auto in m01.mau.values(whole, whole.logic(code)).items():
|
||||
mine = values.setdefault(code, {})
|
||||
if key not in mine and auto is not None:
|
||||
mine[key] = {"값": labor_doc._json(auto), "확인": True}
|
||||
copies.write_file(root, FOLDER, "입력값", current["판"], values)
|
||||
|
||||
|
||||
def test_스냅샷_일위대가는_문서_알약_자재_단가(world):
|
||||
root = world["root"]
|
||||
assert _save(root)["만듦"] == [FOLDER]
|
||||
folder = root / "templates/structures" / FOLDER
|
||||
labor = _read(folder / "일위대가.json")
|
||||
assert labor["줄"] == world["doc"]
|
||||
assert [row["키"] for row in labor["로직"]] == list(LOGICS) and labor["빠진로직"] == []
|
||||
assert {p["키"] for p in labor["알약"]} == set(world["pills"])
|
||||
keys = {
|
||||
row["키"] for data in _read(folder / "단가.json").values() for row in data.get("줄", [])
|
||||
}
|
||||
assert MATERIAL in keys and set(LOGICS) <= keys
|
||||
|
||||
|
||||
def test_입력값_첫_값은_문서_입력값(world):
|
||||
root = world["root"]
|
||||
_save(root)
|
||||
name, value = world["input"]
|
||||
values = _read(root / "templates/structures" / FOLDER / "입력값.json")
|
||||
assert values == {LOGICS[1]: {name: {"값": value, "확인": False}}}
|
||||
|
||||
|
||||
def test_재적용은_프로젝트_값이_이김(world):
|
||||
root = world["root"]
|
||||
_save(root)
|
||||
name, _ = world["input"]
|
||||
current = copies.read_file(root, FOLDER, "입력값")
|
||||
mine = {LOGICS[1]: {name: {"값": 99, "확인": True}}}
|
||||
copies.write_file(root, FOLDER, "입력값", current["판"], mine)
|
||||
got = copies.reapply(root, FOLDER)
|
||||
assert got["값있음"] is True
|
||||
assert _read(root / "templates/structures" / FOLDER / "입력값.json") == mine
|
||||
|
||||
|
||||
def test_계산_반영_합은_손셈_원본과_같음(world):
|
||||
root = world["root"]
|
||||
_save(root)
|
||||
_fill_auto(root)
|
||||
got = copies.calc(root, FOLDER)
|
||||
assert got["멈춤"] == [], got["줄"]
|
||||
lines = {line["로직"]: line for line in got["줄"]}
|
||||
hand, area = lines[LOGICS[1]], lines[LOGICS[2]]
|
||||
assert (hand["수량"], hand["수량출처"]) == (HAND, "손")
|
||||
assert (area["수량"], area["수량출처"]) == (AREA_QTY, "칸")
|
||||
assert lines[LOGICS[0]]["수량출처"] == "없음" and lines[LOGICS[0]]["반영"] is None
|
||||
for line in (hand, area):
|
||||
expect = Decimal(str(line["계"])) * Decimal(str(line["수량"])) / Decimal(str(line["배"]))
|
||||
assert Decimal(str(line["반영"]["계"])) == pytest.approx(expect)
|
||||
(mat,) = got["자재"]
|
||||
assert mat["코드"] == MATERIAL and mat["수량"] == MAT_QTY and mat["기준단가"]
|
||||
assert mat["반영"] == pytest.approx(mat["기준단가"] * MAT_QTY)
|
||||
total = hand["반영"]["계"] + area["반영"]["계"] + mat["반영"]
|
||||
assert got["반영합"]["계"] == pytest.approx(total)
|
||||
# 원본 층 문서 계산(마스터 같은 사본)과 같음 — 입력값은 프로젝트에 채운 것
|
||||
values = _read(root / "templates/structures" / FOLDER / "입력값.json")
|
||||
doc = {
|
||||
**world["doc"],
|
||||
"담은로직": [
|
||||
{**one, "입력값": {k: v["값"] for k, v in values.get(one["로직"], {}).items()}}
|
||||
for one in world["doc"]["담은로직"]
|
||||
],
|
||||
}
|
||||
given = {world["area_pill"]: AREA_QTY, labor_doc.material_key(MATERIAL): MAT_QTY}
|
||||
origin = labor_doc.calc(doc, given)
|
||||
assert got["반영합"] == pytest.approx(origin["반영합"])
|
||||
assert got["기준합"] == pytest.approx(origin["기준합"])
|
||||
|
||||
|
||||
def test_일위대가_쓰기는_막힘(world):
|
||||
root = world["root"]
|
||||
_save(root)
|
||||
current = copies.read_file(root, FOLDER, "일위대가")
|
||||
assert current["내용"]["줄"]["열"] == COLUMN
|
||||
with pytest.raises(ValueError, match="원본"):
|
||||
copies.write_file(root, FOLDER, "일위대가", current["판"], current["내용"])
|
||||
@@ -269,3 +269,18 @@
|
||||
.ui-modal.ui-item-ask {
|
||||
width: min(28rem, 92vw);
|
||||
}
|
||||
|
||||
/* 일위대가 카드(39-1c) — 담은 로직 · 자재 수 · 기준 합 · 반영 합 글 */
|
||||
.ui-item-picker__labor-line {
|
||||
margin: 0 0 var(--spacing-4);
|
||||
color: var(--color-text-body);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.ui-item-picker__labor-line.is-strong {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.ui-item-picker__labor-line.is-warn {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* =============================================================================
|
||||
* ui_template_item_picker_detail.ts
|
||||
* 항목 고르기 모달 오른쪽 상세(계획서 25-2d · 26-3b) — **카드 넷**(도면 · 면적산출 · 재료산출 · 선택 입력).
|
||||
* 카드는 M02 와 같은 한 벌(`M02_MasterTemplete_ItemCards` · `_Inputs`)을 늦게 받아 씀
|
||||
* (B05 · B06 번들에 CAD · 스프레드시트가 안 섞이게 `import()`).
|
||||
* 항목 고르기 모달 오른쪽 상세(계획서 25-2d · 26-3b · 39-1c) — 카드 = 도면 · 일위대가 · 면적산출(· 재료산출) ·
|
||||
* 넣을 값. 카드는 M02 와 같은 한 벌(`M02_MasterTemplete_ItemCards` · `M02_Labor_Values`) + 일위대가 카드
|
||||
* (`ui_template_item_picker_labor`)를 늦게 받아 씀(B05 · B06 번들에 CAD · 스프레드시트가 안 섞이게 `import()`).
|
||||
* 넣을 값 = 일위대가 담은 로직 입력 전부(복사본이면 복사본 입력값).
|
||||
*
|
||||
* 자료 자리 — **그 구조물의 복사본**(폴더 = 구조물 id · 같은 항목을 고른 구조물도 복사본은 따로)을 읽고,
|
||||
* 그 구조물에 아직 없으면 **원본 층**을 읽음(읽기만). [수정] 은 복사본만 고침 — 아직이면 「이 구조물로 가져와 고침」
|
||||
@@ -12,15 +13,12 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showConfirmDialog, showToast } from "./ui_template_elements";
|
||||
import {
|
||||
readInputLinks,
|
||||
readTemplate,
|
||||
type InputLinks,
|
||||
} from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
||||
import { readTemplate } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
||||
import type { EditKind, ItemCardsHandle } from "../M02_MasterTemplete/M02_MasterTemplete_ItemCards";
|
||||
import type { ItemSource } from "../M02_MasterTemplete/M02_MasterTemplete_ItemSource";
|
||||
import type { InputsValueHandle } from "../M02_MasterTemplete/M02_MasterTemplete_Inputs";
|
||||
import type { ValueDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Inputs_Model";
|
||||
import type { InputsValueHandle } from "../M02_MasterTemplete/M02_Labor_Values";
|
||||
import type { ValueDoc } from "../M02_MasterTemplete/M02_Labor_Values_Model";
|
||||
import type { LaborCardHandle } from "./ui_template_item_picker_labor";
|
||||
import { askFields } from "./ui_template_item_picker_ask";
|
||||
import {
|
||||
folderLines,
|
||||
@@ -32,7 +30,8 @@ import {
|
||||
type PickerRow,
|
||||
} from "./ui_template_item_picker_data";
|
||||
import {
|
||||
loadInputs,
|
||||
laborOf,
|
||||
laborValueData,
|
||||
PART_OF,
|
||||
projectItems,
|
||||
sameItem,
|
||||
@@ -118,6 +117,7 @@ const TEXT = {
|
||||
재료: "재료산출",
|
||||
자재조합: "자재조합",
|
||||
입력연결: "선택 입력 자리",
|
||||
일위대가: "일위대가",
|
||||
} as Record<string, string>,
|
||||
};
|
||||
|
||||
@@ -137,6 +137,7 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
||||
let infos: ProjectItemInfo[] | null = null;
|
||||
let cards: ItemCardsHandle | null = null;
|
||||
let inputs: InputsValueHandle | null = null;
|
||||
let labor: LaborCardHandle | null = null;
|
||||
let source: ItemSource = { layer: "system", projectId: null, read: () => Promise.reject() };
|
||||
/** 이 구조물에 있는 복사본 — 고른 항목의 것일 때만(다른 항목의 것이면 `other`) */
|
||||
let info: ProjectItemInfo | null = null;
|
||||
@@ -301,34 +302,36 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
||||
opts.onEdit({ kind, row, info, items, projectId: opts.projectId });
|
||||
}
|
||||
|
||||
async function paintInputs(row: PickerRow, mine: number): Promise<void> {
|
||||
if (!inputs) return;
|
||||
const stop = inputs.loading(); // 카드 안 「불러오는 중」(M02 31-7)
|
||||
/** 일위대가 카드 · 넣을 값 카드 — 복사본이 있으면 복사본(값도) · 아니면 원본 층(39-1c) */
|
||||
async function paintLabor(row: PickerRow, mine: number): Promise<void> {
|
||||
if (!inputs || !labor) return;
|
||||
const stops = [labor.loading(), inputs.loading()]; // 카드 안 「불러오는 중」(M02 31-7)
|
||||
try {
|
||||
const list = await loadInputs(row);
|
||||
let link: InputLinks | null;
|
||||
let values: ValueDoc = {};
|
||||
if (info && items) {
|
||||
// 복사본 것(가져온 때 그대로) — 값도 복사본에서
|
||||
link = (await items.read(info.폴더, "입력연결")) as InputLinks | null;
|
||||
values = ((await items.read(info.폴더, "입력값")) as ValueDoc | null) ?? {};
|
||||
} else {
|
||||
link = await readInputLinks(layerOf(row), row.key).catch(() => null);
|
||||
}
|
||||
stop();
|
||||
if (mine === seq) inputs.set({ list, link, values });
|
||||
const copy = info && items ? info.폴더 : null;
|
||||
const summary = await laborOf(row, copy ? items : null, copy);
|
||||
const values = copy ? (((await items?.read(copy, "입력값")) as ValueDoc | null) ?? {}) : {};
|
||||
const data = summary ? await laborValueData(summary.로직, values) : null;
|
||||
stops.forEach((stop) => stop());
|
||||
if (mine !== seq) return;
|
||||
labor.set(summary);
|
||||
inputs.set(data);
|
||||
} catch (error) {
|
||||
stop();
|
||||
if (mine === seq) inputs.set(null, why(error), () => void paintInputs(row, seq));
|
||||
stops.forEach((stop) => stop());
|
||||
if (mine !== seq) return;
|
||||
const retry = (): void => void paintLabor(row, seq);
|
||||
labor.set(null, why(error), retry);
|
||||
inputs.set(null, why(error), retry);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureCards(): Promise<void> {
|
||||
if (cards && inputs) return;
|
||||
const [cardModule, inputModule] = await Promise.all([
|
||||
if (cards && inputs && labor) return;
|
||||
const [cardModule, inputModule, laborModule] = await Promise.all([
|
||||
import("../M02_MasterTemplete/M02_MasterTemplete_ItemCards"),
|
||||
import("../M02_MasterTemplete/M02_MasterTemplete_Inputs"),
|
||||
import("../M02_MasterTemplete/M02_Labor_Values"),
|
||||
import("./ui_template_item_picker_labor"),
|
||||
]);
|
||||
labor = laborModule.createLaborCard();
|
||||
inputs = inputModule.createInputsValueCard({ mode: "view", onEdit: () => void edit("inputs") });
|
||||
cards = cardModule.createItemCards({
|
||||
source: () => source,
|
||||
@@ -337,6 +340,8 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
||||
warnOnFail: () => false,
|
||||
extraCards: [inputs.root],
|
||||
});
|
||||
// 차례 = 도면 · 일위대가 · 면적산출 — 일위대가 카드를 도면 카드 바로 뒤에
|
||||
cards.root.insertBefore(labor.root, cards.root.children[1] ?? null);
|
||||
cardsHost.replaceChildren(cards.root);
|
||||
}
|
||||
|
||||
@@ -349,6 +354,7 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
||||
cardsHost.replaceChildren();
|
||||
cards = null;
|
||||
inputs = null;
|
||||
labor = null;
|
||||
return;
|
||||
}
|
||||
paintHead(row);
|
||||
@@ -365,14 +371,14 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
||||
other = own && !info ? own : null;
|
||||
source = makeSource(row);
|
||||
await ensureCards();
|
||||
if (mine !== seq || !cards || !inputs) return;
|
||||
if (mine !== seq || !cards || !inputs || !labor) return;
|
||||
const label = !info && canCopy ? TEXT.importEdit : null;
|
||||
cards.relabel({ structure: label, area: label, material: label });
|
||||
inputs.setEditLabel(label ?? "선택 입력 수정");
|
||||
cards.root.hidden = false;
|
||||
paintStatus(row, mine);
|
||||
void cards.load({ id: row.열, label: itemName(row) });
|
||||
void paintInputs(row, mine);
|
||||
void paintLabor(row, mine);
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ import { createButton, el, showConfirmDialog, showToast } from "./ui_template_el
|
||||
import type { SpreadsheetHandle, Workbook } from "../A00_Common/spreadsheet/spreadsheet";
|
||||
import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links";
|
||||
import type { DrawingTemplateHandle } from "../M02_MasterTemplete/M02_MasterTemplete_Drawing";
|
||||
import type { InputsValueHandle } from "../M02_MasterTemplete/M02_MasterTemplete_Inputs";
|
||||
import type { InputsValueHandle } from "../M02_MasterTemplete/M02_Labor_Values";
|
||||
import { pillsOf, type ValueDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Inputs_Model";
|
||||
import type { InputLinks, StructureDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
||||
import type { EditRequest } from "./ui_template_item_picker_detail";
|
||||
import { itemName } from "./ui_template_item_picker_data";
|
||||
import { loadInputs, PART_OF } from "./ui_template_item_picker_project";
|
||||
import { laborValueData, loadInputs, PART_OF } from "./ui_template_item_picker_project";
|
||||
|
||||
export interface EditView {
|
||||
root: HTMLElement;
|
||||
@@ -146,15 +146,18 @@ export function createEditView(req: EditRequest, onBack: (saved: boolean) => voi
|
||||
});
|
||||
}
|
||||
|
||||
/** 넣을 값 — 복사본 일위대가 담은 로직 입력 전부(39-1c) */
|
||||
async function mountInputs(): Promise<void> {
|
||||
const [{ createInputsValueCard }, list, link] = await Promise.all([
|
||||
import("../M02_MasterTemplete/M02_MasterTemplete_Inputs"),
|
||||
loadInputs(row),
|
||||
linkOf(),
|
||||
const [{ createInputsValueCard }, labor, values] = await Promise.all([
|
||||
import("../M02_MasterTemplete/M02_Labor_Values"),
|
||||
items.read(info.폴더, "일위대가") as Promise<{
|
||||
줄?: { 담은로직?: { 로직?: string }[] };
|
||||
} | null>,
|
||||
items.read(info.폴더, "입력값") as Promise<ValueDoc | null>,
|
||||
]);
|
||||
const values = ((await items.read(info.폴더, "입력값")) as ValueDoc | null) ?? {};
|
||||
const logics = (labor?.줄?.담은로직 ?? []).map((one) => String(one.로직 ?? "")).filter(Boolean);
|
||||
card = createInputsValueCard({ mode: "value", onChange: () => setDirty(true) });
|
||||
card.set({ list, link, values });
|
||||
card.set(await laborValueData(logics, values ?? {}));
|
||||
body.replaceChildren(
|
||||
el("div", { className: "ui-item-picker__edit-inputs", children: [card.root] }),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/* =============================================================================
|
||||
* ui_template_item_picker_labor.ts
|
||||
* 항목 고르기 모달 「일위대가」 카드(계획서 39-1c) — 담은 로직 수 · 자재 수 · 기준 합 · 반영 합 글.
|
||||
* 보기만 · 고치기는 원본 층 일위대가 상세. 일위대가 보기 부품(39-3 C)이 오면 몸을 그것으로 바꿈.
|
||||
* 자료 = `ui_template_item_picker_project.laborOf`(복사본이면 복사본 계산 · 아니면 원본 층 계산).
|
||||
* 상세(`ui_template_item_picker_detail.ts`)가 카드를 받을 때 함께 `import()`.
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "./ui_template_elements";
|
||||
import { cardNote, cardShell } from "../M02_MasterTemplete/M02_MasterTemplete_CardShell";
|
||||
import type { LaborSummary } from "./ui_template_item_picker_project";
|
||||
|
||||
const TEXT = {
|
||||
title: "일위대가",
|
||||
none: "일위대가 없음",
|
||||
failed: "일위대가를 못 읽음 — {value}",
|
||||
logics: "담은 로직 {value}개",
|
||||
materials: "자재 {value}개",
|
||||
base: "기준 합 {value}원",
|
||||
reflected: "반영 합 {value}원",
|
||||
gaps: "빠진 입력 {value}개",
|
||||
stuck: "멈춘 로직 {value}개",
|
||||
};
|
||||
|
||||
const fill = (text: string, value: string | number): string =>
|
||||
text.replace("{value}", String(value));
|
||||
const won = (value: number | null): string =>
|
||||
value === null ? "—" : Math.round(value).toLocaleString();
|
||||
|
||||
export interface LaborCardHandle {
|
||||
root: HTMLElement;
|
||||
loading: () => () => void;
|
||||
/** `null` = 일위대가 없음 · `note` 가 있으면 못 읽은 까닭 */
|
||||
set: (summary: LaborSummary | null, note?: string, retry?: () => void) => void;
|
||||
}
|
||||
|
||||
export function createLaborCard(): LaborCardHandle {
|
||||
const number = el("span", { className: "m02-struct__no" });
|
||||
const body = el("div", { className: "m02-struct__sheet ui-item-picker__labor" });
|
||||
const shell = cardShell({ title: TEXT.title, sub: [number], body });
|
||||
const line = (text: string, className = ""): HTMLElement =>
|
||||
el("p", { className: `ui-item-picker__labor-line ${className}`.trim(), text });
|
||||
|
||||
return {
|
||||
root: shell.root,
|
||||
loading: () => shell.loading(),
|
||||
set: (summary, note, retry) => {
|
||||
number.textContent = summary?.번호 ?? "";
|
||||
if (note && retry) return shell.fail(fill(TEXT.failed, note), retry);
|
||||
if (!summary) return void body.replaceChildren(cardNote(TEXT.none));
|
||||
body.replaceChildren(
|
||||
line(`${fill(TEXT.logics, summary.로직.length)} · ${fill(TEXT.materials, summary.자재)}`),
|
||||
line(fill(TEXT.base, won(summary.base))),
|
||||
line(fill(TEXT.reflected, won(summary.reflected)), "is-strong"),
|
||||
...(summary.gaps ? [line(fill(TEXT.gaps, summary.gaps), "is-warn")] : []),
|
||||
...(summary.stuck ? [line(fill(TEXT.stuck, summary.stuck), "is-warn")] : []),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -9,20 +9,24 @@
|
||||
* GET|PUT /api/projects/{id}/items/{폴더}/{부분} → {종류, 판, 내용} · 고침은 {판, 내용}(판이 다르면 409)
|
||||
* POST …/{폴더}/reapply 재적용 → {폴더, 가져온날, 원본판, 덮임: [부분]}
|
||||
* POST …/{폴더}/save-as {to, 경로, 이름} 개인에 저장 → {key, 열, 이름, summary_item}
|
||||
* POST …/{폴더}/calc 복사본만으로 계산(계 · 빠진 입력)
|
||||
* POST …/{폴더}/calc 복사본만으로 계산(계 · 빠진 입력 · 기준합 · 반영합)
|
||||
* 입력 목록 · 항목 연결(sub4 · 25-2a) — `M02_MasterTemplete_Api_Fetch` 의 `fetchItemInputs` · `readInputLinks`
|
||||
* 일위대가(39-1c) — 복사본 `일위대가` 부분 · 원본 층은 `calcLabor` · 넣을 값 목록은 `fetchLogicInputs`
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import {
|
||||
fetchItemInputs,
|
||||
type InputLinks,
|
||||
type ItemInputs,
|
||||
} from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
||||
import { calcLabor, fetchLogicInputs } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Api";
|
||||
import type { ValueDoc } from "../M02_MasterTemplete/M02_Labor_Values_Model";
|
||||
import { layerOf, ownerOf, type PickerRow } from "./ui_template_item_picker_data";
|
||||
|
||||
/** 복사본 폴더 안 부분 — 25-1b 약속 그대로 */
|
||||
export type ProjectPart =
|
||||
"도면" | "면적" | "재료" | "자재조합" | "입력연결" | "입력값" | "고른자재";
|
||||
"도면" | "면적" | "재료" | "자재조합" | "입력연결" | "입력값" | "고른자재" | "일위대가";
|
||||
|
||||
/** 원본 종류 → 복사본 부분 */
|
||||
export const PART_OF: Record<"structure" | "area" | "material", ProjectPart> = {
|
||||
@@ -66,11 +70,13 @@ export interface CreatedItem {
|
||||
summary_item: { layer: string; owner: string | null; key: string; name: string };
|
||||
}
|
||||
|
||||
/** 복사본 계산 결과에서 모달이 보이는 것 — 계 · 빠진 입력 수 · 멈춘 로직 수 */
|
||||
/** 복사본 계산 결과에서 모달이 보이는 것 — 계 · 빠진 입력 수 · 멈춘 로직 수 · 일위대가 기준합 · 반영합 계 */
|
||||
export interface ProjectCalc {
|
||||
total: number;
|
||||
gaps: number;
|
||||
stuck: number;
|
||||
base: number | null;
|
||||
reflected: number | null;
|
||||
}
|
||||
|
||||
interface Reply {
|
||||
@@ -217,11 +223,19 @@ export function projectItems(projectId: string): ProjectItems {
|
||||
calc: async (folder) => {
|
||||
const reply = await call(`${base}/${enc(folder)}/calc`, post({}));
|
||||
if (reply.status !== 200) return null;
|
||||
const got = reply.body as { 계?: number; 빠진입력?: unknown[]; 멈춤?: unknown[] };
|
||||
const got = reply.body as {
|
||||
계?: number;
|
||||
빠진입력?: unknown[];
|
||||
멈춤?: unknown[];
|
||||
기준합?: { 계?: number };
|
||||
반영합?: { 계?: number };
|
||||
};
|
||||
return {
|
||||
total: Number(got.계 ?? 0),
|
||||
gaps: got.빠진입력?.length ?? 0,
|
||||
stuck: got.멈춤?.length ?? 0,
|
||||
base: got.기준합?.계 ?? null,
|
||||
reflected: got.반영합?.계 ?? null,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -242,3 +256,67 @@ export function loadInputs(row: PickerRow): Promise<ItemInputs> {
|
||||
made.catch(() => inputsCache.delete(id));
|
||||
return made;
|
||||
}
|
||||
|
||||
/* ── 일위대가(39-1c) — 카드 요약 · 넣을 값 자료 ─────────────────────────────── */
|
||||
|
||||
/** 일위대가 카드에 보이는 것 — 복사본이면 복사본 계산 · 아니면 원본 층 계산(저장 안 함) */
|
||||
export interface LaborSummary {
|
||||
번호: string | null;
|
||||
/** 담은 로직 키(차례 그대로) — 넣을 값 목록을 받는 데도 씀 */
|
||||
로직: string[];
|
||||
자재: number;
|
||||
base: number | null;
|
||||
reflected: number | null;
|
||||
gaps: number;
|
||||
stuck: number;
|
||||
}
|
||||
|
||||
interface LaborCopy {
|
||||
줄?: { 일위대가번?: string; 담은로직?: { 로직?: string }[]; 자재?: unknown[] };
|
||||
}
|
||||
|
||||
/** 그 항목의 일위대가 — 복사본(`folder`)이 있으면 그것 · 없으면 원본 층 열 · 일위대가가 없으면 null */
|
||||
export async function laborOf(
|
||||
row: PickerRow,
|
||||
items: ProjectItems | null,
|
||||
folder: string | null,
|
||||
): Promise<LaborSummary | null> {
|
||||
if (items && folder) {
|
||||
const copy = (await items.read(folder, "일위대가")) as LaborCopy | null;
|
||||
if (!copy?.줄) return null;
|
||||
const got = await items.calc(folder);
|
||||
return {
|
||||
번호: copy.줄.일위대가번 ?? null,
|
||||
로직: (copy.줄.담은로직 ?? []).map((one) => String(one.로직 ?? "")).filter(Boolean),
|
||||
자재: copy.줄.자재?.length ?? 0,
|
||||
base: got?.base ?? null,
|
||||
reflected: got?.reflected ?? null,
|
||||
gaps: got?.gaps ?? 0,
|
||||
stuck: got?.stuck ?? 0,
|
||||
};
|
||||
}
|
||||
const got = await calcLabor(layerOf(row), row.열, null).catch(() => null);
|
||||
if (!got) return null;
|
||||
return {
|
||||
번호: null,
|
||||
로직: got.줄.map((line) => line.로직),
|
||||
자재: got.자재.length,
|
||||
base: got.기준합.계 ?? null,
|
||||
reflected: got.반영합.계 ?? null,
|
||||
gaps: 0,
|
||||
stuck: got.줄.filter((line) => line.까닭).length,
|
||||
};
|
||||
}
|
||||
|
||||
/** 넣을 값 카드 자료 — 담은 로직 입력 전부(자리 「선택」 으로 감싸 값 카드가 다 보임) */
|
||||
export async function laborValueData(
|
||||
logics: string[],
|
||||
values: ValueDoc,
|
||||
): Promise<{ list: ItemInputs; link: InputLinks; values: ValueDoc }> {
|
||||
const got = await fetchLogicInputs(logics);
|
||||
const list: ItemInputs = { 일위대가: null, 로직: got.로직, 빠진로직: got.빠진로직 };
|
||||
const 줄 = list.로직.flatMap((logic) =>
|
||||
logic.입력.map((input) => ({ 로직: logic.로직, 입력: input.이름, 자리: "선택" as const })),
|
||||
);
|
||||
return { list, link: { 판: null, 줄 }, values };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user