feat(일위대가실무): 324-4 화면 — 좌측 「일위대가 실무자료」 판 · 본 화면(호표 표 · 줄 맞춤 · 확정 · 빠진 줄 · 값 비교) · [로직 추가] 「실무 호표」 줄
- 좌측 판(Hopyo_Side): 일위대가 사전조합 아래 · 구조물 조합 층 · 쓰기 권한 없으면 [엑셀 올리기] 꺼짐 · 숨은 file 입력 .xls,.xlsx · 파일 줄 → 본 화면
- 본 화면(Hopyo_View): 파일 · 시트 · 호표 수 · 검산 · 맞춤 n/m · 호표 표 + 찾기 · [지우기] / 호표 줄 표(갈래 · 품명 · 규격 · 수량 · 단위 · 엑셀 단가 · 맞춤 · 대상 · 입력값 · 환산) · [고르기](후보 5 + 다른 로직 · 자재 찾기 → 미리 채움) · [확정] · [빼기] · [되살림] · [같음 모두 확정] · 빠진 줄 · [사전조합 만들기] → 값 비교 · 다시 씀이면 [새로 만들기](확인 뒤 덮음)
- [로직 추가](Hopyo_Pick · Labor_Pick 늦은 덧줄 `more` · 종류 고르기): 문서 층 · 위 층 호표 줄(맞춤 n/m) → 고르면 서버가 문서 층 사전조합을 만들고 참조 줄(별칭 = 호표 품명) · 빠진 줄 알림 · 프로젝트 층 없음
- Labor_Detail: 제자리 넷 줄(가져오기 · pick 별칭 받음 · withHopyo) · 693 줄 그대로
- 서버: GET /layers/{층}/hopyo-picks(이 층 · 위 층 · 이 층 기억)
- 색 = 공용 토큰(밝음 · 어두움) · 시험 test_m02_hopyo_screen 4 + hopyo-picks 1 · typecheck 0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_Side.ts
|
||||
* 좌측 「일위대가 실무자료」 컨테이너(PLAN 324-4 · 일위대가 사전조합 아래) — 층 = 구조물 조합 층(화면 층) ·
|
||||
* [엑셀 올리기](숨은 file 입력 .xls · .xlsx · 쓰기 권한 없으면 꺼짐) + 파일 줄 → 누르면 본 화면
|
||||
* (`M02_MasterTemplete_Hopyo_View`) · 고름 표시 = 본 화면이 메인 칸에 붙어 있는 동안만.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { listHopyo, pageScope, uploadHopyo, type HopyoFile } from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { buildHopyoView } from "./M02_MasterTemplete_Hopyo_View";
|
||||
|
||||
export const HOPYO_TITLE = "일위대가 실무자료";
|
||||
|
||||
export interface HopyoSectionOptions {
|
||||
/** 이 층 쓰기 권한 — 없으면 올리기 꺼짐 · 본 화면 확정 · 만들기 없음 */
|
||||
canWrite: boolean;
|
||||
/** 저장 안 한 고침을 버릴지 확인 — 취소면 false */
|
||||
confirmLeave: () => Promise<boolean>;
|
||||
/** 메인 칸에 화면을 띄움 */
|
||||
show: (label: string, node: HTMLElement) => (dirty: boolean) => void;
|
||||
/** 화면을 열었음 — 좌측 다른 고름 표시를 지움 */
|
||||
onOpen: () => void;
|
||||
}
|
||||
|
||||
export interface HopyoSection {
|
||||
title: string;
|
||||
body: HTMLElement;
|
||||
/** 다른 화면이 열렸으면 고름 표시를 지움 */
|
||||
paint: () => void;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
||||
|
||||
export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
const scope = pageScope();
|
||||
const list = el("div", { className: "m02-side__items ui-sidebar-list" });
|
||||
const input = el("input", {
|
||||
attrs: { type: "file", accept: ".xls,.xlsx", "data-hopyo-file": "" },
|
||||
}) as HTMLInputElement;
|
||||
input.hidden = true;
|
||||
const upload = createButton({
|
||||
label: "엑셀 올리기",
|
||||
variant: "filled",
|
||||
size: "sm",
|
||||
disabled: !opt.canWrite,
|
||||
onClick: () => input.click(),
|
||||
});
|
||||
upload.title = opt.canWrite ? "설계내역서 엑셀(일위대가 시트)" : "이 층 쓰기 권한 없음";
|
||||
const body = el("div", {
|
||||
className: "m02-hopyo-side",
|
||||
children: [el("div", { className: "m02-side__row", children: [upload, input] }), list],
|
||||
});
|
||||
let files: HopyoFile[] = [];
|
||||
let open: { key: string; view: HTMLElement } | null = null;
|
||||
let seq = 0;
|
||||
|
||||
input.addEventListener("change", () => {
|
||||
const file = input.files?.[0];
|
||||
input.value = "";
|
||||
if (!file) return;
|
||||
upload.disabled = true;
|
||||
uploadHopyo(scope, file)
|
||||
.then(async (got) => {
|
||||
const wrong = got.어긋남 ? ` · 검산 어긋남 ${got.어긋남}` : "";
|
||||
showToast(`올림 — 호표 ${got.호표수}${wrong}`, got.어긋남 ? "warning" : "success");
|
||||
await refresh();
|
||||
await show(got);
|
||||
})
|
||||
.catch((error) => showToast(why(error), "error"))
|
||||
.finally(() => (upload.disabled = !opt.canWrite));
|
||||
});
|
||||
|
||||
function paintList(): void {
|
||||
list.replaceChildren(
|
||||
...(files.length
|
||||
? files.map((f) => {
|
||||
const row = el("button", {
|
||||
className: `m02-side__item ui-sidebar-row${open?.key === f.키 ? " is-active" : ""}`,
|
||||
text: `${f.파일} · 호표 ${f.호표수}`,
|
||||
attrs: { type: "button", title: `${f.키} · ${f.올린때}`, "data-hopyo-key": f.키 },
|
||||
});
|
||||
row.addEventListener("click", () => void show(f));
|
||||
return row;
|
||||
})
|
||||
: [el("p", { className: "m02-side__empty", text: "올린 실무자료 없음" })]),
|
||||
);
|
||||
}
|
||||
|
||||
async function show(file: HopyoFile): Promise<void> {
|
||||
if (open?.key === file.키 && open.view.isConnected) return;
|
||||
if (!(await opt.confirmLeave())) return;
|
||||
const view = buildHopyoView(file.키, {
|
||||
scope,
|
||||
canWrite: opt.canWrite,
|
||||
onDeleted: () => {
|
||||
view.replaceChildren(el("p", { className: "m02-side__empty", text: "지운 실무자료" }));
|
||||
void refresh();
|
||||
},
|
||||
});
|
||||
opt.show(`${HOPYO_TITLE} — ${file.파일}`, view);
|
||||
opt.onOpen();
|
||||
open = { key: file.키, view };
|
||||
paintList();
|
||||
}
|
||||
|
||||
async function refresh(): Promise<void> {
|
||||
const mine = ++seq;
|
||||
try {
|
||||
const got = await listHopyo(scope);
|
||||
if (mine === seq) files = got;
|
||||
} catch (error) {
|
||||
if (mine === seq) files = [];
|
||||
showToast(why(error), "error");
|
||||
}
|
||||
if (mine === seq) paintList();
|
||||
}
|
||||
|
||||
const paint = (): void => {
|
||||
if (open && !open.view.isConnected) {
|
||||
open = null;
|
||||
paintList();
|
||||
}
|
||||
};
|
||||
|
||||
void refresh();
|
||||
return { title: HOPYO_TITLE, body, paint, refresh };
|
||||
}
|
||||
Reference in New Issue
Block a user