feat(M02): 단가산출 승격 서고 지움 · 옛 단가승격 참조 줄 = 읽을 때 로직 줄 + 승격(134-2)
같은 로직 줄이 이미 있으면 옛 참조 줄 그대로(브레인 승인 ③) · 마스터 · 개인 파일 안 고침 · 프로젝트 사본 셈 그대로. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY
This commit is contained in:
@@ -221,19 +221,19 @@ def check_saved(files: dict, changed: list[str], book: dict, narrowed=None) -> l
|
||||
|
||||
|
||||
def _labor_files() -> list[tuple[str, Path]]:
|
||||
"""모든 층(마스터 · 회사 · 개인)의 일위대가 `labor/<열>.json` · 단가승격 `promote/<키>.json`
|
||||
· 사전조합 `precombo/<키>.json`(51-11) — (어디, 파일)."""
|
||||
"""모든 층(마스터 · 회사 · 개인)의 일위대가 `labor/<열>.json` · 사전조합 `precombo/<키>.json`(51-11) —
|
||||
(어디, 파일)."""
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers # M02 가 M01 을 부름 — 늦게
|
||||
|
||||
return [
|
||||
(f"{what} {where}", path)
|
||||
for kind, what in (("labor", "일위대가"), ("promote", "단가승격"), ("precombo", "사전조합"))
|
||||
for kind, what in (("labor", "일위대가"), ("precombo", "사전조합"))
|
||||
for where, path in layers.layer_files(kind)
|
||||
]
|
||||
|
||||
|
||||
def labor_errors(whole) -> list[str]:
|
||||
"""일위대가 · 단가승격 · 사전조합 문서가 담은 로직이 마스터에 없음(39-1d · 50-3) —
|
||||
"""일위대가 · 사전조합 문서가 담은 로직 마스터에 없음(39-1d · 50-3) —
|
||||
로직 지우기를 막음 · 어디서 쓰는지 알림 · 참조 줄(로직 없음)은 건너뜀. 전부터 깨진 것은
|
||||
`save` 가 고치기 전 검사와 견주어 뺌."""
|
||||
out = []
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""M02 일위대가 조합 서고 — 단가승격 · 사전조합 · 참조(PLAN 50-3 · 조사 갈래 A).
|
||||
"""M02 일위대가 사전조합 서고 · 참조(PLAN 50-3 · 조사 갈래 A · 134-2 단가승격 서고 지움).
|
||||
|
||||
자리 = 층 양식 길 새 종류 둘(`M02_Template_Layers.LIBRARY_KINDS`) — 파일 이름 = 키:
|
||||
promote `{층}/promote/SP-######.json` 단가승격
|
||||
`{종류:"단가승격", 키, 이름, 단위, 배, 본뜬, 담은로직:[{로직, 입력값}]}` —
|
||||
줄 하나 · 입력값은 승격 때 고정 · 값 = 로직 기준 ÷ 배(1 단위 값 · `배` 는 저장 때)
|
||||
자리 = 층 양식 길 종류(`M02_Template_Layers.LIBRARY_KINDS`) — 파일 이름 = 키:
|
||||
promote 옛 단가승격 `{층}/promote/SP-######.json`(134-2 · 새로 안 만듦 · 읽기 · 셈만) —
|
||||
`{종류:"단가승격", 키, 이름, 단위, 배, 담은로직:[{로직, 입력값}]}` · 값 = 로직 기준 ÷ 배 ·
|
||||
층 문서를 읽을 때 옛 참조 줄 = 로직 줄 + `승격`(`unpromote`) · 프로젝트 사본은 그대로 셈
|
||||
precombo `{층}/precombo/SC-######.json` 사전조합
|
||||
`{종류:"사전조합", 키, 이름, 단위, 본뜬, 담은로직:[{참조, 수량:{손}, 메모}
|
||||
| {로직, 입력값, 메모, 수량:{손}}]}` — 승격 · 사전조합 · 단가산출 로직(51-11) ·
|
||||
@@ -27,8 +27,8 @@ from typing import Any
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
PREFIX = {"promote": "SP", "precombo": "SC"}
|
||||
TITLE = {"promote": "단가승격", "precombo": "사전조합"}
|
||||
PREFIX = {"precombo": "SC"}
|
||||
TITLE = {"precombo": "사전조합"}
|
||||
DEPTH = 8 # 참조 깊이 상한(저장 · 계산 둘 다)
|
||||
REF_HEAD = "참조|"
|
||||
_KEY = re.compile(r"^(SP|SC)-(\d{6})$")
|
||||
@@ -133,61 +133,61 @@ def check_key(kind: str, name: str) -> None:
|
||||
|
||||
def clean_doc(kind: str, doc: Any) -> dict[str, Any]:
|
||||
"""서고 문서를 서버가 받는 꼴로 — `doc["키"]` = 파일 이름(부르는 쪽이 넣음)."""
|
||||
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
|
||||
if not isinstance(doc, dict):
|
||||
raise _bad("서고 문서가 묶음 아님")
|
||||
key = str(doc.get("키") or "")
|
||||
check_key(kind, key)
|
||||
name, unit = str(doc.get("이름") or "").strip(), str(doc.get("단위") or "").strip()
|
||||
out: dict[str, Any] = {"종류": TITLE[kind], "키": key}
|
||||
if kind == "promote":
|
||||
lines = doc.get("담은로직") or []
|
||||
if len(lines) != 1 or not isinstance(lines[0], dict) or not lines[0].get("로직"):
|
||||
raise _bad("단가승격은 단가산출 로직 한 줄")
|
||||
line = labor.clean({"담은로직": lines})["담은로직"][0]
|
||||
pill = (combo_store.combo_pills({"담은로직": [line]})["물량"] or [{}])[0]
|
||||
out.update(
|
||||
이름=name or pill.get("이름") or line["로직"],
|
||||
단위=unit or pill.get("단위") or None,
|
||||
배=pill.get("배") or 1, # ponytail: 저장 때 적음 · 마스터 결과단위가 바뀌면 다시 저장
|
||||
담은로직=[{k: line[k] for k in ("로직", "입력값", "켜쌓기율") if k in line}],
|
||||
**origin(line["로직"]),
|
||||
)
|
||||
else:
|
||||
if not name:
|
||||
raise _bad("사전조합 이름이 빔")
|
||||
out.update(이름=name, 단위=unit or None, 담은로직=_combo_lines(doc, key))
|
||||
out.update({k: str(doc.get(k) or "").strip() for k in ("구분", "상세구분")})
|
||||
if not name:
|
||||
raise _bad("사전조합 이름이 빔")
|
||||
out.update(이름=name, 단위=unit or None, 담은로직=_combo_lines(doc, key))
|
||||
out.update({k: str(doc.get(k) or "").strip() for k in ("구분", "상세구분")})
|
||||
out["본뜬"] = doc["본뜬"] if isinstance(doc.get("본뜬"), dict) else None
|
||||
return out
|
||||
|
||||
|
||||
def origin(code: str) -> dict[str, Any]:
|
||||
"""단가승격 칸 — 원 로직의 구분 · 상세구분 + 연결 {로직 키, 출처 글}(51-12 · 없으면 빈 글)."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
try:
|
||||
got = m01.loaded()[1].logic(code)
|
||||
except m01.cm.mf.FormulaError:
|
||||
got = {}
|
||||
return {
|
||||
"구분": got.get("구분") or "",
|
||||
"상세구분": got.get("상세구분") or "",
|
||||
"연결": {"로직": code, "출처": got.get("출처") or ""},
|
||||
}
|
||||
|
||||
|
||||
def filled(kind: str, doc: Any) -> Any:
|
||||
"""옛 서고 문서(51-12 앞) 읽을 때 칸 채움 — 승격 = 원 로직 것 · 사전조합 = 빈 글(화면 몫)."""
|
||||
"""옛 서고 문서(51-12 앞) 읽을 때 칸 채움 — 사전조합 = 빈 글(화면 몫)."""
|
||||
if not isinstance(doc, dict) or kind not in PREFIX or "구분" in doc:
|
||||
return doc
|
||||
if kind == "precombo":
|
||||
return {**doc, "구분": "", "상세구분": ""}
|
||||
line = (doc.get("담은로직") or [{}])[0]
|
||||
code = str(line.get("로직") or "") if isinstance(line, dict) else ""
|
||||
return {**doc, **origin(code)}
|
||||
return {**doc, "구분": "", "상세구분": ""}
|
||||
|
||||
|
||||
def unpromote(kind: str, doc: Any) -> Any:
|
||||
"""층 문서를 읽을 때(134-2) — 옛 단가승격 참조 줄 → 로직 줄 `{로직, 입력값, 켜쌓기율, 수량}` +
|
||||
일위대가면 `승격: true`(사전조합 안 로직 줄엔 없음) · 값 같음(승격 값 = 기준 ÷ 배 = 로직 줄 반영) ·
|
||||
같은 로직 줄이 이미 있으면 그 줄은 옛 참조 그대로(읽기 · 셈 · 저장 됨) · 파일은 안 고침([저장] 때 굳음)."""
|
||||
lines = doc.get("담은로직") if isinstance(doc, dict) and kind in ("labor", "precombo") else None
|
||||
if not isinstance(lines, list):
|
||||
return doc
|
||||
have = {str(one.get("로직")) for one in lines if isinstance(one, dict) and one.get("로직")}
|
||||
out, changed = [], False
|
||||
for one in lines:
|
||||
ref = one.get("참조") if isinstance(one, dict) else None
|
||||
src = (
|
||||
read_ref(ref) if isinstance(ref, dict) and kind_of(ref.get("키")) == "promote" else None
|
||||
)
|
||||
line = ((src or {}).get("담은로직") or [None])[0]
|
||||
code = str(line.get("로직") or "") if isinstance(line, dict) else ""
|
||||
if not code or code in have:
|
||||
out.append(one)
|
||||
continue
|
||||
have.add(code)
|
||||
changed = True
|
||||
# ponytail: 수량 null(면적산출 칸 연결)은 알약 키가 로직 것으로 바뀌어 칸을 다시 이어야 함
|
||||
out.append(
|
||||
{
|
||||
"로직": code,
|
||||
"별칭": one.get("별칭"),
|
||||
"메모": one.get("메모"),
|
||||
"입력값": dict(line.get("입력값") or {}),
|
||||
"수량": one.get("수량"),
|
||||
**{k: line[k] for k in ("켜쌓기율", "고름") if line.get(k) is not None},
|
||||
**({"승격": True} if kind == "labor" else {}),
|
||||
}
|
||||
)
|
||||
return {**doc, "담은로직": out} if changed else doc
|
||||
|
||||
|
||||
def _combo_lines(doc: dict, key: str) -> list[dict[str, Any]]:
|
||||
@@ -314,7 +314,7 @@ def carry(doc: dict[str, Any], source: Here) -> dict[str, Any]:
|
||||
return ref
|
||||
if ref_id(ref) in done:
|
||||
return done[ref_id(ref)]
|
||||
if depth > DEPTH or not readable(source, ref):
|
||||
if depth > DEPTH or not readable(source, ref) or kind_of(ref.get("키")) == "promote":
|
||||
raise _bad(f"복사할 수 없는 참조 「{ref.get('키')}」")
|
||||
got = read_ref(ref)
|
||||
if got is None:
|
||||
|
||||
@@ -81,7 +81,7 @@ def _copy(kind: str, doc: dict[str, Any], source: refs.Here, ref: dict) -> dict[
|
||||
@router.post("/layers/{layer}/library/{kind}")
|
||||
def new_library(
|
||||
layer: Layer,
|
||||
kind: Literal["promote", "precombo"],
|
||||
kind: Literal["precombo"],
|
||||
body: LibraryBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
|
||||
@@ -10,8 +10,8 @@ export type Layer = "system" | "company" | "personal" | "project";
|
||||
/** 서버 종류 — 화면의 「표 양식」 = table · 「도면 양식」 = drawing · 「구조물 도면」 = structure ·
|
||||
* 「면적산출」 = area(구조물집계표 열마다 한 통합문서) ·
|
||||
* 「일위대가」 = labor(열마다 문서 · 39장) ·
|
||||
* 「단가승격」 = promote · 「사전조합」 = precombo(일위대가 조합 서고 · 50-3 · 키가 이름) */
|
||||
export type Kind = "table" | "drawing" | "structure" | "area" | "labor" | "promote" | "precombo";
|
||||
* 「사전조합」 = precombo(일위대가 사전조합 서고 · 50-3 · 키가 이름 · 옛 단가승격 promote 는 134-2 에서 뺌) */
|
||||
export type Kind = "table" | "drawing" | "structure" | "area" | "labor" | "precombo";
|
||||
|
||||
export interface TemplateInfo {
|
||||
종류: Kind;
|
||||
@@ -131,7 +131,7 @@ export type LibraryBody =
|
||||
{ 문서: Record<string, unknown> } | { 본뜬: { 층: Layer; 주인: string | null; 키: string } };
|
||||
export const newLibrary = (
|
||||
layer: Layer,
|
||||
kind: "promote" | "precombo",
|
||||
kind: "precombo",
|
||||
body: LibraryBody,
|
||||
): Promise<TemplateInfo> =>
|
||||
call(owned(layer, `/layers/${layer}/library/${kind}`), {
|
||||
|
||||
@@ -242,14 +242,14 @@ export interface LibraryRow {
|
||||
이름: string;
|
||||
제목?: string | null;
|
||||
단위?: string | null;
|
||||
/** 단가승격 = 원 로직 것 · 사전조합 = 사용자가 고름(51-12) */
|
||||
/** 사전조합 = 사용자가 고름(51-12) */
|
||||
구분?: string | null;
|
||||
상세구분?: string | null;
|
||||
}
|
||||
/** 서고 목록 — 그 층(주인) 단가승격 `promote` · 사전조합 `precombo` 문서 */
|
||||
/** 서고 목록 — 그 층(주인) 사전조합 `precombo` 문서 */
|
||||
export const fetchLibrary = async (
|
||||
ref: Omit<LaborRef, "키">,
|
||||
kind: "promote" | "precombo",
|
||||
kind: "precombo",
|
||||
): Promise<LibraryRow[]> => {
|
||||
const q = ref.주인 && ref.층 !== "system" ? `?owner=${enc(ref.주인)}` : "";
|
||||
const got = await call<{ 양식: (LibraryRow & { 종류: string })[] }>(
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
* · 자재목록 = 로직 자재 줄(103-1 · `Labor_LogicMats` · [바꾸기] → `담은로직[].고름` · 입력이 바뀌면 걸린 고름 풂) +
|
||||
* 사용자 자재 줄([삭제]) · 로직 자재와 같은 품목은 사용자 자재로 못 담음
|
||||
* · 오른쪽 「원문」 접는 패널(47-4 · 상세만 · 공용 `ui_template_side_panels`) = 고른 줄 로직의 출처 원문
|
||||
* · 서고 모드(50-3 · `mode`) = 단가승격 · 사전조합 — 이름 칸 · 단가산출 로직 표 · 원문만(자재 · 면적 · 다이어그램 없음) ·
|
||||
* 정보 칸 구분 · 상세구분(51-12 · 승격 = 읽기만 + 연결 · 사전조합 = 고름) ·
|
||||
* 승격은 로직 한 줄(입력값 고정 · 더하기 · 순서 · 수량 없음) · 사전조합 = 일위대가 상세와 같은 [로직 추가] · 표 · 동작(51-11) · 수량 손 입력(기본 1)
|
||||
* · 서고 모드(50-3 · `mode`) = 사전조합 — 이름 칸 · 단가산출 로직 표 · 원문만(자재 · 면적 · 다이어그램 없음) ·
|
||||
* 정보 칸 구분 · 상세구분(51-12 · 고름) · 일위대가 상세와 같은 [로직 추가] · 표 · 동작(51-11) · 수량 손 입력(기본 1)
|
||||
* · 로직 줄 구분 드롭다운(134-1 · 단가산출 | 단가승격 · 서고 모드엔 없음)
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
@@ -84,8 +84,8 @@ export interface LaborDetailOptions {
|
||||
/** 구조물 조합 카드 갈래(43-4) — 단가산출 로직 표 + 자재목록만 · 위로 · 아래로 · 로직 추가 ·
|
||||
* 자재 추가 · 정보(단위 · 본뜬) · 다이어그램 없음 · 옵션 · 삭제 · 줄 · 수량 모달은 상세와 같음 */
|
||||
card?: boolean;
|
||||
/** 일위대가 조합 서고(50-3) — 단가승격 · 사전조합 문서(`column` = 키) */
|
||||
mode?: "promote" | "precombo";
|
||||
/** 일위대가 사전조합 서고(50-3) — 사전조합 문서(`column` = 키) */
|
||||
mode?: "precombo";
|
||||
}
|
||||
|
||||
export interface LaborDetailHandle {
|
||||
@@ -105,7 +105,6 @@ export function mountLaborDetail(
|
||||
const saved = JSON.stringify(cur);
|
||||
const edit = !opt.readOnly;
|
||||
const lib = opt.mode ?? null;
|
||||
const promote = lib === "promote";
|
||||
const logics = new Map<string, ItemLogic>();
|
||||
let calc: LaborCalc | null = null;
|
||||
/** 마지막으로 받은 calc — 다시 세는 동안 로직 자재 줄을 그대로 둠(줄이 사라졌다 나타나지 않게) */
|
||||
@@ -195,7 +194,7 @@ export function mountLaborDetail(
|
||||
logicTable(
|
||||
{ rows: cur.담은로직, logics, calc, fail, cells: cells(), selected: picked },
|
||||
// 읽기만이면 순서 · 삭제 없음([옵션] = 보기 모달은 둠)
|
||||
edit && !promote
|
||||
edit
|
||||
? {
|
||||
open: pick,
|
||||
option: openRow,
|
||||
@@ -458,7 +457,7 @@ export function mountLaborDetail(
|
||||
const id = lineId({ 참조: ref });
|
||||
if (cur.담은로직.some((r) => lineId(r) === id))
|
||||
return showToast(tc("Labor_AlreadyRef").replace("{value}", `${ref.키} ${name}`), "error");
|
||||
const 수량 = lib === "precombo" ? { 손: 1 } : null; // 사전조합 줄 기본 1(51-6)
|
||||
const 수량 = lib ? { 손: 1 } : null; // 사전조합 줄 기본 1(51-6)
|
||||
cur.담은로직.push({ 로직: "", 참조: ref, 별칭: null, 메모: null, 입력값: {}, 수량 });
|
||||
changed();
|
||||
},
|
||||
@@ -472,7 +471,7 @@ export function mountLaborDetail(
|
||||
if (cur.담은로직.some((r) => r.로직 === x.키))
|
||||
return showToast(tc("Labor_Already").replace("{value}", `${x.키} ${x.이름}`), "error");
|
||||
// 사전조합 = 면적산출 없음 → 손 1(51-11)
|
||||
const 수량 = lib === "precombo" ? { 손: 1 } : null;
|
||||
const 수량 = lib ? { 손: 1 } : null;
|
||||
cur.담은로직.push({ 로직: x.키, 별칭: null, 메모: null, 입력값: {}, 수량 });
|
||||
void loadLogics().then(() => changed());
|
||||
},
|
||||
@@ -547,25 +546,9 @@ export function mountLaborDetail(
|
||||
opt.onChanged(dirty());
|
||||
});
|
||||
|
||||
/** 서고 구분 · 상세구분(51-12) — 승격 = 원 로직 것 읽기만 + 연결(원 로직 키 · 출처) ·
|
||||
* 사전조합 = 로직 구분 목록(logic-lib/subs)에서 고름(구분 바꾸면 상세구분 = 그 첫 줄) */
|
||||
const meta = cur as { 구분?: string | null; 상세구분?: string | null; 연결?: unknown };
|
||||
const readField = (label: string, field: string, value: string): HTMLElement => {
|
||||
const f = createInputField({ type: "text", label });
|
||||
f.root.dataset.laborField = field;
|
||||
f.input.value = value;
|
||||
f.input.readOnly = true;
|
||||
return f.root;
|
||||
};
|
||||
/** 서고 구분 · 상세구분(51-12) — 로직 구분 목록(logic-lib/subs)에서 고름(구분 바꾸면 상세구분 = 그 첫 줄) */
|
||||
const meta = cur as { 구분?: string | null; 상세구분?: string | null };
|
||||
const libMeta = (): HTMLElement[] => {
|
||||
if (promote) {
|
||||
const link = (meta.연결 ?? {}) as { 로직?: string; 출처?: string };
|
||||
return [
|
||||
readField(tc("Sub"), "구분", meta.구분 ?? ""),
|
||||
readField(tc("Detail"), "상세구분", meta.상세구분 ?? ""),
|
||||
readField(tc("Lib_Link"), "연결", [link.로직, link.출처].filter(Boolean).join(" · ")),
|
||||
];
|
||||
}
|
||||
const sub = createSelectField({ options: [], label: tc("Sub"), disabled: !edit });
|
||||
const detail = createSelectField({ options: [], label: tc("Detail"), disabled: !edit });
|
||||
sub.root.dataset.laborField = "구분";
|
||||
@@ -633,8 +616,8 @@ export function mountLaborDetail(
|
||||
],
|
||||
}),
|
||||
]),
|
||||
// 승격 = 로직 한 줄(새로 만들 때 고름) · 사전조합 = 같은 [로직 추가](51-11)
|
||||
section(tc("Logics_Title"), opt.card || promote ? null : addLogic, logicHost),
|
||||
// 사전조합 = 같은 [로직 추가](51-11)
|
||||
section(tc("Logics_Title"), opt.card ? null : addLogic, logicHost),
|
||||
...(lib ? [] : [section(tc("Labor_Mats"), opt.card ? null : addMat, matHost)]),
|
||||
...(opt.card || lib ? [] : [dgBox]),
|
||||
],
|
||||
@@ -665,7 +648,7 @@ export function mountLaborDetail(
|
||||
// 서고 모드 = 문서 종류 그대로 · 열 · 자재 없음
|
||||
getDoc: () => {
|
||||
if (!lib) return cur;
|
||||
const out: Record<string, unknown> = { ...cur, 종류: promote ? "단가승격" : "사전조합" };
|
||||
const out: Record<string, unknown> = { ...cur, 종류: "사전조합" };
|
||||
delete out.열;
|
||||
delete out.자재;
|
||||
return out as unknown as LaborDoc;
|
||||
|
||||
@@ -60,11 +60,11 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra
|
||||
detail.select.disabled = true;
|
||||
const close = (): void => backdrop.remove();
|
||||
|
||||
/* 한 목록 — 구분(단가산출 · 단가승격 · 일위조합) 칸 · 층 단추는 서고 줄(승격 · 조합)의 층 */
|
||||
/* 한 목록 — 구분(단가산출 · 일위조합) 칸 · 층 단추는 서고 줄(사전조합)의 층 */
|
||||
const layerBar = el("div", { className: "ui-btn-group", attrs: { "data-pick-layers": "" } });
|
||||
let scopes: Scope[] = [];
|
||||
let scope: Scope | null = null;
|
||||
let libRows: { kind: "Kind_Promote" | "Kind_Combo"; row: LibraryRow }[] = [];
|
||||
let libRows: { kind: "Kind_Combo"; row: LibraryRow }[] = [];
|
||||
let loading = false;
|
||||
let seq = 0;
|
||||
/* 공용 찾기 목록 — 구분 · 상세구분 2열 · 이름 왼쪽 맞춤 · ↑ ↓ Enter */
|
||||
@@ -132,15 +132,9 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra
|
||||
loading = true;
|
||||
find();
|
||||
try {
|
||||
const [promote, combo] = await Promise.all([
|
||||
fetchLibrary(at, "promote"),
|
||||
fetchLibrary(at, "precombo"),
|
||||
]);
|
||||
const combo = await fetchLibrary(at, "precombo");
|
||||
if (mine !== seq) return;
|
||||
libRows = [
|
||||
...promote.map((row) => ({ kind: "Kind_Promote" as const, row })),
|
||||
...combo.map((row) => ({ kind: "Kind_Combo" as const, row })),
|
||||
];
|
||||
libRows = combo.map((row) => ({ kind: "Kind_Combo" as const, row }));
|
||||
} catch (error) {
|
||||
if (mine !== seq) return;
|
||||
libRows = [];
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
* M02_MasterTemplete_Labor_RefEdit.ts
|
||||
* 일위대가 참조 줄 모달(51-14) — 서고 원본(50-3)을 그 자리에서 고침 · [저장] = 서고 저장 길(`saveTemplate`) ·
|
||||
* 그 문서를 쓰는 모든 구조물에 따라감(머리 한 줄 알림) · 그 층을 못 고치면 읽기만.
|
||||
* · 단가승격 [옵션] = 승격 문서 로직 한 줄의 넣을 값 모달(`Labor_Input`)
|
||||
* · 일위조합 [수정] = 사전조합 상세(서고 모드 표 · 안 줄 [옵션] · 수량 · 위로 · 아래로 · 삭제 · 추가)
|
||||
* 닫으면(저장 · 취소 모두) `closed` — 부르는 쪽이 구조물 일위대가 표를 다시 셈.
|
||||
* ========================================================================== */
|
||||
@@ -10,15 +9,13 @@
|
||||
import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { readTemplate, saveTemplate, type Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { fetchLogicInputs, type LaborRow } from "./M02_MasterTemplete_Labor_Api";
|
||||
import type { LaborRow } from "./M02_MasterTemplete_Labor_Api";
|
||||
import type { LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import { openLaborInput } from "./M02_MasterTemplete_Labor_Input";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import { keepKnown } from "./M02_Labor_Values_Model";
|
||||
|
||||
export interface RefEditOptions {
|
||||
ref: LaborRef;
|
||||
/** 「SP-000001 메쌓기」 */
|
||||
/** 「SC-000001 돌붙임」 */
|
||||
title: string;
|
||||
readOnly: boolean;
|
||||
closed: () => void;
|
||||
@@ -28,7 +25,7 @@ const fail = (error: unknown, key: "Load_Failed" | "Lib_SaveFailed"): void =>
|
||||
showToast(error instanceof Error ? error.message : tc(key), "error");
|
||||
|
||||
export async function openRefEdit(opt: RefEditOptions): Promise<void> {
|
||||
const kind = opt.ref.키.startsWith("SP-") ? "promote" : "precombo";
|
||||
const kind = "precombo";
|
||||
const layer = opt.ref.층 as Layer;
|
||||
let doc: { 담은로직?: LaborRow[] };
|
||||
let 판: string;
|
||||
@@ -45,33 +42,6 @@ export async function openRefEdit(opt: RefEditOptions): Promise<void> {
|
||||
);
|
||||
const notice = opt.readOnly ? `${tc("Ref_Shared")} · ${tc("Ref_ReadOnly")}` : tc("Ref_Shared");
|
||||
|
||||
if (kind === "promote") {
|
||||
const row = doc.담은로직?.[0];
|
||||
if (!row) return fail(null, "Load_Failed");
|
||||
const logic = row.로직
|
||||
? ((await fetchLogicInputs([row.로직]).catch(() => null))?.로직[0] ?? null)
|
||||
: null;
|
||||
return openLaborInput({
|
||||
title: opt.title,
|
||||
logic,
|
||||
values: row.입력값,
|
||||
memo: row.메모,
|
||||
rate: row.켜쌓기율 ?? null,
|
||||
readOnly: opt.readOnly,
|
||||
notice,
|
||||
okLabel: tc("Save"),
|
||||
done: (next) => {
|
||||
row.입력값 = logic ? keepKnown(logic.입력, next.입력값).kept : next.입력값;
|
||||
row.메모 = next.메모;
|
||||
if (next.켜쌓기율 === null) delete row.켜쌓기율;
|
||||
else row.켜쌓기율 = next.켜쌓기율;
|
||||
void save(doc)
|
||||
.catch((error) => fail(error, "Lib_SaveFailed"))
|
||||
.finally(opt.closed);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 사전조합 — 상세와 같은 부품(서로 부르므로 늦게 받음)
|
||||
const { mountLaborDetail } = await import("./M02_MasterTemplete_Labor_Detail");
|
||||
let dirty = false;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* 자재목록 = 코드 · 이름 · 규격 · 수량산출 · 단위 · 가격 다섯(기준 칸 강조) · 기준단가 / 반영 금액(43-3).
|
||||
* 금액은 calc 답(없으면 「…」).
|
||||
* 줄 누름 · 줄 단추는 `on` 이 있을 때만(보기 갈래 = 없음) — 순서 칸 2행(51-14 · 위 [위로] [아래로] / 아래 [옵션] 또는 [수정] · [삭제] ·
|
||||
* 묶음 `ui-btn-group` · 순서 없는 카드 갈래 = 아래 줄만) · 단가승격 줄 = [옵션] · 일위조합 줄 = [수정](`ref` · 서고 원본) ·
|
||||
* 묶음 `ui-btn-group` · 순서 없는 카드 갈래 = 아래 줄만) · 일위조합 줄 = [수정](`ref` · 서고 원본 · 옛 단가승격 참조 줄 = 단추 없음) ·
|
||||
* 상세는 줄 누름 = 고름(`selected` 줄 하이라이트 · 그 줄 바로 아래 하위 행 · 51-2 · 화살표 없음 51-7) · 넣을 값 모달은 [옵션](47-5).
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -202,7 +202,7 @@ export interface RowHandlers {
|
||||
open: (i: number) => void;
|
||||
/** [옵션] 단추(47-5 · 넣을 값 모달) — 없으면 단추 없음 */
|
||||
option?: (i: number) => void;
|
||||
/** 참조 줄 단추(51-14) — 단가승격 [옵션] · 일위조합 [수정] = 서고 원본 모달 · 없으면 단추 없음 */
|
||||
/** 참조 줄 단추(51-14) — 일위조합 [수정] = 서고 원본 모달 · 없으면 단추 없음 */
|
||||
ref?: (i: number) => void;
|
||||
/** 수량산출 칸 누름(손 입력 · 엑셀에서 고르기) — 없으면 줄 누름과 같음 */
|
||||
qty?: (i: number) => void;
|
||||
@@ -445,13 +445,8 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
|
||||
: []),
|
||||
group([
|
||||
...(row.참조
|
||||
? ref
|
||||
? [
|
||||
small(
|
||||
tc(row.참조.키.startsWith("SP-") ? "Labor_Option" : "Labor_Edit"),
|
||||
() => ref(i),
|
||||
),
|
||||
]
|
||||
? ref && !row.참조.키.startsWith("SP-")
|
||||
? [small(tc("Labor_Edit"), () => ref(i))]
|
||||
: []
|
||||
: option
|
||||
? [small(tc("Labor_Option"), () => option(i))]
|
||||
|
||||
@@ -140,7 +140,6 @@ const TEXT = {
|
||||
In_Close: ["닫기", "Close"],
|
||||
Lib_Name: ["이름", "Name"],
|
||||
// 서고 목록표(51-4)
|
||||
Lib_Promote: ["단가산출 승격", "Promoted logic"],
|
||||
Lib_PrecomboAll: ["일위대가 사전조합", "Pre-combination"],
|
||||
Lib_Create: ["생성", "Create"],
|
||||
Lib_Copy: ["복사", "Copy"],
|
||||
@@ -150,7 +149,6 @@ const TEXT = {
|
||||
Lib_Base: ["기준 금액", "Base amount"],
|
||||
Lib_Date: ["고친 날", "Modified"],
|
||||
// 서고 구분 · 연결(51-12)
|
||||
Lib_Link: ["연결", "Linked logic"],
|
||||
} as const satisfies Record<string, readonly [string, string]>;
|
||||
|
||||
export type ComboTextKey = keyof typeof TEXT;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Library_List.ts
|
||||
* 일위대가 조합 서고 목록표(51-4) — 좌측 항목(단가산출 승격 · 일위대가 사전조합)을 누르면 상세 칸에 뜸.
|
||||
* 일위대가 사전조합 서고 목록표(51-4) — 좌측 사전조합 층 단추를 누르면 상세 칸에 뜸.
|
||||
* 표 = 고름 · 키 · 구분 · 상세구분(51-12) · 이름 · 단위 · 기준 금액(서버 1 단위 계) · 고친 날 · 줄 누름 = 그 문서 상세.
|
||||
* 머리 [생성] · [복사](고른 줄) · [삭제](고른 줄 · 쓰는 곳 있으면 서버 409 까닭 그대로).
|
||||
* 꼴 = M01 단가산출 로직 목록 — 찾기 줄(`buildHeadFilters`) · 쪽(`buildPager`) · 표(`m01-master__grid`) 를 M01 에서 import.
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
type TemplateInfo,
|
||||
} from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { fetchLibLogicSubs } from "./M02_MasterTemplete_Labor_Api";
|
||||
import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import type { PrecomboKind } from "./M02_MasterTemplete_UI_Side_Precombo";
|
||||
|
||||
@@ -177,20 +176,8 @@ export function mountLibraryList(
|
||||
const create = async (body: LibraryBody, label: string): Promise<void> =>
|
||||
made((await newLibrary(opt.layer, opt.kind, body)).이름, label);
|
||||
|
||||
/** 단가승격 = 단가산출 로직 고르기 → 이름 · 단위 · 구분 · 연결은 로직 것(서버가 채움) ·
|
||||
* 사전조합 = 이름 + 구분 · 상세구분 처음 값 = 로직 구분 목록 첫 줄(51-12 · 상세에서 바꿈) · 빈 묶음 */
|
||||
/** 사전조합 = 이름 + 구분 · 상세구분 처음 값 = 로직 구분 목록 첫 줄(51-12 · 상세에서 바꿈) · 빈 묶음 */
|
||||
btnNew.addEventListener("click", () => {
|
||||
if (opt.kind === "promote")
|
||||
return openLogicPicker((x) => {
|
||||
const doc = {
|
||||
이름: x.이름,
|
||||
단위: x.결과단위 || null,
|
||||
담은로직: [{ 로직: x.키, 입력값: {} }],
|
||||
};
|
||||
void create({ 문서: doc }, x.이름).catch((error) =>
|
||||
showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error"),
|
||||
);
|
||||
});
|
||||
nameDialog(L("M02_NewTitle"), "", async (name) => {
|
||||
const first = (await fetchLibLogicSubs())[0];
|
||||
const 문서 = {
|
||||
|
||||
@@ -311,6 +311,8 @@ async def read_layer_template(
|
||||
found["층"] = layer
|
||||
found["출처"] = layers.read_manifest(folder).get(f"{kind}/{name}")
|
||||
found["문서"] = refs.filled(kind, found["문서"]) # 옛 서고 문서 구분 · 상세구분(51-12)
|
||||
if layer != "project": # 옛 단가승격 참조 줄 = 새 꼴(134-2 · 프로젝트 사본은 그대로)
|
||||
found["문서"] = await asyncio.to_thread(refs.unpromote, kind, found["문서"])
|
||||
return found
|
||||
|
||||
|
||||
|
||||
@@ -570,7 +570,7 @@ def _server(kind: str, doc: Any, source: Path | None = None) -> Any:
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
|
||||
if source is not None and isinstance(doc, dict) and kind != "promote":
|
||||
if source is not None and isinstance(doc, dict):
|
||||
doc = refs.carry(doc, refs.where(source))
|
||||
if kind == "labor":
|
||||
return labor.server(doc)
|
||||
|
||||
@@ -11,15 +11,11 @@ import type { Kind, Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import type { Selection } from "./M02_MasterTemplete_UI_Main";
|
||||
|
||||
export type PrecomboKind = Extract<Kind, "promote" | "precombo">;
|
||||
export type PrecomboKind = Extract<Kind, "precombo">;
|
||||
|
||||
const PARTS = [
|
||||
{ kind: "promote", label: "Lib_Promote" },
|
||||
{ kind: "precombo", label: "Lib_PrecomboAll" },
|
||||
] as const;
|
||||
const PARTS = [{ kind: "precombo", label: "Lib_PrecomboAll" }] as const;
|
||||
export const PRECOMBO_TITLE = "일위대가 조합";
|
||||
export const isPrecombo = (kind: Kind): kind is PrecomboKind =>
|
||||
kind === "promote" || kind === "precombo";
|
||||
export const isPrecombo = (kind: Kind): kind is PrecomboKind => kind === "precombo";
|
||||
|
||||
export interface PrecomboOptions {
|
||||
layer: Layer;
|
||||
|
||||
@@ -28,8 +28,8 @@ from config import config_system
|
||||
LAYERS = ("system", "company", "personal", "project")
|
||||
# area = 면적산출 — 통합문서(구조물집계표 열마다 · `A00_Common/spreadsheet/`)
|
||||
# labor = 일위대가(구조물 열마다 · `M02_Labor_Doc`)
|
||||
# promote = 단가승격(SP-) · precombo = 사전조합(SC-) — 일위대가 조합 서고(`M02_Labor_Ref`)
|
||||
LIBRARY_KINDS = ("promote", "precombo")
|
||||
# precombo = 사전조합(SC-) — 일위대가 사전조합 서고(`M02_Labor_Ref` · 옛 단가승격 promote 는 134-2 에서 뺌)
|
||||
LIBRARY_KINDS = ("precombo",)
|
||||
KINDS = ("table", "drawing", "structure", "labor", "area", *LIBRARY_KINDS)
|
||||
TEMPLATES_DIRNAME = "templates"
|
||||
INITIAL_DIRNAME = "_initial"
|
||||
@@ -173,7 +173,6 @@ _SKELETON = {
|
||||
"structure": ("도면.entities",),
|
||||
"labor": ("담은로직",),
|
||||
"area": ("시트",),
|
||||
"promote": ("담은로직",),
|
||||
"precombo": ("담은로직",),
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ def test_일위대가_카드는_상세_카드_갈래():
|
||||
assert "move: reorder," in detail # 85-3 — 카드도 순서(▲▼ · 끌기) · 빼기는 그대로
|
||||
# 서고 모드(50-3 · lib)도 더하기 · 다이어그램 없음
|
||||
# 51-11 사전조합 = 일위대가 상세와 같은 [로직 추가] · 같은 목록(걸러냄 없음)
|
||||
assert "opt.card || promote ? null : addLogic" in detail
|
||||
assert "opt.card ? null : addLogic" in detail
|
||||
assert "only" not in _text("M02_MasterTemplete_Labor_Pick.ts")
|
||||
assert "...(opt.card || lib ? [] : [dgBox])," in detail
|
||||
assert "opt.card ? null : addMat" in detail # 49-6 — 카드엔 [자재 추가] 없음 · 상세는 그대로
|
||||
@@ -151,11 +151,12 @@ def test_일위대가_줄_고름과_옵션_단추():
|
||||
|
||||
|
||||
def test_순서칸_2행_참조줄_옵션_수정():
|
||||
"""51-14 — 순서 칸 2행(위 [위로] [아래로] / 아래 [옵션]·[수정] [삭제]) · 단가승격 [옵션] · 일위조합 [수정] =
|
||||
서고 원본 모달(저장 = 서고 저장 길 · 자기 층만 고침) · 닫으면 다시 셈."""
|
||||
"""51-14 — 순서 칸 2행(위 [위로] [아래로] / 아래 [옵션]·[수정] [삭제]) · 일위조합 [수정] =
|
||||
서고 원본 모달(저장 = 서고 저장 길 · 자기 층만 고침) · 닫으면 다시 셈 · 옛 단가승격 참조 줄 = 단추 없음(134-2)."""
|
||||
rows = _text("M02_MasterTemplete_Labor_Rows.ts")
|
||||
assert 'className: "m02-labrow__tools"' in rows
|
||||
assert '"SP-") ? "Labor_Option" : "Labor_Edit"' in rows
|
||||
assert 'ref && !row.참조.키.startsWith("SP-")' in rows
|
||||
assert '[small(tc("Labor_Edit"), () => ref(i))]' in rows
|
||||
detail = _text("M02_MasterTemplete_Labor_Detail.ts")
|
||||
assert "readOnly: !edit || ref.층 !== opt.layer," in detail
|
||||
assert "closed: () => alive && recalc()," in detail
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- GC000260 90% = 100% × 0.9(비목 · 계 · 줄 수량 · 금액) · 단가는 그대로.
|
||||
- clean = 0 초과 200 이하 · 소수 한 자리 · 100 · 대상 밖은 안 둠.
|
||||
- 프로젝트 사본 계산(`M02_Item_Prices.calc`)도 같은 율 · 같은 값.
|
||||
- 서고(단가승격)도 율을 담고 같은 값.
|
||||
- 옛 단가승격 서고 문서도 율을 따라 같은 값(134-2 · 프로젝트 사본 셈).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -83,11 +83,8 @@ def test_문서_계산과_프로젝트_사본_같음() -> None:
|
||||
assert _d(line["계"]) == pytest.approx(_d(full["줄"][0]["기준"]["계"]) * Decimal("0.9"))
|
||||
|
||||
|
||||
def test_서고_단가승격도_율() -> None:
|
||||
doc = refs.clean_doc(
|
||||
"promote", {"키": "SP-900001", "담은로직": [{"로직": CODE, "켜쌓기율": 90}]}
|
||||
)
|
||||
assert doc["담은로직"][0]["켜쌓기율"] == 90
|
||||
def test_옛_단가승격도_율() -> None:
|
||||
doc = {"키": "SP-900001", "배": 1, "담은로직": [{"로직": CODE, "켜쌓기율": 90}]}
|
||||
whole = m01.loaded()[1]
|
||||
expand = refs.Expand(whole, lambda ref: doc)
|
||||
got = expand({"층": "system", "주인": None, "키": "SP-900001"})
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""일위대가 조합 서고(PLAN 50-3 · `M02_Labor_Ref`) — 단가승격 · 사전조합 · 참조 줄.
|
||||
"""일위대가 사전조합 서고(PLAN 50-3 · `M02_Labor_Ref`) — 사전조합 · 참조 줄 · 옛 단가승격(134-2).
|
||||
|
||||
펼침(재귀 · 순환 · 깊이) · 층 규칙(위 층만) · 쓰는 곳 지우기 409 · 층 사이 따라 복사 ·
|
||||
프로젝트 굳힘 · 로직 지우기 검사 · TS 짝 settle 거울. 층은 임시 폴더(마스터 · storage).
|
||||
옛 단가승격(SP) = 새로 못 만듦 · 남은 파일은 읽기 · 셈 · 굳힘만(`_promote` = 옛 파일을 바로 씀) ·
|
||||
층 문서를 읽을 때 옛 참조 줄 = 로직 줄 + 승격(`unpromote` · 같은 로직이 있으면 그대로).
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -44,9 +46,26 @@ def world(tmp_path, monkeypatch):
|
||||
}
|
||||
|
||||
|
||||
def _promote(folder, code=TEN, **extra):
|
||||
with store.at(folder):
|
||||
return refs.create("promote", {"담은로직": [{"로직": code, "입력값": {}}], **extra})["이름"]
|
||||
def _promote(folder, code=TEN, values=None, **extra):
|
||||
"""옛 단가승격 파일(134-2 앞 꼴) — 층마다 다음 번호 · 배 = 로직 물량 알약 배."""
|
||||
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||
|
||||
line = {"로직": code, "입력값": values or {}, **extra}
|
||||
pill = (combo_store.combo_pills({"담은로직": [line]})["물량"] or [{}])[0]
|
||||
folder = folder / "promote"
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
key = f"SP-{len(list(folder.glob('SP-*.json'))) + 1:06d}"
|
||||
doc = {
|
||||
"종류": "단가승격",
|
||||
"키": key,
|
||||
"이름": pill.get("이름") or code,
|
||||
"단위": pill.get("단위"),
|
||||
"배": pill.get("배") or 1,
|
||||
"담은로직": [line],
|
||||
"본뜬": None,
|
||||
}
|
||||
(folder / f"{key}.json").write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8")
|
||||
return key
|
||||
|
||||
|
||||
def _combo(folder, lines, name="조합"):
|
||||
@@ -65,30 +84,78 @@ def _base(code):
|
||||
|
||||
|
||||
# ── 자료 틀 ────────────────────────────────────────────────────────────
|
||||
def test_승격은_로직_한_줄_단위_배_이름은_로직에서_키는_층마다_다음_번호(world):
|
||||
assert _promote(world["system"]) == "SP-000001"
|
||||
assert _promote(world["system"], STONE, 이름="돌") == "SP-000002"
|
||||
assert _promote(world["personal"]) == "SP-000001" # 층마다 셈
|
||||
doc = _read(world["system"], "promote", "SP-000001")
|
||||
assert (doc["종류"], doc["단위"], doc["배"]) == ("단가승격", "㎡", 10)
|
||||
assert doc["이름"] and doc["담은로직"] == [{"로직": TEN, "입력값": {}}]
|
||||
assert _read(world["system"], "promote", "SP-000002")["이름"] == "돌"
|
||||
def test_단가승격_서고는_없음_층_종류에서_뺌(world):
|
||||
assert "promote" not in layers.KINDS and layers.LIBRARY_KINDS == ("precombo",)
|
||||
with store.at(world["system"]), pytest.raises(KeyError):
|
||||
refs.create("promote", {"담은로직": [{"로직": TEN}]})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"body",
|
||||
[
|
||||
{"담은로직": []},
|
||||
{"담은로직": [{"로직": TEN}, {"로직": STONE}]},
|
||||
{"담은로직": [{"로직": "GF999999"}]},
|
||||
{"담은로직": [{"참조": {**SYS, "키": "SP-000001"}}]},
|
||||
],
|
||||
)
|
||||
def test_틀린_승격은_400(world, body):
|
||||
_promote(world["system"])
|
||||
with store.at(world["system"]), pytest.raises(store.StoreError) as e:
|
||||
refs.create("promote", body)
|
||||
assert e.value.status == 400
|
||||
# ── 옛 단가승격 참조 줄 = 읽을 때 새 꼴(134-2) ───────────────────────────
|
||||
def test_옛_참조_줄은_로직_줄_승격으로_보이고_값_같음(world):
|
||||
sp = _promote(world["system"], TEN)
|
||||
old = {
|
||||
"열": "rv",
|
||||
"담은로직": [
|
||||
{"참조": {**SYS, "키": sp}, "별칭": "승", "메모": "m", "수량": {"손": 4}},
|
||||
{"로직": STONE, "수량": {"손": 1}},
|
||||
],
|
||||
"자재": [],
|
||||
}
|
||||
got = refs.unpromote("labor", old)
|
||||
assert got["담은로직"][0] == {
|
||||
"로직": TEN,
|
||||
"별칭": "승",
|
||||
"메모": "m",
|
||||
"입력값": {},
|
||||
"수량": {"손": 4},
|
||||
"승격": True,
|
||||
}
|
||||
assert old["담은로직"][0]["참조"]["키"] == sp # 받은 문서는 그대로
|
||||
held = _promote(world["system"], PLAIN, {"규격": "NX"}, 켜쌓기율=90) # 입력값 · 율도 옮김
|
||||
one = refs.unpromote("labor", {"담은로직": [{"참조": {**SYS, "키": held}}]})["담은로직"][0]
|
||||
assert (one["입력값"], one["켜쌓기율"]) == ({"규격": "NX"}, 90)
|
||||
with store.at(world["system"]):
|
||||
before = labor.calc(labor.clean(old), {})["반영합"]["계"]
|
||||
after = labor.clean(got)
|
||||
assert after["담은로직"][0]["승격"] is True # [저장] 때 새 꼴로 굳음
|
||||
assert labor.calc(after, {})["반영합"]["계"] == pytest.approx(before, rel=1e-9)
|
||||
|
||||
|
||||
def test_옛_참조_줄은_같은_로직이_있으면_그대로_사전조합은_승격_없이(world):
|
||||
sp = _promote(world["system"], STONE)
|
||||
lines = [{"로직": STONE, "수량": None}, {"참조": {**SYS, "키": sp}, "수량": None}]
|
||||
doc = {"열": "rv", "담은로직": lines, "자재": []}
|
||||
assert refs.unpromote("labor", doc) is doc # 겹침 = 옛 참조 줄 그대로(저장 됨)
|
||||
with store.at(world["system"]):
|
||||
assert labor.clean(doc)["담은로직"][1]["참조"]["키"] == sp
|
||||
combo = {"담은로직": [{"참조": {**SYS, "키": sp}, "수량": {"손": 2}, "메모": None}]}
|
||||
one = refs.unpromote("precombo", combo)["담은로직"][0]
|
||||
assert one["로직"] == STONE and one["수량"] == {"손": 2} and "승격" not in one
|
||||
assert refs.unpromote("area", combo) is combo # 다른 종류는 안 봄
|
||||
|
||||
|
||||
def test_층_길_읽기는_옛_참조_줄을_새_꼴로_파일은_그대로(world):
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Router_Layers as layer_router
|
||||
|
||||
sp = _promote(world["system"])
|
||||
with store.at(world["personal"]):
|
||||
line = {"참조": {**SYS, "키": sp}, "수량": {"손": 2}}
|
||||
store.write("labor", "x1", "", {"열": "x1", "담은로직": [line], "자재": []})
|
||||
app = FastAPI()
|
||||
app.include_router(layer_router.router)
|
||||
app.dependency_overrides[verify_session] = lambda: {
|
||||
"user_id": 42,
|
||||
"company_id": 7,
|
||||
"role": "USER",
|
||||
"is_master": False,
|
||||
}
|
||||
got = TestClient(app).get("/api/m02/layers/personal/templates/labor/x1").json()
|
||||
assert got["문서"]["담은로직"][0]["로직"] == TEN and got["문서"]["담은로직"][0]["승격"] is True
|
||||
assert _read(world["personal"], "labor", "x1")["담은로직"][0]["참조"]["키"] == sp
|
||||
|
||||
|
||||
def test_사전조합_틀린_줄_400_순환_422(world):
|
||||
@@ -219,25 +286,25 @@ def test_참조_줄_settle_파이썬과_TS_가_같은_답(world):
|
||||
|
||||
# ── 지우기 · 복사 · 굳힘 · 로직 지우기 ─────────────────────────────────
|
||||
def test_쓰는_곳이_있으면_지우기_409_없으면_지움(world):
|
||||
sp = _promote(world["system"])
|
||||
sc = _combo(world["system"], [{"로직": TEN}])
|
||||
with store.at(world["personal"]):
|
||||
line = {"참조": {**SYS, "키": sp}}
|
||||
line = {"참조": {**SYS, "키": sc}}
|
||||
store.write("labor", "x1", "", {"열": "x1", "담은로직": [line], "자재": []})
|
||||
with store.at(world["system"]), pytest.raises(store.StoreError) as e:
|
||||
store.delete("promote", sp)
|
||||
store.delete("precombo", sc)
|
||||
assert e.value.status == 409 and "개인 7/42" in e.value.detail
|
||||
(world["personal"] / "labor" / "x1.json").unlink()
|
||||
with store.at(world["system"]):
|
||||
store.delete("promote", sp)
|
||||
assert not (world["system"] / "promote" / f"{sp}.json").exists()
|
||||
store.delete("precombo", sc)
|
||||
assert not (world["system"] / "precombo" / f"{sc}.json").exists()
|
||||
|
||||
|
||||
def test_복사는_못_읽는_참조를_같이_복사_본뜬(world):
|
||||
sp = _promote(world["personal"])
|
||||
inner = _combo(world["personal"], [{"로직": TEN}], "안")
|
||||
sys_sp = _promote(world["system"])
|
||||
sc = _combo(
|
||||
world["personal"],
|
||||
[{"참조": {**MINE, "키": sp}}, {"참조": {**SYS, "키": sys_sp}, "수량": {"손": 2}}],
|
||||
[{"참조": {**MINE, "키": inner}}, {"참조": {**SYS, "키": sys_sp}, "수량": {"손": 2}}],
|
||||
)
|
||||
with store.at(world["personal"]):
|
||||
doc = store.read("precombo", sc)["문서"]
|
||||
@@ -248,10 +315,15 @@ def test_복사는_못_읽는_참조를_같이_복사_본뜬(world):
|
||||
got = _read(world["company"], "precombo", made["이름"])
|
||||
assert got["본뜬"] == {**MINE, "키": sc}
|
||||
assert [one["참조"] for one in got["담은로직"]] == [
|
||||
{**COMPANY, "키": "SP-000001"}, # 회사가 못 읽는 개인 승격 → 회사 층 새 키
|
||||
{**SYS, "키": sys_sp}, # 마스터는 그대로
|
||||
{**COMPANY, "키": "SC-000001"}, # 회사가 못 읽는 개인 사전조합 → 회사 층 새 키
|
||||
{**SYS, "키": sys_sp}, # 마스터는 그대로(옛 승격도)
|
||||
]
|
||||
assert _read(world["company"], "promote", "SP-000001")["본뜬"] == {**MINE, "키": sp}
|
||||
assert _read(world["company"], "precombo", "SC-000001")["본뜬"] == {**MINE, "키": inner}
|
||||
# 못 읽는 옛 개인 승격은 따라 복사 안 함(134-2 · 승격 서고 없음)
|
||||
mine_sp = _promote(world["personal"])
|
||||
with store.at(world["company"]), pytest.raises(store.StoreError) as e:
|
||||
refs.carry({"담은로직": [{"참조": {**MINE, "키": mine_sp}}]}, ("personal", "7/42"))
|
||||
assert e.value.status == 400
|
||||
|
||||
|
||||
def test_길_새로_복사_권한(world):
|
||||
@@ -271,14 +343,15 @@ def test_길_새로_복사_권한(world):
|
||||
}
|
||||
client = TestClient(app)
|
||||
url = "/api/m02/layers/{}/library/{}"
|
||||
body = {"문서": {"담은로직": [{"로직": TEN}]}}
|
||||
got = client.post(url.format("personal", "promote"), json=body).json()
|
||||
assert got["이름"] == "SP-000001"
|
||||
assert client.post(url.format("system", "promote"), json=body).status_code == 403
|
||||
copy = {"본뜬": {**MINE, "키": "SP-000001"}}
|
||||
assert client.post(url.format("personal", "promote"), json=copy).json()["이름"] == "SP-000002"
|
||||
other = {"본뜬": {"층": "personal", "주인": "7/99", "키": "SP-000001"}}
|
||||
assert client.post(url.format("personal", "promote"), json=other).status_code == 403
|
||||
body = {"문서": {"이름": "가", "담은로직": [{"로직": TEN}]}}
|
||||
got = client.post(url.format("personal", "precombo"), json=body).json()
|
||||
assert got["이름"] == "SC-000001"
|
||||
assert client.post(url.format("system", "precombo"), json=body).status_code == 403
|
||||
copy = {"본뜬": {**MINE, "키": "SC-000001"}}
|
||||
assert client.post(url.format("personal", "precombo"), json=copy).json()["이름"] == "SC-000002"
|
||||
other = {"본뜬": {"층": "personal", "주인": "7/99", "키": "SC-000001"}}
|
||||
assert client.post(url.format("personal", "precombo"), json=other).status_code == 403
|
||||
assert client.post(url.format("personal", "promote"), json=body).status_code == 422 # 134-2
|
||||
|
||||
|
||||
def test_프로젝트는_참조사본으로_굳힘_원본을_지워도_같은_값(world):
|
||||
@@ -305,7 +378,7 @@ def test_프로젝트는_참조사본으로_굳힘_원본을_지워도_같은_
|
||||
assert line["수량"] == 1 and line["반영"]["계"] == pytest.approx(live, rel=1e-9)
|
||||
|
||||
|
||||
def test_로직_지우기_검사는_단가승격도_보고_참조_줄은_건너뜀(world):
|
||||
def test_로직_지우기_검사는_승격_줄도_보고_참조_줄은_건너뜀(world):
|
||||
from M01_MasterData import M01_MasterData_Store_Cache as cache
|
||||
|
||||
sp = _promote(world["system"], STONE)
|
||||
@@ -319,8 +392,14 @@ def test_로직_지우기_검사는_단가승격도_보고_참조_줄은_건너
|
||||
if code == STONE:
|
||||
raise cache.cm.mf.FormulaError("없음")
|
||||
|
||||
assert cache.labor_errors(Gone()) == [] # 옛 승격 서고는 안 봄(134-2) · 참조 줄 건너뜀
|
||||
with store.at(world["system"]):
|
||||
have = store.read("labor", "rv")
|
||||
line = {"로직": STONE, "승격": True}
|
||||
store.write("labor", "rv", have["판"], {**have["문서"], "담은로직": [line]})
|
||||
number = store.read("labor", "rv")["문서"]["일위대가번"]
|
||||
got = cache.labor_errors(Gone())
|
||||
assert got == [f"단가승격 마스터 「{sp}」 이 로직 「{STONE}」 을 씀 — 거기서 먼저 뺌"]
|
||||
assert got == [f"일위대가 마스터 「{number}」 이 로직 「{STONE}」 을 씀 — 거기서 먼저 뺌"]
|
||||
|
||||
|
||||
def test_새_프로젝트_씨앗은_서고를_안_가져감(world, tmp_path):
|
||||
@@ -332,16 +411,11 @@ def test_새_프로젝트_씨앗은_서고를_안_가져감(world, tmp_path):
|
||||
def test_층_목록_서고_줄은_이름_단위_기준금액을_실음(world):
|
||||
sp = _promote(world["system"])
|
||||
sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}, "수량": {"손": 3}}], "셋")
|
||||
rows = [
|
||||
{"종류": "promote", "이름": sp},
|
||||
{"종류": "precombo", "이름": sc},
|
||||
{"종류": "labor", "이름": "rv"},
|
||||
]
|
||||
rows = [{"종류": "precombo", "이름": sc}, {"종류": "labor", "이름": "rv"}]
|
||||
refs.summaries(world["system"], rows)
|
||||
one = float(Decimal(str(_base(TEN)["계"])) / 10)
|
||||
assert rows[0]["기준금액"] == pytest.approx(one)
|
||||
assert (rows[1]["제목"], rows[1]["기준금액"]) == ("셋", pytest.approx(one * 3))
|
||||
assert "기준금액" not in rows[2]
|
||||
assert (rows[0]["제목"], rows[0]["기준금액"]) == ("셋", pytest.approx(one * 3))
|
||||
assert "기준금액" not in rows[1]
|
||||
|
||||
|
||||
def test_사전조합_로직_줄_섞임_합은_손셈_지우기_409_굳힘(world):
|
||||
@@ -370,9 +444,6 @@ def test_사전조합_로직_줄_섞임_합은_손셈_지우기_409_굳힘(world
|
||||
assert one["기준"]["계"] == pytest.approx(float(want))
|
||||
assert one["반영"]["계"] == pytest.approx(float(want * 5))
|
||||
assert [x.get("로직") or x["참조"]["키"] for x in one["안"]] == [sp, STONE, TEN]
|
||||
with store.at(world["system"]), pytest.raises(store.StoreError) as e:
|
||||
store.delete("promote", sp)
|
||||
assert e.value.status == 409 and "사전조합 마스터 「섞음」" in e.value.detail
|
||||
|
||||
class Gone: # STONE 을 지운 마스터
|
||||
def logic(self, code):
|
||||
@@ -409,15 +480,8 @@ def test_사전조합이_자기_자신을_담으면_422_수량_기본_1(world):
|
||||
assert e.value.status == 422 and "자기 자신" in e.value.detail
|
||||
|
||||
|
||||
def test_서고_구분_상세구분_연결_승격은_원_로직_사전조합은_고른_값_옛_문서는_읽을_때(world):
|
||||
import M01_MasterData.M01_MasterData_Store as m01
|
||||
|
||||
row = m01.loaded()[1].logic(TEN)
|
||||
sp = _promote(world["system"], 구분="틀림", 상세구분="틀림", 연결={"로직": "x"})
|
||||
doc = _read(world["system"], "promote", sp)
|
||||
assert row["구분"] and row["상세구분"]
|
||||
assert (doc["구분"], doc["상세구분"]) == (row["구분"], row["상세구분"]) # 화면 값 안 받음
|
||||
assert doc["연결"] == {"로직": TEN, "출처": row.get("출처") or ""}
|
||||
def test_서고_구분_상세구분_사전조합은_고른_값_옛_문서는_읽을_때(world):
|
||||
sp = _promote(world["system"])
|
||||
sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}}])
|
||||
assert (_read(world["system"], "precombo", sc)["구분"], "") == ("", "")
|
||||
with store.at(world["system"]):
|
||||
@@ -425,12 +489,10 @@ def test_서고_구분_상세구분_연결_승격은_원_로직_사전조합은_
|
||||
store.write("precombo", sc, have["판"], {**have["문서"], "구분": " 가 ", "상세구분": "나"})
|
||||
got = _read(world["system"], "precombo", sc)
|
||||
assert (got["구분"], got["상세구분"]) == ("가", "나")
|
||||
# 옛 문서(칸 없음) — 승격은 원 로직 것 · 사전조합은 빈 글
|
||||
old = {k: v for k, v in doc.items() if k not in ("구분", "상세구분", "연결")}
|
||||
(world["system"] / "promote" / f"{sp}.json").write_text(json.dumps(old), encoding="utf-8")
|
||||
assert refs.filled("promote", old)["연결"] == doc["연결"]
|
||||
# 옛 문서(칸 없음) — 사전조합은 빈 글 · 옛 승격은 손대지 않음
|
||||
old = _read(world["system"], "promote", sp)
|
||||
assert refs.filled("promote", old) is old
|
||||
assert refs.filled("precombo", {"담은로직": []})["구분"] == ""
|
||||
rows = [{"종류": "promote", "이름": sp}, {"종류": "precombo", "이름": sc}]
|
||||
rows = [{"종류": "precombo", "이름": sc}]
|
||||
refs.summaries(world["system"], rows)
|
||||
assert (rows[0]["구분"], rows[0]["상세구분"]) == (row["구분"], row["상세구분"])
|
||||
assert (rows[1]["구분"], rows[1]["상세구분"], rows[1]["연결"]) == ("가", "나", None)
|
||||
assert (rows[0]["구분"], rows[0]["상세구분"], rows[0]["연결"]) == ("가", "나", None)
|
||||
|
||||
Reference in New Issue
Block a user