From dda5f6233f5fb3f6731c4debb2f7d0cd62a1b268 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:49:43 +0900 Subject: [PATCH 01/11] =?UTF-8?q?=EF=BB=BFfeat(M02):=20=EC=84=9C=EA=B3=A0?= =?UTF-8?q?=20=EA=B5=AC=EB=B6=84=20=C2=B7=20=EC=83=81=EC=84=B8=EA=B5=AC?= =?UTF-8?q?=EB=B6=84=20=C2=B7=20=EC=97=B0=EA=B2=B0=20=ED=99=94=EB=A9=B4(51?= =?UTF-8?q?-12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 서고 목록표에 구분 · 상세구분 칸(키 뒤) - 단가승격 상세 정보 칸 = 구분 · 상세구분 · 연결(원 로직 키 · 출처) 읽기만 - 사전조합 상세 정보 칸 = 구분 · 상세구분 고르기(로직 구분 목록 logic-lib/subs · 구분 바꾸면 상세구분 = 그 첫 줄) - 새 사전조합 처음 값 = 로직 구분 목록 첫 줄 - 로직 고르기 창 구분 · 상세구분 거름에 승격 · 사전조합 줄도 걸림 - 화면 시나리오 51-12 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- .../M02_MasterTemplete_Labor_Api.ts | 3 + .../M02_MasterTemplete_Labor_Detail.ts | 80 +++++++++- .../M02_MasterTemplete_Labor_Pick.ts | 12 +- .../M02_MasterTemplete_Labor_Text.ts | 2 + .../M02_MasterTemplete_Library_List.ts | 53 +++++-- resources/tester/ui_checks/51-12_서고_구분.js | 142 ++++++++++++++++++ 6 files changed, 275 insertions(+), 17 deletions(-) create mode 100644 resources/tester/ui_checks/51-12_서고_구분.js diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts index ebf4db18c..d4c4bf6c6 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts @@ -233,6 +233,9 @@ export interface LibraryRow { 이름: string; 제목?: string | null; 단위?: string | null; + /** 51-12 — 승격 = 원 로직 것 · 사전조합 = 고른 것 */ + 구분?: string | null; + 상세구분?: string | null; } /** 서고 목록 — 그 층(주인) 단가승격 `promote` · 사전조합 `precombo` 문서 */ export const fetchLibrary = async ( diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index d0bbc892b..e38ba6874 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -9,15 +9,23 @@ * · 같은 로직 두 번 · 같은 자재 두 번 막음 · 빈 일위대가도 저장 됨 * · 오른쪽 「원문」 접는 패널(47-4 · 상세만 · 공용 `ui_template_side_panels`) = 고른 줄 로직의 출처 원문 * · 서고 모드(50-3 · `mode`) = 단가승격 · 사전조합 — 이름 칸 · 단가산출 로직 표 · 원문만(자재 · 면적 · 다이어그램 없음) · + * 정보 칸 구분 · 상세구분(51-12 · 승격 = 읽기만 + 연결 · 사전조합 = 고름) · * 승격은 로직 한 줄(입력값 고정 · 더하기 · 순서 · 수량 없음) · 사전조합 = 일위대가 상세와 같은 [로직 추가] · 표 · 동작(51-11) · 수량 손 입력(기본 1) * ========================================================================== */ -import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements"; +import { + createButton, + createInputField, + createSelectField, + el, + showToast, +} from "@ui/ui_template_elements"; import { createSidePanels } from "@ui/ui_template_side_panels"; import { ROUTES } from "@config/config_frontend"; import { navigateTo } from "../A00_Common/router"; import { setPendingLogicKey } from "../M01_MasterData/M01_MasterData_UI_Side"; import { createSourceView } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Source"; +import type { SubBrief } from "../M01_MasterData/M01_MasterData_UI_Logic_Api"; import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet"; import { recalcWorkbook } from "../A00_Common/spreadsheet/spreadsheet_recalc"; import { @@ -33,6 +41,7 @@ import { lineId, settle, type LaborRef } from "./M02_MasterTemplete_Labor_Calc"; import { areaLinks, calcLabor, + fetchLibLogicSubs, fetchLogicInputs, normalizeLabor, type LaborCalc, @@ -457,6 +466,69 @@ export function mountLaborDetail( opt.onChanged(dirty()); }); + /** 서고 구분 · 상세구분(51-12) — 승격 = 원 로직 것 읽기만 + 연결(원 로직 키 · 출처) · + * 사전조합 = 로직 구분 목록(logic-lib/subs)에서 고름(구분 바꾸면 상세구분 = 그 첫 줄) */ + const meta = cur as { 구분?: string | null; 상세구분?: string | null; 연결?: unknown }; + const readField = (label: string, field: string, value: string): HTMLElement => { + const f = createInputField({ type: "text", label }); + f.root.dataset.laborField = field; + f.input.value = value; + f.input.readOnly = true; + return f.root; + }; + const libMeta = (): HTMLElement[] => { + if (promote) { + const link = (meta.연결 ?? {}) as { 로직?: string; 키?: string; 출처?: string }; + return [ + readField(tc("Sub"), "구분", meta.구분 ?? ""), + readField(tc("Detail"), "상세구분", meta.상세구분 ?? ""), + readField( + tc("Lib_Link"), + "연결", + [link.로직 ?? link.키, link.출처].filter(Boolean).join(" · "), + ), + ]; + } + const sub = createSelectField({ options: [], label: tc("Sub"), disabled: !edit }); + const detail = createSelectField({ options: [], label: tc("Detail"), disabled: !edit }); + sub.root.dataset.laborField = "구분"; + detail.root.dataset.laborField = "상세구분"; + let subs: SubBrief[] = []; + // 목록에 없는 지금 값도 선택지로 둠 + const opts = (names: string[], now: string | null | undefined) => + [...new Set([...(now ? [now] : []), ...names])].map((v) => ({ value: v, text: v })); + const fill = (): void => { + sub.setOptions( + opts( + subs.map((s) => s.name), + meta.구분, + ), + meta.구분 ?? "", + ); + const details = subs.find((s) => s.name === meta.구분)?.details ?? []; + detail.setOptions(opts(details, meta.상세구분), meta.상세구분 ?? ""); + }; + sub.select.addEventListener("change", () => { + meta.구분 = sub.select.value; + meta.상세구분 = subs.find((s) => s.name === meta.구분)?.details[0] ?? null; + fill(); + opt.onChanged(dirty()); + }); + detail.select.addEventListener("change", () => { + meta.상세구분 = detail.select.value; + opt.onChanged(dirty()); + }); + fill(); + void fetchLibLogicSubs().then( + (got) => { + subs = got; + fill(); + }, + () => undefined, + ); + return [sub.root, detail.root]; + }; + const editor = (body: HTMLElement): HTMLElement => side ? el("div", { className: "ui-side-panels-row", children: [body, side.root] }) : body; host.replaceChildren( @@ -473,7 +545,11 @@ export function mountLaborDetail( children: [ el("div", { className: "m01lab__info", - children: [...(lib ? [nameField.root] : []), unit.root, origin.root], + children: [ + ...(lib ? [nameField.root, ...libMeta()] : []), + unit.root, + origin.root, + ], }), ], }), diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts index 445b48d45..af891ca02 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts @@ -82,9 +82,13 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra (!detail.select.value || x.상세구분 === detail.select.value) && (!q || `${x.키} ${x.이름}`.toLowerCase().includes(q)), ); - const libFound = sub.select.value - ? [] - : libRows.filter((x) => !q || `${x.row.이름} ${x.row.제목 ?? ""}`.toLowerCase().includes(q)); + // 서고 줄도 구분 · 상세구분으로 거름(51-12) + const libFound = libRows.filter( + (x) => + (!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; finder.hint.textContent = loading @@ -110,7 +114,7 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra key: x.row.이름, tag: tc(x.kind), name: x.row.제목 || x.row.이름, - sub: x.row.이름, + sub: `${x.row.이름} · ${x.row.구분 ?? ""} ${x.row.상세구분 ?? ""}`.trim(), unit: x.row.단위 ?? "", onPick: () => { close(); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index c7be730db..9ac8e5424 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -124,6 +124,8 @@ const TEXT = { Lib_Unit: ["단위", "Unit"], Lib_Base: ["기준 금액", "Base amount"], Lib_Date: ["고친 날", "Modified"], + // 서고 구분 · 연결(51-12) + Lib_Link: ["연결", "Linked logic"], } as const satisfies Record; export type ComboTextKey = keyof typeof TEXT; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts b/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts index 3a183094e..4e729405d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts @@ -1,7 +1,7 @@ /* ============================================================================= * M02_MasterTemplete_Library_List.ts * 일위대가 조합 서고 목록표(51-4) — 좌측 항목(단가산출 승격 · 일위대가 사전조합)을 누르면 상세 칸에 뜸. - * 표 = 고름 · 키 · 이름 · 단위 · 기준 금액(서버 1 단위 계) · 고친 날 · 줄 누름 = 그 문서 상세. + * 표 = 고름 · 키 · 구분 · 상세구분(51-12) · 이름 · 단위 · 기준 금액(서버 1 단위 계) · 고친 날 · 줄 누름 = 그 문서 상세. * 머리 [생성] · [복사](고른 줄) · [삭제](고른 줄 · 쓰는 곳 있으면 서버 409 까닭 그대로). * 꼴 = M01 단가산출 로직 목록 — 찾기 줄(`buildHeadFilters`) · 쪽(`buildPager`) · 표(`m01-master__grid`) 를 M01 에서 import. * ========================================================================== */ @@ -30,12 +30,19 @@ import { type LibraryBody, type TemplateInfo, } from "./M02_MasterTemplete_Api_Fetch"; +import { fetchLibLogicSubs } from "./M02_MasterTemplete_Labor_Api"; import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import type { PrecomboKind } from "./M02_MasterTemplete_UI_Side_Precombo"; /** 목록 줄 — 서버가 문서 `이름`(제목) · 단위 · 1 단위 기준 계를 실어 보냄 */ -type Row = TemplateInfo & { 제목?: string | null; 단위?: string | null; 기준금액?: number | null }; +type Row = TemplateInfo & { + 제목?: string | null; + 단위?: string | null; + 기준금액?: number | null; + 구분?: string | null; + 상세구분?: string | null; +}; export interface LibraryListOptions { layer: Layer; @@ -99,9 +106,15 @@ export function mountLibraryList( attrs: { "data-lib-key": row.이름 }, children: [ pick, - ...[row.이름, row.제목 ?? "", row.단위 ?? "", money(row.기준금액), row.수정일].map( - (v) => el("td", { text: v, attrs: { title: v } }), - ), + ...[ + row.이름, + row.구분 ?? "", + row.상세구분 ?? "", + row.제목 ?? "", + row.단위 ?? "", + money(row.기준금액), + row.수정일, + ].map((v) => el("td", { text: v, attrs: { title: v } })), ], }); tr.addEventListener("click", () => opt.open(row.이름, libraryLabel(row))); @@ -109,7 +122,7 @@ export function mountLibraryList( }) : [ el("tr", { - children: [el("td", { text: L("M02_NoTemplates"), attrs: { colspan: "6" } })], + children: [el("td", { text: L("M02_NoTemplates"), attrs: { colspan: "8" } })], }), ]), ); @@ -164,7 +177,8 @@ export function mountLibraryList( const create = async (body: LibraryBody, label: string): Promise => made((await newLibrary(opt.layer, opt.kind, body)).이름, label); - /** 단가승격 = 단가산출 로직 고르기 → 이름 · 단위는 로직 것 · 사전조합 = 이름만 · 빈 묶음 */ + /** 단가승격 = 단가산출 로직 고르기 → 이름 · 단위 · 구분 · 연결은 로직 것(서버가 채움) · + * 사전조합 = 이름 + 구분 · 상세구분 처음 값 = 로직 구분 목록 첫 줄(51-12 · 상세에서 바꿈) · 빈 묶음 */ btnNew.addEventListener("click", () => { if (opt.kind === "promote") return openLogicPicker((x) => { @@ -177,9 +191,17 @@ export function mountLibraryList( showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error"), ); }); - nameDialog(L("M02_NewTitle"), "", (name) => - create({ 문서: { 이름: name, 단위: null, 담은로직: [] } }, name), - ); + nameDialog(L("M02_NewTitle"), "", async (name) => { + const first = (await fetchLibLogicSubs())[0]; + const 문서 = { + 이름: name, + 구분: first?.name ?? null, + 상세구분: first?.details[0] ?? null, + 단위: null, + 담은로직: [], + }; + await create({ 문서 }, name); + }); }); /** 복사 = 같은 층 새 키(서버 · 본뜬 = 원래 키) → 고른 이름으로 고쳐 씀 */ @@ -221,7 +243,16 @@ export function mountLibraryList( page = 1; draw(); }); - const heads = ["", "Lib_Key", "Lib_Name", "Lib_Unit", "Lib_Base", "Lib_Date"] as const; + const heads = [ + "", + "Lib_Key", + "Sub", + "Detail", + "Lib_Name", + "Lib_Unit", + "Lib_Base", + "Lib_Date", + ] as const; host.replaceChildren( buildHeadFilters([searchField.root, pager]), el("div", { diff --git a/resources/tester/ui_checks/51-12_서고_구분.js b/resources/tester/ui_checks/51-12_서고_구분.js new file mode 100644 index 000000000..87d16fb80 --- /dev/null +++ b/resources/tester/ui_checks/51-12_서고_구분.js @@ -0,0 +1,142 @@ +/* 51-12 — 서고(단가승격 · 사전조합) 구분 · 상세구분 · 연결 + * 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=일위대가조합&root=master) · 시스템 관리자 + * 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌) + * 쓰기 없음 — GET · calc 밖 요청은 가짜 400(새로 만들기 POST 본문만 잡음) · 마스터 SP-000001 · SC-000001 읽기만 + * 기대 수치: + * - heads = ["", "키", "구분", "상세구분", "이름", ...] · listSP = SP-000001 줄 구분 · 상세구분 빈 것 아님 + * - promote = { 구분, 상세구분, 연결 } 셋 다 글 있음 · readOnly = true + * - precombo = { selects: 2, 구분 = 문서 값 · subOptions ≥ 1 } · switched.상세구분 = 바꾼 구분의 첫 상세구분 · back = 처음 값 + * - created = { 구분: 목록 첫 구분, 상세구분: 그 첫 상세구분 } + * - picker = 구분(SP-000001 것) 고르면 SP-000001 줄 보임 · 다른 구분이면 안 보임 + */ +(async () => { + const wait = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 6000) => { + for (let t = 0; t < ms; t += 100) { + const v = fn(); + if (v) return v; + await wait(100); + } + return null; + }; + const json = (body, status = 200) => + new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } }); + const real = window.fetch; + let posted = null; + window.fetch = async (input, init = {}) => { + const url = String(input?.url ?? input); + const method = (init.method ?? "GET").toUpperCase(); + if (method === "POST" && url.includes("/library/precombo")) { + posted = JSON.parse(init.body); + return json({ detail: "시험 — 쓰기 막음" }, 400); + } + if (method !== "GET" && !url.includes("/calc")) + return json({ detail: "시험 — 쓰기 막음" }, 400); + return real(input, init); + }; + const sec = [...document.querySelectorAll(".m02-side__lists > section")].find( + (s) => s.querySelector(".ui-collapsible__title")?.textContent?.trim() === "일위대가 조합", + ); + const items = [...sec.querySelectorAll("[data-precombo]")]; + const list = (kind) => document.querySelector(`[data-lib-list="${kind}"]`); + const edit = () => document.querySelector(".m02-main__host [data-labor]"); + const field = (name) => edit()?.querySelector(`[data-labor-field="${name}"]`); + const btn = (root, text) => + [...(root?.querySelectorAll("button") ?? [])].find((b) => b.textContent === text); + const subs = await ( + await real("/api/m02/logic-lib/subs", { credentials: "include" }) + ) + .json() + .then((d) => d.subs); + try { + // ① 승격 목록 · 상세 + items[0].click(); + const spRow = await until(() => list("promote")?.querySelector('[data-lib-key="SP-000001"]')); + const heads = [...list("promote").querySelectorAll("thead th")].map((th) => th.textContent); + const cells = [...spRow.querySelectorAll("td")].map((td) => td.textContent); + const listSP = { 구분: cells[2], 상세구분: cells[3] }; + spRow.querySelector("td:nth-child(5)").click(); + await until(() => field("연결")); + const val = (name) => field(name)?.querySelector("input")?.value ?? null; + const promote = { + 구분: val("구분"), + 상세구분: val("상세구분"), + 연결: val("연결"), + readOnly: ["구분", "상세구분", "연결"].every( + (n) => field(n)?.querySelector("input")?.readOnly, + ), + }; + // ② 사전조합 상세 — 고르기 · 바꿔 보고 되돌림 + items[1].click(); + ( + await until(() => + list("precombo")?.querySelector('[data-lib-key="SC-000001"] td:nth-child(5)'), + ) + ).click(); + const subSel = await until(() => field("구분")?.querySelector("select")); + await until(() => subSel.options.length > 1, 4000); + const detSel = field("상세구분").querySelector("select"); + const start = { 구분: subSel.value, 상세구분: detSel.value }; + const precombo = { + selects: edit().querySelectorAll(".m01lab__info select").length, + ...start, + subOptions: subSel.options.length, + }; + const other = subs.find((s) => s.name !== start.구분 && s.details.length); + subSel.value = other.name; + subSel.dispatchEvent(new Event("change")); + const switched = { + 구분: field("구분").querySelector("select").value, + 상세구분: field("상세구분").querySelector("select").value, + want: other.details[0], + }; + const s2 = field("구분").querySelector("select"); + s2.value = start.구분; + s2.dispatchEvent(new Event("change")); + const d2 = field("상세구분").querySelector("select"); + d2.value = start.상세구분; + d2.dispatchEvent(new Event("change")); + const back = { + 구분: field("구분").querySelector("select").value, + 상세구분: field("상세구분").querySelector("select").value, + }; + // ③ 고르기 창 — 구분 거름에 승격 줄 + btn(edit(), "로직 추가").click(); + const pick = await until(() => document.querySelector(".m01-logic__pick")); + await until(() => pick.querySelector('[data-pick="SP-000001"]'), 20000); + const [fSub, fDet] = pick.querySelectorAll(".ui-find__filters select"); + const pickerWith = async (s, d) => { + fSub.value = s; + fSub.dispatchEvent(new Event("change")); + if (d != null) { + fDet.value = d; + fDet.dispatchEvent(new Event("change")); + } + await wait(100); + return !!pick.querySelector('[data-pick="SP-000001"]'); + }; + const picker = { + same: await pickerWith(promote.구분, promote.상세구분), + other: await pickerWith(subs.find((s) => s.name !== promote.구분)?.name ?? "", null), + }; + pick.closest(".m01-logic__backdrop")?.remove(); + // ④ 새 사전조합 처음 값(POST 본문만 잡음) + items[1].click(); + await until(() => list("precombo")); + btn(document.querySelector(".m02-main__slot"), "생성").click(); + const box = await until(() => document.querySelector(".m02-modal")); + box.querySelector("input").value = "시험 51-12"; + btn(box, "생성").click(); + await until(() => posted); + btn(document.querySelector(".m02-modal"), "닫기")?.click(); + document.querySelector(".m02-modal")?.closest(".ui-modal__backdrop, .ui-modal")?.remove(); + const created = { + 구분: posted?.문서?.구분, + 상세구분: posted?.문서?.상세구분, + want: [subs[0].name, subs[0].details[0] ?? null], + }; + return JSON.stringify({ heads, listSP, promote, precombo, switched, back, picker, created }); + } finally { + window.fetch = real; + } +})(); From e66c92f56294da2c15c4baf23af81a001b9dccb6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:50:05 +0900 Subject: [PATCH 02/11] =?UTF-8?q?chore(sync):=20=EA=B3=B5=EC=9C=A0=20docs?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EB=8B=B4=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- docs/raw/사용자요구/2026-10-05_수정요구.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index 14f1cf7f7..4f385fa04 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -112,3 +112,5 @@ 단가산출과 일위대가 사전조합에서 테이블의 양식도 M01에 공통테이블 양식이 있을것같고 버튼들도 M01에 템플릿이 있을거야. 확인해봐 그리고 사전조합의 경우에 검색은 별도의 창이 아니라 구조물 조합의 일위대가 테이블의 조건 추가와 같은 기능이어야. 일위대가 사전조합에서 나오는 모당창의 형식 뿐만아니라 내용도 구조물조합의 일위대가의 상세 화면에서 로직 추가와 같아야된다고 선택되는 항목도 + +어차피 신규로 키값을 만들지 않았음? 그럼 당연히 해당 연결정보와 구분, 상세구분을 만들어야하지 않음? From ce211f779bd872805f3c37a0e767639f67eaabfd Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:52:53 +0900 Subject: [PATCH 03/11] =?UTF-8?q?fix(M02):=20=EC=82=AC=EC=A0=84=EC=A1=B0?= =?UTF-8?q?=ED=95=A9=20=EA=B5=AC=EB=B6=84=20=EB=B9=88=20=EA=B0=92=20=3D=20?= =?UTF-8?q?=E3=80=8C=E2=80=94=E3=80=8D=20=EC=84=A0=ED=83=9D=EC=A7=80=20?= =?UTF-8?q?=C2=B7=20=EC=97=B0=EA=B2=B0=20=3D=20{=EB=A1=9C=EC=A7=81,=20?= =?UTF-8?q?=EC=B6=9C=EC=B2=98}(51-12=20=EC=84=9C=EB=B2=84=20=EC=9D=B4?= =?UTF-8?q?=EC=9D=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 옛 사전조합(구분 빈 글)이 첫 구분으로 보이던 어긋남 없앰 - 화면 시나리오 51-12 [만들기] 단추 이름 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- .../M02_MasterTemplete_Labor_Detail.ts | 14 ++++++-------- resources/tester/ui_checks/51-12_서고_구분.js | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index e38ba6874..31e8cd9b9 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -478,15 +478,11 @@ export function mountLaborDetail( }; const libMeta = (): HTMLElement[] => { if (promote) { - const link = (meta.연결 ?? {}) as { 로직?: string; 키?: string; 출처?: string }; + const link = (meta.연결 ?? {}) as { 로직?: string; 출처?: string }; return [ readField(tc("Sub"), "구분", meta.구분 ?? ""), readField(tc("Detail"), "상세구분", meta.상세구분 ?? ""), - readField( - tc("Lib_Link"), - "연결", - [link.로직 ?? link.키, link.출처].filter(Boolean).join(" · "), - ), + readField(tc("Lib_Link"), "연결", [link.로직, link.출처].filter(Boolean).join(" · ")), ]; } const sub = createSelectField({ options: [], label: tc("Sub"), disabled: !edit }); @@ -494,9 +490,11 @@ export function mountLaborDetail( sub.root.dataset.laborField = "구분"; detail.root.dataset.laborField = "상세구분"; let subs: SubBrief[] = []; - // 목록에 없는 지금 값도 선택지로 둠 + // 목록에 없는 지금 값(빈 것 = 「—」 · 옛 문서)도 선택지로 둠 const opts = (names: string[], now: string | null | undefined) => - [...new Set([...(now ? [now] : []), ...names])].map((v) => ({ value: v, text: v })); + [...new Set([now ?? "", ...names])] + .filter((v, i) => v || i === 0) + .map((v) => ({ value: v, text: v || "—" })); const fill = (): void => { sub.setOptions( opts( diff --git a/resources/tester/ui_checks/51-12_서고_구분.js b/resources/tester/ui_checks/51-12_서고_구분.js index 87d16fb80..52e315697 100644 --- a/resources/tester/ui_checks/51-12_서고_구분.js +++ b/resources/tester/ui_checks/51-12_서고_구분.js @@ -126,7 +126,7 @@ btn(document.querySelector(".m02-main__slot"), "생성").click(); const box = await until(() => document.querySelector(".m02-modal")); box.querySelector("input").value = "시험 51-12"; - btn(box, "생성").click(); + btn(box, "만들기").click(); await until(() => posted); btn(document.querySelector(".m02-modal"), "닫기")?.click(); document.querySelector(".m02-modal")?.closest(".ui-modal__backdrop, .ui-modal")?.remove(); From 74af9f8d9941c6bb1b465b5dee0dc8d4cd4239ae Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:52:54 +0900 Subject: [PATCH 04/11] =?UTF-8?q?chore(sync):=20=EA=B3=B5=EC=9C=A0=20docs?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EB=8B=B4=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- docs/raw/사용자요구/2026-10-05_수정요구.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index 4f385fa04..292d015d7 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -114,3 +114,5 @@ 일위대가 사전조합에서 나오는 모당창의 형식 뿐만아니라 내용도 구조물조합의 일위대가의 상세 화면에서 로직 추가와 같아야된다고 선택되는 항목도 어차피 신규로 키값을 만들지 않았음? 그럼 당연히 해당 연결정보와 구분, 상세구분을 만들어야하지 않음? + +다음으로 구조물 조합에서 일위대가의 상세페이지에서 테이블 행 선택에서 단가승격이야 단가산출과 같은 항목이라 문제가 없는데 일위대가 사전조합의 경우에는 전체를 다 표현하는데 문제가 있어. 일위조합인 경우에는 조합의 세부항목을 기준으로 노무비, 재료비, 경비를 값을 가져와서 사용하면 가능하지 않을까? From 86d4f3e88260568555bd7a06cb1d047f3ee71632 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:55:11 +0900 Subject: [PATCH 05/11] =?UTF-8?q?fix(M02):=2051-13=20=EC=9D=BC=EC=9C=84?= =?UTF-8?q?=EC=A1=B0=ED=95=A9=20=EC=A4=84=20=ED=95=98=EC=9C=84=20=ED=96=89?= =?UTF-8?q?=20=E2=80=94=20=EC=84=B8=EB=B6=80=20=ED=95=AD=EB=AA=A9=20?= =?UTF-8?q?=ED=95=9C=20=EC=B8=B5(=EC=B0=B8=EC=A1=B0=20=EC=A4=84=20=C2=B7?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=A4=84)=20=EB=85=B8=EB=AC=B4?= =?UTF-8?q?=EB=B9=84=20=C2=B7=20=EC=9E=AC=EB=A3=8C=EB=B9=84=20=C2=B7=20?= =?UTF-8?q?=EA=B2=BD=EB=B9=84=20=3D=20=EC=95=88=20=EC=A4=84=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=20=C3=97=20=EC=95=88=20=EC=A4=84=20=EC=88=98=EB=9F=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 하위 행 이름이 참조 키만 읽어 로직 안 줄(51-11)에서 표가 깨짐 → 참조 키 없으면 로직 키 - calc 답 「안」 타입에 로직 줄 자리 추가 - 확인 시나리오 resources/tester/ui_checks/51-13_일위조합_하위행.js (calc 답 바꿔 끼움 · 서고 저장 없음) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7 --- .../M02_MasterTemplete_Labor_Calc.ts | 6 +- .../M02_MasterTemplete_Labor_Rows.ts | 19 ++- .../tester/ui_checks/51-13_일위조합_하위행.js | 132 ++++++++++++++++++ 3 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 resources/tester/ui_checks/51-13_일위조합_하위행.js diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts index 0e8ee2a49..54e3b3e2e 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts @@ -36,11 +36,13 @@ export interface CalcLine { 결과?: unknown; /** 그 로직 재료 · 노무 · 경비 줄(M01 시험계산 표와 같은 모양 · 50-4) */ 줄?: LogicLine[]; - /** 참조 줄 — 가리킨 문서 이름 · 단가승격의 원 로직 키 · 사전조합 안 줄(한 층 · 50-3 ④) */ + /** 참조 줄 — 가리킨 문서 이름 · 단가승격의 원 로직 키 · 사전조합 안 줄(한 층 · 50-3 ④ · + * 참조 줄 = `참조` · 로직 줄(51-11) = `로직`) */ 이름?: string | null; 원로직?: string | null; 안?: { - 참조: LaborRef; + 참조?: LaborRef; + 로직?: string; 수량: number; 이름?: string | null; 단위?: string | null; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts index 94b8ed902..5d108131b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts @@ -229,17 +229,22 @@ export interface LogicTableData { } /** 고른 줄 하위 행(51-2 · 같은 표 · 같은 칸) — 단가산출 · 단가승격 = 그 로직 재료 · 노무 · 경비 줄 · - * 사전조합 = 안 줄 · 금액 칸 = 위 기준(위 줄 기준과 같은 바탕) / 아래 반영(× 위 줄 수량 ÷ 배) · + * 일위조합 = 세부 항목 한 층(51-13 · 참조 줄 · 로직 줄 · 안을 다시 펼치지 않음 · 금액 = 안 줄 기준 × 안 줄 수량) · + * 금액 칸 = 위 기준(위 줄 기준과 같은 바탕) / 아래 반영(× 위 줄 수량 ÷ 배) · * 하위 합 = 위 줄 금액 · 줄이 없으면 빈 한 줄(「…」 · 까닭 · 「—」) */ function subRows(line: LaborCalcLine | undefined, wait: boolean, tools: boolean): HTMLElement[] { const times = line?.수량 === null || line?.수량 === undefined ? null : line.수량 / (line.배 || 1); const items = line?.안 - ? line.안.map((x) => ({ - name: `${x.참조.키} ${x.이름 ?? ""}${x.까닭 ? ` · ${x.까닭}` : ""}`.trim(), - qty: x.수량, - unit: x.단위 ?? "", - cost: x.기준 ? Object.fromEntries(COSTS.map((c) => [c, x.기준![c] * x.수량])) : null, - })) + ? line.안.map((x) => { + const key = x.참조?.키 ?? x.로직 ?? ""; + const label = x.이름 && x.이름 !== key ? `${key} ${x.이름}` : key; // 로직 줄 이름 없으면 키만 + return { + name: `${label}${x.까닭 ? ` · ${x.까닭}` : ""}`, + qty: x.수량, + unit: x.단위 ?? "", + cost: x.기준 ? Object.fromEntries(COSTS.map((c) => [c, x.기준![c] * x.수량])) : null, + }; + }) : (line?.줄 ?? []).map((x) => ({ name: x.출처 ? `${x.이름} (${x.출처.replace(/[A-Z]{2}\d{6}\./g, "")})` : x.이름, qty: x.수량, diff --git a/resources/tester/ui_checks/51-13_일위조합_하위행.js b/resources/tester/ui_checks/51-13_일위조합_하위행.js new file mode 100644 index 000000000..c40dc1b7b --- /dev/null +++ b/resources/tester/ui_checks/51-13_일위조합_하위행.js @@ -0,0 +1,132 @@ +/* 51-13 — 일위대가 표 구분 「일위조합」 줄 누름 = 하위 행 = 그 조합 세부 항목 한 층(참조 줄 · 로직 줄 51-11) — 카드 · 상세 두 곳 + * 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자 · + * 나무에 item3(단가승격 SP-000001 · 일위조합 SC-000001 줄이 있음) + * 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌) + * 쓰기 없음 — item3 calc 답만 바꿔 끼움(fetch · 끝에 되돌림): SC 줄 「안」 = [SP-000001 × 2 · 첫 단가산출 로직 줄 × 1.5] + * (안 줄 기준 = 같은 답의 그 줄 기준 · SC 기준 = 안 기준 × 수량 합 · SC 수량 손 3 · 반영 = 기준 × 3 · 서고 저장 없음) + * 기대 수치(카드 · 상세 같음): + * - sc.subs = sc.inner(2) · sc.cells = [head] · sc.next = true + * - sc.base ≈ sc.subBase(위 줄 기준 = 하위 합 · 반올림 차 ≤ 2) · sc.applied ≈ sc.subApplied ≈ sc.base × 3 + * - sc.names[0] 「SP-000001 …」 · sc.names[1] 「<로직 키>」(로직 줄 · 깨지지 않음) + * - sc.qty = ["2", "1.5"](안 줄 수량) · sp.subs ≥ 1(단가승격 하위 행 그대로) + */ +(async () => { + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 8000) => { + for (let t = 0; t < ms; t += 150) { + const v = fn(); + if (v) return v; + await w(150); + } + return null; + }; + const SC = "참조|system||SC-000001"; + const SP = "참조|system||SP-000001"; + const KEYS = ["노무비", "재료비", "경비", "계"]; + const real = window.fetch; + let inner = 0; + window.fetch = async (input, init) => { + const res = await real(input, init); + const url = typeof input === "string" ? input : input.url; + if (!/\/labor\/item3\/calc/.test(url) || !res.ok) return res; + const j = await res.clone().json(); + const sc = j.줄.find((l) => l.참조?.키 === "SC-000001"); + const sp = j.줄.find((l) => l.참조?.키 === "SP-000001"); + const lg = j.줄.find((l) => !l.참조 && l.기준); + if (sc && sp?.기준 && lg) { + sc.안 = [ + { 참조: sp.참조, 수량: 2, 이름: sp.이름, 단위: sp.분모, 기준: sp.기준, 까닭: null }, + { 로직: lg.로직, 수량: 1.5, 이름: lg.로직, 단위: null, 기준: lg.기준, 까닭: null }, + ]; + sc.기준 = Object.fromEntries(KEYS.map((k) => [k, sp.기준[k] * 2 + lg.기준[k] * 1.5])); + Object.assign(sc, { 수량: 3, 수량출처: "손" }); // 위 줄 수량 3 — 반영 = 기준 × 3 + sc.반영 = Object.fromEntries(KEYS.map((k) => [k, sc.기준[k] * 3])); + inner = sc.안.length; + } + return new Response(JSON.stringify(j), { status: 200, headers: res.headers }); + }; + const n = (s) => Number(String(s ?? "").replace(/[^0-9.-]/g, "")) || 0; + const total = (tr, part) => { + const tds = tr.querySelectorAll(".m02-labrow__money"); + return n(tds[tds.length - 1].querySelector(part).innerText); + }; + const settled = (get) => + until(() => { + const t = get(); + return t && !/…/.test(t.querySelector("tbody").innerText) ? t : null; + }, 10000); + const open = async (get, key) => { + get().querySelector(`tr[data-labor-logic="${key}"] .m02-labrow__name`).click(); + await w(300); + const tb = get(); + const tr = tb.querySelector(`tr[data-labor-logic="${key}"]`); + const subs = [...tb.querySelectorAll("tbody tr[data-labor-sub]")]; + let at = tr.nextElementSibling; + if (at?.hasAttribute("data-labor-opt")) at = at.nextElementSibling; + return { tb, tr, subs, next: at === subs[0] }; + }; + const measure = async (get) => { + const t = await settled(get); + const head = t.querySelectorAll("thead th").length; + const sp = await open(get, SP); + const { tr, subs, next } = await open(get, SC); + const out = { + head, + sp: { subs: sp.subs.length }, + sc: { + inner, + subs: subs.length, + next, + cells: [ + ...new Set(subs.map((s) => [...s.children].reduce((a, c) => a + (c.colSpan || 1), 0))), + ], + base: total(tr, ".m02-labrow__base"), + subBase: Math.round(subs.reduce((a, s) => a + total(s, ".m02-labrow__base"), 0)), + applied: total(tr, ".m02-labrow__applied"), + subApplied: Math.round(subs.reduce((a, s) => a + total(s, ".m02-labrow__applied"), 0)), + names: subs.map((s) => s.querySelector(".m02-labrow__subname").innerText), + qty: subs.map((s) => s.querySelector(".m02-labrow__qty").innerText), + units: subs.map((s) => s.querySelector(".m02-labrow__unit").innerText), + }, + }; + await open(get, SC); // 같은 줄 다시 = 닫힘 + return out; + }; + try { + const node = await until(() => document.querySelector('[data-node="i|item3"]')); + if (!node) return JSON.stringify({ error: "item3 없음 — 폴더를 펼쳐 둘 것" }); + document.querySelector('[data-node="i|item1"]')?.click(); // 다시 읽게 다른 항목 거침 + await w(1500); + node.click(); + const card = await until(() => + [...document.querySelectorAll(".m02-struct__card")].find((c) => + c.querySelector(`table.m02-labrow tbody tr[data-labor-logic="${SC}"]`), + ), + ); + if (!card) return JSON.stringify({ error: "일위조합 줄 카드 없음" }); + await w(1500); + const out = { card: await measure(() => card.querySelector("table.m02-labrow")) }; + const edit = [...card.querySelectorAll(".m02-struct__card-tools button")].find( + (b) => b.innerText.trim() === "수정", + ); + if (!edit) return JSON.stringify({ ...out, error: "일위대가 [수정] 없음" }); + edit.click(); + const detail = () => + [...document.querySelectorAll("table.m02-labrow")].find( + (t) => !t.closest(".m02-struct__card") && t.querySelector(`tr[data-labor-logic="${SC}"]`), + ); + if (!(await until(detail))) return JSON.stringify({ ...out, error: "상세 표 없음" }); + await w(2000); + out.detail = await measure(detail); + document.querySelector('[data-node="i|item3"]')?.click(); + await w(800); + [...document.querySelectorAll(".ui-modal button, dialog button")] + .find((b) => /확인|버림|떠나기/.test(b.innerText)) + ?.click(); + await w(1200); + out.back = !!document.querySelector(".m02-struct__card table.m02-labrow"); + return JSON.stringify(out); + } finally { + window.fetch = real; + } +})(); From f6bcfa52ce3b541d3e3c9e609244dc35390093a5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:55:15 +0900 Subject: [PATCH 06/11] =?UTF-8?q?chore(sync):=20=EA=B3=B5=EC=9C=A0=20docs?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EB=8B=B4=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7 --- docs/raw/사용자요구/2026-10-05_수정요구.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index 14f1cf7f7..292d015d7 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -112,3 +112,7 @@ 단가산출과 일위대가 사전조합에서 테이블의 양식도 M01에 공통테이블 양식이 있을것같고 버튼들도 M01에 템플릿이 있을거야. 확인해봐 그리고 사전조합의 경우에 검색은 별도의 창이 아니라 구조물 조합의 일위대가 테이블의 조건 추가와 같은 기능이어야. 일위대가 사전조합에서 나오는 모당창의 형식 뿐만아니라 내용도 구조물조합의 일위대가의 상세 화면에서 로직 추가와 같아야된다고 선택되는 항목도 + +어차피 신규로 키값을 만들지 않았음? 그럼 당연히 해당 연결정보와 구분, 상세구분을 만들어야하지 않음? + +다음으로 구조물 조합에서 일위대가의 상세페이지에서 테이블 행 선택에서 단가승격이야 단가산출과 같은 항목이라 문제가 없는데 일위대가 사전조합의 경우에는 전체를 다 표현하는데 문제가 있어. 일위조합인 경우에는 조합의 세부항목을 기준으로 노무비, 재료비, 경비를 값을 가져와서 사용하면 가능하지 않을까? From a3368f206dec5a0657408fb104299be8dc3fc8f6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:56:05 +0900 Subject: [PATCH 07/11] =?UTF-8?q?chore(plan):=2010-5=20=EC=84=9C=EA=B3=A0?= =?UTF-8?q?=20=EA=B5=AC=EB=B6=84=20=C2=B7=20=EC=9D=BC=EC=9C=84=EC=A1=B0?= =?UTF-8?q?=ED=95=A9=20=ED=95=98=EC=9C=84=20=ED=96=89=20=EC=9A=94=EA=B5=AC?= =?UTF-8?q?=20=EC=9B=90=EB=AC=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FJqWQpoB1RE7hiaQ2wH8qV --- docs/raw/사용자요구/2026-10-05_수정요구.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index 14f1cf7f7..292d015d7 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -112,3 +112,7 @@ 단가산출과 일위대가 사전조합에서 테이블의 양식도 M01에 공통테이블 양식이 있을것같고 버튼들도 M01에 템플릿이 있을거야. 확인해봐 그리고 사전조합의 경우에 검색은 별도의 창이 아니라 구조물 조합의 일위대가 테이블의 조건 추가와 같은 기능이어야. 일위대가 사전조합에서 나오는 모당창의 형식 뿐만아니라 내용도 구조물조합의 일위대가의 상세 화면에서 로직 추가와 같아야된다고 선택되는 항목도 + +어차피 신규로 키값을 만들지 않았음? 그럼 당연히 해당 연결정보와 구분, 상세구분을 만들어야하지 않음? + +다음으로 구조물 조합에서 일위대가의 상세페이지에서 테이블 행 선택에서 단가승격이야 단가산출과 같은 항목이라 문제가 없는데 일위대가 사전조합의 경우에는 전체를 다 표현하는데 문제가 있어. 일위조합인 경우에는 조합의 세부항목을 기준으로 노무비, 재료비, 경비를 값을 가져와서 사용하면 가능하지 않을까? From a522c1979bb182b319b9cf88ff86e6c80ac6cab3 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:58:11 +0900 Subject: [PATCH 08/11] =?UTF-8?q?chore(M02):=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=ED=8E=B8=EC=A7=91=20=E2=80=94=20=EB=A7=88=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=20=EC=84=9C=EA=B3=A0=20=EC=82=AC=EC=A0=84=EC=A1=B0=ED=95=A9=20?= =?UTF-8?q?SC-000001(=EA=B5=AC=EB=B6=84=20=EA=B3=A0=EB=A6=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FJqWQpoB1RE7hiaQ2wH8qV --- .../master_template/precombo/SC-000001.json | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/resources/master_template/precombo/SC-000001.json b/resources/master_template/precombo/SC-000001.json index c24f868cc..cb35b6b86 100644 --- a/resources/master_template/precombo/SC-000001.json +++ b/resources/master_template/precombo/SC-000001.json @@ -3,6 +3,23 @@ "키": "SC-000001", "이름": "테스트 사전조합", "단위": null, - "담은로직": [], + "담은로직": [ + { + "로직": "GF000297", + "입력값": {}, + "수량": { + "손": 1 + }, + "메모": null + }, + { + "로직": "GF000289", + "입력값": {}, + "수량": { + "손": 1 + }, + "메모": null + } + ], "본뜬": null } From 524c89d85c7371eb724b10830a04b914f929e2e2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:58:39 +0900 Subject: [PATCH 09/11] =?UTF-8?q?chore(plan):=2010-5=20=EC=88=9C=EC=84=9C?= =?UTF-8?q?=20=EC=B9=B8=202=ED=96=89=20=C2=B7=20=EC=8A=B9=EA=B2=A9=20?= =?UTF-8?q?=EC=98=B5=EC=85=98=20=C2=B7=20=EC=A1=B0=ED=95=A9=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=9A=94=EA=B5=AC=20=EC=9B=90=EB=AC=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FJqWQpoB1RE7hiaQ2wH8qV --- docs/raw/사용자요구/2026-10-05_수정요구.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index 292d015d7..bebbfa64f 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -116,3 +116,6 @@ 어차피 신규로 키값을 만들지 않았음? 그럼 당연히 해당 연결정보와 구분, 상세구분을 만들어야하지 않음? 다음으로 구조물 조합에서 일위대가의 상세페이지에서 테이블 행 선택에서 단가승격이야 단가산출과 같은 항목이라 문제가 없는데 일위대가 사전조합의 경우에는 전체를 다 표현하는데 문제가 있어. 일위조합인 경우에는 조합의 세부항목을 기준으로 노무비, 재료비, 경비를 값을 가져와서 사용하면 가능하지 않을까? + +추가로 구조물 조합의 일위대가 상세페이지의 로직 테이블의 순서 열에서 존재하는 버튼을 2행 구조로 변경 +단가 승격에도 옵션 버튼이있어야할것같고. 일위조합의 경우에는 바로 옵션으로 지정할수 없으니 수정버튼(모달)도 잇어할것 같은데? From 47cbbd17bdd4eaa6e49e0106579b5e6143809230 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 21:05:31 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feat(M02):=2051-14=20=EC=9D=BC=EC=9C=84?= =?UTF-8?q?=EB=8C=80=EA=B0=80=20=EC=88=9C=EC=84=9C=20=EC=B9=B8=202?= =?UTF-8?q?=ED=96=89=20=E2=80=94=20=EC=9C=84=20[=EC=9C=84=EB=A1=9C]=20[?= =?UTF-8?q?=EC=95=84=EB=9E=98=EB=A1=9C]=20/=20=EC=95=84=EB=9E=98=20[?= =?UTF-8?q?=EC=98=B5=EC=85=98]=C2=B7[=EC=88=98=EC=A0=95]=20[=EC=82=AD?= =?UTF-8?q?=EC=A0=9C]=20=C2=B7=20=EB=8B=A8=EA=B0=80=EC=8A=B9=EA=B2=A9=20[?= =?UTF-8?q?=EC=98=B5=EC=85=98]=20=C2=B7=20=EC=9D=BC=EC=9C=84=EC=A1=B0?= =?UTF-8?q?=ED=95=A9=20[=EC=88=98=EC=A0=95]=20=EC=84=9C=EA=B3=A0=20?= =?UTF-8?q?=EC=9B=90=EB=B3=B8=20=EB=AA=A8=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 순서 칸 = 단추 묶음 두 줄(공용 ui-btn-group 간격) · 카드 갈래는 아래 줄만 - 단가승격 줄 [옵션] = 승격 문서 넣을 값 모달 · [저장] = 서고 저장 길 · 머리 한 줄 알림(쓰는 모든 구조물에 따라감) - 일위조합 줄 [수정] = 사전조합 상세 모달(서고 모드 표 · 옵션 · 수량 · 순서 · 삭제 · 추가) · [저장] = 서고 원본 · 취소 = 버림 물음 - 자기 층 서고만 고침(위 층 = 읽기만) · 닫으면 구조물 일위대가 다시 셈 - 확인 시나리오 resources/tester/ui_checks/51-14_순서칸_2행_옵션수정.js Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7 --- .../M02_MasterTemplete_Labor.css | 13 ++ .../M02_MasterTemplete_Labor_Detail.ts | 19 ++- .../M02_MasterTemplete_Labor_Input.ts | 24 ++- .../M02_MasterTemplete_Labor_RefEdit.ts | 134 +++++++++++++++ .../M02_MasterTemplete_Labor_Rows.ts | 37 +++- .../M02_MasterTemplete_Labor_Text.ts | 8 + resources/tester/test_m02_card_shell.py | 23 ++- .../ui_checks/51-14_순서칸_2행_옵션수정.js | 159 ++++++++++++++++++ 8 files changed, 403 insertions(+), 14 deletions(-) create mode 100644 M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts create mode 100644 resources/tester/ui_checks/51-14_순서칸_2행_옵션수정.js diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.css b/M02_MasterTemplete/M02_MasterTemplete_Labor.css index 7f212df44..bf59b0c4f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor.css @@ -90,6 +90,14 @@ white-space: nowrap; } +/* 순서 칸 2행(51-14) — 위 [위로] [아래로] / 아래 [옵션]·[수정] [삭제] · 줄 사이도 단추 묶음 간격 */ +.m02-labrow__tools { + display: inline-flex; + flex-direction: column; + align-items: flex-start; + gap: var(--btn-group-gap); +} + /* 옵션 값 줄(44-6) — 이름 ~ 합계에 걸친 한 줄 말줄임(max-width 0 = 표 폭을 안 늘림) · 위 줄과 한 덩이 */ .m02-labrow .m02-labrow__optrow td { padding-top: 0; @@ -179,6 +187,11 @@ color: var(--color-primary); } +/* 참조 줄 모달(51-14) — 일위조합 [수정] = 사전조합 상세 */ +.m02-labref { + width: min(72rem, calc(100vw - 32px)); +} + /* 입력 모달 */ .m02-labin { width: min(40rem, calc(100vw - 32px)); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index d0bbc892b..f30ad063d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -3,7 +3,7 @@ * 일위대가 상세(39-3) — 구조물 열마다 하나(`labor/<열>.json`) · UI_Main 종류 `labor` 가 이 부품을 붙임 * (양식 길 읽기 · [저장] · [다시 읽기]는 UI_Main). 차례 = 정보(단위 · 본뜬) → 담은 로직 → 자재목록 → 다이어그램. * · 담은 로직 줄 누름 = 고름(하이라이트 · 다시 그려도 유지 · 47-5) + 그 줄 바로 아래 하위 행(51-2 · `Labor_Rows`) · - * 같은 줄 다시 누르면 닫힘 · [옵션] = 입력 모달(`Labor_Input`) · + * 같은 줄 다시 누르면 닫힘 · [옵션] = 입력 모달(`Labor_Input`) · 참조 줄 [옵션] · [수정] = 서고 원본 모달(51-14 · `Labor_RefEdit`) · * [확인] = 화면 캐시만 · 저장은 [저장] * · 금액 = 서버 calc(기준 ③ · 최신 응답만 · 저장 안 함) · 고칠 때마다 뒤에서 다시 셈 * · 같은 로직 두 번 · 같은 자재 두 번 막음 · 빈 일위대가도 저장 됨 @@ -47,6 +47,7 @@ import { } from "./M02_MasterTemplete_Labor_Diagram"; import { openLaborInput, type GoArea } from "./M02_MasterTemplete_Labor_Input"; import { openQtyModal } from "./M02_MasterTemplete_Labor_QtyModal"; +import { openRefEdit } from "./M02_MasterTemplete_Labor_RefEdit"; import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick"; import { logicTable, matTable, qtyShort } from "./M02_MasterTemplete_Labor_Rows"; import { tc } from "./M02_MasterTemplete_Labor_Text"; @@ -174,12 +175,13 @@ export function mountLaborDetail( ? { open: pick, option: openRow, + ref: openRef, qty: openQty, ...(opt.card ? {} : { move }), remove: removeRow, alias: setAlias, } - : { open: pick, option: openRow }, + : { open: pick, option: openRow, ref: openRef }, ), ); const draw = (): void => { @@ -347,6 +349,19 @@ export function mountLaborDetail( }); } + /** 참조 줄 [옵션] · [수정](51-14) — 서고 원본 모달 · 자기 층 서고만 고침(위 층 = 읽기만 · 서버도 막음) · + * 닫으면 다시 셈(원본이 바뀌었을 수 있음) */ + function openRef(i: number): void { + const ref = cur.담은로직[i].참조; + if (!ref) return; + void openRefEdit({ + ref, + title: `${ref.키} ${lineOf(lineId({ 참조: ref }) ?? "")?.이름 ?? ""}`.trim(), + readOnly: !edit || ref.층 !== opt.layer, + closed: () => alive && recalc(), + }); + } + /** 수량산출 칸 누름 — 로직 줄 */ function openQty(i: number): void { const row = cur.담은로직[i]; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Input.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Input.ts index 019b3541a..f60fe5c0d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Input.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Input.ts @@ -19,14 +19,19 @@ const fill = (text: string, value: string): string => text.replace("{value}", va /** 면적산출 상세로 감 — 옮겨 갔으면 참(물음에서 취소 = 거짓) */ export type GoArea = () => Promise; -function footer(readOnly: boolean, ok: () => void, close: () => void): HTMLElement { +function footer( + readOnly: boolean, + ok: () => void, + close: () => void, + okLabel = tc("In_Ok"), +): HTMLElement { return el("div", { className: "m02-labin__foot", children: readOnly ? [createButton({ label: tc("In_Close"), variant: "ghost", onClick: close })] : [ createButton({ label: tc("In_Cancel"), variant: "ghost", onClick: close }), - createButton({ label: tc("In_Ok"), variant: "filled", onClick: ok }), + createButton({ label: okLabel, variant: "filled", onClick: ok }), ], }); } @@ -45,6 +50,10 @@ export interface LaborInputOptions { memo: string | null; readOnly: boolean; done: (next: LaborInputResult) => void; + /** 머리 한 줄 알림(51-14 · 서고 원본 고침) */ + notice?: string; + /** 확인 단추 글(기본 「확인」) */ + okLabel?: string; } /** 담은 로직 줄 입력 모달 */ @@ -100,6 +109,15 @@ export function openLaborInput(opt: LaborInputOptions): void { el("div", { className: "m02-inputval", children: [ + ...(opt.notice + ? [ + el("p", { + className: "m02-labin__muted", + text: opt.notice, + attrs: { "data-labor-notice": "" }, + }), + ] + : []), ...(lost.length ? [ el("p", { @@ -120,7 +138,7 @@ export function openLaborInput(opt: LaborInputOptions): void { }), ], }), - footer(opt.readOnly, ok, close), + footer(opt.readOnly, ok, close, opt.okLabel), ); }, }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts new file mode 100644 index 000000000..4ecb9e78f --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts @@ -0,0 +1,134 @@ +/* ============================================================================= + * M02_MasterTemplete_Labor_RefEdit.ts + * 일위대가 참조 줄 모달(51-14) — 서고 원본(50-3)을 그 자리에서 고침 · [저장] = 서고 저장 길(`saveTemplate`) · + * 그 문서를 쓰는 모든 구조물에 따라감(머리 한 줄 알림) · 그 층을 못 고치면 읽기만. + * · 단가승격 [옵션] = 승격 문서 로직 한 줄의 넣을 값 모달(`Labor_Input`) + * · 일위조합 [수정] = 사전조합 상세(서고 모드 표 · 안 줄 [옵션] · 수량 · 위로 · 아래로 · 삭제 · 추가) + * 닫으면(저장 · 취소 모두) `closed` — 부르는 쪽이 구조물 일위대가 표를 다시 셈. + * ========================================================================== */ + +import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; +import { openModal } from "@ui/ui_template_modal"; +import { readTemplate, saveTemplate, type Layer } from "./M02_MasterTemplete_Api_Fetch"; +import { fetchLogicInputs, type LaborRow } from "./M02_MasterTemplete_Labor_Api"; +import type { LaborRef } from "./M02_MasterTemplete_Labor_Calc"; +import { openLaborInput } from "./M02_MasterTemplete_Labor_Input"; +import { tc } from "./M02_MasterTemplete_Labor_Text"; +import { keepKnown } from "./M02_Labor_Values_Model"; + +export interface RefEditOptions { + ref: LaborRef; + /** 「SP-000001 메쌓기」 */ + title: string; + readOnly: boolean; + closed: () => void; +} + +const fail = (error: unknown, key: "Load_Failed" | "Lib_SaveFailed"): void => + showToast(error instanceof Error ? error.message : tc(key), "error"); + +export async function openRefEdit(opt: RefEditOptions): Promise { + const kind = opt.ref.키.startsWith("SP-") ? "promote" : "precombo"; + const layer = opt.ref.층 as Layer; + let doc: { 담은로직?: LaborRow[] }; + let 판: string; + try { + const got = await readTemplate(layer, kind, opt.ref.키, null); + doc = got.문서 as typeof doc; + 판 = got.판 ?? ""; + } catch (error) { + return fail(error, "Load_Failed"); + } + const save = (body: unknown): Promise => + saveTemplate(layer, kind, opt.ref.키, null, 판, body).then(() => + showToast(tc("Saved"), "success"), + ); + const notice = opt.readOnly ? `${tc("Ref_Shared")} · ${tc("Ref_ReadOnly")}` : tc("Ref_Shared"); + + if (kind === "promote") { + const row = doc.담은로직?.[0]; + if (!row) return fail(null, "Load_Failed"); + const logic = row.로직 + ? ((await fetchLogicInputs([row.로직]).catch(() => null))?.로직[0] ?? null) + : null; + return openLaborInput({ + title: opt.title, + logic, + values: row.입력값, + memo: row.메모, + readOnly: opt.readOnly, + notice, + okLabel: tc("Save"), + done: (next) => { + row.입력값 = logic ? keepKnown(logic.입력, next.입력값).kept : next.입력값; + row.메모 = next.메모; + void save(doc) + .catch((error) => fail(error, "Lib_SaveFailed")) + .finally(opt.closed); + }, + }); + } + + // 사전조합 — 상세와 같은 부품(서로 부르므로 늦게 받음) + const { mountLaborDetail } = await import("./M02_MasterTemplete_Labor_Detail"); + let dirty = false; + let handle: ReturnType | null = null; + const finish = (): void => { + handle?.destroy(); + opt.closed(); + }; + openModal({ + title: opt.title, + closeLabel: tc("In_Close"), + dialogClass: "m02-modal m02-labref", + beforeClose: async () => { + if (dirty && !(await showConfirmDialog(tc("Ref_Discard")))) return false; + finish(); + return true; + }, + mount: (body, close) => { + const host = el("div", { attrs: { "data-labor-ref": opt.ref.키 } }); + handle = mountLaborDetail(host, doc, { + layer, + column: opt.ref.키, + readOnly: opt.readOnly, + onChanged: (d) => (dirty = d && !opt.readOnly), + goArea: null, + mode: "precombo", + }); + const cancel = createButton({ + label: tc(opt.readOnly ? "In_Close" : "In_Cancel"), + variant: "ghost", + onClick: async () => { + if (dirty && !(await showConfirmDialog(tc("Ref_Discard")))) return; + close(); + finish(); + }, + }); + const ok = createButton({ + label: tc("Save"), + variant: "filled", + onClick: () => + void save(handle?.getDoc()).then( + () => { + close(); + finish(); + }, + (error) => fail(error, "Lib_SaveFailed"), + ), + }); + body.append( + el("p", { + className: "m02-labin__muted", + text: notice, + attrs: { "data-labor-notice": "" }, + }), + host, + el("div", { + className: "m02-labin__foot", + children: opt.readOnly ? [cancel] : [cancel, ok], + }), + ); + }, + }); +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts index 5d108131b..bcdd9122b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts @@ -6,7 +6,8 @@ * 옵션 값(「지역 서울 · 규격 25mm」)은 줄 아래 이름 ~ 합계에 걸친 한 줄 · 맨 아래 합계 줄(전체 합계 강조). * 자재목록 = 코드 · 이름 · 규격 · 수량산출 · 단위 · 가격 다섯(기준 칸 강조) · 기준단가 / 반영 금액(43-3). * 금액은 calc 답(없으면 「…」). - * 줄 누름 · 줄 단추(위로 · 아래로 · 옵션 · 삭제 · 묶음 `ui-btn-group`)는 `on` 이 있을 때만(보기 갈래 = 없음) · + * 줄 누름 · 줄 단추는 `on` 이 있을 때만(보기 갈래 = 없음) — 순서 칸 2행(51-14 · 위 [위로] [아래로] / 아래 [옵션] 또는 [수정] · [삭제] · + * 묶음 `ui-btn-group` · 순서 없는 카드 갈래 = 아래 줄만) · 단가승격 줄 = [옵션] · 일위조합 줄 = [수정](`ref` · 서고 원본) · * 상세는 줄 누름 = 고름(`selected` 줄 하이라이트 · 그 줄 바로 아래 하위 행 · 51-2 · 화살표 없음 51-7) · 넣을 값 모달은 [옵션](47-5). * ========================================================================== */ @@ -174,6 +175,8 @@ export interface RowHandlers { open: (i: number) => void; /** [옵션] 단추(47-5 · 넣을 값 모달) — 없으면 단추 없음 */ option?: (i: number) => void; + /** 참조 줄 단추(51-14) — 단가승격 [옵션] · 일위조합 [수정] = 서고 원본 모달 · 없으면 단추 없음 */ + ref?: (i: number) => void; /** 수량산출 칸 누름(손 입력 · 엑셀에서 고르기) — 없으면 줄 누름과 같음 */ qty?: (i: number) => void; move?: (i: number, d: number) => void; @@ -290,9 +293,11 @@ function subRows(line: LaborCalcLine | undefined, wait: boolean, tools: boolean) /** 담은 로직 표 — 보기 갈래는 `on` 없이 */ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement { const wait = !data.calc && !data.fail; - const { move, remove, option } = on ?? {}; + const { move, remove, option, ref } = on ?? {}; // 옵션 · 삭제만(구조물 조합 카드 · 43-4)이어도 끝 칸 — 순서 단추는 `move` 가 있을 때만 - const tools = !!(move || remove || option); + const tools = !!(move || remove || option || ref); + const group = (children: HTMLElement[]): HTMLElement => + el("div", { className: "ui-btn-group", children }); const lineOf = (key: string): LaborCalcLine | undefined => data.calc?.줄.find((l) => lineId(l) === key); const trs = data.rows.flatMap((row, i) => { @@ -362,13 +367,31 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement className: "m02-labrow__move", children: [ el("div", { - className: "ui-btn-group", + className: "m02-labrow__tools", children: [ ...(move - ? [small(tc("Up"), () => move(i, -1)), small(tc("Down"), () => move(i, 1))] + ? [ + group([ + small(tc("Up"), () => move(i, -1)), + small(tc("Down"), () => move(i, 1)), + ]), + ] : []), - ...(option && !row.참조 ? [small(tc("Labor_Option"), () => option(i))] : []), - ...(remove ? [small(tc("Remove"), () => remove(i), "danger")] : []), + group([ + ...(row.참조 + ? ref + ? [ + small( + tc(row.참조.키.startsWith("SP-") ? "Labor_Option" : "Labor_Edit"), + () => ref(i), + ), + ] + : [] + : option + ? [small(tc("Labor_Option"), () => option(i))] + : []), + ...(remove ? [small(tc("Remove"), () => remove(i), "danger")] : []), + ]), ], }), ], diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index c7be730db..6b5ea157e 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -35,6 +35,14 @@ const TEXT = { Down: ["아래로", "Down"], Remove: ["삭제", "Delete"], Labor_Option: ["옵션", "Options"], + Labor_Edit: ["수정", "Edit"], + Ref_Shared: [ + "서고 원본을 고침 — 이 문서를 쓰는 모든 구조물에 따라감", + "Edits the library original — every structure using it follows", + ], + Ref_ReadOnly: ["이 층은 고칠 수 없음 — 읽기만", "Read-only layer"], + Lib_SaveFailed: ["저장 못 함", "Save failed"], + Ref_Discard: ["저장 안 한 고침을 버리고 닫음?", "Discard unsaved edits?"], Labor_Source: ["원문", "Source"], Labor_SourceHint: [ "단가산출 로직 줄을 고르면 그 원문이 보임", diff --git a/resources/tester/test_m02_card_shell.py b/resources/tester/test_m02_card_shell.py index 8882439d8..6b03b170e 100644 --- a/resources/tester/test_m02_card_shell.py +++ b/resources/tester/test_m02_card_shell.py @@ -86,7 +86,9 @@ def test_일위대가_카드는_상세_카드_갈래(): assert "...(opt.card || lib ? [] : [dgBox])," in detail assert "opt.card ? null : addMat" in detail # 49-6 — 카드엔 [자재 추가] 없음 · 상세는 그대로 rows = _text("M02_MasterTemplete_Labor_Rows.ts") - assert "const tools = !!(move || remove || option);" in rows and "compactTable" not in rows + assert ( + "const tools = !!(move || remove || option || ref);" in rows and "compactTable" not in rows + ) assert "compact" not in _text("M02_MasterTemplete_Labor_View.ts").split("*/", 1)[1] @@ -127,7 +129,9 @@ def test_일위대가_줄_고름과_옵션_단추(): 단추 위로 · 아래로 · 옵션 · 삭제(빨강) · 묶음 간격 = 공용 `ui-btn-group`.""" detail = _text("M02_MasterTemplete_Labor_Detail.ts") assert "open: pick," in detail and "option: openRow," in detail - assert "{ open: pick, option: openRow }" in detail # 읽기만도 고름 · [옵션] = 보기 모달 + assert ( + "{ open: pick, option: openRow, ref: openRef }" in detail + ) # 읽기만도 고름 · [옵션] = 보기 모달 assert "selected: picked" in detail rows = _text("M02_MasterTemplete_Labor_Rows.ts") up, down = rows.index('small(tc("Up")'), rows.index('small(tc("Down")') @@ -141,3 +145,18 @@ def test_일위대가_줄_고름과_옵션_단추(): assert t in text css = (ROOT / "ui_template" / "ui_template_general_layout.css").read_text(encoding="utf-8") assert ".ui-btn-group {" in css and "gap: var(--btn-group-gap);" in css + + +def test_순서칸_2행_참조줄_옵션_수정(): + """51-14 — 순서 칸 2행(위 [위로] [아래로] / 아래 [옵션]·[수정] [삭제]) · 단가승격 [옵션] · 일위조합 [수정] = + 서고 원본 모달(저장 = 서고 저장 길 · 자기 층만 고침) · 닫으면 다시 셈.""" + rows = _text("M02_MasterTemplete_Labor_Rows.ts") + assert 'className: "m02-labrow__tools"' in rows + assert '"SP-") ? "Labor_Option" : "Labor_Edit"' in rows + detail = _text("M02_MasterTemplete_Labor_Detail.ts") + assert "readOnly: !edit || ref.층 !== opt.layer," in detail + assert "closed: () => alive && recalc()," in detail + ref = _text("M02_MasterTemplete_Labor_RefEdit.ts") + assert "saveTemplate(layer, kind, opt.ref.키, null, 판, body)" in ref + assert 'mode: "precombo",' in ref and "showConfirmDialog" in ref + assert 'Labor_Edit: ["수정"' in _text("M02_MasterTemplete_Labor_Text.ts") diff --git a/resources/tester/ui_checks/51-14_순서칸_2행_옵션수정.js b/resources/tester/ui_checks/51-14_순서칸_2행_옵션수정.js new file mode 100644 index 000000000..19f94dc3a --- /dev/null +++ b/resources/tester/ui_checks/51-14_순서칸_2행_옵션수정.js @@ -0,0 +1,159 @@ +/* 51-14 — 일위대가 표 순서 칸 2행(위 [위로] [아래로] / 아래 [옵션]·[수정] [삭제]) · 단가승격 [옵션] · 일위조합 [수정] 모달 + * 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자 · + * 나무에 item3(단가승격 SP-000001 · 일위조합 SC-000001 줄이 있음) + * 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌) + * 쓰기 없음 — 모달은 모두 [취소]로 닫음 · PUT 수 = 0 확인(서고 저장 안 함) + * 기대 수치: + * - card.sp / card.sc: groups 1 · labels [옵션, 삭제] / [수정, 삭제] + * - detail.sp / detail.sc: groups 2 · top [위로, 아래로] · bottom [옵션, 삭제] / [수정, 삭제] · danger = 삭제 + * - promote: notice 있음 · foot [취소, 저장] · closed = true + * - combo: notice 있음 · add [로직 추가] · foot [취소, 저장] — 모달 안 [로직 추가]로 단가산출 한 줄 넣음(화면만) · + * added.groups 2 · top [위로, 아래로] · bottom [옵션, 삭제] · [취소] = 버림 물음(asked) → 확인 · closed = true · recalc ≥ 1 + * - puts = 0 + */ +(async () => { + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 8000) => { + for (let t = 0; t < ms; t += 150) { + const v = fn(); + if (v) return v; + await w(150); + } + return null; + }; + const SC = "참조|system||SC-000001"; + const SP = "참조|system||SP-000001"; + const real = window.fetch; + let puts = 0; + let calcs = 0; + window.fetch = (input, init) => { + const url = typeof input === "string" ? input : input.url; + if ((init?.method ?? "GET").toUpperCase() === "PUT") puts++; + if (/\/labor\/item3\/calc/.test(url)) calcs++; + return real(input, init); + }; + const txt = (b) => b.innerText.trim(); + const tools = (tb, key) => { + const cell = tb.querySelector(`tr[data-labor-logic="${key}"] .m02-labrow__move`); + const groups = [...cell.querySelectorAll(".m02-labrow__tools > .ui-btn-group")]; + const btn = (g) => [...g.querySelectorAll("button")]; + const last = groups.at(-1) ? btn(groups.at(-1)).at(-1) : null; + return { + groups: groups.length, + top: groups.length > 1 ? btn(groups[0]).map(txt) : [], + bottom: groups.length ? btn(groups.at(-1)).map(txt) : [], + danger: last ? last.className : "", + _btn: (label) => groups.flatMap(btn).find((b) => txt(b) === label), + }; + }; + const clean = (t) => { + delete t._btn; + return t; + }; + const modal = () => [...document.querySelectorAll(".ui-modal")].at(-1) ?? null; + const foot = (m) => [...m.querySelectorAll(".m02-labin__foot button")].map(txt); + const cancel = async (m) => { + [...m.querySelectorAll(".m02-labin__foot button")].find((b) => txt(b) === "취소").click(); + await w(600); + return !document.body.contains(m); + }; + try { + const node = await until(() => document.querySelector('[data-node="i|item3"]')); + if (!node) return JSON.stringify({ error: "item3 없음 — 폴더를 펼쳐 둘 것" }); + document.querySelector('[data-node="i|item1"]')?.click(); // 다시 읽게 다른 항목 거침 + await w(1500); + node.click(); + const card = await until(() => + [...document.querySelectorAll(".m02-struct__card")].find((c) => + c.querySelector(`table.m02-labrow tbody tr[data-labor-logic="${SC}"]`), + ), + ); + if (!card) return JSON.stringify({ error: "일위조합 줄 카드 없음" }); + await w(1500); + const ct = card.querySelector("table.m02-labrow"); + const out = { card: { sp: clean(tools(ct, SP)), sc: clean(tools(ct, SC)) } }; + const edit = [...card.querySelectorAll(".m02-struct__card-tools button")].find( + (b) => txt(b) === "수정", + ); + if (!edit) return JSON.stringify({ ...out, error: "일위대가 [수정] 없음" }); + edit.click(); + const detail = () => + [...document.querySelectorAll("table.m02-labrow")].find( + (t) => + !t.closest(".m02-struct__card, .ui-modal") && + t.querySelector(`tr[data-labor-logic="${SC}"]`), + ); + if (!(await until(detail))) return JSON.stringify({ ...out, error: "상세 표 없음" }); + await w(2000); + const sp = tools(detail(), SP); + const sc = tools(detail(), SC); + out.detail = { sp: clean({ ...sp }), sc: clean({ ...sc }) }; + + // 단가승격 [옵션] — 넣을 값 모달 · 알림 한 줄 · [취소] + sp._btn("옵션").click(); + const pm = await until(() => modal()?.querySelector(".m02-inputval") && modal()); + out.promote = pm + ? { + title: pm.querySelector(".ui-modal__top h3").innerText, + notice: !!pm.querySelector("[data-labor-notice]"), + inputs: pm.querySelectorAll("[data-labor-input]").length, + foot: foot(pm), + closed: await cancel(pm), + } + : { error: "승격 모달 없음" }; + + // 일위조합 [수정] — 사전조합 상세 모달 · [취소] · 닫으면 다시 셈 + tools(detail(), SC)._btn("수정").click(); + const cm = await until(() => document.querySelector(".m02-labref")); + if (!cm) return JSON.stringify({ ...out, error: "일위조합 모달 없음", puts }); + await w(1000); + out.combo = { + title: cm.querySelector(".ui-modal__top h3").innerText, + notice: !!cm.querySelector("[data-labor-notice]"), + rows0: cm.querySelectorAll("table.m02-labrow tbody tr[data-labor-logic]").length, + add: [...cm.querySelectorAll(".m01-logic__section-head button")].map(txt), + foot: foot(cm), + }; + // 모달 안 [로직 추가] → 단가산출 첫 줄(화면 캐시만) + [...cm.querySelectorAll(".m01-logic__section-head button")] + .find((b) => txt(b) === "로직 추가") + .click(); + const pick = await until( + () => + [...document.querySelectorAll(".m01-logic__pick .ui-find__row")].find( + (r) => r.querySelector(".ui-find__tag")?.innerText.trim() === "단가산출", + ), + 30000, // 로직 목록 1천여 줄 + ); + if (!pick) return JSON.stringify({ ...out, error: "고르기 줄 없음", puts }); + pick.click(); + const added = await until(() => + cm.querySelector("table.m02-labrow tbody tr[data-labor-logic]"), + ); + if (added) { + const key = added.getAttribute("data-labor-logic"); + out.combo.added = clean(tools(cm.querySelector("table.m02-labrow"), key)); + } + const before = calcs; + [...cm.querySelectorAll(".m02-labin__foot button")].find((b) => txt(b) === "취소").click(); + const ask = await until(() => document.querySelector(".ui-confirm")); + out.combo.asked = !!ask; + [...(ask?.querySelectorAll("button") ?? [])].find((b) => txt(b) === "확인")?.click(); + await w(600); + out.combo.closed = !document.body.contains(cm); + await w(1200); + out.combo.recalc = calcs - before; + out.puts = puts; + + document.querySelector('[data-node="i|item3"]')?.click(); + await w(800); + [...document.querySelectorAll(".ui-modal button, dialog button")] + .find((b) => /확인|버림|떠나기/.test(b.innerText)) + ?.click(); + await w(1200); + out.back = !!document.querySelector(".m02-struct__card table.m02-labrow"); + return JSON.stringify(out); + } finally { + window.fetch = real; + } +})(); From b7b827a831a3378db3632609215965fcb91f548b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 21:05:43 +0900 Subject: [PATCH 11/11] =?UTF-8?q?chore(sync):=20=EA=B3=B5=EC=9C=A0=20docs?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EB=8B=B4=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7 --- docs/raw/사용자요구/2026-10-05_수정요구.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index 292d015d7..bebbfa64f 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -116,3 +116,6 @@ 어차피 신규로 키값을 만들지 않았음? 그럼 당연히 해당 연결정보와 구분, 상세구분을 만들어야하지 않음? 다음으로 구조물 조합에서 일위대가의 상세페이지에서 테이블 행 선택에서 단가승격이야 단가산출과 같은 항목이라 문제가 없는데 일위대가 사전조합의 경우에는 전체를 다 표현하는데 문제가 있어. 일위조합인 경우에는 조합의 세부항목을 기준으로 노무비, 재료비, 경비를 값을 가져와서 사용하면 가능하지 않을까? + +추가로 구조물 조합의 일위대가 상세페이지의 로직 테이블의 순서 열에서 존재하는 버튼을 2행 구조로 변경 +단가 승격에도 옵션 버튼이있어야할것같고. 일위조합의 경우에는 바로 옵션으로 지정할수 없으니 수정버튼(모달)도 잇어할것 같은데?