Merge remote-tracking branch 'origin/dev' into sub_laptop_4
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
import {
|
||||
ApiError,
|
||||
fetchLogic,
|
||||
fetchLogicFiles,
|
||||
fetchLogics,
|
||||
fetchLogicSubs,
|
||||
saveFiles,
|
||||
@@ -37,7 +38,17 @@ import { buildLabDetail, isOwnLogic } from "./M01_MasterData_UI_LogicLab_Detail"
|
||||
import { setLogicOpener } from "./M01_MasterData_UI_LogicLab_Modal";
|
||||
import { openCopyScreen } from "./M01_MasterData_UI_LogicLab_Copy";
|
||||
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
|
||||
import { buildSplit } from "./M01_MasterData_UI_LogicLab_Flow";
|
||||
import {
|
||||
buildSplit,
|
||||
emptyFlow,
|
||||
fetchFlow,
|
||||
mountLogicFlow,
|
||||
tf,
|
||||
type LogicFlow,
|
||||
} from "./M01_MasterData_UI_LogicLab_Flow";
|
||||
import { askBasicInfo } from "./M01_MasterData_UI_LogicLab_Basic";
|
||||
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
||||
import type { CfLogic } from "../M02_MasterTemplete/DataFlow/M02_DF_Model";
|
||||
import "./M01_MasterData_UI_Logic_Style.css";
|
||||
|
||||
/** 저장 안 한 로직 하나 — origKey null = 새 로직 · row null = 지움 */
|
||||
@@ -62,8 +73,14 @@ interface Opened extends Draft {
|
||||
lastText?: TextAnswer;
|
||||
ownHost: HTMLElement;
|
||||
values: Record<string, string>;
|
||||
/** 흐름 로직의 왼쪽 「지금 내용」 자리 — 흐름도 왼쪽 보기가 그림 */
|
||||
flowHost: HTMLElement;
|
||||
}
|
||||
|
||||
/** 줄의 흐름(있으면 흐름도가 정본) */
|
||||
const flowOf = (row: LogicRow | null): CfLogic | null => (row?.흐름 as CfLogic | undefined) ?? null;
|
||||
const OWN_FILE = "로직_자체.json";
|
||||
|
||||
const cacheKey = "m01_logic_drafts_lab";
|
||||
const ownHost = (): HTMLElement => el("div", { className: "m01lab__own" });
|
||||
|
||||
@@ -90,6 +107,11 @@ export async function mountM01LogicLab(
|
||||
let drafts = loadDrafts();
|
||||
let opened: Opened | null = null;
|
||||
let calcInputs = "";
|
||||
/** 오른쪽 흐름도 패널 — 로직을 열 때마다 새로 */
|
||||
let flowNow: LogicFlow | null = null;
|
||||
const admin = await fetchSessionUser()
|
||||
.then((u) => u?.role === "SYSTEM_ADMIN")
|
||||
.catch(() => false);
|
||||
let recalc: number | undefined;
|
||||
const editor = el("div", { className: "m01-logic__editor" });
|
||||
// 로직 상세 두 칸 — 왼쪽 지금 내용 · 오른쪽 흐름도(패널 자리) · 너비 비율 기억
|
||||
@@ -210,14 +232,54 @@ export async function mountM01LogicLab(
|
||||
ownHost: current.ownHost,
|
||||
onChange: touch,
|
||||
calcHost: calc,
|
||||
flowHost: flowOf(current.row) ? current.flowHost : undefined,
|
||||
});
|
||||
flowNow?.redrawLeft();
|
||||
};
|
||||
|
||||
/** 오른쪽 흐름도 — 줄의 흐름 · 없으면 서버 변환(흐름 받기) · 그것도 없으면 빈 흐름 */
|
||||
const startFlow = async (o: Opened): Promise<void> => {
|
||||
const row = o.row;
|
||||
if (!row) return;
|
||||
split.right.replaceChildren(el("p", { className: "m01lab-flow__wait", text: tf("Waiting") }));
|
||||
const own = flowOf(row);
|
||||
const got = own || o.origKey === null ? null : await fetchFlow(o.origKey, o.file);
|
||||
const flow = own ?? got?.flow ?? (await emptyFlow(row.이름, row.결과단위));
|
||||
if (opened !== o) return;
|
||||
try {
|
||||
const made = await mountLogicFlow({
|
||||
split,
|
||||
flow: clone(flow),
|
||||
title: row.이름 || row.키,
|
||||
readOnly: !admin,
|
||||
notes: got?.notes ?? [],
|
||||
converted: !!got?.converted,
|
||||
leftHost: () => (flowOf(o.row) ? o.flowHost : null),
|
||||
onChange: (lg) => {
|
||||
if (!o.row || opened !== o) return;
|
||||
const was = flowOf(o.row);
|
||||
o.row.흐름 = lg;
|
||||
touch();
|
||||
if (!was) drawEditor(); // 이제 흐름 로직 — 왼쪽을 흐름 보기로
|
||||
},
|
||||
});
|
||||
if (opened !== o) return made.destroy();
|
||||
flowNow = made;
|
||||
} catch (e) {
|
||||
split.right.replaceChildren(
|
||||
el("p", { className: "m01-logic__muted", text: tf("Failed", String(e)) }),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const show = (next: Opened | null): void => {
|
||||
flowNow?.destroy();
|
||||
flowNow = null;
|
||||
opened = next;
|
||||
errors.hidden = true;
|
||||
drawEditor();
|
||||
drawCalc();
|
||||
if (next?.row) void startFlow(next);
|
||||
};
|
||||
|
||||
const open = async (id: string, sub: string, key: string): Promise<void> => {
|
||||
@@ -233,6 +295,7 @@ export async function mountM01LogicLab(
|
||||
text: null,
|
||||
ownHost: ownHost(),
|
||||
values: {},
|
||||
flowHost: el("div"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -252,6 +315,7 @@ export async function mountM01LogicLab(
|
||||
text: null,
|
||||
ownHost: ownHost(),
|
||||
values: {},
|
||||
flowHost: el("div"),
|
||||
});
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
|
||||
@@ -330,12 +394,18 @@ export async function mountM01LogicLab(
|
||||
}
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
await saveFiles([...byFile.values()]);
|
||||
// 키 없는 새 줄은 서버가 새 키를 줌(added) — 저장 뒤 그 로직을 엶
|
||||
const saved = (await saveFiles([...byFile.values()])) as {
|
||||
added?: { file: string; key: string }[];
|
||||
};
|
||||
const was = opened;
|
||||
drafts = {};
|
||||
await reload();
|
||||
showToast(tx("Save_Done"), "success");
|
||||
if (was?.row?.키) await open(logicId(was.sub, was.row.키), was.sub, was.row.키);
|
||||
const newKey = was?.row && !was.row.키 ? saved.added?.at(-1)?.key : undefined;
|
||||
const made = newKey ? allLogics.find((x) => x.키 === newKey) : undefined;
|
||||
if (made) await open(logicId(made.구분, made.키), made.구분, made.키);
|
||||
else if (was?.row?.키) await open(logicId(was.sub, was.row.키), was.sub, was.row.키);
|
||||
else show(null);
|
||||
} catch (error) {
|
||||
failed(error);
|
||||
@@ -360,6 +430,28 @@ export async function mountM01LogicLab(
|
||||
}
|
||||
};
|
||||
|
||||
const onNew = async (): Promise<void> => {
|
||||
const units = [...new Set(allLogics.map((x) => x.결과단위).filter(Boolean))];
|
||||
const info = await askBasicInfo(units.length ? units : ["원"]);
|
||||
if (!info) return;
|
||||
try {
|
||||
const own = (await fetchLogicFiles()).find((f) => f.file === OWN_FILE);
|
||||
const row: LogicRow = {
|
||||
키: "",
|
||||
...info,
|
||||
입력: [],
|
||||
중간: [],
|
||||
흐름: await emptyFlow(info.이름, info.결과단위),
|
||||
};
|
||||
const id = logicId("자체", `새${Date.now()}`);
|
||||
drafts[id] = { sub: "자체", file: OWN_FILE, version: own?.version ?? "", origKey: null, row };
|
||||
persist();
|
||||
await open(id, "자체", "");
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
|
||||
}
|
||||
};
|
||||
|
||||
const saveButton = createButton({ label: tx("Bar_Save"), onClick: () => void onSave() });
|
||||
const discardButton = createButton({
|
||||
label: tx("Bar_Discard"),
|
||||
@@ -386,6 +478,7 @@ export async function mountM01LogicLab(
|
||||
back,
|
||||
el("h2", { text: tx("Title") }),
|
||||
pending,
|
||||
createButton({ label: tf("New"), variant: "filled", onClick: () => void onNew() }),
|
||||
copyButton,
|
||||
createButton({ label: tx("Bar_Delete"), variant: "danger", onClick: () => void onDelete() }),
|
||||
discardButton,
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_LogicLab_Basic.ts
|
||||
* 「신규 로직」 기본정보 칸 — 이름 · 원문번호 · 결과 단위 · 출처(구분 = 자체). 확인하면 값을 돌려줌 · 취소면 null
|
||||
* 저장은 하지 않음 — 부른 쪽이 빈 흐름도와 함께 캐시(drafts)에 새 줄로 넣고 [저장] 때 서버가 GX 새 키를 줌.
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, createInputField, createSelectField, el } from "@ui/ui_template_elements";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
|
||||
const TEXT = {
|
||||
Title: ["신규 로직", "New logic"],
|
||||
Name: ["이름", "Name"],
|
||||
NamePh: ["예: 돌쌓기 메쌓기(인력)", "e.g. dry stone wall (labour)"],
|
||||
Number: ["원문번호", "Source number"],
|
||||
NumberPh: ["예: 13-4-1 · 없으면 비움", "e.g. 13-4-1 · may be empty"],
|
||||
Unit: ["결과 단위", "Result unit"],
|
||||
Source: ["출처", "Source"],
|
||||
Ok: ["만들기", "Create"],
|
||||
Cancel: ["취소", "Cancel"],
|
||||
Close: ["닫기", "Close"],
|
||||
NeedName: ["이름을 넣어 주세요", "Enter a name"],
|
||||
} as const;
|
||||
|
||||
const tb = (key: keyof typeof TEXT): string => TEXT[key][currentLanguageIndex === 1 ? 1 : 0];
|
||||
|
||||
export interface BasicInfo {
|
||||
이름: string;
|
||||
원문번호: string;
|
||||
결과단위: string;
|
||||
출처: string;
|
||||
}
|
||||
|
||||
/** 기본정보 묻기 — units = 결과 단위 후보(마스터 로직의 결과단위) */
|
||||
export function askBasicInfo(units: string[]): Promise<BasicInfo | null> {
|
||||
return new Promise((resolve) => {
|
||||
let done = false;
|
||||
const finish = (v: BasicInfo | null): void => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
resolve(v);
|
||||
};
|
||||
const name = createInputField({ label: tb("Name"), placeholder: tb("NamePh"), compact: true });
|
||||
const number = createInputField({
|
||||
label: tb("Number"),
|
||||
placeholder: tb("NumberPh"),
|
||||
compact: true,
|
||||
});
|
||||
const unit = createSelectField({
|
||||
label: tb("Unit"),
|
||||
options: units.map((u) => ({ value: u, text: u })),
|
||||
value: units[0] ?? "원",
|
||||
compact: true,
|
||||
});
|
||||
const source = createInputField({ label: tb("Source"), value: "자체", compact: true });
|
||||
const why = el("p", { className: "m01-logic__muted", attrs: { role: "alert" } });
|
||||
const modal = openModal({
|
||||
title: tb("Title"),
|
||||
closeLabel: tb("Close"),
|
||||
beforeClose: () => {
|
||||
finish(null);
|
||||
return true;
|
||||
},
|
||||
mount: (body, close) => {
|
||||
const ok = (): void => {
|
||||
const 이름 = name.input.value.trim();
|
||||
if (!이름) {
|
||||
why.textContent = tb("NeedName");
|
||||
return void name.input.focus();
|
||||
}
|
||||
finish({
|
||||
이름,
|
||||
원문번호: number.input.value.trim(),
|
||||
결과단위: unit.select.value,
|
||||
출처: source.input.value.trim() || "자체",
|
||||
});
|
||||
close();
|
||||
};
|
||||
name.input.addEventListener("keydown", (ev) => ev.key === "Enter" && ok());
|
||||
body.append(
|
||||
name.root,
|
||||
number.root,
|
||||
unit.root,
|
||||
source.root,
|
||||
why,
|
||||
el("div", {
|
||||
className: "m01-logic__bar",
|
||||
children: [
|
||||
createButton({ label: tb("Ok"), size: "sm", onClick: ok }),
|
||||
createButton({
|
||||
label: tb("Cancel"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
finish(null);
|
||||
modal.close();
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -38,3 +38,17 @@
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.m01lab-flow__bar[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.m01lab-flow__note {
|
||||
margin: 0 0 var(--spacing-8);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.m01lab-flow__note[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_LogicLab_Flow.ts
|
||||
* 로직 상세 두 칸 — 왼쪽 지금 내용(보기) · 가운데 끌기 막대 · 오른쪽 흐름도 자리(패널은 LogicLab 이 붙임)
|
||||
* 너비 비율은 브라우저에 기억(localStorage · 막히면 기본 반반). 정함 `docs/raw/M01_쉬운단가/흐름도_적용_정함.md` 5 · 10
|
||||
* 로직 상세 두 칸 — 왼쪽 지금 내용(보기) · 가운데 끌기 막대 · 오른쪽 흐름도 패널
|
||||
* 너비 — 패널이 없으면 두 칸 비율(끌기 막대 · localStorage) · 패널이 있으면 패널이 알린 너비(px · 패널 가장자리 끌기)
|
||||
* 잇기(mountLogicFlow) — 패널(M02_DF_Panel) · 왼쪽 보기(LogicLab_FlowView) · 입력 값 · 다른 로직 흐름 받기
|
||||
* DataFlow 파일은 import() 로 늦게(엔진이 M01 첫 묶음에 안 들어가게). 정함 `docs/raw/M01_쉬운단가/흐름도_적용_정함.md` 2 · 5 · 6 · 7
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import type { CfLogic } from "../M02_MasterTemplete/DataFlow/M02_DF_Model";
|
||||
import type { CfValues } from "../M02_MasterTemplete/DataFlow/M02_DF_Eval";
|
||||
import "./M01_MasterData_UI_LogicLab_Flow.css";
|
||||
|
||||
const TEXT = {
|
||||
Resize: ["두 칸 너비 — 끌어서 조절", "Drag to resize the two columns"],
|
||||
Waiting: ["흐름도 자리", "Flow diagram"],
|
||||
New: ["신규 로직", "New logic"],
|
||||
Waiting: ["흐름도 불러오는 중", "Loading the flow diagram"],
|
||||
Converted: [
|
||||
"옛 식을 흐름도로 바꿔 보임 — 저장하면 흐름도가 정본",
|
||||
"Converted from the old formula — saving makes the flow the master",
|
||||
],
|
||||
Failed: ["흐름도를 못 띄움 — {v}", "Couldn't open the flow diagram — {v}"],
|
||||
} as const;
|
||||
|
||||
export const tf = (key: keyof typeof TEXT): string => TEXT[key][currentLanguageIndex === 1 ? 1 : 0];
|
||||
export const tf = (key: keyof typeof TEXT, v = ""): string =>
|
||||
TEXT[key][currentLanguageIndex === 1 ? 1 : 0].replace("{v}", v);
|
||||
|
||||
const STORE = "m01.logic.flowRatio";
|
||||
const MIN = 0.25;
|
||||
@@ -34,14 +46,13 @@ export interface SplitHandle {
|
||||
root: HTMLElement;
|
||||
/** 오른쪽 칸 — 흐름도 패널을 붙이는 곳 */
|
||||
right: HTMLElement;
|
||||
/** 패널이 정한 너비(px) — 끌기 막대는 숨김(패널 가장자리로 조절) · null = 비율로 돌아감 */
|
||||
setRight(px: number | null): void;
|
||||
}
|
||||
|
||||
/** 두 칸 만들기 — left 는 지금 화면 그대로 왼쪽에 둠 */
|
||||
export function buildSplit(left: HTMLElement): SplitHandle {
|
||||
const right = el("div", {
|
||||
className: "m01lab-flow__right",
|
||||
children: [el("p", { className: "m01lab-flow__wait", text: tf("Waiting") })],
|
||||
});
|
||||
const right = el("div", { className: "m01lab-flow__right" });
|
||||
const bar = el("div", {
|
||||
className: "m01lab-flow__bar",
|
||||
attrs: {
|
||||
@@ -55,8 +66,12 @@ export function buildSplit(left: HTMLElement): SplitHandle {
|
||||
const leftCol = el("div", { className: "m01lab-flow__left", children: [left] });
|
||||
const root = el("div", { className: "m01lab-flow", children: [leftCol, bar, right] });
|
||||
let ratio = readRatio();
|
||||
let panelPx: number | null = null;
|
||||
const apply = (): void => {
|
||||
root.style.gridTemplateColumns = `minmax(0, ${ratio}fr) auto minmax(0, ${1 - ratio}fr)`;
|
||||
root.style.gridTemplateColumns =
|
||||
panelPx === null
|
||||
? `minmax(0, ${ratio}fr) auto minmax(0, ${1 - ratio}fr)`
|
||||
: `minmax(0, 1fr) ${panelPx}px`;
|
||||
};
|
||||
const keep = (): void => {
|
||||
try {
|
||||
@@ -90,6 +105,159 @@ export function buildSplit(left: HTMLElement): SplitHandle {
|
||||
apply();
|
||||
keep();
|
||||
});
|
||||
const setRight = (px: number | null): void => {
|
||||
panelPx = px;
|
||||
bar.hidden = px !== null;
|
||||
apply();
|
||||
};
|
||||
apply();
|
||||
return { root, right };
|
||||
return { root, right, setRight };
|
||||
}
|
||||
|
||||
/* ── 흐름 받기 · 잇기 ─────────────────────────────────────────────────── */
|
||||
|
||||
export interface FlowAnswer {
|
||||
flow: CfLogic;
|
||||
converted: boolean;
|
||||
notes: string[];
|
||||
}
|
||||
|
||||
/** 마스터 로직의 흐름 — 줄에 흐름이 있으면 그대로 · 없으면 서버가 옛 식을 바꿔 줌 · 길이 없거나 못 받으면 null */
|
||||
export async function fetchFlow(key: string, file = ""): Promise<FlowAnswer | null> {
|
||||
try {
|
||||
const q = new URLSearchParams({ key, ...(file ? { file } : {}) });
|
||||
const r = await fetch(`${API_BASE_URL}/m01/logic/flow?${q}`, { credentials: "include" });
|
||||
if (!r.ok) return null;
|
||||
const got = (await r.json()) as Partial<FlowAnswer>;
|
||||
return got.flow ? { flow: got.flow, converted: !!got.converted, notes: got.notes ?? [] } : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const loadFlowMods = () =>
|
||||
Promise.all([
|
||||
import("../M02_MasterTemplete/DataFlow/M02_DF_Panel"),
|
||||
import("./M01_MasterData_UI_LogicLab_FlowView"),
|
||||
import("../M02_MasterTemplete/DataFlow/M02_DF_Model"),
|
||||
import("../M02_MasterTemplete/DataFlow/M02_DF_Eval"),
|
||||
import("../M02_MasterTemplete/DataFlow/M02_DF_Api"),
|
||||
import("../M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail"),
|
||||
]).then(([panel, view, model, ev, api, detail]) => ({ panel, view, model, ev, api, detail }));
|
||||
|
||||
/** 빈 흐름 — 신규 로직 · 흐름을 못 받은 로직 */
|
||||
export async function emptyFlow(name: string, unit: string): Promise<CfLogic> {
|
||||
return (await import("../M02_MasterTemplete/DataFlow/M02_DF_Model")).newLogic(name, unit);
|
||||
}
|
||||
|
||||
export interface LogicFlowOpts {
|
||||
split: SplitHandle;
|
||||
flow: CfLogic;
|
||||
title: string;
|
||||
readOnly: boolean;
|
||||
/** 변환 알림 — 패널 위 한 줄 */
|
||||
notes: string[];
|
||||
converted: boolean;
|
||||
/** 흐름 로직이면 왼쪽 「지금 내용」 자리(LogicLab_Detail 의 flowHost) */
|
||||
leftHost(): HTMLElement | null;
|
||||
onChange(lg: CfLogic): void;
|
||||
}
|
||||
|
||||
export interface LogicFlow {
|
||||
/** 밖에서 바꿈(고친 것 버리기) — 패널 · 왼쪽 함께 */
|
||||
set(lg: CfLogic): void;
|
||||
/** 왼쪽만 다시(왼쪽 화면을 새로 그린 뒤) */
|
||||
redrawLeft(): void;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
/** 오른쪽 패널을 띄우고 왼쪽 보기 · 입력 값 · 다른 로직을 이음 */
|
||||
export async function mountLogicFlow(opts: LogicFlowOpts): Promise<LogicFlow> {
|
||||
const M = await loadFlowMods();
|
||||
const host = el("div", { className: "m01lab-flow__panel" });
|
||||
const note = el("p", { className: "m01lab-flow__note" });
|
||||
const notes = [...(opts.converted ? [tf("Converted")] : []), ...opts.notes];
|
||||
note.textContent = notes.join(" · ");
|
||||
note.hidden = !notes.length;
|
||||
opts.split.right.replaceChildren(note, host);
|
||||
let lg = opts.flow;
|
||||
let values: CfValues = {};
|
||||
let valueSig = "";
|
||||
let logics: Record<string, CfLogic> = {};
|
||||
let logicSig = "";
|
||||
let gone = false;
|
||||
|
||||
/** 다른 로직 — 흐름도 저장본(DF-) · 마스터 로직(흐름 받기) */
|
||||
const getLogic = async (key: string): Promise<CfLogic | null> =>
|
||||
key.startsWith("DF-") ? M.api.cfGet(key) : ((await fetchFlow(key))?.flow ?? null);
|
||||
|
||||
const left = (): void => {
|
||||
const at = opts.leftHost();
|
||||
if (!at || gone) return;
|
||||
M.view.buildFlowView(at, lg, values, { logics });
|
||||
};
|
||||
/** 입력 값 · 다른 로직 — 물음이 바뀐 때만 다시 받음 · 받으면 왼쪽 다시 */
|
||||
const refresh = (): void => {
|
||||
let ports: Record<string, Record<string, unknown>> = {};
|
||||
try {
|
||||
ports = M.ev.evalLogic(lg, values, { logics }).ports;
|
||||
} catch {
|
||||
/* 셈이 막혀도 물음은 보냄 */
|
||||
}
|
||||
const pill = (p: { box: string; name: string }): unknown => ports[p.box]?.[p.name] ?? null;
|
||||
const asks = M.detail.queriesOf(lg, pill, M.model.elementPort);
|
||||
const sig = JSON.stringify(asks.map((a) => a.q));
|
||||
if (sig !== valueSig) {
|
||||
valueSig = sig;
|
||||
void M.detail.fetchValues(M.api.cfValues, lg, pill, M.model.elementPort).then(
|
||||
(got) => {
|
||||
if (sig !== valueSig || gone) return;
|
||||
values = got.values;
|
||||
left();
|
||||
refresh(); // 받은 값으로 조건 찾기 값이 바뀌면 한 번 더
|
||||
},
|
||||
() => undefined,
|
||||
);
|
||||
}
|
||||
const keys = JSON.stringify(lg.boxes.map((b) => b.input?.logic?.key ?? ""));
|
||||
if (keys !== logicSig) {
|
||||
logicSig = keys;
|
||||
void M.detail.loadLogics(lg, getLogic).then((got) => {
|
||||
if (keys !== logicSig || gone) return;
|
||||
logics = got.logics;
|
||||
left();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const panel = M.panel.mountFlowPanel(host, {
|
||||
logic: lg,
|
||||
title: opts.title,
|
||||
readOnly: opts.readOnly,
|
||||
logics: getLogic,
|
||||
onResize: (px) => opts.split.setRight(px),
|
||||
onChange: (next) => {
|
||||
lg = next;
|
||||
opts.onChange(next);
|
||||
left();
|
||||
refresh();
|
||||
},
|
||||
});
|
||||
left();
|
||||
refresh();
|
||||
return {
|
||||
set: (next) => {
|
||||
lg = next;
|
||||
panel.set(next);
|
||||
left();
|
||||
refresh();
|
||||
},
|
||||
redrawLeft: left,
|
||||
destroy: () => {
|
||||
gone = true;
|
||||
panel.destroy();
|
||||
opts.split.setRight(null);
|
||||
opts.split.right.replaceChildren();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ import { cfPlain, cfUserElementAdd, cfUserElements, cfValues } from "./M02_DF_Ap
|
||||
import type { CfBox, CfGroup, CfLogic } from "./M02_DF_Model";
|
||||
import {
|
||||
GROUPS,
|
||||
RULE_GROUPS,
|
||||
matchesRow,
|
||||
removeElementNamed,
|
||||
M01_OF,
|
||||
addElement,
|
||||
elementOf,
|
||||
@@ -36,6 +39,7 @@ import {
|
||||
type M01Row,
|
||||
} from "./M02_DF_InputPanel_Build";
|
||||
import { renderLogicInput } from "./M02_DF_InputPanel_Logic";
|
||||
import { renderGroupRule } from "./M02_DF_InputPanel_Rule";
|
||||
import { NO_UNIT, createUnitPick } from "./M02_DF_UnitPick";
|
||||
import { renderMaterial } from "./M02_DF_InputPanel_Material";
|
||||
import { renderTablePick } from "./M02_DF_InputPanel_Table";
|
||||
@@ -63,6 +67,10 @@ const apiAdd = cfUserElementAdd as unknown as (e: Omit<UserEl, "id">) => Promise
|
||||
const TEXT = {
|
||||
Kind: ["무엇을 넣을까요?", "What goes in?"],
|
||||
Variable: ["변수", "Variable"],
|
||||
ModeFixed: ["고정", "Fixed"],
|
||||
ModeRule: ["조건 찾기", "Find by rule"],
|
||||
RuleTag: ["조건 찾기", "Rule"],
|
||||
RuleLine: ["{f} 이(가) 알약 「{n}」 값과 같음", "{f} equals pill “{n}”"],
|
||||
Logic: ["다른 로직", "Another logic"],
|
||||
인력: ["인력", "Labor"],
|
||||
재료: ["재료", "Material"],
|
||||
@@ -193,10 +201,52 @@ function renderGroup(
|
||||
on: { change(lg: CfLogic): void },
|
||||
): void {
|
||||
const chosen = el("div", { className: "m02-dfip__chosen" });
|
||||
const find = el("div", { className: "m02-dfip__find" });
|
||||
host.replaceChildren(chosen, find, newForm(lg, box, group, on));
|
||||
const body = el("div", { className: "m02-dfip__stack" });
|
||||
let mode: "fixed" | "rule" = "fixed";
|
||||
const draw = (): void => {
|
||||
if (mode === "rule")
|
||||
renderGroupRule(body, lg, box.id, group, (l, e) => addElement(l, box.id, e), rulePreview, on);
|
||||
else {
|
||||
const find = el("div", { className: "m02-dfip__find" });
|
||||
body.replaceChildren(find, newForm(lg, box, group, on));
|
||||
void drawFind(find, lg, box, group, on);
|
||||
}
|
||||
};
|
||||
// 인력 · 기계 · 유가전력은 방식(고정 / 조건 찾기)을 고름 — 재료는 재료 판 · 그 밖은 고정만
|
||||
const chips = el("div", { className: "m02-dfip__chips" });
|
||||
const drawChips = (): void =>
|
||||
chips.replaceChildren(
|
||||
...(["fixed", "rule"] as const).map((m) =>
|
||||
createButton({
|
||||
label: m === "fixed" ? t("ModeFixed") : t("ModeRule"),
|
||||
variant: mode === m ? "filled" : "pill",
|
||||
size: "sm",
|
||||
onClick: () => ((mode = m), drawChips(), draw()),
|
||||
}),
|
||||
),
|
||||
);
|
||||
host.replaceChildren(chosen, ...(RULE_GROUPS.includes(group) ? [chips] : []), body);
|
||||
drawChosen(chosen, lg, box, group, on);
|
||||
void drawFind(find, lg, box, group, on);
|
||||
if (RULE_GROUPS.includes(group)) drawChips();
|
||||
draw();
|
||||
}
|
||||
|
||||
/** 조건 찾기 미리 보기 — 알약 값과 이름(키)이 같은 마스터 줄 */
|
||||
async function rulePreview(
|
||||
group: CfGroup,
|
||||
field: "이름" | "키",
|
||||
value: unknown,
|
||||
): Promise<{ found: number; names: string[]; unit: string }> {
|
||||
if (value === null || value === undefined || value === "")
|
||||
return { found: 0, names: [], unit: "" };
|
||||
const page = await fetchRows(await fileOf(group), 1, 50, String(value), false, "", "");
|
||||
const hit = (page.rows as M01Row[]).filter((r) => matchesRow(r, field, value));
|
||||
const units = new Set(hit.map((r) => elementOf(group, r).unit));
|
||||
return {
|
||||
found: hit.length,
|
||||
names: hit.slice(0, 3).map((r) => String(r["이름"] ?? "")),
|
||||
unit: units.size === 1 ? [...units][0] : "",
|
||||
};
|
||||
}
|
||||
|
||||
/** 고른 요소마다 알약 한 줄 — 이름 · 값 · 단위 · 값은 Api 가 마스터에서 */
|
||||
@@ -214,26 +264,48 @@ function drawChosen(
|
||||
...(els.length
|
||||
? els.map((e) => {
|
||||
const val = el("strong", { className: "m02-dfip__value", text: t("NoValue") });
|
||||
values.set(e.ref, val);
|
||||
const rule = e.mode === "rule";
|
||||
if (!rule) values.set(e.ref, val);
|
||||
return el("div", {
|
||||
className: "m02-dfip__item",
|
||||
children: [
|
||||
el("span", { className: "m02-dfip__name", text: e.name }),
|
||||
el("span", {
|
||||
className: "m02-dfip__name",
|
||||
children: [
|
||||
...(rule ? [el("span", { className: "m02-dfip__tag", text: t("RuleTag") })] : []),
|
||||
el("strong", { text: ` ${e.name}` }),
|
||||
...(rule && e.rule
|
||||
? [
|
||||
el("div", {
|
||||
className: "m02-dfip__muted",
|
||||
text: t("RuleLine", { f: e.rule.field, n: e.rule.from.name }),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
val,
|
||||
el("span", { className: "m02-dfip__muted", text: e.unit || "-" }),
|
||||
createButton({
|
||||
label: t("Remove"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => on.change(removeElement(lg, box.id, e.ref)),
|
||||
onClick: () =>
|
||||
on.change(
|
||||
rule
|
||||
? removeElementNamed(lg, box.id, e.name)
|
||||
: removeElement(lg, box.id, e.ref),
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
})
|
||||
: [el("p", { className: "m02-dfip__muted", text: t("NoneChosen") })]),
|
||||
);
|
||||
if (!els.length) return;
|
||||
const items = els.map((e) => ({ id: e.ref, ...queryOf(group, e.ref) }));
|
||||
const items = els
|
||||
.filter((e) => e.mode !== "rule")
|
||||
.map((e) => ({ id: e.ref, ...queryOf(group, e.ref) }));
|
||||
if (!items.length) return;
|
||||
void apiValues(items, lg.userElements).then(
|
||||
(answers) =>
|
||||
answers.forEach((a) => {
|
||||
|
||||
@@ -162,16 +162,21 @@ export function matches(
|
||||
return rule.op === "같음" ? target === v : target.includes(v);
|
||||
}
|
||||
|
||||
/** 지금 잡히는 품목 미리 보기 글 — 0 이면 까닭 · 여럿이면 알림 */
|
||||
/** 지금 잡히는 것 미리 보기 글 — 0 이면 까닭 · 여럿이면 알림(noun = 재료 · 줄) */
|
||||
export function catchNote(
|
||||
found: number,
|
||||
firstName = "",
|
||||
noun = "재료",
|
||||
): { kind: "none" | "one" | "many"; text: string } {
|
||||
if (found === 0) return { kind: "none", text: "맞는 재료가 없음 — 범위나 규칙을 넓혀 봄" };
|
||||
if (found === 1) return { kind: "one", text: `재료 하나가 잡힘 — ${firstName}` };
|
||||
if (found === 0)
|
||||
return {
|
||||
kind: "none",
|
||||
text: `맞는 ${noun}${noun === "줄" ? "이" : "가"} 없음 — 범위나 규칙을 넓혀 봄`,
|
||||
};
|
||||
if (found === 1) return { kind: "one", text: `${noun} 하나가 잡힘 — ${firstName}` };
|
||||
return {
|
||||
kind: "many",
|
||||
text: `재료 ${found} 개가 잡힘 — 둘 이상이면 가장 싼 값을 씀 · 규칙을 더 좁히면 하나로 잡힘`,
|
||||
text: `${noun} ${found} 개가 잡힘 — 둘 이상이면 ${noun === "줄" ? "첫 줄" : "가장 싼 값"}을 씀 · 규칙을 더 좁히면 하나로 잡힘`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -329,6 +334,28 @@ export function masterRef(
|
||||
: ref;
|
||||
}
|
||||
|
||||
/** 옛 로직 줄(입력 · 자동값)에서 바로 읽은 다른 로직 입력 값 — 흐름 변환이 아직 비어 있을 때 · 받는 알약 = 입력마다 · 예시값 = 자동값 */
|
||||
export function rowRef(
|
||||
key: string,
|
||||
name: string,
|
||||
resultUnit: string,
|
||||
inputs: { 이름: string; 단위?: string }[],
|
||||
auto: Record<string, unknown>,
|
||||
): CfLogicRef {
|
||||
const examples: Record<string, number | string> = {};
|
||||
for (const i of inputs) {
|
||||
const v = auto[i.이름];
|
||||
if (typeof v === "number" || (typeof v === "string" && v !== "")) examples[i.이름] = v;
|
||||
}
|
||||
return {
|
||||
key,
|
||||
name,
|
||||
outputs: COST_OUTPUTS.map((n) => ({ name: n, unit: resultUnit || "-" })),
|
||||
designs: inputs.map((i) => ({ name: i.이름, unit: i.단위 || "-" })),
|
||||
...(Object.keys(examples).length ? { examples } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** 이 로직이 자기 자신을 부르거나 돌고 도나 — `load` 가 로직을 주면 입력 상자의 「다른 로직」 을 따라가 봄(깊이 8) */
|
||||
export async function wouldCycle(
|
||||
selfKey: string | null,
|
||||
@@ -566,3 +593,36 @@ export function reconcileUnit(lg: CfLogic, id: string): CfLogic {
|
||||
? lg
|
||||
: { ...lg, boxes: lg.boxes.map((b) => (b.id === id ? { ...b, unit: want } : b)) };
|
||||
}
|
||||
|
||||
/* ── 조건 찾기(rule) — 인력 · 기계 · 유가전력도 씀: 이름 또는 키가 변수 알약 값과 같은 줄(옛 로직 LB:{직종} · EQ:{기계} 꼴) ── */
|
||||
|
||||
/** 조건 찾기를 쓰는 그룹 — 재료는 재료 판(규격 · 이름 포함) · 선택 자리(choice)는 재료만 */
|
||||
export const RULE_GROUPS: CfGroup[] = ["인력", "기계", "유가전력"];
|
||||
|
||||
export type GroupRuleField = "이름" | "키";
|
||||
|
||||
/** 이 줄이 규칙에 맞나 — 이름 또는 키(원문번호도)가 알약 값과 같음(띄어쓰기 · 대소문자는 가리지 않음) */
|
||||
export function matchesRow(row: M01Row, field: GroupRuleField, value: unknown): boolean {
|
||||
const v = squash(value);
|
||||
if (!v) return false;
|
||||
const targets = field === "키" ? [row["키"], row["원문번호"]] : [row["이름"]];
|
||||
return targets.some((x) => squash(x) === v);
|
||||
}
|
||||
|
||||
/** 조건 찾기 요소 — 품목 키는 비우고 알약 이름 = 자리 이름 · 규칙 = 같음(Model 규칙 꼴에 「키」 칸을 더해 씀) */
|
||||
export function groupRuleElement(
|
||||
name: string,
|
||||
field: GroupRuleField,
|
||||
from: CfMaterialRule["from"],
|
||||
): CfElementRef {
|
||||
const rule = { field, op: "같음", from } as unknown as CfMaterialRule;
|
||||
return { ref: "", name: name.trim(), slot: name.trim(), spec: "", unit: "-", mode: "rule", rule };
|
||||
}
|
||||
|
||||
/** 넣기 전 검사 — 쉬운 말 한 줄 · 괜찮으면 null */
|
||||
export function checkGroupRule(el: CfElementRef, others: CfElementRef[]): string | null {
|
||||
if (!el.name) return "자리 이름을 적어 주세요";
|
||||
if (others.some((o) => o.name === el.name)) return "같은 이름의 알약이 이미 있음";
|
||||
if (!el.rule?.from.name) return "규칙에 쓸 변수 알약을 골라 주세요";
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,11 @@ import {
|
||||
el,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { fetchLogics } from "../../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import {
|
||||
fetchAuto,
|
||||
fetchLogic,
|
||||
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 {
|
||||
@@ -23,6 +27,7 @@ import {
|
||||
logicChapters,
|
||||
logicRefOf,
|
||||
masterRef,
|
||||
rowRef,
|
||||
pageRange,
|
||||
setKind,
|
||||
setLogicInput,
|
||||
@@ -235,9 +240,14 @@ export function renderLogicInput(
|
||||
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 empty = got.converted && got.flow.boxes.length === 0;
|
||||
const row = empty ? await fetchLogic(i.키).catch(() => null) : null;
|
||||
const picked = row
|
||||
? rowRef(i.키, i.이름, i.결과단위, row.logic.입력 ?? [], await fetchAuto(i.키))
|
||||
: got.converted
|
||||
? masterRef(i.키, got.flow, true, i.결과단위)
|
||||
: { ...logicRefOf(i.키, got.flow), name: i.이름 };
|
||||
const done = setLogicInput(lg, boxId, picked);
|
||||
// 기본 이름이면 고른 로직 이름으로
|
||||
on.change({
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/* =============================================================================
|
||||
* M02_DF_InputPanel_Rule.ts
|
||||
* 입력 판 — 인력 · 기계 · 유가전력의 「조건 찾기」: 이름 또는 키가 이 로직 변수 알약 값과 같은 줄을 잡음
|
||||
* (옛 로직의 LB:{직종} · EQ:{기계} 꼴) · 지금 잡히는 줄 미리 보기(0 · 여럿이면 까닭) · 글 [한, 영] 짝은 아래
|
||||
* 선택 자리(choice)는 재료만 — 이 판에 없음
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
createSelectField,
|
||||
el,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import type { CfElementRef, CfGroup, CfLogic } from "./M02_DF_Model";
|
||||
import {
|
||||
catchNote,
|
||||
checkGroupRule,
|
||||
groupRuleElement,
|
||||
variablePills,
|
||||
type GroupRuleField,
|
||||
} from "./M02_DF_InputPanel_Build";
|
||||
|
||||
const TEXT = {
|
||||
Help: [
|
||||
"변수 알약 값과 이름(또는 키)이 같은 줄을 잡음 — 예: 직종 알약 값이 「석공」이면 석공 줄",
|
||||
"Catches the row whose name (or key) equals a variable pill's value",
|
||||
],
|
||||
SlotName: ["자리 이름(알약 이름)", "Slot name (pill name)"],
|
||||
Field: ["같아야 하는 칸", "Column"],
|
||||
FieldName: ["이름", "Name"],
|
||||
FieldKey: ["키(원문번호)", "Key (source no.)"],
|
||||
Pill: ["변수 알약", "Variable pill"],
|
||||
NoPill: ["변수 알약이 없음 — 변수 입력 상자를 먼저 만듦", "No variable pill yet"],
|
||||
Preview: ["지금 잡히는 줄", "Rows caught now"],
|
||||
Put: ["넣기", "Add"],
|
||||
Failed: ["불러오지 못함", "Could not load"],
|
||||
} as const;
|
||||
|
||||
type TKey = keyof typeof TEXT;
|
||||
const t = (key: TKey): string => TEXT[key][currentLanguageIndex] as string;
|
||||
|
||||
/** 미리 보기 길 — 그룹 · 칸 · 알약 값으로 마스터 줄을 읽어 잡힌 수 · 이름 · 단위(부르는 쪽이 줌) */
|
||||
export type RulePreview = (
|
||||
group: CfGroup,
|
||||
field: GroupRuleField,
|
||||
value: unknown,
|
||||
) => Promise<{ found: number; names: string[]; unit: string }>;
|
||||
|
||||
/** 조건 찾기 요소 추가 판 */
|
||||
export function renderGroupRule(
|
||||
host: HTMLElement,
|
||||
lg: CfLogic,
|
||||
boxId: string,
|
||||
group: CfGroup,
|
||||
addEl: (lg: CfLogic, el: CfElementRef) => CfLogic,
|
||||
preview: RulePreview,
|
||||
on: { change(lg: CfLogic): void },
|
||||
): void {
|
||||
const els = lg.boxes.find((b) => b.id === boxId)?.input?.elements ?? [];
|
||||
const pills = variablePills(lg);
|
||||
const name = createInputField({
|
||||
type: "text",
|
||||
label: t("SlotName"),
|
||||
compact: true,
|
||||
placeholder: group === "인력" ? "예: 직종" : "예: 기종",
|
||||
});
|
||||
const field = createSelectField({
|
||||
label: t("Field"),
|
||||
options: [
|
||||
{ value: "이름", text: t("FieldName") },
|
||||
{ value: "키", text: t("FieldKey") },
|
||||
],
|
||||
value: "이름",
|
||||
compact: true,
|
||||
onChange: () => void refresh(),
|
||||
});
|
||||
const pill = createSelectField({
|
||||
label: t("Pill"),
|
||||
options: pills.map((p, i) => ({
|
||||
value: String(i),
|
||||
text: `${p.port.name}${p.example !== null ? ` = ${p.example}` : ""}`,
|
||||
})),
|
||||
value: "0",
|
||||
compact: true,
|
||||
onChange: () => void refresh(),
|
||||
});
|
||||
const note = el("p", { className: "m02-dfip__note" });
|
||||
const build = (): CfElementRef =>
|
||||
groupRuleElement(
|
||||
name.input.value,
|
||||
field.select.value as GroupRuleField,
|
||||
pills[Number(pill.select.value)]?.port ?? { box: "", name: "" },
|
||||
);
|
||||
let seq = 0;
|
||||
const refresh = async (): Promise<void> => {
|
||||
const mine = ++seq;
|
||||
const value = pills[Number(pill.select.value)]?.example;
|
||||
note.textContent = `${t("Preview")}: …`;
|
||||
try {
|
||||
const p = await preview(group, field.select.value as GroupRuleField, value);
|
||||
if (mine !== seq) return;
|
||||
const n = catchNote(p.found, p.names[0], "줄");
|
||||
note.textContent = `${t("Preview")}: ${n.text}`;
|
||||
note.className = `m02-dfip__note${n.kind === "one" ? "" : " is-bad"}`;
|
||||
} catch {
|
||||
if (mine === seq) note.textContent = t("Failed");
|
||||
}
|
||||
};
|
||||
const add = createButton({
|
||||
label: t("Put"),
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
const cand = build();
|
||||
const why = checkGroupRule(cand, els);
|
||||
name.setError(why ?? undefined);
|
||||
if (why) return void showToast(why, "error");
|
||||
// 잡히는 줄이 하나면 그 단위를 알약 단위로
|
||||
void preview(
|
||||
group,
|
||||
field.select.value as GroupRuleField,
|
||||
pills[Number(pill.select.value)]?.example,
|
||||
).then(
|
||||
(p) => on.change(addEl(lg, p.unit ? { ...cand, unit: p.unit } : cand)),
|
||||
() => on.change(addEl(lg, cand)),
|
||||
);
|
||||
},
|
||||
});
|
||||
host.replaceChildren(
|
||||
el("p", { className: "m02-dfip__muted", text: t("Help") }),
|
||||
name.root,
|
||||
...(pills.length
|
||||
? [field.root, pill.root, note, add]
|
||||
: [el("p", { className: "m02-dfip__muted is-bad", text: t("NoPill") })]),
|
||||
);
|
||||
if (pills.length) void refresh();
|
||||
}
|
||||
@@ -228,3 +228,41 @@ def test_자기_자신과_돌고_도는_로직은_막음():
|
||||
assert call("wouldCycle", "SELF", "C", graph) is False
|
||||
assert call("wouldCycle", None, "A", graph) is False # 새 로직은 아직 키가 없어 돌 수 없음
|
||||
assert call("wouldCycle", "SELF", "없음", graph) is False # 못 읽으면 막지 않음
|
||||
|
||||
|
||||
def test_조건_찾기는_인력_기계_유가전력에_쓰고_이름_또는_키가_같은_줄():
|
||||
labor = {"키": "LB000033", "원문번호": "1033", "이름": "석공"}
|
||||
assert call("matchesRow", labor, "이름", "석공") is True
|
||||
assert call("matchesRow", labor, "이름", "석 공") is True # 띄어쓰기는 가리지 않음
|
||||
assert call("matchesRow", labor, "이름", "할석공") is False # 같음이라 일부만 맞아선 안 잡음
|
||||
assert call("matchesRow", labor, "키", "lb000033") is True
|
||||
assert call("matchesRow", labor, "키", "1033") is True # 원문번호도 키로 침
|
||||
assert call("matchesRow", labor, "이름", "") is False # 알약 값이 비면 안 잡음
|
||||
|
||||
|
||||
def test_조건_찾기_요소와_검사():
|
||||
port = {"box": "b2", "name": "직종"}
|
||||
el = call("groupRuleElement", "노임", "이름", port)
|
||||
assert (
|
||||
el["mode"] == "rule" and el["ref"] == "" and el["name"] == "노임" and el["slot"] == "노임"
|
||||
)
|
||||
assert el["rule"] == {"field": "이름", "op": "같음", "from": port}
|
||||
assert call("checkGroupRule", el, []) is None
|
||||
assert call("checkGroupRule", {**el, "name": ""}, []) == "자리 이름을 적어 주세요"
|
||||
assert call("checkGroupRule", el, [el]) == "같은 이름의 알약이 이미 있음"
|
||||
no_pill = call("groupRuleElement", "노임", "이름", {"box": "", "name": ""})
|
||||
assert "변수 알약" in call("checkGroupRule", no_pill, [])
|
||||
|
||||
|
||||
def test_옛_줄_입력과_자동값으로_다른_로직_입력을_읽음():
|
||||
inputs = [{"이름": "뒷길이", "단위": "㎝"}, {"이름": "돌"}, {"이름": "쌓기"}]
|
||||
auto = {"뒷길이": 25, "돌": "깬잡석", "쌓기": "", "남는": 1}
|
||||
ref = call("rowRef", "GF000219", "돌쌓기 메쌓기(인력)", "원/㎡", inputs, auto)
|
||||
assert ref["key"] == "GF000219"
|
||||
assert [o["name"] for o in ref["outputs"]] == ["노무비", "재료비", "경비", "계"]
|
||||
assert ref["designs"] == [
|
||||
{"name": "뒷길이", "unit": "㎝"},
|
||||
{"name": "돌", "unit": "-"},
|
||||
{"name": "쌓기", "unit": "-"},
|
||||
]
|
||||
assert ref["examples"] == {"뒷길이": 25, "돌": "깬잡석"} # 빈 글 · 입력에 없는 이름은 뺌
|
||||
|
||||
Reference in New Issue
Block a user