- 내 층 /combos/all · 남의 회사 /layers/company/owners · 회사마다 사람 /layers/personal/owners → 층 조합 하나씩 읽어 담은 것만 - EasyUse.where(「회사 · 이름」 · 「개인 · 이름」) · 못 읽은 층은 「일부를 못 읽음」 한 줄 - 시험 — holding 견본(담은 것만 · 자리 글) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
572 lines
23 KiB
TypeScript
572 lines
23 KiB
TypeScript
/* =============================================================================
|
|
* M01_EasyCalc_Api.ts
|
|
* 쉬운 단가산출 Api — 서버 길 감싸기(목록 · 열기 + 자동값 · 시험 계산 뒤에서 · 새로 · 본뜨기 ·
|
|
* 고치기 · 지우기 · 쓰는 곳) · 403 · 409 · 422 를 쉬운 말로 · 기계 기종 → 부를 기계경비 로직 · 넘길 값
|
|
* 모양은 `docs/raw/M01_쉬운단가/설계.md` 3절 그대로 · 쓰기는 `/logic/*` 만(`/save` 안 씀)
|
|
* 시험: `resources/tester/test_m01_easycalc_api.py`(서버 왕복 + 아래 「순수」 칸을 Node 로)
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
|
import {
|
|
ApiError,
|
|
copyLogic,
|
|
fetchAuto,
|
|
fetchLogic,
|
|
runCalc,
|
|
type CalcAnswer,
|
|
type LogicInput,
|
|
type LogicRow,
|
|
} from "../M01_MasterData_UI_Logic_Api";
|
|
import { safeReason } from "../M01_MasterData_UI_Logic_Text";
|
|
|
|
/** 목록 한 줄 */
|
|
export interface EasyListItem {
|
|
key: string;
|
|
name: string;
|
|
/** 품셈 기본(읽기만) · false = 내가 만든 것 */
|
|
isBase: boolean;
|
|
/** 화면 글 「품셈 기본」 · 「내가 만든 것」 */
|
|
owner: string;
|
|
/** 결과단위 「원/㎡」 */
|
|
resultUnit: string;
|
|
/** 쉬운 길로 열 수 있나 — 돈 아닌 로직(작업량 등)은 false */
|
|
canOpen: boolean;
|
|
/** 못 여는 까닭 쉬운 말 · 열 수 있으면 null */
|
|
whyNot: string | null;
|
|
/** 어디 것인지 — 「산림품셈 13장 구조물」 · 내가 만든 것은 장만 */
|
|
where: string;
|
|
}
|
|
|
|
/** 열기 답 — `trial` = 설계 때 정하는 값의 견본(자동값) */
|
|
export interface EasyOpened {
|
|
row: LogicRow;
|
|
version: string;
|
|
isBase: boolean;
|
|
trial: Record<string, unknown>;
|
|
}
|
|
|
|
/** 새로 · 본뜨기 · 고치기 답 */
|
|
export interface EasySaved {
|
|
key: string;
|
|
version: string;
|
|
row: LogicRow;
|
|
}
|
|
|
|
/** 쓰는 곳 한 줄 — 일위대가 조합 · 이 로직을 부르는 다른 로직
|
|
* `where` = 회사 · 개인 층 조합의 자리 글(「회사 · 산림조합」) · 마스터 층 · 로직은 없음
|
|
* 층을 못 읽었으면 `key` "" · `name` = 「일부를 못 읽음」 한 줄(지우기 전에 확인하게) */
|
|
export interface EasyUse {
|
|
kind: "combo" | "logic";
|
|
where?: string;
|
|
key: string;
|
|
name: string;
|
|
}
|
|
|
|
/** 기계 고르기 목록 한 줄 */
|
|
export interface EasyMachine {
|
|
분류번호: string;
|
|
이름: string;
|
|
규격: string;
|
|
/** 줄 단위(「시간」) */
|
|
단위: string;
|
|
}
|
|
|
|
/** 기종 → 호표 「요소」(기계경비 로직 부름 글) · 그 로직이 받는 설계 때 정하는 값 */
|
|
export interface EasyMachineElement {
|
|
element: string;
|
|
needInputs: LogicInput[];
|
|
}
|
|
|
|
/** 기계 시간당 경비 로직 하나 — 기종 짝 규칙의 재료 */
|
|
export interface MachineLogic {
|
|
key: string;
|
|
name: string;
|
|
resultUnit: string;
|
|
inputs: LogicInput[];
|
|
}
|
|
|
|
/* ── 순수 시작 — 아래 끝까지 밖 이름을 안 씀(`currentLanguageIndex` 만) · 시험이 Node 로 바로 돌림 ── */
|
|
const TEXT = {
|
|
Base: ["품셈 기본", "Standard"],
|
|
Mine: ["내가 만든 것", "Mine"],
|
|
NotMoney: [
|
|
"금액이 아닌 값(작업량 등)을 내는 것이라 쉬운 길로 못 엶 — 지금 화면에서 봄",
|
|
"Gives a non-money value (output etc.) — open it in the full screen",
|
|
],
|
|
E403: [
|
|
"품셈 기본은 바로 못 고침 — 「이걸 바탕으로 고치기」로 내 것을 만들어 고침",
|
|
"Standard items are read-only — use “Edit a copy”",
|
|
],
|
|
E404: [
|
|
"찾을 수 없음 — 이미 지워졌을 수 있음 · 목록을 다시 열어 주세요",
|
|
"Not found — it may be deleted; reopen the list",
|
|
],
|
|
E409: [
|
|
"다른 곳에서 먼저 고쳤음 — 다시 열어 최신 것으로 고쳐 주세요",
|
|
"Changed elsewhere first — reopen and edit again",
|
|
],
|
|
Net: ["서버와 이어지지 않음 — 잠시 뒤 다시 해 주세요", "Server unreachable — try again"],
|
|
Generic: [
|
|
"저장 검사에 걸림 — 고친 줄을 다시 확인해 주세요",
|
|
"Did not pass the check — review your changes",
|
|
],
|
|
More: ["그 밖 {n} 건", "{n} more"],
|
|
UnknownName: [
|
|
"「{name}」 값이 어디에도 없음 — 설계 때 정하는 값으로 더해 주세요",
|
|
"「{name}」 is not defined — add it as a design value",
|
|
],
|
|
NeedCost: [
|
|
"넣을 자리(사람 품값 · 재료값 · 기계·경비)를 골라 주세요",
|
|
"Pick where it goes (labor · material · equipment)",
|
|
],
|
|
NoLines: [
|
|
"줄이 하나도 없음 — 사람 · 재료 · 기계 줄을 하나 이상 더해 주세요",
|
|
"No lines — add at least one labor, material or machine line",
|
|
],
|
|
Source: ["근거 글 모양이 맞지 않음", "The source text has a wrong shape"],
|
|
CallNeeds: [
|
|
"부르는 다른 일위대가가 「{name}」 값을 받아야 함 — 설계 때 정하는 값에 더해 주세요",
|
|
"The called item needs 「{name}」 — add it as a design value",
|
|
],
|
|
CallExtra: [
|
|
"부르는 다른 일위대가는 「{name}」 값을 받지 않음",
|
|
"The called item does not take 「{name}」",
|
|
],
|
|
Kind: [
|
|
"고른 항목과 줄 종류가 맞지 않음 — 다시 골라 주세요",
|
|
"The picked item does not match the line type — pick again",
|
|
],
|
|
Loop: [
|
|
"서로를 부르는 일위대가가 생김 — 한쪽을 빼 주세요",
|
|
"Items call each other — remove one side",
|
|
],
|
|
Unread: ["수량 글을 읽지 못함 — 숫자로 넣어 주세요", "Cannot read the amount — enter a number"],
|
|
Gone: [
|
|
"이어진 항목이 없어짐 — 다른 곳에서 쓰고 있으면 지울 수 없음",
|
|
"A linked item is missing — items still in use cannot be deleted",
|
|
],
|
|
NoName: ["이름을 넣어 주세요", "Enter a name"],
|
|
Broken: ["저장 모양이 덜 갖춰짐 — 다시 열어 주세요", "The saved shape is incomplete — reopen it"],
|
|
Cut: ["합계 자르기 설정이 맞지 않음", "The rounding setting is wrong"],
|
|
Material: [
|
|
"재료 찾는 말에 맞는 자료가 없음 — 다시 골라 주세요",
|
|
"No material matches — pick again",
|
|
],
|
|
Stop: [
|
|
"이 값으로는 셈하지 못함 — 설계 때 정하는 값을 확인해 주세요",
|
|
"Cannot be calculated with these values — check the design values",
|
|
],
|
|
NoMachine: [
|
|
"이 기계는 시간당 값을 셀 자료가 없음 — 다른 기계를 골라 주세요",
|
|
"No hourly cost data for this machine — pick another",
|
|
],
|
|
AtCompany: ["회사 · {name}", "Company · {name}"],
|
|
AtPerson: ["개인 · {name}", "Personal · {name}"],
|
|
MyCompany: ["내 회사", "My company"],
|
|
Me: ["나", "Me"],
|
|
Unseen: [
|
|
"회사 · 개인 조합 일부를 못 읽음 — 지우기 전에 확인 필요",
|
|
"Some company or personal combos could not be read — check before deleting",
|
|
],
|
|
} as const satisfies Record<string, readonly [string, string]>;
|
|
|
|
type SayKey = keyof typeof TEXT;
|
|
|
|
function say(key: SayKey, fill: Record<string, string | number> = {}): string {
|
|
const text: string = TEXT[key][currentLanguageIndex as 0 | 1] ?? TEXT[key][0];
|
|
return text.replace(/\{(\w+)\}/g, (m, k: string) => (k in fill ? String(fill[k]) : m));
|
|
}
|
|
|
|
/** 검사 글 한 줄 → 쉬운 말 — 모르는 꼴은 뭉뚱그림(키 · 날 글이 화면에 안 나가게) */
|
|
function plainCheck(line: string): string {
|
|
const name = (re: RegExp): string | null => re.exec(line)?.[1] ?? null;
|
|
const unknown = name(/모르는 이름 「([^」]+)」/);
|
|
if (unknown) return say("UnknownName", { name: unknown });
|
|
const needs = name(/입력 「([^」]+)」 안 줌/);
|
|
if (needs) return say("CallNeeds", { name: needs });
|
|
const extra = name(/에 입력 「([^」]+)」 없음/);
|
|
if (extra) return say("CallExtra", { name: extra });
|
|
if (/「호표」 도 「결과」 도 없음/.test(line)) return say("NoLines");
|
|
if (/비목 「/.test(line)) return say("NeedCost");
|
|
if (/출처 모양/.test(line)) return say("Source");
|
|
if (/요소 그룹이 종류와 다름|종류 「|고르기 조건은 종류/.test(line)) return say("Kind");
|
|
if (/돌고 도는 참조/.test(line)) return say("Loop");
|
|
if (/식 못 읽음/.test(line)) return say("Unread");
|
|
if (/없는 (로직|요소|키)|로직 키/.test(line)) return say("Gone");
|
|
if (/「이름」 이 없음|이름 없음/.test(line)) return say("NoName");
|
|
if (/끝수/.test(line)) return say("Cut");
|
|
if (/검색어|후보 0|고르기 조건/.test(line)) return say("Material");
|
|
if (/칸 「[^」]+」 없음/.test(line)) return say("Broken");
|
|
return say("Generic");
|
|
}
|
|
|
|
/** 상태 번호 + 서버 몸 → 쉬운 말(여러 줄은 줄바꿈 · 셋까지) · 0 = 서버에 못 닿음 */
|
|
function plainOf(status: number, detail: unknown): string {
|
|
if (status === 0) return say("Net");
|
|
if (status === 403) return say("E403");
|
|
if (status === 404) return say("E404");
|
|
if (status === 409) return say("E409");
|
|
const body = detail as { errors?: unknown } | null;
|
|
const lines =
|
|
status === 422 && body && Array.isArray(body.errors)
|
|
? body.errors.map((x) => String(x))
|
|
: typeof detail === "string"
|
|
? [detail]
|
|
: [];
|
|
if (!lines.length) return say("Generic");
|
|
const plain = [...new Set(lines.map(plainCheck))];
|
|
const shown = plain.slice(0, 3);
|
|
if (plain.length > 3) shown.push(say("More", { n: plain.length - 3 }));
|
|
return shown.join("\n");
|
|
}
|
|
|
|
/** 계산 멈춤 까닭 — 키(`GX000001`)가 섞인 글은 뭉뚱그림 */
|
|
function plainStop(text: string): string {
|
|
return /[A-Z]{2}\d{6}/.test(text) ? say("Stop") : text;
|
|
}
|
|
|
|
const MACHINE = "기계";
|
|
/** 부착물 아닌 입력 — 이 밖의 입력(집게 · 브레이커)을 받는 로직은 부착 장비 전용 */
|
|
const MACHINE_PLAIN = new Set(["기계", "지역", "보정작업", "조종원직종"]);
|
|
|
|
/** 그 로직이 받는 기종(분류번호) 목록 */
|
|
function machineCodes(m: MachineLogic): string[] {
|
|
const spec = m.inputs.find((i) => i.이름 === MACHINE);
|
|
return (spec?.고르기 ?? []).map(String);
|
|
}
|
|
|
|
/** 짝 고르는 차례 — [부착 전용, 손료만, 손료만이면서 보정, 기종 수] 가 작은 것부터.
|
|
* 손료만 = 「지역」(연료 · 노임 값 자리)을 안 받음 · 기종 수가 작을수록 그 기종 전용(굴착기 · 조종원 없음 …) */
|
|
function machineRank(m: MachineLogic): number[] {
|
|
const names = m.inputs.map((i) => i.이름);
|
|
const attach = names.some((n) => !MACHINE_PLAIN.has(n)) ? 1 : 0;
|
|
const lossOnly = names.includes("지역") ? 0 : 1;
|
|
const boost = lossOnly && names.includes("보정작업") ? 1 : 0;
|
|
return [attach, lossOnly, boost, machineCodes(m).length];
|
|
}
|
|
|
|
/** 기종 → 부를 기계경비 로직 하나 · 받는 로직이 없으면 null */
|
|
export function pickMachine(family: MachineLogic[], code: string): MachineLogic | null {
|
|
const ranked = family
|
|
.filter((m) => machineCodes(m).includes(code))
|
|
.map((m) => ({ m, rank: machineRank(m) }))
|
|
.sort((a, b) => {
|
|
for (let i = 0; i < a.rank.length; i += 1) {
|
|
if (a.rank[i] !== b.rank[i]) return a.rank[i] - b.rank[i];
|
|
}
|
|
return a.m.key < b.m.key ? -1 : 1;
|
|
});
|
|
return ranked[0]?.m ?? null;
|
|
}
|
|
|
|
/** 로직 부름 글 — 기계는 고정 분류번호 · 나머지 입력은 같은 이름의 설계 때 정하는 값으로 넘김 */
|
|
export function machineCall(m: MachineLogic, code: string): EasyMachineElement {
|
|
const quote = (n: string): string => (/^[\w가-힣]+$/.test(n) ? n : `'${n}'`);
|
|
const needInputs = m.inputs.filter((i) => i.이름 !== MACHINE);
|
|
const rest = needInputs.map((i) => `, ${quote(i.이름)}=${quote(i.이름)}`).join("");
|
|
return { element: `로직(${m.key}, ${MACHINE}='${code}'${rest})`, needInputs };
|
|
}
|
|
|
|
/** 층 조합 줄 통째(`담은로직` 포함) */
|
|
export interface LayerCombo {
|
|
키: string;
|
|
이름?: string;
|
|
담은로직?: { 로직?: string }[];
|
|
}
|
|
|
|
/** 이 로직을 담은 층 조합 — `layer` = company · personal · `name` = 층 주인 이름 */
|
|
export function holding(
|
|
combos: LayerCombo[],
|
|
key: string,
|
|
layer: "company" | "personal",
|
|
name: string,
|
|
): EasyUse[] {
|
|
const where = say(layer === "company" ? "AtCompany" : "AtPerson", { name });
|
|
return combos
|
|
.filter((c) => (c.담은로직 ?? []).some((one) => one?.로직 === key))
|
|
.map((c): EasyUse => ({ kind: "combo", where, key: c.키, name: c.이름 ?? "" }));
|
|
}
|
|
/* ── 순수 끝 ── */
|
|
|
|
const OWN = "로직_자체.json"; // 내가 만든 것 한 자리(서버 `Store_Make.OWN`)
|
|
const OWNER = "현장";
|
|
const CALC_WAIT_MS = 300;
|
|
const MACHINE_FILE = "건설기계"; // 기계 시간당 경비 로직이 든 장 파일(「08장 건설기계」)
|
|
|
|
/** `/api/m01` 길 · M02 층 조합 읽기는 `m02` */
|
|
const request = <T>(path: string, body?: unknown): Promise<T> =>
|
|
send<T>(`${API_BASE_URL}/m01${path}`, body);
|
|
const m02 = <T>(path: string): Promise<T> => send<T>(`${API_BASE_URL}/m02${path}`);
|
|
|
|
async function send<T>(url: string, body?: unknown): Promise<T> {
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(url, {
|
|
method: body === undefined ? "GET" : "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
});
|
|
} catch {
|
|
throw new ApiError(0, "");
|
|
}
|
|
const data = (await response.json().catch(() => ({}))) as { detail?: unknown } & T;
|
|
if (!response.ok) throw new ApiError(response.status, data.detail ?? response.statusText);
|
|
return data;
|
|
}
|
|
|
|
const query = (params: Record<string, string>): string => new URLSearchParams(params).toString();
|
|
|
|
interface Summary {
|
|
file: string;
|
|
키: string;
|
|
구분: string;
|
|
상세구분: string;
|
|
이름: string;
|
|
결과단위: string;
|
|
}
|
|
|
|
const isMoney = (unit: string): boolean => unit.startsWith("원");
|
|
|
|
/** 목록 — 내가 만든 것 먼저 · 돈 아닌 로직은 「쉬운 길로 못 엶」 */
|
|
export async function listEasy(q: string): Promise<EasyListItem[]> {
|
|
const got = await request<{ logics: Summary[] }>(`/logics?${query({ q })}`);
|
|
const items = got.logics.map((s): EasyListItem => {
|
|
const isBase = s.file !== OWN;
|
|
const canOpen = isMoney(s.결과단위);
|
|
return {
|
|
key: s.키,
|
|
name: s.이름,
|
|
isBase,
|
|
owner: say(isBase ? "Base" : "Mine"),
|
|
resultUnit: s.결과단위,
|
|
canOpen,
|
|
whyNot: canOpen ? null : say("NotMoney"),
|
|
where: isBase ? `${s.구분} ${s.상세구분}`.trim() : s.상세구분,
|
|
};
|
|
});
|
|
return [...items.filter((x) => !x.isBase), ...items.filter((x) => x.isBase)];
|
|
}
|
|
|
|
/** 열기 — 로직 줄 · 판본 · 견본 설계 값(자동값 · 못 받으면 빈 채로) */
|
|
export async function openEasy(key: string): Promise<EasyOpened> {
|
|
const [one, auto] = await Promise.all([fetchLogic(key), fetchAuto(key)]);
|
|
return {
|
|
row: one.logic,
|
|
version: one.version,
|
|
isBase: one.file !== OWN,
|
|
trial: trialFor(one.logic, auto),
|
|
};
|
|
}
|
|
|
|
/** 로직이 받는 설계 값만 — 모르는 이름을 보내면 서버가 멈춤 */
|
|
function trialFor(row: LogicRow, trial: Record<string, unknown>): Record<string, unknown> {
|
|
return Object.fromEntries(
|
|
(row.입력 ?? []).filter((i) => i.이름 in trial).map((i) => [i.이름, trial[i.이름]]),
|
|
);
|
|
}
|
|
|
|
let calcSeq = 0;
|
|
let calcTimer: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
/** 0.3초 뒤 서버 시험 계산 · 최신 응답만 `onDone` · 멈춤 · 실패는 `plain` 쉬운 말 */
|
|
export function calcLater(
|
|
row: LogicRow,
|
|
trial: Record<string, unknown>,
|
|
onDone: (ans: CalcAnswer | null, plain: string | null) => void,
|
|
): void {
|
|
const mine = ++calcSeq;
|
|
clearTimeout(calcTimer);
|
|
calcTimer = setTimeout(() => {
|
|
runCalc({ key: row.키 || "", inputs: trialFor(row, trial), row })
|
|
.then((ans) => {
|
|
if (mine === calcSeq) onDone(ans, ans.ok ? null : plainStop(safeReason(ans.reason)));
|
|
})
|
|
.catch((e: unknown) => {
|
|
if (mine === calcSeq) onDone(null, plainError(e));
|
|
});
|
|
}, CALC_WAIT_MS);
|
|
}
|
|
|
|
/** 새로 — 키 · 갈래는 서버가 줌 · 근거 글이 비면 「자체」(검사가 빈 글을 막음) */
|
|
export async function newEasy(row: LogicRow): Promise<EasySaved> {
|
|
const logic = { ...row, 출처: row.출처 || "자체" };
|
|
const got = await request<{ version: string; key: string; logic: LogicRow }>("/logic/new", {
|
|
logic,
|
|
owner: OWNER,
|
|
});
|
|
return { key: got.key, version: got.version, row: got.logic };
|
|
}
|
|
|
|
/** 이걸 바탕으로 고치기 — 품셈 기본 · 내 것 모두 본뜸 */
|
|
export async function copyEasy(key: string): Promise<EasySaved> {
|
|
const got = await copyLogic(key);
|
|
return { key: got.key, version: got.version, row: got.logic };
|
|
}
|
|
|
|
/** 고치기 — 내가 만든 것만(품셈 기본은 403) · 판본이 다르면 409 */
|
|
export async function editEasy(key: string, version: string, row: LogicRow): Promise<EasySaved> {
|
|
const got = await request<{ version: string; logic: LogicRow }>("/logic/edit", {
|
|
key,
|
|
version,
|
|
logic: row,
|
|
});
|
|
return { key, version: got.version, row: got.logic };
|
|
}
|
|
|
|
/** 지우기 — 내가 만든 것만 · 쓰는 곳이 남으면 422 */
|
|
export async function deleteEasy(key: string, version: string): Promise<{ version: string }> {
|
|
const got = await request<{ version: string }>("/logic/delete", { key, version });
|
|
return { version: got.version };
|
|
}
|
|
|
|
/** 쓰는 곳 — 담은 일위대가 조합(마스터 · 회사 · 개인 층) + 이 로직을 부르는 내가 만든 것.
|
|
* 지우기 누를 때 부름(무거움) · 서버 지우기 검사는 마스터 층 조합만 막음.
|
|
* ponytail: 부르는 로직은 내가 만든 것만 하나씩 받아 훑음(품셈 기본은 내 것을 안 부름) —
|
|
* 내 것이 수백이 되면 서버에 「부르는 로직」 길을 더할 것 */
|
|
export async function usesOf(key: string): Promise<EasyUse[]> {
|
|
const [combos, mine, layered] = await Promise.all([
|
|
request<{ combos: { 키: string; 이름: string }[] }>(`/logic/combos?${query({ key })}`),
|
|
request<{ logics: Summary[] }>(`/logics?${query({ sub: "자체" })}`),
|
|
layerUses(key),
|
|
]);
|
|
const call = `로직(${key}`;
|
|
const others = mine.logics.filter((s) => s.키 !== key);
|
|
const rows = await Promise.all(others.map((s) => fetchLogic(s.키)));
|
|
const callers = rows
|
|
.filter((one) => JSON.stringify(one.logic).includes(call))
|
|
.map((one): EasyUse => ({ kind: "logic", key: one.logic.키, name: one.logic.이름 }));
|
|
return [
|
|
...combos.combos.map((c): EasyUse => ({ kind: "combo", key: c.키, name: c.이름 })),
|
|
...layered,
|
|
...callers,
|
|
];
|
|
}
|
|
|
|
interface LayerTarget {
|
|
layer: "company" | "personal";
|
|
owner: string;
|
|
name: string;
|
|
rows?: { 키: string }[];
|
|
}
|
|
|
|
/** 회사 · 개인 층 조합 가운데 이 로직을 담은 것 — M02 층 길 읽기만(시스템 관리자는 모든 층).
|
|
* 내 층 = `/combos/all` · 남의 회사 = `/layers/company/owners` · 회사마다 사람 = `/layers/personal/owners`.
|
|
* 못 읽은 층이 있으면 「일부를 못 읽음」 한 줄을 더함.
|
|
* ponytail: 층마다 조합 하나씩 받아 훑음 — 조합이 수천이면 서버에 「이 로직을 담은 층 조합」 길을 더할 것 */
|
|
async function layerUses(key: string): Promise<EasyUse[]> {
|
|
let missed = false;
|
|
const soft = <T>(p: Promise<T>, empty: T): Promise<T> =>
|
|
p.catch(() => {
|
|
missed = true;
|
|
return empty;
|
|
});
|
|
type Own = { layer: string; owner: string | null; 줄: { 키: string }[] };
|
|
type Owner = { owner: string; name: string };
|
|
const own = await soft(m02<Own[]>("/combos/all"), []);
|
|
const targets: LayerTarget[] = [];
|
|
let myCompany = "";
|
|
for (const one of own) {
|
|
if (one.layer === "system" || !one.owner) continue;
|
|
const company = one.layer === "company";
|
|
if (company) myCompany = one.owner;
|
|
targets.push({
|
|
layer: company ? "company" : "personal",
|
|
owner: one.owner,
|
|
name: say(company ? "MyCompany" : "Me"),
|
|
rows: one.줄,
|
|
});
|
|
}
|
|
const companies = await soft(m02<Owner[]>("/layers/company/owners"), []);
|
|
for (const c of companies) targets.push({ layer: "company", ...c });
|
|
const people = await Promise.all(
|
|
[myCompany, ...companies.map((c) => c.owner)]
|
|
.filter(Boolean)
|
|
.map((c) => soft(m02<Owner[]>(`/layers/personal/owners?${query({ company: c })}`), [])),
|
|
);
|
|
for (const p of people.flat()) targets.push({ layer: "personal", ...p });
|
|
|
|
const found = await Promise.all(
|
|
targets.map(async (t) => {
|
|
const at = `/layers/${t.layer}/combos`;
|
|
const own = `?${query({ owner: t.owner })}`;
|
|
const rows = t.rows ?? (await soft(m02<{ 줄: { 키: string }[] }>(at + own), { 줄: [] })).줄;
|
|
const combos = await Promise.all(
|
|
rows.map((r) =>
|
|
soft(m02<{ combo: LayerCombo }>(`${at}/${encodeURIComponent(r.키)}${own}`), null),
|
|
),
|
|
);
|
|
const full = combos.flatMap((x) => (x ? [x.combo] : []));
|
|
return holding(full, key, t.layer, t.name);
|
|
}),
|
|
);
|
|
const out = found.flat();
|
|
if (missed) out.push({ kind: "combo", key: "", name: say("Unseen") });
|
|
return out;
|
|
}
|
|
|
|
let family: Promise<MachineLogic[]> | null = null;
|
|
|
|
/** 기계 시간당 경비 로직 묶음 — 한 번 받아 둠(실패하면 다음에 다시) */
|
|
function machineFamily(): Promise<MachineLogic[]> {
|
|
family ??= request<{ logics: Summary[] }>("/logics")
|
|
.then((got) =>
|
|
Promise.all(
|
|
got.logics
|
|
.filter((s) => s.file.includes(MACHINE_FILE) && isMoney(s.결과단위))
|
|
.map((s) => fetchLogic(s.키)),
|
|
),
|
|
)
|
|
.then((all) =>
|
|
all
|
|
.map((one) => ({
|
|
key: one.logic.키,
|
|
name: one.logic.이름,
|
|
resultUnit: one.logic.결과단위,
|
|
inputs: one.logic.입력 ?? [],
|
|
}))
|
|
.filter((m) => machineCodes(m).length > 0),
|
|
)
|
|
.catch((e: unknown) => {
|
|
family = null;
|
|
throw e;
|
|
});
|
|
return family;
|
|
}
|
|
|
|
interface MachineRow {
|
|
원문번호?: string;
|
|
이름?: string;
|
|
규격?: string;
|
|
}
|
|
|
|
/** 기계 고르기 — 이름 · 분류번호 찾기 · 시간당 값을 셀 수 있는 기종만 */
|
|
export async function listMachines(q: string): Promise<EasyMachine[]> {
|
|
const [all, got] = await Promise.all([
|
|
machineFamily(),
|
|
request<{ items: MachineRow[] }>(`/elements?${query({ group: MACHINE, q, limit: "200" })}`),
|
|
]);
|
|
return got.items.flatMap((row): EasyMachine[] => {
|
|
const code = String(row.원문번호 ?? "");
|
|
const m = pickMachine(all, code);
|
|
if (!m) return [];
|
|
const unit = m.resultUnit.split("/")[1] ?? "";
|
|
return [{ 분류번호: code, 이름: row.이름 ?? "", 규격: row.규격 ?? "", 단위: unit }];
|
|
});
|
|
}
|
|
|
|
/** 기종 → 호표 「요소」 글 · 받아야 할 설계 값(지역 등) — 호표 종류는 「로직」 */
|
|
export async function machineElement(기종: string): Promise<EasyMachineElement> {
|
|
const m = pickMachine(await machineFamily(), 기종);
|
|
if (!m) throw new Error(say("NoMachine"));
|
|
return machineCall(m, 기종);
|
|
}
|
|
|
|
/** 어떤 실패든 화면에 찍을 쉬운 말 한두 줄 */
|
|
export function plainError(e: unknown): string {
|
|
if (e instanceof ApiError) return plainOf(e.status, e.detail);
|
|
if (e instanceof Error && e.message === say("NoMachine")) return e.message;
|
|
return say("Net");
|
|
}
|