Merge remote-tracking branches 'origin/sub_laptop_1' and 'origin/sub_laptop_4' into sub_desktop_1
This commit is contained in:
@@ -9,13 +9,16 @@ import { createButton, el } from "@ui/ui_template_elements";
|
||||
import type { ElementBrief, NamedFormula } from "./M01_MasterData_UI_Logic_Api";
|
||||
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import {
|
||||
explain,
|
||||
loadTable,
|
||||
matchRow,
|
||||
relatedFinds,
|
||||
type RelatedFind,
|
||||
type TableRow,
|
||||
} from "./M01_MasterData_UI_Logic_Note";
|
||||
import { pillBar, type Pill } from "./M01_MasterData_UI_LogicLab_Pill";
|
||||
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
|
||||
import "./M01_MasterData_UI_LogicLab_Pill.css";
|
||||
|
||||
export interface ModalTarget {
|
||||
title: string;
|
||||
@@ -87,6 +90,9 @@ function tableView(
|
||||
});
|
||||
}
|
||||
|
||||
const groupOf = (ref: string): string =>
|
||||
ref.startsWith("LB") ? "인력" : ref.startsWith("M") ? "재료" : ref.startsWith("EQ") ? "기계" : "";
|
||||
|
||||
function priceView(ref: string, brief: ElementBrief): HTMLElement {
|
||||
const cols = Object.entries(brief.값칸 ?? {});
|
||||
return el("div", {
|
||||
@@ -134,38 +140,59 @@ export function openMaterialModal(target: ModalTarget): void {
|
||||
backdrop.addEventListener("click", (ev) => ev.target === backdrop && close());
|
||||
backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close());
|
||||
|
||||
const parts: HTMLElement[] = [
|
||||
el("p", { className: "m01-logic__muted", text: tl("Modal_Qty") }),
|
||||
el("pre", { className: "m01lab__expr", text: target.expr }),
|
||||
];
|
||||
const finds = relatedFinds(target.expr, target.middles);
|
||||
const price = target.element ? target.prices[target.element] : undefined;
|
||||
if (target.element?.startsWith("로직(")) {
|
||||
parts.push(el("p", { text: `${tl("Modal_Logic")} · ${target.element}` }));
|
||||
} else if (price) {
|
||||
parts.push(
|
||||
el("p", { className: "m01-logic__muted", text: tl("Modal_Price") }),
|
||||
priceView(target.element as string, price),
|
||||
);
|
||||
const base = target.element?.split(".")[0] ?? "";
|
||||
const price = target.element ? (target.prices[target.element] ?? target.prices[base]) : undefined;
|
||||
const logicRef = target.element?.startsWith("로직(") ? target.element : "";
|
||||
const pills: Pill[] = [];
|
||||
if (price && target.element) {
|
||||
pills.push({
|
||||
label: `${groupOf(base)} ${price.이름 ?? base}`.trim(),
|
||||
view: () => priceView(target.element as string, price),
|
||||
});
|
||||
} else if (target.element && !logicRef) {
|
||||
pills.push({
|
||||
label: `${groupOf(base)} ${target.title}`.trim(),
|
||||
view: () => el("p", { text: target.element }),
|
||||
});
|
||||
}
|
||||
const tables = el("div", { className: "m01lab__tables" });
|
||||
parts.push(tables);
|
||||
if (!finds.length && !price && !target.element?.startsWith("로직(")) {
|
||||
parts.push(el("p", { className: "m01-logic__muted", text: tl("Modal_NoData") }));
|
||||
if (logicRef) {
|
||||
pills.push({
|
||||
label: `${tl("Pill_Logic")} ${logicRef.slice(3).split(",")[0].replace(/\)$/, "")}`,
|
||||
view: () => el("p", { text: `${tl("Modal_Logic")} · ${logicRef}` }),
|
||||
});
|
||||
}
|
||||
for (const f of finds) {
|
||||
pills.push({
|
||||
label: `${tl("Pill_Table")} ${f.find.table}`,
|
||||
view: async () => {
|
||||
const table = await loadTable(f.find.table);
|
||||
return table
|
||||
? tableView(f, table, target.values)
|
||||
: el("p", { className: "m01-logic__bad", text: `${f.find.table} ?` });
|
||||
},
|
||||
});
|
||||
}
|
||||
const unit = price ? `${tl("Modal_UnitPrice")} ${formatNumber(price.값)} × ` : "";
|
||||
const parts: HTMLElement[] = [
|
||||
el("p", { className: "m01-logic__muted", text: tl("Modal_Sum") }),
|
||||
el("p", {
|
||||
className: "m01lab__sum",
|
||||
text: `${target.title} = ${unit}${explain(target.expr)}`,
|
||||
}),
|
||||
...(pills.length
|
||||
? [el("p", { className: "m01-logic__muted", text: tl("Pill_Hint") }), pillBar(pills)]
|
||||
: [el("p", { className: "m01-logic__muted", text: tl("Modal_NoData") })]),
|
||||
el("details", {
|
||||
className: "m01lab__raw",
|
||||
children: [
|
||||
el("summary", { text: tl("Modal_Qty") }),
|
||||
el("pre", { className: "m01lab__expr", text: target.expr }),
|
||||
],
|
||||
}),
|
||||
];
|
||||
body.replaceChildren(...parts);
|
||||
document.body.append(backdrop);
|
||||
dialog.tabIndex = -1;
|
||||
dialog.focus();
|
||||
|
||||
void Promise.all(finds.map(async (f) => ({ f, table: await loadTable(f.find.table) }))).then(
|
||||
(got) =>
|
||||
tables.replaceChildren(
|
||||
...got.map(({ f, table }) =>
|
||||
table
|
||||
? tableView(f, table, target.values)
|
||||
: el("p", { className: "m01-logic__bad", text: `${f.find.table} ?` }),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/* M01 로직 개선 시험 — 줄 모달의 알약 단추 */
|
||||
.m01lab__pills {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.m01lab__pill-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.m01lab__pill {
|
||||
padding: var(--spacing-4) var(--spacing-12);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 999px;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.m01lab__pill.is-open {
|
||||
background: var(--color-accent);
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
.m01lab__pill-stage {
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_LogicLab_Pill.ts
|
||||
* 줄 모달의 알약 단추 — 쓰인 자료를 알약으로만 늘어놓고, 누른 것 하나만 펼침(다시 누르면 접힘)
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
|
||||
export interface Pill {
|
||||
/** 알약 글 — 보기 「표 QF000421」 · 「인력 석공」 */
|
||||
label: string;
|
||||
/** 펼칠 때 그림 — 표처럼 읽어 와야 하면 Promise */
|
||||
view: () => HTMLElement | Promise<HTMLElement>;
|
||||
}
|
||||
|
||||
export function pillBar(pills: Pill[]): HTMLElement {
|
||||
const stage = el("div", { className: "m01lab__pill-stage" });
|
||||
const buttons: HTMLButtonElement[] = [];
|
||||
let open = -1;
|
||||
let turn = 0;
|
||||
const show = async (i: number): Promise<void> => {
|
||||
const mine = ++turn;
|
||||
open = open === i ? -1 : i;
|
||||
buttons.forEach((b, k) => {
|
||||
b.setAttribute("aria-expanded", String(k === open));
|
||||
b.classList.toggle("is-open", k === open);
|
||||
});
|
||||
if (open < 0) return stage.replaceChildren();
|
||||
const view = await pills[open].view();
|
||||
if (mine === turn) stage.replaceChildren(view); // 빨리 눌러도 마지막 것만
|
||||
};
|
||||
pills.forEach((p, i) => {
|
||||
const b = el("button", {
|
||||
className: "m01lab__pill",
|
||||
text: p.label,
|
||||
attrs: { type: "button", "aria-expanded": "false", "data-pill": p.label },
|
||||
});
|
||||
b.addEventListener("click", () => void show(i));
|
||||
buttons.push(b);
|
||||
});
|
||||
return el("div", {
|
||||
className: "m01lab__pills",
|
||||
children: [el("div", { className: "m01lab__pill-row", children: buttons }), stage],
|
||||
});
|
||||
}
|
||||
@@ -45,6 +45,11 @@ const TEXT = {
|
||||
],
|
||||
Click_Hint: ["줄을 누르면 그 줄이 쓰는 자료가 뜸", "Click a line to see the data it uses"],
|
||||
Modal_Close: ["닫기", "Close"],
|
||||
Modal_Sum: ["한 줄 풀이", "In one line"],
|
||||
Pill_Hint: ["쓰인 자료 — 누르면 펼침", "Data used — tap to open"],
|
||||
Modal_UnitPrice: ["단가", "Unit price"],
|
||||
Pill_Table: ["표", "Table"],
|
||||
Pill_Logic: ["로직", "Logic"],
|
||||
Modal_Qty: ["수량 식", "Quantity formula"],
|
||||
Modal_Price: ["단가 자료", "Price source"],
|
||||
Modal_Value: ["값", "Value"],
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
- `MP000006` 자갈 1줄(GF000183)
|
||||
|
||||
연결이 `{이름, 지역=입력}`·`{이름, 계약종별=입력}` 이라 **줄마다 값이 입력(자재지역·전력 계약종별)에 따라 달라짐** — 대표 줄 하나로 굳히면 지역이 바뀔 때 틀린 값이 나옴.
|
||||
→ **정리 끝(2026-09-21)**: 지역 자리 = 로직 입력 `자재지역` · 전력 = 입력 `전력 계약종별` — 17줄 모두 그 입력이 있음 · 대표로 굳히지 않는 것이 맞음 · 품목별 표 `ref/_지역_갈림_재료.md`.
|
||||
모래·자갈은 산림 12장에서 쓴 방식(고르기 조건 + 단위중량 환산)을 건설품셈 줄에도 그대로 옮기면 되지만, 원문 단위가 ㎥ 와 ㎏ 로 갈려 **사용자 확정이 한 번 더 필요**해 이번엔 손대지 않음.
|
||||
|
||||
### ③ 이름에 `{변수}` 가 든 참조 — 22줄
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# 지역으로 값이 갈리는 재료 (2026-09-21)
|
||||
|
||||
자재품목 30,592줄 훑기. **지역 칸은 없음** — 지역은 품명(이름) 글자에만 있음. 값은 지어내지 않음(세기만 함).
|
||||
|
||||
## 지역이 이름에 적히는 꼴 (셋)
|
||||
|
||||
| 꼴 | 보기 |
|
||||
|---|---|
|
||||
| 괄호 꼴 | `레미콘(관급)-경상북도(울진군)` · `아스콘(관급)-경상북도(청송군,영양군,울진군)` |
|
||||
| 쉼표 꼴 | `모 래, 서울, 도착도` · `시멘트, 서울` · `쇄석골재, 포항, …` |
|
||||
| 앞괄호 꼴 | `레미콘(서울)-사급` |
|
||||
|
||||
엔진은 로직 입력 `자재지역`(시·군 이름)이 이름에 든 줄만 후보로 잡음(빈칸 없앰 · 지역 이름 그대로 맞댐) — 꼴을 가리지 않음.
|
||||
|
||||
## 품목별 (줄 = 그 품목 이름 줄 전체 · 지역 수 · 울진)
|
||||
|
||||
| 품목 | 구분 › 상세구분 | 줄 | 지역 수 | 울진 줄 | 울진 값 |
|
||||
|---|---|---|---|---|---|
|
||||
| 레미콘 | 레미콘 › 레미콘 | 3,203 | 87 | 19 | 19 |
|
||||
| 아스콘(관급) | 아스콘·포장 › 아스콘 | 1,479 | 38 | 23 | 23 (「청송군·영양군·울진군」 묶음) |
|
||||
| 쇄석골재 | 시멘트·골재 › 골재 | 124 | 56 | 0 | — |
|
||||
| 혼합골재 | 시멘트·골재 › 골재 | 119 | 52 | 0 | — |
|
||||
| 모래 | 시멘트·골재 › 골재 | 36 | 27 | 0 | — (포항 · 삼척 있음) |
|
||||
| 시멘트 | 시멘트·골재 › 시멘트 | 71 | 21 | 0 | — |
|
||||
| 자연자갈 | 시멘트·골재 › 골재 | 6 | 4 | 0 | — (서울·광주·대전·전주) |
|
||||
| 쇄석자갈 | 시멘트·골재 › 골재 | 8 | 3 | 0 | — (금산·문경·원주) |
|
||||
|
||||
- 지역 수는 이름에서 뽑은 지역어 어휘(99)로 센 값 — 「전국」 · 「제주(관급)」 같은 묶음 이름은 하나로 셈.
|
||||
- 이름에 「울진」 이 든 줄 = 레미콘 19 · 아스콘 23 뿐 — 모래 · 자갈 · 골재 · 시멘트에는 없음.
|
||||
- 철근 · 형강 · 합판 · 각재 · 철선 등도 이름에 지역어가 있으나 **물가지 원문의 판매처·규격 글**이라 지역 선택 대상 아님(울진 0).
|
||||
|
||||
## 로직에서 어떻게 풀리는가
|
||||
|
||||
- 지역 자리 = 로직 입력 **`자재지역`**(자유 글 · 「시·군 이름」). 고르기 조건 칸(구분·상세구분·규격·대표)에는 지역 칸이 **없음** — 조건은 넓게 두고 이 입력이 이름 글자로 후보를 좁힘. 별도 자리를 더 만들지 않음.
|
||||
- 지역 값은 마스터에 굳히지 않음 — 프로젝트가 고름. 시험 계산용 `시험입력` 만 있음(건설품셈 줄 「포항」 · 12-17-1-2 「울진」).
|
||||
- 품셈재료 모래·자갈 줄(`MP000004` · `MP000006`) 비고에 「대표 줄로 굳히지 않음」 을 적음.
|
||||
|
||||
## 막힘이 나는 곳 (지역 값이 없거나 갈림)
|
||||
|
||||
| 시험 입력 | 결과 |
|
||||
|---|---|
|
||||
| `자재지역 = 울진` 으로 모래 줄을 부르는 로직 8줄(GC000211 · 677 · 685 · 933 · 934 · 938 · GF000176 · 183) | 「모래 지역 값 없음 — 울진」 — 울진 모래 줄이 없음 |
|
||||
| `GF000183` (시험입력 울진) | 모래 뒤에 자갈도 울진 없음 — 두 줄 다 막힘 |
|
||||
| `자재지역 = 삼척` | 「후보 여럿 — 규격 선택 필요 (4줄)」 — 삼척 모래는 강모래·해사·육상모래 규격이 섞임 |
|
||||
| `자재지역 = 포항` | 모래 통과 · `GF000183` 은 자갈 줄이 없어 막힘 |
|
||||
|
||||
→ 울진 현장은 **가까운 지역(삼척 등)을 고르거나 울진 값을 새로 받아야** 모래·자갈 줄이 풀림. 이 창은 값을 지어 넣지 않음.
|
||||
→ 12-17-1-2(`GF000183`) 시험입력이 울진이라 시험 계산이 멈춤 — 현장이 울진이면 데이터 대기 목록에 넣을 것.
|
||||
|
||||
## 전력 (계약종별) 원문 대조
|
||||
|
||||
원문 = `resources/knowledge/original/원가계산/전기요금_한국전력/20250101/별표1_월간전기요금표(C)_20250101.md`.
|
||||
|
||||
| 유가전력 줄 | 값 | 원문 | 판정 |
|
||||
|---|---|---|---|
|
||||
| `MO000058` 주택용 저압 전력량요금 | 120.0 | 1-가(2) 처음 200kWh까지 120.0 | 맞음 |
|
||||
| `MO000073` 일반용(갑)Ⅰ 저압 전력량요금 | 114.4 | 2-가(1) 여름 132.4 · 봄가을 91.9 · 겨울 119.0 → 단순평균 114.43 | 맞음 |
|
||||
|
||||
- 두 줄만 남긴 것은 사용자 확정(2026-09-20) — 원문에 계약종별 지정은 없음(전력비 = 실비).
|
||||
- 주택용은 **첫 구간 값만** 담음(다음 200kWh 214.6 · 초과 307.3 은 안 담음) — 시험 계산용 낮은 값이라는 뜻.
|
||||
- 전력 로직 8줄(GC000893∼897 · 933 · 934 · 938)은 입력 `전력 계약종별`(고르기 둘)로 이 두 줄 가운데 하나를 고름 — 대표로 굳히지 않음.
|
||||
@@ -12,9 +12,9 @@
|
||||
{ "키": "MP000001", "원문번호": "레미콘", "이름": "레디믹스트콘크리트", "규격": "", "단위": "㎥", "요구절": ["산림품셈 12-9-1", "산림품셈 12-11-1", "산림품셈 12-11-2-1", "산림품셈 12-12", "산림품셈 12-13", "산림품셈 12-15", "산림품셈 12-16", "산림품셈 12-17-2", "산림품셈 12-17-3", "산림품셈 13-4-4 주①", "산림품셈 13-4-5", "산림품셈 13-5-1 주⑤", "산림품셈 13-5-2", "건설품셈 공통 7-3-1 주⑥ 채움콘크리트"], "연결": {"이름": "레미콘", "규격": "", "지역": "입력"}, "고르기": null, "비고": "규격(강도·골재·슬럼프)별 값이 다르면 관리자가 줄을 나눔 · 후보 조건: 지역 입력" },
|
||||
{ "키": "MP000002", "원문번호": "시멘트", "이름": "시멘트", "규격": "", "단위": "kg", "요구절": ["산림품셈 12-1-3 주⑤⑥", "산림품셈 12-17-1-2 주③", "건설품셈 공통 6-3-1 주⑤", "건설품셈 건축 5-4-9", "건설품셈 건축 9-1-1", "건설품셈 건축 9-2-3", "건설품셈 건축 9-3-1"], "연결": "MT004176", "고르기": null, "비고": "연결: 자재품목 시멘트 전국 kg 줄" },
|
||||
{ "키": "MP000003", "원문번호": "모래(중량)", "이름": "모래", "규격": "건조상태", "단위": "kg", "요구절": ["산림품셈 12-1-3 주⑤"], "연결": null, "고르기": null, "비고": "자재품목 모래는 ㎥ — kg 환산 안 함 · 고르기 없음(자재품목 그 묶음에 같은 단위·이름 줄이 없음)" },
|
||||
{ "키": "MP000004", "원문번호": "모래", "이름": "모래", "규격": "", "단위": "㎥", "요구절": ["산림품셈 12-11-3", "산림품셈 12-17-1-2 주③", "건설품셈 공통 6-3-1 주⑤", "건설품셈 건축 9-1-1", "건설품셈 건축 9-3-1", "건설품셈 기계설비 13-6-3", "건설품셈 기계설비 13-6-6"], "연결": {"이름": "모래", "규격": "", "지역": "입력"}, "고르기": null, "비고": "후보 조건: 지역 입력" },
|
||||
{ "키": "MP000004", "원문번호": "모래", "이름": "모래", "규격": "", "단위": "㎥", "요구절": ["산림품셈 12-11-3", "산림품셈 12-17-1-2 주③", "건설품셈 공통 6-3-1 주⑤", "건설품셈 건축 9-1-1", "건설품셈 건축 9-3-1", "건설품셈 기계설비 13-6-3", "건설품셈 기계설비 13-6-6"], "연결": {"이름": "모래", "규격": "", "지역": "입력"}, "고르기": null, "비고": "후보 조건: 지역 입력 · 대표 줄로 굳히지 않음 — 지역은 프로젝트가 정하는 로직 입력 「자재지역」 · 후보 = 이름에 그 지역이 든 줄(울진 모래 줄 없음 · ref/_지역_갈림_재료.md)" },
|
||||
{ "키": "MP000005", "원문번호": "자갈(중량)", "이름": "자갈 또는 부순돌", "규격": "", "단위": "kg", "요구절": ["산림품셈 12-1-3 주⑤"], "연결": null, "고르기": null, "비고": "품명 둘(자갈·부순돌) · 자갈 값은 ㎥ · 고르기 없음(자재품목 그 묶음에 같은 단위·이름 줄이 없음)" },
|
||||
{ "키": "MP000006", "원문번호": "자갈", "이름": "자갈", "규격": "", "단위": "㎥", "요구절": ["산림품셈 12-17-1-2 주③"], "연결": {"이름": "자갈", "규격": "", "지역": "입력"}, "고르기": null, "비고": "후보 조건: 지역 입력" },
|
||||
{ "키": "MP000006", "원문번호": "자갈", "이름": "자갈", "규격": "", "단위": "㎥", "요구절": ["산림품셈 12-17-1-2 주③"], "연결": {"이름": "자갈", "규격": "", "지역": "입력"}, "고르기": null, "비고": "후보 조건: 지역 입력 · 대표 줄로 굳히지 않음 — 지역은 프로젝트가 정하는 로직 입력 「자재지역」 · 후보 = 이름에 그 지역이 든 줄(울진 자갈 줄 없음 · ref/_지역_갈림_재료.md)" },
|
||||
{ "키": "MP000007", "원문번호": "철근", "이름": "철근", "규격": "", "단위": "ton", "요구절": ["산림품셈 12-15", "산림품셈 12-16"], "연결": null, "고르기": {"구분": "철근·강재", "상세구분": "철근"}, "비고": "SD300·400·500 갈림 — 원문 강도 없음" },
|
||||
{ "키": "MP000008", "원문번호": "이형철근 SD300 D16", "이름": "이형철근", "규격": "SD300, D16", "단위": "ton", "요구절": ["산림품셈 12-9-1"], "연결": "MT014464", "고르기": null, "비고": "연결: 이형철근(SD300) D16 전국 줄" },
|
||||
{ "키": "MP000009", "원문번호": "이형철근 SD300 D13", "이름": "이형철근", "규격": "SD300, D13", "단위": "ton", "요구절": ["산림품셈 12-12"], "연결": "MT014463", "고르기": null, "비고": "연결: 이형철근(SD300) D13 전국 줄" },
|
||||
|
||||
Reference in New Issue
Block a user