Files
Aislo/M01_MasterData/M01_MasterData_UI_Logic_PickTable.ts
T
eomsangdonandClaude Opus 5.5 64affbb00e feat(830-2): 재료 DB 가격 열 재정의 · 항목 통합 이어 감 — 조달청 · 일반가격 · 상하차 · 재료 아닌 줄 삭제
- 열 한 벌 = 키 · 구분 · 상세구분 · 이름 · 규격 · 단위 · 상하차 · 조달청 · 물가자료 · 유통물가 · 물가정보 · 거래가격 · 일반가격 · 출처 · 비고 · 면수(material_merge.COLUMNS 한 곳 · check_master 가 씀)
- 「관급」 칸 → 「조달청」(값 0 · EST 가격정보 값 안 씀) · 새 칸 「일반가격」(빈칸 · 시스템 관리자 손 입력 · 빌더가 원문번호로 이어 받음) · 상하차 = 단위 바로 뒤
- 남는 줄 지역 기준 = 전국 › 지역 없음 › 서울 › 서울 권역 › 경기 › 그 밖(값 중간) · 기준 지역은 비고 「통합: 기준 지역 …」(지역 없는 묶음은 안 적음)
- 재료 아닌 줄 삭제(요구 832) — 품질시험 수수료 60(가격정보 출처 「2026(경북)」) · 일반 도로용 블록 5(「~26.10.16」) · 치즐 2 줄은 남김 · 값 없음
- 재료_자재품목 24,092 → 17,692 줄(통합 6,335 · 재료 아닌 줄 65) · 남는 줄 키 = 830-1 키(7565ee787 이 지운 대장 키를 되살려 다시 지음 · 새 키 0) · 남는 줄 물가지 값 바뀐 것 0 · 빌더 두 번 = 같은 파일
- 가장 낮은 값 = 사급 칸(물가지 넷 + 일반가격) — master_material.SLOTS · M02_Elem_Alias.PRICES · M02 PRICE_SLOTS · 조달청 칸 안 봄 · M01 고르기 「관급」 표시 = 조달청 칸 · B09 대비표 「관급」 출처 = M01 조달청 칸(이름 그대로)
- M01 재료 화면 상하차 = 고르기(부품 createSelectField) · 새 줄 기본 「-」 · 로직 고르기 표 · 시험 계산 값 줄에 일반가격
- check_master 틀 · 로직 · 조합 0 · 단위 27 · 배 7(전과 같음) · _틀.md · _갱신절차.md 열 정의
- 시험: 새 test_830_2_material_merge · test_830_1 지움(EST 그대로 상태 = 830-2 로 끝남 · 남는 검사는 830_2 로) · 지역 줄 · 관급 이름 · 관급 칸을 보던 시험 기대값 고침(m01_api · m01_pick · masterdata_check · 814_1 · 815_1 · 827_2 · 827_3 · 750_1 · m02_hopyo_match · m02_labor_doc) · 「시멘트 전국」 검색어 시험 = xfail(830-3 연결 할 일)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 18:36:51 +09:00

147 lines
5.1 KiB
TypeScript

/* =============================================================================
* M01_MasterData_UI_Logic_PickTable.ts
* 시험 계산의 재료 · 기계 고르기 모달 — 마스터 컨테이너와 같은 표(`GET /rows`) · 쪽 나눔 · 칸 글
* 검색 칸을 채워 열고 · 잡힌 품목 칠함 · 한 줄 누르면 고름 · 저장 안 함(읽기만)
* ========================================================================== */
import { createInputField, el, showToast } from "@ui/ui_template_elements";
import { fetchRows, type Row } from "./M01_MasterData_Api_Fetch";
import { buildPager, flatten, show } from "./M01_MasterData_UI_Cells";
import { fitRows, PAGER_ROOM, pageKeepFirst, watchFit } from "@ui/ui_template_pager";
import { tx } from "./M01_MasterData_UI_Logic_Text";
import { openModal } from "@ui/ui_template_modal";
export type PickTableKind = "재료" | "기계";
const FILES: Record<PickTableKind, string> = { 재료: "재료_자재품목.json", 기계: "기계.json" };
/** 표에 보일 칸 — 컨테이너 표의 칸 이름(묶음 값은 「값›가격」) */
const COLS: Record<PickTableKind, string[]> = {
재료: [
"키",
"구분",
"상세구분",
"이름",
"규격",
"단위",
"물가자료",
"유통물가",
"물가정보",
"거래가격",
"일반가격",
],
기계: ["원문번호", "구분", "상세구분", "이름", "규격", "값›가격"],
};
/** 모달 안 표 칸 + 쪽 단추가 쓸 높이 — 모달 최대 높이(80vh)에서 제목 · 찾기 줄을 뺌 */
const room = (): number => window.innerHeight * 0.8 - 130;
export interface PickTableOptions {
kind: PickTableKind;
title: string;
/** 검색 칸을 채워 엶 — 결과가 없으면 첫 낱말만으로 다시 · 그래도 없으면 `alt` 를 차례로 */
seed: string;
alt?: string[];
/** 지금 잡힌 줄이면 true — 그 줄을 칠함 */
isCurrent: (row: Row) => boolean;
onPick: (row: Row) => void;
}
export function openPickTable(opt: PickTableOptions): void {
const searchField = createInputField({
type: "search",
placeholder: tx("Pick_Search"),
value: opt.seed,
});
const search = searchField.input;
const host = el("div", { className: "m01-logic__pick-list" });
// 모달 부품 — 제목줄 [닫기] · 바깥 누르기 · Esc · 위 고정 · 폭만 표 모달 곁값
const { close } = openModal({
title: opt.title,
closeLabel: tx("Pick_Close"),
dialogClass: "m01-logic__pick m01-logic__pick--table",
backdropClass: "m01-logic__backdrop",
mount: (body) => body.append(searchField.root, host),
});
let page = 1;
let size = fitRows(room() - PAGER_ROOM); // 처음 어림 — 붙은 뒤 다시 재서 어긋나면 바로잡음
const tried = new Set([opt.seed]);
let seq = 0;
const load = async (fallback = false): Promise<void> => {
const mine = ++seq;
try {
const q = search.value.trim();
const got = await fetchRows(FILES[opt.kind], page, size, q);
if (mine !== seq) return; // 최신 응답만
if (!got.total && fallback) {
const next = [...(q.includes(" ") ? [q.split(/\s+/)[0]] : []), ...(opt.alt ?? [])].find(
(w) => w && !tried.has(w),
);
if (next) {
tried.add(next);
search.value = next;
return load(true);
}
}
const cols = COLS[opt.kind];
const body = got.rows.map((row) => {
const flat = flatten(row);
const tr = el("tr", {
className: `m01lab__pickrow${opt.isCurrent(row) ? " is-picked" : ""}`,
children: cols.map((c) =>
el("td", { text: show(flat[c]), attrs: { title: show(flat[c]) } }),
),
});
tr.addEventListener("click", () => {
close();
opt.onPick(row);
});
return tr;
});
host.replaceChildren(
el("div", {
className: "ui-table-wrap m01-master__grid-wrap",
children: [
el("table", {
className: "ui-table m01-master__grid",
children: [
el("thead", {
children: [el("tr", { children: cols.map((c) => el("th", { text: c })) })],
}),
el("tbody", { children: body }),
],
}),
],
}),
buildPager(got.total, size, got.page, (next) => {
page = next;
void load();
}),
);
fit.ask();
host.querySelector(".is-picked")?.scrollIntoView({ block: "nearest" });
} catch (error) {
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
}
};
const fit = watchFit(
host,
() => host.querySelector<HTMLElement>(".m01-master__grid-wrap"),
(rows) => {
if (rows === size) return;
page = pageKeepFirst(page, size, rows);
size = rows;
void load();
},
{ cap: room, once: true },
);
let timer = 0;
search.addEventListener("input", () => {
window.clearTimeout(timer);
timer = window.setTimeout(() => {
page = 1;
void load();
}, 300);
});
void load(true);
}