배정 프로젝트 화면: 돌쌓기(찰) 장에서 봉화 엑셀 읽기 → 「호표 45개 · 구성 줄 209줄」 → 제6호표 기슭막이(깬잡석,찰쌓기) H=2.0m 구성 9줄 넣기 → 목록 「개인 · [고정형] …」 · [내 것 지우기]로 되돌림(목록 기본 한 줄) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
145 lines
5.6 KiB
TypeScript
145 lines
5.6 KiB
TypeScript
/* =============================================================================
|
|
* B08_Quantity_UI_StructureSheet_Stmate.ts
|
|
* 라이브러리 칸 — [STmate 엑셀에서 뽑기] (PLAN 4장 · 2026-09-14 브레인 판정).
|
|
*
|
|
* 두 걸음: [읽기] → 호표 수·구성 줄 수·못 읽은 사유를 보임(아무것도 안 씀) → 호표를 고르고
|
|
* [내 라이브러리에 넣기] → 수를 다시 묻고 **파일을 다시 보냄**(서버가 다시 읽어 씀 — 화면은 줄을 안 보냄).
|
|
* ⚠ 종류는 지금 보고 있는 장의 종류 — 이름으로 자동으로 안 붙임(판정 Ⓒ).
|
|
* ⚠ 모양은 옆 제원 칸(`b08-spec*`) 클래스를 그대로 씀.
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
|
|
interface ReadHopyo {
|
|
no: number;
|
|
name: string;
|
|
spec: string;
|
|
unit: string;
|
|
rows: number;
|
|
percent_rows: number;
|
|
contract_rows: number;
|
|
}
|
|
|
|
interface ReadResult {
|
|
project: string;
|
|
hopyo: ReadHopyo[];
|
|
counts: { hopyo: number; rows: number };
|
|
problems: string[];
|
|
}
|
|
|
|
async function postForm<T>(url: string, form: FormData): Promise<T> {
|
|
const response = await fetch(url, { method: "POST", credentials: "include", body: form });
|
|
const payload = (await response.json().catch(() => ({}))) as T & { message?: string };
|
|
if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`);
|
|
return payload;
|
|
}
|
|
|
|
export function buildStmatePanel(options: {
|
|
projectId: string;
|
|
typeId: string;
|
|
/** 넣은 뒤 — 목록을 다시 받게 함. */
|
|
onSaved: () => void;
|
|
}): HTMLElement {
|
|
const { projectId, typeId, onSaved } = options;
|
|
const base = `${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/quantity/structure-sheets/library`;
|
|
const box = document.createElement("div");
|
|
box.className = "b08-sheet__actions";
|
|
const input = document.createElement("input");
|
|
input.type = "file";
|
|
input.accept = ".xlsx";
|
|
input.className = "b08-spec__input";
|
|
input.title = "STmate 가 내보낸 내역 엑셀(일위대가표 시트)";
|
|
const read = document.createElement("button");
|
|
read.type = "button";
|
|
read.className = "b08-quantity__tab";
|
|
read.textContent = "STmate 엑셀 읽기";
|
|
const list = document.createElement("select");
|
|
list.className = "b08-spec__input";
|
|
list.hidden = true;
|
|
const put = document.createElement("button");
|
|
put.type = "button";
|
|
put.className = "b08-spec__save";
|
|
put.textContent = "내 라이브러리에 넣기";
|
|
put.hidden = true;
|
|
const status = document.createElement("p");
|
|
status.className = "b08-spec__scope";
|
|
|
|
let result: ReadResult | null = null;
|
|
input.addEventListener("change", () => {
|
|
result = null;
|
|
list.hidden = put.hidden = true;
|
|
status.textContent = "";
|
|
});
|
|
|
|
read.addEventListener("click", () => {
|
|
const file = input.files?.[0];
|
|
if (!file) {
|
|
status.textContent = "엑셀 파일을 먼저 고르세요";
|
|
return;
|
|
}
|
|
void (async () => {
|
|
read.disabled = true;
|
|
status.textContent = "읽는 중…";
|
|
try {
|
|
const form = new FormData();
|
|
form.append("file", file);
|
|
result = await postForm<ReadResult>(`${base}/stmate/read`, form);
|
|
list.replaceChildren(
|
|
...result.hopyo.map((h) => {
|
|
const option = document.createElement("option");
|
|
option.value = String(h.no);
|
|
const extra = h.percent_rows ? ` · 가산 행 ${h.percent_rows}` : "";
|
|
option.textContent = `제${h.no}호표 ${h.name} ${h.spec} / ${h.unit} · 구성 ${h.rows}줄${extra}`;
|
|
return option;
|
|
}),
|
|
);
|
|
list.hidden = put.hidden = result.hopyo.length === 0;
|
|
const missed = result.problems.length
|
|
? ` · 못 읽은 것 ${result.problems.length}건 — ${result.problems.slice(0, 3).join(" / ")}`
|
|
: "";
|
|
status.textContent = `「${result.project}」 호표 ${result.counts.hopyo}개 · 구성 줄 ${result.counts.rows}줄 읽음${missed}`;
|
|
} catch (error) {
|
|
status.textContent = error instanceof Error ? error.message : "읽지 못함";
|
|
} finally {
|
|
read.disabled = false;
|
|
}
|
|
})();
|
|
});
|
|
|
|
put.addEventListener("click", () => {
|
|
const file = input.files?.[0];
|
|
const picked = result?.hopyo.find((h) => String(h.no) === list.value);
|
|
if (!file || !result || !picked) return;
|
|
const question =
|
|
`읽은 것: 호표 ${result.counts.hopyo}개 · 구성 줄 ${result.counts.rows}줄\n` +
|
|
`넣을 것: 「${picked.name} ${picked.spec}」 구성 ${picked.rows}줄 → 이 장 종류로 내 라이브러리\n` +
|
|
"· 같은 종류 내 것이 있으면 덮어씀\n· 수량은 원문 시점값(현행 품셈 대조 전) · 단가는 안 가져옴";
|
|
if (!window.confirm(question)) return;
|
|
void (async () => {
|
|
put.disabled = true;
|
|
status.textContent = "넣는 중…";
|
|
try {
|
|
const form = new FormData();
|
|
form.append("file", file);
|
|
form.append("hopyo_no", String(picked.no));
|
|
form.append("type_id", typeId);
|
|
const saved = await postForm<{ code: string; name: string; note: string }>(
|
|
`${base}/stmate/save`,
|
|
form,
|
|
);
|
|
status.textContent = `내 라이브러리에 넣음 — 「${saved.name}」 [고정형] · ${saved.note}`;
|
|
onSaved();
|
|
} catch (error) {
|
|
status.textContent = error instanceof Error ? error.message : "넣지 못함";
|
|
} finally {
|
|
put.disabled = false;
|
|
}
|
|
})();
|
|
});
|
|
|
|
const wrap = document.createElement("div");
|
|
box.append(read, put);
|
|
wrap.append(input, box, list, status);
|
|
return wrap;
|
|
}
|