feat(m02-cf): InputPanel 완성 — 서버 Api 최종 꼴(ref · Model 사용자 요소)에 맞춤 · 환율 유가전력 알약 이름

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ACSGaHdLgnkEkoA4LDtMU
This commit is contained in:
2026-10-01 16:13:08 +09:00
co-authored by Claude Sonnet 5.5
parent 2cecd38f35
commit e2dfbe687b
3 changed files with 99 additions and 46 deletions
@@ -20,11 +20,12 @@ import {
fetchSubs,
type SubInfo,
} from "../../M01_MasterData/M01_MasterData_Api_Fetch";
import { cfPlain, cfUserElementAdd, cfUserElements, cfValues, type CfQuery } from "./M02_CF_Api";
import type { CfBox, CfGroup, CfLogic } from "./M02_CF_Model";
import { cfPlain, cfUserElementAdd, cfUserElements, cfValues } from "./M02_CF_Api";
import type { CfBox, CfElementRef, CfGroup, CfLogic } from "./M02_CF_Model";
import {
GROUPS,
M01_OF,
MACHINE_SEP,
USER_PREFIX,
addElement,
elementOf,
@@ -39,6 +40,24 @@ import { renderTablePick } from "./M02_CF_InputPanel_Table";
import { renderVariables } from "./M02_CF_InputPanel_Vars";
import "./M02_CF_InputPanel.css";
/** Api 모양 — 서버 Api 가 채워지면(사용자 요소 = Model 모양 · 값 물음 = ref) 그 모양을 그대로 씀 */
interface Answer {
id: string;
ok: boolean;
value?: number | string | null;
}
interface UserEl {
id: string;
group: string;
name: string;
spec: string;
unit: string;
value: number | null;
}
const apiValues = cfValues as unknown as (items: object[], users?: object[]) => Promise<Answer[]>;
const apiUsers = cfUserElements as unknown as () => Promise<UserEl[]>;
const apiAdd = cfUserElementAdd as unknown as (e: Omit<UserEl, "id">) => Promise<UserEl>;
const TEXT = {
Kind: ["무엇을 넣을까요?", "What goes in?"],
Variable: ["변수", "Variable"],
@@ -202,18 +221,47 @@ function drawChosen(
: [el("p", { className: "m02-cfip__muted", text: t("NoneChosen") })]),
);
if (!els.length) return;
const items = els.map((e) => ({ id: e.ref, ...queryOf(group, e.ref) }) as unknown as CfQuery);
void cfValues(items).then(
const items = els.map((e) => ({ id: e.ref, ...queryOf(group, e.ref) }));
void apiValues(items, lg.userElements).then(
(answers) =>
answers.forEach((a) => {
const node = values.get(a.id);
if (node && a.ok && a.value !== null && a.value !== undefined)
node.textContent = typeof a.value === "number" ? fmt(a.value) : String(a.value);
}),
() => undefined, // 값을 못 받아도 요소는 넣을 수 있음 — 값 칸만 「—」
// Api 가 아직 값을 못 주면 M01 줄에서 바로 읽어 보임(값 칸만 — 요소는 그대로)
() =>
els.forEach(
(e) =>
void m01Value(group, e).then((v) => {
const node = values.get(e.ref);
if (node && v !== null) node.textContent = fmt(v);
}),
),
);
}
/** 요소 한 개의 값을 M01 줄에서 읽음 — 이름으로 찾아 키가 같은 줄 · 못 찾으면 null */
async function m01Value(group: CfGroup, e: CfElementRef): Promise<number | null> {
if (e.ref.startsWith(USER_PREFIX)) return null;
try {
const [key, field] = e.ref.split(MACHINE_SEP);
const page = await fetchRows(
await fileOf(group),
1,
30,
e.name.split(" · ")[0].split(" ")[0],
false,
"",
"",
);
const row = (page.rows as M01Row[]).find((r) => r["키"] === key);
return row ? valueOf(group, row, field || "가격") : null;
} catch {
return null;
}
}
/** M01 줄 찾기 — 구분 · 상세구분 · 이름 · 내가 만든 것도 함께 */
async function drawFind(
host: HTMLElement,
@@ -259,21 +307,20 @@ async function drawFind(
const word = q.input.value.trim();
const [page, users] = await Promise.all([
fetchRows(file, 1, 40, word, false, sub, detail),
cfUserElements()
.then((u) =>
u.filter((x) => (x.group as string) === group || (x.group as string) === "사용자"),
)
.catch(() => []),
apiUsers().catch((): UserEl[] => []),
]);
if (mine !== seq) return;
const mineRows: M01Row[] = users
.filter((u) => !word || `${u.이름} ${u.규격}`.includes(word))
const known = new Map<string, UserEl>();
for (const u of [...lg.userElements, ...users])
if (u.group === group) known.set(u.id, u as UserEl);
const mineRows: M01Row[] = [...known.values()]
.filter((u) => !word || `${u.name} ${u.spec}`.includes(word))
.map((u) => ({
키: u.key,
이름: u.이름,
규격: u.규격,
단위: u.단위,
값: u.값,
키: u.id,
이름: u.name,
규격: u.spec,
단위: u.unit,
값: u.value,
__mine: true,
}));
const rows = [...mineRows, ...(page.rows as M01Row[])];
@@ -368,34 +415,24 @@ function newForm(
if (n !== null && !Number.isFinite(n)) return value.setError(t("NeedValue"));
name.setError(undefined);
value.setError(undefined);
cfUserElementAdd({
apiAdd({
group,
이름: name.input.value.trim(),
규격: spec.input.value.trim(),
단위: unit.input.value.trim(),
값: n,
name: name.input.value.trim(),
spec: spec.input.value.trim(),
unit: unit.input.value.trim(),
value: n,
}).then(
(made) => {
const el1 = elementOf(group, {
키: made.key,
이름: made.이름,
규격: made.규격,
단위: made.단위,
값: made.값,
키: made.id,
이름: made.name,
규격: made.spec,
단위: made.unit,
값: made.value,
});
const withUser: CfLogic = {
...lg,
userElements: [
...lg.userElements,
{
id: made.key,
group,
name: made.이름,
spec: made.규격,
unit: made.단위,
value: made.값,
},
],
userElements: [...lg.userElements.filter((u) => u.id !== made.id), { ...made, group }],
};
on.change(addElement(withUser, box.id, el1));
},
@@ -80,7 +80,9 @@ export function elementOf(group: CfGroup, row: M01Row, field = "가격"): CfElem
spec,
unit: text(row["단위"]) || "-",
};
const named = [text(row["이름"]), spec].filter(Boolean).join(" "); // 알약 이름 = 이름 + 규격(규격만 다른 요소가 부딪히지 않게)
// 환율 · 유가전력의 규격은 날짜라 알약 이름에 안 넣음(날짜가 바뀌어도 이름이 같게) — 유가전력은 상세구분(지역)으로 가름
const tail = group === "환율" ? "" : group === "유가전력" ? text(row["상세구분"]) : spec;
const named = [text(row["이름"]), tail].filter(Boolean).join(" "); // 알약 이름 = 이름 + 규격(규격만 다른 요소가 부딪히지 않게)
if (group === "기계")
return {
ref: `${key}${MACHINE_SEP}${field}`,
@@ -108,12 +110,11 @@ export function pillNames(box: CfBox): string[] {
/** 입력 값 물음 — Api `cfValues` 한 줄(id 는 부르는 쪽이 정함) */
export function queryOf(group: CfGroup, ref: string): Record<string, unknown> {
if (ref.startsWith(USER_PREFIX)) return { group: "사용자", key: ref };
if (group === "기계") {
if (group === "기계" && !ref.startsWith(USER_PREFIX)) {
const [key, field] = ref.split(MACHINE_SEP);
return { group, key, field: field || "가격" };
return { group, ref: key, field: field || "가격" };
}
return { group, key: ref };
return { group, ref };
}
/** 상자 단위 — 모두 같으면 그 단위 · 여럿이거나 없으면 「-」 */