From f97ea96c08aaec026ca964a0039c37829dd53bab Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 16:50:34 +0900 Subject: [PATCH] =?UTF-8?q?feat(736-1):=20=EC=9D=BC=EC=9C=84=EB=8C=80?= =?UTF-8?q?=EA=B0=80=20[=ED=95=AD=EB=AA=A9=20=EC=B6=94=EA=B0=80]=20?= =?UTF-8?q?=ED=95=98=EB=82=98=20=C2=B7=20=EC=B0=BE=EC=95=84=20=EB=84=A3?= =?UTF-8?q?=EA=B8=B0=20=EC=B0=BD=20=ED=95=98=EB=82=98=EB=A1=9C=20=E2=80=94?= =?UTF-8?q?=20=EC=9E=90=EC=9E=AC=20=EC=B6=94=EA=B0=80=20=EB=8B=A8=EC=B6=94?= =?UTF-8?q?=20=EA=B1=B7=EC=9D=8C=20=C2=B7=20=EC=97=91=EC=85=80=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=C2=B7=20=EB=B3=84=EC=B9=AD=20=EC=9E=A5=EB=B6=80=20?= =?UTF-8?q?=C2=B7=20=EA=B2=AC=EB=B3=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일위대가표 · 구조물 조합 상세 [로직 추가] → [항목 추가] · [자재 추가] 걷음 · 재료 고름 = 자재 목록(로직 안 자재 · 이미 있는 자재 막힘 그대로) - 찾아 넣기 창 탭 막기(kinds) · 찾기 칸 미리 넣기(query) · 재료 길(material) - 불러오기 엑셀 연결 = 「다른 항목 검색…」 한 창(기초 · 재료) · 별칭 장부 = 기초 · 재료 둘 - 부품 모음 단추 견본에 갈래 색(옵션값 · 요소 연결) 칸 - 낡은 시험 고침: 175(머리 단추) · 721(부르는 곳) · 자재 개인(상세 단추) · 호표 화면(장부 단추 글) · 헬퍼 넷(Find_Pick 대역) Co-Authored-By: Claude Sonnet 5.5 --- .../M02_MasterTemplete_CardHelp.ts | 3 +- .../M02_MasterTemplete_Find_Pick.ts | 45 +++++++++++++++---- .../M02_MasterTemplete_Hopyo_Choose.ts | 37 ++++++++------- .../M02_MasterTemplete_Hopyo_Ledger.ts | 18 +++++--- .../M02_MasterTemplete_Labor_Detail.ts | 35 ++++++++------- .../M02_MasterTemplete_Labor_Pick.ts | 11 ++++- .../M02_MasterTemplete_Labor_Table.ts | 31 ++++++------- .../M02_MasterTemplete_Labor_Text.ts | 2 +- resources/tester/helper_729_3_hopyo_left.cjs | 1 + resources/tester/helper_m02_hopyo_check.cjs | 1 + resources/tester/helper_m02_hopyo_pick.cjs | 1 + resources/tester/helper_m02_hopyo_seam.cjs | 1 + resources/tester/test_175_precombo_mats.py | 4 +- resources/tester/test_721_find_pick.py | 31 +++++++++++-- resources/tester/test_m02_hopyo_screen.py | 2 +- resources/tester/test_m02_material_own.py | 2 +- ui_template/ui_template_locale_m2.ts | 2 +- ui_template/ui_template_ui_kit_parts.ts | 4 +- 18 files changed, 152 insertions(+), 79 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_CardHelp.ts b/M02_MasterTemplete/M02_MasterTemplete_CardHelp.ts index e060af9c0..8535729d4 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_CardHelp.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_CardHelp.ts @@ -60,13 +60,12 @@ export const CARD_HELP: Record<"structure" | "labor" | "area", CardHelp> = { features: [ SAVE, take("일위대가"), - ["수정", "일위대가 상세를 엶 — 로직 추가 · 순서 바꾸기 · 다이어그램은 여기"], + ["수정", "일위대가 상세를 엶 — 항목 추가 · 순서 바꾸기 · 다이어그램은 여기"], clear("일위대가"), ["로직 줄 누름", "넣을 값 창 — 그 로직의 넣을 값 고침"], ["수량산출 칸", "수기 입력 · 면적산출 칸 고르기"], ["별칭 칸", "줄에 따로 이름 붙임"], ["줄 끝 버튼", "그 줄을 뺌"], - ["자재 추가", "자재를 골라 자재목록에 넣음"], FOLD, ], steps: [ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Find_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Find_Pick.ts index a1f8ae0cc..3ee870943 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Find_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Find_Pick.ts @@ -7,9 +7,11 @@ * · M02 단가산출 · 일위대가 = 위 층(마스터 › 회사 › 개인) 서고 목록(`/layers/{층}/templates`) · 고름 = 참조 {층 · 주인 · 키} * · 인력 · 재료 고름 = `element` · 일위대가 고정 줄 꼴 = `fixedOf`(705-1 이음 노임 LB · 자재 MT · 수량 1 은 부르는 쪽) · * `element` 없으면 그 줄 회색 막힘(까닭 = 일위대가 표 개편 뒤 · 723) - * · 부르는 곳 = M02 단가산출 [항목 추가](`Sangeun_Pick`) · 일위대가 [로직 추가] + * · 부르는 곳 = M02 단가산출 [항목 추가](`Sangeun_Pick`) · 일위대가 [항목 추가] * (`Labor_Pick` 이 `lib.sangeun` 이면 이 창) — B09 · 불러오기는 옛 창(`Labor_Pick`) * · (727-1) 기초 = 로직마다 한 줄(`logicHeads` · 옵션은 넣은 뒤 고름 · 옛 창도 같이) · 서버 목록 그대로 + * · (736-1) `kinds` = 탭 막기(자리마다 고를 수 있는 종류만 · 불러오기 엑셀 연결 · 별칭 장부) · `material` = 재료 고름을 + * 일위대가 「자재 목록」 자리로(`element` 대신) · `query` = 찾기 칸 미리 넣기 · 부르는 곳 = 일위대가 [항목 추가] * · (728-2) 돈 아닌 기초(결과단위가 「원」 아님 · ㎥/시간 등 21 개) = 목록에서 뺌 * ========================================================================== */ @@ -46,10 +48,19 @@ export interface M01Element { 값: number | null; } +/** 고를 수 있는 종류 — logic = M01 단가산출 · 기초 로직 · labor = 인력 · material = 재료 · sk = M02 단가산출 · pre = 일위대가 */ +export type FindKind = "logic" | "labor" | "material" | "sk" | "pre"; + export interface FindPickOptions { layer: Layer; owner: string | null; title?: string; + /** 736-1 탭 막기 — 이 종류만 보임(없으면 다 · 탭 하나만 남으면 탭 줄 감춤) */ + kinds?: FindKind[]; + /** 찾기 칸에 미리 넣을 글 */ + query?: string; + /** 재료 고름 — 있으면 재료는 `element` 대신 이쪽(일위대가 「자재 목록」 자리) */ + material?: (e: M01Element) => void; /** M01 단가산출 로직 고름 */ logic: (x: LogicSummary) => void; /** M02 단가산출 · 일위대가 고름(참조) */ @@ -117,7 +128,17 @@ interface Line { } export function openFindPicker(opt: FindPickOptions): void { - let tab: Tab = "m01"; + const ok = (k: FindKind): boolean => !opt.kinds || opt.kinds.includes(k); + const allowed = ( + [ + ["m01", ok("logic") || ok("labor") || ok("material")], + ["sk", ok("sk")], + ["pre", ok("pre")], + ] as const + ) + .filter(([, on]) => on) + .map(([k]) => k as Tab); + let tab: Tab = allowed[0] ?? "m01"; let close = (): void => {}; let logics: LogicSummary[] = []; let std: StandardRow[] | null = null; @@ -182,7 +203,7 @@ export function openFindPicker(opt: FindPickOptions): void { 단위: unitOf(m.단위 ?? ""), 값: Number.isFinite(Number(m.값)) && m.값 !== null ? Number(m.값) : null, }; - const put = opt.element; + const put = kind === "재료" && opt.material ? opt.material : opt.element; return { 품명: m.이름, 규격: e.규격, @@ -208,7 +229,11 @@ export function openFindPicker(opt: FindPickOptions): void { function draw(): void { const q = search.input.value.trim(); if (tab === "m01") { - shown = [...logicLines(q), ...elementLines("인력", people), ...elementLines("재료", goods)]; + shown = [ + ...(ok("logic") ? logicLines(q) : []), + ...(ok("labor") ? elementLines("인력", people) : []), + ...(ok("material") ? elementLines("재료", goods) : []), + ]; } else { const words = q.toLowerCase().split(/\s+/).filter(Boolean); const kind = tab === "sk" ? L("M02_PickSangeun") : L("M02_PrecomboTitle"); @@ -231,7 +256,10 @@ export function openFindPicker(opt: FindPickOptions): void { `/logic-lib/pick?kind=${kind}&q=${encodeURIComponent(q)}&limit=200`, ).then((d) => d.items); try { - const [p, g] = await Promise.all([get("job"), q ? get("price") : Promise.resolve([])]); + const [p, g] = await Promise.all([ + ok("labor") ? get("job") : Promise.resolve([]), + q && ok("material") ? get("price") : Promise.resolve([]), + ]); if (mine !== seq) return; [people, goods] = [p, g]; } catch (error) { @@ -252,7 +280,7 @@ export function openFindPicker(opt: FindPickOptions): void { { key: "m01", label: L("M02_FindM01") }, { key: "sk", label: L("M02_PickSangeun") }, { key: "pre", label: L("M02_PrecomboTitle") }, - ], + ].filter((x) => allowed.includes(x.key as Tab)), value: tab, kind: "line", onChange: (key) => { @@ -272,10 +300,11 @@ export function openFindPicker(opt: FindPickOptions): void { el("div", { className: "m02-std-pick", attrs: { "data-find-pick": "" }, - children: [tabs.root, search.root, hint, table.root], + children: [...(allowed.length > 1 ? [tabs.root] : []), search.root, hint, table.root], }), ), })); + if (opt.query) search.input.value = opt.query; search.input.focus(); draw(); @@ -292,7 +321,7 @@ export function openFindPicker(opt: FindPickOptions): void { void upper(opt.layer, opt.owner) .then((scopes) => Promise.all( - (["sk", "pre"] as const).map((t) => + (["sk", "pre"] as const).filter(ok).map((t) => listLayerDocs(scopes, t === "sk" ? SANGEUN_KIND : "precombo").then((rows) => { lists[t] = rows; tabs.setBadge(t, rows.length); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Choose.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Choose.ts index 4d8f6c690..c6b01f65b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Choose.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Choose.ts @@ -5,7 +5,7 @@ * · 후보 묶음(358-1 · 숨김 없음) = 1위 + 판정 / 출처 같음 / 이름 닮은 로직(옛 324 다섯) / 숫자만 닮음(접힘 · * 뜻 낱말 안 겹치는 「후보」는 회색 끝) · 같은 이름이면 책(산림 · 건설) · 판정 꼬리표 · 근거 꼬리표(근거 맞음 초록) * · 고름 = [후보](한 로직) · 또는 체크 + 몫 칸 → [일괄 적용(n)](조합) · 옵션은 부르는 쪽이 서버 `guess` 로 채움 - * · [다른 로직 검색] = `openLogicPicker` 에 품명 첫 뜻 낱말 · [다른 자재 검색] = 자재 줄 · 재료비 있는 줄(358-2) + * · [다른 항목 검색](736-1) = 찾아 넣기 창(`Find_Pick`) 하나 · 품명 첫 뜻 낱말 · 기초 = 자재 아닌 줄 · 재료 = 자재 · 재료비 있는 줄(358-2) * · 자재 줄(361-1) = 1위 / 전국(자리표 · 지역 고른 뒤 되돌림) / 지역 변형(접힘 · 하나 고르면 그 지역) / 그 밖 * 저장 안 함 — 고른 것은 부르는 쪽 캐시(확인 창)로만 감. * ========================================================================== */ @@ -21,8 +21,8 @@ import type { HopyoQty, HopyoTarget, } from "./M02_MasterTemplete_Hopyo_Api"; -import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick"; -import { pickMaterial } from "./M02_Material_Pick"; +import { getActiveLayer, getActiveOwner } from "./M02_MasterTemplete_Api_Fetch"; +import { type FindKind, openFindPicker } from "./M02_MasterTemplete_Find_Pick"; import "./M02_MasterTemplete_Hopyo.css"; const MATERIAL = new Set(["자재", "별도계상"]); @@ -103,7 +103,7 @@ export const qtyText = (q: HopyoQty | undefined): string => ].join(" · ") : ""; -/** [다른 로직 검색] 미리 넣을 글 — 품명 괄호 밖 첫 뜻 낱말(358-1 · 조항 번호는 모든 낱말 맞춤이라 0건 위험) */ +/** [다른 항목 검색] 미리 넣을 글 — 품명 괄호 밖 첫 뜻 낱말(358-1 · 조항 번호는 모든 낱말 맞춤이라 0건 위험) */ export function searchHint(line: Pick): string { return /[가-힣A-Za-z]{2,}/.exec(line.이름.replace(/\([^()]*\)/g, " "))?.[0] ?? ""; } @@ -294,28 +294,27 @@ export function openHopyoChoose(opt: ChooseOptions): void { : []), ], }); - const findMaterial = createButton({ - label: "다른 자재 검색…", + // 736-1 찾아 넣기 창 하나 — 이을 수 있는 종류만(자재 줄 = 재료 · 그 밖 = 기초 로직 · 재료비 있는 줄은 재료도) + const kinds: FindKind[] = [ + ...(material ? [] : (["logic"] as const)), + ...(canPickMaterial(line) ? (["material"] as const) : []), + ]; + const find = createButton({ + label: "다른 항목 검색…", size: "sm", onClick: () => { close(); - pickMaterial((x) => opt.done([{ 자재: x.코드, 배: 1 }])); - }, - }); - const findLogic = createButton({ - label: "다른 로직 검색…", - size: "sm", - onClick: () => { - close(); - openLogicPicker((x) => opt.done([{ 로직: x.키, 입력값: {}, 배: 1 }]), undefined, { + openFindPicker({ + layer: getActiveLayer(), + owner: getActiveOwner(), + kinds, query: searchHint(line), + logic: (x) => opt.done([{ 로직: x.키, 입력값: {}, 배: 1 }]), + material: (e) => opt.done([{ 자재: e.코드, 배: 1 }]), }); }, }); - const more = [ - ...(material ? [] : [findLogic]), - ...(canPickMaterial(line) ? [findMaterial] : []), - ]; + const more = kinds.length ? [find] : []; body.append( ...(head.childElementCount ? [head] : []), note( diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts index f4aaa3a11..c75e7e7c1 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts @@ -3,7 +3,7 @@ * 일위대가 실무자료 별칭 장부 화면(PLAN 338-3 · 설계 4-4) — 본 칸 · 좌측 판 [별칭 장부] 로 엶. * · 표 = 엑셀 품명 · 규격 · 단위 | 갈래 | 우리 로직(여럿) · 옵션 · 배 · 몫 | 누가 · 때 · 처음 파일 | 층(위 층 회색 읽기만) * · 뜻(359-4 · 머리 한 줄 · 좌측 단추 title) = 엑셀 품명 ↔ 우리 로직 · 자재 짝을 기억해 두었다가 같은 품명이 나오면 먼저 제안 - * · 할 일 = 찾기 · 갈래 거름 · 갈래 바꿈 · [로직 바꿈] · [로직 추가] · 배 · 몫 · [옵션] · [삭제] · + * · 할 일 = 찾기 · 갈래 거름 · 갈래 바꿈 · [로직 바꿈] · [항목 추가] · 배 · 몫 · [옵션] · [삭제] · * 위 층 줄 [내 층 복사] · 갈래 글 = 확인 창 드롭다운과 같음(짝 · 로직없음 · 사용안함 · 저장 값 그대로) * · 고친 것 = sessionStorage `m02-hopyo-ledger:{층}:{주인}` → [저장] 때만 `PUT`(서버가 짝을 다시 검사) * · 안 둠 = 발주처 칸 · 자동 올림 · 자동 합침 · 쓰인 횟수 · 회사로 올리기. @@ -23,7 +23,7 @@ import { import { fetchLogicInputs } from "./M02_MasterTemplete_Labor_Api"; import { dateTimeText } from "../common_util/common_util_time"; import { openLaborInput } from "./M02_MasterTemplete_Labor_Input"; -import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick"; +import { openFindPicker } from "./M02_MasterTemplete_Find_Pick"; import "./M02_MasterTemplete_Hopyo.css"; export const LEDGER_TITLE = "별칭 장부"; @@ -212,13 +212,19 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement { if (mine && r.갈래 === "짝") pairs.push( createButton({ - label: "로직 추가", + label: "항목 추가", variant: "ghost", size: "sm", onClick: () => - openLogicPicker((x) => - edit(r, { 짝: [...r.짝, { 로직: x.키, 입력값: {}, 배: 1, 몫: null }] }), - ), + // 736-1 별칭 장부가 가리킬 수 있는 것 = 기초 로직 · 재료 둘(탭 막음) + openFindPicker({ + layer: opt.scope.layer, + owner: opt.scope.owner, + kinds: ["logic", "material"], + logic: (x) => edit(r, { 짝: [...r.짝, { 로직: x.키, 입력값: {}, 배: 1, 몫: null }] }), + material: (e) => + edit(r, { 짝: [...r.짝, { 자재: e.코드, 입력값: {}, 배: 1, 몫: null }] }), + }), }), ); return el("td", { className: "m02-hopyo__parts", children: pairs }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index a3b1aa85f..a0315acaa 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -64,7 +64,6 @@ import { laborGrid } from "./M02_MasterTemplete_Labor_Grid"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import { insideKeys, releaseTied } from "./M02_Labor_LogicMats_Model"; import { keepKnown } from "./M02_Labor_Values_Model"; -import { addMaterialButton } from "./M02_Material_Pick"; import { createUnitPick, NO_UNIT } from "./DataFlow/M02_DF_UnitPick"; import { mountLaborTable } from "./M02_MasterTemplete_Labor_Table"; import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; @@ -79,8 +78,8 @@ export interface LaborDetailOptions { readOnly: boolean; /** 저장 안 한 고침이 생기거나 사라짐 */ onChanged: (dirty: boolean) => void; - /** 구조물 조합 카드 갈래(43-4) — 단가산출 로직 표 + 자재목록만 · 위로 · 아래로 · 로직 추가 · - * 자재 추가 · 정보(단위 · 본뜬) · 다이어그램 없음 · 옵션 · 삭제 · 줄 · 수량 모달은 상세와 같음 */ + /** 구조물 조합 카드 갈래(43-4) — 단가산출 로직 표 + 자재목록만 · 위로 · 아래로 · 항목 추가 · + * 정보(단위 · 본뜬) · 다이어그램 없음 · 옵션 · 삭제 · 줄 · 수량 모달은 상세와 같음 */ card?: boolean; /** 일위대가 사전조합 서고(50-3) — 사전조합 문서(`column` = 키) */ mode?: "precombo"; @@ -515,6 +514,20 @@ export function mountLaborDetail( cur.담은로직.push({ 로직: "", 참조: ref, 별칭, 메모: null, 입력값: {}, 수량: null }); changed(); }, + /** 736-1 찾아 넣기 창에서 재료를 고름 — 「자재 목록」 줄 · 로직 안 자재 · 이미 있는 자재는 막음 */ + material: (m: { + 코드: string; + 이름: string | null; + 규격: string | null; + 단위: string | null; + }) => { + const why = insideKeys((calc ?? last)?.줄).has(m.코드) + ? "Labor_InLogicMat" + : cur.자재.some((x) => x.코드 === m.코드) && "Labor_AlreadyMat"; + if (why) return showToast(tc(why).replace("{value}", `${m.코드} ${m.이름 ?? ""}`), "error"); + cur.자재.push({ 코드: m.코드, 이름: m.이름, 규격: m.규격, 단위: m.단위, 수량: null }); + changed(); + }, }; const addLogic = createButton({ label: tc("AddLogic"), @@ -531,18 +544,6 @@ export function mountLaborDetail( withHopyo(library), ), }); - const addMat = addMaterialButton( - tc("Labor_AddMat"), - (m) => { - const why = insideKeys((calc ?? last)?.줄).has(m.코드) - ? "Labor_InLogicMat" - : cur.자재.some((x) => x.코드 === m.코드) && "Labor_AlreadyMat"; - if (why) return showToast(tc(why).replace("{value}", `${m.코드} ${m.이름 ?? ""}`), "error"); - cur.자재.push({ 코드: m.코드, 이름: m.이름, 규격: m.규격, 단위: m.단위, 수량: null }); - changed(); - }, - opt.layer === "personal" ? { owner: library.owner } : undefined, // 개인 층만 신규 자재(148-3) - ); // 단위 = 표준 단위 목록에서 고르기(지금 값이 목록에 없어도 선택지로 둠) · 저장은 [저장] const unit = { @@ -602,11 +603,11 @@ export function mountLaborDetail( ], }), ]), - // 사전조합 = 같은 [로직 추가](51-11) + // 사전조합 = 같은 [항목 추가](51-11) section(tc("Grid_Title"), null, gridHost), ...[ section(tc("Logics_Title"), opt.card ? null : addLogic, logicHost), - section(tc("Labor_Mats"), opt.card ? null : addMat, matHost), + section(tc("Labor_Mats"), null, matHost), ].filter(() => edit || !opt.card), // 읽기만 카드 = 격자만 ...(opt.card ? [] : [dgBox]), ], diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts index 3e91c8a51..f9c08a56e 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts @@ -39,7 +39,13 @@ import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; // 창 겉틀 import "./M02_MasterTemplete_Labor.css"; import { openModal } from "@ui/ui_template_modal"; import { createText } from "@ui/ui_template_text"; -import { fixedOf, logicHeads, openFindPicker, upper } from "./M02_MasterTemplete_Find_Pick"; +import { + fixedOf, + logicHeads, + type M01Element, + openFindPicker, + upper, +} from "./M02_MasterTemplete_Find_Pick"; export { withHopyo } from "./M02_MasterTemplete_Hopyo_Pick"; @@ -78,6 +84,8 @@ export interface LibraryPick { sangeun?: boolean; /** 721-2 M01 인력 · 재료 → 일위대가 고정 줄(705-1 이음 꼴 · 담은로직 `{로직: "", 고정, 수량}`) */ fixed?: (fx: LaborFixed, name: string) => void; + /** 736-1 M01 재료 → 일위대가 「자재 목록」 줄(있으면 재료는 `fixed` 대신 이쪽 · [자재 추가] 단추 걷음) */ + material?: (e: M01Element) => void; } const LAYER_TEXT = { @@ -101,6 +109,7 @@ export function openLogicPicker( logic: done, ref: lib.pick, element: fixed && ((e) => fixed(fixedOf(e), e.이름)), + material: lib.material, }); } const subs: SubBrief[] = []; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts index 96516971d..7bbbb4138 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts @@ -5,7 +5,7 @@ * · 옵션 값 = 규격 칸 · 「빈 값 n」 = 비고 칸 붉게(입력 빈 로직 줄) * · 수량 칸 = 확인용 임시 입력(723-1 · 휘발 · 문서 · 저장 · dirty 안 건드림) → 그 줄 금액 · 합계 미리 보기 * (짝 셈 `Labor_Calc.settle` · 서버 안 부름) · 문서 안 줄 수량은 늘 null(쓰는 쪽 알약으로 이음) - * · 머리 단추 = [로직 추가] [자재 추가] · 줄 우클릭 · 줄 끝 ⋯ = 위로 · 아래로 · 별칭 · 삭제 + * · 머리 단추 = [항목 추가](736-1 · 자재는 찾기 창에서 고름 → 자재 목록) · 줄 우클릭 · 줄 끝 ⋯ = 위로 · 아래로 · 별칭 · 삭제 * · 줄 누름 = 줄 모달(`Labor_RowModal` — 옵션 · 메모 · 켜쌓기율 · 로직 자재 바꾸기 · 세 칸 · 단가산출) · * 사전조합 참조 줄 = 서고 원본 모달(`Labor_RefEdit`) · 사용자 자재 줄 = 자재 모달(세 칸) * · 금액 = 서버 calc(최신 응답만 · 저장 안 함) · [저장]은 UI_Main(서버가 수량 걷고 다시 셈) @@ -41,7 +41,6 @@ import { openMatModal, openRowModal } from "./M02_MasterTemplete_Labor_RowModal" import { tc } from "./M02_MasterTemplete_Labor_Text"; import { insideKeys } from "./M02_Labor_LogicMats_Model"; import { valueLine } from "./M02_Labor_Values_Model"; -import { addMaterialButton } from "./M02_Material_Pick"; import { createUnitPick, NO_UNIT } from "./DataFlow/M02_DF_UnitPick"; import { libMeta } from "./M02_MasterTemplete_Labor_LibMeta"; import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; @@ -374,6 +373,20 @@ export function mountLaborTable( cur.담은로직.push({ 로직: "", 고정: fx, 별칭: null, 메모: null, 입력값: {}, 수량: null }); changed(); }, + /** 736-1 찾아 넣기 창에서 재료를 고름 — 「자재 목록」 줄 · 로직 안 자재 · 이미 있는 자재는 막음 */ + material: (m: { + 코드: string; + 이름: string | null; + 규격: string | null; + 단위: string | null; + }) => { + const why = insideKeys((calc ?? last)?.줄).has(m.코드) + ? "Labor_InLogicMat" + : cur.자재.some((x) => x.코드 === m.코드) && "Labor_AlreadyMat"; + if (why) return showToast(tc(why).replace("{value}", `${m.코드} ${m.이름 ?? ""}`), "error"); + cur.자재.push({ 코드: m.코드, 이름: m.이름, 규격: m.규격, 단위: m.단위, 수량: null }); + changed(); + }, }; const addLogic = createButton({ label: tc("AddLogic"), @@ -386,18 +399,6 @@ export function mountLaborTable( void loadLogics().then(() => changed()); }, withHopyo(library)), }); - const addMat = addMaterialButton( - tc("Labor_AddMat"), - (m) => { - const why = insideKeys((calc ?? last)?.줄).has(m.코드) - ? "Labor_InLogicMat" - : cur.자재.some((x) => x.코드 === m.코드) && "Labor_AlreadyMat"; - if (why) return showToast(tc(why).replace("{value}", `${m.코드} ${m.이름 ?? ""}`), "error"); - cur.자재.push({ 코드: m.코드, 이름: m.이름, 규격: m.규격, 단위: m.단위, 수량: null }); - changed(); - }, - opt.layer === "personal" ? { owner: library.owner } : undefined, - ); // 정보 칸 — 이름 · 구분 · 상세구분 · 단위 · 본뜬 const nameField = createInputField({ type: "text", label: tc("Lib_Name") }); @@ -461,7 +462,7 @@ export function mountLaborTable( children: [ el("h3", { text: tc("Grid_Title") }), ...(edit - ? [el("div", { className: "ui-btn-group", children: [addLogic, addMat] })] + ? [el("div", { className: "ui-btn-group", children: [addLogic] })] : []), ], }), diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index 508bbd83c..f2357ef78 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -50,7 +50,7 @@ const TEXT = { "단가산출 기초 줄을 고르면 그 원문이 보임", "Pick a logic row to see its source", ], - AddLogic: ["로직 추가", "Add logic"], + AddLogic: ["항목 추가", "Add item"], NoLogic: ["단가산출 기초 없음", "No logics yet"], Open: ["열기", "Open"], Pick_Title: ["로직 고르기", "Pick a logic"], diff --git a/resources/tester/helper_729_3_hopyo_left.cjs b/resources/tester/helper_729_3_hopyo_left.cjs index 8dc2ab7eb..e97ac3c98 100644 --- a/resources/tester/helper_729_3_hopyo_left.cjs +++ b/resources/tester/helper_729_3_hopyo_left.cjs @@ -10,6 +10,7 @@ const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api", ROUTES: {} } const STUB_FILES = [ "M02_MasterTemplete_Labor_Pick", "M02_Material_Pick", + "M02_MasterTemplete_Find_Pick", "M02_MasterTemplete_Labor_Api", "M02_MasterTemplete_Labor_Input", "M01_MasterData_UI_LogicLab_Source", diff --git a/resources/tester/helper_m02_hopyo_check.cjs b/resources/tester/helper_m02_hopyo_check.cjs index df748f5bf..0c965ed17 100644 --- a/resources/tester/helper_m02_hopyo_check.cjs +++ b/resources/tester/helper_m02_hopyo_check.cjs @@ -11,6 +11,7 @@ const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api" } }; const STUB_FILES = [ "M02_MasterTemplete_Labor_Pick", "M02_Material_Pick", + "M02_MasterTemplete_Find_Pick", "M02_MasterTemplete_Labor_Api", "M02_MasterTemplete_Labor_Input", "M01_MasterData_UI_LogicLab_Source", diff --git a/resources/tester/helper_m02_hopyo_pick.cjs b/resources/tester/helper_m02_hopyo_pick.cjs index 79ba22ba6..4f0bdba9f 100644 --- a/resources/tester/helper_m02_hopyo_pick.cjs +++ b/resources/tester/helper_m02_hopyo_pick.cjs @@ -12,6 +12,7 @@ const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api" } }; const STUB_FILES = [ "M02_MasterTemplete_Labor_Pick", "M02_Material_Pick", + "M02_MasterTemplete_Find_Pick", "M02_MasterTemplete_Labor_Api", "M02_MasterTemplete_Labor_Input", "M01_MasterData_UI_LogicLab_Source", diff --git a/resources/tester/helper_m02_hopyo_seam.cjs b/resources/tester/helper_m02_hopyo_seam.cjs index 11150be70..2970532c3 100644 --- a/resources/tester/helper_m02_hopyo_seam.cjs +++ b/resources/tester/helper_m02_hopyo_seam.cjs @@ -12,6 +12,7 @@ const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api" } }; const STUB_FILES = [ "M02_MasterTemplete_Labor_Pick", "M02_Material_Pick", + "M02_MasterTemplete_Find_Pick", "M02_MasterTemplete_Labor_Api", "M02_MasterTemplete_Labor_Input", "M01_MasterData_UI_LogicLab_Source", diff --git a/resources/tester/test_175_precombo_mats.py b/resources/tester/test_175_precombo_mats.py index 43f350d47..2868c77c4 100644 --- a/resources/tester/test_175_precombo_mats.py +++ b/resources/tester/test_175_precombo_mats.py @@ -35,7 +35,7 @@ def _save(folder, doc): def test_화면_서고_모드도_자재목록_세_칸_자재_저장(world): # noqa: F811 - """723-2 — 서고 = 일위대가표만(`Labor_Table`) · [자재 추가] 머리 · 세 칸 = 줄 모달(`Labor_RowModal`).""" + """723-2 — 서고 = 일위대가표만(`Labor_Table`) · [항목 추가] 머리(736-1 · 자재 추가 단추 걷음) · 세 칸 = 줄 모달(`Labor_RowModal`).""" read = lambda name: open(os.path.join(SRC, name), encoding="utf-8").read() # noqa: E731 detail, lib = ( read("M02_MasterTemplete_Labor_Detail.ts"), @@ -43,7 +43,7 @@ def test_화면_서고_모드도_자재목록_세_칸_자재_저장(world): # n ) modal = read("M02_MasterTemplete_Labor_RowModal.ts") assert 'if (opt.mode === "precombo") return mountLaborTable(host, doc, opt);' in detail - assert 'className: "ui-btn-group", children: [addLogic, addMat]' in lib + assert 'className: "ui-btn-group", children: [addLogic]' in lib and "addMat" not in lib assert "setLogicMark(row, at, key, v);" in modal and "setMark(o.mat, key, v);" in modal assert "delete out.자재" not in lib and "delete out.열" in lib diff --git a/resources/tester/test_721_find_pick.py b/resources/tester/test_721_find_pick.py index e22947946..7b2100c1a 100644 --- a/resources/tester/test_721_find_pick.py +++ b/resources/tester/test_721_find_pick.py @@ -43,15 +43,16 @@ def test_721_2_부르는_곳_셋만_새_창(): assert "logicOnly" not in find assert "openFindPicker({" in _text(M02 / "M02_MasterTemplete_Sangeun_Pick.ts") assert "sangeun: true," in _text(M02 / "M02_MasterTemplete_Hopyo_Pick.ts") # 일위대가 - # 옛 창 그대로 — B09 · 불러오기는 sangeun 없이 부름 + # 옛 창 그대로 — B09 단가 연결은 sangeun 없이 부름(736-1 불러오기 · 별칭 장부는 찾아 넣기 창으로) for f in ( ROOT / "B09_Estimation" / "B09_Estimation_UI_Link.ts", ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill_Link.ts", - M02 / "M02_MasterTemplete_Hopyo_Choose.ts", - M02 / "M02_MasterTemplete_Hopyo_Ledger.ts", ): text = _text(f) assert "openLogicPicker(" in text and "sangeun:" not in text, f.name + for f in ("Hopyo_Choose", "Hopyo_Ledger"): + text = _text(M02 / f"M02_MasterTemplete_{f}.ts") + assert "openFindPicker({" in text and "openLogicPicker" not in text.split("*/", 1)[1], f # 탭 셋 · 찾기 칸 하나 · 거름 고르기 없음 · M01 = 로직 · 인력 · 재료(기계 · 호표 없음) for key in ('key: "m01"', 'key: "sk"', 'key: "pre"'): assert key in find @@ -105,3 +106,27 @@ def test_721_2_인력_재료_서버가_받아_셈(): assert sk["노무비"] == pytest.approx(wage) and sk["재료비"] > 0 assert got["반영합"]["계"] == pytest.approx(sk["계"]) assert [ln["이은"] for ln in got["줄"]] == [["노무비"], ["재료비"]] + + +def test_736_1_항목_추가_찾기_창_하나(): + """736-1 — [로직 추가] → [항목 추가] · [자재 추가] 걷음 · 재료 = 자재 목록 · 자리마다 탭 막음.""" + locale = _text(ROOT / "ui_template" / "ui_template_locale_m2.ts") + assert "[로직 추가]" not in locale and "기초는 [항목 추가]로" in locale + assert 'AddLogic: ["항목 추가", "Add item"]' in _text(M02 / "M02_MasterTemplete_Labor_Text.ts") + table = _text(M02 / "M02_MasterTemplete_Labor_Table.ts") + detail = _text(M02 / "M02_MasterTemplete_Labor_Detail.ts") + for text in (table, detail): + assert "addMaterialButton" not in text and "addMat" not in text + assert "material: (m: {" in text # 재료 고름 → 「자재 목록」 + assert '"Labor_InLogicMat"' in text and '"Labor_AlreadyMat"' in text # 중복 검사 그대로 + pick = _text(M02 / "M02_MasterTemplete_Labor_Pick.ts") + assert "material: lib.material," in pick + find = _text(M02 / "M02_MasterTemplete_Find_Pick.ts") + assert 'kind === "재료" && opt.material ? opt.material : opt.element' in find + assert 'export type FindKind = "logic" | "labor" | "material" | "sk" | "pre";' in find + # 불러오기 엑셀 연결 = 기초(자재 아닌 줄) · 재료(자재 · 재료비 줄) · 별칭 장부 = 기초 · 재료 둘 + choose = _text(M02 / "M02_MasterTemplete_Hopyo_Choose.ts") + assert "다른 로직 검색" not in choose and "다른 자재 검색" not in choose + assert 'material ? [] : (["logic"] as const)' in choose + assert 'canPickMaterial(line) ? (["material"] as const) : []' in choose + assert 'kinds: ["logic", "material"],' in _text(M02 / "M02_MasterTemplete_Hopyo_Ledger.ts") diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index 5fd08cda8..605f3d987 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -517,7 +517,7 @@ def test_명사형_단추와_별칭_장부_풀이_359(): assert hint in ledger and "note(`${LEDGER_HINT}" in ledger side = _text("M02_MasterTemplete_Hopyo_Side.ts") assert "title: LEDGER_HINT" in side - for label in ('"변경 취소"', '"로직 추가"', '"내 층 복사"', '"삭제"'): + for label in ('"변경 취소"', '"항목 추가"', '"내 층 복사"', '"삭제"'): assert f"label: {label}" in ledger, label assert '"우리에 없음": "로직없음"' in ledger and '"안 씀": "사용안함"' in ledger mine = ("Hopyo_Side", "Hopyo_Sheet", "Hopyo_Check", "Hopyo_CheckRows", "Hopyo_Ledger") diff --git a/resources/tester/test_m02_material_own.py b/resources/tester/test_m02_material_own.py index 6c33074c0..82581f67f 100644 --- a/resources/tester/test_m02_material_own.py +++ b/resources/tester/test_m02_material_own.py @@ -228,4 +228,4 @@ def test_화면_신규_자재는_머리_단추_개인_층만(): "`/layers/personal/materials" in pick and "키:" not in pick.split("JSON.stringify")[1][:200] ) detail = open(os.path.join(here, "M02_MasterTemplete_Labor_Detail.ts"), encoding="utf-8").read() - assert 'opt.layer === "personal" ? { owner: library.owner } : undefined' in detail + assert "addMaterialButton" not in detail # 736-1 [자재 추가] 단추 걷음 — 재료는 찾아 넣기 창 diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index 50f15f9e3..4ffc66e74 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -306,7 +306,7 @@ export const ui_locales_m2 = { M02_GridLink: ["연결", "Link"], M02_GridLinkTitle: ["안 이은 줄 연결", "Link unlinked rows"], M02_GridLinkNoLogic: [ - "고정 줄은 노임 · 자재 · 기계로 이음 — 기초는 [로직 추가]로 줄을 바꿈", + "고정 줄은 노임 · 자재 · 기계로 이음 — 기초는 [항목 추가]로 줄을 바꿈", "Fixed rows link to labor, material or machine only", ], M02_LibUnlinked: ["안 이은 줄", "Unlinked"], diff --git a/ui_template/ui_template_ui_kit_parts.ts b/ui_template/ui_template_ui_kit_parts.ts index 8c77a6813..dc1fa6582 100644 --- a/ui_template/ui_template_ui_kit_parts.ts +++ b/ui_template/ui_template_ui_kit_parts.ts @@ -303,8 +303,8 @@ function tabsDemo(s: KitState): HTMLElement | null { // prettier-ignore export const KIT_PARTS: KitPart[] = [ // ── 552-2 기본 ───────────────────────────────────────────── - { key: "button", name: "단추", group: "basic", kinds: "주 · 보조 · 고스트 글 · 위험 · 위험 고스트 · 유리 · 글 링크 · 여덟 단", file: "ui_template_button.ts", - demo: (s) => (s === "error" ? null : row(...([["저장", "filled"], ["취소", "ghost"], ["보기", "pill"], ["삭제", "danger"], ["초기화", "danger-ghost"], ["사용법", "link"]] as const).map(([label, variant]) => markState(createButton({ label, variant, size: 28, onClick: noop }), s)))), + { key: "button", name: "단추", group: "basic", kinds: "주 · 보조 · 고스트 글 · 위험 · 위험 고스트 · 유리 · 글 링크 · 갈래 색(옵션값 · 요소 연결) · 여덟 단", file: "ui_template_button.ts", + demo: (s) => (s === "error" ? null : row(...([["저장", "filled"], ["취소", "ghost"], ["보기", "pill"], ["삭제", "danger"], ["초기화", "danger-ghost"], ["사용법", "link"]] as const).map(([label, variant]) => markState(createButton({ label, variant, size: 28, onClick: noop }), s)), ...([["옵션값", "option"], ["요소 연결", "link"]] as const).map(([label, tone]) => markState(createButton({ label, variant: "ghost", size: 20, tone, onClick: noop }), s)))), sizes: (z) => createButton({ label: String(z), size: z, iconStart: "plus", onClick: noop }) }, { key: "icon-button", name: "아이콘 단추", group: "basic", kinds: "고스트 · 선 · 채움 · 유리 · 켜짐(aria-pressed) · 글자 기호 · 여덟 단 · title 필수", file: "ui_template_button.ts", demo: (s) => (s === "error" ? null : row(markState(createIconButton({ icon: "settings", title: "설정" }), s), markState(createIconButton({ icon: "refresh", title: "새로 고침", variant: "outline" }), s), markState(createIconButton({ icon: "plus", title: "추가", variant: "filled" }), s), markState(createIconButton({ icon: "eye", title: "보기", pressed: true }), s), markState(createIconButton({ glyph: "▲", title: "위로", variant: "outline" }), s), markState(createIconButton({ icon: "adjustments", title: "조정", variant: "glass" }), s))),