feat(b09): 단가연결 모달 — 연결 하위 메뉴 다섯 → 한 창(종류 고르기 · 검색) · 이름 단가연결 (251-3 · 251-6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
* 고친 칸 = 칸 강조(셈 줄 `auto_{칸}` 있음) · 풍선 = 자동 값
|
||||
* 줄 누름 = 단가를 낸 탭 줄(자재 → 재료비목록표 · 구조물 → 일위대가목록표)
|
||||
* 레벨 = 줄 `level`(셈이 냄) → 공용 표 level 옵션(공종 · 명칭 칸 들여쓰기 · 201-1) · 다루기 = `…_Bill_Level`
|
||||
* 「연결 ▾」 칸 = 항목 줄(자재 · 조달수수료 빼고) · 누름 = 우클릭 [연결] 과 같은 메뉴
|
||||
* 「단가연결 ▾」 칸 = 항목 줄(자재 · 조달수수료 빼고) · 누름 = 우클릭 [연결] 과 같은 메뉴
|
||||
* 접히지 않는 나무(235-1) = 폴더(묶음 · 자식 있는 줄) = 이름만 · 수량 · 단가 빈칸 · 금액 = 자손 합 수식(읽기 전용) ·
|
||||
* 파일(항목) = 이름 · 규격 · 단위 · 수량 + 연결 · 안내선 · 아이콘 = 공용 표 level.tree · 고칠 칸 = 더블클릭 입력
|
||||
* ========================================================================== */
|
||||
@@ -37,7 +37,7 @@ export const buildBill: TabBuild = (ctx): BillTable => {
|
||||
const rows: DataRow[] = raw.map((r: Row) => ({
|
||||
...r,
|
||||
name: r.no ? `${r.no} ${r.name}` : r.name,
|
||||
connect: linkable(r) ? `${!r.auto && r.note ? r.note : r.ref ? "연결됨" : "연결"} ▾` : "",
|
||||
connect: linkable(r) ? `${!r.auto && r.note ? r.note : r.ref ? "연결됨" : "단가연결"} ▾` : "",
|
||||
}));
|
||||
const direct: Row = bill.totals?.direct || {};
|
||||
const pair = (key: string, label: string) => [
|
||||
@@ -57,14 +57,14 @@ export const buildBill: TabBuild = (ctx): BillTable => {
|
||||
...pair("labor", "노무비"),
|
||||
...pair("material", "재료비"),
|
||||
...pair("expense", "경비"),
|
||||
text("connect", "연결", 96),
|
||||
text("connect", "단가연결", 96),
|
||||
text("note", "비고", 200),
|
||||
],
|
||||
rows,
|
||||
total: { name: "순공사비 계(관급 제외)", ...direct },
|
||||
frozen: 1,
|
||||
hint:
|
||||
"줄을 우클릭하면 항목 추가 · 줄 삭제 · 옮김 · 「연결 ▾」로 일위대가 · 단가산출 연결" +
|
||||
"줄을 우클릭하면 항목 추가 · 줄 삭제 · 옮김 · 「단가연결 ▾」로 일위대가 · 단가산출 연결" +
|
||||
(direct.blank
|
||||
? ` · 빈 줄 ${direct.blank} — 계는 아는 금액만 · 원가계산서는 빈 줄이 없을 때만 받음`
|
||||
: ""),
|
||||
|
||||
@@ -252,8 +252,8 @@ export function billMenu(ctx: TabContext, rows: DataRow[], i: number): MapContex
|
||||
else list.splice(at < 0 ? list.length : at + 1, 0, newItem(e.parent));
|
||||
});
|
||||
if (linkable(row)) {
|
||||
const [picks, more] = linkFor(ctx, rows, i);
|
||||
parts[1].push({ label: "연결", children: picks }, ...more);
|
||||
const [picks, more] = linkFor(ctx, rows, i); // 251-6 단가연결 = 모달 하나(옛 하위 메뉴 다섯)
|
||||
parts[1].push(...picks, ...more);
|
||||
}
|
||||
// 옮김 · 레벨 — 같은 부모 형제와 자손째
|
||||
if (!fixed(key)) {
|
||||
@@ -355,7 +355,8 @@ export function openLinkMenu(ctx: TabContext, rows: DataRow[], i: number): void
|
||||
const box = lastCell?.getBoundingClientRect();
|
||||
if (!shared || !box || !linkable(rows[i])) return;
|
||||
const [picks, more] = linkFor(ctx, rows, i);
|
||||
shared.open(box.left, box.bottom, more.length ? [...picks, "-", ...more] : picks);
|
||||
if (!more.length) return picks[0][1](); // 251-6 아직 연결 없음 = 단가연결 모달 바로
|
||||
shared.open(box.left, box.bottom, [...picks, "-", ...more]);
|
||||
}
|
||||
|
||||
/** B09 메뉴 한 벌(우클릭 · 183-3 칸 고르기 같이 씀) — 화면 기준 자리 */
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* 서고(마스터 · 회사 · 개인) 구조물조합 · 일위대가 사전조합(204-2 — 서버가 프로젝트 사본으로 들여와 새
|
||||
* 대가 호표 · 같은 것 = 있는 사본) · M01 로직(새 산근 = 서버 셈 · [저장] 뒤 단가) · 로직 입력값 고치기 ·
|
||||
* 연결 풀기.
|
||||
* 251 단가연결 = 한 모달(`openPriceLink` · 종류 고르기 + 검색) — B09 설계내역서 · B08 거리이정표 같이 씀 ·
|
||||
* 단가산출 = 옵션 값 창(`askInputs` · 160)
|
||||
*
|
||||
* 줄에 적는 일 = 부르는 쪽 `apply(ref, from)`(`…_UI_Tabs_Bill_Edit` · 저장 꼴 모름)
|
||||
* 로직 · 사전조합 고르기 · 입력값 = M02 창 재사용 · 구조물조합 고르기 = B08 [선택] 나무(173) 재사용 ·
|
||||
@@ -12,9 +14,8 @@
|
||||
|
||||
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { createFindList } from "@ui/ui_template_find_list";
|
||||
import { openItemPicker } from "@ui/ui_template_item_picker";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { loadRows } from "@ui/ui_template_item_picker";
|
||||
import { itemName, type PickerLayer } from "@ui/ui_template_item_picker_data";
|
||||
import { fetchLogicInputs } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Api";
|
||||
import { openLaborInput } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Input";
|
||||
import { openLogicPicker } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Pick";
|
||||
@@ -24,47 +25,14 @@ import type { TabContext } from "./B09_Estimation_UI_Tabs";
|
||||
/** 연결 적용 — ref = null 이면 풀기 · from = 고른 호표(명칭 · 규격 · 단위 채움) */
|
||||
export type ApplyRef = (ref: Row | null, from?: Row) => void;
|
||||
export type Leaf = [string, () => void];
|
||||
|
||||
/** 있는 호표 고르기 창 — 일위대가목록표(dg) · 단가산출근거목록표(sg) */
|
||||
function pickHopyo(ctx: TabContext, apply: ApplyRef, tab: "dg" | "sg"): void {
|
||||
const listing: Row[] = (tab === "dg" ? ctx.sheets.daega : ctx.sheets.sg_list) || [];
|
||||
if (!listing.length) return showToast("고를 호표 없음 — 연결된 구조물 조합 사본이 없음", "error");
|
||||
const label = (r: Row): string => (tab === "dg" ? `대가${r.no}` : `산근 ${r.no}호표`);
|
||||
openModal({
|
||||
title: tab === "dg" ? "일위대가 호표 고르기" : "단가산출 호표 고르기",
|
||||
closeLabel: "닫기",
|
||||
mount: (body, close) => {
|
||||
const finder = createFindList({ placeholder: "호표 · 명칭 · 규격 찾기" });
|
||||
const find = (): void => {
|
||||
const q = finder.search.value.trim().toLowerCase();
|
||||
const found = listing.filter(
|
||||
(r) => !q || `${label(r)} ${r.name} ${r.spec}`.toLowerCase().includes(q),
|
||||
);
|
||||
finder.hint.textContent = `${found.length} / ${listing.length}`;
|
||||
finder.setRows(
|
||||
found.map((r) => ({
|
||||
key: String(r.key),
|
||||
tag: label(r),
|
||||
name: String(r.name || ""),
|
||||
sub: String(r.spec || ""),
|
||||
unit: String(r.unit || ""),
|
||||
onPick: () => {
|
||||
close();
|
||||
apply({ tab, key: r.key }, r);
|
||||
},
|
||||
})),
|
||||
);
|
||||
};
|
||||
finder.search.addEventListener("input", find);
|
||||
find();
|
||||
body.append(finder.root);
|
||||
requestAnimationFrame(() => finder.search.focus());
|
||||
},
|
||||
});
|
||||
}
|
||||
const LAYER_TEXT: Record<PickerLayer, string> = {
|
||||
system: "마스터",
|
||||
company: "회사",
|
||||
personal: "개인",
|
||||
};
|
||||
|
||||
/** M01 로직 → 입력값 창 → 연결(새 산근 · 단가는 [저장] 뒤 서버 셈) */
|
||||
function askInputs(apply: ApplyRef, code: string, name: string, old?: Row): void {
|
||||
export function askInputs(apply: ApplyRef, code: string, name: string, old?: Row): void {
|
||||
fetchLogicInputs([code]).then(
|
||||
(got) => {
|
||||
const logic = got.로직[0] ?? null;
|
||||
@@ -94,10 +62,10 @@ function askInputs(apply: ApplyRef, code: string, name: string, old?: Row): void
|
||||
/** 서고 것 → 프로젝트 사본 · 새 대가 호표(204-2) — 서버가 들여옴 → 받은 원가 자료(대가 블록 · 산근)만
|
||||
* 바꿔 끼우고(사용자 캐시 그대로) 줄 연결 `{tab: dg, key: 블록 id, lib: 사본 폴더}` */
|
||||
async function importLib(
|
||||
ctx: TabContext,
|
||||
apply: ApplyRef,
|
||||
o: { apply: ApplyRef; onPayload?: (payload: Row) => void },
|
||||
pick: { kind: "combo" | "precombo"; layer: string; owner: string | null; key: string },
|
||||
): Promise<void> {
|
||||
const apply = o.apply;
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
if (!projectId) return;
|
||||
showToast("가져오는 중…", "info");
|
||||
@@ -115,7 +83,7 @@ async function importLib(
|
||||
}
|
||||
if (!got.key) throw new Error("사본은 들여왔으나 호표를 못 만듦");
|
||||
const { user: _user, ...rest } = got.payload as Row;
|
||||
Object.assign(ctx.payload, rest);
|
||||
o.onPayload?.(rest);
|
||||
const from = ((rest.sheets?.daega as Row[]) || []).find((r) => r.key === got.key);
|
||||
apply({ tab: "dg", key: got.key, lib: got.폴더 }, from);
|
||||
showToast(
|
||||
@@ -127,39 +95,79 @@ async function importLib(
|
||||
}
|
||||
}
|
||||
|
||||
/** 서고 구조물조합 — B08 [선택] 나무(마스터 · 회사 · 개인 · 보기만) */
|
||||
function pickCombo(ctx: TabContext, apply: ApplyRef): void {
|
||||
void openItemPicker({ current: null, canCopy: false }).then((got) => {
|
||||
if (got.action !== "apply") return;
|
||||
const { layer, owner, key } = got.item;
|
||||
void importLib(ctx, apply, { kind: "combo", layer, owner, key });
|
||||
});
|
||||
}
|
||||
|
||||
/** 서고 일위대가 사전조합 — M02 고르기 창 서고 줄만(층 단추 마스터 · 회사 · 개인) */
|
||||
function pickPrecombo(ctx: TabContext, apply: ApplyRef): void {
|
||||
openLogicPicker(() => undefined, {
|
||||
layer: "personal",
|
||||
owner: null,
|
||||
libOnly: true,
|
||||
title: "일위대가 사전조합 고르기",
|
||||
pick: (ref) =>
|
||||
void importLib(ctx, apply, { kind: "precombo", layer: ref.층, owner: ref.주인, key: ref.키 }),
|
||||
});
|
||||
}
|
||||
|
||||
/** 항목 줄 연결 메뉴 — [고르기 셋, 그 밖(로직 입력값 · 연결 풀기)] · ref = 지금 연결 */
|
||||
/** 항목 줄 단가연결 메뉴(251-6) — [단가연결 모달, 그 밖(로직 입력값 · 단가연결 풀기)] · ref = 지금 연결 */
|
||||
export function linkItems(ctx: TabContext, ref: Row | null, apply: ApplyRef): [Leaf[], Leaf[]] {
|
||||
const more: Leaf[] = [];
|
||||
if (ref?.tab === "logic")
|
||||
more.push(["로직 입력값 고치기", () => askInputs(apply, ref.code, ref.name || "", ref)]);
|
||||
if (ref) more.push(["연결 풀기", () => apply(null)]);
|
||||
const picks: Leaf[] = [
|
||||
["일위대가 호표", () => pickHopyo(ctx, apply, "dg")],
|
||||
["단가산출 호표", () => pickHopyo(ctx, apply, "sg")],
|
||||
["구조물 조합", () => pickCombo(ctx, apply)],
|
||||
["일위대가 사전조합", () => pickPrecombo(ctx, apply)],
|
||||
["단가산출", () => openLogicPicker((logic) => askInputs(apply, logic.키, logic.이름))],
|
||||
if (ref) more.push(["단가연결 풀기", () => apply(null)]);
|
||||
const open: Leaf = [
|
||||
"단가연결",
|
||||
() =>
|
||||
openPriceLink({
|
||||
apply,
|
||||
hopyo: { dg: ctx.sheets.daega || [], sg: ctx.sheets.sg_list || [] },
|
||||
onPayload: (p) => Object.assign(ctx.payload, p),
|
||||
}),
|
||||
];
|
||||
return [picks, more];
|
||||
return [[open], more];
|
||||
}
|
||||
|
||||
/** 단가연결 모달(251 · 159 · 160) — 한 목록에 단가산출 로직 · 일위대가 사전조합 · 구조물 조합(마스터 · 회사 ·
|
||||
* 개인 층 단추) · 호표(B09 · 있을 때) · 종류 고르기 + 검색 + 구분 · 상세구분 거름 ·
|
||||
* 단가산출 = 옵션 값 창(`askInputs` · 160) · 사전조합 · 구조물 조합 = 프로젝트 사본 들여옴 → `{tab: dg, key, lib}` */
|
||||
export interface PriceLinkOpts {
|
||||
apply: ApplyRef;
|
||||
/** B09 호표 목록(일위대가 · 단가산출) — 없으면 그 종류 없음(B08) */
|
||||
hopyo?: { dg: Row[]; sg: Row[] };
|
||||
/** 서고 사본 들여온 뒤 받은 원가 자료(B09 = 화면 자료에 끼움) */
|
||||
onPayload?: (payload: Row) => void;
|
||||
}
|
||||
|
||||
export async function openPriceLink(o: PriceLinkOpts): Promise<void> {
|
||||
const hop = (tab: "dg" | "sg") =>
|
||||
((tab === "dg" ? o.hopyo?.dg : o.hopyo?.sg) ?? []).map((r) => ({
|
||||
kind: tab,
|
||||
key: String(r.key),
|
||||
tag: tab === "dg" ? `대가${r.no}` : `산근 ${r.no}호표`,
|
||||
name: String(r.name || ""),
|
||||
sub: String(r.spec || ""),
|
||||
unit: String(r.unit || ""),
|
||||
onPick: () => o.apply({ tab, key: r.key }, r),
|
||||
}));
|
||||
// 구조물 조합 = 마스터 · 회사 · 개인 한 목록(B08 [선택] 나무와 같은 원천)
|
||||
const combos = (await loadRows().catch(() => [])).map((r) => ({
|
||||
kind: "combo",
|
||||
key: `${r.layer}:${r.owner ?? ""}:${r.key}`,
|
||||
tag: "구조물 조합",
|
||||
name: itemName(r),
|
||||
sub: `${LAYER_TEXT[r.layer ?? "system"]} · ${r.key}`,
|
||||
unit: String(r.단위 || ""),
|
||||
onPick: () =>
|
||||
void importLib(o, {
|
||||
kind: "combo",
|
||||
layer: r.layer ?? "system",
|
||||
owner: r.owner ?? null,
|
||||
key: r.key,
|
||||
}),
|
||||
}));
|
||||
openLogicPicker((logic) => askInputs(o.apply, logic.키, logic.이름), {
|
||||
layer: "personal",
|
||||
owner: null,
|
||||
title: "단가연결",
|
||||
extra: [...combos, ...hop("dg"), ...hop("sg")],
|
||||
kindFilter: [
|
||||
{ value: "logic", text: "단가산출" },
|
||||
{ value: "precombo", text: "일위대가 사전조합" },
|
||||
{ value: "combo", text: "구조물 조합" },
|
||||
...(o.hopyo
|
||||
? [
|
||||
{ value: "dg", text: "일위대가 호표" },
|
||||
{ value: "sg", text: "단가산출 호표" },
|
||||
]
|
||||
: []),
|
||||
],
|
||||
pick: (ref) =>
|
||||
void importLib(o, { kind: "precombo", layer: ref.층, owner: ref.주인, key: ref.키 }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* [로직 더하기] 창 — 한 목록에 구분(단가산출 · 단가승격 · 일위조합) 칸 · 치는 대로 걸러짐(찾기 단추 없음) ·
|
||||
* 단가승격 · 일위조합 줄은 층 단추(위 층만 · 짝 `M02_Labor_Ref.readable`) 층의 서고 · 고르면 참조 {층 · 주인 · 키} ·
|
||||
* 프로젝트 · 카드엔 서고 줄 없음 · 거름 = 로직 품셈 분류 + 사전조합 새 분류(149-2 · 구조물군 › 공통 + 종류)
|
||||
* 단가연결(251 · B08 거리이정표 · B09 설계내역서) = 종류 고르기(단가산출 · 사전조합 · 덧붙인 줄 종류) +
|
||||
* 덧붙일 줄(구조물 조합 · B09 호표) — `lib.extra` · `lib.kindFilter`
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements";
|
||||
@@ -38,6 +40,18 @@ export interface LibraryPick {
|
||||
/** 서고 줄만(로직 목록 안 받음 · B09 설계내역서 연결 204-2) · 창 제목 */
|
||||
libOnly?: boolean;
|
||||
title?: string;
|
||||
/** 목록 끝 덧붙일 줄(251 · 구조물 조합 · B09 호표) — kind = 종류 고르기 값 */
|
||||
extra?: Array<{
|
||||
kind: string;
|
||||
key: string;
|
||||
tag: string;
|
||||
name: string;
|
||||
sub: string;
|
||||
unit: string;
|
||||
onPick: () => void;
|
||||
}>;
|
||||
/** 종류 고르기 칸(251 · 값 logic · precombo · 덧붙인 줄 kind) */
|
||||
kindFilter?: Array<{ value: string; text: string }>;
|
||||
}
|
||||
|
||||
/** 고를 수 있는 층 — 위 층만(마스터 → 마스터 · 회사 → 그 회사 · 마스터 · 개인 → 그 개인 · 그 회사 · 마스터) */
|
||||
@@ -72,12 +86,21 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra
|
||||
let scopes: Scope[] = [];
|
||||
let scope: Scope | null = null;
|
||||
let libRows: { kind: "Kind_Combo"; row: LibraryRow }[] = [];
|
||||
// 251 종류 고르기 — 값 "" = 전체 · logic · precombo · 덧붙인 줄 kind
|
||||
const kindSel = lib?.kindFilter
|
||||
? createSelectField({
|
||||
options: [{ value: "", text: L("M01_All") }, ...lib.kindFilter],
|
||||
compact: true,
|
||||
label: "종류",
|
||||
})
|
||||
: null;
|
||||
const kindOk = (k: string): boolean => !kindSel?.select.value || kindSel.select.value === k;
|
||||
let loading = false;
|
||||
let seq = 0;
|
||||
/* 공용 찾기 목록 — 구분 · 상세구분 2열 · 이름 왼쪽 맞춤 · ↑ ↓ Enter */
|
||||
const finder = createFindList({
|
||||
placeholder: tc("Pick_Search"),
|
||||
filters: [sub.root, detail.root],
|
||||
filters: [...(kindSel ? [kindSel.root] : []), sub.root, detail.root],
|
||||
extra: [layerBar],
|
||||
});
|
||||
|
||||
@@ -85,18 +108,26 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra
|
||||
const q = finder.search.value.trim().toLowerCase();
|
||||
const found = all.filter(
|
||||
(x) =>
|
||||
kindOk("logic") &&
|
||||
(!sub.select.value || x.구분 === sub.select.value) &&
|
||||
(!detail.select.value || x.상세구분 === detail.select.value) &&
|
||||
matchLogic(finder.search.value, x).ok, // 이름 · 키 · 별칭 …(공용 · 208-1)
|
||||
);
|
||||
const libFound = libRows.filter(
|
||||
(x) =>
|
||||
kindOk("precombo") &&
|
||||
(!sub.select.value || x.row.구분 === sub.select.value) &&
|
||||
(!detail.select.value || x.row.상세구분 === detail.select.value) &&
|
||||
(!q || `${x.row.이름} ${x.row.제목 ?? ""}`.toLowerCase().includes(q)),
|
||||
);
|
||||
const total = all.length + libRows.length;
|
||||
const shown = found.length + libFound.length;
|
||||
const extra = (lib?.extra ?? []).filter(
|
||||
(x) =>
|
||||
kindOk(x.kind) &&
|
||||
!sub.select.value &&
|
||||
(!q || `${x.tag} ${x.name} ${x.sub}`.toLowerCase().includes(q)),
|
||||
);
|
||||
const total = all.length + libRows.length + (lib?.extra?.length ?? 0);
|
||||
const shown = found.length + libFound.length + extra.length;
|
||||
finder.hint.textContent = loading
|
||||
? tc("Loading")
|
||||
: shown
|
||||
@@ -132,6 +163,13 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra
|
||||
},
|
||||
};
|
||||
}),
|
||||
...extra.map((x) => ({
|
||||
...x,
|
||||
onPick: () => {
|
||||
close();
|
||||
x.onPick();
|
||||
},
|
||||
})),
|
||||
]);
|
||||
};
|
||||
const loadLib = async (): Promise<void> => {
|
||||
@@ -192,6 +230,7 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra
|
||||
find();
|
||||
});
|
||||
detail.select.addEventListener("change", find);
|
||||
kindSel?.select.addEventListener("change", find);
|
||||
finder.search.addEventListener("input", find);
|
||||
|
||||
layerBar.style.display = "none";
|
||||
|
||||
@@ -227,10 +227,10 @@ console.log(JSON.stringify(out));
|
||||
out = json.loads(_node(src + body))
|
||||
# 246-1 더함 = 「항목 추가」 하나 · 지움 = 「줄 삭제」 · 2단 제목 ← = 1단으로
|
||||
assert out["title"] == ["항목 추가", "-", "내어쓰기 ←", "-", "줄 삭제"]
|
||||
assert out["auto"] == ["항목 추가", "-", "연결", "-", "아래로 ↓", "내어쓰기 ←",
|
||||
assert out["auto"] == ["항목 추가", "-", "단가연결", "-", "아래로 ↓", "내어쓰기 ←",
|
||||
"-", "자동값으로 되돌리기", "줄 삭제"] # fmt: skip
|
||||
# 204 — 위 항목 아래로 들여쓰기 됨(그 항목 = 저절로 묶음)
|
||||
assert out["hand"] == ["항목 추가", "-", "연결", "연결 풀기", "-", "위로 ↑",
|
||||
assert out["hand"] == ["항목 추가", "-", "단가연결", "단가연결 풀기", "-", "위로 ↑",
|
||||
"들여쓰기 →", "내어쓰기 ←", "묶음 줄로 바꾸기", "-", "줄 삭제"] # fmt: skip
|
||||
# 항목 추가(항목 줄) = 바로 아래 같은 부모 · 옛 꼴 → v2(옛 손 줄 칸 · 수량 덮개 옮김)
|
||||
below = out["below"]
|
||||
|
||||
@@ -266,6 +266,13 @@ globalThis.openModal = (o) => {
|
||||
picked.title = o.title;
|
||||
o.mount({ append() {} }, () => (picked.closed = true));
|
||||
};
|
||||
// 251-6 단가연결 = 한 모달(openLogicPicker 바탕) — 호표 줄 = lib.extra
|
||||
globalThis.loadRows = async () => [];
|
||||
globalThis.openLogicPicker = (done, lib) => {
|
||||
picked.title = lib.title;
|
||||
picked.kinds = lib.kindFilter.map((k) => k.text);
|
||||
picked.rows = lib.extra;
|
||||
};
|
||||
let user = __USER__;
|
||||
const ctx = { sheets: { bill: __BILL__, daega: __DAEGA__ }, payload: {},
|
||||
get user() { return user; }, setUser: (u) => (user = u) };
|
||||
@@ -288,11 +295,11 @@ const out = {
|
||||
table.onCell(at("hand:a"), "connect");
|
||||
table.onCell(at("hand:g1"), "connect");
|
||||
table.onCell(at("earth:4"), "connect");
|
||||
table.onCell(at("hand:c"), "connect");
|
||||
table.onCell(at("hand:c"), "connect"); // 연결 없음 = 단가연결 창 바로(메뉴 없음)
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
out.menus = opened.map((m) => [m.x, m.y, m.items.map(name)]);
|
||||
opened[2].items[0][1](); // hand:c → 일위대가 호표 고르기 창
|
||||
picked.rows[0].onPick();
|
||||
out.picked = [picked.title, picked.closed, picked.rows.map((r) => r.tag)];
|
||||
picked.rows.find((r) => r.tag === "대가3").onPick();
|
||||
out.picked = [picked.title, picked.kinds, picked.rows.map((r) => r.tag)];
|
||||
out.ref = user.bill.rows.find((r) => r.key === "hand:c").ref;
|
||||
console.log(JSON.stringify(out));
|
||||
"""
|
||||
@@ -318,17 +325,12 @@ def test_연결_단추_칸_메뉴_고르기_창_TS():
|
||||
] # fmt: skip 201-1 레벨 = 공용 표 level
|
||||
assert out["lv"] == ["name", 1, 3, 4, 3]
|
||||
assert out["edit"] == [True, False] # 묶음 줄 = 이름만
|
||||
picks = [
|
||||
"일위대가 호표",
|
||||
"단가산출 호표",
|
||||
"구조물 조합",
|
||||
"일위대가 사전조합",
|
||||
"단가산출",
|
||||
] # 204-2 · 246-1 이름 셋
|
||||
# 누른 칸 아래(left · bottom) · 묶음 줄 = 안 엶 · 연결된 줄(자동 줄도) = 연결 풀기 더함
|
||||
unlink = [10, 40, [*picks, "-", "연결 풀기"]]
|
||||
assert out["menus"] == [unlink, unlink, [10, 40, picks]]
|
||||
assert out["picked"] == ["일위대가 호표 고르기", True, ["대가3"]] # 166-2 고르기 창 재사용
|
||||
# 251-6 누른 칸 아래(left · bottom) · 묶음 줄 = 안 엶 · 연결된 줄 = [단가연결 · 단가연결 풀기] ·
|
||||
# 연결 없는 줄 = 단가연결 창 바로(종류 = 단가산출 · 사전조합 · 구조물 조합 · 호표 둘)
|
||||
unlink = [10, 40, ["단가연결", "-", "단가연결 풀기"]]
|
||||
assert out["menus"] == [unlink, unlink]
|
||||
kinds = ["단가산출", "일위대가 사전조합", "구조물 조합", "일위대가 호표", "단가산출 호표"]
|
||||
assert out["picked"] == ["단가연결", kinds, ["대가3"]]
|
||||
assert out["ref"] == DG
|
||||
|
||||
|
||||
|
||||
@@ -123,15 +123,15 @@ def test_메뉴_모든_줄():
|
||||
# 맨 위 제목 = 항목 추가(첫 하위) · 아래로(자식 있는 맨 위 = 못 지움 · 맨 위 = 내어쓰기 없음)
|
||||
assert out["sec:1"] == [*add, "-", "아래로 ↓"]
|
||||
# 자동 항목 = 더함 · 연결 · 옮김 · 삭제(고친 칸 없음 = 자동값으로 없음)
|
||||
assert out["earth:4"] == [*add, "-", "연결", "연결 풀기", "-",
|
||||
assert out["earth:4"] == [*add, "-", "단가연결", "단가연결 풀기", "-",
|
||||
"아래로 ↓", "내어쓰기 ←", "-", "줄 삭제"] # fmt: skip
|
||||
# 204 — 위 형제가 항목이어도 들여쓰기 됨(그 항목 = 저절로 묶음)
|
||||
assert out["earth:5"] == [*add, "-", "연결", "-", "위로 ↑", "아래로 ↓",
|
||||
assert out["earth:5"] == [*add, "-", "단가연결", "-", "위로 ↑", "아래로 ↓",
|
||||
"들여쓰기 →", "내어쓰기 ←", "-", "자동값으로 되돌리기",
|
||||
"줄 삭제"] # fmt: skip
|
||||
assert out["hand:g"] == [*add, "-", "위로 ↑", "아래로 ↓", "들여쓰기 →", "내어쓰기 ←",
|
||||
"항목으로 바꾸기", "-", "줄 삭제"] # fmt: skip
|
||||
assert out["hand:a"] == [*add, "-", "연결", "연결 풀기", "-", "내어쓰기 ←",
|
||||
assert out["hand:a"] == [*add, "-", "단가연결", "단가연결 풀기", "-", "내어쓰기 ←",
|
||||
"묶음 줄로 바꾸기", "-", "줄 삭제"] # fmt: skip
|
||||
# 조달수수료 · 관급 자재 = 연결 · 옮김 · 삭제 없음 · 관급자재대 제목 = 옮김 · 삭제 없음 ·
|
||||
# 자재 줄 = 1단으로 못 냄(별도 자재 mat:M1 도)
|
||||
@@ -174,7 +174,7 @@ def test_삭제_옮김_새_공종_자동값으로():
|
||||
("mat:M2", "내어쓰기 ←"), ("earth:5", "자동값으로 되돌리기"),
|
||||
("hand:b", "들여쓰기 →"), ("hand:a", "내어쓰기 ←"), ("sec:1.2", "항목 추가"),
|
||||
("earth:4", "항목 추가"), ("hand:a", "묶음 줄로 바꾸기"), ("hand:g", "항목으로 바꾸기"),
|
||||
("earth:4", "연결 풀기"), ("earth:4", "내어쓰기 ←"), ("fee", "항목 추가")] # fmt: skip
|
||||
("earth:4", "단가연결 풀기"), ("earth:4", "내어쓰기 ←"), ("fee", "항목 추가")] # fmt: skip
|
||||
out = _menu(user, runs=runs)["runs"]
|
||||
keys = lambda b: [k for k, _ in _order(b)] # noqa: E731
|
||||
# 덮개에 없는 보이는 줄(hand:b · 새 자동 줄) = 보이는 앞 줄 뒤에 굳힘
|
||||
@@ -215,7 +215,7 @@ def test_삭제_옮김_새_공종_자동값으로():
|
||||
got = {e["key"]: e for e in out["hand:g|항목으로 바꾸기"]["rows"]}
|
||||
assert "kind" not in got["hand:g"] and got["hand:a"]["parent"] == "sec:1.2"
|
||||
# 자동 줄 연결 풀기 = ref 덮개 null
|
||||
assert {e["key"]: e for e in out["earth:4|연결 풀기"]["rows"]}["earth:4"]["ref"] is None
|
||||
assert {e["key"]: e for e in out["earth:4|단가연결 풀기"]["rows"]}["earth:4"]["ref"] is None
|
||||
# 조달수수료 항목 추가 = 관급자재대 마지막 자식(조달수수료는 셈이 끝에 둠)
|
||||
got = out["fee|항목 추가"]["rows"]
|
||||
assert (got[-1]["name"], got[-1]["parent"], got[-2]["key"]) == ("새 줄", "sec:G", "mat:M2")
|
||||
@@ -270,7 +270,7 @@ def test_표_칸_강조_번호_연결_칸():
|
||||
)
|
||||
out = json.loads(_node(_ts([*SRC, "UI_Tabs_Bill"]) + body))
|
||||
assert out["names"] == ["1 토공사", "1.1 절토", "earth:4"] # 번호 = 셈 no
|
||||
assert out["connect"] == ["연결됨 ▾", "연결 ▾", "대가3 ▾", "", "", ""]
|
||||
assert out["connect"] == ["연결됨 ▾", "단가연결 ▾", "대가3 ▾", "", "", ""]
|
||||
assert out["cls"] == ["is-edited", None, None]
|
||||
assert out["tip"] == "자동 값 5 — 우클릭 [자동값으로 되돌리기]"
|
||||
assert out["edit"] == [True, False, True, True, False]
|
||||
|
||||
@@ -188,10 +188,11 @@ globalThis.showToast = (m, k) => toasts.push([k, m]);
|
||||
globalThis.API_BASE_URL = "/api";
|
||||
globalThis.CURRENT_PROJECT_ID_KEY = "pid";
|
||||
globalThis.localStorage = { getItem: () => "P1" };
|
||||
globalThis.openLogicPicker = (done, lib) => { sent.push(["pre", lib.layer, lib.libOnly, lib.title]);
|
||||
lib.pick({ 층: "company", 주인: "7", 키: "SC-000002" }, "회사 조합"); };
|
||||
globalThis.openItemPicker = async () => ({ action: "apply",
|
||||
item: { layer: "system", owner: null, key: "SS000011", name: "관매설" } });
|
||||
let picker = null; // 251-6 단가연결 = 한 창(openLogicPicker · 사전조합 + 덧붙인 줄 구조물 조합)
|
||||
globalThis.openLogicPicker = (done, lib) => { picker = lib;
|
||||
sent.push(["pre", lib.layer, lib.kindFilter.map((k) => k.value), lib.title]); };
|
||||
globalThis.loadRows = async () => [{ key: "SS000011", layer: "system", owner: null, 단위: "m" }];
|
||||
globalThis.itemName = () => "관매설";
|
||||
const answer = { 폴더: "precombo@company@7@SC-000002", 만듦: true, key: "DG:new",
|
||||
payload: { user: { bill: "서버 것" }, inputs: { daega: { libs: {} } },
|
||||
sheets: { daega: [{ key: "DG:new", name: "회사 조합", spec: "", unit: "㎡" }] } } };
|
||||
@@ -202,9 +203,12 @@ const applied = [];
|
||||
const apply = (ref, from) => applied.push([ref, from && from.name]);
|
||||
const [picks, more] = linkItems(ctx, { tab: "dg", key: "DG:a" }, apply);
|
||||
const out = { picks: picks.map((x) => x[0]), more: more.map((x) => x[0]) };
|
||||
picks.find((x) => x[0] === "일위대가 사전조합")[1]();
|
||||
picks[0][1]();
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
picks.find((x) => x[0] === "구조물 조합")[1]();
|
||||
out.combo = picker.extra.filter((x) => x.kind === "combo").map((x) => [x.tag, x.name, x.sub]);
|
||||
picker.pick({ 층: "company", 주인: "7", 키: "SC-000002" }, "회사 조합");
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
picker.extra.find((x) => x.kind === "combo").onPick();
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
Object.assign(out, { sent, applied, payload: ctx.payload, toasts });
|
||||
@@ -223,16 +227,12 @@ def test_화면_출처_다섯_사전조합_창_들여오기_줄_연결_TS():
|
||||
) # fmt: skip
|
||||
assert result.returncode == 0, result.stderr
|
||||
out = json.loads(result.stdout)
|
||||
assert out["picks"] == [
|
||||
"일위대가 호표",
|
||||
"단가산출 호표",
|
||||
"구조물 조합",
|
||||
"일위대가 사전조합",
|
||||
"단가산출",
|
||||
] # 246-1 이름 셋(서고 · 층 · M01 글 없음)
|
||||
assert out["more"] == ["연결 풀기"]
|
||||
assert out["picks"] == ["단가연결"] # 251-6 고르기 다섯 = 단가연결 창 하나
|
||||
assert out["more"] == ["단가연결 풀기"]
|
||||
pre, (url, body) = out["sent"][0], out["sent"][1]
|
||||
assert pre == ["pre", "personal", True, "일위대가 사전조합 고르기"] # 서고 줄만 · 층 셋
|
||||
kinds = ["logic", "precombo", "combo", "dg", "sg"]
|
||||
assert pre == ["pre", "personal", kinds, "단가연결"] # 층 셋 단추 · 종류 고르기
|
||||
assert out["combo"] == [["구조물 조합", "관매설", "마스터 · SS000011"]]
|
||||
assert url == "/api/projects/P1/estimation/library"
|
||||
assert body == {"kind": "precombo", "layer": "company", "owner": "7", "key": "SC-000002"}
|
||||
assert out["sent"][2][1] == {"kind": "combo", "layer": "system", "owner": None,
|
||||
|
||||
@@ -31,11 +31,12 @@ def test_더함_하나_줄_삭제_연결_이름_셋():
|
||||
assert not set(OLD_ADDS) & set(out[key]) and "줄 지우기" not in out[key]
|
||||
assert "줄 삭제" in out["earth:4"] and "줄 삭제" in out["hand:a"]
|
||||
link = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill_Link.ts").read_text("utf-8")
|
||||
picks = link.split("const picks: Leaf[] = [")[1].split("];")[0]
|
||||
# 251-6 고르기 다섯 = 단가연결 모달 하나 · 종류 이름 = 단가산출 · 사전조합 · 구조물 조합 · 호표 둘
|
||||
kinds = link.split("kindFilter: [")[1].split("],\n pick")[0]
|
||||
for name in ["일위대가 호표", "단가산출 호표", "구조물 조합", "일위대가 사전조합", "단가산출"]:
|
||||
assert f'["{name}",' in picks
|
||||
assert f'text: "{name}"' in kinds
|
||||
for word in ["서고", "마스터", "회사", "개인", "M01", "새 산근", "로직"]:
|
||||
assert word not in picks
|
||||
assert word not in kinds
|
||||
toasts = [part.split(",")[0] for part in link.split("showToast(")[1:]]
|
||||
assert toasts and not any("서고" in t for t in toasts) # 알림 글 쉬운 말
|
||||
bill = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill.ts").read_text("utf-8")
|
||||
|
||||
@@ -165,4 +165,4 @@ def test_옛_아코디언_없음_공용_꼴():
|
||||
assert ".ui-ctx-menu__sub" not in css and ".ui-ctx-menu__item.is-open" in css
|
||||
# B09 연결 = children 꼴 그대로(공용 메뉴가 옆 판으로)
|
||||
edit = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill_Edit.ts").read_text("utf-8")
|
||||
assert 'parts[1].push({ label: "연결", children: picks }, ...more);' in edit
|
||||
assert "parts[1].push(...picks, ...more);" in edit # 251-6 단가연결 = 모달 하나(하위 메뉴 없음)
|
||||
|
||||
Reference in New Issue
Block a user