- 복사본 창구를 sub6 길 모양({항목} · {종류, 판, 내용} · PUT {판, 내용} · 409 message · refresh · calc)으로 — 가짜 응답 지움
- 입력 목록 · 항목 연결은 sub4 길(fetchItemInputs · readInputLinks) · 알약은 pillsOf
- 값 받기 카드를 M02_MasterTemplete_Inputs.ts 에 모드 view · value 로 더함(InputsCard 지움 · 두 벌 금지) · 셈은 _Inputs_Model
- 상태 줄에 복사본 계산 한 줄(계 · 빠진 입력) · 목록은 모달에서 한 번만 받아 들고 있음
- Structure.ts: sub2 의 선택 입력 카드를 ItemCards 의 extraCards 로 다시 얹음
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr
152 lines
6.2 KiB
TypeScript
152 lines
6.2 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Inputs_Model.ts
|
|
* 「선택 입력」 카드의 셈(화면 없음) — 입력 키 · 알약 키 · 자리 고름 · 줄 만들기 · 알약 목록 · 값 글.
|
|
* 약속 = PLAN 25-1b: 입력 키 `{로직키}|{입력이름}` · 알약 키 `입력|{로직키}|{입력이름}` ·
|
|
* 같은 이름의 입력도 로직마다 따로. 처음은 모두 「안 씀」 — 프로그램이 추측해 나누지 않음.
|
|
* ========================================================================== */
|
|
|
|
import type { LinkItem } from "../A00_Common/spreadsheet/spreadsheet_links";
|
|
import type {
|
|
InputLine,
|
|
InputLinks,
|
|
InputSeat,
|
|
ItemInput,
|
|
ItemInputs,
|
|
} from "./M02_MasterTemplete_Api_Fetch";
|
|
|
|
/** 화면 글 ↔ 자리 — 「안 씀」 = 빈 글 */
|
|
export const SEAT_OPTIONS: { value: "" | InputSeat; label: string }[] = [
|
|
{ value: "", label: "안 씀" },
|
|
{ value: "면적", label: "면적산출 알약" },
|
|
{ value: "재료", label: "재료산출 알약" },
|
|
{ value: "선택", label: "선택" },
|
|
];
|
|
|
|
export const inputKey = (logic: string, name: string): string => `${logic}|${name}`;
|
|
export const pillKey = (logic: string, name: string): string => `입력|${logic}|${name}`;
|
|
|
|
/** 자리 고름 표 — 키 = 입력 키 · 값 = 자리(안 쓰면 키 없음) */
|
|
export type SeatMap = Map<string, InputSeat>;
|
|
|
|
export function seatsOf(links: InputLinks | null): SeatMap {
|
|
const out: SeatMap = new Map();
|
|
for (const line of links?.줄 ?? [])
|
|
if (line.자리) out.set(inputKey(line.로직, line.입력), line.자리);
|
|
return out;
|
|
}
|
|
|
|
/** 저장할 줄 — 자리를 고른 것만(없는 것 = 안 씀) · 면적 · 재료는 알약 키를 같이 */
|
|
export function linesOf(list: ItemInputs, seats: SeatMap): InputLine[] {
|
|
const out: InputLine[] = [];
|
|
for (const logic of list.로직)
|
|
for (const input of logic.입력) {
|
|
const seat = seats.get(inputKey(logic.로직, input.이름));
|
|
if (!seat) continue;
|
|
out.push({
|
|
로직: logic.로직,
|
|
입력: input.이름,
|
|
자리: seat,
|
|
...(seat === "선택" ? {} : { 알약: pillKey(logic.로직, input.이름) }),
|
|
});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export const countOf = (list: ItemInputs): { logics: number; inputs: number } => ({
|
|
logics: list.로직.length,
|
|
inputs: list.로직.reduce((sum, logic) => sum + logic.입력.length, 0),
|
|
});
|
|
|
|
/** 그 시트(면적산출 · 재료산출)의 알약 — 고른 자리가 그 시트인 입력만 · 「로직 이름 · 입력 이름」 */
|
|
export function pillsOf(
|
|
list: ItemInputs,
|
|
links: InputLinks | null,
|
|
seat: "면적" | "재료",
|
|
): LinkItem[] {
|
|
const seats = seatsOf(links);
|
|
const out: LinkItem[] = [];
|
|
for (const logic of list.로직)
|
|
for (const input of logic.입력) {
|
|
if (seats.get(inputKey(logic.로직, input.이름)) !== seat) continue;
|
|
out.push({
|
|
키: pillKey(logic.로직, input.이름),
|
|
이름: `${logic.이름} · ${input.이름}`,
|
|
...(input.단위 ? { 단위: input.단위 } : {}),
|
|
고정: true,
|
|
});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const text = (value: unknown): string =>
|
|
value === null || value === undefined
|
|
? ""
|
|
: typeof value === "object"
|
|
? JSON.stringify(value)
|
|
: String(value);
|
|
|
|
/** 고르기 · 범위 표시 — 선택지 목록이면 「a / b / c」 · 범위(배열 둘 · {최소, 최대})면 「최소 ~ 최대」 */
|
|
export function valueHint(input: ItemInput): string {
|
|
if (Array.isArray(input.고르기) && input.고르기.length) return input.고르기.map(text).join(" / ");
|
|
const range = input.범위;
|
|
if (Array.isArray(range) && range.length >= 2) return `${text(range[0])} ~ ${text(range[1])}`;
|
|
if (range && typeof range === "object") {
|
|
const r = range as Record<string, unknown>;
|
|
const lo = r.최소 ?? r.min;
|
|
const hi = r.최대 ?? r.max;
|
|
if (lo !== undefined || hi !== undefined) return `${text(lo)} ~ ${text(hi)}`;
|
|
}
|
|
return text(range);
|
|
}
|
|
|
|
/** 자동값 글 — 없으면 빈 글 */
|
|
export const autoText = (input: ItemInput): string => text(input.자동값);
|
|
|
|
/* ── 프로젝트에서 값 받기(PLAN 25-2d) — 자리 「선택」 인 입력만 로직마다 묶어 값 · 확인 표시 ── */
|
|
|
|
/** `입력값.json` — `{로직키: {입력이름: {값, 확인}}}` (서버 검사 모양 · 값은 수 · 글 · 빈 값) */
|
|
export type ValueDoc = Record<
|
|
string,
|
|
Record<string, { 값: string | number | null; 확인: boolean }>
|
|
>;
|
|
|
|
const number = (value: unknown): number | undefined =>
|
|
typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
|
|
/** 범위 모양 둘(`[최소, 최대]` · `{최소, 최대}`)을 하나로 — 범위가 아니면 null */
|
|
export function rangeOf(input: ItemInput): { min?: number; max?: number } | null {
|
|
const raw = input.범위;
|
|
if (Array.isArray(raw)) return { min: number(raw[0]), max: number(raw[1]) };
|
|
if (raw && typeof raw === "object") {
|
|
const r = raw as Record<string, unknown>;
|
|
return { min: number(r.최소 ?? r.min), max: number(r.최대 ?? r.max) };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** 고르기 목록 → 화면 값 · 이름 짝 — `raw` 는 원래 모양(숫자 목록이면 숫자로 저장) */
|
|
export function choicesOf(input: ItemInput): { 값: string; 이름: string; raw: string | number }[] {
|
|
return (input.고르기 ?? []).map((one) => {
|
|
const object =
|
|
typeof one === "object" && one !== null ? (one as Record<string, unknown>) : null;
|
|
const raw = (object ? (object.값 ?? object.이름 ?? "") : one) as string | number;
|
|
return { 값: String(raw), 이름: object ? String(object.이름 ?? raw) : String(one), raw };
|
|
});
|
|
}
|
|
|
|
/** 자리가 「선택」 인 입력만 로직 차례로 묶음 — 값 받기 카드가 보이는 입력 */
|
|
export function pickedGroups(
|
|
list: ItemInputs,
|
|
links: InputLinks | null,
|
|
): { logic: ItemInputs["로직"][number]; inputs: ItemInput[] }[] {
|
|
const picked = new Set(
|
|
[...seatsOf(links)].filter(([, seat]) => seat === "선택").map(([key]) => key),
|
|
);
|
|
return list.로직
|
|
.map((logic) => ({
|
|
logic,
|
|
inputs: logic.입력.filter((input) => picked.has(inputKey(logic.로직, input.이름))),
|
|
}))
|
|
.filter((group) => group.inputs.length > 0);
|
|
}
|