Files
Aislo/M02_MasterTemplete/M02_Labor_Values_Model.ts
T
eomsangdonandClaude Opus 5.5 c704f18982 refactor(M02): 선택 입력 지움(39-7b G3) — 카드 · 자리 고르기 · inputs/ 연결 · 입력 목록 · 연결 · 알약 길 · 프로젝트 복사본 「입력연결」 · 시스템 층 inputs · 시험
- 넣을 값 카드(M02_Labor_Values)는 담은 로직 입력 전부를 보임(자리 「선택」 감싸기 없앰) · 이름 「넣을 값」
- 고르기 모달 면적산출 알약 = 복사본 일위대가 알약(가져온 때 그대로)
- 입력값.json · 고른자재.json · /calc 는 그대로 · storage 층 inputs/ 파일은 안 건드림(읽기만 끊음)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
2026-10-04 19:36:46 +09:00

120 lines
5.3 KiB
TypeScript

/* =============================================================================
* M02_Labor_Values_Model.ts
* 넣을 값 셈(화면 없음 · 39-3).
* 일위대가 입력 모달 · 프로젝트 고르기 모달 값 카드가 같은 값 칸 규칙(고르기 = 목록 · 범위 = 숫자 ·
* 빈 값 = 자동값)을 씀. 일위대가 입력값 = `{입력 이름: 값}`(빈 값은 안 둠 = 자동값 · 모르는 이름 버림).
* ========================================================================== */
import type { ItemInput } from "./M02_MasterTemplete_Api_Fetch";
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.자동값);
/** `입력값.json` — `{로직키: {입력이름: {값, 확인}}}` (서버 검사 모양 · 값은 수 · 글 · 빈 값) */
/** 입력 키 `{로직키}|{입력이름}` — 같은 이름의 입력도 로직마다 따로 */
export const inputKey = (logic: string, name: string): string => `${logic}|${name}`;
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 isOutside(input: ItemInput, raw: string): boolean {
const range = rangeOf(input);
if (!range || raw.trim() === "") return false;
const value = Number(raw);
return (
!Number.isFinite(value) ||
(range.min !== undefined && value < range.min) ||
(range.max !== undefined && value > range.max)
);
}
/** 칸 글 → 저장 값 — 빈 글 = null(자동값) · 범위 = 수 · 고르기 = 원래 모양 · 그 밖 = 글 */
export function storedOf(input: ItemInput, raw: string): string | number | null {
if (raw === "") return null;
if (rangeOf(input)) return Number(raw);
return choicesOf(input).find((c) => c.값 === raw)?.raw ?? raw;
}
/* ── 일위대가 입력값(39-3) — 로직 한 줄의 `{입력 이름: 값}` ───────────────────────────── */
export type LaborValues = Record<string, string | number>;
/** 로직 입력 목록에 있는 이름 · 빈 값 아닌 것만 남김 + 사라진 이름(로직 입력이 바뀌어 버려질 값) */
export function keepKnown(
inputs: ItemInput[],
values: LaborValues | null | undefined,
): { kept: LaborValues; lost: string[] } {
const names = new Set(inputs.map((i) => i.이름));
const kept: LaborValues = {};
const lost: string[] = [];
for (const [name, value] of Object.entries(values ?? {})) {
if (!names.has(name)) lost.push(name);
else if (value !== "" && value !== null && value !== undefined) kept[name] = value;
}
return { kept, lost };
}
/** 줄 아래 작은 글 — 「지역 서울 · 규격 25mm」(넣은 값만 · 입력 차례) + 빈 값 수(자동값도 없는 입력) */
export function valueLine(
inputs: ItemInput[],
values: LaborValues | null | undefined,
): { text: string; empty: number } {
const parts: string[] = [];
let empty = 0;
for (const input of inputs) {
const value = values?.[input.이름];
if (value !== undefined && value !== "") {
parts.push(`${input.이름} ${value}${input.단위 ? input.단위 : ""}`);
} else if (autoText(input) === "") empty += 1;
}
return { text: parts.join(" · "), empty };
}