Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_2
This commit is contained in:
@@ -206,6 +206,25 @@ export async function cfLogicValue(
|
||||
return send("/logic-value", { key, designs });
|
||||
}
|
||||
|
||||
/** 마스터 로직의 흐름 — 줄에 「흐름」 이 있으면 그대로(converted false) · 없으면 옛 로직을 흐름도로 바꿔 읽음(converted true ·
|
||||
* notes = 못 옮긴 곳 쉬운 말 · 파일은 안 바뀜) — `GET /api/m01/logic/flow` */
|
||||
export async function cfLogicFlow(
|
||||
file: string,
|
||||
key: string,
|
||||
): Promise<{ flow: CfLogic; converted: boolean; notes: string[] }> {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`${API_BASE_URL}/m01/logic/flow?${new URLSearchParams({ file, key })}`, {
|
||||
credentials: "include",
|
||||
});
|
||||
} catch {
|
||||
throw new CfError(0, "");
|
||||
}
|
||||
const data = (await response.json().catch(() => ({}))) as { detail?: unknown };
|
||||
if (!response.ok) throw new CfError(response.status, data.detail ?? response.statusText);
|
||||
return data as { flow: CfLogic; converted: boolean; notes: string[] };
|
||||
}
|
||||
|
||||
/** 설계 때 정할 것 — 변수 · 재료 자리 · 다른 로직 받는 알약(프로젝트 일위대가가 쓸 꼴) */
|
||||
export async function cfDesignInputs(key: string): Promise<CfDesign[]> {
|
||||
const qs = new URLSearchParams({ key }).toString();
|
||||
|
||||
@@ -23,6 +23,9 @@ import {
|
||||
restParts,
|
||||
linkLabel,
|
||||
midOf,
|
||||
pillInfo,
|
||||
svgAdd as add,
|
||||
svgLabel as label,
|
||||
pillsOf,
|
||||
resolveDrop,
|
||||
showValue,
|
||||
@@ -46,6 +49,7 @@ interface HostState {
|
||||
panX: number;
|
||||
panY: number;
|
||||
fitted: boolean;
|
||||
fitKey?: number;
|
||||
watch?: ResizeObserver;
|
||||
}
|
||||
|
||||
@@ -79,6 +83,9 @@ export function renderDiagram(
|
||||
fitted: false,
|
||||
};
|
||||
if (opts.selected !== undefined) st.selected = opts.selected;
|
||||
if (opts.fitKey !== undefined && opts.fitKey !== st.fitKey)
|
||||
[st.fitKey, st.fitted] = [opts.fitKey, false];
|
||||
const ro = !!opts.readOnly; // 보기만 — 끌기 · 잇기 · 더하기 · 지우기 막음(고르기 · 흐름 따라가기는 됨)
|
||||
if (opts.selectedPill !== undefined) st.pill = opts.selectedPill;
|
||||
states.set(host, st);
|
||||
const values = opts.values ?? {};
|
||||
@@ -243,7 +250,7 @@ export function renderDiagram(
|
||||
className: "m02-dfdg__ins",
|
||||
children: [
|
||||
...ins.map((p) => pillEl(b, p, "in")),
|
||||
...(b.kind === "function" ? [plusSlot(b)] : []),
|
||||
...(b.kind === "function" && !ro ? [plusSlot(b)] : []),
|
||||
],
|
||||
}),
|
||||
el("div", {
|
||||
@@ -282,13 +289,13 @@ export function renderDiagram(
|
||||
ArrowUp: [0, -step],
|
||||
ArrowDown: [0, step],
|
||||
}[ev.key];
|
||||
if (d && st.selected === b.id) {
|
||||
if (d && st.selected === b.id && !ro) {
|
||||
ev.preventDefault();
|
||||
on.move(b.id, Math.round(b.x + d[0]), Math.round(b.y + d[1]));
|
||||
}
|
||||
});
|
||||
head.addEventListener("pointerdown", (ev) => {
|
||||
if (ev.button === 0 && !spaceDown) dragBox(ev, b, node);
|
||||
if (ev.button === 0 && !spaceDown && !ro) dragBox(ev, b, node);
|
||||
});
|
||||
return node;
|
||||
};
|
||||
@@ -350,15 +357,8 @@ export function renderDiagram(
|
||||
);
|
||||
} else if (st.pill) {
|
||||
const p = st.pill;
|
||||
const mine = (e: CfPort): boolean => e.box === p.box && e.name === p.port;
|
||||
const where = lg.links
|
||||
.filter((l) => mine(p.dir === "in" ? l.to : l.from))
|
||||
.map((l) => endName(p.dir === "in" ? l.from : l.to))
|
||||
.join(", ");
|
||||
const now = `${tt("Value")} ${showValue(values[p.box]?.[p.port]) || "—"}`;
|
||||
const dir = where ? `${tt(p.dir === "in" ? "From" : "To")} ${where}` : "";
|
||||
tools.push(el("span", { text: [p.port, now, dir].filter(Boolean).join(" · ") }));
|
||||
if (p.dir === "out")
|
||||
tools.push(el("span", { text: pillInfo(lg, p, values) }));
|
||||
if (p.dir === "out" && !ro)
|
||||
tools.push(
|
||||
sm(
|
||||
"Connect",
|
||||
@@ -371,9 +371,9 @@ export function renderDiagram(
|
||||
const line = tt("LineOf", { a: endName(selLink.from), b: endName(selLink.to) });
|
||||
tools.push(
|
||||
el("span", { text: rule ? `${line} · ${rule}` : line }),
|
||||
sm("Disconnect", () => on.unlink(selLink.id), "danger"),
|
||||
...(ro ? [] : [sm("Disconnect", () => on.unlink(selLink.id), "danger")]),
|
||||
);
|
||||
} else if (selBox) {
|
||||
} else if (selBox && !ro) {
|
||||
tools.push(sm("Remove", () => void askRemove(selBox), "danger"));
|
||||
}
|
||||
const legend =
|
||||
@@ -388,18 +388,26 @@ export function renderDiagram(
|
||||
className: "m02-dfdg__bar",
|
||||
children: [
|
||||
el("strong", { className: "m02-dfdg__title", text: tt("Title") }),
|
||||
addBtn("input", "AddInput"),
|
||||
addBtn("function", "AddFunc"),
|
||||
addBtn("output", "AddOutput"),
|
||||
...(ro
|
||||
? []
|
||||
: [
|
||||
addBtn("input", "AddInput"),
|
||||
addBtn("function", "AddFunc"),
|
||||
addBtn("output", "AddOutput"),
|
||||
]),
|
||||
el("span", { className: "m02-dfdg__gap" }),
|
||||
...legend,
|
||||
sm("ZoomOut", () => setZoom(st.zoom / 1.2)),
|
||||
zoomLabel,
|
||||
sm("ZoomIn", () => setZoom(st.zoom * 1.2)),
|
||||
sm("Fit", fit),
|
||||
sm("Arrange", () => {
|
||||
for (const [id, p] of Object.entries(arrange(lg))) on.move(id, p.x, p.y);
|
||||
}),
|
||||
...(ro
|
||||
? []
|
||||
: [
|
||||
sm("Arrange", () =>
|
||||
Object.entries(arrange(lg)).forEach(([id, q]) => on.move(id, q.x, q.y)),
|
||||
),
|
||||
]),
|
||||
],
|
||||
});
|
||||
const root = el("div", {
|
||||
@@ -426,7 +434,7 @@ export function renderDiagram(
|
||||
st.pill = null;
|
||||
redraw();
|
||||
}
|
||||
if (ev.key !== "Delete" || (ev.target as HTMLElement).closest("input, textarea")) return;
|
||||
if (ro || ev.key !== "Delete" || (ev.target as HTMLElement).closest("input, textarea")) return;
|
||||
if (selLink) on.unlink(selLink.id);
|
||||
else if (selBox) void askRemove(selBox);
|
||||
});
|
||||
@@ -477,32 +485,6 @@ export function renderDiagram(
|
||||
const right = (dir === "out") === (side === "near");
|
||||
return toWorld(right ? r.right : r.left, r.top + r.height / 2);
|
||||
};
|
||||
const add = <K extends keyof SVGElementTagNameMap>(
|
||||
parent: Element,
|
||||
tag: K,
|
||||
attrs: Record<string, string>,
|
||||
) => {
|
||||
const n = document.createElementNS(SVG, tag);
|
||||
for (const [k, v] of Object.entries(attrs)) n.setAttribute(k, v);
|
||||
parent.append(n);
|
||||
return n;
|
||||
};
|
||||
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", {
|
||||
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)),
|
||||
);
|
||||
return w;
|
||||
};
|
||||
const defs = add(svg, "defs", {});
|
||||
const arrow = add(defs, "marker", {
|
||||
id: "m02-dfdg-arrow",
|
||||
@@ -657,7 +639,7 @@ export function renderDiagram(
|
||||
}
|
||||
|
||||
function dragLink(ev: PointerEvent, from: CfPort): void {
|
||||
if (ev.button !== 0 || spaceDown) return;
|
||||
if (ev.button !== 0 || spaceDown || ro) return;
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
const pill = ev.currentTarget as HTMLElement;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* =============================================================================
|
||||
* M02_DF_Diagram_Parts.ts
|
||||
* 단가산출 흐름도 다이어그램의 순수 부분(DOM 없음) — 글 · 약속 모양 · 떨군 자리 풀기 · 줄 맞추기 ·
|
||||
* 한눈에 맞춤 · 알약 값 글 · 흐름 길 가르기 · 기댐 선 · 끌기 손잡이. 그리기는 `M02_DF_Diagram.ts`.
|
||||
* 한눈에 맞춤 · 알약 값 글 · 흐름 길 가르기 · 기댐 선 · 끌기 손잡이 · SVG 도우미. 그리기는 `M02_DF_Diagram.ts`.
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
@@ -118,6 +118,10 @@ export interface DiagramOpts {
|
||||
trace?: { back?: CfStep[]; forward?: CfStep[] } | null;
|
||||
/** 끊긴 곳 — Eval breaks */
|
||||
breaks?: CfBreak[];
|
||||
/** 보기만 — 끌기 · 잇기 · 더하기 · 지우기 막음 */
|
||||
readOnly?: boolean;
|
||||
/** 바뀌면 다시 맞춤(자리를 옮겨 끼운 뒤 · 새 로직) */
|
||||
fitKey?: number;
|
||||
}
|
||||
|
||||
/* ── 알약 · 자리 ────────────────────────────────────────────────────── */
|
||||
@@ -330,6 +334,25 @@ export function restParts(
|
||||
return { links, pills, boxes };
|
||||
}
|
||||
|
||||
/** 고른 알약 한 줄 — 이름 · 지금 값 · 어디서(받는) / 어디로(내보내는) */
|
||||
export function pillInfo(
|
||||
lg: CfLogic,
|
||||
p: DfPillSel,
|
||||
values: Record<string, Record<string, unknown>>,
|
||||
): string {
|
||||
const name = (e: CfPort): string =>
|
||||
`${lg.boxes.find((b) => b.id === e.box)?.name ?? ""} · ${e.name}`;
|
||||
const mine = (e: CfPort): boolean => e.box === p.box && e.name === p.port;
|
||||
const where = lg.links
|
||||
.filter((l) => mine(p.dir === "in" ? l.to : l.from))
|
||||
.map((l) => name(p.dir === "in" ? l.from : l.to))
|
||||
.join(", ");
|
||||
const now = `${tt("Value")} ${showValue(values[p.box]?.[p.port]) || "—"}`;
|
||||
return [p.port, now, where ? `${tt(p.dir === "in" ? "From" : "To")} ${where}` : ""]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
}
|
||||
|
||||
/* ── 끌기 손잡이 ─────────────────────────────────────────────────────── */
|
||||
|
||||
/** 누른 뒤 5px 넘게 움직이면 끌기 — 끈 뒤 따라오는 click 한 번만 삼킴 */
|
||||
@@ -360,3 +383,37 @@ export function follow(
|
||||
window.addEventListener("pointermove", mv);
|
||||
window.addEventListener("pointerup", up);
|
||||
}
|
||||
|
||||
/* ── 선 그림 도우미(SVG) ────────────────────────────────────────────── */
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
/** SVG 마디 하나 붙이기 */
|
||||
export function svgAdd<K extends keyof SVGElementTagNameMap>(
|
||||
parent: Element,
|
||||
tag: K,
|
||||
attrs: Record<string, string>,
|
||||
): SVGElementTagNameMap[K] {
|
||||
const n = document.createElementNS(SVG_NS, tag);
|
||||
for (const [k, v] of Object.entries(attrs)) n.setAttribute(k, v);
|
||||
parent.append(n);
|
||||
return n;
|
||||
}
|
||||
|
||||
/** 가운데 이름표 — 둥근 바탕 + 글 · 돌려주는 값 = 이름표 폭 */
|
||||
export function svgLabel(parent: Element, x: number, y: number, text: string, cls: string): number {
|
||||
const tag = svgAdd(parent, "g", { class: cls });
|
||||
const box = svgAdd(tag, "rect", { rx: "4", ry: "4" });
|
||||
const t = svgAdd(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)),
|
||||
);
|
||||
return w;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
/* 시작 화면 */
|
||||
.m02-df__start {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: var(--spacing-24);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,12 +47,11 @@ const loadMods = () =>
|
||||
import("./M02_DF_FuncPanel"),
|
||||
import("./M02_DF_OutputPanel"),
|
||||
import("./M02_DF_Summary"),
|
||||
import("./M02_DF_Sample"),
|
||||
import("./M02_DF_InputPanel"),
|
||||
import("./M02_DF_PillPanel"),
|
||||
import("./M02_DF_LinkPanel"),
|
||||
import("./M02_DF_Standards"),
|
||||
]).then(([model, ev, api, diagram, func, output, summary, sample, input, pill, line, std]) => ({
|
||||
]).then(([model, ev, api, diagram, func, output, summary, input, pill, line, std]) => ({
|
||||
model,
|
||||
ev,
|
||||
api,
|
||||
@@ -60,7 +59,6 @@ const loadMods = () =>
|
||||
func,
|
||||
output,
|
||||
summary,
|
||||
sample,
|
||||
input,
|
||||
pill,
|
||||
line,
|
||||
@@ -78,7 +76,6 @@ const TEXT = {
|
||||
Start_New: ["새로 만들기", "New"],
|
||||
Start_Mine: ["내 로직", "My logics"],
|
||||
Start_MineNone: ["아직 없음", "None yet"],
|
||||
Start_Samples: ["견본 — 복사해서 열기", "Samples — open a copy"],
|
||||
Name: ["로직 이름", "Logic name"],
|
||||
NamePlaceholder: ["예: 메쌓기 내 방식", "e.g. my dry stone wall"],
|
||||
Unit: ["결과 단위", "Result unit"],
|
||||
@@ -217,7 +214,6 @@ function openDataFlow(): void {
|
||||
paper = null;
|
||||
strip.replaceChildren();
|
||||
const mineHost = el("div", { className: "m02-df__list", children: [muted(te("Loading"))] });
|
||||
const sampleHost = el("div", { className: "m02-df__list" });
|
||||
body.replaceChildren(
|
||||
el("div", {
|
||||
className: "m02-df__start",
|
||||
@@ -232,28 +228,10 @@ function openDataFlow(): void {
|
||||
mineHost,
|
||||
],
|
||||
}),
|
||||
el("section", {
|
||||
className: "m02-df__start-col",
|
||||
children: [el("h3", { text: te("Start_Samples") }), sampleHost],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
const M = await mods;
|
||||
sampleHost.replaceChildren(
|
||||
...M.sample.sampleList().map((s) =>
|
||||
listRow(
|
||||
s.name,
|
||||
s.resultUnit,
|
||||
() =>
|
||||
run(async (M2) => {
|
||||
const lg = M2.sample.sampleLoad(s.id);
|
||||
if (lg) await showPaper(M2, lg, false);
|
||||
}),
|
||||
s.note,
|
||||
),
|
||||
),
|
||||
);
|
||||
try {
|
||||
const mine = (await M.api.cfList()).filter((x: CfListItem) => !x.sample);
|
||||
mineHost.replaceChildren(
|
||||
@@ -379,7 +357,7 @@ function openDataFlow(): void {
|
||||
if (sig === logicSig) return;
|
||||
logicSig = sig;
|
||||
const get = async (key: string): Promise<CfLogic | null> =>
|
||||
key.startsWith("DF-") ? M.api.cfGet(key) : M.sample.sampleLoad(key);
|
||||
key.startsWith("DF-") ? M.api.cfGet(key) : Promise.resolve(null);
|
||||
void loadLogics(lg, get).then((got) => {
|
||||
if (sig !== logicSig) return;
|
||||
logics = got.logics;
|
||||
|
||||
@@ -236,19 +236,28 @@ export function checkMaterial(el: CfElementRef, others: CfElementRef[]): string
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 다른 로직에서 읽어 둘 것 — 내보내는 알약 = 출력 상자마다 · 받는 알약 = 설계 때 정할 것(변수 · 재료 자리) */
|
||||
/** 다른 로직에서 읽어 둘 것 — 내보내는 알약 = 출력 상자마다 · 받는 알약 = 설계 때 정할 것(변수 · 재료 자리 · 이어지지 않은 다른 로직 받는 알약)
|
||||
* examples = 받는 알약마다 그 로직 자기 예시값(선이 없을 때 값 차례의 끝) */
|
||||
export function logicRefOf(key: string, other: CfLogic): CfLogicRef {
|
||||
const outputs = other.boxes
|
||||
.filter((b) => b.kind === "output")
|
||||
.map((b) => ({ name: b.name, unit: b.output?.unit || b.unit || "-" }));
|
||||
const designs: { name: string; unit: string }[] = [];
|
||||
const examples: Record<string, number | string> = {};
|
||||
const addDesign = (name: string, unit: string, example?: number | string | null): void => {
|
||||
designs.push({ name, unit: unit || "-" });
|
||||
if (example !== null && example !== undefined && example !== "") examples[name] = example;
|
||||
};
|
||||
for (const b of other.boxes) {
|
||||
if (b.kind !== "input" || !b.input) continue;
|
||||
if (b.input.mode === "variable")
|
||||
for (const v of b.input.variables) designs.push({ name: v.name, unit: v.unit || "-" });
|
||||
for (const e of b.input.elements)
|
||||
if (e.mode === "choice" || e.mode === "rule")
|
||||
designs.push({ name: e.slot || e.name, unit: e.unit || "-" });
|
||||
for (const v of b.input?.variables ?? b.func?.variables ?? [])
|
||||
addDesign(v.name, v.unit, v.example);
|
||||
if (b.input?.mode === "group")
|
||||
for (const e of b.input.elements)
|
||||
if (e.mode === "choice" || e.mode === "rule") addDesign(e.slot || e.name, e.unit);
|
||||
if (b.input?.mode === "logic")
|
||||
for (const d of b.input.logic?.designs ?? [])
|
||||
if (!other.links.some((l) => l.to.box === b.id && l.to.name === d.name))
|
||||
addDesign(d.name, d.unit, b.input.logic?.examples?.[d.name]);
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
return {
|
||||
@@ -256,9 +265,93 @@ export function logicRefOf(key: string, other: CfLogic): CfLogicRef {
|
||||
name: other.name,
|
||||
outputs: outputs.filter((o) => !seen.has(o.name) && !!seen.add(o.name)),
|
||||
designs: designs.filter((d) => (seen.has(`d:${d.name}`) ? false : !!seen.add(`d:${d.name}`))),
|
||||
...(Object.keys(examples).length ? { examples } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/* ── 마스터 로직 고르기 — 목록 · 거름 · 쪽 · 순환 막음(36-6) ── */
|
||||
|
||||
/** 마스터 로직 한 줄 — M01 `fetchLogics` 의 줄(자기 시험 로직 DF-… 도 같은 꼴로 합침) */
|
||||
export interface MasterLogicBrief {
|
||||
file: string;
|
||||
구분: string;
|
||||
상세구분: string;
|
||||
키: string;
|
||||
원문번호: string;
|
||||
이름: string;
|
||||
결과단위: string;
|
||||
blocked?: boolean;
|
||||
reasons?: string[];
|
||||
}
|
||||
|
||||
/** 찾기 — 띄어쓰기로 나눈 말을 모두 포함(이름 · 원문번호 · 키 · 구분 · 상세구분 · 띄어쓰기는 가리지 않음) · 원문(구분) · 장(상세구분) 거름 */
|
||||
export function filterLogics(
|
||||
items: MasterLogicBrief[],
|
||||
q: { text: string; book?: string; chapter?: string },
|
||||
): MasterLogicBrief[] {
|
||||
const words = q.text.split(/\s+/).map(squash).filter(Boolean);
|
||||
return items.filter((i) => {
|
||||
if (q.book && i.구분 !== q.book) return false;
|
||||
if (q.chapter && i.상세구분 !== q.chapter) return false;
|
||||
const hay = squash(`${i.이름} ${i.원문번호} ${i.키} ${i.구분} ${i.상세구분}`);
|
||||
return words.every((w) => hay.includes(w));
|
||||
});
|
||||
}
|
||||
|
||||
/** 원문(구분) 목록 · 장(상세구분) 목록 — 나온 차례대로 */
|
||||
export function logicBooks(items: MasterLogicBrief[]): string[] {
|
||||
return [...new Set(items.map((i) => i.구분).filter(Boolean))];
|
||||
}
|
||||
export function logicChapters(items: MasterLogicBrief[], book: string): string[] {
|
||||
return [
|
||||
...new Set(
|
||||
items
|
||||
.filter((i) => !book || i.구분 === book)
|
||||
.map((i) => i.상세구분)
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/** 옛 로직을 흐름도로 바꿔 읽은 것은 출력 상자가 비목별이라 노무비 · 재료비 · 경비 · 계 알약 넷으로 보임 */
|
||||
export const COST_OUTPUTS = ["노무비", "재료비", "경비", "계"] as const;
|
||||
|
||||
/** 마스터 로직 고른 것 → 다른 로직 입력 값 — 흐름이면 출력 상자 · 변환이면 비용 넷 */
|
||||
export function masterRef(
|
||||
key: string,
|
||||
flow: CfLogic,
|
||||
converted: boolean,
|
||||
resultUnit: string,
|
||||
): CfLogicRef {
|
||||
const ref = logicRefOf(key, flow);
|
||||
return converted
|
||||
? { ...ref, outputs: COST_OUTPUTS.map((name) => ({ name, unit: resultUnit || "-" })) }
|
||||
: ref;
|
||||
}
|
||||
|
||||
/** 이 로직이 자기 자신을 부르거나 돌고 도나 — `load` 가 로직을 주면 입력 상자의 「다른 로직」 을 따라가 봄(깊이 8) */
|
||||
export async function wouldCycle(
|
||||
selfKey: string | null,
|
||||
startKey: string,
|
||||
load: (key: string) => Promise<CfLogic | null>,
|
||||
depth = 8,
|
||||
): Promise<boolean> {
|
||||
if (!selfKey) return false;
|
||||
const seen = new Set<string>();
|
||||
const walk = async (key: string, left: number): Promise<boolean> => {
|
||||
if (key === selfKey) return true;
|
||||
if (seen.has(key) || left <= 0) return false;
|
||||
seen.add(key);
|
||||
const lg = await load(key).catch(() => null);
|
||||
for (const b of lg?.boxes ?? []) {
|
||||
const next = b.input?.mode === "logic" ? b.input.logic?.key : undefined;
|
||||
if (next && (await walk(next, left - 1))) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
return walk(startKey, depth);
|
||||
}
|
||||
|
||||
/** 다른 로직 입력 넣기 · 빼기(null) — 빼면 변수 상태로 돌아가고 이 상자의 알약에 닿은 선은 모두 지움 */
|
||||
export function setLogicInput(lg: CfLogic, id: string, ref: CfLogicRef | null): CfLogic {
|
||||
return {
|
||||
|
||||
@@ -1,42 +1,87 @@
|
||||
/* =============================================================================
|
||||
* M02_DF_InputPanel_Logic.ts
|
||||
* 입력 판 — 입력 갈래 「다른 로직」: 저장한 흐름도 로직(또는 견본)을 찾아 고르면
|
||||
* 그 로직의 출력 = 내보내는 알약 · 설계 때 정할 것 = 받는 알약(이어 주지 않으면 그 로직의 예시값)
|
||||
* 입력 판 — 입력 갈래 「다른 로직」: 마스터 로직 전체(원문 · 장 · 찾기 · 쪽)와 내가 만든 흐름도 로직 가운데 하나를 고르면
|
||||
* 그 로직의 출력 = 내보내는 알약 · 설계 때 정할 것 = 받는 알약(이어 주지 않으면 그 로직의 예시값) · 값은 서버 계산 길
|
||||
* 옛 로직은 서버가 흐름도로 바꿔 읽음(`GET /api/m01/logic/flow` · 파일은 안 바뀜) · 자기 자신 · 돌고 도는 로직은 못 고름
|
||||
* 글 [한, 영] 짝은 아래 TEXT · 단추는 공용 작은 크기
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements";
|
||||
import { cfGet, cfList, cfPlain, type CfListItem } from "./M02_DF_Api";
|
||||
import type { CfLogic } from "./M02_DF_Model";
|
||||
import { logicRefOf, setKind, setLogicInput } from "./M02_DF_InputPanel_Build";
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
createSelectField,
|
||||
el,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { fetchLogics } from "../../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import { cfGet, cfList, cfLogicFlow, cfLogicValue, cfPlain } from "./M02_DF_Api";
|
||||
import type { CfLogic, CfLogicRef } from "./M02_DF_Model";
|
||||
import {
|
||||
filterLogics,
|
||||
logicBooks,
|
||||
logicChapters,
|
||||
logicRefOf,
|
||||
masterRef,
|
||||
pageRange,
|
||||
setKind,
|
||||
setLogicInput,
|
||||
wouldCycle,
|
||||
type MasterLogicBrief,
|
||||
} from "./M02_DF_InputPanel_Build";
|
||||
|
||||
const TEXT = {
|
||||
Help: [
|
||||
"저장한 로직이나 견본을 불러와 그 결과를 이 상자의 알약으로 씀",
|
||||
"Use a saved logic or sample as an input",
|
||||
"마스터 로직이나 내가 만든 흐름도 로직을 불러와 그 결과를 이 상자의 알약으로 씀",
|
||||
"Use any master logic or a logic of mine as an input",
|
||||
],
|
||||
Search: ["로직 이름으로 찾기", "Search by name"],
|
||||
Search: ["로직 이름 · 번호 찾기 — 띄어 쓰면 모두 들어간 것", "Search — all words must match"],
|
||||
Book: ["원문", "Source"],
|
||||
Chapter: ["장", "Chapter"],
|
||||
All: ["전체", "All"],
|
||||
Loading: ["찾는 중…", "Searching…"],
|
||||
None: ["찾는 로직 없음", "No logic found"],
|
||||
Sample: ["견본", "Sample"],
|
||||
Total: ["전체 {n} 개 중 {a}~{b} · {p} / {m} 쪽", "{a}-{b} of {n} · page {p} / {m}"],
|
||||
Prev: ["이전", "Prev"],
|
||||
Next: ["다음", "Next"],
|
||||
Mine: ["내가 만든 것", "Mine"],
|
||||
Blocked: ["읽을 수 없음", "Unreadable"],
|
||||
Chosen: ["고른 로직", "Chosen logic"],
|
||||
Outs: ["내보내는 알약 — 그 로직의 출력", "Pills it sends — its outputs"],
|
||||
Converted: [
|
||||
"옛 로직을 흐름도로 바꿔 읽음 — 파일은 안 바뀜",
|
||||
"Old logic read as a flow — the file is unchanged",
|
||||
],
|
||||
Notes: ["못 옮긴 곳", "Not carried over"],
|
||||
Outs: ["내보내는 알약 — 그 로직의 출력 · 값 · 단위", "Pills it sends — its outputs"],
|
||||
Designs: [
|
||||
"받는 알약 — 그 로직의 설계 때 정할 것(이어 주지 않으면 그 로직의 예시값)",
|
||||
"받는 알약 — 그 로직의 설계 때 정할 것(이어 주지 않으면 예시값)",
|
||||
"Pills it takes — its design inputs",
|
||||
],
|
||||
NoOut: ["출력 상자가 없음", "No output box"],
|
||||
NoDesign: ["설계 때 정할 것이 없음", "Nothing to decide"],
|
||||
Change: ["다른 로직 고르기", "Pick another"],
|
||||
Remove: ["빼기", "Remove"],
|
||||
Self: ["이 로직 자신은 못 씀", "Can't use itself"],
|
||||
Self: ["이 로직 자신은 못 고름", "Can't pick itself"],
|
||||
Cycle: [
|
||||
"돌고 도는 로직이 됨 — 이 로직을 부르는 로직은 못 고름",
|
||||
"That would loop — a logic that uses this one can't be picked",
|
||||
],
|
||||
NoFlow: [
|
||||
"이 로직의 흐름을 아직 못 읽음 — 서버 흐름 받기 길이 없음",
|
||||
"Can't read this logic's flow yet",
|
||||
],
|
||||
Failed: ["불러오지 못함", "Could not load"],
|
||||
Wait: ["값 셈 중…", "Calculating…"],
|
||||
} as const;
|
||||
|
||||
type TKey = keyof typeof TEXT;
|
||||
const t = (key: TKey): string => TEXT[key][currentLanguageIndex] as string;
|
||||
const t = (key: TKey, v: Record<string, string | number> = {}): string =>
|
||||
Object.entries(v).reduce(
|
||||
(s, [k, x]) => s.replace(`{${k}}`, String(x)),
|
||||
TEXT[key][currentLanguageIndex] as string,
|
||||
);
|
||||
|
||||
const fmt = (n: number): string => Number(n.toPrecision(8)).toLocaleString("ko-KR");
|
||||
|
||||
const plain = (e: unknown): string => {
|
||||
try {
|
||||
@@ -46,6 +91,40 @@ const plain = (e: unknown): string => {
|
||||
}
|
||||
};
|
||||
|
||||
const PAGE = 12;
|
||||
const MINE = "흐름도";
|
||||
|
||||
/** 마스터 로직 목록 한 번 + 내 흐름도 로직(견본은 뺌) */
|
||||
let cache: Promise<MasterLogicBrief[]> | null = null;
|
||||
const loadAll = (): Promise<MasterLogicBrief[]> =>
|
||||
(cache ??= Promise.all([fetchLogics(), cfList().catch(() => [])])
|
||||
.then(([master, mine]) => [
|
||||
...mine
|
||||
.filter((i) => !i.sample)
|
||||
.map((i): MasterLogicBrief => ({
|
||||
file: MINE,
|
||||
구분: MINE,
|
||||
상세구분: t("Mine"),
|
||||
키: i.key,
|
||||
원문번호: "",
|
||||
이름: i.name,
|
||||
결과단위: i.resultUnit,
|
||||
})),
|
||||
...master,
|
||||
])
|
||||
.catch((e: unknown) => ((cache = null), Promise.reject(e))));
|
||||
|
||||
/** 고른 로직에서 읽은 것 — 옛 로직을 바꿔 읽었나 · 못 옮긴 곳(화면에만 · 저장 안 함) */
|
||||
const readNotes = new Map<string, { converted: boolean; notes: string[] }>();
|
||||
|
||||
/** 로직 하나를 읽어 흐름도로 — 내 흐름도 로직 = 서버 저장 그대로 · 마스터 = 흐름 받기 길 */
|
||||
async function flowOf(
|
||||
item: Pick<MasterLogicBrief, "file" | "키">,
|
||||
): Promise<{ flow: CfLogic; converted: boolean; notes: string[] }> {
|
||||
if (item.file === MINE) return { flow: await cfGet(item.키), converted: false, notes: [] };
|
||||
return cfLogicFlow(item.file, item.키);
|
||||
}
|
||||
|
||||
/** 다른 로직 입력 판 */
|
||||
export function renderLogicInput(
|
||||
host: HTMLElement,
|
||||
@@ -55,32 +134,86 @@ export function renderLogicInput(
|
||||
): void {
|
||||
const ref = lg.boxes.find((b) => b.id === boxId)?.input?.logic;
|
||||
if (ref) return void showChosen(host, lg, boxId, ref, on);
|
||||
|
||||
const q = createInputField({ type: "search", placeholder: t("Search"), compact: true });
|
||||
const list = el("div", { className: "m02-dfip__list" });
|
||||
let all: CfListItem[] = [];
|
||||
const draw = (): void => {
|
||||
const word = q.input.value.trim().toLowerCase();
|
||||
const rows = all.filter(
|
||||
(i) => i.key !== lg.key && (!word || i.name.toLowerCase().includes(word)),
|
||||
const all = { value: "", text: t("All") };
|
||||
let book = "";
|
||||
let chapter = "";
|
||||
let page = 1;
|
||||
let items: MasterLogicBrief[] = [];
|
||||
const bookBox = createSelectField({
|
||||
label: t("Book"),
|
||||
options: [all],
|
||||
value: "",
|
||||
compact: true,
|
||||
onChange: (v) => ((book = v), (chapter = ""), (page = 1), fillChapters(), draw()),
|
||||
});
|
||||
const chapterBox = createSelectField({
|
||||
label: t("Chapter"),
|
||||
options: [all],
|
||||
value: "",
|
||||
compact: true,
|
||||
onChange: (v) => ((chapter = v), (page = 1), draw()),
|
||||
});
|
||||
const fillChapters = (): void =>
|
||||
chapterBox.setOptions(
|
||||
[all, ...logicChapters(items, book).map((c) => ({ value: c, text: c }))],
|
||||
chapter,
|
||||
);
|
||||
const list = el("div", { className: "m02-dfip__list" });
|
||||
const info = el("span", { className: "m02-dfip__muted" });
|
||||
const prev = createButton({
|
||||
label: t("Prev"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => ((page -= 1), draw()),
|
||||
});
|
||||
const next = createButton({
|
||||
label: t("Next"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => ((page += 1), draw()),
|
||||
});
|
||||
|
||||
const draw = (): void => {
|
||||
const rows = filterLogics(
|
||||
items.filter((i) => i.키 !== lg.key), // 자기 자신은 목록에서 뺌
|
||||
{ text: q.input.value, book, chapter },
|
||||
);
|
||||
const range = pageRange(rows.length, page, PAGE);
|
||||
page = Math.min(page, range.pages);
|
||||
const shown = rows.slice((page - 1) * PAGE, page * PAGE);
|
||||
info.textContent = t("Total", {
|
||||
n: rows.length,
|
||||
a: range.from,
|
||||
b: range.to,
|
||||
p: page,
|
||||
m: range.pages,
|
||||
});
|
||||
prev.disabled = page <= 1;
|
||||
next.disabled = page >= range.pages;
|
||||
list.replaceChildren(
|
||||
...(rows.length
|
||||
? rows.map((i) => {
|
||||
...(shown.length
|
||||
? shown.map((i) => {
|
||||
const b = el("button", {
|
||||
className: "m02-dfip__row",
|
||||
attrs: { type: "button" },
|
||||
attrs: {
|
||||
type: "button",
|
||||
...(i.blocked ? { disabled: "true", title: (i.reasons ?? []).join(" · ") } : {}),
|
||||
},
|
||||
children: [
|
||||
el("span", {
|
||||
className: "m02-dfip__name",
|
||||
children: [
|
||||
el("strong", { text: i.name }),
|
||||
el("strong", { text: i.이름 }),
|
||||
el("span", { className: "m02-dfip__muted", text: ` ${i.원문번호}` }),
|
||||
el("span", {
|
||||
className: "m02-dfip__tag",
|
||||
text: i.sample ? t("Sample") : t("Mine"),
|
||||
text: i.blocked ? t("Blocked") : i.file === MINE ? t("Mine") : i.상세구분,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
el("span", { className: "m02-dfip__muted", text: i.resultUnit }),
|
||||
el("span", { className: "m02-dfip__muted", text: i.결과단위 }),
|
||||
],
|
||||
});
|
||||
b.addEventListener("click", () => void pick(i));
|
||||
@@ -89,56 +222,114 @@ export function renderLogicInput(
|
||||
: [el("p", { className: "m02-dfip__muted", text: t("None") })]),
|
||||
);
|
||||
};
|
||||
const pick = async (i: CfListItem): Promise<void> => {
|
||||
|
||||
/** 순환 검사에 쓰는 읽기 — 목록에서 파일을 찾아 흐름을 읽음 */
|
||||
const loadForCycle = async (key: string): Promise<CfLogic | null> => {
|
||||
const it = items.find((x) => x.키 === key);
|
||||
return it ? (await flowOf(it)).flow : null;
|
||||
};
|
||||
|
||||
const pick = async (i: MasterLogicBrief): Promise<void> => {
|
||||
try {
|
||||
const other = await cfGet(i.key);
|
||||
const next = setLogicInput(lg, boxId, logicRefOf(i.key, other));
|
||||
if (i.키 === lg.key) return void showToast(t("Self"), "error");
|
||||
if (await wouldCycle(lg.key, i.키, loadForCycle)) return void showToast(t("Cycle"), "error");
|
||||
const got = await flowOf(i);
|
||||
readNotes.set(i.키, { converted: got.converted, notes: got.notes });
|
||||
const picked = got.converted
|
||||
? masterRef(i.키, got.flow, true, i.결과단위)
|
||||
: { ...logicRefOf(i.키, got.flow), name: i.이름 };
|
||||
const done = setLogicInput(lg, boxId, picked);
|
||||
// 기본 이름이면 고른 로직 이름으로
|
||||
on.change({
|
||||
...next,
|
||||
boxes: next.boxes.map((b) =>
|
||||
b.id === boxId && /^(입력|새)/.test(b.name) ? { ...b, name: other.name || b.name } : b,
|
||||
...done,
|
||||
boxes: done.boxes.map((b) =>
|
||||
b.id === boxId && /^(입력|새)/.test(b.name) ? { ...b, name: i.이름 || b.name } : b,
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
showToast(plain(e), "error");
|
||||
showToast((e as { status?: number }).status === 404 ? t("NoFlow") : plain(e), "error");
|
||||
}
|
||||
};
|
||||
q.input.addEventListener("input", draw);
|
||||
|
||||
let timer = 0;
|
||||
q.input.addEventListener("input", () => {
|
||||
window.clearTimeout(timer);
|
||||
timer = window.setTimeout(() => ((page = 1), draw()), 200);
|
||||
});
|
||||
list.replaceChildren(el("p", { className: "m02-dfip__muted", text: t("Loading") }));
|
||||
host.replaceChildren(el("p", { className: "m02-dfip__muted", text: t("Help") }), q.root, list);
|
||||
void cfList().then(
|
||||
(items) => ((all = items), draw()),
|
||||
host.replaceChildren(
|
||||
el("p", { className: "m02-dfip__muted", text: t("Help") }),
|
||||
el("div", { className: "m02-dfip__bar", children: [bookBox.root, chapterBox.root, q.root] }),
|
||||
info,
|
||||
list,
|
||||
el("div", { className: "m02-dfip__chips", children: [prev, next] }),
|
||||
);
|
||||
void loadAll().then(
|
||||
(rows) => {
|
||||
items = rows;
|
||||
bookBox.setOptions([all, ...logicBooks(rows).map((b) => ({ value: b, text: b }))], "");
|
||||
fillChapters();
|
||||
draw();
|
||||
},
|
||||
(e) => list.replaceChildren(el("p", { className: "m02-dfip__muted", text: plain(e) })),
|
||||
);
|
||||
}
|
||||
|
||||
/** 고른 로직 — 내보내는 알약 · 받는 알약 */
|
||||
/** 고른 로직 — 내보내는 알약(값 · 단위) · 받는 알약(예시값) */
|
||||
function showChosen(
|
||||
host: HTMLElement,
|
||||
lg: CfLogic,
|
||||
boxId: string,
|
||||
ref: NonNullable<CfLogic["boxes"][number]["input"]>["logic"],
|
||||
ref: CfLogicRef,
|
||||
on: { change(lg: CfLogic): void },
|
||||
): void {
|
||||
if (!ref) return;
|
||||
const pillLine = (p: { name: string; unit: string }): HTMLElement =>
|
||||
const read = readNotes.get(ref.key);
|
||||
const values = new Map<string, HTMLElement>();
|
||||
const errors = el("p", { className: "m02-dfip__muted is-bad" });
|
||||
const line = (name: string, unit: string, valueNode?: HTMLElement, extra = ""): HTMLElement =>
|
||||
el("div", {
|
||||
className: "m02-dfip__item",
|
||||
children: [
|
||||
el("span", { className: "m02-dfip__name", text: p.name }),
|
||||
el("span", { className: "m02-dfip__muted", text: p.unit || "-" }),
|
||||
el("span", { className: "m02-dfip__name", text: name }),
|
||||
...(valueNode
|
||||
? [valueNode]
|
||||
: extra
|
||||
? [el("span", { className: "m02-dfip__muted", text: extra })]
|
||||
: []),
|
||||
el("span", { className: "m02-dfip__muted", text: unit || "-" }),
|
||||
],
|
||||
});
|
||||
host.replaceChildren(
|
||||
el("h5", { text: `${t("Chosen")} — ${ref.name ?? ref.key}` }),
|
||||
...(read?.converted
|
||||
? [
|
||||
el("p", { className: "m02-dfip__muted", text: t("Converted") }),
|
||||
...(read.notes.length
|
||||
? [
|
||||
el("details", {
|
||||
children: [
|
||||
el("summary", { text: `${t("Notes")} · ${read.notes.length}` }),
|
||||
...read.notes.map((n) => el("p", { text: n })),
|
||||
],
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
]
|
||||
: []),
|
||||
el("h5", { text: `${t("Outs")} · ${ref.outputs.length}` }),
|
||||
...(ref.outputs.length
|
||||
? ref.outputs.map(pillLine)
|
||||
? ref.outputs.map((o) => {
|
||||
const v = el("strong", { className: "m02-dfip__value", text: "—" });
|
||||
values.set(o.name, v);
|
||||
return line(o.name, o.unit, v);
|
||||
})
|
||||
: [el("p", { className: "m02-dfip__muted", text: t("NoOut") })]),
|
||||
errors,
|
||||
el("h5", { text: `${t("Designs")} · ${ref.designs.length}` }),
|
||||
...(ref.designs.length
|
||||
? ref.designs.map(pillLine)
|
||||
? ref.designs.map((d) =>
|
||||
line(d.name, d.unit, undefined, String(ref.examples?.[d.name] ?? "")),
|
||||
)
|
||||
: [el("p", { className: "m02-dfip__muted", text: t("NoDesign") })]),
|
||||
el("div", {
|
||||
className: "m02-dfip__chips",
|
||||
@@ -158,4 +349,17 @@ function showChosen(
|
||||
],
|
||||
}),
|
||||
);
|
||||
// 값 — 서버 계산 길(받는 알약 예시값으로)
|
||||
errors.textContent = t("Wait");
|
||||
void cfLogicValue(ref.key, ref.examples ?? {}).then(
|
||||
(r) => {
|
||||
for (const [name, node] of values) {
|
||||
const v = r.outputs[name] ?? (r.cost as unknown as Record<string, unknown>)[name];
|
||||
if (typeof v === "number") node.textContent = fmt(v);
|
||||
else if (typeof v === "string") node.textContent = v;
|
||||
}
|
||||
errors.textContent = r.errors.join(" · ");
|
||||
},
|
||||
(e) => (errors.textContent = plain(e)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
/* =============================================================================
|
||||
* M02_DF_Paper.ts
|
||||
* 흐름도 고치는 핵심 — 다이어그램 · 상세 내용(상자 · 알약 · 선 판) · 원가 · 검증 칸 · 실행 취소
|
||||
* Entry 시험판의 고치는 화면(showPaper)을 옮겨 적은 공용 조각 — 오른쪽 패널(M02_DF_Panel) · 「크게 보기」 가 함께 씀.
|
||||
* 저장 · 지우기 · 이름 · 결과 단위는 없음(부르는 쪽 페이지 몫) · 바뀔 때마다 onChange(객체 그대로).
|
||||
* readOnly = 끌기 · 고치기 막고 보기 · 흐름 따라가기 · 검증 칸 예시값만(onChange 안 부름).
|
||||
* 판 DOM 은 한 번 만들고 자리만 옮겨 끼움(패널 ↔ 크게 보기) — 고친 것 · 고른 것 그대로.
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, createInputField, el } from "@ui/ui_template_elements";
|
||||
import * as api from "./M02_DF_Api";
|
||||
import { renderDiagram, type DfPillSel } from "./M02_DF_Diagram";
|
||||
import * as ev from "./M02_DF_Eval";
|
||||
import type { CfBreak, CfEvalOptions, CfEvalResult, CfValues } from "./M02_DF_Eval";
|
||||
import { renderFuncPanel } from "./M02_DF_FuncPanel";
|
||||
import { renderInputPanel } from "./M02_DF_InputPanel";
|
||||
import { renderLinkPanel } from "./M02_DF_LinkPanel";
|
||||
import * as model from "./M02_DF_Model";
|
||||
import type { CfKind, CfLogic, CfPort, CfStep } from "./M02_DF_Model";
|
||||
import { renderOutputPanel } from "./M02_DF_OutputPanel";
|
||||
import { renderPillPanel } from "./M02_DF_PillPanel";
|
||||
import { renderSummary } from "./M02_DF_Summary";
|
||||
import type { CfSel } from "./M02_DF_Verify";
|
||||
import {
|
||||
detailFrame,
|
||||
drawHead,
|
||||
drawWarnings,
|
||||
fetchValues,
|
||||
loadLogics,
|
||||
queriesOf,
|
||||
type DfPick,
|
||||
} from "./M02_DF_Entry_Detail";
|
||||
import "./M02_DF_Entry.css";
|
||||
|
||||
const TEXT = {
|
||||
Undo: ["실행 취소", "Undo"],
|
||||
Color: ["상자 색", "Box colour"],
|
||||
BoxName: ["상자 이름", "Box name"],
|
||||
NameTaken: ["「{v}」 — 같은 이름의 상자가 있음", "“{v}” — another box has this name"],
|
||||
Values: ["입력 값을 못 받음 — {v}", "Couldn't get input values — {v}"],
|
||||
Logics: ["다른 로직을 못 읽음 — {v}", "Couldn't read other logics — {v}"],
|
||||
ReadOnly: ["보기만 — 고치기는 시스템 관리자", "View only — editing is for system admins"],
|
||||
} as const;
|
||||
|
||||
const tp = (key: keyof typeof TEXT, v = ""): string =>
|
||||
(TEXT[key][currentLanguageIndex as 0 | 1] ?? TEXT[key][0]).replace("{v}", v);
|
||||
|
||||
export interface PaperOpts {
|
||||
logic: CfLogic;
|
||||
readOnly?: boolean;
|
||||
/** 고칠 때마다 — 객체 그대로(부르는 쪽 캐시에 쌓음) */
|
||||
onChange(lg: CfLogic): void;
|
||||
/** 다른 로직 입력이 부르는 로직 — 없으면 흐름도 저장본(DF-) 만 */
|
||||
logics?(key: string): Promise<CfLogic | null>;
|
||||
}
|
||||
|
||||
export interface Paper {
|
||||
/** 다이어그램 자리 · 상세 내용 · 원가 · 검증 · 알림 줄 · 실행 취소 단추 */
|
||||
parts: {
|
||||
diagram: HTMLElement;
|
||||
detail: HTMLElement;
|
||||
summary: HTMLElement;
|
||||
note: HTMLElement;
|
||||
undo: HTMLButtonElement;
|
||||
};
|
||||
get(): CfLogic;
|
||||
/** 밖에서 바꿈(고친 것 버리기 등) — 실행 취소 기록은 비움 */
|
||||
set(lg: CfLogic): void;
|
||||
undo(): void;
|
||||
/** 다이어그램을 다시 맞춤(자리를 옮겨 끼운 뒤) */
|
||||
refit(): void;
|
||||
redraw(): void;
|
||||
}
|
||||
|
||||
export function createPaper(opts: PaperOpts): Paper {
|
||||
const readOnly = !!opts.readOnly;
|
||||
let lg = opts.logic;
|
||||
let stack: CfLogic[] = [];
|
||||
let pick: DfPick = null;
|
||||
let values: CfValues = {};
|
||||
let valueSig = "";
|
||||
let logics: NonNullable<CfEvalOptions["logics"]> = {};
|
||||
let logicSig = "";
|
||||
let logicNotes: string[] = [];
|
||||
let notes: string[] = [];
|
||||
let broken: CfBreak[] = [];
|
||||
let result: CfEvalResult;
|
||||
let fitKey = 0;
|
||||
const pickedBox = (): string | null => (pick?.kind === "box" ? pick.id : null);
|
||||
|
||||
const note = el("p", { className: "m02-df__note", attrs: { role: "status" } });
|
||||
const diagramHost = el("div", { className: "m02-dfp__diagram" });
|
||||
const summaryHost = el("div", { className: "m02-dfp__summary" });
|
||||
const colorBox = el("input", {
|
||||
className: "m02-df__color-input",
|
||||
attrs: { type: "color", "aria-label": tp("Color") },
|
||||
});
|
||||
colorBox.addEventListener("change", () => {
|
||||
const id = pickedBox();
|
||||
if (id) edit(model.setColor(lg, id, colorBox.value));
|
||||
});
|
||||
const colorRow = el("label", {
|
||||
className: "m02-df__color",
|
||||
children: [el("span", { text: tp("Color") }), colorBox],
|
||||
});
|
||||
const boxName = createInputField({ label: tp("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 = tp("NameTaken", next);
|
||||
if (!next || taken || next === box.name) return void (boxName.input.value = box.name);
|
||||
edit(model.updateBox(lg, box.id, { name: next }));
|
||||
});
|
||||
// 판마다 제 상태를 쥠 — 종류마다 따로 두고 숨기기만
|
||||
const funcHost = el("div");
|
||||
const outHost = el("div");
|
||||
const inHost = el("div");
|
||||
const linkHost = el("div");
|
||||
const pillHost = el("div");
|
||||
const detail = detailFrame();
|
||||
detail.settings.append(boxName.root, colorRow);
|
||||
// 보기만 — 판 안 칸 · 단추를 한 번에 막음(고르기 · 흐름 따라가기는 다이어그램에서 됨)
|
||||
const lock = el("fieldset", {
|
||||
className: "m02-dfp__lock",
|
||||
children: [inHost, funcHost, outHost, linkHost, pillHost],
|
||||
});
|
||||
lock.disabled = readOnly;
|
||||
detail.body.append(lock);
|
||||
if (readOnly) detail.root.prepend(el("p", { className: "m02-df__muted", text: tp("ReadOnly") }));
|
||||
|
||||
const evalNow = (): CfEvalResult => {
|
||||
try {
|
||||
return ev.evalLogic(lg, values, { logics });
|
||||
} catch (e) {
|
||||
const cost = { 노무비: 0, 재료비: 0, 경비: 0, 값들: {}, 계: 0 };
|
||||
return { ports: {}, outputs: {}, cost, errors: [api.cfPlain(e)] };
|
||||
}
|
||||
};
|
||||
|
||||
/** 고침 — readOnly 면 무시 · 기록에 쌓고 onChange */
|
||||
const edit = (next: CfLogic): void => {
|
||||
if (readOnly || next === lg) return;
|
||||
stack = [...stack, lg];
|
||||
lg = next;
|
||||
opts.onChange(lg);
|
||||
redraw();
|
||||
};
|
||||
/** 검증 칸 예시값 — readOnly 에서도 됨(보기용 · onChange 안 부름) */
|
||||
const editExample = (next: CfLogic): void => {
|
||||
if (!readOnly) return edit(next);
|
||||
lg = next;
|
||||
redraw();
|
||||
};
|
||||
const editFromDiagram = (next: CfLogic): void => {
|
||||
edit(next);
|
||||
setTimeout(redraw);
|
||||
};
|
||||
|
||||
const refreshValues = (): void => {
|
||||
const pill = (p: CfPort): unknown => result?.ports[p.box]?.[p.name] ?? null;
|
||||
const sig = JSON.stringify(queriesOf(lg, pill, model.elementPort).map((a) => a.q));
|
||||
if (sig === valueSig) return;
|
||||
valueSig = sig;
|
||||
void fetchValues(api.cfValues, lg, pill, model.elementPort).then(
|
||||
(got) => {
|
||||
if (sig !== valueSig) return;
|
||||
values = got.values;
|
||||
notes = got.errors.length ? [tp("Values", got.errors.join(" · "))] : [];
|
||||
redraw();
|
||||
},
|
||||
(e: unknown) => {
|
||||
notes = [tp("Values", api.cfPlain(e))];
|
||||
redraw();
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const refreshLogics = (): void => {
|
||||
const sig = JSON.stringify(lg.boxes.map((b) => b.input?.logic?.key ?? ""));
|
||||
if (sig === logicSig) return;
|
||||
logicSig = sig;
|
||||
const get =
|
||||
opts.logics ?? (async (key: string) => (key.startsWith("DF-") ? api.cfGet(key) : null));
|
||||
void loadLogics(lg, get).then((got) => {
|
||||
if (sig !== logicSig) return;
|
||||
logics = got.logics;
|
||||
logicNotes = got.errors.length ? [tp("Logics", got.errors.join(" · "))] : [];
|
||||
redraw();
|
||||
});
|
||||
};
|
||||
|
||||
const choose = (sel: CfSel): void => {
|
||||
const box = lg.boxes.find((b) => b.id === sel.box);
|
||||
pick = sel.link
|
||||
? { kind: "link", id: sel.link }
|
||||
: box && sel.port
|
||||
? { kind: "pill", box: box.id, port: sel.port, dir: model.portDir(box, sel.port) }
|
||||
: box
|
||||
? { kind: "box", id: box.id }
|
||||
: null;
|
||||
redraw();
|
||||
};
|
||||
|
||||
/** 흐름 길 — 알약 = 그 알약에서 앞뒤 · 선 = 보낸 알약 앞 · 받는 알약 뒤 */
|
||||
const traceNow = (): { back: CfStep[]; forward: CfStep[] } | null => {
|
||||
try {
|
||||
if (pick?.kind === "pill") {
|
||||
const p = { box: pick.box, name: pick.port };
|
||||
return { back: model.traceBack(lg, p), forward: model.traceForward(lg, p) };
|
||||
}
|
||||
const link = lg.links.find((l) => pick?.kind === "link" && l.id === pick.id);
|
||||
if (link)
|
||||
return { back: model.traceBack(lg, link.from), forward: model.traceForward(lg, link.to) };
|
||||
} catch {
|
||||
/* 따라갈 수 없으면 칠하지 않음 */
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const diagramOn = {
|
||||
select: (id: string | null, pill?: DfPillSel): void => {
|
||||
pick = pill
|
||||
? { kind: "pill", ...pill }
|
||||
: !id
|
||||
? null
|
||||
: lg.links.some((l) => l.id === id)
|
||||
? { kind: "link", id }
|
||||
: { kind: "box", id };
|
||||
setTimeout(redraw);
|
||||
},
|
||||
move: (id: string, x: number, y: number): void => edit(model.moveBox(lg, id, x, y)),
|
||||
link: (from: CfPort, to: CfPort): string | null => {
|
||||
const got = model.link(lg, from, to);
|
||||
if (got.why) return got.why;
|
||||
editFromDiagram(got.lg);
|
||||
return null;
|
||||
},
|
||||
unlink: (id: string): void => editFromDiagram(model.unlink(lg, id)),
|
||||
add: (kind: CfKind, x: number, y: number): void => {
|
||||
const got = model.addBox(lg, kind, x, y);
|
||||
pick = { kind: "box", id: got.box.id };
|
||||
editFromDiagram(got.lg);
|
||||
},
|
||||
remove: (id: string): void => {
|
||||
if (pickedBox() === id) pick = null;
|
||||
editFromDiagram(model.removeBox(lg, id));
|
||||
},
|
||||
};
|
||||
|
||||
const drawPanel = (): void => {
|
||||
const boxId = pickedBox();
|
||||
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 || readOnly;
|
||||
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")
|
||||
renderLinkPanel(linkHost, lg, pick.id, { change: edit }, { values, logics });
|
||||
if (pick?.kind === "pill")
|
||||
renderPillPanel(
|
||||
pillHost,
|
||||
lg,
|
||||
pick.box,
|
||||
pick.port,
|
||||
{ pick: (id) => choose({ box: id }) },
|
||||
{ values, logics },
|
||||
);
|
||||
drawWarnings(detail, pick, broken);
|
||||
if (box && /^#[0-9a-f]{6}$/i.test(box.color)) colorBox.value = box.color;
|
||||
funcHost.hidden = box?.kind !== "function";
|
||||
outHost.hidden = box?.kind !== "output";
|
||||
inHost.hidden = box?.kind !== "input";
|
||||
if (box?.kind === "input") renderInputPanel(inHost, lg, box.id, { change: edit });
|
||||
if (box?.kind === "function") renderFuncPanel(funcHost, lg, box.id, { change: edit });
|
||||
if (box?.kind === "output")
|
||||
renderOutputPanel(outHost, lg, box.id, { change: edit }, { result });
|
||||
};
|
||||
|
||||
const redraw = (): void => {
|
||||
result = evalNow();
|
||||
refreshValues();
|
||||
refreshLogics();
|
||||
const linkId = pick?.kind === "link" ? pick.id : "";
|
||||
if (linkId && !lg.links.some((l) => l.id === linkId)) pick = null;
|
||||
try {
|
||||
broken = ev.breaks(lg, values, { logics });
|
||||
} catch {
|
||||
broken = result.breaks ?? [];
|
||||
}
|
||||
undo.disabled = readOnly || !stack.length;
|
||||
renderDiagram(diagramHost, lg, diagramOn, {
|
||||
values: result.ports,
|
||||
selected: pick?.kind === "box" || pick?.kind === "link" ? pick.id : null,
|
||||
selectedPill:
|
||||
pick?.kind === "pill" ? { box: pick.box, port: pick.port, dir: pick.dir } : null,
|
||||
trace: traceNow(),
|
||||
breaks: broken,
|
||||
readOnly,
|
||||
fitKey,
|
||||
});
|
||||
drawPanel();
|
||||
renderSummary(summaryHost, lg, result, { change: editExample, select: choose }, { values });
|
||||
note.textContent = [...notes, ...logicNotes, ...result.errors].slice(0, 4).join("\n");
|
||||
};
|
||||
|
||||
const doUndo = (): void => {
|
||||
const back = stack[stack.length - 1];
|
||||
if (!back || readOnly) return;
|
||||
stack = stack.slice(0, -1);
|
||||
lg = back;
|
||||
opts.onChange(lg);
|
||||
redraw();
|
||||
};
|
||||
const undo = createButton({ label: tp("Undo"), variant: "ghost", size: "sm", onClick: doUndo });
|
||||
|
||||
redraw();
|
||||
return {
|
||||
parts: { diagram: diagramHost, detail: detail.root, summary: summaryHost, note, undo },
|
||||
get: () => lg,
|
||||
set: (next) => {
|
||||
lg = next;
|
||||
stack = [];
|
||||
pick = null;
|
||||
fitKey += 1;
|
||||
redraw();
|
||||
},
|
||||
undo: doUndo,
|
||||
refit: () => {
|
||||
fitKey += 1;
|
||||
redraw();
|
||||
},
|
||||
redraw,
|
||||
};
|
||||
}
|
||||
@@ -102,3 +102,8 @@ export function showRounded(v: number, r?: CfRound): string {
|
||||
maximumFractionDigits: Math.max(0, r.places),
|
||||
});
|
||||
}
|
||||
|
||||
/** 원가 합계 기본 끝수(표를 읽어 오기 전에는 계금 1원 미만 버림) — 화면이 바로 쓰는 값 */
|
||||
let totalNow: CfRound = { places: 0, method: "버림" };
|
||||
void defaultTotalRound().then((r) => (totalNow = r));
|
||||
export const totalRoundNow = (): CfRound => totalNow;
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
/* =============================================================================
|
||||
* M02_DF_Sample.ts
|
||||
* 단가산출 흐름도 견본 — 처음 열었을 때 바로 보고 따라 만들 견본(`_sample/*.json` · 손으로 짬)
|
||||
* ① 돌쌓기 메쌓기(인력) — 변수 · 품표 · 증가율표 · 인력 → 표 찾기 · 높이 할증(직렬) · 품 × 노임(병렬) → 노무비
|
||||
* ② 지반 가르기 — 같은 굴착량이 「조건 통과」 선 둘(지반 = 토사 / ≠ 토사)로 굴삭기 · 브레이커에 감 → 경비
|
||||
* ③ 흄관 관경 조건 찾기 — 재료 자리(조건 찾기 · 규격에 관경 포함) → 본수 × 단가 → 재료비
|
||||
* ④ 다른 로직 잇기 — 작업량 로직(⑤) 출력 → 장비 시간 → 경비
|
||||
* ⑤ 덤프트럭 시간당 작업량(값) — ④ 가 부르는 다른 로직
|
||||
* 견본은 읽기만 — `sampleLoad` 는 「복사해서 열기」 사본(key 없음 · 저장하면 `_data` 에 새 번호).
|
||||
* 입력 값(노임 · 표 · 재료)은 견본에 안 들고 열 때 · 저장 때 마스터에서 받음.
|
||||
* 모양 = `M02_DF_Model` CfLogic(선 규칙 · 재료 방식 · 다른 로직 입력 포함) · 알약 이름 규칙 — 정함
|
||||
* `docs/raw/M01_쉬운단가/흐름도_정함.md` 1 · 4~7 · 12. 이 폴더째 지워도 됨.
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import type { CfLogic } from "./M02_DF_Model";
|
||||
|
||||
const FILES = import.meta.glob<CfLogic>("./_sample/*.json", { eager: true, import: "default" });
|
||||
|
||||
/** 견본마다 한 줄 설명 — [한, 영] */
|
||||
const NOTE: Record<string, readonly [string, string]> = {
|
||||
masonry: [
|
||||
"변수 · 품표 · 증가율표 · 인력 → 표 찾기 · 높이 할증 → 품 × 노임 → 노무비",
|
||||
"Variables, tables and labor → lookup, height surcharge → crew × wage → labor cost",
|
||||
],
|
||||
ground: [
|
||||
"지반이 토사면 굴삭기로 · 아니면 브레이커로 — 조건 통과 선 둘 → 경비",
|
||||
"Soil goes to the excavator, rock to the breaker — two gate lines → expense",
|
||||
],
|
||||
hume: [
|
||||
"관경으로 흄관 품목을 찾음(재료 자리 · 규격에 관경 포함) → 본수 × 단가 → 재료비",
|
||||
"Pipe size picks the hume pipe item (spec contains size) → count × price → material cost",
|
||||
],
|
||||
linked: [
|
||||
"작업량 로직의 출력을 받아 장비 시간 → 경비",
|
||||
"Takes the output of a productivity logic → machine hours → expense",
|
||||
],
|
||||
value: [
|
||||
"q · f · E · Cm → 60 × q × f × E ÷ Cm → 값(시간당 작업량)",
|
||||
"q, f, E, Cm → 60 × q × f × E ÷ Cm → value (output per hour)",
|
||||
],
|
||||
};
|
||||
|
||||
/** 보여 줄 차례 */
|
||||
const ORDER = ["masonry", "ground", "hume", "linked", "value"];
|
||||
|
||||
export interface CfSampleItem {
|
||||
/** 파일 이름(확장자 뺌) */
|
||||
id: string;
|
||||
name: string;
|
||||
resultUnit: string;
|
||||
/** 쉬운 말 한 줄 */
|
||||
note: string;
|
||||
}
|
||||
|
||||
const idOf = (path: string): string => path.replace(/^.*\//, "").replace(/\.json$/, "");
|
||||
const byId = (): Map<string, CfLogic> =>
|
||||
new Map(Object.entries(FILES).map(([path, lg]) => [idOf(path), lg]));
|
||||
|
||||
/** 견본 목록 — 정한 차례 먼저 · 나머지는 이름 차례 */
|
||||
export function sampleList(): CfSampleItem[] {
|
||||
const all = byId();
|
||||
const rank = (id: string): number => (ORDER.includes(id) ? ORDER.indexOf(id) : ORDER.length);
|
||||
return [...all.entries()]
|
||||
.sort(([a, x], [b, y]) => rank(a) - rank(b) || x.name.localeCompare(y.name))
|
||||
.map(([id, lg]) => ({
|
||||
id,
|
||||
name: lg.name,
|
||||
resultUnit: lg.resultUnit,
|
||||
note: NOTE[id]?.[currentLanguageIndex === 1 ? 1 : 0] ?? "",
|
||||
}));
|
||||
}
|
||||
|
||||
/** 견본 하나 「복사해서 열기」 — 깊은 사본 · key 없음(저장하면 서버가 새 번호) · 없으면 null */
|
||||
export function sampleLoad(id: string): CfLogic | null {
|
||||
const lg = byId().get(id);
|
||||
return lg ? { ...(JSON.parse(JSON.stringify(lg)) as CfLogic), key: null } : null;
|
||||
}
|
||||
@@ -1,423 +0,0 @@
|
||||
{
|
||||
"key": "SAMPLE-ground",
|
||||
"name": "견본 · 지반에 따라 굴삭기 / 브레이커",
|
||||
"resultUnit": "원/㎥",
|
||||
"boxes": [
|
||||
{
|
||||
"id": "in-ground",
|
||||
"kind": "input",
|
||||
"name": "지반 · 굴착량",
|
||||
"color": "#2f80ed",
|
||||
"unit": "-",
|
||||
"x": 40,
|
||||
"y": 60,
|
||||
"input": {
|
||||
"mode": "variable",
|
||||
"elements": [],
|
||||
"variables": [
|
||||
{
|
||||
"name": "지반",
|
||||
"type": "choice",
|
||||
"unit": "-",
|
||||
"example": "토사",
|
||||
"choices": ["토사", "암"]
|
||||
},
|
||||
{
|
||||
"name": "굴착량",
|
||||
"type": "number",
|
||||
"unit": "㎥",
|
||||
"example": 100
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "in-cost",
|
||||
"kind": "input",
|
||||
"name": "장비 시간당 경비",
|
||||
"color": "#2f80ed",
|
||||
"unit": "원/시간",
|
||||
"x": 40,
|
||||
"y": 300,
|
||||
"input": {
|
||||
"mode": "variable",
|
||||
"elements": [],
|
||||
"variables": [
|
||||
{
|
||||
"name": "굴삭기 시간당 경비",
|
||||
"type": "number",
|
||||
"unit": "원/시간",
|
||||
"example": 60000
|
||||
},
|
||||
{
|
||||
"name": "브레이커 시간당 경비",
|
||||
"type": "number",
|
||||
"unit": "원/시간",
|
||||
"example": 80000
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-exc",
|
||||
"kind": "function",
|
||||
"name": "굴삭기로 파기",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "원",
|
||||
"x": 380,
|
||||
"y": 40,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "s-fn-exc",
|
||||
"이름": "굴삭기로 파기",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "굴착량"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · 지반이 토사일 때만 옴"
|
||||
},
|
||||
"A3": {
|
||||
"값": "시간당 경비"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값 · 원/시간"
|
||||
},
|
||||
"A4": {
|
||||
"값": "시간당 작업량"
|
||||
},
|
||||
"B4": {
|
||||
"값": 30
|
||||
},
|
||||
"C4": {
|
||||
"값": "㎥/시간 · 고침"
|
||||
},
|
||||
"A5": {
|
||||
"값": "작업 시간"
|
||||
},
|
||||
"B5": {
|
||||
"식": "B2/B4"
|
||||
},
|
||||
"C5": {
|
||||
"값": "시간"
|
||||
},
|
||||
"A6": {
|
||||
"값": "굴삭기 경비"
|
||||
},
|
||||
"B6": {
|
||||
"식": "B5*B3"
|
||||
},
|
||||
"C6": {
|
||||
"값": "원"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "굴착량",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "시간당 경비",
|
||||
"cell": "B3"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B6",
|
||||
"name": "굴삭기 경비",
|
||||
"unit": "원"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-brk",
|
||||
"kind": "function",
|
||||
"name": "브레이커로 깨기",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "원",
|
||||
"x": 380,
|
||||
"y": 300,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "s-fn-brk",
|
||||
"이름": "브레이커로 깨기",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "굴착량"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · 지반이 토사가 아닐 때만 옴"
|
||||
},
|
||||
"A3": {
|
||||
"값": "시간당 경비"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값 · 원/시간"
|
||||
},
|
||||
"A4": {
|
||||
"값": "시간당 작업량"
|
||||
},
|
||||
"B4": {
|
||||
"값": 8
|
||||
},
|
||||
"C4": {
|
||||
"값": "㎥/시간 · 고침"
|
||||
},
|
||||
"A5": {
|
||||
"값": "작업 시간"
|
||||
},
|
||||
"B5": {
|
||||
"식": "B2/B4"
|
||||
},
|
||||
"C5": {
|
||||
"값": "시간"
|
||||
},
|
||||
"A6": {
|
||||
"값": "브레이커 경비"
|
||||
},
|
||||
"B6": {
|
||||
"식": "B5*B3"
|
||||
},
|
||||
"C6": {
|
||||
"값": "원"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "굴착량",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "시간당 경비",
|
||||
"cell": "B3"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B6",
|
||||
"name": "브레이커 경비",
|
||||
"unit": "원"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-sum",
|
||||
"kind": "function",
|
||||
"name": "1㎥ 경비",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "원/㎥",
|
||||
"x": 720,
|
||||
"y": 170,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "s-fn-sum",
|
||||
"이름": "1㎥ 경비",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "굴삭기 경비"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · 안 오면 빈 칸 = 0"
|
||||
},
|
||||
"A3": {
|
||||
"값": "브레이커 경비"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값 · 안 오면 빈 칸 = 0"
|
||||
},
|
||||
"A4": {
|
||||
"값": "굴착량"
|
||||
},
|
||||
"C4": {
|
||||
"값": "받는 값 · ㎥"
|
||||
},
|
||||
"A5": {
|
||||
"값": "1㎥ 경비"
|
||||
},
|
||||
"B5": {
|
||||
"식": "(B2+B3)/B4"
|
||||
},
|
||||
"C5": {
|
||||
"값": "둘 중 온 것 ÷ 굴착량"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "굴삭기 경비",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "브레이커 경비",
|
||||
"cell": "B3"
|
||||
},
|
||||
{
|
||||
"port": "굴착량",
|
||||
"cell": "B4"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B5",
|
||||
"name": "1㎥ 경비",
|
||||
"unit": "원/㎥"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "out-exp",
|
||||
"kind": "output",
|
||||
"name": "경비",
|
||||
"color": "#27ae60",
|
||||
"unit": "원/㎥",
|
||||
"x": 1040,
|
||||
"y": 170,
|
||||
"output": {
|
||||
"cost": "경비",
|
||||
"unit": "원/㎥",
|
||||
"round": {
|
||||
"places": 1,
|
||||
"method": "버림"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "l1",
|
||||
"from": {
|
||||
"box": "in-ground",
|
||||
"name": "굴착량"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-exc",
|
||||
"name": "굴착량"
|
||||
},
|
||||
"rule": {
|
||||
"kind": "gate",
|
||||
"from": {
|
||||
"box": "in-ground",
|
||||
"name": "지반"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "토사"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l2",
|
||||
"from": {
|
||||
"box": "in-ground",
|
||||
"name": "굴착량"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-brk",
|
||||
"name": "굴착량"
|
||||
},
|
||||
"rule": {
|
||||
"kind": "gate",
|
||||
"from": {
|
||||
"box": "in-ground",
|
||||
"name": "지반"
|
||||
},
|
||||
"op": "≠",
|
||||
"value": "토사"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l3",
|
||||
"from": {
|
||||
"box": "in-cost",
|
||||
"name": "굴삭기 시간당 경비"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-exc",
|
||||
"name": "시간당 경비"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l4",
|
||||
"from": {
|
||||
"box": "in-cost",
|
||||
"name": "브레이커 시간당 경비"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-brk",
|
||||
"name": "시간당 경비"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l5",
|
||||
"from": {
|
||||
"box": "fn-exc",
|
||||
"name": "굴삭기 경비"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-sum",
|
||||
"name": "굴삭기 경비"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l6",
|
||||
"from": {
|
||||
"box": "fn-brk",
|
||||
"name": "브레이커 경비"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-sum",
|
||||
"name": "브레이커 경비"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l7",
|
||||
"from": {
|
||||
"box": "in-ground",
|
||||
"name": "굴착량"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-sum",
|
||||
"name": "굴착량"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l8",
|
||||
"from": {
|
||||
"box": "fn-sum",
|
||||
"name": "1㎥ 경비"
|
||||
},
|
||||
"to": {
|
||||
"box": "out-exp",
|
||||
"name": "값"
|
||||
}
|
||||
}
|
||||
],
|
||||
"userElements": []
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
{
|
||||
"key": "SAMPLE-hume",
|
||||
"name": "견본 · 흄관 관경 조건 찾기",
|
||||
"resultUnit": "원/m",
|
||||
"boxes": [
|
||||
{
|
||||
"id": "in-size",
|
||||
"kind": "input",
|
||||
"name": "관경 · 연장",
|
||||
"color": "#2f80ed",
|
||||
"unit": "-",
|
||||
"x": 40,
|
||||
"y": 60,
|
||||
"input": {
|
||||
"mode": "variable",
|
||||
"elements": [],
|
||||
"variables": [
|
||||
{
|
||||
"name": "관경",
|
||||
"type": "choice",
|
||||
"unit": "mm",
|
||||
"example": 600,
|
||||
"choices": [400, 450, 500, 600, 700, 800, 900, 1000, 1100, 1200]
|
||||
},
|
||||
{
|
||||
"name": "연장",
|
||||
"type": "number",
|
||||
"unit": "m",
|
||||
"example": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "in-pipe",
|
||||
"kind": "input",
|
||||
"name": "흄관",
|
||||
"color": "#2f80ed",
|
||||
"unit": "본",
|
||||
"x": 40,
|
||||
"y": 300,
|
||||
"input": {
|
||||
"mode": "group",
|
||||
"group": "재료",
|
||||
"variables": [],
|
||||
"elements": [
|
||||
{
|
||||
"ref": "MT014991",
|
||||
"name": "흄관",
|
||||
"spec": "Φ600mm",
|
||||
"unit": "본",
|
||||
"mode": "rule",
|
||||
"scope": {
|
||||
"구분": "관·이음관",
|
||||
"상세구분": "흄관",
|
||||
"찾는말": "접속흄관"
|
||||
},
|
||||
"rule": {
|
||||
"field": "규격",
|
||||
"op": "포함",
|
||||
"from": {
|
||||
"box": "in-size",
|
||||
"name": "관경"
|
||||
}
|
||||
},
|
||||
"default": "MT014991",
|
||||
"slot": "흄관"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-pipe",
|
||||
"kind": "function",
|
||||
"name": "흄관 재료비",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "원/m",
|
||||
"x": 380,
|
||||
"y": 160,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "s-fn-pipe",
|
||||
"이름": "흄관 재료비",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "연장"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · m"
|
||||
},
|
||||
"A3": {
|
||||
"값": "흄관 단가"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값 · 원/본(관경으로 찾은 품목)"
|
||||
},
|
||||
"A4": {
|
||||
"값": "1본 길이"
|
||||
},
|
||||
"B4": {
|
||||
"값": 2.5
|
||||
},
|
||||
"C4": {
|
||||
"값": "m · 고침"
|
||||
},
|
||||
"A5": {
|
||||
"값": "본수"
|
||||
},
|
||||
"B5": {
|
||||
"식": "ROUNDUP(B2/B4,0)"
|
||||
},
|
||||
"C5": {
|
||||
"값": "올림"
|
||||
},
|
||||
"A6": {
|
||||
"값": "1m 재료비"
|
||||
},
|
||||
"B6": {
|
||||
"식": "B5*B3/B2"
|
||||
},
|
||||
"C6": {
|
||||
"값": "본수 × 단가 ÷ 연장"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "연장",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "흄관 단가",
|
||||
"cell": "B3"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B6",
|
||||
"name": "1m 재료비",
|
||||
"unit": "원/m"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "out-mat",
|
||||
"kind": "output",
|
||||
"name": "재료비",
|
||||
"color": "#27ae60",
|
||||
"unit": "원/m",
|
||||
"x": 720,
|
||||
"y": 160,
|
||||
"output": {
|
||||
"cost": "재료비",
|
||||
"unit": "원/m",
|
||||
"round": {
|
||||
"places": 1,
|
||||
"method": "버림"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "l1",
|
||||
"from": {
|
||||
"box": "in-size",
|
||||
"name": "연장"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-pipe",
|
||||
"name": "연장"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l2",
|
||||
"from": {
|
||||
"box": "in-pipe",
|
||||
"name": "흄관"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-pipe",
|
||||
"name": "흄관 단가"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l3",
|
||||
"from": {
|
||||
"box": "fn-pipe",
|
||||
"name": "1m 재료비"
|
||||
},
|
||||
"to": {
|
||||
"box": "out-mat",
|
||||
"name": "값"
|
||||
}
|
||||
}
|
||||
],
|
||||
"userElements": []
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
{
|
||||
"key": "SAMPLE-linked",
|
||||
"name": "견본 · 다른 로직 잇기(작업량 → 장비 시간)",
|
||||
"resultUnit": "원/㎥",
|
||||
"boxes": [
|
||||
{
|
||||
"id": "in-logic",
|
||||
"kind": "input",
|
||||
"name": "덤프트럭 시간당 작업량",
|
||||
"color": "#2f80ed",
|
||||
"unit": "㎥/시간",
|
||||
"x": 40,
|
||||
"y": 60,
|
||||
"input": {
|
||||
"mode": "logic",
|
||||
"elements": [],
|
||||
"variables": [],
|
||||
"logic": {
|
||||
"key": "value",
|
||||
"name": "견본 · 덤프트럭 시간당 작업량(값)",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "시간당 작업량",
|
||||
"unit": "㎥/시간"
|
||||
}
|
||||
],
|
||||
"designs": [
|
||||
{
|
||||
"name": "q",
|
||||
"unit": "㎥"
|
||||
},
|
||||
{
|
||||
"name": "f",
|
||||
"unit": "-"
|
||||
},
|
||||
{
|
||||
"name": "E",
|
||||
"unit": "-"
|
||||
},
|
||||
{
|
||||
"name": "Cm",
|
||||
"unit": "분"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "in-var",
|
||||
"kind": "input",
|
||||
"name": "운반량 · 경비",
|
||||
"color": "#2f80ed",
|
||||
"unit": "-",
|
||||
"x": 40,
|
||||
"y": 300,
|
||||
"input": {
|
||||
"mode": "variable",
|
||||
"elements": [],
|
||||
"variables": [
|
||||
{
|
||||
"name": "운반량",
|
||||
"type": "number",
|
||||
"unit": "㎥",
|
||||
"example": 120
|
||||
},
|
||||
{
|
||||
"name": "덤프트럭 시간당 경비",
|
||||
"type": "number",
|
||||
"unit": "원/시간",
|
||||
"example": 50000
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-hours",
|
||||
"kind": "function",
|
||||
"name": "운반 시간",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "시간",
|
||||
"x": 380,
|
||||
"y": 60,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "s-fn-hours",
|
||||
"이름": "운반 시간",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "시간당 작업량"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · 다른 로직 출력"
|
||||
},
|
||||
"A3": {
|
||||
"값": "운반량"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값 · ㎥"
|
||||
},
|
||||
"A4": {
|
||||
"값": "운반 시간"
|
||||
},
|
||||
"B4": {
|
||||
"식": "B3/B2"
|
||||
},
|
||||
"C4": {
|
||||
"값": "운반량 ÷ 시간당 작업량"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "시간당 작업량",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "운반량",
|
||||
"cell": "B3"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B4",
|
||||
"name": "운반 시간",
|
||||
"unit": "시간",
|
||||
"round": {
|
||||
"places": 2,
|
||||
"method": "반올림"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-cost",
|
||||
"kind": "function",
|
||||
"name": "1㎥ 운반 경비",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "원/㎥",
|
||||
"x": 700,
|
||||
"y": 180,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "s-fn-cost",
|
||||
"이름": "1㎥ 운반 경비",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "운반 시간"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · 시간"
|
||||
},
|
||||
"A3": {
|
||||
"값": "시간당 경비"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값 · 원/시간"
|
||||
},
|
||||
"A4": {
|
||||
"값": "운반량"
|
||||
},
|
||||
"C4": {
|
||||
"값": "받는 값 · ㎥"
|
||||
},
|
||||
"A5": {
|
||||
"값": "1㎥ 운반 경비"
|
||||
},
|
||||
"B5": {
|
||||
"식": "B2*B3/B4"
|
||||
},
|
||||
"C5": {
|
||||
"값": "시간 × 경비 ÷ 운반량"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "운반 시간",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "시간당 경비",
|
||||
"cell": "B3"
|
||||
},
|
||||
{
|
||||
"port": "운반량",
|
||||
"cell": "B4"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B5",
|
||||
"name": "1㎥ 운반 경비",
|
||||
"unit": "원/㎥"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "out-exp",
|
||||
"kind": "output",
|
||||
"name": "경비",
|
||||
"color": "#27ae60",
|
||||
"unit": "원/㎥",
|
||||
"x": 1020,
|
||||
"y": 180,
|
||||
"output": {
|
||||
"cost": "경비",
|
||||
"unit": "원/㎥",
|
||||
"round": {
|
||||
"places": 1,
|
||||
"method": "버림"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "l1",
|
||||
"from": {
|
||||
"box": "in-logic",
|
||||
"name": "시간당 작업량"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-hours",
|
||||
"name": "시간당 작업량"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l2",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "운반량"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-hours",
|
||||
"name": "운반량"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l3",
|
||||
"from": {
|
||||
"box": "fn-hours",
|
||||
"name": "운반 시간"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-cost",
|
||||
"name": "운반 시간"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l4",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "덤프트럭 시간당 경비"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-cost",
|
||||
"name": "시간당 경비"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l5",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "운반량"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-cost",
|
||||
"name": "운반량"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l6",
|
||||
"from": {
|
||||
"box": "fn-cost",
|
||||
"name": "1㎥ 운반 경비"
|
||||
},
|
||||
"to": {
|
||||
"box": "out-exp",
|
||||
"name": "값"
|
||||
}
|
||||
}
|
||||
],
|
||||
"userElements": []
|
||||
}
|
||||
@@ -1,785 +0,0 @@
|
||||
{
|
||||
"key": "SAMPLE-masonry",
|
||||
"name": "견본 · 돌쌓기 메쌓기(인력)",
|
||||
"resultUnit": "원/㎡",
|
||||
"boxes": [
|
||||
{
|
||||
"id": "in-var",
|
||||
"kind": "input",
|
||||
"name": "쌓기 조건",
|
||||
"color": "#2f80ed",
|
||||
"unit": "-",
|
||||
"x": 40,
|
||||
"y": 40,
|
||||
"input": {
|
||||
"mode": "variable",
|
||||
"elements": [],
|
||||
"variables": [
|
||||
{
|
||||
"name": "뒷길이",
|
||||
"type": "choice",
|
||||
"unit": "㎝",
|
||||
"example": 30,
|
||||
"choices": [25, 30, 35, 45, 55, 60, 75]
|
||||
},
|
||||
{
|
||||
"name": "돌",
|
||||
"type": "choice",
|
||||
"unit": "-",
|
||||
"example": "깬돌",
|
||||
"choices": ["깬잡석", "견치돌", "깬돌", "호박돌 및 야면석"]
|
||||
},
|
||||
{
|
||||
"name": "쌓기",
|
||||
"type": "choice",
|
||||
"unit": "-",
|
||||
"example": "골쌓기",
|
||||
"choices": ["골쌓기", "켜쌓기"]
|
||||
},
|
||||
{
|
||||
"name": "높이",
|
||||
"type": "number",
|
||||
"unit": "m",
|
||||
"example": 4.5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "in-poom",
|
||||
"kind": "input",
|
||||
"name": "메쌓기 품표",
|
||||
"color": "#2f80ed",
|
||||
"unit": "-",
|
||||
"x": 40,
|
||||
"y": 300,
|
||||
"input": {
|
||||
"mode": "group",
|
||||
"group": "소요량",
|
||||
"elements": [],
|
||||
"table": {
|
||||
"key": "QF000421"
|
||||
},
|
||||
"variables": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "in-rate",
|
||||
"kind": "input",
|
||||
"name": "높이 증가율표",
|
||||
"color": "#2f80ed",
|
||||
"unit": "-",
|
||||
"x": 40,
|
||||
"y": 460,
|
||||
"input": {
|
||||
"mode": "group",
|
||||
"group": "소요량",
|
||||
"elements": [],
|
||||
"table": {
|
||||
"key": "QF000422"
|
||||
},
|
||||
"variables": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "in-labor",
|
||||
"kind": "input",
|
||||
"name": "인력",
|
||||
"color": "#2f80ed",
|
||||
"unit": "원/일",
|
||||
"x": 40,
|
||||
"y": 620,
|
||||
"input": {
|
||||
"mode": "group",
|
||||
"group": "인력",
|
||||
"variables": [],
|
||||
"elements": [
|
||||
{
|
||||
"ref": "LB000033",
|
||||
"name": "석공",
|
||||
"spec": "",
|
||||
"unit": "원/일"
|
||||
},
|
||||
{
|
||||
"ref": "LB000002",
|
||||
"name": "보통인부",
|
||||
"spec": "",
|
||||
"unit": "원/일"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-rate",
|
||||
"kind": "function",
|
||||
"name": "증가율 찾기",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "%",
|
||||
"x": 340,
|
||||
"y": 420,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "cf-rate",
|
||||
"이름": "증가율 찾기",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "높이"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · m"
|
||||
},
|
||||
"A3": {
|
||||
"값": "표에서 찾은 증가율"
|
||||
},
|
||||
"B3": {
|
||||
"식": "SUMIFS($G$2:$G$100,$E$2:$E$100,\"<\"&B2,$F$2:$F$100,\">=\"&B2)"
|
||||
},
|
||||
"C3": {
|
||||
"값": "높이가 「아래 < 높이 ≤ 위」 인 줄"
|
||||
},
|
||||
"A4": {
|
||||
"값": "증가율"
|
||||
},
|
||||
"B4": {
|
||||
"식": "IF(B2<=3,0,B3)"
|
||||
},
|
||||
"C4": {
|
||||
"값": "3m 까지는 할증 없음"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "높이",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "증가율표",
|
||||
"cell": "E1"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B4",
|
||||
"name": "증가율",
|
||||
"unit": "%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-poom0",
|
||||
"kind": "function",
|
||||
"name": "석공 품",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "인/㎡",
|
||||
"x": 640,
|
||||
"y": 80,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "cf-poom0",
|
||||
"이름": "석공 품",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "뒷길이"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · ㎝"
|
||||
},
|
||||
"A3": {
|
||||
"값": "돌"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값"
|
||||
},
|
||||
"A4": {
|
||||
"값": "쌓기"
|
||||
},
|
||||
"C4": {
|
||||
"값": "받는 값"
|
||||
},
|
||||
"A5": {
|
||||
"값": "증가율"
|
||||
},
|
||||
"C5": {
|
||||
"값": "받는 값 · %"
|
||||
},
|
||||
"A6": {
|
||||
"값": "표에서 찾은 품"
|
||||
},
|
||||
"B6": {
|
||||
"식": "SUMIFS($H$2:$H$200,$E$2:$E$200,B2,$F$2:$F$200,B3,$G$2:$G$200,B4)"
|
||||
},
|
||||
"C6": {
|
||||
"값": "세 조건이 맞는 줄의 품"
|
||||
},
|
||||
"A7": {
|
||||
"값": "석공 품"
|
||||
},
|
||||
"B7": {
|
||||
"식": "B6*(1+B5/100)"
|
||||
},
|
||||
"C7": {
|
||||
"값": "높이 할증을 얹음"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "뒷길이",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "돌",
|
||||
"cell": "B3"
|
||||
},
|
||||
{
|
||||
"port": "쌓기",
|
||||
"cell": "B4"
|
||||
},
|
||||
{
|
||||
"port": "증가율",
|
||||
"cell": "B5"
|
||||
},
|
||||
{
|
||||
"port": "품표",
|
||||
"cell": "E1"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B7",
|
||||
"name": "석공 품",
|
||||
"unit": "인/㎡"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-poom1",
|
||||
"kind": "function",
|
||||
"name": "보통인부 품",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "인/㎡",
|
||||
"x": 640,
|
||||
"y": 300,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "cf-poom1",
|
||||
"이름": "보통인부 품",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "뒷길이"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · ㎝"
|
||||
},
|
||||
"A3": {
|
||||
"값": "돌"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값"
|
||||
},
|
||||
"A4": {
|
||||
"값": "쌓기"
|
||||
},
|
||||
"C4": {
|
||||
"값": "받는 값"
|
||||
},
|
||||
"A5": {
|
||||
"값": "증가율"
|
||||
},
|
||||
"C5": {
|
||||
"값": "받는 값 · %"
|
||||
},
|
||||
"A6": {
|
||||
"값": "표에서 찾은 품"
|
||||
},
|
||||
"B6": {
|
||||
"식": "SUMIFS($I$2:$I$200,$E$2:$E$200,B2,$F$2:$F$200,B3,$G$2:$G$200,B4)"
|
||||
},
|
||||
"C6": {
|
||||
"값": "세 조건이 맞는 줄의 품"
|
||||
},
|
||||
"A7": {
|
||||
"값": "보통인부 품"
|
||||
},
|
||||
"B7": {
|
||||
"식": "B6*(1+B5/100)"
|
||||
},
|
||||
"C7": {
|
||||
"값": "높이 할증을 얹음"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "뒷길이",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "돌",
|
||||
"cell": "B3"
|
||||
},
|
||||
{
|
||||
"port": "쌓기",
|
||||
"cell": "B4"
|
||||
},
|
||||
{
|
||||
"port": "증가율",
|
||||
"cell": "B5"
|
||||
},
|
||||
{
|
||||
"port": "품표",
|
||||
"cell": "E1"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B7",
|
||||
"name": "보통인부 품",
|
||||
"unit": "인/㎡"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-pay0",
|
||||
"kind": "function",
|
||||
"name": "석공 노무비",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "원/㎡",
|
||||
"x": 940,
|
||||
"y": 80,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "cf-pay0",
|
||||
"이름": "석공 노무비",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "석공 품"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · 인/㎡"
|
||||
},
|
||||
"A3": {
|
||||
"값": "석공 노임"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값 · 원/인"
|
||||
},
|
||||
"A4": {
|
||||
"값": "석공 노무비"
|
||||
},
|
||||
"B4": {
|
||||
"식": "B2*B3"
|
||||
},
|
||||
"C4": {
|
||||
"값": "품 × 노임"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "석공 품",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "석공 노임",
|
||||
"cell": "B3"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B4",
|
||||
"name": "석공 노무비",
|
||||
"unit": "원/㎡"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-pay1",
|
||||
"kind": "function",
|
||||
"name": "보통인부 노무비",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "원/㎡",
|
||||
"x": 940,
|
||||
"y": 300,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "cf-pay1",
|
||||
"이름": "보통인부 노무비",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "보통인부 품"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · 인/㎡"
|
||||
},
|
||||
"A3": {
|
||||
"값": "보통인부 노임"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값 · 원/인"
|
||||
},
|
||||
"A4": {
|
||||
"값": "보통인부 노무비"
|
||||
},
|
||||
"B4": {
|
||||
"식": "B2*B3"
|
||||
},
|
||||
"C4": {
|
||||
"값": "품 × 노임"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "보통인부 품",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "보통인부 노임",
|
||||
"cell": "B3"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B4",
|
||||
"name": "보통인부 노무비",
|
||||
"unit": "원/㎡"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-sum",
|
||||
"kind": "function",
|
||||
"name": "노무비 합",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "원/㎡",
|
||||
"x": 1240,
|
||||
"y": 190,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "cf-sum",
|
||||
"이름": "노무비 합",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "석공 노무비"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값"
|
||||
},
|
||||
"A3": {
|
||||
"값": "보통인부 노무비"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값"
|
||||
},
|
||||
"A4": {
|
||||
"값": "노무비"
|
||||
},
|
||||
"B4": {
|
||||
"식": "B2+B3"
|
||||
},
|
||||
"C4": {
|
||||
"값": "둘을 더함"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "석공 노무비",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "보통인부 노무비",
|
||||
"cell": "B3"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B4",
|
||||
"name": "노무비",
|
||||
"unit": "원/㎡"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "out-pay",
|
||||
"kind": "output",
|
||||
"name": "노무비",
|
||||
"color": "#27ae60",
|
||||
"unit": "원/㎡",
|
||||
"x": 1520,
|
||||
"y": 190,
|
||||
"output": {
|
||||
"cost": "노무비",
|
||||
"unit": "원/㎡"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "l1",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "높이"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-rate",
|
||||
"name": "높이"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l2",
|
||||
"from": {
|
||||
"box": "in-rate",
|
||||
"name": "표"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-rate",
|
||||
"name": "증가율표"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l3",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "뒷길이"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom0",
|
||||
"name": "뒷길이"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l4",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "돌"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom0",
|
||||
"name": "돌"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l5",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "쌓기"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom0",
|
||||
"name": "쌓기"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l6",
|
||||
"from": {
|
||||
"box": "fn-rate",
|
||||
"name": "증가율"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom0",
|
||||
"name": "증가율"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l7",
|
||||
"from": {
|
||||
"box": "in-poom",
|
||||
"name": "표"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom0",
|
||||
"name": "품표"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l8",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "뒷길이"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom1",
|
||||
"name": "뒷길이"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l9",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "돌"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom1",
|
||||
"name": "돌"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l10",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "쌓기"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom1",
|
||||
"name": "쌓기"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l11",
|
||||
"from": {
|
||||
"box": "fn-rate",
|
||||
"name": "증가율"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom1",
|
||||
"name": "증가율"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l12",
|
||||
"from": {
|
||||
"box": "in-poom",
|
||||
"name": "표"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom1",
|
||||
"name": "품표"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l13",
|
||||
"from": {
|
||||
"box": "fn-poom0",
|
||||
"name": "석공 품"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-pay0",
|
||||
"name": "석공 품"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l14",
|
||||
"from": {
|
||||
"box": "in-labor",
|
||||
"name": "석공"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-pay0",
|
||||
"name": "석공 노임"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l15",
|
||||
"from": {
|
||||
"box": "fn-poom1",
|
||||
"name": "보통인부 품"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-pay1",
|
||||
"name": "보통인부 품"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l16",
|
||||
"from": {
|
||||
"box": "in-labor",
|
||||
"name": "보통인부"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-pay1",
|
||||
"name": "보통인부 노임"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l17",
|
||||
"from": {
|
||||
"box": "fn-pay0",
|
||||
"name": "석공 노무비"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-sum",
|
||||
"name": "석공 노무비"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l18",
|
||||
"from": {
|
||||
"box": "fn-pay1",
|
||||
"name": "보통인부 노무비"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-sum",
|
||||
"name": "보통인부 노무비"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l19",
|
||||
"from": {
|
||||
"box": "fn-sum",
|
||||
"name": "노무비"
|
||||
},
|
||||
"to": {
|
||||
"box": "out-pay",
|
||||
"name": "값"
|
||||
}
|
||||
}
|
||||
],
|
||||
"userElements": []
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
{
|
||||
"key": "SAMPLE-value",
|
||||
"name": "견본 · 덤프트럭 시간당 작업량(값)",
|
||||
"resultUnit": "㎥/시간",
|
||||
"boxes": [
|
||||
{
|
||||
"id": "in-var",
|
||||
"kind": "input",
|
||||
"name": "덤프트럭 조건",
|
||||
"color": "#2f80ed",
|
||||
"unit": "-",
|
||||
"x": 40,
|
||||
"y": 80,
|
||||
"input": {
|
||||
"mode": "variable",
|
||||
"elements": [],
|
||||
"variables": [
|
||||
{
|
||||
"name": "q",
|
||||
"type": "number",
|
||||
"unit": "㎥",
|
||||
"example": 6
|
||||
},
|
||||
{
|
||||
"name": "f",
|
||||
"type": "number",
|
||||
"unit": "-",
|
||||
"example": 1.2
|
||||
},
|
||||
{
|
||||
"name": "E",
|
||||
"type": "number",
|
||||
"unit": "-",
|
||||
"example": 0.9
|
||||
},
|
||||
{
|
||||
"name": "Cm",
|
||||
"type": "number",
|
||||
"unit": "분",
|
||||
"example": 25
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-q",
|
||||
"kind": "function",
|
||||
"name": "시간당 작업량",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "㎥/시간",
|
||||
"x": 340,
|
||||
"y": 80,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "cf-q",
|
||||
"이름": "시간당 작업량",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "q"
|
||||
},
|
||||
"C2": {
|
||||
"값": "적재량 · ㎥"
|
||||
},
|
||||
"A3": {
|
||||
"값": "f"
|
||||
},
|
||||
"C3": {
|
||||
"값": "토량환산계수"
|
||||
},
|
||||
"A4": {
|
||||
"값": "E"
|
||||
},
|
||||
"C4": {
|
||||
"값": "작업효율"
|
||||
},
|
||||
"A5": {
|
||||
"값": "Cm"
|
||||
},
|
||||
"C5": {
|
||||
"값": "1회 사이클 시간 · 분"
|
||||
},
|
||||
"A6": {
|
||||
"값": "시간당 작업량"
|
||||
},
|
||||
"B6": {
|
||||
"식": "ROUND(60*B2*B3*B4/B5,3)"
|
||||
},
|
||||
"C6": {
|
||||
"값": "60 × q × f × E ÷ Cm · 소수 셋째 자리"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "q",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "f",
|
||||
"cell": "B3"
|
||||
},
|
||||
{
|
||||
"port": "E",
|
||||
"cell": "B4"
|
||||
},
|
||||
{
|
||||
"port": "Cm",
|
||||
"cell": "B5"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B6",
|
||||
"name": "시간당 작업량",
|
||||
"unit": "㎥/시간"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "out-q",
|
||||
"kind": "output",
|
||||
"name": "시간당 작업량",
|
||||
"color": "#27ae60",
|
||||
"unit": "㎥/시간",
|
||||
"x": 640,
|
||||
"y": 80,
|
||||
"output": {
|
||||
"cost": "값",
|
||||
"unit": "㎥/시간"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "l1",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "q"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-q",
|
||||
"name": "q"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l2",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "f"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-q",
|
||||
"name": "f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l3",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "E"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-q",
|
||||
"name": "E"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l4",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "Cm"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-q",
|
||||
"name": "Cm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l5",
|
||||
"from": {
|
||||
"box": "fn-q",
|
||||
"name": "시간당 작업량"
|
||||
},
|
||||
"to": {
|
||||
"box": "out-q",
|
||||
"name": "값"
|
||||
}
|
||||
}
|
||||
],
|
||||
"userElements": []
|
||||
}
|
||||
Reference in New Issue
Block a user