Merge remote-tracking branches 'origin/sub_laptop_2', 'origin/sub_laptop_3' and 'origin/sub_laptop_4' into main_laptop_1

This commit is contained in:
2026-10-01 20:13:47 +09:00
5 changed files with 42 additions and 88 deletions
@@ -314,7 +314,7 @@ def _rounded(value: Any, r: dict) -> Any:
def _lookup(rule: dict, block: Any, conds: dict) -> tuple[Any, str | None]:
"""표에서 찾기 — 같음 · 범위(「이름·아래」 < 값 ≤ 「이름·위」 · 빈 끝 열림) · 맞은 줄 하나만."""
"""표에서 찾기 — 같음 · 범위(아래 < 값 ≤ 위 · 빈 끝 열림 · 아래 = 위면 같음) · 맞은 줄 하나."""
if not isinstance(block, list) or len(block) < 2:
return None, "오류"
head = [str(h if h is not None else "") for h in block[0]]
@@ -336,6 +336,8 @@ def _lookup(rule: dict, block: Any, conds: dict) -> tuple[Any, str | None]:
def fits(row, lo=lo, hi=hi, x=x) -> bool:
low, high = _num_of(row[lo]), _num_of(row[hi])
if low is not None and low == high: # 숫자 하나(아래 = 위) = 같음(브레인 정함)
return x is not None and x == low
return (
x is not None
and (row[lo] is None or (low is not None and low < x))
@@ -21,12 +21,10 @@ import {
type SubInfo,
} from "../../M01_MasterData/M01_MasterData_Api_Fetch";
import { cfPlain, cfUserElementAdd, cfUserElements, cfValues } from "./M02_DF_Api";
import type { CfBox, CfElementRef, CfGroup, CfLogic } from "./M02_DF_Model";
import type { CfBox, CfGroup, CfLogic } from "./M02_DF_Model";
import {
GROUPS,
M01_OF,
MACHINE_SEP,
USER_PREFIX,
addElement,
elementOf,
machineFields,
@@ -243,39 +241,10 @@ function drawChosen(
if (node && a.ok && a.value !== null && a.value !== undefined)
node.textContent = typeof a.value === "number" ? fmt(a.value) : String(a.value);
}),
// 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);
}),
),
() => undefined, // 값을 못 받아도 요소는 넣을 수 있음 — 값 칸만 「—」
);
}
/** 요소 한 개의 값을 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,
@@ -406,7 +375,6 @@ async function drawFind(
list,
);
void load();
void USER_PREFIX;
}
/** 새 요소 — M01 표 「추가」 화면 꼴(이름 · 규격 · 단위 · 값) · 마스터에 안 쓰고 사용자 요소로 */
@@ -1,11 +1,10 @@
/* =============================================================================
* M02_DF_InputPanel_Material.ts
* 입력 판 — 재료 요소 방식 셋(고정 · 선택 자리 · 조건 찾기) · 자재 찾기(띄어쓰기 AND · 규격까지 · 쪽 넘기기 · 전체 수)
* 서버 `/api/m02/df/materials` 를 먼저 부르고 아직 없으면 M01 `/pick?kind=price` 로 같은 모양을 만듦
* 서버 `/api/m02/df/materials`(cfMaterials)
* 글 [한, 영] 짝은 아래 TEXT · 단추는 공용 작은 크기
* ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend";
import { currentLanguageIndex } from "@ui/ui_template_locale";
import {
createButton,
@@ -15,7 +14,7 @@ import {
showToast,
} from "@ui/ui_template_elements";
import { fetchFiles, fetchSubs, type SubInfo } from "../../M01_MasterData/M01_MasterData_Api_Fetch";
import { cfValues } from "./M02_DF_Api";
import { cfMaterials, cfValues } from "./M02_DF_Api";
import type { CfElementRef, CfLogic, CfMaterialMode, CfMaterialRule } from "./M02_DF_Model";
import {
catchNote,
@@ -101,48 +100,9 @@ export interface MatPage {
items: MatItem[];
}
async function getJson<T>(path: string): Promise<T> {
const res = await fetch(`${API_BASE_URL}${path}`, { credentials: "include" });
if (!res.ok) throw new Error(String(res.status));
return (await res.json()) as T;
}
/** 서버가 없으면 M01 값 줄에서 같은 모양으로 — 띄어쓰기 AND(이름 + 규격) · 쪽은 앞에서 잘라 줌 */
async function fallback(q: MatQuery): Promise<MatPage> {
const r = await getJson<{ items: Record<string, unknown>[] }>(
`/m01/pick?${new URLSearchParams({ kind: "price", q: q.q, limit: "200" })}`,
);
const rows = r.items.filter((i) => !q.sub || i["구분"] === q.sub);
const items = rows.map((i): MatItem => ({
ref: String(i["ref"]),
name: String(i["이름"] ?? ""),
spec: String(i["규격"] ?? ""),
unit: String(i["단위"] ?? ""),
price: typeof i["값"] === "number" ? i["값"] : null,
구분: String(i["구분"] ?? ""),
}));
const from = (q.page - 1) * q.size;
return {
total: items.length,
page: q.page,
size: q.size,
items: items.slice(from, from + q.size),
};
}
/** 서버 자재 찾기 — 띄어쓰기로 나눈 말을 모두 포함 · 쪽 넘기기 · 전체 수 */
export async function searchMaterials(q: MatQuery): Promise<MatPage> {
const params = new URLSearchParams({
q: q.q,
page: String(q.page),
size: String(q.size),
sub: q.sub ?? "",
detail: q.detail ?? "",
});
try {
return await getJson<MatPage>(`/m02/df/materials?${params}`);
} catch {
return fallback(q);
}
return cfMaterials({ q: q.q, sub: q.sub, detail: q.detail, page: q.page, size: q.size });
}
let subsCache: SubInfo[] | null = null;
+15 -9
View File
@@ -2,12 +2,13 @@
* M02_DF_UnitPick.ts
* 단가산출 흐름도 공용 단위 고르기 — 치면 걸러진 목록 · 고르기만(손글 안 받음) · 「-」 = 없음
* 목록 = 단위 표준(산림품셈 1-2-2) 단위 + 부른 쪽이 주는 마스터 결과단위 등 + 원
* 표준 표 읽는 길은 `setUnitSource` 로 바꿔 끼움(기본 = 처음 값 `_standard/단위표준.json`) — 다른 판도 이것을 씀
* 표준 표 읽는 길은 `setUnitSource` 로 바꿔 끼움(기본 = 서버 /standards) — 다른 판도 이것을 씀
* 글 [한, 영] 짝은 아래 TEXT
* ========================================================================== */
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { createButton, el } from "@ui/ui_template_elements";
import { cfStandards } from "./M02_DF_Api";
import "./M02_DF_UnitPick.css";
const TEXT = {
@@ -36,9 +37,9 @@ interface StandardRow {
"단위수량 소수자리"?: number | null;
}
/** 처음 값 — 번들에 든 표준 표(단위표준.json) */
const bundled: UnitSource = async () => {
const table = (await import("./_standard/단위표준.json")).default as { 줄: StandardRow[] };
/** 서버 표준 표(/standards) — 고친 표가 있으면 그것, 없으면 처음 값 */
const fromServer: UnitSource = async () => {
const table = (await cfStandards()).unit as { 줄: StandardRow[] };
const units = table.줄
.flatMap((r) => [r["규격 단위"], r["단위수량 단위"]])
.filter((u): u is string => !!u);
@@ -52,13 +53,13 @@ const bundled: UnitSource = async () => {
const places = Object.fromEntries(
Object.entries(count).map(([u, c]) => [
u,
Number(Object.entries(c).sort((a, b) => b[1] - a[1])[0][0]),
Number(Object.entries(c).sort((x, y) => y[1] - x[1])[0][0]),
]),
);
return { units, places };
};
let source: UnitSource = bundled;
let source: UnitSource = fromServer;
let cache: Promise<UnitStandard> | null = null;
/** 표준 표 읽는 길 바꾸기(서버에서 고친 표를 읽을 때) — 목록을 다시 받음 */
@@ -67,15 +68,20 @@ export function setUnitSource(next: UnitSource): void {
cache = null;
}
/** 표준 단위 + 원 — 같은 글은 한 번 · 「없음」 은 뺌 */
export function loadStandard(): Promise<UnitStandard> {
/** 표준 단위 + 원 — 같은 글은 한 번 · 「없음」 은 뺌 · 서버를 못 읽으면 원만(다음에 다시 읽음) */
export async function loadStandard(): Promise<UnitStandard> {
cache ??= source().then((std) => {
const set = new Set([...std.units, "원"]);
set.delete("없음");
set.delete(NO_UNIT);
return { units: [...set], places: std.places };
});
return cache;
try {
return await cache;
} catch {
cache = null;
return { units: ["원"], places: {} };
}
}
/** 단위를 고르면 표준 표에서 소수자리 제안 — 없으면 null */
+18
View File
@@ -547,6 +547,24 @@ def test_선_규칙_끝수_재료_조건_설계_때_정할_것(client: TestClien
assert _d(
_post(client, "/api/m02/df/save", {"logic": lg})["values"]["ports"]["f"]["합"]
) == int(Decimal("0.5") + 33000 + 3)
lg["boxes"][0]["input"]["variables"][0]["example"] = (
"견치돌" # 「높이 4」 줄 = 아래 = 위 → 같음(정함)
)
lg["boxes"][0]["input"]["variables"][1]["example"] = 4
assert _d(
_post(client, "/api/m02/df/save", {"logic": lg})["values"]["ports"]["f"]["합"]
) == int(Decimal("0.7") + 33000 + 4)
lookup = {
"kind": "lookup",
"col": "석공",
"conds": [{"col": "높이", "from": {"box": "v", "name": "높이"}}],
}
block = [["높이·아래", "높이·위", "석공"], [4, 4, 0.7], [None, 3, 0.5]]
assert [cf.dc.apply_rule(lookup, block, {"높이": h})[0] for h in (4, 3.5, 3)] == [
0.7,
None,
0.5,
]
lg["boxes"][0]["input"]["variables"][2]["example"] = "D999" # 맞는 재료 없음
lg["boxes"][0]["input"]["variables"][0]["example"] = "없는돌" # 표에 없는 조건 → 함수 멈춤
bad = _post(client, "/api/m02/df/save", {"logic": lg})["values"]