refactor(794-1b): M01 흐름도 패널 걷음 — 로직 상세 오른쪽 = 원문만 · [신규 로직] = 빈 시트 줄 · 흐름 서버 길 · DataFlow 화면 지움

요구 651 · 653 — 계산 공식 참고용 흐름도(① M01 로직 상세 오른쪽 접는 패널)를 걷음. ② M02 일위대가 다이어그램은 그대로.

지움(64 파일 · 18,666 줄 · 되돌릴 길 = git 이력)
- M01 6 파일 1,064 줄 — Store_Flow.py · UI_LogicLab_Flow.ts/.css · FlowRows.ts · FlowView.ts/.css
- M02_MasterTemplete/DataFlow 43 파일 12,799 줄 — 흐름도 화면 · 셈(Compute · Convert · Lookup · Model · Eval …)
- 시험 15 파일 4,803 줄 — test_m01_logic_flow{view,_engine,_project,_save} · test_m02_df_*(값 찾기 values_old 뺌) — 지운 흐름 코드만 지키던 것

남김(바닥) — DataFlow M02_DF_UnitPick(일위대가 셋) · M02_DF_Values(M02_Item_Prices) · M02_DF_Round · _standard 표 ·
  M02_DF_Router = GET /api/m02/df/standards 하나 · M02_DF_Api = cfStandards 하나 · CfRound 꼴은 Round 안으로

고침
- M01 Router — /save = Store_Make.save(개정 확인 · 자체 파일 새 줄 GX · added 그대로) · /calc /text /logic/auto = 옛 셈만 · /logic/flow 둘 지움
- master_formula — 흐름 훅 지움 · 줄에 「흐름」 이 남으면 「흐름도 로직은 걷음」 까닭 오류(저장된 흐름 로직 0 — 마스터 · storage 다시 확인)
- Sangeun_Calc._defaults = 옛 자동값 · M02_Logic_Lib_Read /logic/flow 지움 · main.py 주석
- 화면 — LogicLab(startFlow · scheduleFlow · 예시값 잇기 지움 · [신규 로직] 줄 = 입력 · 중간 · 호표 빈 목록 + 끝수 null) ·
  Side(원문 패널 하나) · Source(머리 칸 이름 m01lab-src__top/__title · DF_Panel.css 안 씀) · Detail · View(nowInputs 지움) · Sheet(흐름 갈래 지움) · 글
- 시험 — 지운 파일을 가리키던 목록 넷(spreadsheet_size · 208_1 · 588 · input_prefs) · 717_1 흐름 길 두 줄 · values_old 흐름 셈 부분 지움
- test_713_3_list 기대 — 로직 1,461 → 1,515 · 묶음 307 → 335 · 작업량 21 → 65 · 부르는 506 → 512(782-1 · 2 · 3 건설 기초 로직 54 더함 ·
  옛 키만 세면 307 · 21 · 506 그대로)
- 새 지킴 test_794_1b_flow_gone — 흐름 길 404 · 표준 표 남음 · 신규 로직 자체 GX · 흐름 남은 줄 까닭 오류

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 19:38:39 +09:00
co-authored by Claude Opus 5.5
parent 9456935d2d
commit 598b80eb26
91 changed files with 203 additions and 19307 deletions
+5 -32
View File
@@ -15,7 +15,6 @@ from pydantic import BaseModel
from M01_MasterData import M01_MasterData_Sheet as line_sheet
from M01_MasterData import M01_MasterData_Store as store
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
from M01_MasterData import M01_MasterData_Store_Flow as flow
from M01_MasterData import M01_MasterData_Store_List as store_list
from M01_MasterData import M01_MasterData_Store_Make as make
from M01_MasterData import M01_MasterData_Store_Revise as revise
@@ -215,9 +214,8 @@ def get_logic_source(src: str) -> dict:
@router.get("/logic/auto")
def get_logic_auto(key: str) -> dict:
"""견본 입력 — 마스터 보기용 자동값(프로젝트 기본값 아님) · 흐름 로직은 설계 예시값."""
got = _call(flow.auto, key)
return got if got is not None else _call(store.auto, key)
"""견본 입력 — 마스터 보기용 자동값(프로젝트 기본값 아님)."""
return _call(store.auto, key)
@router.get("/elements")
@@ -246,10 +244,6 @@ def get_materials(
@router.post("/calc")
def post_calc(body: CalcBody) -> dict:
"""줄에 「흐름」 이 있으면 흐름 셈(입력 = 설계 때 정할 것) · 없으면 옛 셈(정함 4)."""
got = _call(flow.calc, body.key, body.inputs, body.row)
if got is not None:
return got
with store.cm.mf.mp.choosing(body.고름):
got = _call(store.calc, body.key, body.inputs, body.row, body.file)
return _pct(got, body)
@@ -272,36 +266,15 @@ def _pct(got: dict, body: CalcBody) -> dict:
@router.post("/text")
def post_text(body: CalcBody) -> dict:
"""읽는 식 줄 — 몸은 `/calc` 와 같음 · 흐름 로직은 출력 상자 줄."""
got = _call(flow.text, body.key, body.inputs, body.row)
if got is not None:
return got
"""읽는 식 줄 — 몸은 `/calc` 와 같음."""
with store.cm.mf.mp.choosing(body.고름):
return _call(sheet.text, body.key, body.inputs, body.row, body.file) # 줄 덧칸(714-1)
@router.post("/save")
def post_save(body: SaveBody) -> dict:
"""「흐름」 줄은 검사 · 서버 다시 셈 · 새 줄(키 없음)은 자체 GX — {files, added, warnings}."""
return _call(flow.save, [f.model_dump() for f in body.files], body.개정확인)
@router.get("/logic/flow")
def get_logic_flow(key: str, file: str = "") -> dict:
"""흐름 받기 — {flow, converted, notes} · 흐름이 있으면 그대로 · 없으면 변환(파일 안 바뀜)."""
return _call(flow.get_flow, file, key)
class FlowDraft(BaseModel):
file: str = ""
key: str = ""
row: dict[str, Any]
@router.post("/logic/flow")
def post_logic_flow(body: FlowDraft) -> dict:
"""저장 안 한 고친 줄(옛 편집기) → 흐름 변환 — {flow, converted, notes} · 쓰기 없음."""
return _call(flow.get_flow, body.file, body.key, body.row)
"""로직 정본 줄 고침 = 개정 확인 · 새 줄(키 없음)은 자체 GX — {files, added, warnings}."""
return _call(make.save, [f.model_dump() for f in body.files], body.개정확인)
@router.post("/logic/new")
-422
View File
@@ -1,422 +0,0 @@
"""M01 로직 「흐름」 칸(PLAN 36-4 · 정함 1 · 2 · 4 · 10 · 11 · 12).
정함 = `docs/raw/M01_쉬운단가/흐름도_적용_정함.md`.
· 흐름 = DataFlow Model `CfLogic` 꼴 그대로 · 로직 줄 칸 「흐름」 · 있으면 흐름이 정본
· 쓰는 때 = `/save` 줄에 실어 온 것뿐(자동 변환 결과는 안 씀) — 꼴 검사(상자 · 선 · 돌고 도는 선) →
서버 셈(`M02_DF_Compute.run`)으로 다시 셈 → 받는 칸에 값만 되돌려 넣음 · 빈 받는 자리는 경고
· 새 줄(키 없음) + 흐름 = 자체 파일(`로직_자체.json`)에 더함 → 키 = 대장 `GX` 다음 · 구분 자체
· 계산 = 흐름이 있으면 흐름 셈 · 없으면 옛 셈(`Store.calc`) · 입력 이름 = 「설계 때 정할 것」
· 다른 로직 입력 = 마스터 키도 — 흐름 로직은 흐름 셈 · 옛 로직은 옛 셈(노무비 · 재료비 · 경비 · 계)
· 흐름 받기 = 줄의 흐름 그대로 · 없으면 sub7 변환(`M02_DF_Convert.convert`) · 아직 없으면 빈 흐름
권한 = `/api/m01` 전체가 시스템 관리자(`main.py`) — 공용 로직 흐름도 관리자가 고침.
"""
from __future__ import annotations
import importlib
from contextvars import ContextVar
from decimal import Decimal, InvalidOperation
from typing import Any
from common_util import common_util_money as mfm
from M01_MasterData import M01_MasterData_Store as store
from M01_MasterData import M01_MasterData_Store_Make as make
from M01_MasterData import M01_MasterData_Store_Revise as revise
from M02_MasterTemplete.DataFlow import M02_DF_Compute as dc
from M02_MasterTemplete.DataFlow import M02_DF_Values as dv
FLOW = "흐름"
KINDS = ("input", "function", "output")
CONVERT = "M02_MasterTemplete.DataFlow.M02_DF_Convert"
NOT_READY = "변환 준비 중"
_ACTIVE: ContextVar[tuple] = ContextVar("m01_flow_active", default=()) # 셈 중인 흐름 키
# ── 검사(Model `link` · `ruleWhy` 와 같은 뜻) ──────────────────────────────
def _outs(box: dict) -> set:
"""내보내는 알약 — Model `portsOut`."""
if box.get("kind") == "function":
return {((box.get("func") or {}).get("output") or {}).get("name")}
inp = box.get("input") or {}
if box.get("kind") != "input":
return set()
if inp.get("mode") == "logic":
return {p.get("name") for p in (inp.get("logic") or {}).get("outputs") or []}
out = {dc._port(e) for e in inp.get("elements") or []} if inp.get("mode") == "group" else set()
if inp.get("mode") == "group" and inp.get("table"):
out.add(dc.TABLE_PORT)
return out | {v.get("name") for v in inp.get("variables") or []}
def _ins(box: dict) -> set:
"""받는 알약 — Model `portsIn`."""
if box.get("kind") == "function":
return {p.get("port") for p in (box.get("func") or {}).get("inputs") or []}
if box.get("kind") == "output":
return {dc.OUTPUT_PORT}
inp = box.get("input") or {}
return {p.get("name") for p in (inp.get("logic") or {}).get("designs") or []}
def check(flow: Any) -> list[str]:
"""흐름 꼴 검사 — 틀리면 422 · 돌려받는 것 = 경고(빈 받는 자리)."""
if not isinstance(flow, dict) or not isinstance(flow.get("boxes"), list):
raise store.StoreError(422, {"errors": ["흐름 꼴이 아님 — 상자(boxes) 목록이 없음"]})
if not isinstance(flow.get("links"), list):
raise store.StoreError(422, {"errors": ["흐름 꼴이 아님 — 선(links) 목록이 없음"]})
errors, boxes = [], {}
for b in flow["boxes"]:
if not isinstance(b, dict) or not b.get("id"):
errors.append("번호(id) 없는 상자가 있음")
continue
name = b.get("name") or b["id"]
if b["id"] in boxes:
errors.append(f"상자 번호가 겹침 「{b['id']}」")
boxes[b["id"]] = b
if b.get("kind") not in KINDS:
errors.append(f"「{name}」 모르는 상자 종류 「{b.get('kind')}」")
elif b["kind"] == "function" and not ((b.get("func") or {}).get("output") or {}).get(
"name"
):
errors.append(f"「{name}」 함수 상자에 결과 알약이 없음")
taken = set()
for ln in flow["links"]:
src, to = (ln or {}).get("from") or {}, (ln or {}).get("to") or {}
a, b = boxes.get(src.get("box")), boxes.get(to.get("box"))
if (
a is None
or b is None
or src.get("name") not in _outs(a)
or to.get("name") not in _ins(b)
):
errors.append(f"선 「{(ln or {}).get('id')}」 이 없는 알약을 이음")
continue
if (to["box"], to["name"]) in taken:
errors.append(f"「{b.get('name')} · {to['name']}」 에 선이 둘 이상")
taken.add((to["box"], to["name"]))
for p in dc._sources(ln.get("rule")):
if p.get("name") not in _outs(boxes.get(p.get("box")) or {}):
errors.append(f"선 「{ln.get('id')}」 규칙의 조건 알약이 없음")
if not errors:
errors += [
f"「{b.get('name')}」 상자가 서로를 기다림(돌고 도는 선)" for b in dc._waves(flow)[1]
]
if errors:
raise store.StoreError(422, {"errors": errors})
return [
f"「{b.get('name')} · {port}」 받는 자리에 들어오는 선이 없음"
for b in boxes.values()
if b.get("kind") != "input"
for port in sorted(_ins(b) - {n for bx, n in taken if bx == b["id"]}, key=str)
]
# ── 다른 로직 · 계산 ───────────────────────────────────────────────────
def _old(key: str, designs: dict) -> dict:
"""옛 로직 값 — 옛 셈 · 내보내는 알약 = 노무비 · 재료비 · 경비 · 계(돈 아닌 로직은 결과)."""
given = {k: v for k, v in designs.items() if v is not None}
whole, mf = dv.SOURCE.get(), store.cm.mf
if whole is not None: # 단가 자리(프로젝트 사본)의 옛 로직
try:
given = mf.fill_empty(whole.logic(key), {k: _value(v) for k, v in given.items()})
got = mf.run(whole, key, given)
except (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError) as e:
return {"outputs": {}, "errors": [str(e) or type(e).__name__]}
if "결과" in got:
return {"outputs": {"결과": got["결과"]}, "errors": []}
return {"outputs": {k: got[k] for k in mf.BUNDLE}, "errors": []}
got = store.calc(key, given)
if not got.get("ok"):
return {"outputs": {}, "errors": [got.get("reason") or "옛 셈 오류"]}
if "result" in got:
return {"outputs": {"결과": got["result"]}, "errors": []}
return {"outputs": dict(got["sums"]), "errors": []}
def load(key: str) -> dict:
"""다른 로직 입력이 부르는 마스터 로직 — 흐름이면 흐름 · 옛 로직이면 옛 셈 함수(`calc`)."""
whole = dv.SOURCE.get()
row = whole.logic(key) if whole is not None else store.logic(key)["logic"]
if isinstance(row.get(FLOW), dict):
return {**row[FLOW], "key": key}
return {"key": key, "calc": lambda designs: _old(key, designs)}
def old_designs(key: str) -> list[dict]:
"""옛 로직의 받는 알약 = 로직 입력 — Model `CfDesign` 꼴."""
out = []
for one in store.logic(key)["logic"].get("입력") or []:
choices = one.get("고르기")
variable = {"name": one.get("이름"), "type": "choice" if choices else "number",
"unit": one.get("단위") or "-", "example": None} # fmt: skip
if choices:
variable["choices"] = choices
out.append({"box": "", "port": one.get("이름"), "kind": "변수",
"unit": one.get("단위") or "-", "variable": variable}) # fmt: skip
return out
def _value(v: Any, words: set | frozenset = frozenset()) -> Any:
"""화면이 보낸 값 — 수 글은 수로(「3」 → 3) · 고르기 글 값(「080」 · 「1048」)은 글 그대로."""
if isinstance(v, str) and v in words:
return v
if isinstance(v, str) and v.strip():
try:
return Decimal(v.strip())
except InvalidOperation:
return v
return store._dec(v)
def run(flow: dict, key: str, inputs: dict, strict: bool = False) -> dict:
"""흐름 셈 — 입력 = 「설계 때 정할 것」 이름 → 값 · strict(조합 · 프로젝트) = 안 준 입력은
예시값
대신 빈 값 + 알림 「빈 입력」(옛 엔진처럼 그 값을 쓰는 곳만 비움)."""
active = _ACTIVE.get()
if key and key in active: # 옛 셈을 건너 다시 돌아온 흐름(흐름 → 옛 로직 → 흐름)
raise store.cm.mf.FormulaError(f"로직이 서로를 부름 — {' → '.join([*active, key])}")
token = _ACTIVE.set((*active, key))
try:
words = { # 고르기에 글로 든 값 — 옛 셈처럼 글 그대로(수로 바꾸면 고르기 밖)
c
for d in dc.design_inputs(flow)
for c in (d.get("variable") or {}).get("choices") or []
if isinstance(c, str)
}
given = {k: _value(v, words) for k, v in (inputs or {}).items()}
flow = {**flow, "key": key}
values = dc._design_values(flow, given)
designs = dc.design_inputs(flow) if strict else []
lack = [d["port"] for d in designs if d["port"] not in given]
if lack: # 안 준 입력 — 엔진 `_inputs` 와 같이 멈춤(빈 값으로 준 입력은 아래 「빈 입력」)
raise store.cm.mf.FormulaError(f"「{key}」 입력 「{lack[0]}」 없음")
blank = [d for d in designs if given.get(d["port"]) is None]
for d in blank:
values.setdefault(d["box"], {})[d["port"]] = None
got = dc.run(flow, values, load, flow.get("userElements") or [], [key])
got["notes"] = [*(f"입력 「{d['port']}」 빈 입력" for d in blank), *got.get("notes", [])]
return got
finally:
_ACTIVE.reset(token)
def _engine(master, ref: str, row: dict, given: dict) -> dict:
"""옛 엔진 `mf.run` 갈래(조합 · 프로젝트 단가 · 읽는 식 · 옛 식의 로직(…)) — `run` 과 같은 꼴.
단가 · 값 · 표 = 부른 쪽이 넘긴 master(프로젝트 사본이면 그 단가 · `M02_DF_Values.SOURCE`).
"""
picks = (
store.cm.mf.mp.CHOSEN.get() or {}
) # 고른 자재 — 흐름은 재료 자리 이름으로(호표 차례 아님)
slots = {d["port"] for d in dc.design_inputs(row[FLOW]) if d.get("kind") == "재료 자리"}
given = {**given, **{k: v for k, v in picks.items() if k in slots}}
token = dv.SOURCE.set(master)
try:
got = run(row[FLOW], ref, given, strict=True)
finally:
dv.SOURCE.reset(token)
# 빈 값(빈 입력 · 단가 없는 품목 · 표에 없는 조건 → #N/A)을 쓴 칸은 엔진처럼 그 값만 비움 — 알림
empty = [e for e in got["errors"] if "#N/A" in e or "단가 없는 품목" in e]
stop = [e for e in got["errors"] if e not in empty]
if stop:
raise store.cm.mf.FormulaError(f"「{ref}」 흐름 셈 — {' · '.join(stop)}")
cost, notes = got["cost"], [*(got.get("notes") or []), *empty]
outs = [b for b in row[FLOW].get("boxes") or [] if b.get("kind") == "output"]
extra = {"중간": {}, "값들": cost["값들"], **({"알림": notes} if notes else {})}
if outs and all((b.get("output") or {}).get("cost") in (None, "값") for b in outs):
first = got["outputs"].get(outs[0]["id"]) # 돈 아닌 로직 = 첫 출력이 결과 · 나머지는 값들
return {"결과": first, **extra}
lines = [] # 옛 「줄」 꼴 — 돈 출력 상자마다 한 줄(수량 1 · 단가 = 금액)
for b in outs:
kind, value = (b.get("output") or {}).get("cost"), got["outputs"].get(b["id"])
value = None if value == dc.NA_VALUE else value
if kind in dc.COSTS:
unit = (b.get("output") or {}).get("unit") or ""
lines.append({"이름": b.get("name") or "", "단위": unit,
"수량": Decimal(1), "단가": value, "금액": value, "비목": {kind: value},
**({} if value is not None else {"까닭": "값 없음"})}) # fmt: skip
sums = {k: cost[k] for k in dc.COSTS}
return {"줄": lines, **sums, "계": cost["계"], **extra}
def _inputs(row: dict) -> list[dict]:
"""흐름 로직 입력 = 「설계 때 정할 것」 → 옛 입력 꼴(이름 · 단위 · 고르기 · 범위 · 예시)."""
out = []
for d in dc.design_inputs(row[FLOW]):
v = d.get("variable") or {}
one = {"이름": d["port"], "단위": None if d.get("unit") in (None, "-") else d["unit"]}
one |= {"고르기": v["choices"]} if v.get("choices") else {}
one |= {"범위": v["range"]} if v.get("range") else {}
out.append(one | {"예시": v.get("example")})
return out
store.cm.mf.FLOW_RUN = _engine # 옛 엔진을 부르는 곳이 모두 흐름 로직을 흐름 셈으로(한 곳 갈래)
store.cm.mf.FLOW_INPUTS = (
_inputs # 입력을 읽는 곳(시험입력 · 자동값 · 프로젝트 입력 · 연결)도 흐름에서
)
def text(key: str, inputs: dict, row: dict | None = None) -> dict | None:
"""읽는 식 줄(`/text` 꼴) — 흐름 로직이면 출력 상자를 비목 묶음 줄로 · 아니면 None(옛 글)."""
got = calc(key, inputs, row)
if got is None or not got.get("flow") or "outputs" not in got:
return got
groups: dict[str, list] = {}
for o in got["outputs"]:
value = o["value"]
# 비목 소계 = 정수 · 그 밖 = 수량 꼴
shown = (mfm.fmt_money if o["cost"] else mfm.fmt)(value)
line = {"이름": o["name"], "글": shown, "이름글": o["name"], "금액": value, "조각": []}
groups.setdefault(o["cost"] or "값", []).append(line)
out = [{"비목": k, "줄": v} for k, v in groups.items()]
return {
"ok": got["ok"],
"flow": True,
"groups": out,
**({"reason": got["reason"]} if "reason" in got else {}),
}
def auto(key: str) -> dict | None:
"""견본 입력 — 흐름 로직이면 「설계 때 정할 것」 예시값 · 아니면 None(옛 자동값)."""
flow = store.logic(key)["logic"].get(FLOW)
if not isinstance(flow, dict):
return None
return {
"값": {d["port"]: (d.get("variable") or {}).get("example") for d in dc.design_inputs(flow)}
}
def calc(key: str, inputs: dict, row: dict | None = None) -> dict | None:
"""계산 갈래 — 줄에 흐름이 없으면 None(옛 셈 그대로) · 있으면 흐름 셈을 `/calc` 꼴로."""
if row is not None:
flow, key = row.get(FLOW), str(row.get("키") or key)
else:
try:
flow = store.logic(key)["logic"].get(FLOW)
except store.StoreError:
return None
if not isinstance(flow, dict):
return None
try:
got = run(flow, key, inputs)
except store.cm.mf.FormulaError as e:
return {"ok": False, "flow": True, "reason": str(e)}
names = {b["id"]: b for b in flow.get("boxes") or []}
outputs = [
{"box": i, "name": names[i].get("name"), "cost": (names[i].get("output") or {}).get("cost"),
"unit": (names[i].get("output") or {}).get("unit") or names[i].get("unit"), "value": v}
for i, v in got["outputs"].items() if i in names
] # fmt: skip
cost = got["cost"]
out = {"ok": not got["errors"], "flow": True, "lines": [], "middle": {},
"sums": {k: cost[k] for k in (*dc.COSTS, "계")}, "values": cost["값들"],
"outputs": outputs, "errors": got["errors"], "breaks": got["breaks"]} # fmt: skip
if got["errors"]:
out["reason"] = " · ".join(got["errors"])
return out
# ── 흐름 받기 ──────────────────────────────────────────────────────────
def _row(file: str, key: str) -> dict:
if not file:
return store.logic(key)["logic"]
data = store.read(file)[0]
hit = next((r for r in data.get("줄") or [] if str(r.get("키")) == key), None)
if hit is None:
raise store.StoreError(404, f"{file} · 없는 키 「{key}」")
return hit
def get_flow(file: str, key: str, draft: dict | None = None) -> dict:
"""{flow, converted, notes} — 흐름이 있으면 그대로 · 없으면 변환(파일은 안 바뀜)
· draft = 저장 안 한 고친 줄(옛 편집기) — 늘 그 줄 그대로 변환(쓰기 없음)."""
row = _row(file, key) if draft is None else store._dec({**draft, "키": draft.get("키") or key})
if draft is not None:
row.pop(FLOW, None)
elif isinstance(row.get(FLOW), dict):
return {"flow": {**row[FLOW], "key": key}, "converted": False, "notes": []}
try:
convert = importlib.import_module(CONVERT).convert
except ModuleNotFoundError as e:
if e.name != CONVERT:
raise
empty = {"key": key, "name": row.get("이름") or "", "resultUnit": row.get("결과단위") or "",
"boxes": [], "links": [], "userElements": []} # fmt: skip
return {"flow": empty, "converted": True, "notes": [NOT_READY]}
got = convert(row, {"key": key, "file": file, "master": store.loaded()[1]})
return {"flow": {**got["flow"], "key": key}, "converted": True, "notes": got.get("notes") or []}
# ── 저장 ──────────────────────────────────────────────────────────────
def _redo(row: dict, key: str) -> list[str]:
"""흐름 검사 → 서버 셈으로 다시 셈 → 받는 칸 값만 되돌려 넣음 · 돌려받는 것 = 경고."""
warns = check(row[FLOW])
got = run(row[FLOW], key, {})
filled = got["filled"]
filled.pop("key", None) # 키는 줄의 「키」 하나 — 흐름 안에 두지 않음
row[FLOW] = filled
return warns + got["errors"]
def _bare(row: dict) -> dict:
"""흐름 로직 줄 — 계산 칸(중간 · 호표 · 결과 · 덧줄)은 지움(흐름이 정본) · 「입력」 은 흐름의
「설계 때 정할 것」 으로 같은 꼴로 다시 씀(로직 입력 약속 하나 · 설명은 이름이 같으면 살림)
· 끝수 · 시험입력 · 메타 칸은 둠 · 출처 없으면 「자체」."""
kept = {k: v for k, v in row.items() if k not in store.cm.mf.CALC_FIELDS}
said = {s.get("이름"): s.get("설명") for s in row.get("입력") or [] if s.get("설명")}
inputs = []
for spec in store.cm.mf.inputs_of(row):
one = {
"이름": spec["이름"],
**({"설명": said[spec["이름"]]} if spec["이름"] in said else {}),
}
one |= {k: spec[k] for k in ("단위", "고르기", "범위") if spec.get(k) is not None}
inputs.append(one)
return {"끝수": None, **kept, "입력": inputs, "출처": row.get("출처") or "자체"}
def save(batch: list[dict], confirmed: bool = False) -> dict:
"""`/save` — 흐름 줄은 검사 · 다시 셈 · 새 줄은 자체 GX · 그 밖은 `Store.save` 그대로 ·
로직 정본 줄 고침은 `confirmed`(「원문 개정 반영」) 없으면 409(`Store_Revise` · 713-1)."""
revise.guard(batch, confirmed)
warns, fresh = [], False
for part in batch:
for ch in part.get("changes") or []:
row = ch.get("row")
if not isinstance(row, dict) or FLOW not in row:
continue
check(row[FLOW]) # 꼴부터 — 입력 약속을 흐름에서 다시 쓰기 전에
new = ch.get("op") == "add" or (ch.get("op") == "edit" and not ch.get("key"))
if new:
if part.get("file") != make.OWN:
raise store.StoreError(400, f"새 흐름 로직은 「{make.OWN}」 에만 더함")
row = make._clean(_bare(row), str(row.get("소유") or make.OWNERS[0]))
ch.update(op="add", key=None)
row = _bare(row) if not new else row
ch["row"] = row
name = row.get("이름") or ch.get("key") or "새 로직"
warns += [f"「{name}」 {w}" for w in _redo(row, str(ch.get("key") or ""))]
if part.get("file") == make.OWN and not make._own_path().is_file():
make._make_file()
fresh = True
part["version"] = part.get("version") or store.read(make.OWN)[1]
before = {p["file"]: _keys(p["file"]) for p in batch}
try:
files = store.save(batch)
except store.StoreError:
if fresh:
make._own_path().unlink(missing_ok=True)
raise
added = [{"file": f, "key": k} for f, old in before.items() for k in _keys(f) if k not in old]
return {"files": files, "added": added, "warnings": warns}
def _keys(file: str) -> list[str]:
try:
data = store.read(file)[0]
except store.StoreError:
return []
return [str(r.get("키")) for r in data.get(store.items_key(data)) or []]
@@ -12,6 +12,7 @@ from pathlib import Path
from common_util.common_util_time import kst_today
from M01_MasterData import M01_MasterData_Store as store
from M01_MasterData import M01_MasterData_Store_Revise as revise
cm, mk, mcp = store.cm, store.mk, store.mcp
@@ -278,6 +279,43 @@ def _make_file() -> None:
path.write_text(store.dump(head), encoding="utf-8", newline="\n")
def save(batch: list[dict], confirmed: bool = False) -> dict:
"""`/save` — 로직 정본 줄 고침은 `confirmed`(「원문 개정 반영」) 없으면 409(`Store_Revise`) ·
자체 파일 새 줄(키 없음)은 서버가 칸을 잡고 GX 다음 키 — {files, added, warnings}."""
revise.guard(batch, confirmed)
fresh = False
for part in batch:
if part.get("file") != OWN:
continue
for ch in part.get("changes") or []:
row = ch.get("row")
if isinstance(row, dict) and (ch.get("op") == "add" or not ch.get("key")):
ch.update(op="add", key=None, row=_clean(row, str(row.get("소유") or OWNERS[0])))
if not _own_path().is_file():
_make_file()
fresh = True
part["version"] = part.get("version") or store.read(OWN)[1]
before = {p["file"]: _file_keys(p["file"]) for p in batch}
try:
files = store.save(batch)
except store.StoreError:
if fresh:
_own_path().unlink(missing_ok=True)
raise
added = [
{"file": f, "key": k} for f, old in before.items() for k in _file_keys(f) if k not in old
]
return {"files": files, "added": added, "warnings": []}
def _file_keys(file: str) -> list[str]:
try:
data = store.read(file)[0]
except store.StoreError:
return []
return [str(r.get("키")) for r in data.get(store.items_key(data)) or []]
def _find(key: str) -> tuple[str, dict]:
got = store.logic(key)
return got["file"], got["logic"]
+13 -128
View File
@@ -1,6 +1,6 @@
/* =============================================================================
* M01_MasterData_UI_LogicLab.ts
* 관리자 화면 — 로직 칸. 로직을 열면 두 칸 — 왼쪽 시트(794-1 · 식 고침 = 관리자) · 단가산출 상세 · 시험 계산 / 오른쪽 흐름도
* 관리자 화면 — 로직 칸. 로직을 열면 두 칸 — 왼쪽 시트(794-1 · 식 고침 = 관리자) · 단가산출 상세 · 시험 계산 / 오른쪽 원문
* 로직 목록(원문·장 고르기 · 찾기 · 막힘 거름)은 왼쪽 패널 「단가산출 로직」 컨테이너(`listHost`) 안
*
* 데이터 흐름(CLAUDE.md 5장) — 고친 것은 캐시(sessionStorage)에 쌓고 [저장] 한 번에 `POST /save`.
@@ -38,18 +38,6 @@ import { buildLabDetail, isOwnLogic, trial } from "./M01_MasterData_UI_LogicLab_
import { setLogicOpener } from "./M01_MasterData_UI_LogicLab_Modal";
import { openCopyScreen } from "./M01_MasterData_UI_LogicLab_Copy";
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
import {
emptyFlow,
designFields,
examplesOf,
fetchFlow,
fetchFlowFor,
sameShape,
withExamples,
mountLogicFlow,
tf,
type LogicFlow,
} from "./M01_MasterData_UI_LogicLab_Flow";
import { askBasicInfo } from "./M01_MasterData_UI_LogicLab_Basic";
import { saveRevised } from "./M01_MasterData_UI_LogicLab_Revise";
import { buildLogicSide } from "./M01_MasterData_UI_LogicLab_Side";
@@ -57,7 +45,6 @@ import { createViewPicker } from "./M01_MasterData_UI_LogicLab_View";
import { createLabSheet } from "./M01_MasterData_UI_LogicLab_Sheet";
import { buildHeadBar } from "./M01_MasterData_UI_Head";
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
import type { CfLogic } from "../M02_MasterTemplete/DataFlow/M02_DF_Model";
import "./M01_MasterData_UI_Logic_Style.css";
import { createEmpty } from "@ui/ui_template_empty";
@@ -83,20 +70,11 @@ interface Opened extends Draft {
lastText?: TextAnswer;
ownHost: HTMLElement;
values: Record<string, string>;
/** 흐름 로직의 왼쪽 「지금 내용」 자리 — 흐름도 왼쪽 보기가 그림 */
flowHost: HTMLElement;
/** 오른쪽 흐름도의 기준(예시값만 다른지 견줄 때) */
flowBase?: CfLogic;
}
/** 줄의 흐름(있으면 흐름도로 셈) */
const flowOf = (row: LogicRow | null): CfLogic | null => (row?.흐름 as CfLogic | undefined) ?? null;
const OWN_FILE = "로직_자체.json";
/** 시험 계산에 넘길 줄 — 흐름 로직은 입력 칸을 흐름의 「설계 때 정할 것」으로(옛 식 칸은 서버가 지움) · 그 밖은 시험 고침(722-2) 덮은 사본 */
const calcRow = (row: LogicRow | null): LogicRow | null => {
const f = flowOf(row);
return f && row ? { ...row, 입력: designFields(f) } : trial.apply(row);
};
/** 시험 계산에 넘길 줄 — 시험 고침(722-2) 덮은 사본 */
const calcRow = (row: LogicRow | null): LogicRow | null => trial.apply(row);
const cacheKey = "m01_logic_drafts_lab";
const ownHost = (): HTMLElement => el("div", { className: "m01lab__own" });
@@ -123,20 +101,18 @@ export async function mountM01LogicLab(
let drafts = loadDrafts();
let opened: Opened | null = null;
let calcInputs = "";
/** 오른쪽 흐름도 패널 — 로직을 열 때마다 새로 */
let flowNow: LogicFlow | null = null;
const admin = await fetchSessionUser()
.then((u) => u?.role === "SYSTEM_ADMIN")
.catch(() => false);
let recalc: number | undefined;
const sheet = createLabSheet({
admin,
onInput: (name, v) => opened && syncLeft(opened, { [name]: v }, true),
onInput: (name, v) => opened && syncLeft(opened, { [name]: v }),
edited: () => (touch(), drawEditor(), drawCalc()), // 줄 사본 → 캐시 → [저장](기존 길)
run: () => calc.querySelector<HTMLButtonElement>("[data-calc-run]")?.click(),
});
const editor = el("div", { className: "m01-logic__editor" });
// 로직 상세 — 왼쪽 지금 내용 · 오른쪽 접는 패널 둘(흐름도 · 원문 · 하나만 열림 · 폭 공유)
// 로직 상세 — 왼쪽 지금 내용 · 오른쪽 접는 패널 「원문」
const panes = buildLogicSide(editor);
const views = createViewPicker(editor); // 보기 고르기 — 머리줄 [신규 로직] 왼쪽
const errors = el("div", {
@@ -195,7 +171,6 @@ export async function mountM01LogicLab(
if (opened.origKey !== null && now === opened.original) delete drafts[opened.id];
else drafts[opened.id] = pick(opened, clone(opened.row));
persist();
if (!flowOf(opened.row)) scheduleFlow(opened); // 왼쪽 편집기로 고침 — 오른쪽 흐름도도 따라감
const inputs = JSON.stringify(calcRow(opened.row)?.입력 ?? []);
if (inputs !== calcInputs) drawCalc();
if (isOwnLogic(opened.row)) {
@@ -263,120 +238,31 @@ export async function mountM01LogicLab(
values: current.values,
ownHost: current.ownHost,
onChange: touch,
onInput: (name, v) => syncLeft(current, { [name]: v }, true),
onInput: (name, v) => syncLeft(current, { [name]: v }),
onTrial: drawCalc,
calcHost: calc,
flowHost: flowOf(current.row) ? current.flowHost : undefined,
admin,
sheet: sheet.root,
});
sheet.draw(current.row as LogicRow, current.original, current.values);
flowNow?.redrawLeft();
};
/** 왼쪽 고침 뒤 잠시 쉬면 고친 줄을 서버가 흐름도로 바꿔 줌 → 오른쪽에 넣음 · 마지막 물음만 받음 */
let flowTimer: number | undefined;
let flowSeq = 0;
const scheduleFlow = (o: Opened): void => {
window.clearTimeout(flowTimer);
flowTimer = window.setTimeout(() => {
const seq = ++flowSeq;
if (!o.row) return;
void fetchFlowFor(o.row, o.origKey ?? "", o.file).then((got) => {
if (!got || seq !== flowSeq || opened !== o || !flowNow || flowOf(o.row)) return;
o.flowBase = withExamples(got.flow, o.values);
flowNow.set(clone(o.flowBase));
});
}, 700);
};
/** 오른쪽 흐름도 — 줄의 흐름 · 없으면 서버 변환(흐름 받기) · 그것도 없으면 빈 흐름 */
const startFlow = async (o: Opened): Promise<void> => {
const row = o.row;
if (!row) return;
panes.flowSplit.right.replaceChildren(
el("p", { className: "m01lab-flow__wait", text: tf("Waiting") }),
);
const own = flowOf(row);
const got = own || o.origKey === null ? null : await fetchFlow(o.origKey, o.file);
// 시험 입력 한 벌 — 열 때 왼쪽 시험 계산 입력을 흐름 변수 예시값으로
o.flowBase = withExamples(
own ?? got?.flow ?? (await emptyFlow(row.이름, row.결과단위)),
o.values,
);
if (opened !== o) return;
try {
const made = await mountLogicFlow({
split: panes.flowSplit,
folded: panes.flowFolded(),
onFold: (on) => panes.pick(on ? "none" : "flow"),
flow: clone(o.flowBase),
title: row.이름 || row.키,
readOnly: !admin,
notes: got?.notes ?? [],
converted: !!got?.converted,
leftHost: () => (flowOf(o.row) ? o.flowHost : null),
onChange: (lg) => {
if (!o.row || opened !== o) return;
const was = flowOf(o.row);
if (sameShape(lg, was ?? o.flowBase ?? lg)) {
// 예시값만 바뀜 — 고친 것 아님 · 왼쪽 시험 계산 입력에 같은 이름으로 넣고 다시 셈
o.flowBase = lg;
syncLeft(o, examplesOf(lg));
return;
}
o.row.흐름 = lg;
touch();
if (!was) drawEditor(); // 이제 흐름 로직 — 왼쪽을 흐름 보기로
},
});
if (opened !== o) return made.destroy();
flowNow = made;
panes.bindFlow(made.fold);
made.setExamples(o.values); // 띄우는 사이에 온 자동값까지
} catch (e) {
panes.flowSplit.right.replaceChildren(
el("p", { className: "ui-text ui-text--muted", text: tf("Failed", String(e)) }),
);
}
};
/** 흐름 예시값 → 왼쪽 시험 계산 입력(이름이 있는 칸만) · 바뀌면 왼쪽을 다시 그리고 셈 */
const syncLeft = (o: Opened, ex: Record<string, string>, loud = false): void => {
/** 텍스트 수식 · 시트 입력 → 왼쪽 시험 계산 입력(이름이 있는 칸만) · 바뀌면 다시 그리고 셈 */
const syncLeft = (o: Opened, ex: Record<string, string>): void => {
const names = new Set((calcRow(o.row)?.입력 ?? []).map((x) => x.이름));
const changed = Object.entries(ex).filter(([k, v]) => names.has(k) && o.values[k] !== v);
if (!changed.length) return;
quiet = !loud; // 텍스트 수식 고침(loud)은 흐름 예시값에도 — 시험 계산 칸 고침과 같은 길
for (const [k, v] of changed) o.values[k] = v;
quiet = false;
drawCalc();
calc.querySelector<HTMLButtonElement>("[data-calc-run]")?.click();
};
/** 왼쪽 시험 계산 입력 — 칸이 바뀌면 흐름 예시값으로(이 로직이 열려 있을 때만) */
let quiet = false;
const watchValues = (): Record<string, string> => {
const self: Record<string, string> = new Proxy({} as Record<string, string>, {
set(t, k, v) {
const was = t[k as string];
t[k as string] = v as string;
if (!quiet && was !== v && opened?.values === self)
flowNow?.setExamples({ [k as string]: v as string });
return true;
},
});
return self;
};
const show = (next: Opened | null): void => {
flowNow?.destroy();
flowNow = null;
panes.bindFlow(null);
opened = next;
panes.showSource(next?.row?.출처 ?? "");
errors.hidden = true;
drawEditor();
drawCalc();
if (next?.row) void startFlow(next);
};
const open = async (id: string, sub: string, key: string): Promise<void> => {
@@ -391,8 +277,7 @@ export async function mountM01LogicLab(
lines: null,
text: null,
ownHost: ownHost(),
values: watchValues(),
flowHost: el("div"),
values: {},
});
return;
}
@@ -411,8 +296,7 @@ export async function mountM01LogicLab(
lines: null,
text: null,
ownHost: ownHost(),
values: watchValues(),
flowHost: el("div"),
values: {},
});
} catch (error) {
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
@@ -557,7 +441,8 @@ export async function mountM01LogicLab(
...info,
입력: [],
중간: [],
흐름: await emptyFlow(info.이름, info.결과단위),
호표: [], // 빈 시트 줄 — 자체 로직 편집기 · 시트로 채움
끝수: null,
};
const id = logicId("자체", `새${Date.now()}`);
drafts[id] = { sub: "자체", file: OWN_FILE, version: own?.version ?? "", origKey: null, row };
@@ -595,7 +480,7 @@ export async function mountM01LogicLab(
status: [pending],
buttons: [
views.root,
createButton({ label: tf("New"), variant: "filled", onClick: () => void onNew() }),
createButton({ label: tl("New"), variant: "filled", onClick: () => void onNew() }),
copyButton,
createButton({ label: tx("Bar_Delete"), variant: "danger", onClick: () => void onDelete() }),
discardButton,
@@ -1,7 +1,7 @@
/* =============================================================================
* M01_MasterData_UI_LogicLab_Basic.ts
* 「신규 로직」 기본정보 칸 — 이름 · 원문번호 · 결과 단위 · 출처(구분 = 자체). 확인하면 값을 돌려줌 · 취소면 null
* 저장은 하지 않음 — 부른 쪽이 빈 흐름도와 함께 캐시(drafts)에 새 줄로 넣고 [저장] 때 서버가 GX 새 키를 줌.
* 저장은 하지 않음 — 부른 쪽이 빈 시트 줄로 캐시(drafts)에 넣고 [저장] 때 서버가 GX 새 키를 줌.
* ========================================================================== */
import { currentLanguageIndex } from "@ui/ui_template_locale";
@@ -18,7 +18,7 @@ import { trial } from "./M01_MasterData_UI_LogicLab_Cover";
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
import "./M01_MasterData_UI_LogicLab_Style.css";
import "./M01_MasterData_UI_LogicLab_Pill.css";
import { block, nowInputs } from "./M01_MasterData_UI_LogicLab_View";
import { block } from "./M01_MasterData_UI_LogicLab_View";
export { trial };
@@ -30,10 +30,7 @@ export interface DetailContext extends HoContext {
onChange: () => void;
/** 시험 계산 칸(`buildCalc` 가 채움) — 둘째 컨테이너 안에 놓음 */
calcHost: HTMLElement;
/** 흐름이 있는 로직 — 이 칸에 왼쪽 「지금 내용」(단가산출 상세 · 텍스트 수식 = `buildFlowView`)을 그림 ·
* 있으면 호표 · 읽는 식 두 묶음 대신 이 칸을 놓음(기본정보 · 시험 계산은 그대로) */
flowHost?: HTMLElement;
/** 시스템 관리자 — 기본 로직 안내 글이 다름(흐름도에서 고침 / 보기만) */
/** 시스템 관리자 — 기본 로직 안내 글이 다름(시트에서 고침 / 보기만) */
admin?: boolean;
/** 722-2 시험 고침 뒤 — 시험 계산 칸을 고친 줄로 다시 그림(있으면 그 자리 고침 켜짐 · 셈은 여기서 누름) */
onTrial?: () => void;
@@ -244,12 +241,8 @@ export function buildLabDetail(host: HTMLElement, ctx: DetailContext): void {
]),
"est",
),
...(ctx.flowHost && !ctx.cover
? [block(nowInputs(ctx.values), "ho"), ctx.flowHost]
: [
ctx.cover ? block(section(tl("Ho_Title"), [ctx.cover.ho]), "ho") : hoBox(ctx),
block(section(tl("Ho_Title"), [formula]), "formula"),
]),
ctx.cover ? block(section(tl("Ho_Title"), [ctx.cover.ho]), "ho") : hoBox(ctx),
block(section(tl("Ho_Title"), [formula]), "formula"),
);
const own = kept?.node.dataset.own; // 기본정보 칸은 새로 만들어지므로 이름표로 다시 찾음
const back = own ? host.querySelector<HTMLInputElement>(`[data-own="${own}"]`) : kept?.node;
@@ -1,18 +0,0 @@
.m01lab-flow__wait {
margin: 0;
padding: var(--spacing-16);
border: 1px dashed var(--color-border);
border-radius: var(--radius-md);
color: var(--color-text-secondary);
text-align: center;
}
.m01lab-flow__note {
margin: 0 0 var(--spacing-8);
color: var(--color-text-secondary);
font-size: var(--text-body-sm);
}
.m01lab-flow__note[hidden] {
display: none;
}
@@ -1,273 +0,0 @@
/* =============================================================================
* M01_MasterData_UI_LogicLab_Flow.ts
* 로직 상세 오른쪽 흐름도 — 칸 · 폭은 오른쪽 접는 패널(LogicLab_Side) 몫
* 잇기(mountLogicFlow) — 패널(M02_DF_Panel) · 왼쪽 보기(LogicLab_FlowView) · 입력 값 · 다른 로직 흐름 받기
* DataFlow 파일은 import() 로 늦게(엔진이 M01 첫 묶음에 안 들어가게). 정함 `docs/raw/M01_쉬운단가/흐름도_적용_정함.md` 2 · 5 · 6 · 7
* ========================================================================== */
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { el } from "@ui/ui_template_elements";
import { API_BASE_URL } from "@config/config_frontend";
import type { CfLogic } from "../M02_MasterTemplete/DataFlow/M02_DF_Model";
import type { CfValues } from "../M02_MasterTemplete/DataFlow/M02_DF_Eval";
import "./M01_MasterData_UI_LogicLab_Flow.css";
const TEXT = {
New: ["신규 로직", "New logic"],
Waiting: ["흐름도 불러오는 중", "Loading the flow diagram"],
Converted: [
"이 로직의 계산 방법을 흐름도로 보여 줌",
"Shows how this logic is calculated as a flow diagram",
],
Failed: ["흐름도를 못 띄움 — {v}", "Couldn't open the flow diagram — {v}"],
} as const;
export const tf = (key: keyof typeof TEXT, v = ""): string =>
TEXT[key][currentLanguageIndex === 1 ? 1 : 0].replace("{v}", v);
export interface SplitHandle {
/** 오른쪽 칸 — 흐름도 패널을 붙이는 곳 */
right: HTMLElement;
/** 패널이 알린 너비(px) · null = 패널 내림 */
setRight(px: number | null): void;
}
/* ── 흐름 받기 · 잇기 ─────────────────────────────────────────────────── */
export interface FlowAnswer {
flow: CfLogic;
converted: boolean;
notes: string[];
}
/** 마스터 로직의 흐름 — 줄에 흐름이 있으면 그대로 · 없으면 서버가 계산 방법을 흐름도로 바꿔 줌 · 못 받으면 null */
export async function fetchFlow(key: string, file = ""): Promise<FlowAnswer | null> {
try {
const q = new URLSearchParams({ key, ...(file ? { file } : {}) });
const r = await fetch(`${API_BASE_URL}/m01/logic/flow?${q}`, { credentials: "include" });
if (!r.ok) return null;
const got = (await r.json()) as Partial<FlowAnswer>;
return got.flow ? { flow: got.flow, converted: !!got.converted, notes: got.notes ?? [] } : null;
} catch {
return null;
}
}
/** 고치는 중인 줄의 흐름 — 왼쪽 편집기로 고친 줄(저장 전)을 서버가 흐름도로 바꿔 줌 · 못 받으면 null */
export async function fetchFlowFor(
row: unknown,
key: string,
file: string,
): Promise<FlowAnswer | null> {
try {
const r = await fetch(`${API_BASE_URL}/m01/logic/flow`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ file, key, row }),
});
if (!r.ok) return null;
const got = (await r.json()) as Partial<FlowAnswer>;
return got.flow ? { flow: got.flow, converted: !!got.converted, notes: got.notes ?? [] } : null;
} catch {
return null;
}
}
const loadFlowMods = () =>
Promise.all([
import("../M02_MasterTemplete/DataFlow/M02_DF_Panel"),
import("./M01_MasterData_UI_LogicLab_FlowView"),
import("../M02_MasterTemplete/DataFlow/M02_DF_Model"),
import("../M02_MasterTemplete/DataFlow/M02_DF_Eval"),
import("../M02_MasterTemplete/DataFlow/M02_DF_Api"),
import("../M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail"),
]).then(([panel, view, model, ev, api, detail]) => ({ panel, view, model, ev, api, detail }));
/** 빈 흐름 — 신규 로직 · 흐름을 못 받은 로직 */
export async function emptyFlow(name: string, unit: string): Promise<CfLogic> {
return (await import("../M02_MasterTemplete/DataFlow/M02_DF_Model")).newLogic(name, unit);
}
/* ── 시험 입력 한 벌 — 왼쪽 시험 계산 입력 ↔ 흐름 변수 예시값(이름이 같은 변수끼리) ── */
const varsOf = (lg: CfLogic) =>
lg.boxes.flatMap((b) => [...(b.input?.variables ?? []), ...(b.func?.variables ?? [])]);
/** 흐름 변수 예시값 — 이름 → 글(빈 값은 뺌) */
export function examplesOf(lg: CfLogic): Record<string, string> {
const out: Record<string, string> = {};
for (const v of varsOf(lg))
if (v.example !== null && v.example !== undefined && v.example !== "")
out[v.name] = String(v.example);
return out;
}
/** 예시값을 넣은 새 흐름 — 수처럼 생긴 글은 수로 · 빈 값은 그대로 둠 */
export function withExamples(lg: CfLogic, vals: Record<string, string>): CfLogic {
const next = JSON.parse(JSON.stringify(lg)) as CfLogic;
for (const v of varsOf(next)) {
const raw = (vals[v.name] ?? "").trim();
if (raw) v.example = /^-?\d+(\.\d+)?$/.test(raw) ? Number(raw) : raw;
}
return next;
}
/** 흐름 로직의 시험 계산 입력 칸 — 「설계 때 정할 것」(변수 · 고르기 · 범위 · 단위) · 이름이 겹치면 하나 */
export function designFields(
lg: CfLogic,
): { 이름: string; 단위?: string; 고르기?: (string | number)[]; 범위?: [number, number] }[] {
const seen = new Set<string>();
return varsOf(lg).flatMap((v) => {
if (!v.name || seen.has(v.name)) return [];
seen.add(v.name);
return [
{
이름: v.name,
...(v.unit && v.unit !== "-" ? { 단위: v.unit } : {}),
...(v.type === "choice" && v.choices?.length ? { 고르기: v.choices } : {}),
...(v.range ? { 범위: v.range } : {}),
},
];
});
}
/** 예시값만 다른가 — 그 밖이 같으면 고친 것으로 안 셈 */
export function sameShape(a: CfLogic, b: CfLogic): boolean {
const strip = (lg: CfLogic): string => {
const c = JSON.parse(JSON.stringify(lg)) as CfLogic;
for (const v of varsOf(c)) v.example = null;
return JSON.stringify(c);
};
return strip(a) === strip(b);
}
export interface LogicFlowOpts {
split: SplitHandle;
flow: CfLogic;
title: string;
readOnly: boolean;
/** 변환 알림 — 패널 위 한 줄 */
notes: string[];
converted: boolean;
/** 흐름 로직이면 왼쪽 「지금 내용」 자리(LogicLab_Detail 의 flowHost) */
leftHost(): HTMLElement | null;
onChange(lg: CfLogic): void;
/** 패널 접힘 — 부르는 쪽(오른쪽 두 패널)이 정함 */
folded?: boolean;
onFold?(on: boolean): void;
}
export interface LogicFlow {
/** 밖에서 바꿈(고친 것 버리기) — 패널 · 왼쪽 함께 */
set(lg: CfLogic): void;
/** 왼쪽만 다시(왼쪽 화면을 새로 그린 뒤) */
redrawLeft(): void;
/** 왼쪽 시험 계산 입력이 바뀜 — 같은 이름 변수의 예시값으로(고친 것 아님) */
setExamples(vals: Record<string, string>): void;
/** 패널 접기 · 펴기 */
fold(on: boolean): void;
destroy(): void;
}
/** 오른쪽 패널을 띄우고 왼쪽 보기 · 입력 값 · 다른 로직을 이음 */
export async function mountLogicFlow(opts: LogicFlowOpts): Promise<LogicFlow> {
const M = await loadFlowMods();
const host = el("div", { className: "m01lab-flow__panel" });
const note = el("p", { className: "m01lab-flow__note" });
const notes = [...(opts.converted ? [tf("Converted")] : []), ...opts.notes];
note.textContent = notes.join(" · ");
note.hidden = !notes.length;
opts.split.right.replaceChildren(note, host);
let lg = opts.flow;
let values: CfValues = {};
let valueSig = "";
let logics: Record<string, CfLogic> = {};
let logicSig = "";
let gone = false;
/** 다른 로직 — 마스터 로직(흐름 받기) */
const getLogic = async (key: string): Promise<CfLogic | null> =>
(await fetchFlow(key))?.flow ?? null;
const left = (): void => {
const at = opts.leftHost();
if (!at || gone) return;
M.view.buildFlowView(at, lg, values, { logics });
};
/** 입력 값 · 다른 로직 — 물음이 바뀐 때만 다시 받음 · 받으면 왼쪽 다시 */
const refresh = (): void => {
let ports: Record<string, Record<string, unknown>> = {};
try {
ports = M.ev.evalLogic(lg, values, { logics }).ports;
} catch {
/* 셈이 막혀도 물음은 보냄 */
}
const pill = (p: { box: string; name: string }): unknown => ports[p.box]?.[p.name] ?? null;
const asks = M.detail.queriesOf(lg, pill, M.model.elementPort);
const sig = JSON.stringify(asks.map((a) => a.q));
if (sig !== valueSig) {
valueSig = sig;
void M.detail.fetchValues(M.api.cfValues, lg, pill, M.model.elementPort).then(
(got) => {
if (sig !== valueSig || gone) return;
values = got.values;
left();
refresh(); // 받은 값으로 조건 찾기 값이 바뀌면 한 번 더
},
() => undefined,
);
}
const keys = JSON.stringify(lg.boxes.map((b) => b.input?.logic?.key ?? ""));
if (keys !== logicSig) {
logicSig = keys;
void M.detail.loadLogics(lg, getLogic).then((got) => {
if (keys !== logicSig || gone) return;
logics = got.logics;
left();
});
}
};
const panel = M.panel.mountFlowPanel(host, {
logic: lg,
title: opts.title,
readOnly: opts.readOnly,
logics: getLogic,
onResize: (px) => opts.split.setRight(px),
folded: opts.folded,
onFold: opts.onFold,
onChange: (next) => {
lg = next;
opts.onChange(next);
left();
refresh();
},
});
left();
refresh();
return {
set: (next) => {
lg = next;
panel.set(next);
left();
refresh();
},
redrawLeft: left,
fold: (on) => panel.fold(on),
setExamples: (vals) => {
const next = withExamples(lg, vals);
if (JSON.stringify(next) === JSON.stringify(lg)) return;
lg = next;
panel.set(next);
left();
refresh();
},
destroy: () => {
gone = true;
panel.destroy();
opts.split.setRight(null);
opts.split.right.replaceChildren();
},
};
}
@@ -1,122 +0,0 @@
/* =============================================================================
* M01_MasterData_UI_LogicLab_FlowRows.ts
* 흐름 로직의 왼쪽 「단가산출 상세」 줄 만들기(DOM 없음 · 순수) — 흐름도 출력 상자를 비용 종류별로 묶어
* 줄(이름 · 단위 · 값) · 소계 · 계 · 함수 상자별 결과 값. 값은 DataFlow 계산 결과(`CfEvalResult`)를 읽기만 함.
* 모양은 docs/raw/M01_쉬운단가/흐름도_적용_정함.md 7. 시험 = resources/tester/test_m01_logic_flowview.py
* ========================================================================== */
import type { CfEvalResult } from "../M02_MasterTemplete/DataFlow/M02_DF_Eval";
import type { CfBox, CfCost, CfLogic, CfRound } from "../M02_MasterTemplete/DataFlow/M02_DF_Model";
import { roundBy, showRounded } from "../M02_MasterTemplete/DataFlow/M02_DF_Round";
export type FlowGroupKey = "labor" | "material" | "cost" | "plain";
/** 비용 종류 → 묶음(인력 = 노무비 · 재료 = 재료비 · 경비) · 값(비용 아님)은 따로 */
export const GROUP_OF: Record<CfCost, FlowGroupKey> = {
노무비: "labor",
재료비: "material",
경비: "cost",
값: "plain",
};
export const GROUP_ORDER: FlowGroupKey[] = ["labor", "material", "cost", "plain"];
const COST_OF: Record<Exclude<FlowGroupKey, "plain">, "노무비" | "재료비" | "경비"> = {
labor: "노무비",
material: "재료비",
cost: "경비",
};
export interface FlowLine {
box: string;
name: string;
unit: string;
value: number | string | boolean | null;
/** 보이는 글 — 숫자는 그 출력의 끝수 자리대로 */
text: string;
}
export interface FlowGroup {
key: FlowGroupKey;
lines: FlowLine[];
/** 비용 묶음만 — 합계 끝수를 적용한 소계 */
sub: number | null;
subText: string;
}
export interface FlowFunc {
box: string;
name: string;
unit: string;
value: unknown;
text: string;
}
export interface FlowRows {
groups: FlowGroup[];
/** 합계 끝수(기본 1원 미만 버림)를 적용한 계 */
total: number;
totalText: string;
funcs: FlowFunc[];
errors: string[];
}
const plain = (v: unknown): string =>
v === null || v === undefined || Array.isArray(v)
? "—"
: typeof v === "number"
? v.toLocaleString("ko-KR", { maximumFractionDigits: 6 })
: String(v);
/** 줄 모으기 — round.item = 출력 기본 끝수 · round.total = 합계 기본 끝수 */
export function flowRows(
lg: CfLogic,
ev: CfEvalResult,
round: { item: CfRound; total: CfRound | null },
): FlowRows {
const outs: CfBox[] = lg.boxes.filter((b) => b.kind === "output");
const groups = GROUP_ORDER.map((key): FlowGroup => {
const mine = outs.filter((b) => GROUP_OF[b.output?.cost ?? "값"] === key);
const lines = mine.map((b): FlowLine => {
const v = ev.outputs[b.id] ?? null;
const r = b.output?.round ?? (key === "plain" ? undefined : round.item);
return {
box: b.id,
name: b.name,
unit: b.output?.unit ?? b.unit,
value: v,
text: typeof v === "number" ? showRounded(v, r) : plain(v),
};
});
const raw = key === "plain" ? null : ev.cost[COST_OF[key]];
const sub = raw === null ? null : roundBy(round.total, raw);
return {
key,
lines,
sub,
subText: sub === null ? "" : showRounded(sub, round.total ?? undefined),
};
}).filter((g) => g.lines.length);
const total = roundBy(round.total, ev.cost.계);
const funcs = lg.boxes
.filter((b) => b.kind === "function" && b.func)
.map((b): FlowFunc => {
const out = b.func!.output;
const v = ev.ports[b.id]?.[out.name];
return {
box: b.id,
name: b.name,
unit: out.unit || b.unit,
value: v,
text:
typeof v === "number"
? v.toLocaleString("ko-KR", { maximumFractionDigits: 6 })
: plain(v),
};
});
return {
groups,
total,
totalText: showRounded(total, round.total ?? undefined),
funcs,
errors: ev.errors,
};
}
@@ -1,24 +0,0 @@
.m01flow__group {
margin: 0 0 var(--spacing-12);
}
.m01flow__block {
margin: 0 0 var(--spacing-8);
}
.m01flow__func {
margin: 0 0 var(--spacing-4);
color: var(--color-text-body);
font-weight: 600;
}
.m01flow__branch {
margin: 0 0 0 var(--spacing-16);
color: var(--color-text-muted);
font-size: var(--text-body-sm);
}
.m01flow__branch--now {
color: var(--color-text-body);
font-weight: 600;
}
@@ -1,205 +0,0 @@
/* =============================================================================
* M01_MasterData_UI_LogicLab_FlowView.ts
* 흐름 로직의 왼쪽 「지금 내용」 — ② 단가산출 상세(출력 상자를 비용 종류별 줄 · 소계 · 계) · ③ 텍스트 수식(함수 상자마다
* 식 두 줄 — sub2 `formulaLines`). 기본정보 · 시험 계산은 Detail 이 둠.
* 값은 DataFlow 계산(Eval · 같은 코드)으로 바로 — 오른쪽 패널이 고치면 LogicLab 이 이 함수를 다시 부름.
* 화면은 `buildFlowView(host, lg, values)` 하나. 이 파일은 DataFlow 를 정적으로 가져오므로 LogicLab 은 import() 로 늦게 부름.
* 모양은 docs/raw/M01_쉬운단가/흐름도_적용_정함.md 7 · 8.
* ========================================================================== */
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { el } from "@ui/ui_template_elements";
import {
evalLogic,
type CfEvalOptions,
type CfEvalResult,
type CfValues,
} from "../M02_MasterTemplete/DataFlow/M02_DF_Eval";
import {
cellValuesOf,
formulaLines,
type FormulaLine,
} from "../M02_MasterTemplete/DataFlow/M02_DF_FormulaView";
import type { CfLogic } from "../M02_MasterTemplete/DataFlow/M02_DF_Model";
import {
itemRoundNow,
roundText,
totalRoundNow,
totalRoundOf,
} from "../M02_MasterTemplete/DataFlow/M02_DF_Round";
import {
flowRows,
type FlowFunc,
type FlowGroup,
type FlowGroupKey,
} from "./M01_MasterData_UI_LogicLab_FlowRows";
import "./M01_MasterData_UI_LogicLab_FlowView.css";
import { createText } from "@ui/ui_template_text";
const TEXT = {
Detail: ["단가산출 상세", "Unit-price detail"],
Formula: ["텍스트 수식", "Text formulas"],
Labor: ["인력 · 노무비", "Labor · labor cost"],
Material: ["재료 · 재료비", "Material · material cost"],
Cost: ["경비", "Expense"],
Plain: ["값(비용 아님)", "Values (not costs)"],
HName: ["이름", "Name"],
HUnit: ["단위", "Unit"],
HValue: ["값", "Value"],
Sub: ["소계", "Subtotal"],
Total: ["계", "Total"],
TotalNote: ["합계 끝수", "Total rounding"],
None: ["출력 상자가 없음", "No output box"],
NoFunc: ["함수 상자가 없음", "No function box"],
} as const;
type TextKey = keyof typeof TEXT;
const tf = (key: TextKey): string => TEXT[key][currentLanguageIndex === 1 ? 1 : 0];
const GROUP_TITLE: Record<FlowGroupKey, TextKey> = {
labor: "Labor",
material: "Material",
cost: "Cost",
plain: "Plain",
};
const section = (title: string, body: HTMLElement[]): HTMLElement =>
el("section", {
className: "m01-logic__section m01lab__box",
children: [
el("div", { className: "m01-logic__section-head", children: [el("h3", { text: title })] }),
...body,
],
});
const groupTable = (g: FlowGroup): HTMLElement => {
const sub =
g.sub === null
? []
: [
el("tr", {
className: "m01lab__sub",
children: [
el("td", { text: tf("Sub"), attrs: { colspan: "2" } }),
el("td", { className: "m01-logic__money", text: g.subText }),
],
}),
];
return el("div", {
className: "m01flow__group",
children: [
el("h4", { text: tf(GROUP_TITLE[g.key]) }),
el("div", {
className: "m01-logic__scroll",
children: [
el("table", {
className: "ui-table ui-table--data m01-logic__grid",
children: [
el("thead", {
children: [
el("tr", {
children: [tf("HName"), tf("HUnit"), tf("HValue")].map((h) =>
el("th", { text: h }),
),
}),
],
}),
el("tbody", {
children: [
...g.lines.map((l) =>
el("tr", {
children: [
el("td", { text: l.name }),
el("td", { text: l.unit }),
el("td", { className: "m01-logic__money", text: l.text }),
],
}),
),
...sub,
],
}),
],
}),
],
}),
],
});
};
/** 함수 상자 하나의 글 줄 — 식 보기가 있으면 두 줄(+ 갈래) · 없으면 이름 = 값 */
function funcBlock(f: FlowFunc, lines: FormulaLine[] | null): HTMLElement {
const unit = f.unit && f.unit !== "-" ? ` ${f.unit}` : "";
const head = el("p", { className: "m01flow__func", text: `${f.name} = ${f.text}${unit}` });
if (!lines?.length) return el("div", { className: "m01flow__block", children: [head] });
return el("div", {
className: "m01flow__block",
children: [
head,
...lines.flatMap((l) => [
el("p", { className: "m01lab__expr", text: `${l.name} = ${l.named}` }),
el("p", { className: "m01lab__expr", text: `${l.name} = ${l.valued}` }),
...(l.branches ?? []).map((b) =>
el("p", {
className: `m01flow__branch${b.now ? " m01flow__branch--now" : ""}`,
text: `${b.now ? "▶ " : ""}${b.when} → ${b.then}`,
}),
),
]),
],
});
}
/** 왼쪽 「지금 내용」 — 단가산출 상세 + 텍스트 수식을 `host` 에 그림(`values` = 입력 값 · 다시 부르면 다시 그림) */
export function buildFlowView(
host: HTMLElement,
lg: CfLogic,
values: CfValues,
opts: { logics?: CfEvalOptions["logics"]; result?: CfEvalResult } = {},
): void {
let ev: CfEvalResult;
try {
ev = opts.result ?? evalLogic(lg, values, { logics: opts.logics });
} catch (e) {
const failed = section(tf("Detail"), [
el("p", { className: "ui-text ui-text--muted", text: String(e) }),
]);
failed.dataset.block = "ho";
host.replaceChildren(failed);
return;
}
const totalRound = totalRoundOf(lg, totalRoundNow());
const rows = flowRows(lg, ev, { item: itemRoundNow(), total: totalRound });
const detail = section(tf("Detail"), [
...(rows.groups.length
? rows.groups.map(groupTable)
: [el("p", { className: "ui-text ui-text--muted", text: tf("None") })]),
el("p", {
className: "m01lab__total",
attrs: { "data-sum": String(rows.total) },
text: `${tf("Total")} ${rows.totalText}`,
}),
el("p", {
className: "ui-text ui-text--muted",
text: `${tf("TotalNote")}: ${roundText(totalRound)}`,
}),
...rows.errors.slice(0, 3).map((t) => createText(t, "error", 14)),
]);
/** 함수 상자마다 식 두 줄(sub2 식 보기 · 받는 알약 값을 칸에 넣고 셈) — 못 그리면 이름 = 값만 */
const linesOf = (id: string): FormulaLine[] | null => {
const box = lg.boxes.find((b) => b.id === id);
if (!box?.func) return null;
try {
return formulaLines(box, cellValuesOf(box, ev.arrived?.[id]));
} catch {
return null;
}
};
const bodies = rows.funcs.map((f) => funcBlock(f, linesOf(f.box)));
const text = section(
tf("Formula"),
bodies.length ? bodies : [el("p", { className: "ui-text ui-text--muted", text: tf("NoFunc") })],
);
// 보기 고르기 이름표(`LogicLab_View.ts`) — 고른 덩이만 보임
detail.dataset.block = "ho";
text.dataset.block = "formula";
host.replaceChildren(detail, text);
}
@@ -48,7 +48,6 @@ const TEXT = {
Close: ["식 닫기", "Close formula"],
Back: ["원래로", "Revert"],
Wait: ["[계산] 을 누르면 값이 채워짐", "Press Calculate to fill values"],
Flow: ["흐름 로직 — 시트로 못 봄", "Flow logic — no sheet"],
Address: ["번지", "Address"],
} as const satisfies Record<string, readonly [string, string]>;
const t = (k: keyof typeof TEXT): string => TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0];
@@ -267,7 +266,6 @@ export function createLabSheet(opt: LabSheetOptions) {
function paint(d: LabSheetDraw): void {
last = d;
drawStatus(d);
if (d.row.흐름) return void body.replaceChildren(createText(t("Flow"), "muted"));
const items = labItems({ ...d, names });
const active = document.activeElement as HTMLElement | null;
const keep = active && body.contains(active) ? active.dataset.sheetCell : "";
@@ -1,19 +1,3 @@
/* 로직 상세 오른쪽 두 패널(흐름도 · 원문) — 공용 오른쪽 접는 패널 안 */
.m01lab-side__slot {
min-width: 0;
}
/* 접기 · 폭은 공용 패널 몫 — 흐름도 패널 자체 접기 단추 · 끌기 경계 · 테두리는 뺌 */
.m01lab-side .m02-dfp__toggle,
.m01lab-side .m02-dfp__grip {
display: none;
}
.m01lab-side .m02-dfp {
border-left: 0;
background: none;
}
/* 원문 꼴 한 벌(M01 · M02 같음) — 둘레 글자 크기(M01 14)를 안 따르고 본문 크기 */
.m01lab-src {
font-size: var(--text-body);
@@ -23,6 +7,22 @@
min-width: 0;
}
/* 원문 머리 — 제목 · [원문 다운로드] */
.m01lab-src__top {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--spacing-8);
}
.m01lab-src__title {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.m01lab-src__body {
display: flex;
flex-direction: column;
@@ -1,77 +1,45 @@
/* =============================================================================
* M01_MasterData_UI_LogicLab_Side.ts
* 로직 상세 — 왼쪽 지금 내용 · 오른쪽 접는 패널 둘 「흐름도」(M02_DF_Panel) · 「원문」(품셈 본문 절)
* 공용 오른쪽 접는 패널(ui_template_side_panels) — 하나만 열림 · 모두 접힘 됨 · 폭 공유 · 접힌 패널 = 세로 글 띠
* 열린 패널 · 폭은 취향 키(`m01-logic-side`) · 원문 = 공용 원문 보기(`LogicLab_Source`)
* 로직 상세 — 왼쪽 지금 내용 · 오른쪽 접는 패널 「원문」(품셈 본문 절)
* 공용 오른쪽 접는 패널(ui_template_side_panels) — 접힘 됨 · 접힌 패널 = 세로 글 띠
* 열림 · 폭은 취향 키(`m01-logic-side`) · 원문 = 공용 원문 보기(`LogicLab_Source`)
* ========================================================================== */
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { createSidePanels } from "@ui/ui_template_side_panels";
import { el } from "@ui/ui_template_elements";
import type { SplitHandle } from "./M01_MasterData_UI_LogicLab_Flow";
import { createSourceView } from "./M01_MasterData_UI_LogicLab_Source";
import "../M02_MasterTemplete/DataFlow/M02_DF_Panel.css";
import "./M01_MasterData_UI_LogicLab_Side.css";
const TEXT = {
Flow: ["흐름도", "Flow"],
Source: ["원문", "Source"],
} as const;
const ts = (key: keyof typeof TEXT): string => TEXT[key][currentLanguageIndex === 1 ? 1 : 0];
export type PanelPick = "flow" | "source" | "none";
export interface LogicSide {
/** [왼쪽 지금 내용, 오른쪽 패널] 칸 */
root: HTMLElement;
/** 흐름도 패널에 넘길 칸 — right = 흐름도 자리 · 폭은 오른쪽 패널 몫(setRight 안 씀) */
flowSplit: SplitHandle;
flowFolded(): boolean;
pick(next: PanelPick): void;
/** 흐름도 패널이 뜨면 접기 함수 · 내리면 null */
bindFlow(fold: ((on: boolean) => void) | null): void;
/** 연 로직의 출처 글 — 원문 패널이 열려 있으면 찾음 */
showSource(src: string): void;
}
export function buildLogicSide(left: HTMLElement): LogicSide {
const flowSlot = el("div", { className: "m01lab-side__slot" });
const source = createSourceView();
const sp = createSidePanels({
prefKey: "m01-logic-side",
defaultWidth: 520,
minWidth: 320,
panels: [
{ id: "flow", title: ts("Flow"), body: flowSlot },
{ id: "source", title: ts("Source"), body: source.root },
],
onChange: () => {
flowFold?.(sp.current() !== "flow"); // 다시 펴면 흐름도 판을 폭에 맞춤
load();
},
});
const root = el("div", {
className: "m01lab-side ui-side-panels-row",
children: [left, sp.root],
});
let flowFold: ((on: boolean) => void) | null = null;
let want = "";
/** 원문 패널이 열려 있을 때만 찾음 */
const load = (): void => {
if (sp.current() === "source") source.show(want);
};
const sp = createSidePanels({
prefKey: "m01-logic-side",
defaultWidth: 520,
minWidth: 320,
panels: [{ id: "source", title: ts("Source"), body: source.root }],
onChange: load,
});
return {
root,
flowSplit: { right: flowSlot, setRight: () => undefined },
flowFolded: () => sp.current() !== "flow",
pick: (next) => sp.open(next === "none" ? null : next),
bindFlow: (f) => {
flowFold = f;
},
root: el("div", { className: "m01lab-side ui-side-panels-row", children: [left, sp.root] }),
showSource: (src) => {
want = src;
load();
@@ -8,7 +8,6 @@
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { el } from "@ui/ui_template_elements";
import { API_BASE_URL } from "@config/config_frontend";
import "../M02_MasterTemplete/DataFlow/M02_DF_Panel.css";
import "./M01_MasterData_UI_LogicLab_Side.css";
const TEXT = {
@@ -85,7 +84,7 @@ export interface SourceViewOptions {
export function createSourceView(opts: SourceViewOptions = {}): SourceView {
const body = el("div", { className: "m01lab-src__body" });
const title = el("strong", { className: "m02-dfp__title" });
const title = el("strong", { className: "m01lab-src__title" });
const download = opts.pdf
? (el("a", {
className: "ui-btn ui-btn--ghost ui-btn--sm",
@@ -149,7 +148,7 @@ export function createSourceView(opts: SourceViewOptions = {}): SourceView {
};
const head = download
? el("div", { className: "m02-dfp__head", children: [title, download] })
? el("div", { className: "m01lab-src__top", children: [title, download] })
: title;
return { root: el("div", { className: "m01lab-src", children: [head, body] }), show };
}
@@ -219,11 +219,6 @@
margin-bottom: var(--spacing-8);
}
/* 상세 머리 「지금 입력」 한 줄 */
.m01lab__now {
margin: 0;
}
/* 재료 모달(711-2) — 위 물가 넷 가로 · 시험 계산이 쓴 값만 굵게 · 후보 목록은 모달 안 스크롤 */
.m01lab__slots .ui-stat:not(.is-used) .ui-stat__value {
font-weight: 400;
@@ -7,9 +7,10 @@ import { currentLanguageIndex } from "@ui/ui_template_locale";
const TEXT = {
Info_Title: ["기본정보", "Basic info"],
New: ["신규 로직", "New logic"],
Info_Base: [
"기본 로직 — 식은 오른쪽 흐름도에서 고침(시스템 관리자) · 여기서는 비고만",
"Base logic — edit formulas in the flow diagram on the right (system admin) · only the note here",
"기본 로직 — 식은 시트에서 고침(시스템 관리자) · 여기서는 비고만",
"Base logic — edit formulas in the sheet (system admin) · only the note here",
],
Info_ReadOnly: [
"기본 로직 — 보기만 · 비고만 고칠 수 있음",
@@ -36,8 +37,6 @@ const TEXT = {
View_Sheet: ["시트", "Sheet"],
Edit_SheetHint: ["식은 「시트」 보기에서 고침", "Edit formulas in the Sheet view"],
View_Label: ["보기", "View"],
Now_Inputs: ["지금 입력", "Current inputs"],
Now_None: ["없음", "none"],
G_labor: ["인력", "Labor"],
G_material: ["재료", "Material"],
G_cost: ["경비", "Expense"],
@@ -7,7 +7,6 @@
* 기억 = 이 화면 안에서만(로직을 바꿔도 유지 · 새로 들어오면 시트)
* ========================================================================== */
import { el } from "@ui/ui_template_elements";
import { createSegment } from "@ui/ui_template_segment";
import { createToolbar } from "@ui/ui_template_toolbar";
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
@@ -38,14 +37,3 @@ export function createViewPicker(host: HTMLElement): { root: HTMLElement } {
});
return { root: createToolbar({ items: [segment.root, "divider"], label: tl("View_Label") }) };
}
/** 상세 머리 「지금 입력」 한 줄(읽기만) — 숨은 시험 계산 칸에 든 값 */
export function nowInputs(values: Record<string, string>): HTMLElement {
const filled = Object.entries(values).filter(([, v]) => String(v ?? "").trim() !== "");
const text = filled.length ? filled.map(([k, v]) => `${k} ${v}`).join(" · ") : tl("Now_None");
return el("p", {
className: "ui-text ui-text--muted m01lab__now",
attrs: { "data-now": "" },
text: `${tl("Now_Inputs")} · ${text}`,
});
}