Merge remote-tracking branches 'origin/sub_laptop_7' and 'origin/sub_laptop_5' into main_laptop_1
This commit is contained in:
@@ -1,20 +1,13 @@
|
||||
/* =============================================================================
|
||||
* M02_DF_Material.ts
|
||||
* 검증 칸이 쓰는 재료 찾기 · 이름 · 단가 — 서버 `GET /api/m02/df/materials?q&sub&detail&page&size`
|
||||
* → {total, page, size, items}(아직 없으면 M01 재료 찾기로 대신) · 단가는 `cfValues`(재료 ref = 키)
|
||||
* 검증 칸이 쓰는 재료 찾기 · 이름 · 단가 — 서버 `GET /api/m02/df/materials`(Api `cfMaterials` ·
|
||||
* 띄어쓰기로 나눈 말을 모두 포함 · 쪽 넘기기) · 단가 = 그 줄의 단가(값을 받는 길과 같은 셈)
|
||||
* 모양은 docs/raw/M01_쉬운단가/흐름도_정함.md 5. 이 폴더째 지워도 됨.
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { searchElements } from "../../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import { cfValues } from "./M02_DF_Api";
|
||||
import { cfMaterials, type CfMaterial } from "./M02_DF_Api";
|
||||
|
||||
export interface CfMat {
|
||||
ref: string;
|
||||
name: string;
|
||||
spec: string;
|
||||
unit: string;
|
||||
}
|
||||
export type CfMat = CfMaterial;
|
||||
|
||||
export interface CfMatPage {
|
||||
total: number;
|
||||
@@ -23,99 +16,34 @@ export interface CfMatPage {
|
||||
items: CfMat[];
|
||||
}
|
||||
|
||||
const text = (v: unknown): string =>
|
||||
typeof v === "string" || typeof v === "number" ? String(v) : "";
|
||||
|
||||
const norm = (it: Record<string, unknown>): CfMat => ({
|
||||
ref: text(it.ref ?? it.키),
|
||||
name: text(it.이름 ?? it.name),
|
||||
spec: text(it.규격 ?? it.spec),
|
||||
unit: text(it.단위 ?? it.unit),
|
||||
});
|
||||
|
||||
/** 재료 찾기 — q = 찾는 말 · sub = 구분 · detail = 상세구분 · page 는 1 부터 */
|
||||
export async function findMaterials(cond: {
|
||||
export function findMaterials(cond: {
|
||||
q?: string;
|
||||
sub?: string;
|
||||
detail?: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
}): Promise<CfMatPage> {
|
||||
const page = cond.page ?? 1;
|
||||
const size = cond.size ?? 10;
|
||||
try {
|
||||
const qs = new URLSearchParams({
|
||||
q: cond.q ?? "",
|
||||
sub: cond.sub ?? "",
|
||||
detail: cond.detail ?? "",
|
||||
page: String(page),
|
||||
size: String(size),
|
||||
});
|
||||
const r = await fetch(`${API_BASE_URL}/m02/df/materials?${qs.toString()}`, {
|
||||
credentials: "include",
|
||||
});
|
||||
if (r.ok) {
|
||||
const d = (await r.json()) as {
|
||||
total: number;
|
||||
page?: number;
|
||||
size?: number;
|
||||
items: Record<string, unknown>[];
|
||||
};
|
||||
return {
|
||||
total: d.total,
|
||||
page: d.page ?? page,
|
||||
size: d.size ?? size,
|
||||
items: d.items.map(norm),
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
/* 서버 길이 아직 없으면 M01 재료 찾기로 */
|
||||
}
|
||||
const got = await searchElements("재료", cond.q ?? "");
|
||||
const all = got.items.filter((it) => {
|
||||
const x = it as unknown as Record<string, unknown>;
|
||||
return (!cond.sub || x.구분 === cond.sub) && (!cond.detail || x.상세구분 === cond.detail);
|
||||
});
|
||||
const from = (page - 1) * size;
|
||||
return {
|
||||
total: all.length,
|
||||
page,
|
||||
size,
|
||||
items: all.slice(from, from + size).map((it) => norm(it as unknown as Record<string, unknown>)),
|
||||
};
|
||||
return cfMaterials({ ...cond, page: cond.page ?? 1, size: cond.size ?? 10 });
|
||||
}
|
||||
|
||||
const names = new Map<string, CfMat | null>();
|
||||
const byKey = new Map<string, CfMat | null>();
|
||||
|
||||
/** 재료 키 → 이름 · 규격 (한 번 물은 것은 다시 안 물음) */
|
||||
/** 재료 키 → 그 줄(이름 · 규격 · 단가) — 한 번 물은 것은 다시 안 물음 */
|
||||
export async function materialOf(key: string): Promise<CfMat | null> {
|
||||
if (!key) return null;
|
||||
if (names.has(key)) return names.get(key) ?? null;
|
||||
if (byKey.has(key)) return byKey.get(key) ?? null;
|
||||
let hit: CfMat | null = null;
|
||||
try {
|
||||
const got = await searchElements("재료", key);
|
||||
const it = got.items.find((x) => x.ref === key);
|
||||
hit = it ? norm(it as unknown as Record<string, unknown>) : null;
|
||||
hit = (await cfMaterials({ q: key, size: 20 })).items.find((m) => m.ref === key) ?? null;
|
||||
} catch {
|
||||
hit = null;
|
||||
}
|
||||
names.set(key, hit);
|
||||
byKey.set(key, hit);
|
||||
return hit;
|
||||
}
|
||||
|
||||
const prices = new Map<string, number | null>();
|
||||
|
||||
/** 재료 키 → 단가(원) — 못 구하면 null */
|
||||
export async function priceOf(key: string): Promise<number | null> {
|
||||
if (!key) return null;
|
||||
if (prices.has(key)) return prices.get(key) ?? null;
|
||||
let v: number | null = null;
|
||||
try {
|
||||
const a = (await cfValues([{ id: "p", group: "재료", ref: key }]))[0];
|
||||
v = a?.ok && typeof a.value === "number" ? a.value : null;
|
||||
} catch {
|
||||
v = null;
|
||||
}
|
||||
prices.set(key, v);
|
||||
return v;
|
||||
return (await materialOf(key))?.price ?? null;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
* M02_DF_Round.ts
|
||||
* 끝수 기본값 — 금액 단위 표준(산림품셈 1-2-2 「금액의 단위표준」)을 읽어
|
||||
* 출력(비용) 기본 = 일위대가표 금액란 · 원가 합계 기본 = 일위대가표 계금 (모두 미만 버림)
|
||||
* 표 읽기 = 서버 `GET /api/m02/df/standards` → {unit, money, saved} · 아직 없으면 `_standard/금액단위표준.json`
|
||||
* 표 읽기 = 서버 `GET /api/m02/df/standards`(Api `cfStandards`) → {unit, money, saved}
|
||||
* 모양은 docs/raw/M01_쉬운단가/흐름도_정함.md 11 · 12. 이 폴더째 지워도 됨.
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { cfStandards } from "./M02_DF_Api";
|
||||
import type { CfRound } from "./M02_DF_Model";
|
||||
|
||||
/** 표 한 줄 — 원문 그대로(종목 · 단위 · 지위 · 비고) */
|
||||
@@ -17,11 +17,6 @@ export interface CfMoneyRow {
|
||||
비고?: string;
|
||||
}
|
||||
|
||||
const FILES = import.meta.glob<{ 줄?: CfMoneyRow[] }>("./_standard/금액단위표준.json", {
|
||||
eager: true,
|
||||
import: "default",
|
||||
});
|
||||
|
||||
/** 표 모양 둘(`{줄: […]}` · 줄 목록)을 줄 목록으로 */
|
||||
const rowsOf = (x: unknown): CfMoneyRow[] => {
|
||||
const list = Array.isArray(x) ? x : (x as { 줄?: unknown } | null)?.줄;
|
||||
@@ -30,21 +25,15 @@ const rowsOf = (x: unknown): CfMoneyRow[] => {
|
||||
|
||||
let loaded: Promise<CfMoneyRow[]> | null = null;
|
||||
|
||||
/** 금액 단위 표준 줄 — 사용자가 고쳐 저장한 것이 있으면 그것 · 서버가 없으면 처음 값 */
|
||||
/** 금액 단위 표준 줄 — 서버가 고친 것(`_data`)이 있으면 그것 · 없으면 처음 값을 줌 */
|
||||
export function loadMoneyStandard(): Promise<CfMoneyRow[]> {
|
||||
loaded ??= (async () => {
|
||||
try {
|
||||
const r = await fetch(`${API_BASE_URL}/m02/df/standards`, { credentials: "include" });
|
||||
if (r.ok) {
|
||||
const d = (await r.json()) as { money?: unknown; saved?: { money?: unknown } | null };
|
||||
const rows = rowsOf(d.saved?.money ?? d.money);
|
||||
if (rows.length) return rows;
|
||||
}
|
||||
} catch {
|
||||
/* 서버가 없으면 처음 값 */
|
||||
}
|
||||
return rowsOf(Object.values(FILES)[0]);
|
||||
})();
|
||||
loaded ??= cfStandards().then(
|
||||
(s) => rowsOf(s.money),
|
||||
() => {
|
||||
loaded = null; // 못 읽었으면 다음에 다시 — 지금은 정해 둔 기본값으로
|
||||
return [];
|
||||
},
|
||||
);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* 선 규칙 · 끝수 → 엑셀 수식 — 우리 스프레드시트 엔진 한 벌로 셈 · 선 위 규칙 글(쉬운 말) · DOM 없음
|
||||
*
|
||||
* 한 번 셈 = 시트 하나: A1 부터 보낸 값(표면 머리 줄 포함 덩어리) · 그 오른쪽에 조건 값 · 맨 끝 칸에 규칙 수식
|
||||
* lookup = 줄마다 「맞음」 칸(같음 · 범위는 「이름·아래」 < 값 ≤ 「이름·위」 · 빈 끝은 열림) → 맞은 줄 수 · INDEX/MATCH
|
||||
* lookup = 줄마다 「맞음」 칸(같음 · 범위는 「이름·아래」 < 값 ≤ 「이름·위」 · 빈 끝은 열림 · 아래 = 위면 값 == 그 수)
|
||||
* → 맞은 줄 수 · INDEX/MATCH
|
||||
* gate = IF(조건, 보낸 값, NA()) · scale = × ÷ (1 ± %) · round = ROUND · ROUNDUP · ROUNDDOWN
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -177,7 +178,8 @@ function lookup(
|
||||
else if (low && high)
|
||||
tests.push(
|
||||
(r) =>
|
||||
`AND(OR(ISBLANK(${low}${r}),${low}${r}<${ref}),OR(ISBLANK(${high}${r}),${ref}<=${high}${r}))`,
|
||||
`IF(AND(NOT(ISBLANK(${low}${r})),${low}${r}=${high}${r}),${ref}=${low}${r},` +
|
||||
`AND(OR(ISBLANK(${low}${r}),${low}${r}<${ref}),OR(ISBLANK(${high}${r}),${ref}<=${high}${r})))`,
|
||||
);
|
||||
else bad = true;
|
||||
});
|
||||
|
||||
@@ -609,3 +609,14 @@ def test_다_다른_로직_받는_알약_예시값_차례() -> None:
|
||||
assert (a, b, c) == (70000, 14000, 21000)
|
||||
assert designs == [["변수", "길이", 7], ["다른 로직", "계수", 10]]
|
||||
assert plain == [7, None]
|
||||
|
||||
|
||||
def test_표_범위칸_숫자하나는_같음() -> None:
|
||||
got = _run(
|
||||
RULE
|
||||
+ "const T = [['높이·아래', '높이·위', '석공'], [4, 4, 0.5], [4, 6, 0.6], [null, 3, 0.4]];\n"
|
||||
"const rule = { kind: 'lookup', conds: [{ col: '높이', from: { box: 'b1', name: '높이' } }], col: '석공' };\n"
|
||||
"out([4, 5, 2, 3.5, 6].map(h => { const r = R.applyRule(rule, T, { 높이: h }); return [r.value, r.why]; }));"
|
||||
)
|
||||
# 4 = 「4」 줄만(같음) · 4 는 「4~6」(초과) 에 안 듦 · 빈 아래 끝은 열림
|
||||
assert got == [[0.5, None], [0.6, None], [0.4, None], [None, "표에 없는 조건"], [0.6, None]]
|
||||
|
||||
Reference in New Issue
Block a user