Merge remote-tracking branch 'origin/dev' into sub_laptop_4
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();
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/* =============================================================================
|
||||
* M02_DF_CellTable.ts
|
||||
* 함수 판 「칸 연결 표」 — 칸 · 연결 알약 · 입력 / 변수 / 결과 · 이어진 선 · 표에서 바로 칸 바꾸기 · 풀기
|
||||
* 결과 칸은 하나 · 색은 시트 칸 표시와 같은 쪽(입력 파랑 · 변수 노랑 · 결과 초록) + 글자(입력 · 변수 · 결과)
|
||||
* 글 [한, 영] 짝은 아래 TEXT
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import type { CfBox, CfLogic } from "./M02_DF_Model";
|
||||
import { portText } from "./M02_DF_Detail";
|
||||
|
||||
const TEXT = {
|
||||
Title: ["칸 연결 표", "Cell connections"],
|
||||
Cell: ["칸", "Cell"],
|
||||
Pill: ["연결 알약", "Pill"],
|
||||
Kind: ["종류", "Kind"],
|
||||
Line: ["이어진 선", "Line"],
|
||||
In: ["입력", "Input"],
|
||||
Var: ["변수", "Variable"],
|
||||
Out: ["결과", "Result"],
|
||||
None: ["아직 연결된 칸이 없음", "No cell is connected yet"],
|
||||
Unplaced: ["칸 없음", "no cell"],
|
||||
NoLine: ["-", "-"],
|
||||
Release: ["풀기", "Release"],
|
||||
BadCell: ["칸 주소가 아님 — 예: B2", "Not a cell address — e.g. B2"],
|
||||
} as const;
|
||||
|
||||
const tt = (key: keyof typeof TEXT): string => TEXT[key][currentLanguageIndex === 1 ? 1 : 0];
|
||||
|
||||
export type CellKind = "in" | "var" | "out";
|
||||
|
||||
export interface CellTableHandlers {
|
||||
/** 표에서 칸을 바꿈 — key = 알약 이름 */
|
||||
move(kind: CellKind, key: string, cell: string): void;
|
||||
/** 풀기 — 입력은 칸만 비우고 알약은 남김 · 변수는 지움 · 결과는 칸만 비움 */
|
||||
release(kind: CellKind, key: string): void;
|
||||
}
|
||||
|
||||
interface Row {
|
||||
kind: CellKind;
|
||||
key: string;
|
||||
cell: string;
|
||||
line: string;
|
||||
}
|
||||
|
||||
function rowsOf(lg: CfLogic, box: CfBox): Row[] {
|
||||
const f = box.func;
|
||||
if (!f) return [];
|
||||
const into = (port: string): string => {
|
||||
const l = lg.links.find((x) => x.to.box === box.id && x.to.name === port);
|
||||
return l ? portText(lg, l.from) : tt("NoLine");
|
||||
};
|
||||
const out = (): string => {
|
||||
const names = lg.links.filter((l) => l.from.box === box.id && l.from.name === f.output.name);
|
||||
return names.length ? names.map((l) => portText(lg, l.to)).join(", ") : tt("NoLine");
|
||||
};
|
||||
return [
|
||||
...f.inputs.map((i): Row => ({ kind: "in", key: i.port, cell: i.cell, line: into(i.port) })),
|
||||
...f.variables.map((v): Row => ({
|
||||
kind: "var",
|
||||
key: v.name,
|
||||
cell: v.cell ?? "",
|
||||
line: tt("NoLine"),
|
||||
})),
|
||||
{ kind: "out", key: f.output.name, cell: f.output.cell, line: out() },
|
||||
];
|
||||
}
|
||||
|
||||
/** 칸 연결 표 그리기 — 바뀜은 handlers 로만 알림(그리기는 부른 쪽이 다시) */
|
||||
export function renderCellTable(
|
||||
host: HTMLElement,
|
||||
lg: CfLogic,
|
||||
box: CfBox,
|
||||
readOnly: boolean,
|
||||
handlers: CellTableHandlers,
|
||||
): void {
|
||||
host.classList.add("m02-dfcells");
|
||||
const rows = rowsOf(lg, box).filter((r) => r.kind === "out" || r.key);
|
||||
const head = el("tr", {
|
||||
children: [tt("Cell"), tt("Pill"), tt("Kind"), tt("Line"), ""].map((t) =>
|
||||
el("th", { text: t }),
|
||||
),
|
||||
});
|
||||
const body = rows.map((r) => {
|
||||
const input = el("input", {
|
||||
className: "ui-input ui-input--sm m02-dfcells__cell",
|
||||
attrs: { type: "text", "aria-label": `${r.key} ${tt("Cell")}`, placeholder: tt("Unplaced") },
|
||||
});
|
||||
input.value = r.cell;
|
||||
input.readOnly = readOnly;
|
||||
input.addEventListener("change", () => {
|
||||
const m = /^([A-Za-z]{1,3})(\d+)$/.exec(input.value.trim());
|
||||
input.classList.toggle("is-bad", !m);
|
||||
input.title = m ? "" : tt("BadCell");
|
||||
if (m) handlers.move(r.kind, r.key, `${m[1].toUpperCase()}${m[2]}`);
|
||||
});
|
||||
return el("tr", {
|
||||
className: `m02-dfcells__row is-${r.kind}`,
|
||||
children: [
|
||||
el("td", { children: [input] }),
|
||||
el("td", { text: r.key }),
|
||||
el("td", {
|
||||
children: [
|
||||
el("span", {
|
||||
className: "m02-dfcells__kind",
|
||||
text: tt(r.kind === "in" ? "In" : r.kind === "var" ? "Var" : "Out"),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
el("td", { text: r.line }),
|
||||
el("td", {
|
||||
children:
|
||||
readOnly || (r.kind !== "var" && !r.cell)
|
||||
? []
|
||||
: [
|
||||
createButton({
|
||||
label: tt("Release"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => handlers.release(r.kind, r.key),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
host.replaceChildren(
|
||||
el("strong", { className: "m02-dfcells__title", text: tt("Title") }),
|
||||
el("table", {
|
||||
className: "m02-dfcells__table",
|
||||
children: [el("thead", { children: [head] }), el("tbody", { children: body })],
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -320,7 +320,7 @@ export function renderDiagram(
|
||||
return { x: b.x, y: b.y, w: n?.offsetWidth ?? 180, h: n?.offsetHeight ?? 80 };
|
||||
});
|
||||
const r = view.getBoundingClientRect();
|
||||
Object.assign(st, fitView(rects, { w: r.width, h: r.height }));
|
||||
Object.assign(st, fitView(rects, { w: r.width, h: r.height }, 40, opts.fitMin));
|
||||
applyView();
|
||||
drawLines();
|
||||
};
|
||||
|
||||
@@ -122,6 +122,8 @@ export interface DiagramOpts {
|
||||
readOnly?: boolean;
|
||||
/** 바뀌면 다시 맞춤(자리를 옮겨 끼운 뒤 · 새 로직) */
|
||||
fitKey?: number;
|
||||
/** 저절로 맞출 때 가장 작은 확대 — 없으면 FIT_MIN(좁은 패널은 더 작게) */
|
||||
fitMin?: number;
|
||||
}
|
||||
|
||||
/* ── 알약 · 자리 ────────────────────────────────────────────────────── */
|
||||
@@ -193,13 +195,14 @@ export function fitView(
|
||||
rects: { x: number; y: number; w: number; h: number }[],
|
||||
view: { w: number; h: number },
|
||||
pad = 40,
|
||||
min = FIT_MIN,
|
||||
): { zoom: number; panX: number; panY: number } {
|
||||
if (!rects.length) return { zoom: 1, panX: 0, panY: 0 };
|
||||
const x0 = Math.min(...rects.map((r) => r.x)) - pad;
|
||||
const y0 = Math.min(...rects.map((r) => r.y)) - pad;
|
||||
const x1 = Math.max(...rects.map((r) => r.x + r.w)) + pad;
|
||||
const y1 = Math.max(...rects.map((r) => r.y + r.h)) + pad;
|
||||
const zoom = clampZoom(Math.max(FIT_MIN, Math.min(view.w / (x1 - x0), view.h / (y1 - y0), 1.5)));
|
||||
const zoom = clampZoom(Math.max(min, Math.min(view.w / (x1 - x0), view.h / (y1 - y0), 1.5)));
|
||||
/** 남으면 가운데 · 넘치면 왼쪽(위) 끝부터 */
|
||||
const place = (size: number, from: number, to: number): number =>
|
||||
Math.max(0, (size - (to - from) * zoom) / 2) - from * zoom;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
/* =============================================================================
|
||||
* M02_DF_FormulaView.ts
|
||||
* 함수 상자 「식 보기」 — 결과 칸과 그 칸이 기대는 중간 칸의 식을 칸 주소 → 연결 알약 이름으로 · ×, ÷ · 값 들어간 식 · = 결과 ·
|
||||
* IF 는 만약 / 아니면 줄 + 지금 타는 갈래(now) · 앞 절반은 DOM 을 모름(시험이 Node 로 돌림) · 상세 내용 함수 판과 왼쪽 텍스트 수식이 함께 씀
|
||||
* 모양은 `docs/raw/M01_쉬운단가/흐름도_적용_정함.md` 약속 8 · 글 [한, 영] 짝은 아래 TEXT
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { parseA1 } from "../../A00_Common/spreadsheet/spreadsheet_address";
|
||||
import { recalcWorkbook } from "../../A00_Common/spreadsheet/spreadsheet_recalc";
|
||||
import { ifBody, splitArgs } from "../../A00_Common/spreadsheet/spreadsheet_condition_text";
|
||||
import type { CalcValue } from "../../A00_Common/spreadsheet/spreadsheet_types";
|
||||
import type { CfBox } from "./M02_DF_Model";
|
||||
|
||||
export type CellScalar = number | string | boolean | null;
|
||||
|
||||
export interface FormulaBranch {
|
||||
when: string;
|
||||
then: string;
|
||||
/** 지금 타는 갈래 */
|
||||
now: boolean;
|
||||
}
|
||||
|
||||
export interface FormulaLine {
|
||||
/** 이 식이 든 칸의 이름(연결 알약 이름 · 없으면 칸 주소) */
|
||||
name: string;
|
||||
/** 이름으로 쓴 식 — 「석공량 × (1 + 증가율 ÷ 100)」 */
|
||||
named: string;
|
||||
/** 값 넣은 식 — 「0.15 × (1 + 4 ÷ 100) = 0.156」 */
|
||||
valued: string;
|
||||
/** IF 면 만약 / 아니면 줄 */
|
||||
branches?: FormulaBranch[];
|
||||
}
|
||||
|
||||
const TEXT = {
|
||||
Title: ["식 보기", "Formulas"],
|
||||
Empty: ["식이 든 결과 칸이 아직 없음", "No result cell with a formula yet"],
|
||||
Else: ["아니면", "Otherwise"],
|
||||
If: ["만약", "If"],
|
||||
Now: ["지금", "now"],
|
||||
} as const;
|
||||
|
||||
const tv = (key: keyof typeof TEXT): string => TEXT[key][currentLanguageIndex === 1 ? 1 : 0];
|
||||
|
||||
/* ── 칸 · 이름 ──────────────────────────────────────────────────────────── */
|
||||
|
||||
const REF = /(?<![A-Za-z0-9_.'!])\$?([A-Z]{1,3})\$?(\d+)(?![A-Za-z0-9_(])/g;
|
||||
|
||||
/** 따옴표 안 글은 그대로 두고 바깥 글만 바꿈 */
|
||||
function outside(text: string, fn: (part: string) => string): string {
|
||||
return text
|
||||
.split(/("(?:[^"]|"")*")/)
|
||||
.map((p, i) => (i % 2 ? p : fn(p)))
|
||||
.join("");
|
||||
}
|
||||
|
||||
const upper = (a1: string): string => a1.toUpperCase().replace(/\$/g, "");
|
||||
|
||||
/** 칸 주소 → 이름 — 받는 알약 · 변수 · 결과 칸 이름(연결 없는 칸은 주소 그대로) */
|
||||
export function cellNames(box: CfBox): Record<string, string> {
|
||||
const f = box.func;
|
||||
const out: Record<string, string> = {};
|
||||
if (!f) return out;
|
||||
for (const i of f.inputs) if (i.cell) out[upper(i.cell)] = i.port;
|
||||
for (const v of f.variables) if (v.cell) out[upper(v.cell)] = v.name;
|
||||
if (f.output.cell) out[upper(f.output.cell)] = f.output.name;
|
||||
return out;
|
||||
}
|
||||
|
||||
const refsIn = (formula: string): string[] => {
|
||||
const found: string[] = [];
|
||||
outside(formula, (p) => p.replace(REF, (m, c: string, r: string) => (found.push(`${c}${r}`), m)));
|
||||
return found;
|
||||
};
|
||||
|
||||
const OPS: [RegExp, string][] = [
|
||||
[/\*/g, " × "],
|
||||
[/\//g, " ÷ "],
|
||||
[/<>/g, " ≠ "],
|
||||
[/>=/g, " ≥ "],
|
||||
[/<=/g, " ≤ "],
|
||||
[/(?<![<>≠≥≤])=/g, " = "],
|
||||
[/\+/g, " + "],
|
||||
[/(?<=[\w가-힣)])-(?=[\w가-힣(])/g, " − "],
|
||||
];
|
||||
|
||||
/** 보기 좋게 — × ÷ ≠ ≥ ≤ 로 */
|
||||
function pretty(text: string): string {
|
||||
return outside(text, (p) => OPS.reduce((s, [re, to]) => s.replace(re, to), p))
|
||||
.replace(/\s{2,}/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
const fmt = (n: number): string =>
|
||||
Number(n.toPrecision(12)).toLocaleString("ko-KR", { maximumFractionDigits: 6 });
|
||||
|
||||
function showScalar(v: CellScalar | undefined): string {
|
||||
if (v === undefined || v === null) return "0";
|
||||
if (typeof v === "number") return fmt(v);
|
||||
if (typeof v === "boolean") return v ? "TRUE" : "FALSE";
|
||||
return `"${v}"`;
|
||||
}
|
||||
|
||||
/** 이름으로 쓴 식 */
|
||||
export function namedText(formula: string, names: Record<string, string>): string {
|
||||
return pretty(
|
||||
outside(formula, (p) =>
|
||||
p.replace(REF, (_m, c: string, r: string) => names[`${c}${r}`] ?? `${c}${r}`),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** 값 넣은 식(값이 문자면 따옴표) */
|
||||
export function valuedText(formula: string, values: Record<string, CellScalar>): string {
|
||||
return pretty(
|
||||
outside(formula, (p) =>
|
||||
p.replace(REF, (_m, c: string, r: string) => showScalar(values[`${c}${r}`])),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/* ── 갈래 ───────────────────────────────────────────────────────────────── */
|
||||
|
||||
const num = (v: CellScalar | undefined): number | null => {
|
||||
if (typeof v === "number") return v;
|
||||
if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
|
||||
return null;
|
||||
};
|
||||
|
||||
/** 조건 한 줄 `왼 비교 오른` 의 참거짓 — 못 읽으면 null */
|
||||
function holds(cond: string, values: Record<string, CellScalar>): boolean | null {
|
||||
const m = /^\s*(.+?)\s*(>=|<=|<>|=|>|<)\s*(.+?)\s*$/.exec(cond);
|
||||
if (!m) return null;
|
||||
const read = (t: string): CellScalar | undefined => {
|
||||
const x = t.trim();
|
||||
const cell = /^\$?([A-Z]{1,3})\$?(\d+)$/.exec(x);
|
||||
if (cell) return values[`${cell[1]}${cell[2]}`];
|
||||
const q = /^"((?:[^"]|"")*)"$/.exec(x);
|
||||
if (q) return q[1].replace(/""/g, '"');
|
||||
return Number.isFinite(Number(x)) ? Number(x) : undefined;
|
||||
};
|
||||
const a = read(m[1]);
|
||||
const b = read(m[3]);
|
||||
if (a === undefined || b === undefined) return null;
|
||||
const [l, r] = num(a) !== null && num(b) !== null ? [num(a)!, num(b)!] : [String(a), String(b)];
|
||||
switch (m[2]) {
|
||||
case "=":
|
||||
return l === r;
|
||||
case "<>":
|
||||
return l !== r;
|
||||
case ">=":
|
||||
return l >= r;
|
||||
case "<=":
|
||||
return l <= r;
|
||||
case ">":
|
||||
return l > r;
|
||||
default:
|
||||
return l < r;
|
||||
}
|
||||
}
|
||||
|
||||
/** IF 사슬 → 갈래들 — IF 가 아니면 undefined */
|
||||
function branchesOf(
|
||||
formula: string,
|
||||
names: Record<string, string>,
|
||||
values: Record<string, CellScalar>,
|
||||
): FormulaBranch[] | undefined {
|
||||
const out: FormulaBranch[] = [];
|
||||
let rest = formula;
|
||||
let taken = false;
|
||||
for (;;) {
|
||||
const body = ifBody(rest);
|
||||
if (body === null) break;
|
||||
const a = splitArgs(body);
|
||||
if (a.length !== 3) return undefined;
|
||||
const ok = holds(a[0], values) === true && !taken;
|
||||
if (ok) taken = true;
|
||||
out.push({ when: namedText(a[0], names), then: namedText(a[1], names), now: ok });
|
||||
rest = a[2];
|
||||
}
|
||||
if (!out.length) return undefined;
|
||||
out.push({ when: tv("Else"), then: namedText(rest, names), now: !taken });
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ── 줄 ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
/** 식 보기 줄들 — 결과 칸이 기대는 칸의 식이 먼저 · 결과 칸이 마지막 */
|
||||
export function formulaLines(box: CfBox, values: Record<string, CellScalar>): FormulaLine[] {
|
||||
const f = box.func;
|
||||
if (!f?.output.cell) return [];
|
||||
const names = cellNames(box);
|
||||
const cells = f.sheet.칸;
|
||||
const order: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
const visit = (a1: string): void => {
|
||||
const key = upper(a1);
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
const formula = cells[key]?.식;
|
||||
if (formula === undefined) return;
|
||||
for (const r of refsIn(formula)) visit(r);
|
||||
order.push(key);
|
||||
};
|
||||
visit(f.output.cell);
|
||||
return order.map((key) => {
|
||||
const formula = cells[key].식 as string;
|
||||
const result = showScalar(values[key]);
|
||||
const branches = branchesOf(formula, names, values);
|
||||
return {
|
||||
name: names[key] ?? key,
|
||||
named: namedText(formula, names),
|
||||
valued: `${valuedText(formula, values)} = ${result}`,
|
||||
...(branches ? { branches } : {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** 시트 칸 값 — 받는 알약 칸에 들어온 값(arrived) · 변수 예시값을 넣고 엔진으로 셈 */
|
||||
export function cellValuesOf(
|
||||
box: CfBox,
|
||||
arrived: Record<string, unknown> | undefined,
|
||||
): Record<string, CellScalar> {
|
||||
const f = box.func;
|
||||
if (!f) return {};
|
||||
const sheet = structuredClone(f.sheet);
|
||||
const scalar = (v: unknown): number | string | boolean | undefined =>
|
||||
typeof v === "number" || typeof v === "string" || typeof v === "boolean" ? v : undefined;
|
||||
const put = (cell: string, v: number | string | boolean | undefined): void => {
|
||||
if (!parseA1(cell) || v === undefined) return;
|
||||
sheet.칸[upper(cell)] = { 값: v };
|
||||
};
|
||||
for (const i of f.inputs) if (i.cell) put(i.cell, scalar(arrived?.[i.port]));
|
||||
for (const v of f.variables) if (v.cell && v.example !== null) put(v.cell, v.example);
|
||||
const calc = recalcWorkbook({ 종류: "통합문서", 판: 1, 열: "DF", 서식: [{}], 시트: [sheet] });
|
||||
const out: Record<string, CellScalar> = {};
|
||||
for (const [key, cell] of Object.entries(sheet.칸)) {
|
||||
const c: CalcValue | undefined = calc[sheet.id]?.[key];
|
||||
out[key] = cell.식 === undefined ? (cell.값 ?? null) : c ? fromCalc(c) : null;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function fromCalc(v: CalcValue): CellScalar {
|
||||
if ("수" in v) return Number(v.수);
|
||||
if ("글" in v) return v.글;
|
||||
if ("참" in v) return v.참;
|
||||
return v.오류;
|
||||
}
|
||||
|
||||
/* ── 그리기 ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
/** 식 보기 그리기 — 줄마다 이름 식 · 값 식 · 갈래(지금 갈래는 ▶ 와 「지금」 글) */
|
||||
export function renderFormulaView(host: HTMLElement, lines: FormulaLine[]): void {
|
||||
host.classList.add("m02-df-formula");
|
||||
host.replaceChildren(
|
||||
el("strong", { className: "m02-df-formula__title", text: tv("Title") }),
|
||||
...(lines.length
|
||||
? lines.map((l) =>
|
||||
el("div", {
|
||||
className: "m02-df-formula__line",
|
||||
children: [
|
||||
el("div", { children: [el("b", { text: `${l.name} = ` }), l.named] }),
|
||||
el("div", { className: "m02-df-formula__valued", text: l.valued }),
|
||||
...(l.branches
|
||||
? [
|
||||
el("ul", {
|
||||
className: "m02-df-formula__branches",
|
||||
children: l.branches.map((b, i) =>
|
||||
el("li", {
|
||||
className: b.now ? "is-now" : "",
|
||||
text: `${b.now ? "▶ " : ""}${i < l.branches!.length - 1 ? `${tv("If")} ${b.when} → ` : `${b.when} → `}${b.then}${b.now ? ` (${tv("Now")})` : ""}`,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
)
|
||||
: [el("p", { className: "m02-df-formula__empty", text: tv("Empty") })]),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/* =============================================================================
|
||||
* M02_DF_FuncExtra.ts
|
||||
* 함수 판 보조 — 식 보기 그리기 · 칸 연결 표 손잡이 · 우클릭 「조건 만들기」 에 주는 것
|
||||
* 판 본체(M02_DF_FuncPanel)가 700줄 안에 들도록 뗌 · 상태는 모르고 손잡이(ExtraContext)만 받음
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import type { ConditionOptions } from "../../A00_Common/spreadsheet/spreadsheet_condition";
|
||||
import { renderCellTable } from "./M02_DF_CellTable";
|
||||
import { evalSafe, type DetailOptions } from "./M02_DF_Detail";
|
||||
import { cellValuesOf, formulaLines, renderFormulaView } from "./M02_DF_FormulaView";
|
||||
import { TEXT } from "./M02_DF_FuncText";
|
||||
import { valueChoices, type CfFunc, type CfLogic } from "./M02_DF_Model";
|
||||
|
||||
export interface ExtraContext {
|
||||
lg: CfLogic;
|
||||
boxId: string;
|
||||
detail: DetailOptions;
|
||||
readOnly: boolean;
|
||||
place(port: string, cell: string): void;
|
||||
setOutput(cell: string): void;
|
||||
patch(make: (f: CfFunc) => CfFunc): void;
|
||||
used(f: CfFunc): Map<string, string>;
|
||||
}
|
||||
|
||||
const busy = (): string => TEXT.Busy[currentLanguageIndex === 1 ? 1 : 0];
|
||||
const boxOf = (x: ExtraContext) => x.lg.boxes.find((b) => b.id === x.boxId);
|
||||
|
||||
/** 식 보기 — 칸 식을 고치면 부른 쪽이 다시 부름 */
|
||||
export function drawFormula(host: HTMLElement, x: ExtraContext): void {
|
||||
const box = boxOf(x);
|
||||
if (!box?.func) return;
|
||||
const ev = evalSafe(x.lg, x.detail);
|
||||
renderFormulaView(host, formulaLines(box, cellValuesOf(box, ev?.arrived?.[x.boxId])));
|
||||
}
|
||||
|
||||
/** 칸 연결 표 — 표에서 칸 바꾸기 · 풀기(입력/변수/결과 모두 같은 규칙: 이미 쓰는 칸 막음 · 결과 칸 하나) */
|
||||
export function drawCells(host: HTMLElement, x: ExtraContext): void {
|
||||
const box = boxOf(x);
|
||||
if (!box?.func) return;
|
||||
renderCellTable(host, x.lg, box, x.readOnly, {
|
||||
move: (kind, key, cell) => {
|
||||
if (kind === "in") return x.place(key, cell);
|
||||
if (kind === "out") return x.setOutput(cell);
|
||||
if (box.func && x.used(box.func).has(cell)) return void showToast(busy(), "error");
|
||||
x.patch((f) => ({
|
||||
...f,
|
||||
variables: f.variables.map((v) => (v.name === key ? { ...v, cell } : v)),
|
||||
}));
|
||||
},
|
||||
release: (kind, key) =>
|
||||
x.patch((f) =>
|
||||
kind === "in"
|
||||
? { ...f, inputs: f.inputs.map((i) => (i.port === key ? { ...i, cell: "" } : i)) }
|
||||
: kind === "var"
|
||||
? { ...f, variables: f.variables.filter((v) => v.name !== key) }
|
||||
: { ...f, output: { ...f.output, cell: "" } },
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
/** 우클릭 「조건 만들기」 에 주는 것 — 칸에 올 값 목록(알약이면 valueChoices · 변수면 고르기 목록) · 이름 붙은 칸 */
|
||||
export function conditionsOf(x: ExtraContext): ConditionOptions {
|
||||
return {
|
||||
valuesFor: (a1) => {
|
||||
const f = boxOf(x)?.func;
|
||||
const at = a1.toUpperCase();
|
||||
const v = f?.variables.find((y) => y.cell?.toUpperCase() === at);
|
||||
if (v) return v.type === "choice" && v.choices?.length ? [...v.choices] : null;
|
||||
const port = f?.inputs.find((i) => i.cell.toUpperCase() === at)?.port;
|
||||
const link = port
|
||||
? x.lg.links.find((l) => l.to.box === x.boxId && l.to.name === port)
|
||||
: undefined;
|
||||
return link ? valueChoices(x.lg, link.from, { values: x.detail.values }) : null;
|
||||
},
|
||||
pills: () => {
|
||||
const f = boxOf(x)?.func;
|
||||
return [
|
||||
...(f?.inputs ?? []).filter((i) => i.cell).map((i) => ({ name: i.port, cell: i.cell })),
|
||||
...(f?.variables ?? []).filter((v) => v.cell).map((v) => ({ name: v.name, cell: v.cell! })),
|
||||
];
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -111,11 +111,11 @@ button.m02-dfpanel-chip:focus-visible {
|
||||
}
|
||||
|
||||
.m02-dfpanel-chip.is-out {
|
||||
border-color: #f59e0b;
|
||||
border-color: #27ae60;
|
||||
}
|
||||
|
||||
.m02-dfpanel-chip.is-var {
|
||||
border-color: #22a06b;
|
||||
border-color: #d99a00;
|
||||
}
|
||||
|
||||
/* 판 높이 크게 — 안쪽 세로 스크롤 없이 보이게 */
|
||||
@@ -133,11 +133,11 @@ button.m02-dfpanel-chip:focus-visible {
|
||||
}
|
||||
|
||||
.m02-dfpanel .aislo-cell[data-cf-kind="var"] {
|
||||
box-shadow: inset 0 0 0 2px #22a06b;
|
||||
box-shadow: inset 0 0 0 2px #d99a00;
|
||||
}
|
||||
|
||||
.m02-dfpanel .aislo-cell[data-cf-kind="out"] {
|
||||
box-shadow: inset 0 0 0 2px #f59e0b;
|
||||
box-shadow: inset 0 0 0 2px #27ae60;
|
||||
}
|
||||
|
||||
.m02-dfpanel .aislo-cell[data-cf-kind]::after {
|
||||
@@ -170,3 +170,112 @@ button.m02-dfpanel-chip:focus-visible {
|
||||
outline: 2px dashed var(--color-danger);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* 시트 + 칸 연결 표 — 좁으면 표가 아래로 */
|
||||
.m02-dfpanel-body {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-12);
|
||||
}
|
||||
|
||||
.m02-dfpanel-body > .m02-dfpanel-sheet {
|
||||
flex: 1 1 28em;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.m02-dfcells {
|
||||
flex: 0 1 24em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.m02-dfcells__title {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.m02-dfcells__table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.m02-dfcells__table th,
|
||||
.m02-dfcells__table td {
|
||||
padding: var(--spacing-4);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.m02-dfcells__cell {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
.m02-dfcells .ui-input.is-bad {
|
||||
border-color: var(--color-danger);
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.m02-dfcells__kind {
|
||||
padding: 0 var(--spacing-8);
|
||||
border: 2px solid;
|
||||
border-radius: var(--radius-pills);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.m02-dfcells__row.is-in .m02-dfcells__kind {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.m02-dfcells__row.is-var .m02-dfcells__kind {
|
||||
border-color: #d99a00;
|
||||
}
|
||||
|
||||
.m02-dfcells__row.is-out .m02-dfcells__kind {
|
||||
border-color: #27ae60;
|
||||
}
|
||||
|
||||
/* 식 보기 */
|
||||
.m02-df-formula {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.m02-df-formula__title {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.m02-df-formula__line {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding-top: var(--spacing-4);
|
||||
border-top: 1px dashed var(--color-border);
|
||||
}
|
||||
|
||||
.m02-df-formula__valued {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.m02-df-formula__branches {
|
||||
margin: 0;
|
||||
padding-left: var(--spacing-16);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.m02-df-formula__branches .is-now {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.m02-df-formula__empty {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import type {
|
||||
Workbook,
|
||||
} from "../../A00_Common/spreadsheet/spreadsheet";
|
||||
import type { CfBox, CfFunc, CfLogic, CfRound, CfVariable } from "./M02_DF_Model";
|
||||
import type { DetailOptions } from "./M02_DF_Detail";
|
||||
import { conditionsOf, drawCells, drawFormula, type ExtraContext } from "./M02_DF_FuncExtra";
|
||||
import { createRoundRow } from "./M02_DF_RoundRow";
|
||||
import { createUnitPick, suggestPlaces } from "./M02_DF_UnitPick";
|
||||
import { ERR_KEY, FRAMES, TEXT, tf } from "./M02_DF_FuncText";
|
||||
@@ -37,6 +39,8 @@ interface State {
|
||||
lg: CfLogic;
|
||||
on: Handlers;
|
||||
readOnly: boolean;
|
||||
/** 셈에 넘길 것 — 값 묶음 · 다른 로직들 */
|
||||
detail: DetailOptions;
|
||||
handle: SpreadsheetHandle | null;
|
||||
/** 수식 칸 계산값 — 바뀐 뒤에만 새로 받음(받으면 격자가 다시 그려져, 표시 쪽에서 부르면 맴돎) */
|
||||
snap: CalcValues;
|
||||
@@ -48,6 +52,8 @@ interface State {
|
||||
suggest: number | null;
|
||||
parts: {
|
||||
head: HTMLElement;
|
||||
formula: HTMLElement;
|
||||
cells: HTMLElement;
|
||||
got: HTMLElement;
|
||||
vars: HTMLElement;
|
||||
bar: HTMLElement;
|
||||
@@ -201,6 +207,20 @@ function problems(st: State): string[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ------------------------------ 식 보기 · 칸 연결 표 · 조건 만들기 ------------------------------ */
|
||||
|
||||
/** 보조 파일(M02_DF_FuncExtra)에 넘기는 손잡이 */
|
||||
const extra = (st: State): ExtraContext => ({
|
||||
lg: st.lg,
|
||||
boxId: st.boxId,
|
||||
detail: st.detail,
|
||||
readOnly: st.readOnly,
|
||||
place: (port, cell) => placePort(st, port, cell),
|
||||
setOutput: (cell) => setOutput(st, cell),
|
||||
patch: (make) => patchFunc(st, make),
|
||||
used: (f) => usedCells(f),
|
||||
});
|
||||
|
||||
/* ------------------------------ 칸 위 표시 ------------------------------ */
|
||||
|
||||
function applyMarks(st: State): void {
|
||||
@@ -563,6 +583,7 @@ async function mountSheet(st: State): Promise<void> {
|
||||
preview: true,
|
||||
tabs: false,
|
||||
readOnly: st.readOnly,
|
||||
conditions: conditionsOf(extra(st)),
|
||||
onChange: (b) => onBook(st, b),
|
||||
onSelect: (s) => {
|
||||
st.cell = s.칸;
|
||||
@@ -593,13 +614,14 @@ export function renderFuncPanel(
|
||||
lg: CfLogic,
|
||||
boxId: string,
|
||||
on: Handlers,
|
||||
opts: { readOnly?: boolean } = {},
|
||||
opts: DetailOptions & { readOnly?: boolean } = {},
|
||||
): void {
|
||||
const old = states.get(host);
|
||||
const readOnly = !!opts.readOnly;
|
||||
if (old && old.boxId === boxId && old.readOnly === readOnly) {
|
||||
old.lg = lg;
|
||||
old.on = on;
|
||||
old.detail = opts;
|
||||
const f = funcOf(old);
|
||||
if (old.handle && f && sheetJson(f) !== old.json) {
|
||||
old.json = sheetJson(f);
|
||||
@@ -615,6 +637,8 @@ export function renderFuncPanel(
|
||||
}
|
||||
const parts: State["parts"] = {
|
||||
head: el("div", { className: "m02-dfpanel-head" }),
|
||||
formula: el("div"),
|
||||
cells: el("div"),
|
||||
got: el("div", { className: "m02-dfpanel-row" }),
|
||||
vars: el("div", { className: "m02-dfpanel-vars" }),
|
||||
bar: el("div", { className: "m02-dfpanel-bar" }),
|
||||
@@ -626,6 +650,7 @@ export function renderFuncPanel(
|
||||
lg,
|
||||
on,
|
||||
readOnly,
|
||||
detail: opts,
|
||||
handle: null,
|
||||
snap: {},
|
||||
json: "",
|
||||
@@ -639,12 +664,22 @@ export function renderFuncPanel(
|
||||
drawGot(st);
|
||||
drawVars(st);
|
||||
drawBar(st);
|
||||
drawFormula(st.parts.formula, extra(st));
|
||||
drawCells(st.parts.cells, extra(st));
|
||||
applyMarks(st);
|
||||
},
|
||||
};
|
||||
states.set(host, st);
|
||||
host.classList.add("m02-dfpanel");
|
||||
host.replaceChildren(parts.head, parts.got, parts.vars, parts.bar, parts.msg, parts.sheet);
|
||||
host.replaceChildren(
|
||||
parts.head,
|
||||
parts.formula,
|
||||
parts.got,
|
||||
parts.vars,
|
||||
parts.bar,
|
||||
parts.msg,
|
||||
el("div", { className: "m02-dfpanel-body", children: [parts.sheet, parts.cells] }),
|
||||
);
|
||||
parts.msg.hidden = true;
|
||||
st.redraw();
|
||||
void mountSheet(st);
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,12 @@
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import {
|
||||
gateConds,
|
||||
makeGate,
|
||||
portsOut,
|
||||
reaches,
|
||||
valueChoices,
|
||||
type CfGateCond,
|
||||
type CfGateOp,
|
||||
type CfLogic,
|
||||
type CfPort,
|
||||
@@ -77,6 +81,12 @@ const TEXT = {
|
||||
],
|
||||
WarnTake: ["꺼낼 칸을 고르지 않음", "No column to take"],
|
||||
Count: ["선", "Line"],
|
||||
And: ["그리고", "and"],
|
||||
Or: ["또는", "or"],
|
||||
If: ["조건", "If"],
|
||||
AddLine: ["+ 조건 줄", "+ Condition line"],
|
||||
Join: ["잇는 말", "Join with"],
|
||||
OutRange: ["범위 {n} 밖", "Outside the range {n}"],
|
||||
Range: ["{n} (범위)", "{n} (range)"],
|
||||
} as const;
|
||||
|
||||
@@ -92,6 +102,7 @@ interface Ctx {
|
||||
linkId: string;
|
||||
readOnly: boolean;
|
||||
headers: string[];
|
||||
detail: DetailOptions;
|
||||
change(lg: CfLogic): void;
|
||||
}
|
||||
|
||||
@@ -240,28 +251,122 @@ function lookupEditor(ctx: Ctx, rule: Extract<CfRule, { kind: "lookup" }>): HTML
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 보낸 알약이 입력 상자의 숫자 변수면 그 범위 */
|
||||
function rangeOf(ctx: Ctx, p: CfPort): [number, number] | null {
|
||||
const v = ctx.lg.boxes
|
||||
.find((b) => b.id === p.box)
|
||||
?.input?.variables.find((x) => x.name === p.name);
|
||||
return v?.type === "number" && v.range ? v.range : null;
|
||||
}
|
||||
|
||||
/** 값 칸 — 목록이 있으면 고르기 · 없으면 숫자 칸(범위 밖이면 한 줄 알림) */
|
||||
function gateValue(ctx: Ctx, c: CfGateCond, onPick: (v: number | string) => void): HTMLElement[] {
|
||||
const list = valueChoices(ctx.lg, c.from, { values: ctx.detail.values });
|
||||
if (list?.length) {
|
||||
const key = (v: string | number): string => `${typeof v}:${v}`;
|
||||
const options = list.map((v) => ({ value: key(v), text: String(v) }));
|
||||
return [
|
||||
select(
|
||||
ctx,
|
||||
options,
|
||||
key(c.value),
|
||||
(k) => {
|
||||
const hit = list.find((v) => key(v) === k);
|
||||
if (hit !== undefined) onPick(hit);
|
||||
},
|
||||
tk("Value"),
|
||||
),
|
||||
];
|
||||
}
|
||||
const range = rangeOf(ctx, c.from);
|
||||
const out: HTMLElement[] = [numberBox(ctx, c.value, onPick, tk("Value"))];
|
||||
const n = Number(c.value);
|
||||
if (range && Number.isFinite(n) && (n < range[0] || n > range[1])) {
|
||||
out.push(
|
||||
el("span", {
|
||||
className: "m02-dflink-warn",
|
||||
text: `⚠ ${tk("OutRange", `${range[0]} ~ ${range[1]}`)}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function gateEditor(ctx: Ctx, rule: Extract<CfRule, { kind: "gate" }>): HTMLElement[] {
|
||||
const put = (next: Partial<typeof rule>): void => setRule(ctx, { ...rule, ...next });
|
||||
return [
|
||||
el("p", { className: "m02-dflink-note", text: tk("GateHint") }),
|
||||
row(
|
||||
select(
|
||||
ctx,
|
||||
pillOptions(ctx),
|
||||
toKey(rule.from),
|
||||
(v) => put({ from: fromKey(v) }),
|
||||
tk("Match"),
|
||||
const g = gateConds(rule);
|
||||
const put = (conds: CfGateCond[], join = g.join): void => setRule(ctx, makeGate(conds, join));
|
||||
const edit = (i: number, patch: Partial<CfGateCond>): void =>
|
||||
put(g.conds.map((c, j) => (j === i ? { ...c, ...patch } : c)));
|
||||
const out: HTMLElement[] = [el("p", { className: "m02-dflink-note", text: tk("GateHint") })];
|
||||
g.conds.forEach((c, i) => {
|
||||
out.push(
|
||||
row(
|
||||
el("span", {
|
||||
className: "m02-dflink-label",
|
||||
text: i ? tk(g.join === "그리고" ? "And" : "Or") : tk("If"),
|
||||
}),
|
||||
select(
|
||||
ctx,
|
||||
pillOptions(ctx),
|
||||
toKey(c.from),
|
||||
(v) => {
|
||||
const from = fromKey(v);
|
||||
edit(i, {
|
||||
from,
|
||||
value: valueChoices(ctx.lg, from, { values: ctx.detail.values })?.[0] ?? 0,
|
||||
});
|
||||
},
|
||||
tk("Match"),
|
||||
),
|
||||
select(
|
||||
ctx,
|
||||
GATE_OPS.map((o) => ({ value: o, text: o })),
|
||||
c.op,
|
||||
(v) => edit(i, { op: v as CfGateOp }),
|
||||
tk("Kinds"),
|
||||
),
|
||||
...gateValue(ctx, c, (v) => edit(i, { value: v })),
|
||||
ctx.readOnly || g.conds.length < 2
|
||||
? ""
|
||||
: createButton({
|
||||
label: tk("Remove"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => put(g.conds.filter((_, j) => j !== i)),
|
||||
}),
|
||||
),
|
||||
select(
|
||||
ctx,
|
||||
GATE_OPS.map((o) => ({ value: o, text: o })),
|
||||
rule.op,
|
||||
(v) => put({ op: v as CfGateOp }),
|
||||
tk("Kinds"),
|
||||
);
|
||||
});
|
||||
if (!ctx.readOnly) {
|
||||
const first = g.conds[0];
|
||||
out.push(
|
||||
row(
|
||||
createButton({
|
||||
label: tk("AddLine"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () =>
|
||||
put([...g.conds, { from: first?.from ?? { box: "", name: "" }, op: "=", value: 0 }]),
|
||||
}),
|
||||
...(g.conds.length > 1
|
||||
? [
|
||||
el("span", { className: "m02-dflink-label", text: tk("Join") }),
|
||||
select(
|
||||
ctx,
|
||||
[
|
||||
{ value: "그리고", text: tk("And") },
|
||||
{ value: "또는", text: tk("Or") },
|
||||
],
|
||||
g.join,
|
||||
(v) => put(g.conds, v as "그리고" | "또는"),
|
||||
tk("Join"),
|
||||
),
|
||||
]
|
||||
: []),
|
||||
),
|
||||
numberBox(ctx, rule.value, (v) => put({ value: v }), tk("Value")),
|
||||
),
|
||||
];
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function scaleEditor(ctx: Ctx, rule: Extract<CfRule, { kind: "scale" }>): HTMLElement[] {
|
||||
@@ -323,7 +428,7 @@ function roundEditor(
|
||||
|
||||
const DEFAULT: Record<Exclude<Kind, "none">, (from: CfPort) => CfRule> = {
|
||||
lookup: () => ({ kind: "lookup", conds: [], col: "" }),
|
||||
gate: (from) => ({ kind: "gate", from, op: "≥", value: 0 }),
|
||||
gate: (from) => makeGate([{ from, op: "≥", value: 0 }]),
|
||||
scale: () => ({ kind: "scale", op: "×", value: 1 }),
|
||||
round: () => ({ kind: "round", places: 0, method: "반올림" }),
|
||||
};
|
||||
@@ -345,14 +450,23 @@ export function renderLinkPanel(
|
||||
Array.isArray(table) && Array.isArray(table[0])
|
||||
? table[0].map((h) => String(h ?? "")).filter(Boolean)
|
||||
: [];
|
||||
const ctx: Ctx = { lg, linkId, readOnly: !!opts.readOnly, headers, change: on.change };
|
||||
const ctx: Ctx = {
|
||||
lg,
|
||||
linkId,
|
||||
readOnly: !!opts.readOnly,
|
||||
headers,
|
||||
detail: opts,
|
||||
change: on.change,
|
||||
};
|
||||
const rule = link.rule;
|
||||
const kind: Kind = rule?.kind ?? "none";
|
||||
const parts: HTMLElement[] = [
|
||||
el("div", {
|
||||
className: "m02-dflink-head",
|
||||
children: [
|
||||
el("strong", { text: `${portText(lg, link.from)} → ${portText(lg, link.to)}` }),
|
||||
el("strong", {
|
||||
text: `${portText(lg, link.from)} → ${rule ? `(${labelOf(rule)}) → ` : ""}${portText(lg, link.to)}`,
|
||||
}),
|
||||
el("span", { className: "m02-dflink-note", text: `(${unitOf(lg, link.from)})` }),
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/* 오른쪽 흐름도 패널 — 부르는 쪽 칸 안에서 100% */
|
||||
.m02-dfp {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
border-left: 1px solid var(--color-border);
|
||||
background: var(--color-canvas);
|
||||
}
|
||||
|
||||
.m02-dfp__open {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
min-width: 0;
|
||||
padding: var(--spacing-8) var(--spacing-12) var(--spacing-12);
|
||||
}
|
||||
|
||||
.m02-dfp__head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.m02-dfp__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.m02-dfp__tools {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
/* 좁은 패널 — 다이어그램 판 높이를 패널에 맞춤 */
|
||||
.m02-dfp .m02-dfdg__view {
|
||||
height: max(320px, 45vh);
|
||||
}
|
||||
|
||||
.m02-dfp__lock {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.m02-dfp__cost > summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
padding: var(--spacing-4) 0;
|
||||
}
|
||||
|
||||
/* 왼쪽 가장자리 끌기 */
|
||||
.m02-dfp__grip {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -4px;
|
||||
z-index: 2;
|
||||
width: 8px;
|
||||
cursor: col-resize;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.m02-dfp__grip:hover,
|
||||
.m02-dfp__grip:focus-visible,
|
||||
.m02-dfp.is-resizing .m02-dfp__grip {
|
||||
background: color-mix(in srgb, var(--color-accent) 35%, transparent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.m02-dfp.is-resizing {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* 접기 = 얇은 띠 */
|
||||
.m02-dfp__strip {
|
||||
width: 100%;
|
||||
padding: var(--spacing-12) 0;
|
||||
border: 0;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-secondary);
|
||||
font: inherit;
|
||||
writing-mode: vertical-rl;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.m02-dfp__strip:hover,
|
||||
.m02-dfp__strip:focus-visible {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.m02-dfp__strip[hidden],
|
||||
.m02-dfp__open[hidden],
|
||||
.m02-dfp__grip[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 크게 보기 — 화면 전체 */
|
||||
.m02-dfp-full {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-12) var(--spacing-16);
|
||||
overflow: auto;
|
||||
background: var(--color-canvas);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.m02-dfp-full__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.m02-dfp-full__head strong {
|
||||
flex: 1;
|
||||
font-size: var(--text-body);
|
||||
}
|
||||
|
||||
.m02-dfp-full__diagram .m02-dfdg__view {
|
||||
height: max(420px, 60vh);
|
||||
}
|
||||
|
||||
.m02-dfp-full__lower {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
|
||||
gap: var(--spacing-16);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.m02-dfp-full__lower h3 {
|
||||
margin: 0 0 var(--spacing-8);
|
||||
font-size: var(--text-body);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.m02-dfp-full__lower {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/* =============================================================================
|
||||
* M02_DF_Panel.ts
|
||||
* 오른쪽 흐름도 패널(흐름도 적용 정함 5) — mountFlowPanel(host, opts) → { set, destroy, expand }
|
||||
* 패널 안 세로: 머리(제목 · 「크게 보기」 · 접기) → 다이어그램(+ 입력 · 함수 · 출력 · 실행 취소 · 맞추기) →
|
||||
* 상세 내용(상자 · 알약 · 선 판) → 원가 · 검증 칸(접기). 저장 단추 없음(페이지 [저장]) · 바뀔 때마다 onChange.
|
||||
* 너비 = 부르는 쪽 칸 안 100% · 왼쪽 가장자리 끌기 → onResize(px) · 마지막 너비 localStorage ·
|
||||
* 접기 = 얇은 띠(「흐름도 ▸」) · 크게 보기 = 화면 전체(같은 흐름 객체 · 닫으면 패널로 · 고친 것 그대로).
|
||||
* 고치는 핵심은 `M02_DF_Paper.ts`(판 DOM 을 옮겨 끼움).
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import type { CfLogic } from "./M02_DF_Model";
|
||||
import { createPaper } from "./M02_DF_Paper";
|
||||
import "./M02_DF_Panel.css";
|
||||
|
||||
const TEXT = {
|
||||
Big: ["크게 보기", "Full screen"],
|
||||
Fold: ["접기", "Collapse"],
|
||||
Strip: ["흐름도 ▸", "Flow ▸"],
|
||||
StripTitle: ["흐름도 펴기", "Open the flow panel"],
|
||||
Close: ["닫기", "Close"],
|
||||
Detail: ["상세 내용", "Details"],
|
||||
Cost: ["원가 · 검증", "Cost · check"],
|
||||
Grip: ["패널 너비 — 끌거나 화살표 키", "Panel width — drag or arrow keys"],
|
||||
} as const;
|
||||
|
||||
const tf = (key: keyof typeof TEXT): string =>
|
||||
TEXT[key][currentLanguageIndex as 0 | 1] ?? TEXT[key][0];
|
||||
|
||||
export interface FlowPanelOpts {
|
||||
logic: CfLogic;
|
||||
readOnly?: boolean;
|
||||
title: string;
|
||||
/** 고칠 때마다 — 객체 그대로(페이지 캐시에 쌓음 · [저장] 은 페이지) */
|
||||
onChange(lg: CfLogic): void;
|
||||
/** 다른 로직 입력이 부르는 로직 */
|
||||
logics?(key: string): Promise<CfLogic | null>;
|
||||
/** 패널 너비가 바뀜(끌기 · 접기 · 펴기) — 부르는 쪽이 칸 너비를 맞춤 */
|
||||
onResize?(px: number): void;
|
||||
}
|
||||
|
||||
export interface FlowPanel {
|
||||
/** 밖에서 바꿈(고친 것 버리기 · 다른 로직 열기) */
|
||||
set(lg: CfLogic): void;
|
||||
destroy(): void;
|
||||
/** 크게 보기 열기 */
|
||||
expand(): void;
|
||||
}
|
||||
|
||||
/* ── 너비(DOM 없음) ─────────────────────────────────────────────────── */
|
||||
|
||||
export const PANEL_MIN = 320;
|
||||
export const STRIP_WIDTH = 40;
|
||||
/** 좁은 패널에서 저절로 맞출 때 가장 작은 확대 */
|
||||
export const PANEL_FIT_MIN = 0.25;
|
||||
const WIDTH_KEY = "m02.dfpanel.width";
|
||||
const FOLD_KEY = "m02.dfpanel.folded";
|
||||
|
||||
/** 끌어서 정한 너비 — 왼쪽 가장자리를 왼쪽으로 끌면 넓어짐 · 가장 작게 PANEL_MIN · 가장 크게 화면의 85% */
|
||||
export function dragWidth(start: number, startX: number, x: number, viewport: number): number {
|
||||
const max = Math.max(PANEL_MIN, Math.round(viewport * 0.85));
|
||||
return Math.min(max, Math.max(PANEL_MIN, Math.round(start + (startX - x))));
|
||||
}
|
||||
|
||||
/** 저장해 둔 너비 — 숫자가 아니거나 너무 작으면 null */
|
||||
export function storedWidth(raw: string | null): number | null {
|
||||
const n = Number(raw);
|
||||
return raw && Number.isFinite(n) && n >= PANEL_MIN ? Math.round(n) : null;
|
||||
}
|
||||
|
||||
const readStore = (key: string): string | null => {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const writeStore = (key: string, value: string): void => {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
} catch {
|
||||
/* 저장 칸이 막힌 브라우저 — 이번 화면에서만 */
|
||||
}
|
||||
};
|
||||
|
||||
/* ── 패널 ───────────────────────────────────────────────────────────── */
|
||||
|
||||
export function mountFlowPanel(host: HTMLElement, opts: FlowPanelOpts): FlowPanel {
|
||||
/** 크게 보기 화면 — 열려 있을 때만 */
|
||||
let full: HTMLElement | null = null;
|
||||
const paper = createPaper({
|
||||
logic: opts.logic,
|
||||
readOnly: opts.readOnly,
|
||||
onChange: opts.onChange,
|
||||
logics: opts.logics,
|
||||
// 좁은 패널은 「한눈에」 가 되게 더 작게 · 크게 보기는 글이 읽히게 90% 아래로 안 내림
|
||||
fitMin: () => (full ? 0.9 : PANEL_FIT_MIN),
|
||||
});
|
||||
const { diagram, detail, summary, note, undo } = paper.parts;
|
||||
let width = storedWidth(readStore(WIDTH_KEY));
|
||||
let folded = readStore(FOLD_KEY) === "1";
|
||||
const tell = (): void => opts.onResize?.(folded ? STRIP_WIDTH : (width ?? host.clientWidth));
|
||||
|
||||
const sm = (label: string, onClick: () => void, variant: "ghost" | "filled" = "ghost") =>
|
||||
createButton({ label, size: "sm", variant, onClick });
|
||||
|
||||
/* 펼친 패널 */
|
||||
const costBox = el("details", {
|
||||
className: "m02-dfp__cost",
|
||||
attrs: { open: "" },
|
||||
children: [el("summary", { text: tf("Cost") })],
|
||||
});
|
||||
const tools = el("div", { className: "m02-dfp__tools", children: [undo] });
|
||||
const open = el("div", {
|
||||
className: "m02-dfp__open",
|
||||
children: [
|
||||
el("header", {
|
||||
className: "m02-dfp__head",
|
||||
children: [
|
||||
el("strong", { className: "m02-dfp__title", text: opts.title }),
|
||||
sm(tf("Big"), () => expand()),
|
||||
sm(tf("Fold"), () => setFolded(true)),
|
||||
],
|
||||
}),
|
||||
tools,
|
||||
note,
|
||||
diagram,
|
||||
detail,
|
||||
costBox,
|
||||
],
|
||||
});
|
||||
costBox.append(summary);
|
||||
|
||||
/* 접힌 띠 */
|
||||
const strip = el("button", {
|
||||
className: "m02-dfp__strip",
|
||||
text: tf("Strip"),
|
||||
attrs: { type: "button", title: tf("StripTitle"), "aria-label": tf("StripTitle") },
|
||||
});
|
||||
strip.addEventListener("click", () => setFolded(false));
|
||||
|
||||
/* 왼쪽 가장자리 — 끌기 · 화살표 키 */
|
||||
const grip = el("div", {
|
||||
className: "m02-dfp__grip",
|
||||
attrs: {
|
||||
role: "separator",
|
||||
"aria-orientation": "vertical",
|
||||
"aria-label": tf("Grip"),
|
||||
tabindex: "0",
|
||||
},
|
||||
});
|
||||
const setWidth = (px: number, save: boolean): void => {
|
||||
width = px;
|
||||
if (save) writeStore(WIDTH_KEY, String(px));
|
||||
tell();
|
||||
};
|
||||
grip.addEventListener("pointerdown", (ev) => {
|
||||
if (ev.button !== 0) return;
|
||||
ev.preventDefault();
|
||||
const start = root.getBoundingClientRect().width;
|
||||
const startX = ev.clientX;
|
||||
root.classList.add("is-resizing");
|
||||
const move = (m: PointerEvent): void =>
|
||||
setWidth(dragWidth(start, startX, m.clientX, window.innerWidth), false);
|
||||
const up = (u: PointerEvent): void => {
|
||||
window.removeEventListener("pointermove", move);
|
||||
window.removeEventListener("pointerup", up);
|
||||
root.classList.remove("is-resizing");
|
||||
setWidth(dragWidth(start, startX, u.clientX, window.innerWidth), true);
|
||||
paper.refit();
|
||||
};
|
||||
window.addEventListener("pointermove", move);
|
||||
window.addEventListener("pointerup", up);
|
||||
});
|
||||
grip.addEventListener("keydown", (ev) => {
|
||||
const step = ev.shiftKey ? 80 : 20;
|
||||
const d = ev.key === "ArrowLeft" ? step : ev.key === "ArrowRight" ? -step : 0;
|
||||
if (!d) return;
|
||||
ev.preventDefault();
|
||||
const now = root.getBoundingClientRect().width;
|
||||
setWidth(dragWidth(now, 0, -d, window.innerWidth), true);
|
||||
});
|
||||
|
||||
const root = el("aside", { className: "m02-dfp", children: [grip, open, strip] });
|
||||
root.addEventListener("keydown", (ev) => {
|
||||
if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === "z" && !ev.shiftKey) {
|
||||
if ((ev.target as HTMLElement).closest("input, textarea, [contenteditable]")) return;
|
||||
ev.preventDefault();
|
||||
paper.undo();
|
||||
}
|
||||
});
|
||||
|
||||
const setFolded = (on: boolean): void => {
|
||||
folded = on;
|
||||
writeStore(FOLD_KEY, on ? "1" : "0");
|
||||
root.classList.toggle("is-folded", on);
|
||||
open.hidden = on;
|
||||
strip.hidden = !on;
|
||||
grip.hidden = on;
|
||||
tell();
|
||||
if (!on) paper.refit();
|
||||
};
|
||||
|
||||
/* 크게 보기 — 같은 판을 화면 전체로 옮겨 끼우고 닫으면 되돌림 */
|
||||
const expand = (): void => {
|
||||
if (full) return;
|
||||
const close = sm(tf("Close"), () => shrink());
|
||||
full = el("div", {
|
||||
className: "m02-dfp-full",
|
||||
attrs: { role: "dialog", "aria-modal": "true", "aria-label": opts.title, tabindex: "-1" },
|
||||
children: [
|
||||
el("header", {
|
||||
className: "m02-dfp-full__head",
|
||||
children: [el("strong", { text: opts.title }), undo, close],
|
||||
}),
|
||||
note,
|
||||
el("div", { className: "m02-dfp-full__diagram", children: [diagram] }),
|
||||
el("div", {
|
||||
className: "m02-dfp-full__lower",
|
||||
children: [
|
||||
el("section", { children: [el("h3", { text: tf("Detail") }), detail] }),
|
||||
el("section", { children: [el("h3", { text: tf("Cost") }), summary] }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
full.addEventListener("keydown", (ev) => {
|
||||
if (ev.key === "Escape" && !ev.defaultPrevented) shrink();
|
||||
if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === "z" && !ev.shiftKey) {
|
||||
if ((ev.target as HTMLElement).closest("input, textarea, [contenteditable]")) return;
|
||||
ev.preventDefault();
|
||||
paper.undo();
|
||||
}
|
||||
});
|
||||
document.body.append(full);
|
||||
full.focus();
|
||||
paper.refit();
|
||||
};
|
||||
const shrink = (): void => {
|
||||
if (!full) return;
|
||||
full.remove();
|
||||
full = null;
|
||||
tools.append(undo);
|
||||
costBox.append(summary);
|
||||
// 머리 · 실행 취소 줄 뒤에 판을 처음 차례대로 되돌려 끼움
|
||||
open.replaceChildren(
|
||||
open.firstElementChild as HTMLElement,
|
||||
tools,
|
||||
note,
|
||||
diagram,
|
||||
detail,
|
||||
costBox,
|
||||
);
|
||||
paper.refit();
|
||||
};
|
||||
|
||||
host.replaceChildren(root);
|
||||
setFolded(folded);
|
||||
|
||||
return {
|
||||
set: (lg) => paper.set(lg),
|
||||
destroy: () => {
|
||||
shrink();
|
||||
root.remove();
|
||||
},
|
||||
expand,
|
||||
};
|
||||
}
|
||||
@@ -53,6 +53,8 @@ export interface PaperOpts {
|
||||
onChange(lg: CfLogic): void;
|
||||
/** 다른 로직 입력이 부르는 로직 — 없으면 흐름도 저장본(DF-) 만 */
|
||||
logics?(key: string): Promise<CfLogic | null>;
|
||||
/** 저절로 맞출 때 가장 작은 확대 — 부를 때마다 물음(패널 · 크게 보기가 다름) */
|
||||
fitMin?(): number;
|
||||
}
|
||||
|
||||
export interface Paper {
|
||||
@@ -304,6 +306,7 @@ export function createPaper(opts: PaperOpts): Paper {
|
||||
breaks: broken,
|
||||
readOnly,
|
||||
fitKey,
|
||||
fitMin: opts.fitMin?.(),
|
||||
});
|
||||
drawPanel();
|
||||
renderSummary(summaryHost, lg, result, { change: editExample, select: choose }, { values });
|
||||
|
||||
@@ -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