From fc6bb7240e0b533f7155f588f008d12c75d3e936 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 22 Sep 2026 00:09:49 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(M01):=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EC=8B=9C=ED=97=98=20=EC=95=8C=EC=95=BD=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=A7=88=EC=8A=A4=ED=84=B0=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9E=87=EA=B8=B0=20=C2=B7=20=EB=8B=A8?= =?UTF-8?q?=EA=B0=80=20=EC=97=86=EB=8A=94=20=EC=9E=AC=EB=A3=8C=20=EC=95=8C?= =?UTF-8?q?=EC=95=BD=EC=97=90=20=EC=9D=B4=EC=9C=A0=20=ED=95=9C=20=EB=A7=88?= =?UTF-8?q?=EB=94=94=20(PLAN=205-3=20=C2=B7=205-4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01PAdA5ThqmtVSsbzjusk1cJ --- .../M01_MasterData_UI_LogicLab_Modal.ts | 17 ++++++- .../M01_MasterData_UI_LogicLab_Text.ts | 7 +++ M01_MasterData/M01_MasterData_UI_Page.ts | 45 ++++++++++++++++++- M01_MasterData/M01_MasterData_UI_Side.ts | 15 ++++++- 4 files changed, 81 insertions(+), 3 deletions(-) diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts index 0fbfa52f..7fd3ca41 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts @@ -16,6 +16,7 @@ import { type RelatedFind, type TableRow, } from "./M01_MasterData_UI_Logic_Note"; +import { jumpToMaster, type MasterJump } from "./M01_MasterData_UI_Side"; import { pillBar, type Pill } from "./M01_MasterData_UI_LogicLab_Pill"; import { tl } from "./M01_MasterData_UI_LogicLab_Text"; import "./M01_MasterData_UI_LogicLab_Pill.css"; @@ -72,6 +73,7 @@ function tableView( className: "m01-logic__muted", text: `${find.source} → ${find.find.col} · ${hit ? tl("Modal_Hit") : tl("Modal_NoHit")}`, }), + goButton({ kind: "table", key: table.키 }), el("div", { className: "m01-logic__scroll", children: [ @@ -93,6 +95,14 @@ function tableView( const groupOf = (ref: string): string => ref.startsWith("LB") ? "인력" : ref.startsWith("M") ? "재료" : ref.startsWith("EQ") ? "기계" : ""; +function goButton(target: MasterJump): HTMLElement { + return createButton({ + label: tl("Pill_Go"), + variant: "ghost", + onClick: () => jumpToMaster(target), + }); +} + function priceView(ref: string, brief: ElementBrief): HTMLElement { const cols = Object.entries(brief.값칸 ?? {}); return el("div", { @@ -114,6 +124,7 @@ function priceView(ref: string, brief: ElementBrief): HTMLElement { ...cols.flatMap(([k, v]) => [el("dt", { text: k }), el("dd", { text: String(v) })]), ], }), + goButton({ kind: "element", group: groupOf(ref), ref: brief.ref ?? ref, file: brief.file }), ], }); } @@ -153,7 +164,11 @@ export function openMaterialModal(target: ModalTarget): void { } else if (target.element && !logicRef) { pills.push({ label: `${groupOf(base)} ${target.title}`.trim(), - view: () => el("p", { text: target.element }), + view: () => + el("p", { + className: "m01-logic__muted", + text: `${target.element} — ${tl(target.element?.includes("{") ? "Pill_NoPick" : "Pill_NoValue")}`, + }), }); } if (logicRef) { diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts index 8d21e699..91943b63 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts @@ -52,6 +52,13 @@ const TEXT = { Modal_UnitPrice: ["단가", "Unit price"], Pill_Table: ["표", "Table"], Pill_Logic: ["로직", "Logic"], + Pill_Go: ["마스터 화면에서 보기", "Open in master"], + Pill_Back: ["← 로직 개선 시험으로 돌아가기", "← Back to logic lab"], + Pill_NoPick: [ + "고르기 조건에 맞는 마스터 줄이 없음 — 단가를 못 찾음", + "No master row matches the pick conditions", + ], + Pill_NoValue: ["관리자 값 미확보 — 마스터에 단가가 없음", "No price in master yet"], Modal_Qty: ["수량 식", "Quantity formula"], Modal_Price: ["단가 자료", "Price source"], Modal_Value: ["값", "Value"], diff --git a/M01_MasterData/M01_MasterData_UI_Page.ts b/M01_MasterData/M01_MasterData_UI_Page.ts index ae12a8b3..08f2e216 100644 --- a/M01_MasterData/M01_MasterData_UI_Page.ts +++ b/M01_MasterData/M01_MasterData_UI_Page.ts @@ -20,7 +20,7 @@ import { createWorkflowOverlays } from "@ui/ui_template_overlay"; import { ROUTES } from "@config/config_frontend"; import { navigateTo } from "../A00_Common/router"; import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; -import { saveFiles } from "./M01_MasterData_Api_Fetch"; +import { saveFiles, type FileInfo } from "./M01_MasterData_Api_Fetch"; import { discard, isStale, onDraftChange, payload, totalCount } from "./M01_MasterData_Draft"; import { renderRows } from "./M01_MasterData_UI_Rows"; import { @@ -28,10 +28,14 @@ import { fileLabel, loadView, saveView, + setMasterJump, setPendingLogicKey, takePendingLogicKey, + type MasterJump, type Pick, } from "./M01_MasterData_UI_Side"; +import { loadTable } from "./M01_MasterData_UI_Logic_Note"; +import { tl } from "./M01_MasterData_UI_LogicLab_Text"; import { renderTables } from "./M01_MasterData_UI_Tables"; import type { LogicHandle } from "./M01_MasterData_UI_Logic_Page"; import type { ComboHandle } from "./M01_MasterData_UI_Combo"; @@ -227,6 +231,45 @@ function buildPage(): HTMLElement { }); }; + /** 알약 → 마스터 화면 — 찾기 글을 그 키로 두고 열고, 알림줄에 「시험으로 돌아가기」를 둠 */ + const jump = async (target: MasterJump): Promise => { + let group = target.kind === "element" ? target.group : ""; + let file: FileInfo | undefined; + let ref = ""; + if (target.kind === "table") { + const table = await loadTable(target.key); + group = + ["소요량", "계수"].find((g) => side.files(g).some((f) => f.file === table?.file)) ?? ""; + file = side.files(group).find((f) => f.file === table?.file); + ref = target.key; + } else { + const list = side.files(group); + file = list.find((f) => f.file === target.file) ?? list[0]; + ref = target.ref; + } + if (!file) return showToast(L("M01_LoadFailed"), "error"); + side.setActive(null); + openFile({ group, file, sub: "", detail: "", label: group, view: { q: ref, page: 1 } }); + const back = createButton({ label: tl("Pill_Back"), variant: "ghost" }); + back.addEventListener("click", () => { + showNotice([]); + openLabTab(); + }); + showNotice([back]); + if (target.kind === "table") { + const key = target.key; + const seen = new MutationObserver(() => { + const row = body.querySelector(`[data-key="${CSS.escape(key)}"]`); + if (!row) return; + seen.disconnect(); + row.click(); + }); + seen.observe(body, { childList: true, subtree: true }); + window.setTimeout(() => seen.disconnect(), 8000); + } + }; + setMasterJump((target) => void jump(target)); + /* --- 좌측: 컨테이너 --- */ const side = buildSide(openFile, () => openLogicTab(), openLabTab, openComboTab); diff --git a/M01_MasterData/M01_MasterData_UI_Side.ts b/M01_MasterData/M01_MasterData_UI_Side.ts index 1e30922d..4d586ae9 100644 --- a/M01_MasterData/M01_MasterData_UI_Side.ts +++ b/M01_MasterData/M01_MasterData_UI_Side.ts @@ -59,6 +59,16 @@ export function takePendingLogicKey(): string | null { } } +/** 「로직 개선 시험」 알약 → 마스터 화면 — 표(키) 또는 요소(그룹 · 키 · 파일) */ +export type MasterJump = + { kind: "table"; key: string } | { kind: "element"; group: string; ref: string; file?: string }; + +let masterJump: (target: MasterJump) => void = () => {}; +export const setMasterJump = (fn: (target: MasterJump) => void): void => { + masterJump = fn; +}; +export const jumpToMaster = (target: MasterJump): void => masterJump(target); + /** 요소 화면에서 열 것 — 파일 · 하위 거름(구분·세부분류) · 상세구분 · 제목 */ export interface Pick { group: string; @@ -82,6 +92,8 @@ export interface SideHandle { labHost: HTMLElement; /** 그룹의 파일 판본을 다시 받음(저장 뒤) — 돌려받는 것 = 새 목록 */ refresh: (group: string) => Promise; + /** 그룹의 파일 목록(이미 받은 것) */ + files: (group: string) => FileInfo[]; setActive: (id: string | null) => void; /** 좁은 화면에서 사용자가 거름을 다 골랐을 때 부를 것 — 패널을 접어 목록을 드러냄 */ setOnPicked: (fn: () => void) => void; @@ -456,5 +468,6 @@ export function buildSide( const setOnPicked = (fn: () => void): void => { onPicked = fn; }; - return { root, logicHost, comboHost, labHost, refresh, setActive, setOnPicked, filter }; + const files = (group: string): FileInfo[] => lists.get(group as Group) ?? []; + return { root, logicHost, comboHost, labHost, refresh, files, setActive, setOnPicked, filter }; } From df4aa43026fd25258b255ee6a3b777cc134ac15f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 22 Sep 2026 00:18:22 +0900 Subject: [PATCH 2/2] =?UTF-8?q?knowledge(M01):=20=EC=A1=B0=ED=95=A9=20?= =?UTF-8?q?=EB=A7=88=EA=B0=90=20=EA=B2=80=EC=A6=9D=20=E2=80=94=20=EB=A9=94?= =?UTF-8?q?=EB=AA=A8=20=EC=B9=B8=20=EB=AA=A8=EC=96=91=20=C2=B7=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=AA=A8=EC=96=91=20=EB=91=90=20=EA=B3=B3=20?= =?UTF-8?q?=EC=96=B4=EA=B8=8B=EB=82=A8=20(=EC=9D=BC=EA=B0=90=2024)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01V1MKKZKpUHTPKb513FneU8 --- resources/master_data/ref/_검증_조합.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 resources/master_data/ref/_검증_조합.md diff --git a/resources/master_data/ref/_검증_조합.md b/resources/master_data/ref/_검증_조합.md new file mode 100644 index 00000000..be73126d --- /dev/null +++ b/resources/master_data/ref/_검증_조합.md @@ -0,0 +1,20 @@ +# 검증 — 일위대가 조합 (일감 24) + +읽기 전용 · 시험용 복사본 폴더에서 서버 길(TestClient)로 만들고 지움 — 정본 `일위대가조합.json`(줄 0)·`_키대장.json` 은 안 건드림. + +## 어긋난 것 (2) + +1. **`메모` 칸으로 수량이 새 들어갈 수 있음** — 담은 로직 줄의 `메모` 에 글자가 아닌 값(숫자 `5` · 묶음 `{"수량": 3}`)을 보내도 200 으로 저장됨. 검사(`master_combo.check_row`)는 칸 이름(로직 · 메모)만 보고 `메모` 값의 모양은 안 봄 → 4-0(조합에 수량·계산 없음)의 뒷문. 재현: `POST /combo/new` 에 `담은로직: [{"로직": "GF000219", "메모": {"수량": 3}}]`. 고칠 곳: `check_row` 에서 `메모` 는 글자 또는 null 만. `check_master` 도 같은 함수를 써서 같이 잡힘. +2. **`이름` 이 글자가 아니어도 저장됨** — `이름: ["x"]` 가 200 으로 저장(`UA000005`) · `check_combo_form` 도 `str()` 로 감싸 통과. 고칠 곳: 이름은 글자만(서버 400 · 틀 검사). + +참고(계약대로라 어긋남 아님 · 확인만): `구분` · `단위` 를 안 주면 null 로 저장됨 — 미리 보기 `결과단위` 없는 조합이 생길 수 있음. + +## 통과한 것 + +- ① 만들기 → 읽기 → 고치기 → 지우기: 키는 서버가 `UA` 다음 번호로 줌(지운 번호는 다시 안 씀) · 열 한 벌(키·이름·구분·상세구분·단위·담은로직·출처·소유·비고)이 그대로 저장 · 고치기로 로직 빼기 · 지우면 줄 0. `키`·`소유` 를 몸에 실어 보내도 서버 것이 이김. +- 수량·계산 칸: 조합 위쪽 여분 칸(`수량` · `계산` · `비율`)은 200 이되 **저장되지 않고 버려짐** · 담은 로직 줄의 여분 칸(`수량` · `계산`)은 422(「모르는 칸」). +- ② 검사: 없는 로직 키(422 「없는 요소」) · 모양 틀린 키 · 같은 로직 두 번(줄 번호 표시) · 빈 조합 · `담은로직` 없음·null · 조합이 조합을 담기(`UA…`) · 로직 아닌 키(`EA…`) · 목록 아님 · 줄이 묶음 아님 · 이름 공백 400 · 모르는 소유 400 · 없는 키 404 — 모두 막힘. 거절된 요청은 파일·키 번호를 바꾸지 않음(같은 번호가 다시 나옴). +- 낡은 판본: 고치기·지우기 모두 409 `{stale: ["일위대가조합.json"]}` · 거절된 고치기 뒤 판본 그대로. +- 담은 로직이 나중에 사라지면: 조회는 200 + `blocked` true + 까닭 · 미리 보기는 그 줄만 멈추고 나머지 합계는 냄. +- ③ 미리 보기 합계: 로직 셋(GF000219 산림 찾기 · GC000996 덧줄 있음 · GC000994 로직 부르기 있음)을 담아 각 로직의 시험 계산과 비목별로 대조 — 줄마다 노무비·재료비·경비가 같고, 합계도 노무·재료·경비 비목별 합과 같음(계 729,595.598). 저장 전 조합 줄로 보내도 같은 합계 · 미리 보기는 저장하지 않음(줄 수 그대로). 입력에 없는 이름을 주면 그 줄만 「입력 … 없음」으로 멈춤. `/logic/combos` 거꾸로 찾기 · 목록의 `count` · 소유·찾기 거름도 맞음. +- ④ `check_master`: 조합 검사가 들어 있음(`조합` 모드 · `전부` 모드에 포함 · 틀은 `check_combo_form`) — 정본에서 틀 0 · 로직 0 · 조합 0건. 시험 `test_m01_combo.py` 17 통과.