From 5ef189ca4517da5c2228e109d035162c2fe931b6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 20:33:13 +0900 Subject: [PATCH 01/18] =?UTF-8?q?fix(M02-=ED=9D=90=EB=A6=84=EB=8F=84):=20?= =?UTF-8?q?=EC=95=8C=EC=95=BD=20=ED=8C=90=20=EC=88=AB=EC=9E=90=EC=97=90=20?= =?UTF-8?q?=EC=B2=9C=20=EB=8B=A8=EC=9C=84=20=EC=89=BC=ED=91=9C=20=C2=B7=20?= =?UTF-8?q?=EC=98=A8=20=EA=B8=B8=20=EC=A4=84=EC=97=90=20=EC=84=A0=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=EA=B8=80(=EB=B3=B4=EB=82=B8=20=EC=95=8C?= =?UTF-8?q?=EC=95=BD=20=E2=86=92=20(=EA=B7=9C=EC=B9=99)=20=E2=86=92=20?= =?UTF-8?q?=EB=B0=9B=EB=8A=94=20=EC=95=8C=EC=95=BD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01D2nUMUMTsqm93PbQtipo6c --- M02_MasterTemplete/DataFlow/M02_DF_Detail.ts | 6 +++- .../DataFlow/M02_DF_LinkPanel.ts | 9 +++-- .../DataFlow/M02_DF_PillPanel.ts | 34 ++++++++++++------- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Detail.ts b/M02_MasterTemplete/DataFlow/M02_DF_Detail.ts index 30b1312f8..2f4cf9fc2 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Detail.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Detail.ts @@ -69,11 +69,15 @@ export function evalSafe(lg: CfLogic, o: DetailOptions = {}): CfEvalResult | nul export const valueOf = (ev: CfEvalResult | null, p: CfPort): CfValue | undefined => ev?.ports[p.box]?.[p.name]; +/** 숫자 글 — 천 단위 쉼표 · 군더더기 소수는 뗌 */ +export const commas = (n: number): string => + Number(n.toPrecision(12)).toLocaleString("ko-KR", { maximumFractionDigits: 6 }); + /** 화면에 보일 값 글 — 표는 줄 수 */ export function showValue(v: CfValue | undefined): string { if (v === undefined || v === null) return td("Empty"); if (Array.isArray(v)) return td("Table", { n: Math.max(0, v.length - 1) }); - return typeof v === "number" ? String(Number(v.toPrecision(12))) : String(v); + return typeof v === "number" ? commas(v) : String(v); } export function unitOf(lg: CfLogic, p: CfPort): string { diff --git a/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts b/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts index 1d7c4043c..5b4be1dc6 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts @@ -19,6 +19,7 @@ import { } from "./M02_DF_Model"; import { GATE_OPS, + commas, SCALE_OPS, evalSafe, labelOf, @@ -127,6 +128,7 @@ function select( return s; } +/** 숫자 칸 — 보일 때만 천 단위 쉼표 · 고치는 동안은 쉼표 없이 */ function numberBox( ctx: Ctx, value: number | string, @@ -135,10 +137,13 @@ function numberBox( ): HTMLElement { const i = el("input", { className: "ui-input ui-input--sm m02-dflink-num", - attrs: { type: "text", "aria-label": aria }, + attrs: { type: "text", inputmode: "decimal", "aria-label": aria }, }); - i.value = String(value); + const shown = (): string => (typeof value === "number" ? commas(value) : String(value)); + i.value = shown(); i.readOnly = ctx.readOnly; + i.addEventListener("focus", () => (i.value = String(value))); + i.addEventListener("blur", () => (i.value = shown())); i.addEventListener("change", () => { const t = i.value.trim().replace(/,/g, ""); onCommit(t !== "" && Number.isFinite(Number(t)) ? Number(t) : t); diff --git a/M02_MasterTemplete/DataFlow/M02_DF_PillPanel.ts b/M02_MasterTemplete/DataFlow/M02_DF_PillPanel.ts index dcd3a4aa3..deff36790 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_PillPanel.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_PillPanel.ts @@ -47,6 +47,7 @@ const line = (label: string, ...value: (HTMLElement | string)[]): HTMLElement => children: [el("span", { className: "m02-dflink-label", text: label }), ...value], }); +/** 한 줄 = 선 하나면 「보낸 알약 → (규칙 글) → 받는 알약」 · 함수 안 걸음이면 알약 하나 */ function steps( lg: CfLogic, list: CfStep[], @@ -55,21 +56,30 @@ function steps( onBox?: (id: string) => void, ): HTMLElement { if (!list.length) return el("p", { className: "m02-dflink-note", text: empty }); + const pill = (box: string, name: string): HTMLElement | string => + onBox + ? createButton({ + label: portText(lg, { box, name }), + variant: "ghost", + size: "sm", + onClick: () => onBox(box), + }) + : portText(lg, { box, name }); return el("ol", { className: "m02-dfpill-list", children: list.map((s) => { - const parts: (HTMLElement | string)[] = [ - arrow, - onBox - ? createButton({ - label: portText(lg, { box: s.box, name: s.port }), - variant: "ghost", - size: "sm", - onClick: () => onBox(s.box), - }) - : portText(lg, { box: s.box, name: s.port }), - ]; - if (s.rule) parts.push(el("span", { className: "m02-dfpill-rule", text: labelOf(s.rule) })); + const link = s.link ? lg.links.find((l) => l.id === s.link) : undefined; + const parts: (HTMLElement | string)[] = link + ? [ + arrow, + pill(link.from.box, link.from.name), + "→", + ...(s.rule + ? [el("span", { className: "m02-dfpill-rule", text: `(${labelOf(s.rule)})` }), "→"] + : []), + pill(link.to.box, link.to.name), + ] + : [arrow, pill(s.box, s.port)]; return el("li", { className: "m02-dfpill-step", children: parts }); }), }); From b5462afa942927a1086ec5501cdafe933f7ccc03 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 20:33:28 +0900 Subject: [PATCH 02/18] =?UTF-8?q?fix(m02-df):=20=ED=91=9C=EC=A4=80=20?= =?UTF-8?q?=ED=91=9C=20=ED=8C=90=20=EC=A0=9C=EB=AA=A9=EC=9D=84=20=EA=B3=A0?= =?UTF-8?q?=EB=A5=BC=20=EC=88=98=20=EC=9E=88=EA=B2=8C=20=E2=80=94=20render?= =?UTF-8?q?Standards=20opts.title(=EA=B8=B0=EB=B3=B8=20true=20=C2=B7=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=EC=9D=80=20false)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BU9gbNE3aWFUvE61pS2WGt --- .../DataFlow/M02_DF_Standards.css | 3 ++- .../DataFlow/M02_DF_Standards.ts | 20 ++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Standards.css b/M02_MasterTemplete/DataFlow/M02_DF_Standards.css index 066d8dc0f..ca3351b9c 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Standards.css +++ b/M02_MasterTemplete/DataFlow/M02_DF_Standards.css @@ -11,7 +11,8 @@ gap: var(--spacing-8, 8px); } -.m02-df-std__head h3 { +/* 첫 칸(제목 · 제목이 없으면 상태 글)이 단추를 오른쪽으로 밂 */ +.m02-df-std__head > :first-child { margin: 0 auto 0 0; } diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Standards.ts b/M02_MasterTemplete/DataFlow/M02_DF_Standards.ts index 8d4bd646b..969a92a01 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Standards.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Standards.ts @@ -4,7 +4,7 @@ * 처음 값 = `_standard/단위표준.json` · `금액단위표준.json`(산림품셈 1-2-2 · 원문 흠 그대로 — 사용자가 고침) * 서버 = `M02_DF_Api` cfStandards · cfStandardsSave · cfStandardsReset(`/api/m02/df/standards`) → {unit, money, saved} · * 고친 것은 `_data`(마스터에 안 씀) - * `renderStandards(host, on)` = 시험판 안 「표준 표」 판(두 표 보기 · 칸 고치기 · 저장 · 처음 값으로) + * `renderStandards(host, on, { title })` = 시험판 안 「표준 표」 판(두 표 보기 · 칸 고치기 · 저장 · 처음 값으로) * `standardsList()` = 단위 고르기 · 끝수 제안이 부르는 목록(동기 · 처음엔 번들 값 · 판이 서버 값을 받으면 그것) * 이 폴더째 지워도 됨. * ========================================================================== */ @@ -185,8 +185,17 @@ function parseCell(key: string, text: string): string | number | null | undefine const show = (v: unknown): string => v === null || v === undefined ? "" : typeof v === "number" ? v.toLocaleString() : String(v); +export interface StandardsOpts { + /** 판 제목 「표준 표」 — 모달처럼 겉에 제목이 있으면 false(기본 true) */ + title?: boolean; +} + /** 시험판 안 「표준 표」 판 */ -export function renderStandards(host: HTMLElement, on: StandardsOn = {}): void { +export function renderStandards( + host: HTMLElement, + on: StandardsOn = {}, + opts: StandardsOpts = {}, +): void { let work = clone(current); let dirty = false; const state = el("span", { className: "m02-df-std__state" }); @@ -334,7 +343,12 @@ export function renderStandards(host: HTMLElement, on: StandardsOn = {}): void { children: [ el("div", { className: "m02-df-std__head", - children: [el("h3", { text: tx("Title") }), state, reset, save], + children: [ + ...(opts.title === false ? [] : [el("h3", { text: tx("Title") })]), + state, + reset, + save, + ], }), el("p", { className: "m02-df-std__hint", text: `${tx("Source")} · ${tx("Hint")}` }), body, From 5bae4c9587fb59c1b109700eedfad1eb1a2b03f4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 20:33:35 +0900 Subject: [PATCH 03/18] =?UTF-8?q?fix(m02-df):=20=EC=B6=9C=EB=A0=A5=20?= =?UTF-8?q?=EC=83=81=EC=9E=90=EB=B3=84=20=EA=B0=92=EC=9D=80=20=EA=B7=B8=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=EB=81=9D=EC=88=98=20=EC=9E=90=EB=A6=AC?= =?UTF-8?q?=EB=8C=80=EB=A1=9C=20=C2=B7=20=EA=B2=80=EC=A6=9D=20=EC=B9=B8=20?= =?UTF-8?q?=EA=B8=80=20=EB=AA=85=EC=82=AC=ED=98=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr --- .../DataFlow/M02_DF_OutputPanel.ts | 13 +++++-------- M02_MasterTemplete/DataFlow/M02_DF_Round.ts | 14 ++++++++++++++ M02_MasterTemplete/DataFlow/M02_DF_Summary.ts | 10 ++++++++-- M02_MasterTemplete/DataFlow/M02_DF_Verify.ts | 18 +++++++++--------- 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_OutputPanel.ts b/M02_MasterTemplete/DataFlow/M02_DF_OutputPanel.ts index bef75f6cb..e9c050c5b 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_OutputPanel.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_OutputPanel.ts @@ -27,12 +27,12 @@ const TEXT = { From: ["어디서", "From"], Value: ["값", "Value"], NoLink: [ - "아직 이어진 선이 없어요 — 입력이나 함수 상자에서 이 상자로 선을 이어 주세요", + "아직 이어진 선이 없음 — 입력이나 함수 상자에서 이 상자로 선을 이어야 함", "Nothing is linked yet — draw a line from an input or function box", ], Table: ["표", "Table"], - Calc: ["계산 중이 아니에요", "Not calculated"], - Cant: ["지금은 바꿀 수 없어요", "Cannot change this now"], + Calc: ["계산 중이 아님", "Not calculated"], + Cant: ["지금은 바꿀 수 없음", "Cannot change this now"], Round: ["끝수", "Rounding"], RoundPlaces: ["자리", "Places"], RoundMethod: ["방법", "Method"], @@ -43,10 +43,7 @@ const TEXT = { Apply: ["적용", "Apply"], RoundReset: ["기본으로", "Back to default"], RoundUser: ["고른 끝수", "Chosen rounding"], - PlainHint: [ - "원가 합계에 안 넣고 값으로만 보여요", - "Shown as a value, not added to the cost total", - ], + PlainHint: ["원가 합계에 안 넣고 값으로만 보임", "Shown as a value, not added to the cost total"], } as const; type TextKey = keyof typeof TEXT; @@ -128,7 +125,7 @@ export function renderOutputPanel( el("dt", { text: to("Value") }), el("dd", { className: "m02-dfout__num", - text: `${showValue(value, cost !== "값")}${unit && unit !== "-" ? ` ${unit}` : ""}`, + text: `${showValue(value)}${unit && unit !== "-" ? ` ${unit}` : ""}`, }), ], }), diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Round.ts b/M02_MasterTemplete/DataFlow/M02_DF_Round.ts index 3d1a54d90..5341b5a83 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Round.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Round.ts @@ -88,3 +88,17 @@ export function roundText(r: CfRound): string { const step = r.places > 0 ? Number((10 ** -r.places).toFixed(r.places)) : 1; return `${step}원 미만 ${r.method}`; } + +/** 출력 기본 끝수(표를 읽어 오기 전에는 금액란 0.1원 미만 버림) — 화면이 바로 쓰는 값 */ +let itemNow: CfRound = { places: 1, method: "버림" }; +void defaultItemRound().then((r) => (itemNow = r)); +export const itemRoundNow = (): CfRound => itemNow; + +/** 끝수대로 자른 값을 그 자리 수만큼 보임(3,216.6) — 끝수가 없으면 소수 여섯째 자리까지 */ +export function showRounded(v: number, r?: CfRound): string { + if (!r) return v.toLocaleString("ko-KR", { maximumFractionDigits: 6 }); + return roundBy(r, v).toLocaleString("ko-KR", { + minimumFractionDigits: 0, + maximumFractionDigits: Math.max(0, r.places), + }); +} diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Summary.ts b/M02_MasterTemplete/DataFlow/M02_DF_Summary.ts index 19a2754ac..a4686af39 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Summary.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Summary.ts @@ -12,7 +12,7 @@ import { formatMoney } from "../../M01_MasterData/M01_MasterData_UI_Logic_Money" import type { CfEvalOptions, CfEvalResult, CfValues } from "./M02_DF_Eval"; import type { CfBox, CfLogic, CfRound } from "./M02_DF_Model"; import { showValue } from "./M02_DF_OutputPanel"; -import { defaultTotalRound, roundBy, roundText } from "./M02_DF_Round"; +import { defaultTotalRound, itemRoundNow, roundBy, roundText, showRounded } from "./M02_DF_Round"; import { renderVerify, type CfSel } from "./M02_DF_Verify"; import "./M02_DF_Summary.css"; @@ -49,6 +49,12 @@ const costText = (c: string): string => (c === "값" ? ts("Plain3") : c); let totalRound: CfRound = { places: 0, method: "버림" }; void defaultTotalRound().then((r) => (totalRound = r)); +/** 출력 상자 값 — 그 출력의 끝수 자리대로(끝수를 안 정했으면 비용은 표준 기본 · 값은 그대로) */ +const shown = (b: CfBox, v: unknown, isCost: boolean): string => + typeof v === "number" + ? showRounded(v, b.output?.round ?? (isCost ? itemRoundNow() : undefined)) + : showValue(v as never); + /** 원가 합계 칸 — result = 화면 계산 결과 · on 이 있으면 예시값을 바꿀 수 있음 · opts.values = 받은 입력 값(검사가 씀) */ export function renderSummary( host: HTMLElement, @@ -89,7 +95,7 @@ export function renderSummary( ...(isCost ? [el("td", { text: costText(b.output?.cost ?? "") })] : []), el("td", { className: "m02-dfsum__num", - text: showValue(result.outputs[b.id], isCost), + text: shown(b, result.outputs[b.id], isCost), }), el("td", { text: b.output?.unit ?? b.unit }), ], diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Verify.ts b/M02_MasterTemplete/DataFlow/M02_DF_Verify.ts index c02658d53..9c1e422bd 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Verify.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Verify.ts @@ -23,9 +23,9 @@ import "./M02_DF_Verify.css"; const TEXT = { Breaks: ["끊긴 곳", "Broken spots"], - BreaksHint: ["누르면 그 상자 · 선을 골라요", "Click to select the box or line"], + BreaksHint: ["누르면 그 상자 · 선을 고름", "Click to select the box or line"], Design: ["설계 때 정할 것", "To decide at design time"], - DesignHint: ["여기서 바꾸면 해당 상자도 바뀌어요", "Changing here changes the box too"], + DesignHint: ["여기서 바꾸면 그 상자도 바뀜", "Changing here changes the box too"], Vars: ["변수 예시값", "Variable samples"], Slots: ["재료 선택 자리", "Material choice"], Rules: ["재료 조건 자리", "Material by condition"], @@ -37,23 +37,23 @@ const TEXT = { Next: ["다음", "Next"], Search: ["재료 이름 · 규격", "Material name or size"], Page: ["쪽 {p} / {n} · 모두 {t}", "Page {p} / {n} · {t} in all"], - NoHit: ["찾은 재료가 없어요", "No material found"], - NoDefault: ["아직 고르지 않았어요", "Nothing chosen yet"], + NoHit: ["찾은 재료 없음", "No material found"], + NoDefault: ["아직 고르지 않음", "Nothing chosen yet"], Price: ["단가", "Unit price"], Won: ["원", "KRW"], Loading: ["찾는 중", "Searching"], - NeedValue: ["조건 값이 아직 없어요", "The condition value is not there yet"], + NeedValue: ["조건 값이 아직 없음", "The condition value is not there yet"], Caught: ["잡힌 품목", "Matched item"], Zero: ["맞는 재료 없음(0)", "No matching material (0)"], Many: ["여럿 맞음({n}) — 첫째를 씀", "{n} match — the first is used"], Cond: ["{field}에 「{v}」 {op}", "{field} {op} “{v}”"], - Pick: ["고르세요", "Choose"], - Bad: ["숫자를 넣어 주세요", "Enter a number"], + Pick: ["선택", "Choose"], + Bad: ["숫자를 넣어야 함", "Enter a number"], Range: ["{a} ∼ {b} 사이", "between {a} and {b}"], - Cant: ["지금은 바꿀 수 없어요", "Cannot change this now"], + Cant: ["지금은 바꿀 수 없음", "Cannot change this now"], OwnSample: ["비우면 그 로직 예시값", "Empty = the logic's own sample"], OtherLogic: [ - "이어 주지 않아서 그 로직의 예시값을 써요", + "이어 주지 않아 그 로직의 예시값을 씀", "Not linked — the logic's own sample is used", ], } as const; From 078e168f7d8d3fe68eeaed342a650468713b6c3f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 20:35:24 +0900 Subject: [PATCH 04/18] =?UTF-8?q?feat(M02):=20=ED=9D=90=EB=A6=84=EB=8F=84?= =?UTF-8?q?=20=EB=8B=A4=EC=9D=B4=EC=96=B4=EA=B7=B8=EB=9E=A8=20=E2=80=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8C=90=20=EC=84=A0(=EA=B7=9C=EC=B9=99=EC=9D=98=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EC=95=8C=EC=95=BD=20=E2=86=92=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=EC=9E=90=EB=A6=AC)=20=C2=B7=20=EB=B9=88=20?= =?UTF-8?q?=ED=8C=90=20=EC=95=88=EB=82=B4=20=EC=84=B8=20=EC=A4=84=20=C2=B7?= =?UTF-8?q?=20=EC=89=BC=20=EA=B0=88=EB=9E=98(=ED=9A=8C=EC=83=89=20?= =?UTF-8?q?=EC=A0=90=EC=84=A0=20=C2=B7=20=EB=B0=9B=EB=8A=94=20=EC=83=81?= =?UTF-8?q?=EC=9E=90=20=ED=9D=90=EB=A6=AC=EA=B2=8C=20=C2=B7=20=E3=80=8C!?= =?UTF-8?q?=E3=80=8D=20=EB=8A=94=20=EB=AC=B8=EC=A0=9C=EC=97=90=EB=A7=8C)(P?= =?UTF-8?q?LAN=2035-9=20=EB=B8=8C=EB=A0=88=EC=9D=B8=20=EA=B2=80=EC=A6=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Model ruleSources · elementPort 로 기댐 선(표 조건 · 조건 · 재료 「규격에 포함」) · 흐름 길에도 칠함 · 누르면 그 규칙 자리(선 · 재료 상자) 고름 - breaks level(쉼 · 문제) · 없으면 「통과 안 됨」 = 쉼 - 끌기 손잡이를 Parts 로(Diagram 681줄 · Parts 320줄) - 시험 test_m02_df_diagram_parts.py 4 통과 · DataFlow 전부 검사 초록 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z --- .../DataFlow/M02_DF_Diagram.css | 88 +++++++ M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts | 157 +++++++----- .../DataFlow/M02_DF_Diagram_Parts.ts | 89 ++++++- resources/tester/test_m02_df_diagram_parts.py | 238 ++++++++++++++++++ 4 files changed, 513 insertions(+), 59 deletions(-) create mode 100644 resources/tester/test_m02_df_diagram_parts.py diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.css b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.css index 9381a6c08..74e5bed78 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.css +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.css @@ -410,3 +410,91 @@ font-size: 11px; font-weight: 700; } + +/* 기댐 선 — 규칙이 기대는 알약 → 그 규칙이 있는 곳 · 흐린 점선 + 화살 */ +.m02-dfdg__dep { + pointer-events: auto; + cursor: pointer; +} + +.m02-dfdg__depwire { + fill: none; + stroke: color-mix(in srgb, var(--color-text-muted) 70%, transparent); + stroke-width: 1.5; + stroke-dasharray: 3 4; +} + +.m02-dfdg__arrow { + fill: var(--color-text-muted); +} + +.m02-dfdg__deptext rect { + fill: var(--color-canvas); + stroke: none; + opacity: 0.85; +} + +.m02-dfdg__deptext text { + fill: var(--color-text-muted); + font-size: 10px; +} + +.m02-dfdg__dep.is-strong .m02-dfdg__depwire { + stroke: var(--color-accent); + stroke-width: 2.5; +} + +.m02-dfdg__dep.is-back .m02-dfdg__depwire { + stroke: var(--dfdg-back); + stroke-width: 2.5; +} + +.m02-dfdg__dep.is-fwd .m02-dfdg__depwire { + stroke: var(--dfdg-fwd); + stroke-width: 2.5; +} + +.m02-dfdg__dep.is-dim { + opacity: 0.25; +} + +/* 쉼 — 조건이 안 맞아 이번엔 안 지나감(문제 아님) */ +.m02-dfdg__link.is-rest .m02-dfdg__wire { + stroke: var(--color-text-muted); + stroke-width: 1.5; + stroke-dasharray: 2 5; +} + +.m02-dfdg__restmark rect { + fill: var(--color-surface); + stroke: var(--color-text-muted); + stroke-dasharray: 2 2; +} + +.m02-dfdg__restmark text { + fill: var(--color-text-muted); + font-size: 10px; +} + +.m02-dfdg__box.is-rest { + opacity: 0.5; +} + +/* 빈 판 안내 — 가운데 세 줄 */ +.m02-dfdg__empty { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + margin: 0; + padding: var(--spacing-16) var(--spacing-24); + list-style: none; + border: 1px dashed var(--color-border); + border-radius: var(--radius-lg); + background: var(--color-surface); + color: var(--color-text-secondary); + display: flex; + flex-direction: column; + gap: var(--spacing-8); + pointer-events: none; +} diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts index a6ccd0742..f67ab35cd 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts @@ -14,6 +14,10 @@ import { clampZoom, fitView, KIND_COLOR, + depInTrace, + follow, + depLines, + isRest, linkLabel, midOf, pillsOf, @@ -76,7 +80,15 @@ export function renderDiagram( states.set(host, st); const values = opts.values ?? {}; const labels = opts.labels ?? {}; - const breaks = opts.breaks ?? []; + const allBreaks = opts.breaks ?? []; + /** 「!」 는 문제에만 · 쉼(조건이 안 맞아 이번엔 안 지나감)은 회색 점선 선 + 받는 상자 흐리게 */ + const breaks = allBreaks.filter((k) => !isRest(k)); + const rests = allBreaks.filter(isRest); + const restLinks = new Set(rests.map((k) => k.link).filter(Boolean)); + const restBoxes = new Set([ + ...rests.filter((k) => !k.link).map((k) => k.box), + ...lg.links.filter((l) => restLinks.has(l.id)).map((l) => l.to.box), + ]); const boxName = (id: string): string => lg.boxes.find((b) => b.id === id)?.name ?? ""; const redraw = (): void => renderDiagram(host, lg, on, { ...opts, selected: st.selected, selectedPill: st.pill }); @@ -210,7 +222,8 @@ export function renderDiagram( const node = el("div", { className: `m02-dfdg__box is-${b.kind}${st.selected === b.id ? " is-selected" : ""}` + - (st.selected === b.id ? "" : markOf((s) => s.some((x) => x.box === b.id))), + (st.selected === b.id ? "" : markOf((s) => s.some((x) => x.box === b.id))) + + (restBoxes.has(b.id) ? " is-rest" : ""), attrs: { "data-box": b.id, tabindex: "0", @@ -400,7 +413,7 @@ export function renderDiagram( }), ], }), - lg.boxes.length ? view : el("p", { className: "m02-dfdg__hint", text: tt("Empty") }), + view, ], }); root.addEventListener("keydown", (ev) => { @@ -438,19 +451,27 @@ export function renderDiagram( ); view.addEventListener("auxclick", (ev) => ev.button === 1 && ev.preventDefault()); view.addEventListener("mousedown", (ev) => ev.button === 1 && ev.preventDefault()); + if (!lg.boxes.length) + view.append( + el("ol", { + className: "m02-dfdg__empty", + children: (["Empty1", "Empty2", "Empty3"] as const).map((k) => el("li", { text: tt(k) })), + }), + ); host.replaceChildren(root); applyView(); - /* 선 — 알약 자리를 재서 세상 좌표로 · 가운데 규칙 이름표 · 끊긴 곳 */ + /* 선 — 알약 자리를 재서 세상 좌표로 · 가운데 규칙 이름표 · 끊긴 곳 · 쉼 · 기댐 선 */ function drawLines(): void { svg.replaceChildren(); - const at = (end: CfPort, dir: "in" | "out"): { x: number; y: number } | null => { + const at = (end: CfPort, dir: "in" | "out", side: "near" | "far" = "near") => { const pill = world.querySelector( `.m02-dfdg__pill[data-box="${CSS.escape(end.box)}"][data-name="${CSS.escape(end.name)}"][data-dir="${dir}"]`, ); if (!pill) return null; const r = pill.getBoundingClientRect(); - return toWorld(dir === "out" ? r.right : r.left, r.top + r.height / 2); + const right = (dir === "out") === (side === "near"); + return toWorld(right ? r.right : r.left, r.top + r.height / 2); }; const add = ( parent: Element, @@ -462,6 +483,33 @@ export function renderDiagram( parent.append(n); return n; }; + const label = (parent: Element, x: number, y: number, text: string, cls: string): void => { + const tag = add(parent, "g", { class: cls }); + const box = add(tag, "rect", { rx: "4", ry: "4" }); + const t = add(tag, "text", { + x: x.toFixed(1), + y: y.toFixed(1), + "dominant-baseline": "central", + "text-anchor": "middle", + }); + t.textContent = text; + const w = (t.getComputedTextLength?.() || text.length * 7) + 10; + Object.entries({ x: x - w / 2, y: y - 9, width: w, height: 18 }).forEach(([k, v]) => + box.setAttribute(k, v.toFixed(1)), + ); + }; + const defs = add(svg, "defs", {}); + const arrow = add(defs, "marker", { + id: "m02-dfdg-arrow", + viewBox: "0 0 8 8", + refX: "7", + refY: "4", + markerWidth: "7", + markerHeight: "7", + orient: "auto", + }); + add(arrow, "path", { d: "M0,0 L8,4 L0,8 z", class: "m02-dfdg__arrow" }); + const mids = new Map(); for (const l of lg.links) { const a = at(l.from, "out"); const b = at(l.to, "in"); @@ -474,42 +522,30 @@ export function renderDiagram( const traced = markOf((s) => s.some((x) => x.link === l.id)); const mark = strong && traced === " is-dim" ? "" : traced; // 고른 선은 흐리게 하지 않음 const dim = !mark && st.selected !== null && !strong ? " is-dim" : ""; + const rest = restLinks.has(l.id) ? " is-rest" : ""; const g = add(svg, "g", { - class: `m02-dfdg__link${strong ? " is-strong" : ""}${dim}${mark}`, + class: `m02-dfdg__link${strong ? " is-strong" : ""}${dim}${mark}${rest}`, "data-link": l.id, "data-ends": [a.x, a.y, b.x, b.y].map((n) => n.toFixed(1)).join(","), }); add(g, "path", { d, class: "m02-dfdg__hit" }); add(g, "path", { d, class: "m02-dfdg__wire" }); const mid = midOf(a, b); + mids.set(l.id, mid); const rule = labels[l.id] ?? linkLabel(l); - if (rule) { - const tag = add(g, "g", { class: "m02-dfdg__rule" }); - const box = add(tag, "rect", { rx: "4", ry: "4" }); - const text = add(tag, "text", { - x: mid.x.toFixed(1), - y: mid.y.toFixed(1), - "dominant-baseline": "central", - "text-anchor": "middle", - }); - text.textContent = rule; - const w = (text.getComputedTextLength?.() || rule.length * 7) + 10; - Object.entries({ x: mid.x - w / 2, y: mid.y - 9, width: w, height: 18 }).forEach(([k, v]) => - box.setAttribute(k, v.toFixed(1)), - ); - } + if (rule) label(g, mid.x, mid.y, rule, "m02-dfdg__rule"); + if (rest) label(g, mid.x, mid.y + (rule ? 18 : 0), tt("Rest"), "m02-dfdg__restmark"); const broken = breaks.filter((k) => k.link === l.id); + const why = [...broken, ...rests.filter((k) => k.link === l.id)] + .map((k) => k.text) + .join(" · "); if (broken.length) { - const why = broken.map((k) => k.text).join(" · "); + const cy = (mid.y - (rule ? 18 : 0)).toFixed(1); const badge = add(g, "g", { class: "m02-dfdg__linkwarn", "data-why": why }); - add(badge, "circle", { - cx: mid.x.toFixed(1), - cy: (mid.y - (rule ? 18 : 0)).toFixed(1), - r: "8", - }); + add(badge, "circle", { cx: mid.x.toFixed(1), cy, r: "8" }); const t = add(badge, "text", { x: mid.x.toFixed(1), - y: (mid.y - (rule ? 18 : 0)).toFixed(1), + y: cy, "dominant-baseline": "central", "text-anchor": "middle", }); @@ -518,10 +554,43 @@ export function renderDiagram( } g.addEventListener("click", (ev) => { ev.stopPropagation(); - const why = broken.map((k) => k.text).join(" · "); select(st.selected === l.id ? null : l.id, undefined, st.selected === l.id ? "" : why); }); } + // 기댐 선 — 규칙이 기대는 알약 → 그 규칙이 있는 곳 · 흐린 점선 · 화살 + for (const dep of depLines(lg)) { + const a = at(dep.from, "out"); + const b = + "link" in dep.to + ? mids.get(dep.to.link) + : at({ box: dep.to.box, name: dep.to.port }, "out", "far"); + if (!a || !b) continue; + const dx = Math.max(40, Math.abs(b.x - a.x) / 2); + const d = `M${a.x},${a.y} C${a.x + dx},${a.y} ${b.x - dx},${b.y} ${b.x},${b.y}`; + const where = "link" in dep.to ? dep.to.link : dep.to.box; + const steps = opts.trace; + const traced = + steps?.back && depInTrace(dep, steps.back) + ? " is-back" + : steps?.forward && depInTrace(dep, steps.forward) + ? " is-fwd" + : steps && (steps.back?.length || steps.forward?.length) + ? " is-dim" + : ""; + const g = add(svg, "g", { + class: `m02-dfdg__dep${traced}${st.selected === where ? " is-strong" : ""}`, + "data-dep": dep.id, + "data-ends": [a.x, a.y, b.x, b.y].map((n) => n.toFixed(1)).join(","), + }); + add(g, "path", { d, class: "m02-dfdg__hit" }); + add(g, "path", { d, class: "m02-dfdg__depwire", "marker-end": "url(#m02-dfdg-arrow)" }); + const m = midOf(a, b); + label(g, m.x, m.y, dep.text, "m02-dfdg__deptext"); + g.addEventListener("click", (ev) => { + ev.stopPropagation(); + select(where); + }); + } } const firstFit = (): void => { if (st.fitted || !lg.boxes.length || !view.clientWidth) return drawLines(); @@ -534,34 +603,6 @@ export function renderDiagram( if (view.isConnected) firstFit(); /* 끌기 셋 — 상자 · 판 · 선 */ - function follow( - ev: PointerEvent, - move: (m: PointerEvent) => void, - end: (u: PointerEvent, moved: boolean) => void, - ): void { - const x0 = ev.clientX; - const y0 = ev.clientY; - let moved = false; - const mv = (m: PointerEvent): void => { - if (!moved && Math.hypot(m.clientX - x0, m.clientY - y0) < 5) return; - moved = true; - move(m); - }; - const up = (u: PointerEvent): void => { - window.removeEventListener("pointermove", mv); - window.removeEventListener("pointerup", up); - if (moved) { - // 끈 뒤 바로 따라오는 click 만 삼킴 — 안 오면 곧 풀어 다음 누름은 그대로 - const swallow = (c: MouseEvent): void => c.stopPropagation(); - window.addEventListener("click", swallow, { capture: true, once: true }); - setTimeout(() => window.removeEventListener("click", swallow, { capture: true }), 0); - } - end(u, moved); - }; - window.addEventListener("pointermove", mv); - window.addEventListener("pointerup", up); - } - function dragBox(ev: PointerEvent, b: CfBox, node: HTMLElement): void { ev.preventDefault(); // 끌면서 글자가 잡히지 않게 const start = toWorld(ev.clientX, ev.clientY); diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts b/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts index 2b9ac51e0..6ee7fdf61 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts @@ -1,7 +1,7 @@ /* ============================================================================= * M02_DF_Diagram_Parts.ts * 단가산출 흐름도 다이어그램의 순수 부분(DOM 없음) — 글 · 약속 모양 · 떨군 자리 풀기 · 줄 맞추기 · - * 한눈에 맞춤 · 알약 값 글 · 흐름 길 가르기. 그리기는 `M02_DF_Diagram.ts`. + * 한눈에 맞춤 · 알약 값 글 · 흐름 길 가르기 · 기댐 선 · 끌기 손잡이. 그리기는 `M02_DF_Diagram.ts`. * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; @@ -15,6 +15,8 @@ import { type CfPort, type CfLink, type CfStep, + elementPort, + ruleSources, } from "./M02_DF_Model"; import type { CfBreak } from "./M02_DF_Eval"; import { ruleLabel } from "./M02_DF_Rule"; @@ -63,6 +65,13 @@ export const TEXT = { ], Break: ["끊긴 곳", "Break"], Back: ["온 길", "Came from"], + DepLookup: ["표 조건", "table key"], + DepGate: ["조건", "condition"], + DepMaterial: ["{f}에 {o}", "{f} {o}"], + Rest: ["쉼", "Resting"], + Empty1: ["① 「+ 입력」 으로 상자를 놓음", "① Place a box with “+ Input”"], + Empty2: ["② 알약을 끌어 다른 상자에 놓으면 선", "② Drag a pill onto another box to draw a line"], + Empty3: ["③ 선을 누르면 규칙", "③ Tap a line to set its rule"], Forward: ["가는 길", "Goes to"], } as const; @@ -231,3 +240,81 @@ export const midOf = ( x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, }); + +/* ── 기댐 선 · 쉼 ──────────────────────────────────────────────────── */ + +/** 기댐 선 — 규칙이 기대는 알약 → 그 규칙이 있는 곳(선이면 그 선 이름표 · 재료 조건 찾기면 그 재료 알약) */ +export interface DepLine { + id: string; + from: CfPort; + to: { link: string } | { box: string; port: string }; + text: string; +} + +export function depLines(lg: CfLogic): DepLine[] { + const out: DepLine[] = []; + for (const l of lg.links) + ruleSources(l.rule).forEach((from, i) => + out.push({ + id: `dep:${l.id}:${i}`, + from, + to: { link: l.id }, + text: tt(l.rule?.kind === "lookup" ? "DepLookup" : "DepGate"), + }), + ); + for (const b of lg.boxes) + for (const e of b.input?.elements ?? []) + if (e.mode === "rule" && e.rule) + out.push({ + id: `dep:${b.id}:${elementPort(e)}`, + from: e.rule.from, + to: { box: b.id, port: elementPort(e) }, + text: tt("DepMaterial", { f: e.rule.field, o: e.rule.op }), + }); + return out; +} + +/** 기댐 선이 흐름 길에 드나 — 기대는 알약과 닿는 곳(선 · 상자)이 같은 길에 있을 때 */ +export function depInTrace(dep: DepLine, steps: CfStep[]): boolean { + const src = steps.some((s) => s.box === dep.from.box && s.port === dep.from.name); + const dst = + "link" in dep.to + ? steps.some((s) => s.link === (dep.to as { link: string }).link) + : steps.some((s) => s.box === (dep.to as { box: string }).box); + return src && dst; +} + +/** 끊긴 곳 갈래 — 쉼(조건이 안 맞아 이번엔 안 지나감 · 문제 아님) · 문제 — level 이 없으면 「통과 안 됨」 을 쉼으로 */ +export const isRest = (k: CfBreak): boolean => + ((k as CfBreak & { level?: string }).level ?? (k.kind === "통과 안 됨" ? "쉼" : "문제")) === "쉼"; + +/* ── 끌기 손잡이 ─────────────────────────────────────────────────────── */ + +/** 누른 뒤 5px 넘게 움직이면 끌기 — 끈 뒤 따라오는 click 한 번만 삼킴 */ +export function follow( + ev: PointerEvent, + move: (m: PointerEvent) => void, + end: (u: PointerEvent, moved: boolean) => void, +): void { + const x0 = ev.clientX; + const y0 = ev.clientY; + let moved = false; + const mv = (m: PointerEvent): void => { + if (!moved && Math.hypot(m.clientX - x0, m.clientY - y0) < 5) return; + moved = true; + move(m); + }; + const up = (u: PointerEvent): void => { + window.removeEventListener("pointermove", mv); + window.removeEventListener("pointerup", up); + if (moved) { + // 끈 뒤 바로 따라오는 click 만 삼킴 — 안 오면 곧 풀어 다음 누름은 그대로 + const swallow = (c: MouseEvent): void => c.stopPropagation(); + window.addEventListener("click", swallow, { capture: true, once: true }); + setTimeout(() => window.removeEventListener("click", swallow, { capture: true }), 0); + } + end(u, moved); + }; + window.addEventListener("pointermove", mv); + window.addEventListener("pointerup", up); +} diff --git a/resources/tester/test_m02_df_diagram_parts.py b/resources/tester/test_m02_df_diagram_parts.py new file mode 100644 index 000000000..372581c53 --- /dev/null +++ b/resources/tester/test_m02_df_diagram_parts.py @@ -0,0 +1,238 @@ +"""단가산출 흐름도 다이어그램 순수 부분(`M02_DF_Diagram_Parts.ts`) — 기댐 선 · 흐름 길 포함 · 쉼 가르기 · 맞춤. + +화면 TS 를 그 자리에서 트랜스파일해 node 로 돌림 — 상대 경로 TS(Model · Rule …)는 이어 트랜스파일 · +화면 부품(@ui)은 빈 흉내. 견본 로직은 이 시험 안에 둠. +""" + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +SOURCE = ROOT / "M02_MasterTemplete" / "DataFlow" / "M02_DF_Diagram_Parts.ts" + +RUNNER = r""" +const fs = require("fs"); +const path = require("path"); +const [root, file] = process.argv.slice(1); +const ts = require(path.join(root, "config", "node_modules", "typescript")); +const stub = new Proxy({}, { get: (_t, k) => (k === "currentLanguageIndex" ? 0 : () => undefined) }); +const cache = {}; +function load(f) { + if (cache[f]) return cache[f].exports; + const js = ts.transpileModule(fs.readFileSync(f, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = (cache[f] = { exports: {} }); + const req = (id) => { + if (id.startsWith(".")) { + const p = path.resolve(path.dirname(f), id); + return fs.existsSync(p + ".ts") ? load(p + ".ts") : stub; + } + return stub; + }; + new Function("exports", "module", "require", js)(box.exports, box, req); + return box.exports; +} +const m = load(file); +const jobs = JSON.parse(fs.readFileSync(0, "utf8")); +const out = jobs.map((job) => { + if (job.op === "deps") return m.depLines(job.lg).map((d) => ({ id: d.id, from: d.from, to: d.to })); + if (job.op === "inTrace") return m.depLines(job.lg).map((d) => m.depInTrace(d, job.steps)); + if (job.op === "rest") return job.breaks.map((b) => m.isRest(b)); + if (job.op === "fit") return m.fitView(job.rects, job.view); + if (job.op === "value") return job.values.map((v) => m.showValue(v)); + return null; +}); +process.stdout.write(JSON.stringify(out)); +""" + + +def run(jobs: list[dict]) -> list: + if shutil.which("node") is None: + pytest.skip("node 가 없음") + done = subprocess.run( + ["node", "-e", RUNNER, str(ROOT), str(SOURCE)], + input=json.dumps(jobs, ensure_ascii=False), + capture_output=True, + text=True, + encoding="utf-8", + check=True, + ) + return json.loads(done.stdout) + + +def port(box, name): + return {"box": box, "name": name} + + +# 견본 — 관경 → 흄관 재료(조건 찾기) · 지반 → 조건 선 · 뒷길이 → 표 조건 +LOGIC = { + "key": None, + "name": "견본", + "resultUnit": "원/m", + "userElements": [], + "boxes": [ + { + "id": "v", + "kind": "input", + "name": "설계 값", + "color": "", + "unit": "-", + "x": 0, + "y": 0, + "input": { + "mode": "variable", + "elements": [], + "variables": [ + {"name": "관경", "type": "choice", "unit": "㎜", "example": 600}, + {"name": "지반", "type": "choice", "unit": "-", "example": "암"}, + {"name": "뒷길이", "type": "number", "unit": "㎝", "example": 30}, + ], + }, + }, + { + "id": "m", + "kind": "input", + "name": "재료", + "color": "", + "unit": "-", + "x": 0, + "y": 200, + "input": { + "mode": "group", + "group": "재료", + "variables": [], + "elements": [ + { + "ref": "", + "name": "흄관", + "spec": "", + "unit": "본", + "mode": "rule", + "slot": "흄관", + "rule": {"field": "규격", "op": "포함", "from": port("v", "관경")}, + }, + {"ref": "MT1", "name": "모래", "spec": "", "unit": "㎥", "mode": "fixed"}, + ], + }, + }, + { + "id": "t", + "kind": "input", + "name": "품 표", + "color": "", + "unit": "-", + "x": 0, + "y": 400, + "input": { + "mode": "group", + "group": "소요량", + "elements": [], + "variables": [], + "table": {"key": "QF1"}, + }, + }, + { + "id": "f", + "kind": "function", + "name": "셈", + "color": "", + "unit": "-", + "x": 300, + "y": 100, + "func": { + "sheet": {}, + "inputs": [{"port": "품", "cell": "B1"}, {"port": "흄관", "cell": "B2"}], + "variables": [], + "output": {"cell": "B9", "name": "금액", "unit": "원"}, + }, + }, + ], + "links": [ + { + "id": "l1", + "from": port("t", "표"), + "to": port("f", "품"), + "rule": { + "kind": "lookup", + "conds": [{"col": "뒷길이", "from": port("v", "뒷길이")}], + "col": "석공", + }, + }, + { + "id": "l2", + "from": port("m", "흄관"), + "to": port("f", "흄관"), + "rule": {"kind": "gate", "from": port("v", "지반"), "op": "=", "value": "암"}, + }, + {"id": "l3", "from": port("v", "관경"), "to": port("f", "흄관")}, + ], +} + + +def test_기댐_선_셋(): + [deps] = run([{"op": "deps", "lg": LOGIC}]) + got = sorted((d["from"]["name"], json.dumps(d["to"], ensure_ascii=False)) for d in deps) + assert got == sorted( + [ + ("뒷길이", json.dumps({"link": "l1"})), + ("지반", json.dumps({"link": "l2"})), + ("관경", json.dumps({"box": "m", "port": "흄관"}, ensure_ascii=False)), + ] + ) + + +def test_기댐_선이_흐름_길에_듦(): + steps = [ + {"box": "v", "port": "지반", "dir": "out"}, + {"box": "f", "port": "흄관", "dir": "in", "link": "l2"}, + {"box": "m", "port": "흄관", "dir": "out"}, + ] + [marks] = run([{"op": "inTrace", "lg": LOGIC, "steps": steps}]) + # 차례: l1 표 조건(뒷길이) · l2 조건(지반) · 재료 조건 찾기(관경) + assert marks == [False, True, False] + + +def test_쉼_가르기(): + [rest] = run( + [ + { + "op": "rest", + "breaks": [ + {"kind": "통과 안 됨", "box": "f", "link": "l2", "text": "지반이 암이 아님"}, + {"kind": "빈 받는 자리", "box": "f", "port": "품", "text": "비어 있음"}, + {"kind": "통과 안 됨", "box": "f", "level": "문제", "text": "level 이 먼저"}, + {"kind": "조건 겹침", "box": "f", "level": "쉼", "text": "level 이 먼저"}, + ], + } + ] + ) + assert rest == [True, False, False, True] + + +def test_한눈에_90_아래로_안_내림과_알약_값(): + fit, values = run( + [ + { + "op": "fit", + "rects": [{"x": 0, "y": 0, "w": 3000, "h": 200}], + "view": {"w": 1000, "h": 600}, + }, + { + "op": "value", + "values": [ + 97882.512, + 268908, + 0.3640, + "깬돌", + [["뒷길이", "석공"], [25, 0.15], [30, 0.22]], + ], + }, + ] + ) + assert fit["zoom"] == 0.9 + assert values == ["97,882.512", "268,908", "0.364", "깬돌", "· 2줄"] From 047f3a2798d89ea3d5e0454c56f52d12ee4fb07f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 20:35:39 +0900 Subject: [PATCH 05/18] =?UTF-8?q?fix(m02-df):=20=EC=9E=AC=EB=A3=8C=20?= =?UTF-8?q?=EC=9E=90=EB=A6=AC=20=EA=B0=92=20=EB=AC=BC=EC=9D=8C=20=C2=B7=20?= =?UTF-8?q?=EC=83=81=EC=9E=90=20=EC=9D=B4=EB=A6=84=20=EC=B9=B8=20=C2=B7=20?= =?UTF-8?q?=EC=9E=91=EC=9D=80=20=EC=B9=B8=20=C2=B7=20=EC=8B=9C=EC=9E=91=20?= =?UTF-8?q?=EB=8B=A8=EC=B6=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 재료 선택 자리 · 조건 찾기를 서버 /values 에 slot(방식 · 기본 품목 · 범위 · 찾기 규칙) · value(이어진 알약 지금 값)로 물음 — 물음 서명에 들어가 값이 바뀌면 다시 받음(관경 600 → 400 = 116,600) - 상세 내용 설정에 「상자 이름」(입력 · 함수 상자 · 빈 이름 · 같은 이름 안 받음 · 출력은 출력 판 이름 칸) - 머리 로직 이름 · 결과 단위 칸 공용 작은 크기(compact) - 시작 화면 「새로 만들기」 단추가 칸 폭으로 늘어나지 않게 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- M02_MasterTemplete/DataFlow/M02_DF_Entry.ts | 30 ++++++++++++++--- .../DataFlow/M02_DF_Entry_Detail.ts | 32 +++++++++++++++---- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts b/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts index 3bec34b5a..c6f845eeb 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts @@ -89,6 +89,8 @@ const TEXT = { Diagram: ["다이어그램", "Diagram"], Summary: ["원가", "Cost"], Color: ["상자 색", "Box colour"], + BoxName: ["상자 이름", "Box name"], + NameTaken: ["「{v}」 — 같은 이름의 상자가 있음", "“{v}” — another box has this name"], Standards: ["표준 표", "Standards"], Logics: ["다른 로직을 못 읽음 — {v}", "Couldn't read other logics — {v}"], NoName: ["로직 이름을 넣어 주세요", "Enter a logic name"], @@ -307,6 +309,17 @@ function openDataFlow(): void { className: "m02-df__color", children: [el("span", { text: te("Color") }), colorBox], }); + // 상자 이름 — 입력 · 함수 상자(출력 상자는 출력 판의 이름 칸) · 빈 이름 · 같은 이름은 안 받음 + const boxName = createInputField({ label: te("BoxName"), compact: true }); + boxName.input.addEventListener("change", () => { + const box = lg.boxes.find((b) => b.id === pickedBox()); + if (!box) return; + const next = boxName.input.value.trim(); + const taken = lg.boxes.some((b) => b.id !== box.id && b.name === next); + if (taken) note.textContent = te("NameTaken", { v: next }); + if (!next || taken || next === box.name) return void (boxName.input.value = box.name); + edit(M.model.updateBox(lg, box.id, { name: next })); + }); // 판마다 제 상태를 쥠 — 종류마다 따로 두고 숨기기만(갈아끼우면 편집기가 있다고 여김) const funcHost = el("div"); const outHost = el("div"); @@ -315,7 +328,7 @@ function openDataFlow(): void { const linkHost = el("div"); const pillHost = el("div"); const detail = detailFrame(); - detail.settings.append(colorRow); + detail.settings.append(boxName.root, colorRow); detail.body.append(inHost, funcHost, outHost, linkHost, pillHost); const evalNow = (): CfEvalResult => { @@ -341,10 +354,12 @@ function openDataFlow(): void { /** 입력 값 다시 받기 — 물음이 바뀐 때만 · 같은 조작의 뒤끝이라 기록을 더 쌓지 않음 */ const refreshValues = (): void => { - const sig = JSON.stringify(queriesOf(lg).map((a) => a.q)); + // 조건 찾기 재료는 이어진 알약 값(지금 셈)을 함께 물음 — 값이 바뀌면 다시 받음 + const pill = (p: CfPort): unknown => result?.ports[p.box]?.[p.name] ?? null; + const sig = JSON.stringify(queriesOf(lg, pill, M.model.elementPort).map((a) => a.q)); if (sig === valueSig) return; valueSig = sig; - void fetchValues(M.api.cfValues, lg).then( + void fetchValues(M.api.cfValues, lg, pill, M.model.elementPort).then( (got) => { if (sig !== valueSig) return; values = got.values; @@ -410,6 +425,7 @@ function openDataFlow(): void { label: te("Name"), placeholder: te("NamePlaceholder"), value: lg.name, + compact: true, onInput: (v) => edit({ ...lg, name: v }), }); name.root.classList.add("m02-df__name"); @@ -418,6 +434,7 @@ function openDataFlow(): void { label: te("Unit"), options: unitList.map((u) => ({ value: u, text: u.replace(/^원\//, "") })), value: lg.resultUnit, + compact: true, onChange: (v) => edit({ ...lg, resultUnit: v }), }); @@ -458,6 +475,8 @@ function openDataFlow(): void { const box = lg.boxes.find((b) => b.id === pickedBox()); drawHead(detail, lg, pick); colorRow.hidden = !box; + boxName.root.hidden = !box || box.kind === "output"; + if (box && document.activeElement !== boxName.input) boxName.input.value = box.name; linkHost.hidden = pick?.kind !== "link"; pillHost.hidden = pick?.kind !== "pill"; if (pick?.kind === "link") @@ -638,6 +657,7 @@ function part(title: string, className: string, children: HTMLElement[]): HTMLEl }); } -function bigButton(label: string, onClick: () => void): HTMLButtonElement { - return createButton({ label, variant: "filled", size: "sm", onClick }); +/** 보통 크기 단추 — 칸 폭으로 늘어나지 않게 감쌈 */ +function bigButton(label: string, onClick: () => void): HTMLElement { + return el("div", { children: [createButton({ label, variant: "filled", size: "sm", onClick })] }); } diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail.ts b/M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail.ts index fc27df74b..4cd321278 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail.ts @@ -10,7 +10,7 @@ import { el } from "@ui/ui_template_elements"; import { fetchLogics } from "../../M01_MasterData/M01_MasterData_UI_Logic_Api"; import type { CfAnswer, CfQuery } from "./M02_DF_Api"; import type { CfBreak, CfValues } from "./M02_DF_Eval"; -import type { CfLogic } from "./M02_DF_Model"; +import type { CfElementRef, CfLogic, CfPort } from "./M02_DF_Model"; const TEXT = { Detail: ["상세 내용", "Details"], @@ -169,14 +169,32 @@ export async function unitChoices(): Promise { return units.length ? units : ["원/㎡"]; } -/** 입력 값 물음 — 그룹 입력 상자의 요소(알약 = 요소 이름) · 표(알약 「표」) · 사용자 요소 · 변수는 Eval 이 스스로 봄 */ -export function queriesOf(lg: CfLogic): { q: CfQuery; box: string; port: string }[] { +/** 입력 값 물음 — 그룹 입력 상자의 요소(알약 = 요소 이름) · 재료 자리(알약 = 자리 이름 · 조건 찾기는 이어진 알약 값을 + * pill 로 읽어 넘김) · 표(알약 「표」) · 사용자 요소 · 변수는 Eval 이 스스로 봄 */ +export function queriesOf( + lg: CfLogic, + pill: (p: CfPort) => unknown = () => null, + elementPort: (e: CfElementRef) => string = (e) => e.name, +): { q: CfQuery; box: string; port: string }[] { return lg.boxes.flatMap((b) => { const inp = b.input; if (b.kind !== "input" || !inp || inp.mode !== "group" || !inp.group) return []; const group = inp.group; + const slots = inp.elements + .filter((e) => group === "재료" && e.mode && e.mode !== "fixed") + .map((e) => { + const port = elementPort(e); + const q: CfQuery = { + id: `${b.id}\u0001${port}`, + group, + unit: e.unit, + slot: { ...e, field: e.rule?.field, op: e.rule?.op }, + }; + if (e.mode === "rule" && e.rule) q.value = pill(e.rule.from); + return { q, box: b.id, port }; + }); const els = inp.elements - .filter((e) => e.ref && !e.ref.startsWith("UE-")) + .filter((e) => e.ref && !e.ref.startsWith("UE-") && !slots.some((s) => s.port === e.name)) .map((e) => { // 기계 요소는 「키#값 칸」 — 서버에는 키와 칸(field)을 따로 넘김 const [ref, field] = group === "기계" ? e.ref.split("#") : [e.ref, undefined]; @@ -187,7 +205,7 @@ export function queriesOf(lg: CfLogic): { q: CfQuery; box: string; port: string const table = inp.table ? [{ q: { id: `${b.id}\u0001표`, group, table: inp.table }, box: b.id, port: "표" }] : []; - return [...els, ...table]; + return [...slots, ...els, ...table]; }); } @@ -195,8 +213,10 @@ export function queriesOf(lg: CfLogic): { q: CfQuery; box: string; port: string export async function fetchValues( ask: (qs: CfQuery[], els: never) => Promise, lg: CfLogic, + pill?: (p: CfPort) => unknown, + elementPort?: (e: CfElementRef) => string, ): Promise<{ values: CfValues; errors: string[] }> { - const asks = queriesOf(lg); + const asks = queriesOf(lg, pill, elementPort); if (!asks.length) return { values: {}, errors: [] }; const answers = await ask( asks.map((a) => a.q), From 28469de6250ee99d645af59d86af9f0d9fd3adb3 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 20:36:24 +0900 Subject: [PATCH 06/18] =?UTF-8?q?fix(m02-df):=20=ED=91=9C=EC=A4=80=20?= =?UTF-8?q?=ED=91=9C=20=EB=AA=A8=EB=8B=AC=20=EC=A0=9C=EB=AA=A9=20=ED=95=9C?= =?UTF-8?q?=20=EB=B2=88=20=E2=80=94=20renderStandards=20title:false?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- M02_MasterTemplete/DataFlow/M02_DF_Entry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts b/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts index c6f845eeb..00cf48c4c 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts @@ -615,7 +615,7 @@ function openDataFlow(): void { title: te("Standards"), closeLabel: te("Close"), mount: (host: HTMLElement) => - M.std.renderStandards(host, { change: () => redraw() }), + M.std.renderStandards(host, { change: () => redraw() }, { title: false }), }), }), undo, From 018177408bddf41f737ea6db0f04f6ca5e2d7933 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 20:38:21 +0900 Subject: [PATCH 07/18] =?UTF-8?q?feat(m02-df):=20=EB=81=8A=EA=B8=B4=20?= =?UTF-8?q?=EA=B3=B3=20=EB=AC=B4=EA=B2=8C=20=E2=80=94=20=EC=89=BC(?= =?UTF-8?q?=EC=89=AC=EB=8A=94=20=EA=B0=88=EB=9E=98)=20=C2=B7=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CfBreak.level: 통과 안 됨 · 그 아래로 번진 빈 값 = 쉼 · 빈 받는 자리 · 표에 없는 조건 · 맞는 재료 없음 · 조건 겹침 · 안 맞음 · 오류 = 문제 - 쉬는 갈래에서 온 빈 값은 받는 상자도 쉼(셈 안 함 · 출력 빈 값 · 문제로 안 셈) - M02_DF_Compute.py breaks 에 같은 뜻 level 한 칸 - 시험 25개 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk --- M02_MasterTemplete/DataFlow/M02_DF_Compute.py | 2 + M02_MasterTemplete/DataFlow/M02_DF_Eval.ts | 42 +++++++++++++++++-- resources/tester/test_m02_df_model.py | 29 +++++++++++-- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Compute.py b/M02_MasterTemplete/DataFlow/M02_DF_Compute.py index cc7d9c1ed..d447e8eda 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Compute.py +++ b/M02_MasterTemplete/DataFlow/M02_DF_Compute.py @@ -470,6 +470,8 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N breaks.append( { "kind": kind, + # 무게 — 통과 안 됨 = 쉼(조건이 안 맞아 쉬는 갈래) · 나머지 = 문제(Eval CfBreak.level 과 같음) + "level": "쉼" if kind == "통과 안 됨" else "문제", "box": to.get("box"), "port": to.get("name"), "link": ln.get("id"), diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts b/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts index 9b6380065..aba6e5f26 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts @@ -43,6 +43,8 @@ export interface CfBreak { | "조건 겹침" | "조건 안 맞음" | "오류"; + /** 무게 — 「쉼」 = 조건이 안 맞아 쉬는 갈래(그 아래로 번진 빈 값 포함) · 「문제」 = 고쳐야 할 끊김 */ + level: "문제" | "쉼"; box: string; port?: string; link?: string; @@ -112,6 +114,11 @@ const TEXT = { "「{from}」 → 「{box}」 선 — 표에 맞는 줄이 여럿", "Line 「{from}」 → 「{box}」 — several table rows match", ], + /** {from} {box} */ + Rest: [ + "「{from}」 → 「{box}」 — 앞 조건이 안 맞아 쉬는 갈래", + "「{from}」 → 「{box}」 — resting branch (an earlier condition doesn't match)", + ], RuleError: [ "「{from}」 → 「{box}」 선 규칙을 셀 수 없음", "Line 「{from}」 → 「{box}」 rule can't be worked out", @@ -251,9 +258,26 @@ export function evalLogic( const passed = new Map(); /** 선 규칙에서 막힌 상자 — 그 함수는 셈하지 않고 빈 값을 넘김(갈래가 끊김) */ const blocked = new Set(); + /** 조건이 안 맞아 쉬는 상자 — 그 아래로 번짐 */ + const resting = new Set(); /** 선 하나로 들어오는 값 — 규칙이 있으면 엔진으로 */ const through = (l: CfLink): CfValue => { const v = valueOf(l.from.box, l.from.name); + // 쉬는 갈래에서 온 빈 값 — 받는 상자도 쉼(셈하지 않음 · 문제로 안 셈) + if (v === null && resting.has(l.from.box)) { + resting.add(l.to.box); + blocked.add(l.to.box); + const text = tl("Rest", { from: name(l.from.box), box: name(l.to.box) }); + found.push({ + kind: "통과 안 됨", + level: "쉼", + box: l.to.box, + port: l.to.name, + link: l.id, + text, + }); + return null; + } if (!l.rule) return v; if (v === null && (l.rule.kind === "scale" || l.rule.kind === "round")) return null; const conds = Object.fromEntries( @@ -262,15 +286,17 @@ export function evalLogic( const res = applyRule(l.rule, v, conds); if (l.rule.kind === "gate") passed.set(l.id, res.why === null); const fill = { from: name(l.from.box), box: name(l.to.box), rule: ruleLabel(l.rule) }; - if (res.why === "통과 안 됨") + if (res.why === "통과 안 됨") { + resting.add(l.to.box); found.push({ kind: "통과 안 됨", + level: "쉼", box: l.to.box, port: l.to.name, link: l.id, text: tl("NotPassed", fill), }); - else if (res.why) { + } else if (res.why) { const kind = res.why === "표에 없는 조건" ? "표에 없는 조건" : "오류"; const key = res.why === "표에 없는 조건" @@ -279,7 +305,7 @@ export function evalLogic( ? "ManyRows" : "RuleError"; const text = tl(key, fill); - found.push({ kind, box: l.to.box, port: l.to.name, link: l.id, text }); + found.push({ kind, level: "문제", box: l.to.box, port: l.to.name, link: l.id, text }); errors.push(text); } if (res.why) blocked.add(l.to.box); @@ -303,7 +329,13 @@ export function evalLogic( for (const e of b.input?.elements ?? []) if (e.mode === "rule" && ports[b.id][elementPort(e)] === null) { const text = tl("NoMaterial", { box: b.name, pill: elementPort(e) }); - found.push({ kind: "맞는 재료 없음", box: b.id, port: elementPort(e), text }); + found.push({ + kind: "맞는 재료 없음", + level: "문제", + box: b.id, + port: elementPort(e), + text, + }); errors.push(text); } } else if (b.kind === "input") ports[b.id] = otherLogic(b, arrive(b), given, opts, errors); @@ -383,6 +415,7 @@ function gates( const text = tl(n > 1 ? "Overlap" : "NoneMatch", fill); found.push({ kind: n > 1 ? "조건 겹침" : "조건 안 맞음", + level: "문제", box: g.from.box, port: g.from.name, text, @@ -402,6 +435,7 @@ export function breaks(lg: CfLogic, values: CfValues = {}, opts: CfEvalOptions = if (!lg.links.some((l) => l.to.box === b.id && l.to.name === p.name)) out.push({ kind: "빈 받는 자리", + level: "문제", box: b.id, port: p.name, text: tl("EmptyIn", { box: b.name, pill: p.name }), diff --git a/resources/tester/test_m02_df_model.py b/resources/tester/test_m02_df_model.py index 449a25a04..fe429974c 100644 --- a/resources/tester/test_m02_df_model.py +++ b/resources/tester/test_m02_df_model.py @@ -450,16 +450,17 @@ def test_나_조건_통과로_둘로_가르기_표에서_찾기() -> None: a_out, a_breaks, b_out, b_breaks, far = got # 토사: 0.5 × 100000 × 1.1 · 암 줄은 통과 안 됨 · 높이 4.5 → 증가율 40 assert a_out == {"b6": 55000, "b7": None, "b8": 40} - assert a_breaks == [["통과 안 됨", "l2"]] + assert a_breaks == [["통과 안 됨", "l2"], ["통과 안 됨", "l5"]] # 암 갈래 + 그 아래로 번진 쉼 assert b_out == { "b6": None, "b7": 50000, "b8": 30, } # 암 50,000 → 천 원 버림 그대로 · 높이 3.5 → 30 - assert b_breaks == ["통과 안 됨"] + assert b_breaks == ["통과 안 됨", "통과 안 됨"] # 토사 갈래 + 번진 쉼 assert far == [ ["통과 안 됨", "「입력」 → 「함수 2」 선이 조건(지반 = 암 일 때만)을 통과하지 않음"], ["표에 없는 조건", "「입력 2」 → 「함수 3」 선 — 표에 맞는 줄이 없음"], + ["통과 안 됨", "「함수 2」 → 「출력 2」 — 앞 조건이 안 맞아 쉬는 갈래"], ] @@ -471,7 +472,7 @@ def test_나_조건_선_겹침_안맞음() -> None: ) overlap, none = got assert overlap == [["조건 겹침", "「입력」 의 「지반」 에서 나간 조건 선이 둘 이상 통과함"]] - assert none == ["통과 안 됨", "통과 안 됨", "조건 안 맞음"] + assert none == ["통과 안 됨", "통과 안 됨", "통과 안 됨", "통과 안 됨", "조건 안 맞음"] def test_나_규칙_글과_엑셀_수식() -> None: @@ -620,3 +621,25 @@ def test_표_범위칸_숫자하나는_같음() -> None: ) # 4 = 「4」 줄만(같음) · 4 는 「4~6」(초과) 에 안 듦 · 빈 아래 끝은 열림 assert got == [[0.5, None], [0.6, None], [0.4, None], [None, "표에 없는 조건"], [0.6, None]] + + +def test_쉬는_갈래와_문제_무게() -> None: + got = _run( + SPLIT + "const a = E.breaks(lg, V('토사'));\n" + "const b = E.breaks(lg, V('자갈', 9));\n" + "out([a.map(x => [x.kind, x.level, x.box, x.text]), b.filter(x => x.level === '문제').map(x => x.kind)," + " E.evalLogic(lg, V('토사')).outputs]);" + ) + a, b_problems, outs = got + assert a == [ + [ + "통과 안 됨", + "쉼", + "b4", + "「입력」 → 「함수 2」 선이 조건(지반 = 암 일 때만)을 통과하지 않음", + ], + ["통과 안 됨", "쉼", "b7", "「함수 2」 → 「출력 2」 — 앞 조건이 안 맞아 쉬는 갈래"], + ] + # 하나도 안 맞음 · 표에 없는 조건만 문제 — 쉬는 갈래 넷은 문제로 안 셈 + assert b_problems == ["표에 없는 조건", "조건 안 맞음"] + assert outs["b7"] is None # 쉬는 갈래 끝 출력은 빈 값(0 아님) From e2b047ab1a6164437d5d2ca0dc9036e8acdfd0a2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 20:38:36 +0900 Subject: [PATCH 08/18] =?UTF-8?q?fix(M02-=ED=9D=90=EB=A6=84=EB=8F=84):=20?= =?UTF-8?q?=EC=84=A0=20=EA=B7=9C=EC=B9=99=20=ED=91=9C=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20-=20=EC=9D=B4=EB=A6=84=C2=B7=EC=95=84?= =?UTF-8?q?=EB=9E=98/=EC=9D=B4=EB=A6=84=C2=B7=EC=9C=84=20=EC=A7=9D?= =?UTF-8?q?=EC=9D=84=20=ED=95=9C=20=EC=A4=84=20=E3=80=8C=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=20(=EB=B2=94=EC=9C=84)=E3=80=8D=20=EB=A1=9C=20=EB=AC=B6?= =?UTF-8?q?=EC=96=B4=20=EA=B3=A0=EB=A6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01D2nUMUMTsqm93PbQtipo6c --- .../DataFlow/M02_DF_LinkPanel.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts b/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts index 5b4be1dc6..fcf156dc5 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts @@ -77,6 +77,7 @@ const TEXT = { ], WarnTake: ["꺼낼 칸을 고르지 않음", "No column to take"], Count: ["선", "Line"], + Range: ["{n} (범위)", "{n} (range)"], } as const; const tk = (key: keyof typeof TEXT, n?: number | string): string => @@ -174,13 +175,25 @@ function lookupEditor(ctx: Ctx, rule: Extract): HTML ...new Set([...ctx.headers, ...rule.conds.map((c) => c.col), rule.col].filter(Boolean)), ]; const colOpts = cols.map((c) => ({ value: c, text: c })); + // 조건 칸 — 「이름·아래」 「이름·위」 짝은 「이름 (범위)」 한 줄(고르면 칸 이름 = 이름 · Rule 이 아래 < 값 ≤ 위 로 찾음) + const range = (c: string): string | null => { + const m = /^(.+)·(아래|위)$/.exec(c); + return m && cols.includes(`${m[1]}·아래`) && cols.includes(`${m[1]}·위`) ? m[1] : null; + }; + const condOpts = [ + ...cols.filter((c) => !range(c)).map((c) => ({ value: c, text: c })), + ...[...new Set(cols.map(range).filter((x): x is string => !!x))].map((x) => ({ + value: x, + text: tk("Range", x), + })), + ]; const pills = pillOptions(ctx); const put = (next: Partial): void => setRule(ctx, { ...rule, ...next }); const rows = rule.conds.map((c, i) => row( select( ctx, - colOpts, + condOpts, c.col, (v) => put({ conds: rule.conds.map((x, j) => (j === i ? { ...x, col: v } : x)) }), tk("Col"), @@ -211,9 +224,11 @@ function lookupEditor(ctx: Ctx, rule: Extract): HTML label: tk("AddCond"), variant: "ghost", size: "sm", - disabled: !pills.length || !cols.length, + disabled: !pills.length || !condOpts.length, onClick: () => - put({ conds: [...rule.conds, { col: cols[0], from: fromKey(pills[0].value) }] }), + put({ + conds: [...rule.conds, { col: condOpts[0].value, from: fromKey(pills[0].value) }], + }), }), ), ); From 8a9f23c6e3b9bef8503ad1152a6affc7feddc0e9 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 20:39:25 +0900 Subject: [PATCH 09/18] =?UTF-8?q?refactor(M02):=20=ED=9D=90=EB=A6=84?= =?UTF-8?q?=EB=8F=84=20=EB=8B=A4=EC=9D=B4=EC=96=B4=EA=B7=B8=EB=9E=A8=20?= =?UTF-8?q?=EC=89=BC=20=EA=B0=80=EB=A5=B4=EA=B8=B0=EB=A5=BC=20Eval=20level?= =?UTF-8?q?=20=EA=B7=B8=EB=8C=80=EB=A1=9C(=EA=B0=80=EC=A0=95=20=EA=B1=B7?= =?UTF-8?q?=EC=9D=8C=20=C2=B7=20PLAN=2035-9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 시험 test_m02_df_diagram_parts.py 4 통과 · DataFlow 전부 검사 초록 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z --- .../DataFlow/M02_DF_Diagram_Parts.ts | 5 ++--- resources/tester/test_m02_df_diagram_parts.py | 20 +++++++++++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts b/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts index 6ee7fdf61..6c03d9b21 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts @@ -284,9 +284,8 @@ export function depInTrace(dep: DepLine, steps: CfStep[]): boolean { return src && dst; } -/** 끊긴 곳 갈래 — 쉼(조건이 안 맞아 이번엔 안 지나감 · 문제 아님) · 문제 — level 이 없으면 「통과 안 됨」 을 쉼으로 */ -export const isRest = (k: CfBreak): boolean => - ((k as CfBreak & { level?: string }).level ?? (k.kind === "통과 안 됨" ? "쉼" : "문제")) === "쉼"; +/** 끊긴 곳 갈래 — 쉼(조건이 안 맞아 이번엔 안 지나감 · 문제 아님) · 문제 — Eval 의 level 그대로 */ +export const isRest = (k: CfBreak): boolean => k.level === "쉼"; /* ── 끌기 손잡이 ─────────────────────────────────────────────────────── */ diff --git a/resources/tester/test_m02_df_diagram_parts.py b/resources/tester/test_m02_df_diagram_parts.py index 372581c53..ef01ab2f1 100644 --- a/resources/tester/test_m02_df_diagram_parts.py +++ b/resources/tester/test_m02_df_diagram_parts.py @@ -203,10 +203,22 @@ def test_쉼_가르기(): { "op": "rest", "breaks": [ - {"kind": "통과 안 됨", "box": "f", "link": "l2", "text": "지반이 암이 아님"}, - {"kind": "빈 받는 자리", "box": "f", "port": "품", "text": "비어 있음"}, - {"kind": "통과 안 됨", "box": "f", "level": "문제", "text": "level 이 먼저"}, - {"kind": "조건 겹침", "box": "f", "level": "쉼", "text": "level 이 먼저"}, + { + "kind": "통과 안 됨", + "level": "쉼", + "box": "f", + "link": "l2", + "text": "지반이 암이 아님", + }, + { + "kind": "빈 받는 자리", + "level": "문제", + "box": "f", + "port": "품", + "text": "비어 있음", + }, + {"kind": "통과 안 됨", "level": "문제", "box": "f", "text": "level 이 정함"}, + {"kind": "조건 겹침", "level": "쉼", "box": "f", "text": "level 이 정함"}, ], } ] From aaa36625e8285d155108283f35c6e89cc9c53d92 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 20:39:31 +0900 Subject: [PATCH 10/18] =?UTF-8?q?feat(m02-df):=20=EB=81=8A=EA=B8=B4=20?= =?UTF-8?q?=EA=B3=B3=EC=9D=80=20=EB=AC=B8=EC=A0=9C=EB=A7=8C=20=C2=B7=20?= =?UTF-8?q?=EC=89=AC=EB=8A=94=20=EA=B0=88=EB=9E=98=EB=8A=94=20=EB=94=B0?= =?UTF-8?q?=EB=A1=9C=20=ED=9A=8C=EC=83=89=20=EC=A4=84(level)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr --- M02_MasterTemplete/DataFlow/M02_DF_Verify.css | 12 ++++ M02_MasterTemplete/DataFlow/M02_DF_Verify.ts | 63 ++++++++++++------- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Verify.css b/M02_MasterTemplete/DataFlow/M02_DF_Verify.css index 7843ee96f..7f5dd2c53 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Verify.css +++ b/M02_MasterTemplete/DataFlow/M02_DF_Verify.css @@ -116,3 +116,15 @@ color: var(--color-danger); font-size: var(--text-caption); } + +/* 쉬는 갈래 — 문제가 아니라 회색으로 */ +.m02-dfver__rest { + padding: var(--spacing-8) var(--spacing-12); + border-left: 4px solid var(--color-border); + background: var(--color-surface); + color: var(--color-text-muted); +} + +.m02-dfver__rest .ui-btn { + color: var(--color-text-muted); +} diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Verify.ts b/M02_MasterTemplete/DataFlow/M02_DF_Verify.ts index 9c1e422bd..b19f747bb 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Verify.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Verify.ts @@ -24,6 +24,11 @@ import "./M02_DF_Verify.css"; const TEXT = { Breaks: ["끊긴 곳", "Broken spots"], BreaksHint: ["누르면 그 상자 · 선을 고름", "Click to select the box or line"], + Rest: ["쉬는 갈래", "Resting branches"], + RestHint: [ + "조건이 안 맞아 쉬는 곳 — 누르면 그 선을 고름", + "Idle because a condition is not met — click to select the line", + ], Design: ["설계 때 정할 것", "To decide at design time"], DesignHint: ["여기서 바꾸면 그 상자도 바뀜", "Changing here changes the box too"], Vars: ["변수 예시값", "Variable samples"], @@ -333,33 +338,45 @@ export function renderVerify( /* 끊긴 곳 */ const spots = breaks(lg, opts.values ?? {}, { logics: opts.logics }); - const spotBlock = spots.length - ? [ - el("div", { - className: "m02-dfver__block m02-dfver__breaks", + const spotList = (list: typeof spots, hint: TextKey): HTMLElement[] => [ + el("ul", { + className: "m02-dfver__list", + children: list.map((s) => + el("li", { children: [ - el("strong", { text: tv("Breaks") }), - el("ul", { - className: "m02-dfver__list", - children: spots.map((s) => - el("li", { - children: [ - createButton({ - label: s.text, - variant: "ghost", - size: "sm", - disabled: !s.box || !on.select, - onClick: () => on.select?.({ box: s.box, port: s.port, link: s.link }), - }), - ], - }), - ), + createButton({ + label: s.text, + variant: "ghost", + size: "sm", + disabled: !s.box || !on.select, + onClick: () => on.select?.({ box: s.box, port: s.port, link: s.link }), }), - el("p", { className: "m02-dfver__muted", text: tv("BreaksHint") }), ], }), - ] - : []; + ), + }), + el("p", { className: "m02-dfver__muted", text: tv(hint) }), + ]; + const problems = spots.filter((s) => s.level !== "쉼"); + const resting = spots.filter((s) => s.level === "쉼"); + const spotBlock = [ + ...(problems.length + ? [ + el("div", { + className: "m02-dfver__block m02-dfver__breaks", + children: [el("strong", { text: tv("Breaks") }), ...spotList(problems, "BreaksHint")], + }), + ] + : []), + ...(resting.length + ? [ + el("div", { + className: "m02-dfver__block m02-dfver__rest", + children: [el("strong", { text: tv("Rest") }), ...spotList(resting, "RestHint")], + }), + ] + : []), + ]; /* 설계 때 정할 것 — Model `designInputs`(변수 · 재료 자리 · 다른 로직) + 재료 조건 자리 */ const designs = designInputs(lg); From 1dfe6bfefb12adaf46d4c065299a0271dfafb5dd Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 21:00:14 +0900 Subject: [PATCH 11/18] =?UTF-8?q?fix(m02-df):=20=EB=A7=8C=EB=82=98?= =?UTF-8?q?=EB=8A=94=20=EC=83=81=EC=9E=90=EB=8A=94=20=EC=82=B4=EC=95=84=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=20=EC=84=A0=EC=9D=B4=20=EC=9E=88=EC=9C=BC?= =?UTF-8?q?=EB=A9=B4=20=EC=85=88=20=E2=80=94=20=EC=84=9C=EB=B2=84=20=3D=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 받는 상자는 들어오는 선이 모두 쉬는 갈래일 때만 쉼 · 하나라도 살아 있으면 셈(쉬는 갈래 값은 빈 칸) · 쉬는 선은 쉼 줄만 - 조건 선 바로 뒤 상자는 그대로 막음 · M02_DF_Compute.py 도 같은 뜻(resting · arrive) - 시험: 지반 견본 토사 → 1㎥ 경비 2,000 · 암 → 브레이커 갈래 값 · 쉼 줄 · 문제 0 · 서버 = 화면(26개) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk --- M02_MasterTemplete/DataFlow/M02_DF_Compute.py | 37 ++++++++++++++-- M02_MasterTemplete/DataFlow/M02_DF_Eval.ts | 44 +++++++++++-------- resources/tester/test_m02_df_model.py | 37 ++++++++++++++++ 3 files changed, 96 insertions(+), 22 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Compute.py b/M02_MasterTemplete/DataFlow/M02_DF_Compute.py index d447e8eda..3abcdc73b 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Compute.py +++ b/M02_MasterTemplete/DataFlow/M02_DF_Compute.py @@ -440,6 +440,9 @@ def _sheet_of(box: dict, arrived: dict, given: dict) -> dict: return sheet +REST = "앞 조건이 안 맞아 쉬는 갈래" + + def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | None = None) -> dict: """셈 — {ports, outputs, cost, errors, breaks, filled}(filled = 받는 칸에 값만 되돌려 넣음).""" ctx = {"load": load, "local": local, "stack": stack or [lg.get("key") or ""]} @@ -448,6 +451,7 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N name = lambda i: boxes.get(i, {}).get("name") or i # noqa: E731 errors, breaks, ports, arrived, outputs = [], [], {}, {}, {} blocked: set[str] = set() + resting: set[str] = set() # 조건이 안 맞아 쉬는 상자 — Eval 과 같은 뜻 cost: dict[str, Any] = {**dict.fromkeys(COSTS, Decimal(0)), "값들": {}} def through(ln: dict) -> Any: @@ -464,13 +468,15 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N got, why = apply_rule(rule, v, conds) if why: blocked.add(ln["to"].get("box")) + if why == "통과 안 됨": + resting.add(ln["to"].get("box")) text = f"「{name(src.get('box'))}」 → 「{name(ln['to'].get('box'))}」 {why}" kind = "오류" if why in ("표에 여럿 맞음", "오류") else why to = ln["to"] breaks.append( { "kind": kind, - # 무게 — 통과 안 됨 = 쉼(조건이 안 맞아 쉬는 갈래) · 나머지 = 문제(Eval CfBreak.level 과 같음) + # 무게 — 통과 안 됨 = 쉼 · 나머지 = 문제(Eval CfBreak.level 과 같음) "level": "쉼" if kind == "통과 안 됨" else "문제", "box": to.get("box"), "port": to.get("name"), @@ -483,9 +489,32 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N return None if why else got def arrive(box: dict) -> dict: - got = { - ln["to"].get("name"): through(ln) for ln in links if ln["to"].get("box") == box["id"] - } + """받는 값 — 쉬는 갈래 빈 값은 「쉼」 줄만 · 선이 모두 쉬면 상자도 쉼(Eval 과 같음).""" + ins = [ln for ln in links if ln["to"].get("box") == box["id"]] + got, rest = {}, 0 + for ln in ins: + src, to = ln.get("from") or {}, ln["to"] + if ( + src.get("box") in resting + and ports.get(src.get("box"), {}).get(src.get("name")) is None + ): + rest += 1 + got[to.get("name")] = None + breaks.append( + { + "kind": "통과 안 됨", + "level": "쉼", + "box": to.get("box"), + "port": to.get("name"), + "link": ln.get("id"), + "text": f"「{name(src.get('box'))}」 → 「{name(to.get('box'))}」 — {REST}", + } + ) + continue + got[to.get("name")] = through(ln) + if ins and rest == len(ins): + resting.add(box["id"]) + blocked.add(box["id"]) arrived[box["id"]] = got return got diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts b/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts index aba6e5f26..a35d22cf9 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts @@ -263,21 +263,6 @@ export function evalLogic( /** 선 하나로 들어오는 값 — 규칙이 있으면 엔진으로 */ const through = (l: CfLink): CfValue => { const v = valueOf(l.from.box, l.from.name); - // 쉬는 갈래에서 온 빈 값 — 받는 상자도 쉼(셈하지 않음 · 문제로 안 셈) - if (v === null && resting.has(l.from.box)) { - resting.add(l.to.box); - blocked.add(l.to.box); - const text = tl("Rest", { from: name(l.from.box), box: name(l.to.box) }); - found.push({ - kind: "통과 안 됨", - level: "쉼", - box: l.to.box, - port: l.to.name, - link: l.id, - text, - }); - return null; - } if (!l.rule) return v; if (v === null && (l.rule.kind === "scale" || l.rule.kind === "round")) return null; const conds = Object.fromEntries( @@ -311,10 +296,33 @@ export function evalLogic( if (res.why) blocked.add(l.to.box); return res.why ? null : res.value; }; + /** 받는 값 — 쉬는 갈래에서 온 빈 값은 「쉼」 줄만 남김 · 들어오는 선이 모두 쉬는 갈래면 받는 상자도 쉼 + * (하나라도 살아 있으면 셈 · 쉬는 갈래 값은 빈 칸) · 조건 선 바로 뒤 상자는 through 가 막음 */ const arrive = (box: CfBox): Record => { - const got = Object.fromEntries( - lg.links.filter((l) => l.to.box === box.id).map((l) => [l.to.name, through(l)]), - ); + const ins = lg.links.filter((l) => l.to.box === box.id); + const got: Record = {}; + let rest = 0; + for (const l of ins) { + if (resting.has(l.from.box) && valueOf(l.from.box, l.from.name) === null) { + rest++; + got[l.to.name] = null; + const text = tl("Rest", { from: name(l.from.box), box: name(l.to.box) }); + found.push({ + kind: "통과 안 됨", + level: "쉼", + box: l.to.box, + port: l.to.name, + link: l.id, + text, + }); + continue; + } + got[l.to.name] = through(l); + } + if (ins.length && rest === ins.length) { + resting.add(box.id); + blocked.add(box.id); + } arrived[box.id] = got; return got; }; diff --git a/resources/tester/test_m02_df_model.py b/resources/tester/test_m02_df_model.py index fe429974c..4cfbee4d8 100644 --- a/resources/tester/test_m02_df_model.py +++ b/resources/tester/test_m02_df_model.py @@ -643,3 +643,40 @@ def test_쉬는_갈래와_문제_무게() -> None: # 하나도 안 맞음 · 표에 없는 조건만 문제 — 쉬는 갈래 넷은 문제로 안 셈 assert b_problems == ["표에 없는 조건", "조건 안 맞음"] assert outs["b7"] is None # 쉬는 갈래 끝 출력은 빈 값(0 아님) + + +def _ground() -> dict: + return json.loads((CF / "_sample" / "ground.json").read_text(encoding="utf-8")) + + +def _server(lg: dict, values: dict) -> dict: + from M02_MasterTemplete.DataFlow import M02_DF_Compute as cp + + return cp.run(lg, values, lambda key: {}, []) + + +def test_만나는_상자는_살아_있는_선이_있으면_셈_서버와_같음() -> None: + lg = _ground() + soil = {"in-ground": {"지반": "토사"}} + rock = {"in-ground": {"지반": "암"}} + got = _run( + f"const lg = {json.dumps(lg, ensure_ascii=False)};\n" + f"const s = E.evalLogic(lg, {json.dumps(soil, ensure_ascii=False)});\n" + f"const r = E.evalLogic(lg, {json.dumps(rock, ensure_ascii=False)});\n" + "out([s.outputs, s.breaks.map(b => [b.kind, b.level, b.link]), r.outputs, r.breaks.map(b => [b.kind, b.level, b.link])," + " s.ports['fn-sum'], r.ports['fn-sum']]);" + ) + s_out, s_breaks, r_out, r_breaks, s_sum, r_sum = got + assert s_out == {"out-exp": 2000} # 굴삭기 갈래만 · 브레이커 갈래는 쉼(빈 칸) + assert s_breaks == [["통과 안 됨", "쉼", "l2"], ["통과 안 됨", "쉼", "l6"]] + assert [b for b in s_breaks if b[1] == "문제"] == [] + assert r_breaks == [["통과 안 됨", "쉼", "l1"], ["통과 안 됨", "쉼", "l5"]] + assert r_out["out-exp"] == r_sum["1㎥ 경비"] and r_out["out-exp"] != 2000 # 브레이커 갈래 값 + assert s_sum == {"1㎥ 경비": 2000} + # 서버(Compute) = 화면(Eval) + for values, screen_out, screen_breaks in ((soil, s_out, s_breaks), (rock, r_out, r_breaks)): + srv = _server(lg, values) + assert {k: float(v) for k, v in srv["outputs"].items()} == { + k: float(v) for k, v in screen_out.items() + } + assert [[b["kind"], b["level"], b["link"]] for b in srv["breaks"]] == screen_breaks From c2f7694e1de32f20bd55cbf345f9ce768b15a2f6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 21:01:27 +0900 Subject: [PATCH 12/18] =?UTF-8?q?fix(M02-=ED=9D=90=EB=A6=84=EB=8F=84):=20?= =?UTF-8?q?=EC=84=A0=20=EA=B7=9C=EC=B9=99=20=ED=91=9C=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20-=20=EB=B2=94=EC=9C=84=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EC=9D=80=20=EC=9D=B4=EB=A6=84=20(=EB=B2=94=EC=9C=84)?= =?UTF-8?q?=20=ED=95=9C=20=EC=A4=84=EB=A7=8C=20=C2=B7=20=EA=BA=BC=EB=82=BC?= =?UTF-8?q?=20=EC=B9=B8=EC=9D=80=20=ED=91=9C=20=EB=A8=B8=EB=A6=AC=20?= =?UTF-8?q?=EC=B9=B8=EB=A7=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01D2nUMUMTsqm93PbQtipo6c --- M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts b/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts index fcf156dc5..5f08dcf38 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts @@ -171,21 +171,18 @@ const row = (...children: (HTMLElement | string)[]): HTMLElement => /* ── 규칙마다 ─────────────────────────────────────────────────────────── */ function lookupEditor(ctx: Ctx, rule: Extract): HTMLElement[] { - const cols = [ - ...new Set([...ctx.headers, ...rule.conds.map((c) => c.col), rule.col].filter(Boolean)), - ]; + // 꺼낼 칸 = 표 머리 칸만 + const cols = [...new Set([...ctx.headers, rule.col].filter(Boolean))]; const colOpts = cols.map((c) => ({ value: c, text: c })); - // 조건 칸 — 「이름·아래」 「이름·위」 짝은 「이름 (범위)」 한 줄(고르면 칸 이름 = 이름 · Rule 이 아래 < 값 ≤ 위 로 찾음) + // 조건 칸 — 「이름·아래」 「이름·위」 짝은 「이름 (범위)」 한 줄만(고르면 칸 이름 = 이름 · Rule 이 아래 < 값 ≤ 위 로 찾음) const range = (c: string): string | null => { const m = /^(.+)·(아래|위)$/.exec(c); return m && cols.includes(`${m[1]}·아래`) && cols.includes(`${m[1]}·위`) ? m[1] : null; }; + const bases = [...new Set(cols.map(range).filter((x): x is string => !!x))]; const condOpts = [ - ...cols.filter((c) => !range(c)).map((c) => ({ value: c, text: c })), - ...[...new Set(cols.map(range).filter((x): x is string => !!x))].map((x) => ({ - value: x, - text: tk("Range", x), - })), + ...cols.filter((c) => !range(c) && !bases.includes(c)).map((c) => ({ value: c, text: c })), + ...bases.map((x) => ({ value: x, text: tk("Range", x) })), ]; const pills = pillOptions(ctx); const put = (next: Partial): void => setRule(ctx, { ...rule, ...next }); From 9052cb7411357ca696e307168eb21e038d4dabd4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 21:02:21 +0900 Subject: [PATCH 13/18] =?UTF-8?q?fix(M02):=20=ED=9D=90=EB=A6=84=EB=8F=84?= =?UTF-8?q?=20=EA=B8=B0=EB=8C=90=20=EC=84=A0=20=EB=81=9D=EC=9D=84=20?= =?UTF-8?q?=EC=95=8C=EC=95=BD=20=C2=B7=20=EC=9D=B4=EB=A6=84=ED=91=9C?= =?UTF-8?q?=EC=97=90=20=EB=B6=99=EC=9E=84=20=E2=80=94=20=EC=8B=9C=EC=9E=91?= =?UTF-8?q?=20=3D=20=EC=A1=B0=EA=B1=B4=20=EC=95=8C=EC=95=BD=20=EC=98=A4?= =?UTF-8?q?=EB=A5=B8=EC=AA=BD=20=C2=B7=20=EB=81=9D=20=3D=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=EC=84=A0=20=EC=9D=B4=EB=A6=84=ED=91=9C=20=EA=B0=80?= =?UTF-8?q?=EC=9E=A5=EC=9E=90=EB=A6=AC=20=EB=98=90=EB=8A=94=20=EC=9E=AC?= =?UTF-8?q?=EB=A3=8C=20=EC=95=8C=EC=95=BD=20=EC=98=A4=EB=A5=B8=EC=AA=BD(PL?= =?UTF-8?q?AN=2035-9=20=EB=B8=8C=EB=A0=88=EC=9D=B8=20=EC=9E=AC=EA=B2=80?= =?UTF-8?q?=EC=A6=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 시작에 작은 점 · 끝에 화살 · 끝 계산 depEnd · depPath(Parts) - 시험 test_m02_df_diagram_parts.py 5 통과 · DataFlow 전부 검사 초록 · Diagram 697줄 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z --- M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts | 40 +++++++++++++------ .../DataFlow/M02_DF_Diagram_Parts.ts | 19 +++++++++ resources/tester/test_m02_df_diagram_parts.py | 21 ++++++++++ 3 files changed, 68 insertions(+), 12 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts index f67ab35cd..b385ae6d7 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts @@ -14,7 +14,9 @@ import { clampZoom, fitView, KIND_COLOR, + depEnd, depInTrace, + depPath, follow, depLines, isRest, @@ -483,7 +485,7 @@ export function renderDiagram( parent.append(n); return n; }; - const label = (parent: Element, x: number, y: number, text: string, cls: string): void => { + const label = (parent: Element, x: number, y: number, text: string, cls: string): number => { const tag = add(parent, "g", { class: cls }); const box = add(tag, "rect", { rx: "4", ry: "4" }); const t = add(tag, "text", { @@ -497,6 +499,7 @@ export function renderDiagram( Object.entries({ x: x - w / 2, y: y - 9, width: w, height: 18 }).forEach(([k, v]) => box.setAttribute(k, v.toFixed(1)), ); + return w; }; const defs = add(svg, "defs", {}); const arrow = add(defs, "marker", { @@ -509,7 +512,16 @@ export function renderDiagram( orient: "auto", }); add(arrow, "path", { d: "M0,0 L8,4 L0,8 z", class: "m02-dfdg__arrow" }); - const mids = new Map(); + const dot = add(defs, "marker", { + id: "m02-dfdg-dot", + viewBox: "0 0 8 8", + refX: "4", + refY: "4", + markerWidth: "6", + markerHeight: "6", + }); + add(dot, "circle", { cx: "4", cy: "4", r: "3", class: "m02-dfdg__arrow" }); + const mids = new Map(); for (const l of lg.links) { const a = at(l.from, "out"); const b = at(l.to, "in"); @@ -531,9 +543,8 @@ export function renderDiagram( add(g, "path", { d, class: "m02-dfdg__hit" }); add(g, "path", { d, class: "m02-dfdg__wire" }); const mid = midOf(a, b); - mids.set(l.id, mid); const rule = labels[l.id] ?? linkLabel(l); - if (rule) label(g, mid.x, mid.y, rule, "m02-dfdg__rule"); + mids.set(l.id, { ...mid, w: rule ? label(g, mid.x, mid.y, rule, "m02-dfdg__rule") : 0 }); if (rest) label(g, mid.x, mid.y + (rule ? 18 : 0), tt("Rest"), "m02-dfdg__restmark"); const broken = breaks.filter((k) => k.link === l.id); const why = [...broken, ...rests.filter((k) => k.link === l.id)] @@ -559,14 +570,14 @@ export function renderDiagram( } // 기댐 선 — 규칙이 기대는 알약 → 그 규칙이 있는 곳 · 흐린 점선 · 화살 for (const dep of depLines(lg)) { + // 시작 = 조건 알약 오른쪽 · 끝 = 그 규칙 선의 이름표 가장자리 또는 받는 재료 알약 오른쪽 const a = at(dep.from, "out"); - const b = - "link" in dep.to - ? mids.get(dep.to.link) - : at({ box: dep.to.box, name: dep.to.port }, "out", "far"); - if (!a || !b) continue; - const dx = Math.max(40, Math.abs(b.x - a.x) / 2); - const d = `M${a.x},${a.y} C${a.x + dx},${a.y} ${b.x - dx},${b.y} ${b.x},${b.y}`; + const tag = "link" in dep.to ? mids.get(dep.to.link) : null; + const pill = "box" in dep.to ? at({ box: dep.to.box, name: dep.to.port }, "out") : null; + const end = tag ? depEnd(a, tag) : pill && { ...pill, side: "right" as const }; + if (!a || !end) continue; + const b = { x: end.x, y: end.y }; + const d = depPath(a, b, end.side); const where = "link" in dep.to ? dep.to.link : dep.to.box; const steps = opts.trace; const traced = @@ -583,7 +594,12 @@ export function renderDiagram( "data-ends": [a.x, a.y, b.x, b.y].map((n) => n.toFixed(1)).join(","), }); add(g, "path", { d, class: "m02-dfdg__hit" }); - add(g, "path", { d, class: "m02-dfdg__depwire", "marker-end": "url(#m02-dfdg-arrow)" }); + add(g, "path", { + d, + class: "m02-dfdg__depwire", + "marker-start": "url(#m02-dfdg-dot)", + "marker-end": "url(#m02-dfdg-arrow)", + }); const m = midOf(a, b); label(g, m.x, m.y, dep.text, "m02-dfdg__deptext"); g.addEventListener("click", (ev) => { diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts b/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts index 6c03d9b21..5181c80ed 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts @@ -284,6 +284,25 @@ export function depInTrace(dep: DepLine, steps: CfStep[]): boolean { return src && dst; } +type Pt = { x: number; y: number }; + +/** 이름표에 닿는 끝 — 시작이 왼쪽이면 이름표 왼쪽 가장자리 · 오른쪽이면 오른쪽 가장자리(이름표가 없으면 선 가운데) */ +export function depEnd(a: Pt | null, tag: Pt & { w: number }): Pt & { side: "left" | "right" } { + const fromLeft = !a || a.x <= tag.x; + return { + x: tag.x + (fromLeft ? -1 : 1) * (tag.w / 2 + 2), + y: tag.y, + side: fromLeft ? "left" : "right", + }; +} + +/** 기댐 선 길 — 알약 오른쪽에서 나가 끝에 닿음 · side = 끝으로 들어가는 쪽(재료 알약은 오른쪽에서) */ +export function depPath(a: Pt, b: Pt, side: "left" | "right"): string { + const dx = Math.max(40, Math.abs(b.x - a.x) / 2); + const bx = side === "left" ? b.x - dx : b.x + dx; + return `M${a.x},${a.y} C${a.x + dx},${a.y} ${bx},${b.y} ${b.x},${b.y}`; +} + /** 끊긴 곳 갈래 — 쉼(조건이 안 맞아 이번엔 안 지나감 · 문제 아님) · 문제 — Eval 의 level 그대로 */ export const isRest = (k: CfBreak): boolean => k.level === "쉼"; diff --git a/resources/tester/test_m02_df_diagram_parts.py b/resources/tester/test_m02_df_diagram_parts.py index ef01ab2f1..8f2cf9eae 100644 --- a/resources/tester/test_m02_df_diagram_parts.py +++ b/resources/tester/test_m02_df_diagram_parts.py @@ -45,6 +45,8 @@ const out = jobs.map((job) => { if (job.op === "rest") return job.breaks.map((b) => m.isRest(b)); if (job.op === "fit") return m.fitView(job.rects, job.view); if (job.op === "value") return job.values.map((v) => m.showValue(v)); + if (job.op === "end") return m.depEnd(job.a, job.tag); + if (job.op === "path") return m.depPath(job.a, job.b, job.side); return null; }); process.stdout.write(JSON.stringify(out)); @@ -248,3 +250,22 @@ def test_한눈에_90_아래로_안_내림과_알약_값(): ) assert fit["zoom"] == 0.9 assert values == ["97,882.512", "268,908", "0.364", "깬돌", "· 2줄"] + + +def test_기댐_선_끝이_알약과_이름표에_붙음(): + left, right, path_left, path_right = run( + [ + {"op": "end", "a": {"x": 100, "y": 50}, "tag": {"x": 400, "y": 120, "w": 80}}, + {"op": "end", "a": {"x": 600, "y": 50}, "tag": {"x": 400, "y": 120, "w": 80}}, + {"op": "path", "a": {"x": 100, "y": 50}, "b": {"x": 358, "y": 120}, "side": "left"}, + {"op": "path", "a": {"x": 100, "y": 50}, "b": {"x": 130, "y": 300}, "side": "right"}, + ] + ) + # 시작이 왼쪽이면 이름표 왼쪽 가장자리(400 − 40 − 2) · 오른쪽이면 오른쪽 가장자리 + assert left == {"x": 358, "y": 120, "side": "left"} + assert right == {"x": 442, "y": 120, "side": "right"} + # 길은 시작 알약에서 나가 끝에서 끝남 · 재료 알약은 오른쪽에서 들어감(끝 손잡이가 끝보다 오른쪽) + assert path_left.startswith("M100,50 ") and path_left.endswith(" 358,120") + assert path_right.startswith("M100,50 ") and path_right.endswith(" 130,300") + handle = float(path_right.split(" ")[2].split(",")[0]) + assert handle > 130 From 321e63f9c143c7b99d7c7e567a17488324569f70 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 21:16:41 +0900 Subject: [PATCH 14/18] =?UTF-8?q?fix(M02):=20=ED=9D=90=EB=A6=84=EB=8F=84?= =?UTF-8?q?=20=EC=89=BC=20=ED=9D=90=EB=A6=BC=20=E2=80=94=20=EC=83=81?= =?UTF-8?q?=EC=9E=90=EB=8A=94=20=EB=93=A4=EC=96=B4=EC=98=A4=EB=8A=94=20?= =?UTF-8?q?=EC=84=A0=EC=9D=B4=20=EB=AA=A8=EB=91=90=20=EC=89=BC(=EB=98=90?= =?UTF-8?q?=EB=8A=94=20=EC=89=BC=EC=9D=B4=20=EA=B1=B8=EB=A6=AC=EA=B3=A0=20?= =?UTF-8?q?=EB=82=B4=EB=B3=B4=EB=82=B4=EB=8A=94=20=EA=B0=92=EC=9D=B4=20?= =?UTF-8?q?=EB=AA=A8=EB=91=90=20=EB=B9=88=20=EA=B0=92)=EC=9D=BC=20?= =?UTF-8?q?=EB=95=8C=EB=A7=8C=20=C2=B7=20=EC=9D=BC=EB=B6=80=EB=A9=B4=20?= =?UTF-8?q?=EA=B7=B8=20=EC=84=A0=EA=B3=BC=20=EB=B0=9B=EB=8A=94=20=EC=95=8C?= =?UTF-8?q?=EC=95=BD=EB=A7=8C(PLAN=2035-9=20=EB=B8=8C=EB=A0=88=EC=9D=B8=20?= =?UTF-8?q?=EC=9E=AC=EA=B2=80=EC=A6=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - restParts(Parts) · 받는 알약 쉼 모양(점선 · 흐린 글) - 시험 test_m02_df_diagram_parts.py 6 통과 · DataFlow 전부 검사 초록 · Diagram 699줄 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z --- .../DataFlow/M02_DF_Diagram.css | 8 +++++ M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts | 12 ++++--- .../DataFlow/M02_DF_Diagram_Parts.ts | 23 ++++++++++++ resources/tester/test_m02_df_diagram_parts.py | 35 +++++++++++++++++++ 4 files changed, 73 insertions(+), 5 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.css b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.css index 74e5bed78..2687a588f 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.css +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.css @@ -498,3 +498,11 @@ gap: var(--spacing-8); pointer-events: none; } + +/* 쉼 — 받는 알약만(상자 흐림은 들어오는 선이 모두 쉼일 때) */ +.m02-dfdg__pill.is-in.is-rest { + border-style: dashed; + border-color: var(--color-text-muted); + color: var(--color-text-muted); + opacity: 0.7; +} diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts index b385ae6d7..9dd41038a 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts @@ -20,6 +20,7 @@ import { follow, depLines, isRest, + restParts, linkLabel, midOf, pillsOf, @@ -86,11 +87,11 @@ export function renderDiagram( /** 「!」 는 문제에만 · 쉼(조건이 안 맞아 이번엔 안 지나감)은 회색 점선 선 + 받는 상자 흐리게 */ const breaks = allBreaks.filter((k) => !isRest(k)); const rests = allBreaks.filter(isRest); - const restLinks = new Set(rests.map((k) => k.link).filter(Boolean)); - const restBoxes = new Set([ - ...rests.filter((k) => !k.link).map((k) => k.box), - ...lg.links.filter((l) => restLinks.has(l.id)).map((l) => l.to.box), - ]); + const { + links: restLinks, + pills: restPills, + boxes: restBoxes, + } = restParts(lg, rests, opts.values ?? {}); const boxName = (id: string): string => lg.boxes.find((b) => b.id === id)?.name ?? ""; const redraw = (): void => renderDiagram(host, lg, on, { ...opts, selected: st.selected, selectedPill: st.pill }); @@ -160,6 +161,7 @@ export function renderDiagram( wired ? "" : "is-empty", chosen ? "is-chosen" : "", broken ? "is-broken" : "", + dir === "in" && restPills.has(`${b.id}|${p.name}`) ? "is-rest" : "", st.linking && dir === "in" && b.id !== st.linking.box ? "is-target" : "", ].join(" ") + markOf((s) => s.some((x) => x.box === b.id && x.port === p.name)), attrs: { diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts b/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts index 5181c80ed..c96fe7fe4 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts @@ -306,6 +306,29 @@ export function depPath(a: Pt, b: Pt, side: "left" | "right"): string { /** 끊긴 곳 갈래 — 쉼(조건이 안 맞아 이번엔 안 지나감 · 문제 아님) · 문제 — Eval 의 level 그대로 */ export const isRest = (k: CfBreak): boolean => k.level === "쉼"; +/** 쉼 모양 자리 — 선 · 받는 알약(「상자|알약」) · 흐릴 상자 + * 상자 흐림은 들어오는 선이 모두 쉼일 때 · 또는 상자에 걸린 쉼이 있고 그 상자 내보내는 값이 모두 빈 값일 때만 */ +export function restParts( + lg: CfLogic, + rests: CfBreak[], + values: Record>, +): { links: Set; pills: Set; boxes: Set } { + const links = new Set(rests.map((k) => k.link).filter((x): x is string => !!x)); + const pills = new Set( + lg.links.filter((l) => links.has(l.id)).map((l) => `${l.to.box}|${l.to.name}`), + ); + const boxes = new Set(); + const empty = (v: unknown): boolean => v === null || v === undefined || v === ""; + for (const b of lg.boxes) { + const incoming = lg.links.filter((l) => l.to.box === b.id); + const allRest = incoming.length > 0 && incoming.every((l) => links.has(l.id)); + const outs = Object.values(values[b.id] ?? {}); + const hit = rests.some((k) => !k.link && k.box === b.id); + if (allRest || (hit && outs.every(empty))) boxes.add(b.id); + } + return { links, pills, boxes }; +} + /* ── 끌기 손잡이 ─────────────────────────────────────────────────────── */ /** 누른 뒤 5px 넘게 움직이면 끌기 — 끈 뒤 따라오는 click 한 번만 삼킴 */ diff --git a/resources/tester/test_m02_df_diagram_parts.py b/resources/tester/test_m02_df_diagram_parts.py index 8f2cf9eae..f5842893e 100644 --- a/resources/tester/test_m02_df_diagram_parts.py +++ b/resources/tester/test_m02_df_diagram_parts.py @@ -46,6 +46,10 @@ const out = jobs.map((job) => { if (job.op === "fit") return m.fitView(job.rects, job.view); if (job.op === "value") return job.values.map((v) => m.showValue(v)); if (job.op === "end") return m.depEnd(job.a, job.tag); + if (job.op === "restParts") { + const r = m.restParts(job.lg, job.rests, job.values ?? {}); + return { links: [...r.links].sort(), pills: [...r.pills].sort(), boxes: [...r.boxes].sort() }; + } if (job.op === "path") return m.depPath(job.a, job.b, job.side); return null; }); @@ -269,3 +273,34 @@ def test_기댐_선_끝이_알약과_이름표에_붙음(): assert path_right.startswith("M100,50 ") and path_right.endswith(" 130,300") handle = float(path_right.split(" ")[2].split(",")[0]) assert handle > 130 + + +def test_쉼은_일부면_선과_받는_알약만_모두면_상자도(): + two_in = { + **LOGIC, + "links": [ + {"id": "a", "from": port("v", "지반"), "to": port("f", "품")}, + {"id": "b", "from": port("m", "흄관"), "to": port("f", "흄관")}, + ], + } + one_rest = [ + {"kind": "통과 안 됨", "level": "쉼", "box": "f", "port": "흄관", "link": "b", "text": "쉼"} + ] + all_rest = one_rest + [ + {"kind": "통과 안 됨", "level": "쉼", "box": "f", "port": "품", "link": "a", "text": "쉼"} + ] + box_rest = [{"kind": "통과 안 됨", "level": "쉼", "box": "m", "text": "쉼"}] + part, every, filled, empty = run( + [ + {"op": "restParts", "lg": two_in, "rests": one_rest, "values": {"f": {"금액": 2000}}}, + {"op": "restParts", "lg": two_in, "rests": all_rest, "values": {"f": {"금액": None}}}, + {"op": "restParts", "lg": two_in, "rests": box_rest, "values": {"m": {"흄관": 52000}}}, + {"op": "restParts", "lg": two_in, "rests": box_rest, "values": {"m": {"흄관": None}}}, + ] + ) + # 하나만 쉼 — 셈하는 상자는 안 흐림 · 그 선과 받는 알약만 + assert part == {"links": ["b"], "pills": ["f|흄관"], "boxes": []} + # 들어오는 선이 모두 쉼 — 상자도 흐림 + assert every["boxes"] == ["f"] + # 상자에 걸린 쉼 — 내보내는 값이 있으면 안 흐림 · 모두 빈 값이면 흐림 + assert filled["boxes"] == [] and empty["boxes"] == ["m"] From 7d209c56f66bc6bab108bcf294bcc244bab45131 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 21:17:51 +0900 Subject: [PATCH 15/18] =?UTF-8?q?fix(m02-df):=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=20=EC=84=A4=EC=A0=95=20=EC=A4=84=EC=9D=80=20?= =?UTF-8?q?=EC=83=81=EC=9E=90=EB=A5=BC=20=EA=B3=A0=EB=A5=B8=20=EB=95=8C?= =?UTF-8?q?=EB=A7=8C=20=E2=80=94=20=EC=84=A0=20=C2=B7=20=EC=95=8C=EC=95=BD?= =?UTF-8?q?=EC=9D=B4=EB=A9=B4=20=EC=A4=84=EC=A7=B8=20=EC=88=A8=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- M02_MasterTemplete/DataFlow/M02_DF_Entry.css | 4 ++++ M02_MasterTemplete/DataFlow/M02_DF_Entry.ts | 2 ++ 2 files changed, 6 insertions(+) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Entry.css b/M02_MasterTemplete/DataFlow/M02_DF_Entry.css index 0047e4863..05228f379 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Entry.css +++ b/M02_MasterTemplete/DataFlow/M02_DF_Entry.css @@ -254,6 +254,10 @@ border-bottom: 0; } +.m02-df__detail-row[hidden] { + display: none; +} + .m02-df__detail-row h4 { margin: 0; color: var(--color-text-secondary); diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts b/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts index 00cf48c4c..e6a60b0e8 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts @@ -474,6 +474,8 @@ function openDataFlow(): void { if (boxId && !lg.boxes.some((b) => b.id === boxId)) pick = null; const box = lg.boxes.find((b) => b.id === pickedBox()); drawHead(detail, lg, pick); + // 설정 줄(상자 이름 · 색)은 상자를 고른 때만 — 선 · 알약이면 줄째 숨김 + (detail.settings.parentElement as HTMLElement).hidden = !box; colorRow.hidden = !box; boxName.root.hidden = !box || box.kind === "output"; if (box && document.activeElement !== boxName.input) boxName.input.value = box.name; From 2c134b440fd8bf0cc85019797c77b792ce687f4c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 21:21:30 +0900 Subject: [PATCH 16/18] =?UTF-8?q?fix(M02):=20=ED=9D=90=EB=A6=84=EB=8F=84?= =?UTF-8?q?=20restParts=20=E2=80=94=20=EC=89=BC=EC=9D=B4=20=EA=B7=B8=20?= =?UTF-8?q?=EC=83=81=EC=9E=90=EC=97=90=20=EA=B1=B8=EB=A0=B8=EB=82=98?= =?UTF-8?q?=EB=8A=94=20link=20=EC=99=80=20=EB=AC=B4=EA=B4=80=20=C2=B7=20?= =?UTF-8?q?=EB=B9=88=20=EA=B0=92=20=ED=8C=90=EC=A0=95=EC=9D=80=20=EB=82=B4?= =?UTF-8?q?=EB=B3=B4=EB=82=B4=EB=8A=94=20=EC=95=8C=EC=95=BD=20=EA=B0=92?= =?UTF-8?q?=EB=A7=8C(PLAN=2035-9=20=EB=B8=8C=EB=A0=88=EC=9D=B8=20=EC=9E=AC?= =?UTF-8?q?=EA=B2=80=EC=A6=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 지반 견본(토사) 꼴 시험: 흐림 = 브레이커로 깨기 하나 · 1㎥ 경비 · 경비는 안 흐림 - 시험 test_m02_df_diagram_parts.py 7 통과 · DataFlow 전부 검사 초록 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z --- .../DataFlow/M02_DF_Diagram_Parts.ts | 5 +- resources/tester/test_m02_df_diagram_parts.py | 100 ++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts b/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts index c96fe7fe4..cf0616712 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts @@ -322,8 +322,9 @@ export function restParts( for (const b of lg.boxes) { const incoming = lg.links.filter((l) => l.to.box === b.id); const allRest = incoming.length > 0 && incoming.every((l) => links.has(l.id)); - const outs = Object.values(values[b.id] ?? {}); - const hit = rests.some((k) => !k.link && k.box === b.id); + // 내보내는 알약 값만(들어온 값은 빼고) · 쉼이 그 상자에 걸렸나(Eval 쉼 줄은 link 가 늘 있음) + const outs = portsOut(b).map((p) => values[b.id]?.[p.name]); + const hit = rests.some((k) => k.box === b.id); if (allRest || (hit && outs.every(empty))) boxes.add(b.id); } return { links, pills, boxes }; diff --git a/resources/tester/test_m02_df_diagram_parts.py b/resources/tester/test_m02_df_diagram_parts.py index f5842893e..ae8248233 100644 --- a/resources/tester/test_m02_df_diagram_parts.py +++ b/resources/tester/test_m02_df_diagram_parts.py @@ -304,3 +304,103 @@ def test_쉼은_일부면_선과_받는_알약만_모두면_상자도(): assert every["boxes"] == ["f"] # 상자에 걸린 쉼 — 내보내는 값이 있으면 안 흐림 · 모두 빈 값이면 흐림 assert filled["boxes"] == [] and empty["boxes"] == ["m"] + + +def test_지반_토사_쉼_흐림은_브레이커로_깨기_하나(): + def fn(id_, name, ins, out): + return { + "id": id_, + "kind": "function", + "name": name, + "color": "", + "unit": "-", + "x": 0, + "y": 0, + "func": { + "sheet": {}, + "inputs": [{"port": p, "cell": f"B{i + 1}"} for i, p in enumerate(ins)], + "variables": [], + "output": {"cell": "B9", "name": out, "unit": "원"}, + }, + } + + gate = {"kind": "gate", "from": port("ig", "지반"), "op": "=", "value": "암"} + ground = { + "key": None, + "name": "지반", + "resultUnit": "원/㎥", + "userElements": [], + "boxes": [ + { + "id": "ig", + "kind": "input", + "name": "지반 · 굴착량", + "color": "", + "unit": "-", + "x": 0, + "y": 0, + "input": { + "mode": "variable", + "elements": [], + "variables": [ + {"name": "지반", "type": "choice", "unit": "-", "example": "토사"}, + {"name": "굴착량", "type": "number", "unit": "㎥", "example": 1}, + ], + }, + }, + fn("exc", "굴삭기로 파기", ["굴착량", "시간당 경비"], "굴삭기 경비"), + fn("brk", "브레이커로 깨기", ["굴착량", "시간당 경비"], "브레이커 경비"), + fn("sum", "1㎥ 경비", ["굴삭기 경비", "브레이커 경비", "굴착량"], "1㎥ 경비"), + { + "id": "out", + "kind": "output", + "name": "경비", + "color": "", + "unit": "원", + "x": 0, + "y": 0, + "output": {"cost": "경비", "unit": "원"}, + }, + ], + "links": [ + { + "id": "l1", + "from": port("ig", "굴착량"), + "to": port("exc", "굴착량"), + "rule": {**gate, "op": "≠"}, + }, + {"id": "l2", "from": port("ig", "굴착량"), "to": port("brk", "굴착량"), "rule": gate}, + {"id": "l5", "from": port("exc", "굴삭기 경비"), "to": port("sum", "굴삭기 경비")}, + {"id": "l6", "from": port("brk", "브레이커 경비"), "to": port("sum", "브레이커 경비")}, + {"id": "l7", "from": port("ig", "굴착량"), "to": port("sum", "굴착량")}, + {"id": "l8", "from": port("sum", "1㎥ 경비"), "to": port("out", "값")}, + ], + } + # Eval 쉼 줄은 link 가 늘 있음 — 조건 선 l2(받는 상자 브레이커) · 빈 값이 흘러간 l6(받는 상자 1㎥ 경비) + rests = [ + { + "kind": "통과 안 됨", + "level": "쉼", + "box": "brk", + "port": "굴착량", + "link": "l2", + "text": "암이 아님", + }, + { + "kind": "통과 안 됨", + "level": "쉼", + "box": "sum", + "port": "브레이커 경비", + "link": "l6", + "text": "쉼", + }, + ] + values = { + "brk": {"시간당 경비": 80000, "굴착량": None, "브레이커 경비": None}, + "sum": {"굴삭기 경비": 2000, "브레이커 경비": None, "굴착량": 1, "1㎥ 경비": 2000}, + "out": {"값": 2000}, + } + [got] = run([{"op": "restParts", "lg": ground, "rests": rests, "values": values}]) + assert got["boxes"] == ["brk"] + assert got["links"] == ["l2", "l6"] + assert got["pills"] == ["brk|굴착량", "sum|브레이커 경비"] From 69a138af0017a28759c424d4b1748b69d3042186 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 23:21:20 +0900 Subject: [PATCH 17/18] =?UTF-8?q?chore(M01):=20=ED=9D=90=EB=A6=84=EB=8F=84?= =?UTF-8?q?=20=EB=B3=B8=20=ED=94=84=EB=A1=9C=EA=B7=B8=EB=9E=A8=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20=EC=A0=95=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YAxqrz1iadqdygC6bE3pic --- docs/raw/M01_쉬운단가/흐름도_적용_정함.md | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/raw/M01_쉬운단가/흐름도_적용_정함.md diff --git a/docs/raw/M01_쉬운단가/흐름도_적용_정함.md b/docs/raw/M01_쉬운단가/흐름도_적용_정함.md new file mode 100644 index 000000000..2536a147b --- /dev/null +++ b/docs/raw/M01_쉬운단가/흐름도_적용_정함.md @@ -0,0 +1,37 @@ +# 흐름도 본 프로그램 적용 — 정함 (PLAN 36 · 브레인 · 10-01 23시) + +사용자 지시 · 결정은 PLAN.md 36장 머리 줄(그대로 인용)이 기준. 아래는 창끼리 맞출 약속. + +## 목적 + +「단가산출 로직」 화면에서 로직을 열면 왼쪽 = 지금 내용(보기) · 오른쪽 = 같은 로직의 흐름도(고치는 곳). 한 로직 · 두 보기 · 고칠 곳은 흐름도 한 곳. 옛 로직은 열 때 흐름도로 자동 변환(파일은 안 바뀜) · 흐름도로 [저장]하면 그 로직은 흐름도가 정본. + +## 약속 + +1. **로직 줄 「흐름」 칸** — 마스터 로직 줄(`resources/master_data/로직_*.json` 의 `줄[]`)에 `흐름`(CfLogic JSON · DataFlow Model 꼴 그대로). 있으면 흐름도가 정본. 쓰는 때 = 화면 [저장](기존 LogicLab `POST /save` 의 줄에 실어 보냄)뿐 · 자동 변환 결과를 파일에 쓰지 않음. 서버는 저장 때 흐름을 검사(Model 검사 꼴)하고 서버 셈으로 다시 셈. +2. **흐름 받기 길** — `GET /api/m01/logic/flow?file=&key=` → `{flow: CfLogic, converted: bool, notes: string[]}` · 줄에 `흐름` 이 있으면 그대로(converted false) · 없으면 변환(converted true · notes = 못 옮긴 곳 쉬운 말). 변환 모듈 = `M02_MasterTemplete/DataFlow/M02_DF_Convert.py`(sub7 · 서버 단독 · 옛 식 파서는 지금 서버가 쓰는 것을 그대로 씀). +3. **변환 규칙** — 입력 → 입력 상자(변수 · 고르기 = choice · 범위) · 중간 식 → 함수 상자(식 하나 = 결과 하나) · 호표 줄 → 요소 알약(단가) + 수량 함수 + 출력 상자(비목별: 노무비 · 재료비 · 경비) · 결과 식 · 덧줄 · 끝수 → 함수 · 끝수 · 옛 식에 나온 이름마다 알약을 칸에 연결하고 식은 칸 주소로(M02 방식) · 만약 → IF · 또는 → OR · 최대/최소 → MAX/MIN · 올림/반올림/버림 → ROUNDUP/ROUND/ROUNDDOWN · 찾기(표, 조건=값).칸 → 표 알약 + 선 규칙 「표에서 찾기」 · 로직(…) → 다른 로직 입력 상자 · 자동 배치(입력 왼쪽 → 함수 가운데 → 출력 오른쪽 · 겹침 없음). 전수 대조 = 1,472 로직마다 옛 셈 값(기본 입력 · 시험입력) = 변환 흐름도 셈 값 · 맞는 비율 · 안 맞는 까닭 목록(읽기만 · 파일 안 씀). +4. **계산 길** — 기존 M01 계산(`/calc` 꼴 · 시험 계산 · 다른 화면이 부르는 셈)이 줄에 `흐름` 이 있으면 DataFlow 서버 셈(`M02_DF_Compute.py`)으로 · 없으면 지금 셈 그대로. 입력 이름 = 흐름의 「설계 때 정할 것」(변수 이름) 그대로. +5. **오른쪽 패널** — `M02_MasterTemplete/DataFlow/M02_DF_Panel.ts`(sub8) `mountFlowPanel(host, opts) → handle` · opts = { logic: CfLogic; readOnly?: boolean; title: string; onChange(lg: CfLogic): void; logics?(key) } · handle = { set(lg), destroy(), expand() } · 다이어그램 · 상세 내용 · 원가 · 검증 칸을 패널 안에(세로로) · 너비 끌어 조절 · 접기 · 「크게 보기」(전체 화면 · 닫으면 패널로) · 저장 단추는 패널에 없음(페이지 [저장] 하나) · 실행 취소는 패널 안. +6. **페이지 저장 하나** — 패널 조작 = 페이지 캐시(drafts · sessionStorage)에 쌓임 · [저장] 한 번에 줄(흐름 포함) 보냄 · [고친 것 버리기] = 흐름도 고친 것도 버림 · 자동저장 없음. +7. **왼쪽 보기(흐름 로직)** — 기본정보(지금대로) · 단가산출 상세 = 흐름도 출력 상자를 비용 종류별 줄(이름 · 단위 · 값) · 소계 · 계 · 텍스트 수식 = 함수 상자마다 「식 보기」 두 줄(이름 식 · 값 식) · 시험 계산 = 계산 길(4) 그대로. 흐름 없는 옛 로직의 왼쪽은 지금 그대로. +8. **식 보기 · 조건 만들기** — `M02_DF_FormulaView.ts`(sub2) `formulaLines(box: CfBox, values) → { name: string; named: string; valued: string; branches?: {when: string; then: string; now: boolean}[] }[]` (칸 주소 → 연결 알약 이름 · ×, ÷ · IF 는 만약 / 아니면 줄) — 상세 내용 함수 판과 왼쪽 텍스트 수식(sub5)이 함께 씀. 「조건 만들기」 = 엑셀 시트 안 기능(`A00_Common/spreadsheet/spreadsheet_condition.ts` 새 파일 · 편집기 우클릭 「조건 만들기」 · 부르는 쪽이 `conditions` 선택을 줄 때만 보임 → M02 화면은 그대로) · 만약 [칸 · 알약] [비교] [값] → [칸 · 값] 줄 · 아니면 만약 · 아니면 → 고른 칸에 IF 식 · 값 목록은 부르는 쪽 `valuesFor(a1)` 이 줌. +9. **선 조건(조건 통과)** — 웹앱 모양 그대로 · CfRule gate = `{ kind: "gate"; conds: { from: CfPort; op; value }[]; join: "그리고" | "또는" }`(옛 한 줄 꼴도 읽음) · Model `valueChoices(lg, port) → (string | number)[] | null`(변수 고르기 · 표 칸 값 · 재료 규격 · 없으면 null = 숫자 칸 + 범위 검사) · 규칙 → 받는 칸 엑셀 식 · 서버 Compute 같은 뜻 · 선 이름표 「지반 = 토사 그리고 높이 ≥ 3 일 때만」. +10. **단추** — 「식으로 새로 만들기」 지움 · 「신규 로직」(빈 흐름도 + 기본정보 칸 이름 · 원문번호 · 결과 단위 · 출처 · 구분 자체 → drafts 새 줄 → [저장] 때 서버가 GX 새 키) · 「본떠 만들기」 → 「로직 복사/생성」(신규 로직 오른쪽 · 흐름도 있으면 함께 복사) · 「시험판 ▾」 과 시험판 단추 둘 지움. +11. **다른 로직 입력** — 고를 대상 = 마스터 로직 전체(기존 로직 목록 찾기) · 내보내는 알약 = 그 로직 출력(흐름이면 출력 상자 · 옛 로직이면 노무비 · 재료비 · 경비 · 계) · 받는 알약 = 그 로직 입력 · 값 = 서버 계산 길(4). +12. **권한** — 흐름도 고치기 · 저장 = 시스템 관리자(공용 로직 포함) · 그 밖은 보기만. +13. **안전** — 서버는 환경 변수 `AISLO_MASTER_DIR` 가 있으면 그 폴더를 마스터 폴더로(없으면 지금대로 · sub4). 화면 저장 시험은 자기 폴더 `tmp/master_copy_36/`(resources/master_data 사본) + 그 변수로 띄운 자기 서버에서만 · 끝나면 git status 로 resources/master_data 안 바뀜 확인. 시험(pytest)은 사본 폴더 · 마스터 내용에 기대지 않음. +14. **정리** — DataFlow · CostFlow 폴더 그대로 · 흐름도 시작 화면 견본 목록 지움 · 시험이 쓰는 견본은 `resources/tester/fixtures_df/` 로 옮기고 그 밖 견본 지움 · 시험판 입구 지움. + +## 창 + +| 창 | 맡음 | +| --- | --- | +| sub7 | 36-0b 선 조건 틀(gate conds · join · valueChoices · Rule 식 · 시험) 먼저 → 36-2 변환(M02_DF_Convert.py · 자동 배치 · 전수 대조) | +| sub2 | 36-0 상세 내용 함수 판 「식 보기」 · 칸 연결 표 · 칸 색 · M02_DF_FormulaView.ts · 엑셀 「조건 만들기」(spreadsheet_condition.ts) · LinkPanel 조건 여러 줄 · 값 목록 고르기 | +| sub6 | 36-1 조사(옛 식 문법 전수 · 파서 위치 · 못 옮기는 꼴 · 계산 길을 부르는 곳) → sub7 에 넘김 → 36-7 정리(견본 · 시험 옮김) | +| sub8 | 36-3 M02_DF_Panel.ts(패널 꼴 · 너비 · 접기 · 크게 보기) · 선 이름표 여러 조건 | +| sub4 | 36-4 서버 — 「흐름」 칸 저장 · 검사 · /api/m01/logic/flow · 계산 길 갈래 · 신규 GX · 복사에 흐름 · 권한 · AISLO_MASTER_DIR · Compute 여러 조건 | +| sub5 | 36-4 왼쪽 보기(흐름 로직의 단가산출 상세 · 텍스트 수식 · 시험 계산) | +| sub3 | 36-6 다른 로직 입력 = 마스터 로직 고르기 · 값 | +| sub1 | 36-5 단추 · 시험판 지움 · LogicLab 두 칸 배치 · 패널 잇기 · drafts 저장 하나 → 36-8 끝 검증 | From 533826d613be4ca768b19b2fe9639b5aa7beea3c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 23:21:47 +0900 Subject: [PATCH 18/18] =?UTF-8?q?chore(M02):=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=EB=A7=88=EC=8A=A4=ED=84=B0=20=ED=8B=80=20=EA=B3=A0=EC=B9=A8?= =?UTF-8?q?=20=E2=80=94=20=ED=99=94=EB=A9=B4=20=EC=A0=80=EC=9E=A5=2022:44~?= =?UTF-8?q?22:54=20(=EA=B5=AC=EC=A1=B0=EB=AC=BC=EC=A7=91=EA=B3=84=ED=91=9C?= =?UTF-8?q?=20=C2=B7=20=ED=95=AD=EB=AA=A9=203=20=C2=B7=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YAxqrz1iadqdygC6bE3pic --- resources/master_template/area/item3.json | 17 ++++ resources/master_template/inputs/item1.json | 96 +++++++++++++++++++ resources/master_template/material/item3.json | 17 ++++ .../master_template/structure/item3.json | 19 ++++ .../master_template/table/구조물집계표.json | 21 +++- .../master_template/구조물집계표조합.json | 25 ++++- 6 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 resources/master_template/area/item3.json create mode 100644 resources/master_template/inputs/item1.json create mode 100644 resources/master_template/material/item3.json create mode 100644 resources/master_template/structure/item3.json diff --git a/resources/master_template/area/item3.json b/resources/master_template/area/item3.json new file mode 100644 index 000000000..a57a2c5e6 --- /dev/null +++ b/resources/master_template/area/item3.json @@ -0,0 +1,17 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "item3", + "서식": [ + {} + ], + "시트": [ + { + "id": "s1", + "이름": "면적산출", + "칸": {} + } + ], + "활성": "s1", + "면적번": "SA-000003" +} diff --git a/resources/master_template/inputs/item1.json b/resources/master_template/inputs/item1.json new file mode 100644 index 000000000..6d62c3446 --- /dev/null +++ b/resources/master_template/inputs/item1.json @@ -0,0 +1,96 @@ +{ + "판": 1, + "줄": [ + { + "로직": "GC000297", + "입력": "형식", + "자리": "선택" + }, + { + "로직": "GC000297", + "입력": "두께", + "자리": "면적", + "알약": "입력|GC000297|두께" + }, + { + "로직": "GF000156", + "입력": "구분", + "자리": "선택" + }, + { + "로직": "GF000156", + "입력": "믹서", + "자리": "선택" + }, + { + "로직": "GF000156", + "입력": "지역", + "자리": "선택" + }, + { + "로직": "GF000156", + "입력": "재료배합", + "자리": "선택" + }, + { + "로직": "GF000156", + "입력": "골재최대치수", + "자리": "선택" + }, + { + "로직": "GF000156", + "입력": "수중", + "자리": "선택" + }, + { + "로직": "GF000156", + "입력": "모래상태", + "자리": "선택" + }, + { + "로직": "GF000156", + "입력": "자갈상태", + "자리": "선택" + }, + { + "로직": "GF000156", + "입력": "모래단위중량", + "자리": "선택" + }, + { + "로직": "GF000156", + "입력": "자갈단위중량", + "자리": "선택" + }, + { + "로직": "GF000156", + "입력": "자재지역", + "자리": "선택" + }, + { + "로직": "GF000162", + "입력": "포장두께", + "자리": "선택" + }, + { + "로직": "GF000162", + "입력": "운반", + "자리": "선택" + }, + { + "로직": "GF000162", + "입력": "운반감률", + "자리": "선택" + }, + { + "로직": "GF000162", + "입력": "공급감률", + "자리": "선택" + }, + { + "로직": "GF000162", + "입력": "종단경사", + "자리": "선택" + } + ] +} diff --git a/resources/master_template/material/item3.json b/resources/master_template/material/item3.json new file mode 100644 index 000000000..a75069ccd --- /dev/null +++ b/resources/master_template/material/item3.json @@ -0,0 +1,17 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "item3", + "서식": [ + {} + ], + "시트": [ + { + "id": "s1", + "이름": "재료산출", + "칸": {} + } + ], + "활성": "s1", + "재료번": "SM-000003" +} diff --git a/resources/master_template/structure/item3.json b/resources/master_template/structure/item3.json new file mode 100644 index 000000000..c344045ef --- /dev/null +++ b/resources/master_template/structure/item3.json @@ -0,0 +1,19 @@ +{ + "양식": "구조물도면", + "종류": "structure", + "판": 1, + "열": "item3", + "도면": { + "format": 6, + "entities": [], + "layers": [ + { + "id": "기본", + "name": "기본", + "isVisible": true, + "isLocked": false + } + ] + }, + "도번": "SD-000003" +} diff --git a/resources/master_template/table/구조물집계표.json b/resources/master_template/table/구조물집계표.json index 3ba7f70fc..33c51c0f4 100644 --- a/resources/master_template/table/구조물집계표.json +++ b/resources/master_template/table/구조물집계표.json @@ -53,7 +53,7 @@ "id": "item1", "단위": "m", "꼴": "수", - "일위대가": null, + "일위대가": "UA000006", "자재조합": null, "경로": [ "기슭막이", @@ -95,6 +95,25 @@ "ss" ] }, + { + "id": "item3", + "단위": null, + "꼴": "수", + "일위대가": null, + "자재조합": null, + "경로": [ + "기슭막이", + "ㄴㄴ" + ], + "머리": [ + "기슭막이", + "ㄴㄴ", + null, + null, + null, + null + ] + }, { "id": "memo", "머리": [ diff --git a/resources/master_template/구조물집계표조합.json b/resources/master_template/구조물집계표조합.json index 3cf8e81dd..9c261bbf4 100644 --- a/resources/master_template/구조물집계표조합.json +++ b/resources/master_template/구조물집계표조합.json @@ -2,7 +2,7 @@ "그룹": "구조물집계표조합", "판": 1, "비고": "구조물집계표 구조물 열(꼴=수)마다 한 줄 · 서버가 집계표 [저장] 때 맞춤(손으로 고치지 않음) · 키(마스터 SS · 회사 SC{회사}- · 개인 SP{사용자}- + 6자리)는 한 번 주면 안 바뀌고 지운 열의 키는 다시 안 씀 · 옛키 = 옮기기 전 키 · 경로 = 폴더 이름 배열", - "다음": 54, + "다음": 55, "지운번호": [ "01010100", "01010101", @@ -74,7 +74,7 @@ "분류2": "ss", "분류3": "aaa", "단위": "m", - "일위대가": null, + "일위대가": "UA000006", "자재조합": null, "도번": "SD-000001", "면적번": "SA-000001", @@ -106,6 +106,27 @@ "재료번": "SM-000002", "바인딩": null, "펼침": null + }, + { + "키": "SS000054", + "열": "item3", + "경로": [ + "기슭막이", + "ㄴㄴ" + ], + "이름": "기슭막이 ㄴㄴ", + "종류": "기슭막이", + "분류1": "ㄴㄴ", + "분류2": null, + "분류3": null, + "단위": null, + "일위대가": null, + "자재조합": null, + "도번": "SD-000003", + "면적번": "SA-000003", + "재료번": "SM-000003", + "바인딩": null, + "펼침": null } ] }