feat(일위대가실무): 329-3 엑셀 보기 창 — 읽기 전용 시트(일위대가 · 단가산출 탭) + 호표 목록(체크 · 찾기 · 맞춤) · 목록 누름 → 영역 강조 · 칸 누름 → 그 호표 · 고른 호표 한 번에 사전조합 + 결과 표 · 여는 자리 넷(올린 직후 · 엑셀로 보기 · 엑셀에서 보기 · 로직 추가 덧줄) · 늦게 불러옴
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
@@ -149,3 +149,81 @@
|
||||
gap: var(--spacing-4);
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
/* 엑셀 보기 창(329) — 왼쪽 읽기 전용 시트 · 오른쪽 호표 목록 · 불러오기 결과 */
|
||||
.m02-hopyo-sheet__dialog > .ui-modal__body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.m02-hopyo-sheet {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 22rem;
|
||||
gap: var(--spacing-8);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.m02-hopyo-sheet__grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.m02-hopyo-sheet__side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.m02-hopyo-sheet__list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.m02-hopyo-sheet__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border-left: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.m02-hopyo-sheet__item:hover {
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.m02-hopyo-sheet__item.is-current {
|
||||
border-left-color: var(--color-accent);
|
||||
background: var(--color-mist-violet);
|
||||
}
|
||||
|
||||
.m02-hopyo-sheet__no {
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.m02-hopyo-sheet__name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.m02-hopyo-sheet__result {
|
||||
max-height: 40%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.m02-hopyo-sheet__result:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet";
|
||||
import { call, getActiveLayer, getActiveOwner, type Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import type { LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
||||
|
||||
@@ -34,6 +35,22 @@ export interface HopyoBrief {
|
||||
금액: Money;
|
||||
/** [들어감 수, 줄 수] */
|
||||
맞춤: [number, number];
|
||||
/** 일위대가 시트 행 [시작, 끝](엑셀 1부터 · 329 · 옛 파일 없음) */
|
||||
행?: [number, number];
|
||||
}
|
||||
|
||||
/** 엑셀 보기 창 목록 한 줄 — 호표 · 단가산출 블록 행 범위 */
|
||||
export interface HopyoSpan {
|
||||
번: number;
|
||||
이름: string;
|
||||
행: [number, number];
|
||||
}
|
||||
|
||||
/** 엑셀 보기 창 격자(329) — 시트 「일위대가」 · 「단가산출」 */
|
||||
export interface HopyoGrid {
|
||||
책: Workbook;
|
||||
호표: HopyoSpan[];
|
||||
단산: HopyoSpan[];
|
||||
}
|
||||
|
||||
export interface HopyoView extends HopyoFile {
|
||||
@@ -136,6 +153,10 @@ export const listHopyo = async (scope: HopyoScope): Promise<HopyoFile[]> =>
|
||||
export const readHopyo = (scope: HopyoScope, key: string): Promise<HopyoView> =>
|
||||
call(at(scope, `/hopyo/${enc(key)}`));
|
||||
|
||||
/** 격자 — 없으면 404 「격자 없음 — 다시 올리면 보임」(옛 파일) */
|
||||
export const readHopyoGrid = (scope: HopyoScope, key: string): Promise<HopyoGrid> =>
|
||||
call(at(scope, `/hopyo-grid/${enc(key)}`));
|
||||
|
||||
export const readHopyoOne = (scope: HopyoScope, key: string, no: number): Promise<HopyoOne> =>
|
||||
call(at(scope, `/hopyo/${enc(key)}/${no}`));
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* M02_MasterTemplete_Hopyo_Pick.ts
|
||||
* [로직 추가] 창의 「실무 호표」 줄(PLAN 324-4) — 일위대가 문서 층 · 위 층 실무자료의 호표마다 한 줄(맞춤 n/m) ·
|
||||
* 고르면 서버가 그 호표를 문서 층 사전조합으로 만들고(`/hopyo-build` · 있던 것은 다시 씀) `lib.pick` 으로
|
||||
* 참조 줄(별칭 = 호표 품명) · 빠진 줄이 있으면 알림. 프로젝트 층엔 없음.
|
||||
* 참조 줄(별칭 = 호표 품명) · 빠진 줄이 있으면 알림 · 파일마다 「엑셀로 보기 · 여러 호표」 줄(329 엑셀 보기 창 ·
|
||||
* 고른 호표마다 참조 줄). 프로젝트 층엔 없음.
|
||||
* ========================================================================== */
|
||||
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
@@ -25,7 +26,42 @@ export function withHopyo(lib: LibraryPick): LibraryPick {
|
||||
{ value: "precombo", text: tc("Kind_Combo") },
|
||||
{ value: HOPYO_KIND, text: "실무 호표" },
|
||||
],
|
||||
more: async () => (await listHopyoPicks(scope)).map((row) => hopyoExtra(row, lib)),
|
||||
more: async () => {
|
||||
const rows = await listHopyoPicks(scope);
|
||||
const files = new Map<string, HopyoPickRow[]>();
|
||||
for (const row of rows) {
|
||||
const key = `${row.층}|${row.주인 ?? ""}|${row.키}`;
|
||||
files.set(key, [...(files.get(key) ?? []), row]);
|
||||
}
|
||||
return [
|
||||
...rows.map((row) => hopyoExtra(row, lib)),
|
||||
...[...files].map(([key, list]) => sheetExtra(key, list[0], list.length, lib)),
|
||||
];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 파일 하나 — 엑셀 보기 창에서 여러 호표를 골라 한 번에(만드는 층 = 문서 층) */
|
||||
export function sheetExtra(key: string, row: HopyoPickRow, n: number, lib: LibraryPick): PickExtra {
|
||||
return {
|
||||
kind: HOPYO_KIND,
|
||||
key: `${key}|엑셀`,
|
||||
tag: "실무 호표",
|
||||
name: "엑셀로 보기 · 여러 호표",
|
||||
sub: `${row.파일} · ${LAYER_TEXT[row.층] ?? row.층}`,
|
||||
unit: `호표 ${n}`,
|
||||
onPick: () =>
|
||||
void import("./M02_MasterTemplete_Hopyo_Sheet").then((m) =>
|
||||
m.openHopyoSheet({
|
||||
scope: { layer: row.층, owner: row.주인 },
|
||||
key: row.키,
|
||||
파일: row.파일,
|
||||
canWrite: true,
|
||||
target: { layer: lib.layer, owner: lib.owner },
|
||||
onImport: (got, h) =>
|
||||
lib.pick(got.참조, got.이름, h.이름.slice(0, ALIAS_MAX).trim() || null),
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_Sheet.ts
|
||||
* 일위대가 실무자료 엑셀 보기 창(PLAN 329) — 큰 모달 · 왼쪽 = 올린 엑셀을 연 것처럼 읽기 전용 시트(A00 산출근거
|
||||
* 스프레드시트 · 탭 일위대가 · 단가산출) · 오른쪽 = 호표 목록(체크 · 찾기 · 맞춤 n/m) + [고른 n개 불러오기].
|
||||
* · 목록 누름 → 시트에서 그 호표 영역(B열 ~ 끝 열) 고름 · 칸 누름 → 그 행 호표를 목록에서 강조 · 영역 고름
|
||||
* · 불러오기 = 고른 호표마다 차례로 사전조합 만들기(`/hopyo-build`) → 결과 표(사전조합 키 · 빠진 줄 · 값 차이) ·
|
||||
* `onImport` 가 있으면 호표마다 알림([로직 추가] 참조 줄)
|
||||
* · 격자 = 서버 `/hopyo-grid/{키}` · 옛 파일(격자 없음)은 시트 자리에 까닭 글 · 목록 · 불러오기는 됨
|
||||
* ⚠ 부르는 쪽은 `import()` 로 엶 · 스프레드시트도 열 때 불러옴(다른 화면 번들에 안 섞이게).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import type { Sheet } from "../A00_Common/spreadsheet/spreadsheet_types";
|
||||
import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet";
|
||||
import {
|
||||
buildHopyo,
|
||||
readHopyo,
|
||||
readHopyoGrid,
|
||||
type HopyoBrief,
|
||||
type HopyoBuilt,
|
||||
type HopyoGrid,
|
||||
type HopyoScope,
|
||||
type HopyoSpan,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { builtWord } from "./M02_MasterTemplete_Hopyo_View";
|
||||
import "./M02_MasterTemplete_Hopyo.css";
|
||||
|
||||
export interface HopyoSheetOptions {
|
||||
/** 실무자료 파일 층 */
|
||||
scope: HopyoScope;
|
||||
key: string;
|
||||
파일: string;
|
||||
/** 만들 층 쓰기 권한 — 없으면 체크 · 불러오기 없음(보기만) */
|
||||
canWrite: boolean;
|
||||
/** 사전조합을 만들 층(없으면 `scope` · [로직 추가] = 문서 층) */
|
||||
target?: HopyoScope;
|
||||
/** 처음 고를 호표 번(호표 화면 [엑셀에서 보기]) */
|
||||
호표?: number;
|
||||
/** 처음 보일 단가산출 행(330-3 근거 [엑셀에서 보기]) */
|
||||
단산행?: number | [number, number];
|
||||
/** 호표 하나를 만들었음 — [로직 추가] 참조 줄 */
|
||||
onImport?: (got: HopyoBuilt, hopyo: HopyoBrief) => void;
|
||||
}
|
||||
|
||||
const ILWI = "일위대가";
|
||||
const DANSAN = "단가산출";
|
||||
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
||||
const won = (v: number | null | undefined): string =>
|
||||
v === null || v === undefined ? "" : v.toLocaleString("ko-KR");
|
||||
const note = (text: string): HTMLElement => el("span", { className: "m02-hopyo__note", text });
|
||||
|
||||
/** 엑셀 행(1부터)이 든 호표 번 · 없으면 null */
|
||||
export const hopyoAtRow = (list: HopyoSpan[], row: number): number | null =>
|
||||
list.find((h) => h.행[0] <= row && row <= h.행[1])?.번 ?? null;
|
||||
|
||||
/** 호표 영역 A1 — B열 ~ 시트 끝 열(북부청 = `B20:O24`) */
|
||||
export const spanText = (행: [number, number], last: string): string => `B${행[0]}:${last}${행[1]}`;
|
||||
|
||||
/** 시트 끝 열 글자 — 칸 · 열 정보 중 가장 오른쪽 */
|
||||
export function lastColumn(sheet: Pick<Sheet, "칸" | "열">): string {
|
||||
let best = "A";
|
||||
for (const k of [...Object.keys(sheet.칸), ...Object.keys(sheet.열 ?? {})]) {
|
||||
const c = /^[A-Z]+/.exec(k)?.[0];
|
||||
if (c && (c.length > best.length || (c.length === best.length && c > best))) best = c;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/** 단가산출 행 → 행 전체 범위 A1(`4:9`) */
|
||||
export const rowsText = (행: number | [number, number]): string =>
|
||||
typeof 행 === "number" ? `${행}:${행}` : `${행[0]}:${행[1]}`;
|
||||
|
||||
export function openHopyoSheet(opt: HopyoSheetOptions): void {
|
||||
let sheet: SpreadsheetHandle | null = null;
|
||||
let closed = false;
|
||||
let briefs: HopyoBrief[] = [];
|
||||
let spans: HopyoSpan[] = [];
|
||||
let ilwi = ""; // 일위대가 시트 id
|
||||
let last = "O";
|
||||
let at = { 시트: "", 칸: "", 범위: "" }; // 시트 고름
|
||||
let current: number | null = opt.호표 ?? null;
|
||||
const checked = new Set<number>();
|
||||
const items = new Map<number, HTMLElement>();
|
||||
|
||||
const left = el("div", {
|
||||
className: "m02-hopyo-sheet__grid",
|
||||
children: [note("엑셀을 읽는 중…")],
|
||||
});
|
||||
const search = el("input", {
|
||||
className: "ui-input ui-input--sm",
|
||||
attrs: { type: "search", placeholder: "호표 · 품명 · 규격 찾기" },
|
||||
}) as HTMLInputElement;
|
||||
const list = el("div", { className: "m02-hopyo-sheet__list" });
|
||||
const count = note("");
|
||||
const go = createButton({ label: "고른 0개 불러오기", variant: "filled", size: "sm" });
|
||||
go.disabled = true;
|
||||
const result = el("div", { className: "m02-hopyo-sheet__result" });
|
||||
const right = el("div", {
|
||||
className: "m02-hopyo-sheet__side",
|
||||
children: [
|
||||
search,
|
||||
el("div", { className: "m02-hopyo__bar", children: [count, ...(opt.canWrite ? [go] : [])] }),
|
||||
list,
|
||||
result,
|
||||
],
|
||||
});
|
||||
|
||||
openModal({
|
||||
title: `엑셀 보기 — ${opt.파일}`,
|
||||
closeLabel: "닫기",
|
||||
wide: true,
|
||||
dialogClass: "m02-modal m02-hopyo-sheet__dialog",
|
||||
beforeClose: async () => {
|
||||
closed = true;
|
||||
sheet?.destroy();
|
||||
return true;
|
||||
},
|
||||
mount: (body) =>
|
||||
body.append(el("div", { className: "m02-hopyo-sheet", children: [left, right] })),
|
||||
});
|
||||
|
||||
const paintGo = (): void => {
|
||||
go.querySelector(".ui-btn__label")!.textContent = `고른 ${checked.size}개 불러오기`;
|
||||
go.disabled = !checked.size;
|
||||
};
|
||||
|
||||
/** 목록 강조 · 보이게(시트 고름은 그대로) */
|
||||
function mark(no: number | null): void {
|
||||
items.get(current ?? NaN)?.classList.remove("is-current");
|
||||
current = no;
|
||||
const row = items.get(no ?? NaN);
|
||||
row?.classList.add("is-current");
|
||||
row?.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
|
||||
/** 호표 하나 강조 — 목록 · 시트 영역(같은 범위면 다시 안 부름) */
|
||||
function focus(no: number): void {
|
||||
mark(no);
|
||||
const span = spans.find((h) => h.번 === no)?.행 ?? briefs.find((h) => h.번 === no)?.행;
|
||||
if (!sheet || !span) return;
|
||||
const text = spanText(span, last);
|
||||
if (at.시트 !== ilwi || at.범위 !== text) sheet.select(text, ilwi);
|
||||
}
|
||||
|
||||
function paintList(): void {
|
||||
const q = search.value.trim().toLowerCase();
|
||||
items.clear();
|
||||
const shown = briefs.filter(
|
||||
(h) => !q || `제${h.번}호표 ${h.이름} ${h.규격}`.toLowerCase().includes(q),
|
||||
);
|
||||
list.replaceChildren(
|
||||
...shown.map((h) => {
|
||||
const box = el("input", { attrs: { type: "checkbox" } }) as HTMLInputElement;
|
||||
box.checked = checked.has(h.번);
|
||||
box.hidden = !opt.canWrite;
|
||||
box.addEventListener("click", (e) => e.stopPropagation());
|
||||
box.addEventListener("change", () => {
|
||||
if (box.checked) checked.add(h.번);
|
||||
else checked.delete(h.번);
|
||||
paintGo();
|
||||
});
|
||||
const row = el("div", {
|
||||
className: `m02-hopyo-sheet__item${h.번 === current ? " is-current" : ""}`,
|
||||
attrs: { "data-hopyo-no": String(h.번), title: `${h.이름} ${h.규격}`.trim() },
|
||||
children: [
|
||||
box,
|
||||
el("span", { className: "m02-hopyo-sheet__no", text: `제${h.번}호표` }),
|
||||
el("span", { className: "m02-hopyo-sheet__name", text: `${h.이름} ${h.규격}`.trim() }),
|
||||
note(`${h.맞춤[0]}/${h.맞춤[1]}`),
|
||||
],
|
||||
});
|
||||
row.addEventListener("click", () => focus(h.번));
|
||||
items.set(h.번, row);
|
||||
return row;
|
||||
}),
|
||||
);
|
||||
count.textContent = q ? `${shown.length} / ${briefs.length}` : `호표 ${briefs.length}`;
|
||||
}
|
||||
search.addEventListener("input", paintList);
|
||||
|
||||
async function mountSheet(grid: HopyoGrid): Promise<void> {
|
||||
const { createSpreadsheet } = await import("../A00_Common/spreadsheet/spreadsheet");
|
||||
if (closed) return;
|
||||
const book = structuredClone(grid.책);
|
||||
const first = book.시트.find((s) => s.이름 === ILWI) ?? book.시트[0];
|
||||
if (!first) return void left.replaceChildren(note("시트 없음"));
|
||||
ilwi = first.id;
|
||||
last = lastColumn(first);
|
||||
book.활성 = first.id;
|
||||
left.replaceChildren();
|
||||
sheet = createSpreadsheet(left, book, {
|
||||
readOnly: true,
|
||||
onSelect: (s) => {
|
||||
at = s;
|
||||
if (s.시트 !== ilwi) return;
|
||||
const no = hopyoAtRow(spans, Number(/\d+/.exec(s.칸)?.[0]));
|
||||
if (no !== null && no !== current) mark(no);
|
||||
},
|
||||
});
|
||||
// 칸을 눌러 뗌 → 그 호표 영역 고름(끌기 · 방향키 도중엔 안 바꿈)
|
||||
left.querySelector(".ss-grid-host")?.addEventListener("mouseup", (e) => {
|
||||
if ((e as MouseEvent).button !== 0 || at.시트 !== ilwi) return;
|
||||
const no = hopyoAtRow(spans, Number(/\d+/.exec(at.칸)?.[0]));
|
||||
if (no !== null) focus(no);
|
||||
});
|
||||
}
|
||||
|
||||
async function load(): Promise<void> {
|
||||
try {
|
||||
const [file, grid] = await Promise.all([
|
||||
readHopyo(opt.scope, opt.key),
|
||||
readHopyoGrid(opt.scope, opt.key).catch((error: unknown) => new Error(why(error))),
|
||||
]);
|
||||
if (closed) return;
|
||||
briefs = file.호표;
|
||||
spans = grid instanceof Error ? [] : grid.호표;
|
||||
paintList();
|
||||
if (grid instanceof Error) return void left.replaceChildren(note(grid.message));
|
||||
await mountSheet(grid);
|
||||
if (opt.단산행 !== undefined) sheet?.select(rowsText(opt.단산행), DANSAN);
|
||||
else if (current !== null) focus(current);
|
||||
} catch (error) {
|
||||
if (!closed) left.replaceChildren(note(`못 읽음 — ${why(error)}`));
|
||||
}
|
||||
}
|
||||
|
||||
async function importPicked(): Promise<void> {
|
||||
const nos = [...checked].sort((a, b) => a - b);
|
||||
const target = opt.target ?? opt.scope;
|
||||
const rows: HTMLElement[] = [];
|
||||
let ok = 0;
|
||||
go.disabled = true;
|
||||
for (const no of nos) {
|
||||
const h = briefs.find((x) => x.번 === no);
|
||||
if (!h || closed) continue;
|
||||
const head = el("td", { text: `제${no}호표 ${h.이름}`.trim() });
|
||||
try {
|
||||
const from = { 층: opt.scope.layer, 주인: opt.scope.owner, 키: opt.key, 번: no };
|
||||
const got = await buildHopyo(target, from);
|
||||
ok += 1;
|
||||
opt.onImport?.(got, h);
|
||||
rows.push(
|
||||
el("tr", {
|
||||
attrs: { "data-built": got.참조.키 },
|
||||
children: [
|
||||
head,
|
||||
el("td", { text: `${got.참조.키} · ${builtWord(got, false)}` }),
|
||||
el("td", { text: String(got.빠진줄.length), className: "m02-hopyo__num" }),
|
||||
el("td", { text: won(got.비교.차이), className: "m02-hopyo__num" }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
rows.push(el("tr", { children: [head, el("td", { text: `실패 — ${why(error)}` })] }));
|
||||
}
|
||||
result.replaceChildren(
|
||||
el("table", {
|
||||
className: "ui-general-block__table m02-hopyo__table",
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [
|
||||
el("tr", {
|
||||
children: ["호표", "사전조합", "빠진 줄", "값 차이"].map((t) =>
|
||||
el("th", { text: t }),
|
||||
),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
el("tbody", { children: rows }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
paintGo();
|
||||
showToast(`불러옴 ${ok} / ${nos.length}`, ok === nos.length ? "success" : "warning");
|
||||
}
|
||||
go.addEventListener("click", () => void importPicked());
|
||||
|
||||
void load();
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
* M02_MasterTemplete_Hopyo_Side.ts
|
||||
* 좌측 「일위대가 실무자료」 컨테이너(PLAN 324-4 · 일위대가 사전조합 아래) — 층 = 구조물 조합 층(화면 층) ·
|
||||
* [엑셀 올리기](숨은 file 입력 .xls · .xlsx · 쓰기 권한 없으면 꺼짐) + 파일 줄 → 누르면 본 화면
|
||||
* (`M02_MasterTemplete_Hopyo_View`) · 고름 표시 = 본 화면이 메인 칸에 붙어 있는 동안만.
|
||||
* (`M02_MasterTemplete_Hopyo_View`) · 고름 표시 = 본 화면이 메인 칸에 붙어 있는 동안만 ·
|
||||
* 올린 직후 엑셀 보기 창(329 · `M02_MasterTemplete_Hopyo_Sheet` 늦게 불러옴)을 자동으로 엶.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
@@ -66,6 +67,8 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
showToast(`올림 — 호표 ${got.호표수}${wrong}`, got.어긋남 ? "warning" : "success");
|
||||
await refresh();
|
||||
await show(got);
|
||||
const sheet = await import("./M02_MasterTemplete_Hopyo_Sheet");
|
||||
sheet.openHopyoSheet({ scope, key: got.키, 파일: got.파일, canWrite: opt.canWrite });
|
||||
})
|
||||
.catch((error) => showToast(why(error), "error"))
|
||||
.finally(() => (upload.disabled = !opt.canWrite));
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* M02_MasterTemplete_Hopyo_View.ts
|
||||
* 일위대가 실무자료 본 화면(PLAN 324-4) — 파일 하나: 파일 · 시트 · 호표 수 · 검산 · 맞춤 n/m · 호표 표 + 찾기 ·
|
||||
* [지우기] / 호표 하나: 줄 표(갈래 · 품명 · 규격 · 수량 · 단위 · 엑셀 단가 · 맞춤 · 대상 · 입력값 · 환산 ·
|
||||
* [고르기] [확정] [빼기]) · [같음 모두 확정] · 빠진 줄 · [사전조합 만들기] → 값 비교 · 다시 씀이면 [새로 만들기].
|
||||
* [고르기] [확정] [빼기]) · [같음 모두 확정] · 빠진 줄 · [사전조합 만들기] → 값 비교 · 다시 씀이면 [새로 만들기] ·
|
||||
* [엑셀로 보기] · [엑셀에서 보기](329 엑셀 보기 창 · 늦게 불러옴 · 호표 화면은 그 호표 고른 채).
|
||||
* 맞춤 · 만들기는 서버(`M02_Hopyo_Match` · `M02_Hopyo_Build`) · 쓰기 단추는 층 쓰기 권한만.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
type HopyoTarget,
|
||||
type HopyoView,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import type { HopyoSheetOptions } from "./M02_MasterTemplete_Hopyo_Sheet";
|
||||
import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick";
|
||||
import { pickMaterial } from "./M02_Material_Pick";
|
||||
import "./M02_MasterTemplete_Hopyo.css";
|
||||
@@ -52,6 +54,9 @@ const why = (error: unknown): string => (error instanceof Error ? error.message
|
||||
const won = (v: number | null | undefined): string =>
|
||||
v === null || v === undefined ? "" : v.toLocaleString("ko-KR");
|
||||
const fail = (error: unknown): void => showToast(`실패 — ${why(error)}`, "error");
|
||||
/** 엑셀 보기 창(329) — 열 때 불러옴 */
|
||||
const openSheet = (o: HopyoSheetOptions): void =>
|
||||
void import("./M02_MasterTemplete_Hopyo_Sheet").then((m) => m.openHopyoSheet(o), fail);
|
||||
|
||||
const grid = (heads: string[], body: HTMLElement, cls = ""): HTMLElement =>
|
||||
el("div", {
|
||||
@@ -176,6 +181,12 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement
|
||||
note(`호표 ${file.호표수}`),
|
||||
note(wrong.length ? `검산 어긋남 ${wrong.length}` : "검산 맞음"),
|
||||
note(`맞춤 ${file.맞춤[0]} / ${file.맞춤[1]}`),
|
||||
createButton({
|
||||
label: "엑셀로 보기",
|
||||
size: "sm",
|
||||
onClick: () =>
|
||||
openSheet({ scope: opt.scope, key, 파일: file.파일, canWrite: opt.canWrite }),
|
||||
}),
|
||||
...(opt.canWrite ? [dropButton(file)] : []),
|
||||
],
|
||||
}),
|
||||
@@ -240,6 +251,18 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement
|
||||
note(h.단위),
|
||||
note(`엑셀 단가 ${won(h.금액.계)}`),
|
||||
note(`맞춤 ${h.맞춤[0]} / ${h.맞춤[1]}`),
|
||||
createButton({
|
||||
label: "엑셀에서 보기",
|
||||
size: "sm",
|
||||
onClick: () =>
|
||||
openSheet({
|
||||
scope: opt.scope,
|
||||
key,
|
||||
파일: got.파일,
|
||||
canWrite: opt.canWrite,
|
||||
호표: no,
|
||||
}),
|
||||
}),
|
||||
...tools,
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* M02 일위대가 실무자료(324-4) 헬퍼 — `M02_MasterTemplete_Hopyo_Pick.ts`(고르기 창 「실무 호표」 덧줄) ·
|
||||
* `M02_MasterTemplete_Hopyo_View.ts` 글 도우미를 트랜스파일해 Node 로 돌림 · fetch 대역이 부른 길 · 몸을 모음 ·
|
||||
* stdin = {rows, built} · stdout = {calls, kinds, rows, picked, project, view} JSON. */
|
||||
* `M02_MasterTemplete_Hopyo_View.ts` 글 도우미 · `M02_MasterTemplete_Hopyo_Sheet.ts` 엑셀 보기 창 도우미(329 · 행 → 호표)를
|
||||
* 트랜스파일해 Node 로 돌림 · fetch 대역이 부른 길 · 몸을 모음 ·
|
||||
* stdin = {rows, built} · stdout = {calls, kinds, rows, picked, project, view, sheet} JSON. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
@@ -55,6 +56,13 @@ function loadTs(file) {
|
||||
await pick.pickHopyo(input.rows[0], lib);
|
||||
const project = pick.withHopyo({ ...lib, layer: "project" });
|
||||
const view = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_View.ts"));
|
||||
const sheet = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Sheet.ts"));
|
||||
const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Api.ts"));
|
||||
await api.readHopyoGrid({ layer: "company", owner: "7" }, "HP-000001");
|
||||
const spans = [
|
||||
{ 번: 3, 이름: "임도표지석", 행: [20, 24] },
|
||||
{ 번: 4, 이름: "배수관", 행: [25, 30] },
|
||||
];
|
||||
const out = {
|
||||
calls,
|
||||
kinds: wrapped.kindFilter.map((k) => k.value),
|
||||
@@ -80,6 +88,12 @@ function loadTs(file) {
|
||||
view.builtWord({ 다시씀: true }, true),
|
||||
],
|
||||
},
|
||||
sheet: {
|
||||
at: [19, 20, 22, 24, 25, 30, 31].map((row) => sheet.hopyoAtRow(spans, row)),
|
||||
span: sheet.spanText([20, 24], sheet.lastColumn({ 칸: { A1: {}, N24: {} }, 열: { O: {} } })),
|
||||
last: [sheet.lastColumn({ 칸: { Z9: {}, AA1: {} } }), sheet.lastColumn({ 칸: {} })],
|
||||
rows: [sheet.rowsText(4345), sheet.rowsText([93, 101])],
|
||||
},
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
})().catch((error) => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
- 고르기 창 덧줄(Node · fetch 대역): 이 층 · 위 층 호표 줄(맞춤 n/m) · 종류 고르기 ·
|
||||
고르면 문서 층에 만들기 길 → `lib.pick` 참조 줄(별칭 = 호표 품명) · 프로젝트 층엔 없음 ·
|
||||
본 화면 글 도우미
|
||||
- 엑셀 보기 창(329): 행 → 호표 · 호표 영역 A1 · 격자 길 · 여는 자리 넷 · 늦게 불러옴
|
||||
- 소스: 좌측 판 자리(사전조합 아래 · 구조물 조합 층 · 쓰기 권한 없으면 올리기 꺼짐 ·
|
||||
숨은 file 입력) · Labor_Detail 줄 수 그대로 · 새 파일 이름 · 700 줄 · 색 = 공용 토큰
|
||||
"""
|
||||
@@ -15,7 +16,7 @@ from pathlib import Path
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
M02 = Path(HERE).parents[1] / "M02_MasterTemplete"
|
||||
NEW = ("Hopyo_Api", "Hopyo_Pick", "Hopyo_Side", "Hopyo_View")
|
||||
NEW = ("Hopyo_Api", "Hopyo_Pick", "Hopyo_Side", "Hopyo_View", "Hopyo_Sheet")
|
||||
|
||||
|
||||
def _text(name: str) -> str:
|
||||
@@ -68,7 +69,15 @@ def test_고르기_창_실무_호표_줄과_고름():
|
||||
"name": "임도표지석 910×600×150",
|
||||
"sub": "북부청.xls · 제3호표 · 마스터",
|
||||
"unit": "개소 · 맞춤 2/4",
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "hopyo",
|
||||
"key": "system||HP-000001|엑셀",
|
||||
"tag": "실무 호표",
|
||||
"name": "엑셀로 보기 · 여러 호표",
|
||||
"sub": "북부청.xls · 마스터",
|
||||
"unit": "호표 1",
|
||||
},
|
||||
]
|
||||
build = got["calls"][1]
|
||||
assert build["url"] == "/api/m02/layers/company/hopyo-build?owner=7" # 만드는 층 = 문서 층
|
||||
@@ -145,3 +154,37 @@ def test_로직_추가_한_곳과_파일_규칙():
|
||||
css = _text("M02_MasterTemplete_Hopyo.css")
|
||||
assert "var(--color-" in css and not re.search(r"#[0-9a-fA-F]{3,6}\b", css)
|
||||
assert '"./M02_MasterTemplete_Hopyo.css"' in _text("M02_MasterTemplete_Hopyo_View.ts")
|
||||
|
||||
|
||||
def test_엑셀_보기_창_행과_호표():
|
||||
got = _run()
|
||||
assert got["calls"][2] == {
|
||||
"url": "/api/m02/layers/company/hopyo-grid/HP-000001?owner=7",
|
||||
"method": "GET",
|
||||
"body": None,
|
||||
}
|
||||
sheet = got["sheet"]
|
||||
assert sheet["at"] == [None, 3, 3, 3, 4, 4, None] # 22행 → 제3호표 · 범위 밖 = 없음
|
||||
assert sheet["span"] == "B20:O24" # 제3호표 영역 = B열 ~ 끝 열(열 폭 정보 O)
|
||||
assert sheet["last"] == ["AA", "A"]
|
||||
assert sheet["rows"] == ["4345:4345", "93:101"] # 단가산출 행 전체
|
||||
|
||||
|
||||
def test_엑셀_보기_창_여는_자리와_늦게_불러옴():
|
||||
lazy = 'import("./M02_MasterTemplete_Hopyo_Sheet")'
|
||||
side = _text("M02_MasterTemplete_Hopyo_Side.ts")
|
||||
assert lazy in side.split("await show(got);")[1].split("})")[0] # 올린 직후 자동
|
||||
view = _text("M02_MasterTemplete_Hopyo_View.ts")
|
||||
assert lazy in view and '"엑셀로 보기"' in view and '"엑셀에서 보기"' in view
|
||||
assert "호표: no" in view # 호표 화면 = 그 호표 고른 채
|
||||
pick = _text("M02_MasterTemplete_Hopyo_Pick.ts")
|
||||
assert lazy in pick and "lib.pick(got.참조" in pick
|
||||
static = r'^import (?!type )[^(;]*"\./M02_MasterTemplete_Hopyo_Sheet"'
|
||||
for text in (side, view, pick):
|
||||
assert not re.search(static, text, re.M) # 정적 import 없음(타입만)
|
||||
sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts")
|
||||
assert 'import("../A00_Common/spreadsheet/spreadsheet")' in sheet
|
||||
assert not re.search(
|
||||
r'^import \{[^}]*\} from "\.\./A00_Common/spreadsheet/spreadsheet"', sheet, re.M
|
||||
)
|
||||
assert "readOnly: true" in sheet and "wide: true" in sheet
|
||||
|
||||
Reference in New Issue
Block a user