diff --git a/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts b/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts index cb51508ff..0d3bcc8d2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts @@ -66,6 +66,7 @@ const TEXT = { saveLabel: "저장하고 보내기", failed: "표를 못 만듦 — {value}", noRange: "보낼 범위를 시트에서 먼저 고름", + empty: "보낼 칸에 내용이 없음", place: "도면에서 표를 놓을 자리를 누름(Esc = 그만)", noPlace: "이 도면 화면은 표 놓기를 아직 못 함", stale: "면적산출 값이 바뀐 표 {n}개", @@ -129,6 +130,10 @@ export interface AreaToDrawing { onArea: (at: Selection, readOnly: boolean, slot: HTMLElement) => void; /** 도면 상세가 섰음 — 넘겨받은 표 놓기 · 연결표 다름 알림 */ onDrawing: (handle: unknown, at: Selection, doc: StructureDoc) => void; + /** 보낸 표 놓기 기다리는 중 — 편집 칸 「저장 안 한 고침」 에 듦 */ + waiting: () => boolean; + /** 떠나기로 함(물음 [확인]) — 기다리던 표 버림 */ + drop: () => void; /** 도면 저장 문서 — 맨 위 `연결표` 를 도면의 표에 맞춤 */ withLinks: (doc: StructureDoc) => StructureDoc; } @@ -141,6 +146,8 @@ export function createAreaToDrawing(opt: AreaToDrawingOptions): AreaToDrawing { let fresh = new Map(); let latest = new Map>(); let shown: string | null = null; + /** 보낸 표가 도면에 놓이길 기다림 — 떠날 때 「저장 안 한 고침」 물음(`waiting`) */ + let waiting = false; /** 글자 크기 작은 창 — 보내기면 크기 · 닫으면 null */ const askSize = (spec: { 범위: string }): Promise => @@ -183,15 +190,20 @@ export function createAreaToDrawing(opt: AreaToDrawingOptions): AreaToDrawing { if (opt.isDirty()) return; // 저장 실패 — 알림은 저장 길이 띄움 } const picked = range; + const get = (size: number): Promise => + fetchTable(at.layer, at.name, { 시트: picked.시트, 범위: picked.범위, 글자크기: size }).catch( + (error: unknown) => (showToast(fill(TEXT.failed, "value", why(error)), "error"), null), + ); + // 보이는 글이 하나도 없는 범위(빈 칸만)는 크기를 묻기 전에 막음 + let got = await get(SIZE); + if (!got) return; + if (!Object.values(got.값 ?? {}).some((v) => String(v ?? "").trim())) { + return showToast(TEXT.empty, "error"); + } const size = await askSize(picked); if (size === null) return; - const spec: LinkSpec = { 시트: picked.시트, 범위: picked.범위, 글자크기: size }; - let got: AreaTable; - try { - got = await fetchTable(at.layer, at.name, spec); - } catch (error) { - return showToast(fill(TEXT.failed, "value", why(error)), "error"); - } + if (size !== SIZE) got = await get(size); + if (!got) return; pending = { layer: at.layer, column: at.name, got, 글자크기: size }; await opt.open({ layer: at.layer, @@ -247,7 +259,10 @@ export function createAreaToDrawing(opt: AreaToDrawingOptions): AreaToDrawing { return { select: (s) => (range = { 시트: s.시트, 범위: s.범위 }), + waiting: () => waiting, + drop: () => (waiting = false), onArea: (at, readOnly, slot) => { + waiting = false; // 범위는 그대로 — 시트가 처음 고름(A1)을 이 갈고리보다 먼저 알림 if (readOnly || at.layer === "project") return; const button = createButton({ label: TEXT.send, variant: "ghost", size: "sm" }); @@ -257,6 +272,7 @@ export function createAreaToDrawing(opt: AreaToDrawingOptions): AreaToDrawing { slot.append(button); }, onDrawing: (raw, at, doc) => { + waiting = false; const handle = raw as TableHandle; shown = `${at.layer}:${at.name}`; range = null; // 면적 상세를 떠남 @@ -273,12 +289,14 @@ export function createAreaToDrawing(opt: AreaToDrawingOptions): AreaToDrawing { const style = { ...(표.shapeData.style ?? {}), [LINK]: id }; fresh.set(id, { 연결: id, 시트, 범위, 글자크기: give.글자크기, 값 }); handle.placeTable({ ...표, shapeData: { ...표.shapeData, style } }, id); + waiting = true; // 놓기 전 떠나도 물음(놓으면 도면 고침 · [저장] 때 풂) showToast(TEXT.place, "info"); } } void checkStale(handle, at, doc); }, withLinks: (doc) => { + waiting = false; const old = new Map((doc.연결표 ?? []).map((row) => [row.연결, row])); const rows: TableLink[] = []; for (const e of doc.도면?.entities ?? []) { diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts index f62db757e..3e6a63430 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts @@ -532,8 +532,9 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle { c.host.replaceChildren(); } const source = opts.source(); - // 0.3초 넘으면 카드마다 칸 안 「불러오는 중」(PLAN 30-6 · 31-7) - const stops = cards.map((c) => c.shell.loading()); + // 0.3초 넘으면 카드마다 칸 안 「불러오는 중」(PLAN 30-6 · 31-7) — 일위대가 카드는 뺌: + // 몸(Labor_View)은 따로 그려지고 제 「불러오는 중」 이 있음 · 넣으면 덮였다 지워져 빈 카드(40 D1) + const stops = cards.filter((c) => c !== laborCard).map((c) => c.shell.loading()); const [drawing, ...books] = await Promise.allSettled([ source.read("structure", col.id), ...sheets.map((c) => source.read(c.kind, col.id)), diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.css b/M02_MasterTemplete/M02_MasterTemplete_Labor.css index 6bccbd95a..220c81a2a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor.css @@ -282,6 +282,52 @@ } } +/* 더 좁으면(카드 320px 안팎 · 340px 이하) 줄마다 두 줄 — 윗줄 항목(3칸) · 수량산출(1칸) · 아랫줄 금액 넷 · + 머리 줄도 같은 꼴(글은 그대로) · 가로 넘침 없음 */ +@container m02-labview (max-width: 340px) { + .m02-labline, + .m02-labline thead, + .m02-labline tbody, + .m02-labline tfoot { + display: block; + width: 100%; + } + + .m02-labline tr { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + border-bottom: 1px solid var(--color-border); + } + + .m02-labline th, + .m02-labline td { + display: block; + min-width: 0; + padding: 2px; + border: 0; + } + + .m02-labline th:first-child, + .m02-labline .m02-labrow__name { + grid-column: span 3; + } + + .m02-labline th:nth-child(2), + .m02-labline .m02-labrow__qty { + text-align: right; + white-space: normal; + } + + .m02-labline th:nth-child(3), + .m02-labline .m02-labrow__unit { + display: none; + } + + .m02-labline .m02-labrow__num { + font-size: 11px; + } +} + /* 수량산출 칸(40-1) — 위 결과단위 기준 수량 · 아래 지금 수량 · 누르면 손 입력 · 엑셀에서 고르기 */ .m02-labrow__qty { white-space: nowrap; diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 7e6d10079..13630fac5 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -185,7 +185,7 @@ export function createMain( let seq = 0; let dirty = false; /** 저장 안 한 고침 — 이 화면 + 카드(도면 31-4 · 면적산출 31-3) */ - const busy = (): boolean => dirty || !!panel?.dirty(); + const busy = (): boolean => dirty || !!panel?.dirty() || toDrawing.waiting(); // + 놓기 기다리는 표(41) const guard = (event: BeforeUnloadEvent): void => { if (!root.isConnected) return window.removeEventListener("beforeunload", guard); @@ -204,6 +204,7 @@ export function createMain( return false; } dirty = false; + toDrawing.drop(); return true; }; setLeaveGuard(confirmLeave); // 머리 메뉴 · 뒤로 가기로 떠날 때도 물음 diff --git a/docs/raw/plans/2026-10-04_plan_M02구조물조합개편_2차_도면표.md b/docs/raw/plans/2026-10-04_plan_M02구조물조합개편_2차_도면표.md new file mode 100644 index 000000000..03f77c17e --- /dev/null +++ b/docs/raw/plans/2026-10-04_plan_M02구조물조합개편_2차_도면표.md @@ -0,0 +1,78 @@ +# 지난 계획 — 39~41장 (2026-10-04) + +M02 구조물 조합 개편(일위대가 = 구조물마다 · 재료산출 · 선택 입력 지움 · 실무문서 · 권한 · 오른쪽 클릭) · 2차 수정(단가산출 로직 · 수량산출 열 · 카드 3열 · 삭제 · 알약 끌어 놓기 · 색) · 면적산출 표 → 도면 표. 검증은 사용자(10-4). + +## 32장에서 39 로 넘긴 줄 + +- [X] 선택 입력 정리(조사 10-1 끝 · 고침은 사용자 지시 뒤) — 일위대가 「넣을 값」과 구조물 「선택 입력」 이름 · 값 칸 규칙 두 벌 · 사라진 입력 안 알림 · 항목마다 자리 다시 고름 · 39 로 넘김(10-4 · 선택 입력 지움 · 값 칸 규칙 한 벌 · 사라진 값 알림은 39-3) +- [X] 로직 지우기 서버 검사가 마스터 층 조합만 봄(10-1 sub4) — 회사 · 개인 조합에 담긴 로직도 막기(`M01_MasterData_Store_Combo.py:194` · `Store_Cache.py:220` · 쉬운 길은 화면에서 미리 찾아 알림) · 39 로 넘김(10-4 · 39-1b 로직 지우기 검사 = 모든 층 labor) + +## 39. M02 구조물 조합 개편 (사용자 지시 10-4) + +- 사용자 요구 원문 = `docs/raw/M02_구조물조합개편/사용자요구_1004.md`(2 일위대가 → 구조물 조합 하위 · 3 이름 · 4 구분선 · 5 권한 · 6 항목 변경 · 9 오른쪽 클릭) +- 사용자 확정(일위대가): 구조물 조합에 일위대가 · 도면 · 면적산출이 엮임 · 구조물 조합 복사 = 일위대가 새 키(로직 · 값 같이 복사) · 새 구조물 조합 = 미리보기에서 「새로 만들기」 / 「복사 생성」(목록 · 기본 탭 = 일반 개인 · 회사 관리자 개인 · 시스템 관리자 마스터 · 탭으로 다른 곳) · 항목 하나만 다른 것을 복사해 바꿈(저장 뒤에도) · 매칭 안 된 일위대가 조합 = 마스터 「실무문서」 항목으로 따로(나중에 한 번에 지움) +- 사용자 확정(10-4): 원본 단가산출 로직(resources/master_data 로직 · 표 · 요소 · 마스터 일위대가조합.json)은 절대 손대지 않음(읽기만) · 입력값은 사용자 · 설계자가 일위대가에 정해 저장 → 프로젝트로 복사해 씀 +- 정함(브레인): 조사 먼저(창마다 자기 몫 · 코드 안 고침) → 브레인이 설계 정함(이 절에 줄 더함) → 구현 · 조사 보고 = 각 창 `tmp/조사_39-0*.md` + +- [X] 39-0 조사(보고 여덟 · 정함 = `docs/raw/M02_구조물조합개편/설계_정함.md`) — A 좌측 패널 · 이름 · 구분선(sub5) · B 자료 틀(일위대가 입력값 · 자재목록 · 수량 연결 자리 · sub7) · C 일위대가 상세(sub2) · D 면적산출(sub3) · E 권한(sub4) · F 오른쪽 클릭(sub8) · G 재료산출 · 선택 입력 지우기 영향(sub6) · H 「전체 항목」 목록 화면(sub1) +- [X] 39-9a 오른쪽 클릭 막기(sub8) — 정함(브레인): 공용 진입 한 곳 창 캡처 듣기 · 대상 주소 `/^[bm]\d/` · 글 입력 칸(읽기 전용 포함)은 브라우저 메뉴 살림 · 입력 칸 밖 고른 글은 막음 · CAD 는 cad_host iframe load 때 같은 함수 · 남은 막기 코드 그대로(주석 셋만) · 근거 sub8 `tmp/조사_39-0F.md` — 확인 = 새 시험 test_common_native_menu.py · ORCA `defaultPrevented` 수치(B01 · B05 · B07 · M01 · M02 참 · 입력 칸 · A01 거짓) + - 검증완료(sub8 · 0f8efaa42) — 시험 4 통과 · ORCA 5183 B01 · B05(노선 편집 패널 포함) · B07 · M01 · M02 본문 · 헤더 · 모달 · CAD iframe 리본 · 패널 · 캔버스 막힘 참 · 입력 칸(CAD 안 포함) · A01 거짓 · CAD 오른쪽 단추 확정 그대로 +- [X] 39-9b 메뉴 공통 양식(sub8) — 공용 CSS 한 벌(지도 · 표 메뉴 · 나무 메뉴 · 상태줄 흠) · 「한 번에 하나」 장부 합침 · 조건 만들기 = 칸 메뉴 끝 한 줄(가로채기 지움) · 부품 API 합치기는 안 함 — 확인 = 메뉴마다 폭 · 그림자 · 글자 수치 같음 · 스프레드시트 하네스 시험 + - 검증완료(sub8 · 277311955) — B04 관 · B05 종단 · 배수 · 3D · 칸 · 상태줄 · M02 나무 · 조건 만들기 메뉴 폭 160 · 글자 13 · 여백 8/12 · 모서리 8 · 그림자 같음(밝음 · 어두움) · 나무 ↔ 칸 메뉴 한 번에 하나 · 시험 52 통과 · 조건 만들기를 쓰는 곳(M01 흐름 식 칸)은 칸 메뉴 없는 미리보기 편집기 → 같은 공용 메뉴에 이 줄만 · 시트 탭 메뉴는 탭 켠 화면이 없어 화면 확인 못 함(같은 부품) +- [X] 39-1a 자료 틀(sub7) — labor 종류 · SU 번호 · 문서 검사 · 계산(기준 ③ 서버 · 반영 ① 짝 · [저장] 때 계산값) · `_PER` 1,000 · calc · pills 길 — 확인 = 새 시험 test_m02_labor_doc.py · 거울 시험 · 기존 store · layers 시험 +- [X] 39-1b 옮기기 · 복사 · 프로젝트(sub7) — 옮기기 스크립트(시스템 층 바로 · storage 는 사본 뒤 브레인) · copy_from 층 넘김 + 실무문서 조합 → 일위대가 · 구조물 복사 = 새 SU — 확인 = 옮기기 시험(UP10-000001 → 열 둘) · tree_paste · item 시험 + - 검증완료(sub7) — 새 시험 labor_doc 19 · 거울 3 · migrate 4 · labor_copy 6 + store · structure · structure_combo · item_keys · layers_store · tree_paste · combo_layers · combo_branch · item_inputs · layer_access · M01 combo · cache 통과 · typecheck · 8009 실제 길: item1 SU-000001 기준합 2,658,983 · 알약 13 · 칸 값 3 → 수량출처 칸 · 시스템 층 옮김 item1←UA000008 · item3←UA000001 · item2 빈 · storage 층 안 돌림 · B8 은 sub8(39-1c) · 은퇴 길(use-combo · 갈라 내기 · preview)은 화면이 아직 불러 남김 · 로직 지우기 검사는 ecc4a651a 로 먼저 함(39-1d 와 겹침) +- [X] 39-1d 은퇴 · 로직 지우기 검사(sub4) — 로직 지우기 검사 = 모든 층 labor 담은로직 + 실무문서 · 일위대가 갈라 내기 · 키 적기(use-combo) · 조합 고치기 길과 화면 부름 정리(쓰는 곳 0 확인 뒤) · 남의 층 기록 = 성공한 쓰기만 — 확인 = 로직 지우기 시험(회사 · 개인 labor 에 담긴 로직 막힘) · grep 0 + - 검증완료 — 로직 지우기 시험(회사 · 개인 · 마스터 labor · 실무문서 → 422 · 어디서 쓰는지 알림 · 다 빼면 지워짐 · 임시 사본) · M01 시험 56 · 조합 층 14 · 권한 · 양식 18 통과 · 남의 층 기록 = 성공한 쓰기만(409 · 403 안 남김) +- [X] 39-1d 남은 은퇴(sub4 · sub3 D3 · sub5 받은 뒤) — 쓰는 곳이 남아 못 지움: 갈라 내기 `branch_combo` · 키 적기 `use_combo`(Branch.py · Router.py · Router_Items 길 · Api_Fetch `branchCombo` · `setColumnCombo` ← Combo.ts · ComboPanel.ts) · `fetchUsableCombos` · `/combos/all` ← ComboPanel · Structure · 조합 만들기 · 지우기 길(POST · DELETE) ← 죽은 Labor_Edit.ts(sub2) · 조합 시험 · `Combo_Store.edit`(sub5) · Labor_Api `newCombo` · `editCombo` · `deleteCombo` · `fetchAllCombos` · `editComboTree*` · `pasteCombos` · `fetchLibLogic` · `fetchLibAuto`(sub2 · sub5) + - 검증완료 — 지움: Branch.py · 갈라 내기 · use-combo(옛 · 층 길) · 조합 만들기 · 지우기 · 알약 두 길 · grep 0 · typecheck · 시험 m01_combo · combo_inputs 29 · combo_layers 9 · combo_branch · layers_store 통과 · 남김: /combos/all · fetchUsableCombos(항목 추가 창 일위대가 고르기 pickCombo) · 집계표 열 「일위대가」 칸(그 창이 씀) · 목록 · 키 읽기(Practice · ItemPick) · 실무문서 통째 지우기 · Combo_Store create · delete · pills(sub5 · 시험 · Labor_Doc 알약) +- [X] 39-1c 프로젝트 층(sub8 · 39-1a 뒤) — 스냅샷 일위대가 = labor 문서 · 입력값.json 첫 값 = labor 입력값 · 단가.json 자재 MT · 계산 반영 같은 함수 · B05 고르기 모달 카드 = 도면 · 일위대가(C 보기 부품) · 면적산출 + 값 카드 — 확인 = item 시험 · 고르기 모달 값 → /calc 합계 + - 검증완료(sub8 · e1ad3d362 · 1baf761c4) — 시험 67 통과(새 test_m02_item_labor 5 · picker 20 · copy · inputs) · ORCA 5183 sub8_검증 B05 고르기 모달 카드 도면 · 일위대가 · 면적산출 · 값 넣고 저장 → /calc 반영 합 337,721.97 = 손셈 = 원본 층 계산 · 빈 값 = 멈춘 로직 1 · 빠진 입력 1(붉게) · 저장 안 하고 뒤로 = 버릴지 물음 · 일위대가 쓰기 400 · 밝음 · 어두움 +- [X] 39-1e 프로젝트 면적산출 알약 = labor 알약(sub8 · 39-7b 뒤) — 옛 입력연결 알약 대신 스냅샷 labor 알약 · 정함(브레인): 프로젝트 계산은 빠진 입력이면 그 로직 멈춤 그대로(입력 유도 · 원본 층은 자동값) — 확인 = 고르기 모달 면적 편집 알약 수 = labor 알약 수 · sub6 G3(c704f1898)이 함께 바꿈 · 39-8 에서 확인 +- [X] 39-0i 통합(sub5) — dev 에 sub2 · sub7 · sub3 · sub1 · sub4 · sub8 합침(Api_Fetch · Store_Cache 로직 지우기 검사 두 벌 → sub4 하나 · Structure.ts import) → sub6 이 자기 충돌 풂 → 모두 받기 · 통합 1 f0b8882a8 · 통합 2(sub6 · sub1) 진행 +- [X] 39-2 좌측(sub5) — 일위대가 컨테이너 · 나무 지움 · 하위 차례 도면 → 일위대가 → 면적산출 · 수평선 · 「전체 항목」 줄 · 이름 「일위대가」 · 실무문서 컨테이너(시스템 관리자) · 나무 서버 길 지움 · 대시보드 카드 — 확인 = typecheck · ORCA 5180 줄 수 · 세 역할 화면 + - 검증완료(sub5) — typecheck · 고친 시험 · ORCA 5180 시스템 관리자: 하위 차례 · 수평선(밝음 · 어두움) · 전체 항목 셋 · 일위대가 열 줄 3(SU 는 39-1b 옮기기 뒤 · 지금 「없음」) · 실무문서 37 · 상세 · 지우기 물음 취소 · 회사 · 개인 층엔 실무문서 없음 · 회사 관리자 · 일반은 서버 403 시험(계정 화면은 못 봄) +- [X] 39-3 일위대가 상세(sub2) — labor 종류 상세 · 입력 모달 · 작은 글 · 금액 두 줄 · 자재목록 · 미리 보기 지움 · 다이어그램 · 보기 갈래 부품 · 입력 부품 옮김 — 확인 = 거울 · 다이어그램 시험 · ORCA 5177 값 넣고 저장 → 다시 열기 + - 검증완료(43a59a689 · d847f724c · 3ad7678e1 · 5b4aed1b2) — 시험 통과 · ORCA 5177 item1: 기준 60,984 × 손 5 = 반영 304,922 · 자재 6,090 × 10 = 60,900 · 합 365,822 = 저장 계산값 · 다시 열어 값 남음 · 범위 밖 · 빈 손 수량 · 같은 로직 막음 · 떠날 때 물음 · 어두움 · 남음 = 읽기만 화면(권한 계정) 미확인 · 검증용 labor/item1.json 지움 +- [X] 39-4 면적산출(sub3) — 알약 켬끔(기본 꺼짐) · 오른쪽 패널 = 담은 로직 + 자재목록 · 칸 지정 모달 · setLink · 자재 알약 · 조합 패널 지움 — 확인 = setLink 시험 · ORCA 5178 칸 잇기 → 금액 즉시 · 검증완료(sub3 · 시험 31 · ORCA 5178: 카드 알약 줄 기본 0px → 켬 147px → 다시 열어도 그대로 · 상세 늘 보임 · 오른쪽 패널 로직 14 줄 · C3 = 12.5 잇기 → 반영 726,535 · 20 → 1,162,456 바로 · 「수량 C3 = 12.5」 · 알약 ↔ 모달 같은 연결 · 빈 · 틀린 주소 막음 · 풀기 · 어두움 · 저장 안 하고 옮김 물음 · 옛 조합 패널 쓰는 곳 0) · 검증 대기 +- [X] 39-5 목록 · 고르기 · 카드(sub1) — ItemList · 전체 항목 화면 · 상세 제목 M01 꼴 · 새 항목 두 단추 · 복사 생성 탭 · 카드 셋 · 빈 카드 두 단추 · 복사해 가져오기 — 확인 = item_list 시험 · ORCA 5174 복사 생성 → 새 SU + - 검증완료(18a063925 · 134a727c9 · 364d22ff6 · 6cf72bbf1 · 6f8470890 · 6d46dda54) — 시험 통과 · ORCA 5174 마스터: 전체 항목 줄 수 = 좌측 줄 수(3 · 3 · 3) · 제목 18px · 탭 = 시스템 관리자 다섯(기본 마스터) · 일반 셋(기본 개인) · 빈 사람 = 빈 목록 · 복사 생성 item1 → SS000055 · SD-000004 · SA-000004 · SU-000004(로직 14) · 폴더 안 같은 이름 「ㄴㄴ (2)」 · 실무문서 UA000001 → SU-000002 로직 3(번호 그대로) · 개인 → 마스터 면적산출(SA-000002 그대로) · 카드 차례 도면 → 일위대가 → 면적산출 · 어두움 · 마스터 사본 되돌림(차이 0) · 남음 = 재료산출 · 선택 입력 카드 지움은 sub6 받은 뒤 · 읽기만 계정 화면 미확인 +- [X] 39-6 권한(sub4) — 맡은 회사 규칙 · 사람 나무 · 첫 쓰기 뼈대 · 「〇〇로 복사…」 폴더 · 감사 기록 · 실무문서 문 · apply · sources · readableSources — 확인 = layer_access · 새 layer_manage 시험 · ORCA 세 계정 + - 검증완료 — 시험 layer_access 4 · template_layers · combo_layers 14 · layer_screen(39-6 몫) 통과 · 관련 M02 시험 66 통과 · API 세 계정 25/25 · ORCA 회사 관리자 · 일반 · 시스템 관리자(고객 회사 3) 사람 나무 · 「〇〇로 복사…」 폴더 창 · 밝음 · 어두움 · 쓰기 성공 길은 임시 storage 시험(새 파일 대신 layer_access 에 더함) +- [X] 39-7a 지우기 G1 · G2(sub6) — 자재 조합 · 재료산출 지움 · pickMaterial 옮김 — 확인 = typecheck · check_master · 고친 시험 · ORCA 5181 카드 · 줄 +- [X] 39-7b 지우기 G3(sub6 · 39-3 입력 모달 뒤) — 선택 입력 카드 · inputs · 자리 · 알약 길 지움 — 확인 = 프로젝트 고르기 값 → /calc 합계 그대로 + - 검증완료(sub6 · 54ce67ceb · 61d2ed599 · 79e4bba1b · c704f1898): typecheck 0 · check_master 틀 0건 · 고친 대상 시험 초록(test_m02_card_shell 1건은 origin/dev 자체 빨강) · ORCA 5181 좌측 = 구조물 도면 · 일위대가 · 면적산출 · 집계표 줄 = 일위대가 · 도면 · 면적산출 · 카드 = 도면 정보 · 면적산출(선택 입력 없음) · 면적산출 편집 알약 = 일위대가 알약 · 옵션 제목 밝음 · 어두움 · /calc = 시험(test_item_copy_complete)으로 · 프로젝트 고르기 모달 면적 알약 = 복사본 labor 알약으로 바꿈(39-1e 몫과 겹침) +- [X] 39-1f 새 항목 창 일위대가 고르기 없앰(sub4) — 새 구조물 일위대가 = 늘 빈 문서 · 카드 [새로 만들기] / [복사 생성] · 열 「일위대가」 새로 쓰기 없앰(든 값은 옮기기 뒤 정리) — 확인 = 창 줄 수 · 새 열 빈 labor · sub4 8087c7306 · 창 칸 둘 · 새 열 labor 빈 문서 · 시험 64 +- [X] 39-8 끝 검증 — 흐름 셋(새 구조물 새로 만들기 · 복사 생성 · 항목 하나 복사) × 세 역할 · 일위대가 값 → 금액 · 면적 칸 → 반영 · 실무문서 · storage 옮기기 뒤 확인 · 검증완료(가 sub1 19/22 · 나 sub2 26/27 → 흠 ①~④ · H1~H3 · X3 모두 고침) · main 합침 e6fe5232e · storage 옮기기(브레인 · 사본 tmp/storage_templates_39.zip · labor 5 · 두 번째 「그대로 5」) · 남은 빨강: spreadsheet 늦게 불러오기 시험 1(흐름도 DataFlow · 39 밖) · 검증 대기 +- [X] 39-10 요구 대조(브레인 · 사용자 요청 10-4) — 끝난 뒤 사용자 대화 입력 원문(요구 2~6 · 9 · 답 1~4 · 원본 로직 손대지 않음 · 입력값 복사 흐름) 줄마다 반영 여부 · 근거(화면 · 파일 · 수치) 표 · 요구 31 줄 모두 반영(흠 B1~B4 고친 뒤) · 표 = 보고 Artifact · 검증 대기 +- [X] 39-11 브레인 실제 검증(사용자 요청 10-4) — 끝난 뒤 브레인이 사용자처럼 브라우저 조작(진짜 마우스 · 사진 · 세 역할) · 보고 Artifact · 세 계정 · 진짜 마우스 · 사진(sub5 서버 s01~s15 · 메인 m01~m04) · 흠 B1(표 일위대가 줄 SU) · B2 · B3(전체 항목 합계 · 단위) · B4(새 항목 단추) 고침 → main 11ef83985 · 저장 안 함 · 검증 대기 + +## 40. M02 구조물 조합 개편 2차 (사용자 지시 10-4 밤) + +- 사용자 요구 원문 = `docs/raw/M02_구조물조합개편/사용자요구_1004_2.md` · 「구조물 옵션 보이기」 칸은 그대로(사용자 확인) +- 정함(브레인): 「담은 로직」 → 「단가산출 로직」(상세 · 카드 · 면적 패널 모두) · 수량산출 열 = 위 결과단위 기준 수량(분모 · 예 「10 ㎡」) · 아래 수량(손 · 칸 · 「—」) · 누르면 [손 입력] / [엑셀에서 고르기](그 열 면적산출 시트 모달 · 칸 누르면 그 로직 알약을 그 칸에 이음 · 일위대가 [저장] 때 면적 문서도 함께 저장) · 버튼 = 공용 버튼 양식 · 카드 단추 「가져오기」 · 「수정」(면적산출 카드도 같은 이름) · 카드 [삭제] = 그 항목 비우기(번호 · 열 그대로 · 물음 한 번) · 일위대가 카드 = 한 줄 표(항목 · 수량산출 · 단위 · 노무비 · 재료비 · 경비 · 합계 · 금액 = 수량 반영 값 · 없으면 기준 값 흐리게) · 알약 색 = 알약 키로 자동(두 테마) · 이은 칸 배경 같은 색 옅게 · 반짝임 그대로 + +- [X] 40-1 일위대가 상세(sub2) — 이름 「단가산출 로직」 · 「자동값 xx」 조사 → 브레인 정함 → 고침 · 수량산출 열 + 손 입력 · 엑셀 모달 잇기(sub3 부품) · 로직 추가 · 올리기 · 내리기 · 빼기 · 자재 추가 = 버튼 · Labor_View 한 줄 표 갈래(카드용) — 확인 = 시험 · ORCA 수량 칸 손 · 엑셀 고름 → 반영 값 · 저장 → 다시 열기 + - 검증완료(1c0868b78 · fd4830556 · e70fc184a · 35ccc26af) — 시험 34 통과 · ORCA 5177 SU-000003: 손 5 → 79,280 × 5 = 396,398 · 엑셀 B6(12.5) → 4,523 × 12.5 = 56,537 · 저장 → 면적 연결 「노체포설|㎥ → B6」 · 손 5 · 반영합 = 화면 · 다시 열기 같음 · 저장 안 하고 떠나면 물음 · 고르기 칸 「자동값」 줄 없음(B05 값 카드 확인 상자 그대로) · 한 줄 표 300~560 · 면적 패널 320~560 가로 넘침 없음 · 밝음 · 어두움 · 검증용 마스터 2 파일 되씀 +- [X] 40-2 구조물 조합 상세(sub1) — 카드 단추 「가져오기」 · 「수정」 · 일위대가 카드 한 줄 표(sub2 갈래) · 「새 항목」 버튼 · 카드 셋 3열 · 카드 [삭제](비우기) — 확인 = ORCA 3열 · 삭제 → 빈 카드 · 시험 + - 검증완료(74a524202 · e3236558b) — 시험 53 통과 · ORCA 127.0.0.1:5174 폭 1600 = 3열(카드 402px · 높이 628 같음) · 1000 · 400 = 1열 · 가로 넘침 0 · 400 에서 면적 카드 단추 여섯 줄 바꿈(카드 밖 0) · 단추 「수정」 · 「가져오기」 · 「삭제」 · 「새 항목」 높이 32 · 글 14px(저장과 같음) · item1 일위대가 [삭제] → SU-000001 · item1 그대로 · 담은로직 14 → 0 · 빈 카드 두 단추 · 면적산출 [삭제] → SA-000001 그대로 · 칸 · 연결 0 · 일반 계정 마스터 · 회사 카드 단추 0 · 어두움 · 마스터 사본 되씀(차이 0) · 남음 = 도면 [삭제] 는 마스터 도면이 다 비어 화면 확인 못 함(서버 길은 40-4 시험) +- [X] 40-3 면적산출 상세(sub3) — 알약 끌어 칸에 놓기 추가 · 알약 색 자동 · 이은 칸 같은 색 · 반짝임 그대로 · 엑셀 칸 고르기 모달 부품(sub2 용) — 확인 = 끌어 놓기 → 연결 · 색 수치 · 두 테마 · 검증완료(sub3 · 시험 16 + 하네스 15 · ORCA 5178: 칸 고르기 모달 C5 · 풀기 · 그만둠 · 끌어 C3 → 연결 · Ctrl+Z 풂 · F14 에 놓기 = 물음(취소 그대로 · 바꿈 = 갈아끼움) · 색 24 서로 다름 · 테두리 대비 최소 밝음 3.40 · 어두움 3.55 · 글 8.85 · 6.35 · 카드 · 상세 둘 다 · 마스터 변화 0) · 검증 대기 +- [X] 40-4 서버 비우기 길(sub7) — 항목(도면 · 일위대가 · 면적산출) 비우기 = 빈 문서로 씀(번호 · 열 그대로 · 권한 규칙 그대로) — 확인 = 시험(세 종류 · 권한 403) + - 검증완료(sub7) — test_m02_part_clear 7(세 종류 빈 문서 = 새 열 꼴 · 번호 · 장부 그대로 · 면적 비우면 일위대가 수량 「없음」 · 일반 마스터 403 · 남의 개인 403 · 회사 관리자 동료 개인 200 · 없는 열 404 · 모르는 종류 422) · typecheck · 00cb6db66 · 화면 함수 clearPart +- [x] 40-5 합치기 · 끝 검증 · 메인 반영 · 브레인 요구 대조 · 실제 검증 · 보고 · main 795b861da(40 · 41 함께 · 흠 D1~D4 고침 · CAD 빌드 · 시험 90) · 끝 검증 · 브레인 실제 검증은 사용자가 하기로(10-4) · 검증 대기 + +## 41. 면적산출 표 → 도면 표 (사용자 지시 10-4 밤) + +- 사용자 요구 원문 = `docs/raw/M02_구조물조합개편/사용자요구_1004_2.md` 끝 「추가 기능」 — 면적산출에서 범위를 골라 도면으로 보내면 도면에 표가 들어가고 칸 내용이 따라감(어려우면 [업데이트] 단추) · 표 여러 개 · 자리는 도면에서 사용자가 끌어 정함 +- 정함(브레인): 조사 먼저(CAD 표 · 묶음 꼴 · 호스트 ↔ CAD 메시지 · 끌어 놓기 · 값 따라가기 길) → 브레인이 설계 정함 → 구현 · 원본 단가산출 로직 자료 손대지 않음 + +- [X] 41-0 조사(sub8) — 보고 `tmp/조사_41-0.md` · 끝(CAD 표 객체 TableEntity 있음 · 미리보기 틀 있음 · DXF 표 빠짐 흠) + +- 정함(브레인 · 조사 추천 그대로): 표 = CAD 표 객체(TableEntity · 한 덩어리) · 따라가기 = 면적 [저장] 때 서버가 그 열 도면 표 칸 글을 다시 적음 + 도면을 열 때 기록 값 ≠ 지금 값이면 알림 · [업데이트] · 연결 표지 = 표 style.연결(표 id) · 칸 key 「시트!A1」 · 보낼 꼴 = 표시 글(형식 적용) · 병합 · 가로 정렬 · 굵게 · 열 너비 비율(채움 · 테두리색 · 글자색 안 보냄) · 크기 = 표 글자 크기 하나(기본 2.5 · 보낼 때 바꿀 수 있음) · 넘김 = 같은 페이지 안(캐시 · 서버 안 씀) · 시작은 면적 상세 [도면으로 보내기] 단추(칸 메뉴는 뒤) · DXF 내보내기에 표 = 선 + 글 + +- [x] 41-1 서버(sub7) — 범위 → 표 칸 변환(Node 묶음 formatValue 다시 씀 · 새 M02_Area_Table.py) · POST /api/m02/layers/{층}/area/{열}/table · 면적 저장 때 도면 표 맞춤(마스터 · 회사 · 개인 층 + 프로젝트 층 길) — 확인 = 시험(형식 · 병합 · 한도 · 권한 · 저장 뒤 표 글 바뀜) + - 검증완료(sub7) — test_m02_area_table 10(형식 「1,234.50」 · 식 칸 · 굵게 · 가로 · 병합 rowSpan · 범위 밖 병합 잘라냄 · 숨긴 행 3 · 열 D 뺌 · 빈 칸 · 한도 171>100 · 없는 시트 · 범위 꼴 · 길 200 · 저장 안 함 · 남의 개인 403 · 없는 열 404 · 면적 저장 뒤 T1 칸 「10.00」 · 자리 · 크기 그대로 · 다른 표 그대로 · 표 없는 T2 기록 값 갱신 · 프로젝트 층 맞춤) + store · structure · template_layers · labor · clear · copy · spreadsheet_store 74 통과 · typecheck · 8009 실제 길 200 · 3ded39875 +- [x] 41-2 CAD · 호스트(sub8) — 메시지 place-table · update-table · 「표 놓기」 도구(커서 따라오는 미리보기 · 누르면 놓임 · Esc · 되돌리기) · cad_host · Drawing.ts 함수 · CAD 빌드 · DXF 표 = 선 + 글 — 확인 = vitest · ORCA 놓기 · DXF LINE · TEXT 수 + - 검증완료(sub8 · c426fa2ad · 0bc560663 · cc63e49ab · b2aa2a987) — vitest 8(놓기 · 연결 남음 · 칸 갈기 · 넘침 · 없는 연결) · DXF 시험 4(거울 = TS 표 기하 · 면적 표 LINE 13 · TEXT 5 · 정렬 L/C/R · B07 수량표 LINE 7 · TEXT 3 — 전에는 표가 빠짐) · ORCA 5183 M02 도면 상세: 미리보기가 커서 따라옴 · 누르면 놓임(drawing-changed) · Esc 그만 · Ctrl+Z 1→0 · 다시하기 · MOVE(새 id) 뒤 저장 JSON style.연결 T1 남음 · update-table T1 = 1 · 없는 연결 0 · 자리 · 너비 그대로 · 밝음 · 어두움 · 저장 안 하고 나감 · 자동백업 지움 +- [x] 41-3 화면(sub5) — 면적 상세 [도면으로 보내기](고른 범위 · 글자 크기) → 그 열 도면 상세로 넘겨 놓기 · [저장] 때 연결표 기록 · 다시 열 때 다름 알림 + [업데이트] · 표 여러 개 — 확인 = ORCA 범위 고름 → 보냄 → 끌어 놓음 → 저장 → 면적 값 바꿔 저장 → 도면 표 글 바뀜 + - 검증완료(sub5) — typecheck · test_m02_area_to_drawing 7 · structure_panel · ORCA 5180 마스터: A1:C4 보냄 → 놓음 → 저장(표 T1 · 연결표 T1) · 면적 A1 · B1 · C2(식) 저장 → 도면 표 글 길이 · 12.5 · 25 · 같으면 알림 0 · 기록 값 옛 값이면 「바뀐 표 1개」 + [업데이트] → 저장 값 갱신 · 표 셋(T1~T3 · 글자 5 · 2.5) · T3 지움 → 도면 · 연결표 둘 다 빠짐 · 마스터 사본 되씀(git 빈 상태) · 권한 없는 층 단추 0 은 시험으로만 diff --git a/resources/tester/test_m02_area_to_drawing.py b/resources/tester/test_m02_area_to_drawing.py index b0cf649be..b0e1a1923 100644 --- a/resources/tester/test_m02_area_to_drawing.py +++ b/resources/tester/test_m02_area_to_drawing.py @@ -94,6 +94,9 @@ globalThis.fetch = async (url, init) => { const old = { 연결: "T1", 시트: "s1", 범위: "A1:A1", 글자크기: 3, 값: { "s1!A1": "옛" } }; const drawing = (ents, links) => ({ 양식: "구조물도면", 종류: "structure", 판: 1, 열: "item1", 도번: "SD-1", 도면: { entities: ents }, ...(links ? { 연결표: links } : {}) }); ad.onDrawing(handle, { ...area, kind: "structure" }, drawing([table([["옛"]], { 연결: "T1" })], [old])); + const waitPlaced = ad.waiting(); // 놓기 기다림 = 저장 안 한 고침 + ad.drop(); + const waitDropped = ad.waiting(); await new Promise((r) => setTimeout(r, 20)); const notice = notices.at(-1) ?? []; const upd = notice.find((x) => x && x.on); @@ -109,7 +112,14 @@ globalThis.fetch = async (url, init) => { await slot.kids[0].on.click(); await new Promise((r) => setTimeout(r, 20)); ad.onDrawing(handle, { ...area, kind: "structure", name: "item2" }, drawing([])); - console.log(JSON.stringify({ ro, buttons: slot.kids.length, sent, placed, notice: notice.map((x) => (typeof x === "string" ? x : x.label)), saved: saved.연결표, plain: "연결표" in plain, other: placed.length - before })); + // 빈 칸만 고른 범위 — 크기를 묻기 전에 막고 알림 · 도면 안 엶 + now = [["", " "]]; + const openedBefore = opened.length; + ad.select({ 시트: "산출", 칸: "E1", 범위: "E1:F1" }); + await slot.kids[0].on.click(); + await new Promise((r) => setTimeout(r, 20)); + const empty = { opened: opened.length - openedBefore, toast: toasts.at(-1) }; + console.log(JSON.stringify({ ro, buttons: slot.kids.length, sent, placed, notice: notice.map((x) => (typeof x === "string" ? x : x.label)), saved: saved.연결표, plain: "연결표" in plain, other: placed.length - before, waitPlaced, waitDropped, waitSaved: ad.waiting(), empty })); })(); """ @@ -163,4 +173,14 @@ def test_다른_열_도면에는_안_놓음(got): def test_편집_칸은_갈고리만(): main = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts").read_text("utf-8") - assert main.count("toDrawing.") == 4 # select · onArea · onDrawing · withLinks + # select · onArea · onDrawing · withLinks · waiting(저장 안 한 고침) · drop(떠나기 확인) + assert main.count("toDrawing.") == 6 + assert "|| toDrawing.waiting()" in main.split("const busy = ")[1].splitlines()[0] + + +def test_놓기_기다림은_저장_안_한_고침(got): + assert got["waitPlaced"] is True and got["waitDropped"] is False and got["waitSaved"] is False + + +def test_빈_범위는_막고_알림(got): + assert got["empty"] == {"opened": 0, "toast": ["error", "보낼 칸에 내용이 없음"]} diff --git a/resources/tester/test_ui_loading.py b/resources/tester/test_ui_loading.py index 8ea31872e..f065511eb 100644 --- a/resources/tester/test_ui_loading.py +++ b/resources/tester/test_ui_loading.py @@ -82,5 +82,7 @@ def test_실패_글과_다시_불러오기_M02_자리(): assert "const stop = showLoading(view);" in practice assert "loadingError(why(error), () => void drawList())" in practice assert "loadingError(why(error), () => void drawOne(key))" in practice - assert "cards.map((c) => c.shell.loading())" in _text(M02 / "M02_MasterTemplete_ItemCards.ts") + cards = _text(M02 / "M02_MasterTemplete_ItemCards.ts") + # 일위대가 카드는 뺌 — 몸(Labor_View)을 덮었다 지우면 빈 카드(40 D1) + assert "cards.filter((c) => c !== laborCard).map((c) => c.shell.loading())" in cards assert "loading: () => showLoading(host)," in _text(M02 / "M02_MasterTemplete_CardShell.ts")