feat(b08): 라이브러리 칸 [STmate 엑셀 읽기] → 호표 고르기 → [내 라이브러리에 넣기] — 읽은 호표 수·구성 줄 수를 보이고 넣기 전에 다시 물음(덮어씀·원문 시점 수량·단가 안 가져옴) · 넣기는 파일을 다시 보냄
배정 프로젝트 화면: 돌쌓기(찰) 장에서 봉화 엑셀 읽기 → 「호표 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
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import { API_BASE_URL } from "@config/config_frontend";
|
import { API_BASE_URL } from "@config/config_frontend";
|
||||||
|
import { buildStmatePanel } from "./B08_Quantity_UI_StructureSheet_Stmate";
|
||||||
|
|
||||||
const TIER_LABELS: Record<string, string> = { personal: "개인", company: "회사", program: "기본" };
|
const TIER_LABELS: Record<string, string> = { personal: "개인", company: "회사", program: "기본" };
|
||||||
/** 항목 종류 배지 — 양식형 = 제원을 바꾸면 수량이 다시 남 · 고정형 = 박힌 수량(명세 13장). */
|
/** 항목 종류 배지 — 양식형 = 제원을 바꾸면 수량이 다시 남 · 고정형 = 박힌 수량(명세 13장). */
|
||||||
@@ -191,5 +192,15 @@ export function buildLibraryPanel(options: LibraryPanelOptions): HTMLElement {
|
|||||||
mine.append(save, remove);
|
mine.append(save, remove);
|
||||||
|
|
||||||
panel.append(title, scope, load, list, take, mine, status);
|
panel.append(title, scope, load, list, take, mine, status);
|
||||||
|
// 고정형 항목을 만드는 둘째 길 — STmate 출력 엑셀에서 호표 하나를 뽑아 개인 단에(PLAN 4장).
|
||||||
|
panel.append(
|
||||||
|
buildStmatePanel({
|
||||||
|
projectId,
|
||||||
|
typeId,
|
||||||
|
onSaved: () => {
|
||||||
|
if (!list.hidden) load.click();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"""라이브러리 칸의 [STmate 엑셀에서 뽑기] — 읽은 수를 보이고 묻고 넣는 두 걸음(2026-09-14 브레인 판정).
|
||||||
|
|
||||||
|
⚠ 읽기 전에 넣기가 열리면 안 됨 · 넣기 전에 호표 수·구성 줄 수·덮어씀을 묻는 확인이 있어야 함 ·
|
||||||
|
넣기는 **파일을 다시 보냄**(서버가 다시 읽음 — 화면이 읽은 줄을 보내지 않음).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
UI = ROOT / "B08_Quantity" / "B08_Quantity_UI_StructureSheet_Stmate.ts"
|
||||||
|
PANEL = (ROOT / "B08_Quantity" / "B08_Quantity_UI_StructureSheet_Library.ts").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_라이브러리_칸에_뽑기_칸이_붙는다() -> None:
|
||||||
|
assert "buildStmatePanel(" in PANEL
|
||||||
|
|
||||||
|
|
||||||
|
def test_읽고_수를_보이고_묻고_파일을_다시_보내_넣는다() -> None:
|
||||||
|
ui = UI.read_text(encoding="utf-8")
|
||||||
|
assert "/stmate/read" in ui and "/stmate/save" in ui
|
||||||
|
assert 'form.append("file"' in ui and 'form.append("hopyo_no"' in ui
|
||||||
|
assert "window.confirm(" in ui and "counts.hopyo" in ui and "counts.rows" in ui
|
||||||
|
assert "put.hidden = true" in ui # 읽기 전에는 넣기 단추가 숨음
|
||||||
|
assert "rows:" not in ui.split("/stmate/save")[1][:400] # 줄을 보내지 않음
|
||||||
Reference in New Issue
Block a user