diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css b/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css index 9c4632c22..38a3d639f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css @@ -28,60 +28,12 @@ max-width: 320px; } -.m02-hopyo__row { - cursor: pointer; -} - -.m02-hopyo__row:hover { - background: var(--color-accent-soft); -} - -.m02-hopyo__num { - text-align: right; - font-variant-numeric: tabular-nums; -} - /* 실무자료 표 — 한글은 낱말 단위로만 줄바꿈(두세 글자마다 안 갈라짐) · 너무 긴 낱말만 쪼갬 */ .m02-hopyo__table :is(th, td) { word-break: keep-all; overflow-wrap: break-word; } -/* 확인 창 줄 표(338 · 339 · 359-3) — 일위대가 상세 꼴(m02-labrow) · 체크 칸 · 수량 · 단위 = 줄바꿈 없음 · - 단추 = 세로로 쌓아 좁게 · 이름 · 별칭 · 품 비교가 남는 폭 · 칸 여백 좁힘(공용 표보다 앞서게 두 갈래 이름) */ -.m02-hopyo__table.m02-hopyo__lines :is(th, td) { - padding: var(--spacing-8); -} - -.m02-hopyo__table.m02-hopyo__lines td:first-child, -.m02-hopyo__lines :is(.m02-labrow__qty, .m02-labrow__unit) { - white-space: nowrap; -} - -/* 이름 칸 대상 줄 — 키 · 이름 · 배 칸 · 몫 한 줄(상세 이름 칸의 한 줄 말줄임을 풂) */ -.m02-hopyo__lines .m02-labrow__name > .m02-hopyo__part { - display: flex; - max-width: none; - overflow: visible; -} - -.m02-hopyo__lines .m02-labrow__name > .m02-hopyo__state, -.m02-hopyo__lines .m02-labrow__kind > .m02-hopyo__state { - display: inline-block; - margin-left: var(--spacing-4); -} - -/* 품 비교 — 항목마다 한 줄(글의 줄바꿈 그대로) */ -.m02-hopyo__table.m02-hopyo__lines td.m02-hopyo__compare { - min-width: 9em; - white-space: pre-line; -} - -.m02-hopyo__subhead { - margin: var(--spacing-8) 0 0; - font-size: var(--text-body-sm); -} - .m02-hopyo-ledger__table td:nth-child(1) { min-width: 7em; } @@ -90,62 +42,6 @@ min-width: 6em; } -/* 단추 칸 — 단추 · 넣음/뺌을 세로로 쌓아 칸 폭 = 가장 긴 단추 */ -.m02-hopyo__table.m02-hopyo__lines td.m02-hopyo__acts { - width: 1%; -} - -.m02-hopyo__lines .m02-hopyo__acts > * { - display: flex; - width: 100%; - white-space: nowrap; -} - -.m02-hopyo__lines .m02-hopyo__acts > * + * { - margin: var(--spacing-4) 0 0; -} - -.m02-hopyo__line.is-out { - color: var(--color-text-muted); -} - -.m02-hopyo__state { - display: inline-block; - padding: 0 var(--spacing-4); - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); - font-size: var(--text-caption); - white-space: nowrap; -} - -/* 상태 색(338) — 맞음 초록 · 별칭 파랑 · 다른 점 주황 · 후보 회색(테두리 기본) · 없음 빨강 */ -.m02-hopyo__state.is-맞음, -.m02-hopyo__state.is-호표 { - border-color: var(--color-success); - color: var(--color-success); -} - -.m02-hopyo__state.is-별칭 { - border-color: var(--color-info); - color: var(--color-info); -} - -.m02-hopyo__state.is-다른점 { - border-color: var(--color-warning); - color: var(--color-warning); -} - -.m02-hopyo__state.is-후보, -.m02-hopyo__state.is-뺌, -.m02-hopyo__state.is-노임 { - color: var(--color-text-secondary); -} - -.m02-hopyo__state.is-없음 { - border-color: var(--color-danger); - color: var(--color-danger); -} - /* 우리 로직 칸 — 대상 여럿 = 칸 안 여러 줄(이름 · 키 · 몫 · 배 칸 · [옵션]) */ .m02-hopyo__parts { min-width: 11em; @@ -162,12 +58,6 @@ margin-top: var(--spacing-4); } -.m02-hopyo__whole { - padding: var(--spacing-4) var(--spacing-8); - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); -} - .m02-hopyo__times { width: 6em; } @@ -193,56 +83,6 @@ margin-left: var(--spacing-4); } -.m02-hopyo__block { - display: flex; - flex-direction: column; - gap: var(--spacing-4); -} - -.m02-hopyo__block h4 { - margin: 0; - font-size: var(--text-body-sm); -} - -.m02-hopyo__built { - padding: var(--spacing-8); - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); - background: var(--color-surface-raised); -} - -/* 고르기 창 머리 — 제목이 두 줄이어도 [닫기] 글이 안 갈라지고 안 줄어듦 */ -.m02-hopyo__choose .ui-modal__top > .ui-btn { - flex-shrink: 0; - white-space: nowrap; -} - -.m02-hopyo__cands { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: var(--spacing-4); - margin-bottom: var(--spacing-8); -} - -.m02-hopyo__cand { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--spacing-4); -} - -/* 고르기 창 후보 묶음(358-1) — 뜻 낱말 안 겹치는 「후보」 = 회색 · 숫자만 닮음 = 접힘 */ -.m02-hopyo__cand.is-weak { - color: var(--color-text-muted); -} - -.m02-hopyo__group > summary { - cursor: pointer; - color: var(--color-text-secondary); - margin-bottom: var(--spacing-4); -} - /* 별칭 장부(338) — 위 층 줄 = 회색 읽기만 · 고친 줄 = 왼쪽 띠 */ .m02-hopyo-ledger__row.is-upper { color: var(--color-text-muted); @@ -252,11 +92,7 @@ box-shadow: inset 3px 0 0 var(--color-accent); } -/* 엑셀 보기 창(329) — 왼쪽 읽기 전용 시트 · 오른쪽 호표 목록 · 불러오기 결과 */ -.m02-hopyo-sheet__dialog > .ui-modal__body { - overflow: hidden; -} - +/* 엑셀 보기(329) — 왼쪽 읽기 전용 시트 · 오른쪽 호표 목록 */ .m02-hopyo-sheet { display: grid; grid-template-columns: minmax(0, 1fr) 22rem; @@ -325,6 +161,18 @@ display: none; } +/* 좁은 폭(785-7 · 390) — 줄 판이 열리면 위 칸(시트 + 줄 판)을 넓히고 줄 판 높이 바닥을 둠 */ +@media (max-width: 900px) { + .m02-hopyo-sheet:has(.m02-hopyo-sheet__lines:not([hidden])) { + grid-template-rows: minmax(0, 5fr) minmax(0, 1fr); + } + + .m02-hopyo-sheet__lines { + flex: 1 1 60%; + min-height: 16rem; + } +} + /* 세 보기 막대 · 범례(779-4) — 시트 위 한 줄씩 · 넘치면 감김 */ .m02-hopyo-sheet__nav { display: flex; @@ -433,36 +281,6 @@ flex-shrink: 0; } -.m02-hopyo-sheet__result { - max-height: 40%; - overflow: auto; -} - -.m02-hopyo-sheet__result:empty { - display: none; -} - -/* 고르기 창 근거(330) — 후보 꼬리표 · 엑셀 근거 줄 */ -.m02-hopyo__basis { - margin-left: var(--spacing-8); - padding: 0 var(--spacing-4); - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); - color: var(--color-text-secondary); - font-size: var(--text-caption); - white-space: nowrap; -} - -.m02-hopyo__basis.is-ok { - border-color: var(--color-success); - color: var(--color-success); -} - -.m02-hopyo__source { - padding-bottom: var(--spacing-8); - border-bottom: 1px solid var(--color-border); -} - /* 머리 글 ↔ [파일 업로드] — 구조물 조합 머리 ↔ 층 단추와 같은 12 px */ .m02-hopyo-side > .m02-side__row { margin-top: var(--spacing-12); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts index f833c9eac..fed15206c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts @@ -2,14 +2,19 @@ * M02_MasterTemplete_Hopyo_Api.ts * 일위대가 실무자료(PLAN 324 · 338) 서버 길 — `/api/m02/layers/{층}/hopyo…` · 층 = 화면 층(`Api_Fetch` 지금 층) · * 올리기 = FormData(숨은 file 입력) · 그 밖 = 공용 `call`(서버 까닭 글 그대로) · - * 맞춤 응답 꼴 = 설계 3-3(상태 · 체크 제안 · 대상 목록 · 품 비교) · 만들기 몸 = 확인한 줄만(3-6) · 별칭 장부(3-5). + * 목록 · 올림 · 엑셀 보기 머리(1차 판정 줄상태) · 격자 · 지우기 · 별칭 장부 · 붙여넣기 · 다시 잇기 — + * 판정 · 검산 · 옮기기 길 = `Hopyo_JudgeApi` · 옛 맞춤 꼴(줄 맞춤 · 확인 만들기 · 실무 호표)은 785-7 에서 지움. * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet"; import { call, getActiveLayer, getActiveOwner, type Layer } from "./M02_MasterTemplete_Api_Fetch"; -import type { LaborFixed, LaborRef } from "./M02_MasterTemplete_Labor_Calc"; -import type { MatMarks } from "./M02_MasterTemplete_Labor_Api"; +import type { LaborFixed } from "./M02_MasterTemplete_Labor_Calc"; + +/** 뒤 셈 중 다시 읽기 사이(밀리초) */ +export const POLL_MS = 3000; +/** 사전조합 · 단가산출을 만들었음(document 알림) — 좌측 목록 · 엑셀 보기 칠 다시 */ +export const HOPYO_BUILT = "m02-hopyo-built"; /** 층 · 주인(null = 자기 층) */ export interface HopyoScope { @@ -35,11 +40,11 @@ export interface HopyoBrief { 규격: string; 단위: string; 금액: Money; - /** [체크 제안 수, 줄 수] */ + /** [1차 판정 제안 있음 수, 줄 수](785-7 `Judge.view`) */ 맞춤: [number, number]; - /** 연결할 줄 수(% · 노임 · 뺌 밖 · 358-3) · 연결된 줄 수(그중 체크 제안 — 확인 창과 한 규칙) */ - 연결할?: number; - 연결된?: number; + /** 줄마다 [차례, 1차 판정, 제안 있음] · 만든 SC 키(칠 · `SheetMap.BriefMarks`) */ + 줄상태?: [number, string, boolean][]; + 만든?: string | null; /** 일위대가 시트 행 [시작, 끝](엑셀 1부터 · 329 · 옛 파일 없음) */ 행?: [number, number]; } @@ -62,294 +67,8 @@ export interface HopyoView extends HopyoFile { 검산: { 어긋남: string[]; 총괄표: number }; 호표: HopyoBrief[]; 맞춤: [number, number]; - /** 파일 전체 연결할 · 연결된 줄 수(358-3) */ - 연결할?: number; - 연결된?: number; - /** 뒤 숫자 판정 셈(끝나면 맞춤 수가 바뀜) */ - 셈?: HopyoCalc; -} - -/** 후보가 단가산출 근거와 맞는 것(330-2) — 맞은 조항 글 · 겹친 직종 · 단위 같음 · 점수 · 까닭 꼬리표 */ -export interface HopyoBasisHit { - 조항: string[]; - 직종: string[]; - 단위: boolean; - 점수: number; - 까닭: string; -} - -/** 단산 줄 근거(330-1) — 단가산출 블록의 출처([책, 번호]) · 직종 · 엑셀 행 [시작, 끝] */ -export interface HopyoLineBasis { - 출처: [string, string][]; - 직종: string[]; - 행: [number, number]; -} - -/** 후보 · 고른 대상 글 — 로직 · 자재 · 다른 호표 */ -export interface HopyoTarget { - 로직?: string; - 자재?: string; - 호표?: number; - /** 「전국」 자리표(361-1) — 지역 변형만 있는 자재 · 키 · 단가 없음 */ - 전국?: boolean; - /** 729-2 사용자 이음 — 노임 LB · 기계 EQ · % 줄 밑수(엑셀 숫자 + 이음 · 하나만) */ - 노임?: string; - 기계?: string; - 비율?: LinkBase; - /** 같은 품목 지역 · 관급/사급 변형(361-1 · 고르기 창이 접음) */ - 변형?: boolean; - 이름?: string; - 규격?: string; - 단위?: string | null; - 점수?: number; - /** 근거 맞춤(330 · 옛 파일 · 근거 없는 줄은 없음) */ - 근거?: HopyoBasisHit; - /** 후보 판정(338) — 맞음 · 다른 점 · 후보 */ - 판정?: "맞음" | "다른 점" | "후보"; - 글?: string; - /** 묶음 표시(358-1 · 로직 후보만) — 품명 뜻 낱말 겹침 · 엑셀 출처 조항 같음(≥ 0.9) · 책(산림품셈 · 건설품셈) */ - 뜻겹침?: boolean; - 출처같음?: boolean; - /** 745-1 미리 고름 점 — 「다른 점」 1위 + 출처 같음(서버) · 자동 확정 아님 · 만들기는 사용자가 누름 */ - 미리?: boolean; - 책?: string; -} - -/** 맞춤 대상 하나(338 · 로직 여럿 가능) — 배 = 환산 × 몫 · 우리 수량 = 엑셀 수량 × 배 */ -export interface HopyoPart extends HopyoTarget { - 입력값: Record; - 못읽음?: string[]; - /** 칸마다 값 출처 — 별칭 · 블록 · 규격 · 자동값 */ - 출처?: Record; - 환산?: { 배: number | null; 갈래: string } | null; - 몫?: number | null; - 배: number | null; - /** 블록 절 제목(조합 덧 로직) */ - 어디?: string; - /** 자재 세 칸(361-2) — 만들기에 적힐 값(서버 `Material.marks`) */ - 세칸?: MatMarks; -} - -/** 품 묶음 — 직종 품 · 장비 시간 · 자재 수량 */ -export interface HopyoQty { - 인력?: Record; - 장비?: { 갈래: string | string[]; 시간: number }[]; - 자재?: { 이름: string; 수량: number }[]; -} - -export type HopyoState = - "맞음" | "별칭" | "다른 점" | "후보" | "없음" | "뺌" | "호표" | "%" | "노임"; - -export interface HopyoMatch { - 열쇠: string; - 상태: HopyoState; - /** 처음 체크 제안(확인 대기 — 들어감 아님) */ - 체크: boolean; - 대상: HopyoPart[]; - 후보?: HopyoTarget[]; - /** 이름 닮은 로직 다섯(358-1 · 옛 324 판 그대로 · 숫자 판정 차례와 따로 · 로직 줄만) */ - 이름후보?: HopyoTarget[]; - 글?: string; - 꼬리표?: string[]; - 품?: { 엑셀: HopyoQty; 우리: HopyoQty; 같음: Record }; - 갈리는칸?: string[]; - 별칭?: { 층: Layer; 주인: string | null; 규격다름?: boolean; 근거다름?: boolean }; - 뺌?: string; - /** 739-1 내 제안(승인 전 회사 · 개인 층 장부 짝) — 자동 아님 · 고르기 창 맨 위 */ - 제안?: { 갈래: string; 짝: LedgerPair[]; 대상: HopyoPart[]; 층: Layer; 때: string | null }; -} - -/** 숫자 판정 셈 — 끝 · 한 블록 · 모두 · 까닭(우리 로직 준비 중 등) */ -export interface HopyoCalc { - 끝: boolean; - 한: number; - 모두: number; - 까닭?: string; -} - -export interface HopyoLine { - 차례: number; - 이름: string; - 규격: string; - 수량: number | null; - 단위: string; - 갈래: string; - 단가: Money; - 금액: Money; - 비고: string; - 맞춤: HopyoMatch; - /** 단가산출 근거(330 · 「단산N참조」 줄만) */ - 근거?: HopyoLineBasis; -} - -export interface HopyoOne extends HopyoFile { - 호표: HopyoBrief & { - 줄: HopyoLine[]; - /** 직접 줄 호표 통째 제안(노임 · 장비를 바로 적은 호표) */ - 통째?: { 대상: HopyoPart[]; 글?: string; 체크: boolean } | null; - 셈?: HopyoCalc; - /** 만들 층에 이 호표로 만든 사전조합 */ - 만든?: { - 키: string; - 확인: boolean; - /** 705-3 — 품셈으로 넣은 엑셀 줄 차례(옛 것 = null) · 고정 줄 수 */ - 품셈?: number[] | null; - 고정?: number; - } | null; - }; -} - -/** 미리 셈(저장 안 함) — 고른 대상 · 옵션으로 우리 품 */ -export interface HopyoGuess { - 대상: HopyoPart[]; - 품?: HopyoMatch["품"]; - 글?: string; -} - -/** 만들기 몸 대상 하나 — 로직 · 자재 · 호표 중 하나(금액 · 품 칸 없음 — 서버가 셈) */ -export interface BuildTarget { - 로직?: string; - 자재?: string; - 호표?: number; - /** 「전국」 자재(361-1) — 이름 · 규격 · 단위는 서버가 줄에서 다시 셈 */ - 전국?: boolean; - /** 729-2 사용자 이음(하나만 · 입력값 없음) */ - 노임?: string; - 기계?: string; - 비율?: LinkBase; - 입력값?: Record; - 배: number; - 몫?: number | null; -} - -/** % 줄 밑수 비목(729-2) */ -export type LinkBase = "노무비" | "재료비" | "경비"; - -/** 단가산출 기초 후보(729-1 고름 대기) — 판정 · 글 */ -export interface SangeunCand { - 로직: string; - 이름: string; - 단위: string; - 판정: string; - 글: string; - /** 729-4 — 기준단위 ↔ 엑셀 단위 성격 다름 = 환산 계수 필요(가 1 = 나 몇) */ - 계수?: { 가: string; 나: string; 갈래: string } | null; -} - -/** M01 요소 후보(746-1) — 못옮김 · 안 이은 기계 줄 · 고르면 `{노임|기계|자재: 키}` 로 이음 */ -export interface ElemCand { - 키: string; - 갈래: "노임" | "기계" | "자재"; - 이름: string; - 규격?: string; - 단위?: string; - 단가?: number | null; -} - -/** 요소 후보인가(기초 후보 `로직` 과 가름) */ -export const isElem = (c: unknown): c is ElemCand => - typeof c === "object" && c !== null && "키" in c && "갈래" in c; - -/** 안 이은 줄(729-2) — 만들면 남을 엑셀 줄 · 단가산출 줄 후보 = 고름 대기 기초 후보 */ -export interface HopyoLeft { - 차례: number; - 이름: string; - 규격: string; - 단위: string; - 수량: number | null; - 갈래: string; - 까닭: string; - 후보: (SangeunCand | ElemCand)[]; - /** 729-4 — 못옮김 남은 SK 를 쓰는 줄 = 그 블록 · SK · 못옮김(잇기 = [단가산출 만들기]) */ - 단산?: number; - SK?: string; - 못옮김?: SangeunLost[]; -} - -/** 옮기지 못한 엑셀 줄(729-4) — M01 요소로 이으면 SK 더한줄 · id = 「갈래:이름」 */ -export interface SangeunLost { - id: string; - 이름: string; - 규격?: string; - 단위?: string; - 수량?: number | null; - 갈래: "자재" | "기계" | "노임"; - 글?: string; - 후보?: ElemCand[]; -} - -/** 못옮김 잇기 한 줄(729-4) — 이음 키(LB · EQ · MT) 또는 뺌 까닭 */ -export type SangeunLink = { id: string; 뺌?: string } & Partial< - Record<"노임" | "기계" | "자재", string> ->; - -/** [단가산출 만들기](729-1) 답 — 만듦 = 기초 · SK · 고름 대기 = 블록마다 후보 */ -export interface SangeunBuilt { - 블록: number; - 만듦: { 번: number; 이름: string; 키: string; 갈래: "기초" | "SK"; 못옮김?: SangeunLost[] }[]; - 그대로: number; - 기초: number; - SK: number; - 고름대기: { - 번: number; - 이름: string; - 단위: string; - 갈래: string; - 까닭: string; - 후보: SangeunCand[]; - }[]; - 별칭: number; - /** 729-5 — 이미 만든(그대로) SK 중 못옮김 남은 것 */ - 남은못옮김?: { 번: number; 이름: string; 키: string; 못옮김: SangeunLost[] }[]; - /** 733-1 — 이 층 같은 내용 SK 를 씀(새로 안 만듦) */ - 같은것씀?: { 번: number; 키: string }[]; - /** 733-1 — 위 층(회사 · 마스터)에 같은 내용 SK(알림만 · 연결 안 함) */ - 위층같음?: { 번: number; 층: string; 주인: string | null; 키: string }[]; -} - -/** 확인한 줄 — 대상 목록 또는 뺌 까닭 */ -export interface BuildLine { - 차례: number; - 대상?: BuildTarget[]; - 뺌?: "우리에 없음" | "안 씀"; -} - -export interface HopyoMissing { - 차례: number; - 이름: string; - 규격: string; - 수량: number | null; - 단위: string; - 금액: number | null; - 까닭: string; -} - -export interface HopyoBuilt { - 참조: LaborRef; - 이름: string; - 다시씀: boolean; - 빠진줄: HopyoMissing[]; - 비교: { - 엑셀: number | null; - 우리: number | null; - 차이: number | null; - /** 705-3 — 엑셀 끝수 규칙으로 같음 */ - 같음?: boolean; - 까닭: string | null; - }; - 장부?: { 씀: number }; - /** 705-3 — 엑셀 줄 갈래 수 */ - 줄수?: HopyoLineCount; -} - -/** 705-3 줄 갈래 수 — 품셈 · 고정 · 호표(하위 참조) · 빠짐 · 이음(저절로) · 이음후보(단가 다름) */ -export interface HopyoLineCount { - 품셈: number; - 고정: number; - 호표: number; - 빠짐: number; - 이음: number; - 이음후보: number; + /** 블록 풀이 뒤 셈(785-5 파생 저장본 · 끝 = 판정 칠 다 셈) */ + 셈?: { 끝: boolean; 까닭?: string }; } /** [모두 고정으로 들여오기] 결과(705-3) */ @@ -376,15 +95,6 @@ export interface HopyoBuiltAll { 만듦?: { 번: number; 이름: string; 키: string }[]; } -/** [로직 추가] 「실무 호표」 줄 하나 — 확인 = 문서 층에 확인된 사전조합 */ -export interface HopyoPickRow extends Omit { - 층: Layer; - 주인: string | null; - 키: string; - 파일: string; - 확인?: LaborRef | null; -} - /** 별칭 장부 짝 하나 */ export interface LedgerPair { 로직?: string; @@ -434,30 +144,6 @@ export const readHopyo = (scope: HopyoScope, key: string): Promise => export const readHopyoGrid = (scope: HopyoScope, key: string): Promise => call(at(scope, `/hopyo-grid/${enc(key)}`)); -/** 호표 하나 — `to` = 만들 층(호표 참조 줄 체크 · 만든 · 없으면 파일 층) */ -export const readHopyoOne = ( - scope: HopyoScope, - key: string, - no: number, - to?: HopyoScope, -): Promise => { - const want = to ? `?to=${to.layer}${to.owner ? `&to_owner=${enc(to.owner)}` : ""}` : ""; - return call(at(scope, `/hopyo/${enc(key)}/${no}${want}`)); -}; - -/** 미리 셈(저장 안 함) — 고른 대상 목록 · 옵션 → 우리 품 */ -export const guessHopyo = ( - scope: HopyoScope, - key: string, - no: number, - 차례: number, - 대상: BuildTarget[], -): Promise => - call(at(scope, `/hopyo/${enc(key)}/${no}/guess`), { - method: "POST", - body: JSON.stringify({ 차례, 대상 }), - }); - export const readLedger = async (scope: HopyoScope): Promise => (await call<{ 줄: LedgerRow[] }>(at(scope, "/hopyo-ledger"))).줄; @@ -477,42 +163,6 @@ export const saveLedger = async ( export const deleteHopyo = (scope: HopyoScope, key: string, keepalive = false): Promise => call(at(scope, `/hopyo/${enc(key)}`), { method: "DELETE", keepalive }); -/** [확인하고 사전조합 만들기] — 확인한 줄만 · 직접 줄 호표 = `통째` → `target` 층 사전조합(파일 층 = `from`) */ -export const buildHopyo = ( - target: HopyoScope, - from: { 층: Layer; 주인: string | null; 키: string; 번: number }, - 줄: BuildLine[], - 통째?: { 대상: BuildTarget[] } | null, -): Promise => - call(at(target, "/hopyo-build"), { - method: "POST", - body: JSON.stringify(통째 ? { ...from, 통째 } : { ...from, 줄 }), - }); - -/** [모두 고정으로 들여오기](705-3) — 시트 호표 모두 · 숫자는 서버가 파일에서 */ -export const buildHopyoAll = ( - target: HopyoScope, - from: { 층: Layer; 주인: string | null; 키: string }, -): Promise => - call(at(target, "/hopyo-build-all"), { method: "POST", body: JSON.stringify(from) }); - -/** 안 이은 줄 미리 보기(729-2 · 저장 없음) — 몸 = 만들기 몸 */ -export const leftHopyo = ( - target: HopyoScope, - from: { 층: Layer; 주인: string | null; 키: string; 번: number }, - 줄: BuildLine[], -): Promise<{ 남은줄: HopyoLeft[]; 수: number }> => - call(at(target, "/hopyo-unlinked"), { method: "POST", body: JSON.stringify({ ...from, 줄 }) }); - -/** 409 몸의 남은 줄(729-2 · 만들기 막힘) — 아니면 null */ -export const leftOf = (error: unknown): HopyoLeft[] | null => { - const got = (error as { detail?: { 남은줄?: unknown } } | null)?.detail?.남은줄; - return Array.isArray(got) ? (got as HopyoLeft[]) : null; -}; - -export const listHopyoPicks = async (scope: HopyoScope): Promise => - (await call<{ 줄: HopyoPickRow[] }>(at(scope, "/hopyo-picks"))).줄; - /** 엑셀 올리기 — 서버 까닭 글 그대로(시트 없음 · 20 MB · 엉터리) */ export async function uploadHopyo(scope: HopyoScope, file: File): Promise { const form = new FormData(); @@ -606,27 +256,3 @@ export const relinkApply = ( 문서: { 키: string; 이름: string; 전: number | null; 후: number | null }[]; 되돌림: RelinkChange[]; }> => call(at(scope, "/hopyo-relink"), { method: "POST", body: JSON.stringify({ 바꿈 }) }); - -/** 규격 묶음 제안(704-4) — 이름 · 단위 · 줄 구성 같고 규격만 다른 호표(연 호표 먼저) */ -export const hopyoSiblings = ( - scope: HopyoScope, - key: string, - no: number, -): Promise<{ 묶음: { 번: number; 규격: string; 열: string }[]; 이름: string | null }> => - call(at(scope, `/hopyo/${enc(key)}/${no}/siblings`)); - -/** [구조물 조합으로](704-4) — 확인 창 몸 그대로 → 이 층 구조물 조합 항목(사전조합 안 만듦) · - * `extra` = 같은 내용 409 뒤 다시 보냄(733-2 · 씀 = 그 항목 · 답 `씀` · 새로 = 그래도 새로) */ -export const buildHopyoItem = ( - target: HopyoScope, - from: { 층: Layer; 주인: string | null; 키: string; 번: number }, - 줄: BuildLine[], - 통째: { 대상: BuildTarget[] } | null, - 묶음: number[], - 이름: string, - extra: { 씀?: string; 새로?: boolean } = {}, -): Promise<{ key: string; 열: string; 이름: string; 줄수?: number; 열수?: number; 씀?: boolean }> => - call(at(target, "/hopyo-item"), { - method: "POST", - body: JSON.stringify({ ...from, ...(통째 ? { 통째 } : { 줄 }), 묶음, 이름, ...extra }), - }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts deleted file mode 100644 index 771805135..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts +++ /dev/null @@ -1,662 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Hopyo_Check.ts - * 일위대가 실무자료 확인 창(PLAN 338-3 · 설계 4-1 · 359-3) — 호표 하나의 줄마다 서버 맞춤(상태 · 체크 제안 · - * 우리 로직 · 옵션 · 품 비교)을 보이고, 사용자가 체크한 줄만 [사전조합 생성]으로 서버에 보냄. - * · 자리 = 엑셀 보기 창 호표 [수정] · [로직 추가] 「실무 호표」 모달 - * · 머리 = 호표 · 「제안 n / 줄 m」 · 숫자 판정 셈 중이면 3 초마다 다시 읽음(끝나면 멈춤 · 만진 줄은 그대로) - * · 줄 표 = 구조물 조합 일위대가 상세 꼴(로직 표 · 자재 표 · 품 비교 · 단추 — `M02_MasterTemplete_Hopyo_CheckRows`) - * · 금액 = 줄마다 서버 calc 미리 셈(`logic/M02_MasterTemplete_Hopyo_Draft` 문서 · 저장 안 함 · 줄마다 최신 응답만) - * · 대상 · 옵션을 바꾸면 서버 `guess`(저장 안 함 · 줄마다 최신 응답만)가 우리 품을 다시 셈 - * · 캐시 = sessionStorage `m02-hopyo-check:{층}:{주인}:{키}:{번}`(조작 즉시 · 만들기 성공하면 지움) · 자동 저장 없음 - * · 화면 잠금은 [사전조합 생성] 때만 · 쓰기 단추 = 만들 층 쓰기 권한만. - * · 705-3 — 체크 안 한 줄 = 고정 줄(엑셀 숫자 그대로) · 만든 사전조합이 있으면 첫 체크 = 그 품셈 줄(`만든.품셈`) · - * [품셈으로 바꾸기 (n)] = 맞음 · 별칭 줄 체크 + 생성 · [모두 고정으로] = 체크 모두 끔 + 생성(되돌리기) · - * 생성 물음에 전후 금액(품셈 = 우리 미리 셈 · 고정 = 엑셀) · 품 다른 줄 수. - * · 704-4 — [구조물 조합으로] = 같은 몸으로 구조물 조합 항목 바로(`Hopyo_ToItem` · 규격 묶음 제안) - * 계산은 서버(`M02_Hopyo_Match` · `M02_Hopyo_Build` · `/labor/{열}/calc`) — 화면은 보이기 · 체크 · 고름만. - * ========================================================================== */ - -import { - createButton, - el, - hideLoadingOverlay, - showConfirmDialog, - showLoadingOverlay, - showToast, -} from "@ui/ui_template_elements"; -import { loadingError, showLoading } from "@ui/ui_template_loading"; -import { openModal } from "@ui/ui_template_modal"; -import { t as L } from "@ui/ui_template_locale"; -import { createSourceView } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Source"; -import { - guessHopyo, - readHopyoOne, - type BuildLine, - type BuildTarget, - type HopyoBuilt, - type HopyoLine, - type HopyoMatch, - type HopyoOne, - type HopyoPart, - type HopyoScope, -} from "./M02_MasterTemplete_Hopyo_Api"; -import { buildWithKids as buildHopyo } from "./M02_MasterTemplete_Hopyo_Kids"; // 777-1 하위 함께 -import { builtWord, openHopyoChoose, targetText } from "./M02_MasterTemplete_Hopyo_Choose"; -import { - checkTables, - type LinePrice, - type RowsContext, -} from "./M02_MasterTemplete_Hopyo_CheckRows"; -import type { HopyoSheetOptions } from "./M02_MasterTemplete_Hopyo_Sheet"; -import { calcLabor, fetchLogicInputs } from "./M02_MasterTemplete_Labor_Api"; -import { openLaborInput } from "./M02_MasterTemplete_Labor_Input"; -import { draftDoc } from "./logic/M02_MasterTemplete_Hopyo_Draft"; -import { toItem } from "./M02_MasterTemplete_Hopyo_ToItem"; -import { openLeftOf } from "./M02_MasterTemplete_Hopyo_Left"; -import { formatNumber, formatPrice } from "../common_util/common_util_money"; -import "./M02_MasterTemplete_Hopyo.css"; - -export const POLL_MS = 3000; -/** 사전조합을 만들었음(document 알림) — 좌측 「일위대가 사전조합」 목록 다시 */ -export const HOPYO_BUILT = "m02-hopyo-built"; -/** 미리 셈 문서 열 이름(서버 calc 는 보낸 문서로 셈 · 저장 안 함) */ -const DRAFT_COLUMN = "hopyo-draft"; - -/** 확인 창 캐시 — 사용자가 만진 것만(서버 제안 위에 덮어 보임) */ -export interface CheckDraft { - 체크: Record; - 대상: Record; - 뺌: Record; - 품: Record; - 통째?: boolean; -} - -const empty = (): CheckDraft => ({ 체크: {}, 대상: {}, 뺌: {}, 품: {} }); -export const draftKey = (scope: HopyoScope, key: string, no: number): string => - `m02-hopyo-check:${scope.layer}:${scope.owner ?? ""}:${key}:${no}`; - -export function loadDraft(k: string): CheckDraft { - try { - const raw = sessionStorage.getItem(k); - return raw ? { ...empty(), ...(JSON.parse(raw) as Partial) } : empty(); - } catch { - return empty(); - } -} -/** 확인 창을 열어 만진 적 있음(캐시 있음) */ -export function hasDraft(k: string): boolean { - try { - return sessionStorage.getItem(k) !== null; - } catch { - return false; - } -} -export function saveDraft(k: string, d: CheckDraft): void { - try { - sessionStorage.setItem(k, JSON.stringify(d)); - } catch {} // 캐시 못 씀 = 이 화면에만 남음 - document.dispatchEvent(new Event("m02-hopyo-draft")); // 엑셀 보기 창 다시 칠(779-4 SheetNav.HOPYO_DRAFT) -} -export function dropDraft(k: string): void { - try { - sessionStorage.removeItem(k); - } catch { - /* 없음 */ - } -} - -/** 줄의 지금 대상 — 사용자가 고른 것(캐시) · 없으면 서버 제안 */ -export const partsOf = (line: HopyoLine, d: CheckDraft): HopyoPart[] => { - const got = d.대상[line.차례] ?? line.맞춤.대상; - return Array.isArray(got) ? got : []; // 옛 응답(대상 하나) = 대상 없음 -}; -/** 체크 — 사용자가 만진 줄은 캐시 · 아니면 서버 제안 */ -export const isChecked = (line: HopyoLine, d: CheckDraft): boolean => - d.체크[line.차례] ?? !!line.맞춤.체크; -/** 체크할 수 있음 — % 줄 아님 · 뺌 까닭 또는 대상(배 0 넘음) */ -export function checkable(line: HopyoLine, d: CheckDraft): boolean { - if (d.뺌[line.차례]) return true; - if (line.맞춤.상태 === "%") return !!partsOf(line, d)[0]?.비율; // 729-3 밑수 고른 % 줄만 - const parts = partsOf(line, d); - return parts.length > 0 && parts.every((p) => p.배 !== null && p.배 > 0); -} - -/** 대상 → 만들기 몸 대상(이름 · 품 같은 보이기 칸은 안 보냄) */ -export function toTarget(p: HopyoPart): BuildTarget { - if (p.호표 !== undefined) return { 호표: p.호표, 배: p.배 ?? 1 }; - const link = (["노임", "기계", "비율"] as const).find((x) => p[x]); // 729-3 사용자 이음(엑셀 숫자 + 이음) - if (link) return { [link]: p[link], 배: 1 } as BuildTarget; - const head = p.로직 - ? { 로직: p.로직, 입력값: p.입력값 ?? {} } - : p.전국 - ? { 전국: true } - : { 자재: p.자재 }; - return { ...head, 배: p.배 ?? 1, ...(p.몫 != null ? { 몫: p.몫 } : {}) }; -} - -/** 체크한 줄만 → 만들기 몸 `줄` */ -export const bodyOf = (lines: HopyoLine[], d: CheckDraft): BuildLine[] => - lines - .filter((l) => isChecked(l, d) && checkable(l, d)) - .map((l) => - d.뺌[l.차례] - ? { 차례: l.차례, 뺌: d.뺌[l.차례] as BuildLine["뺌"] } - : { 차례: l.차례, 대상: partsOf(l, d).map(toTarget) }, - ); - -/** 빠질 줄 까닭별 수 — 확인 안 함 · 뺌 · % 줄 */ -export function leftOut(lines: HopyoLine[], d: CheckDraft): Record { - const out: Record = {}; - for (const l of lines) { - const why = - l.맞춤.상태 === "%" - ? "% 줄" - : !isChecked(l, d) || !checkable(l, d) - ? "확인 안 함" - : d.뺌[l.차례] - ? "뺌" - : null; - if (why) out[why] = (out[why] ?? 0) + 1; - } - return out; -} - -const n4 = (v: number | undefined): string => (v === undefined ? "–" : formatNumber(v)); - -/** 품 비교 글 — 「콘크리트공 0.1091 → 0.1091 ✓ · 굴착기 0.29 → 0.31 ≠」 */ -export function compareText(q: HopyoMatch["품"] | undefined): string { - if (!q) return ""; - const ex = q.엑셀.인력 ?? {}; - const ours = q.우리.인력 ?? {}; - const mark = (k: string): string => (q.같음[k] ? "✓" : "≠"); - const out = [...new Set([...Object.keys(ex), ...Object.keys(ours)])].map( - (k) => `${k} ${n4(ex[k])} → ${n4(ours[k])} ${mark(k)}`, - ); - // 같은 갈래 여러 줄은 합쳐 한 줄(서버 `_qty` 와 같은 열쇠 — 갈래 가나다 차례 + 로 묶음) - const name = (g: string | string[]): string => [g].flat().sort().join("+"); - const sum = (xs: { 갈래: string | string[]; 시간: number }[], k: string): number | undefined => - xs - .filter((y) => name(y.갈래) === k) - .reduce((s, y) => (s ?? 0) + y.시간, undefined); - for (const k of new Set((q.엑셀.장비 ?? []).map((x) => name(x.갈래)))) { - out.push(`${k} ${n4(sum(q.엑셀.장비 ?? [], k))} → ${n4(sum(q.우리.장비 ?? [], k))} ${mark(k)}`); - } - return out.join(" · "); -} - -/** 줄마다 최신 요청만 — `ask(줄)` 이 돌려준 함수가 그 뒤 같은 줄 요청이 없을 때만 true */ -export function newest(): (key: number) => () => boolean { - const last = new Map(); - return (key) => { - const my = (last.get(key) ?? 0) + 1; - last.set(key, my); - return () => last.get(key) === my; - }; -} - -/** [맞음 · 별칭 일괄 체크] 대상 줄 */ -export const sureLines = (lines: HopyoLine[], d: CheckDraft): HopyoLine[] => - lines.filter((l) => ["맞음", "별칭"].includes(l.맞춤.상태) && checkable(l, d)); - -const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); -const won = (v: number | null | undefined): string => - v === null || v === undefined ? "" : formatPrice(v); -const fail = (error: unknown): void => showToast(`실패 — ${why(error)}`, "error"); -const note = (text: string): HTMLElement => - el("span", { className: "ui-text ui-text--muted", text, attrs: { "data-text": "14" } }); -const td = (text: string, cls = ""): HTMLElement => - el("td", { text, className: cls, attrs: { title: text } }); -const openSheet = (o: HopyoSheetOptions): void => - void import("./M02_MasterTemplete_Hopyo_Sheet").then((m) => m.openHopyoSheet(o), fail); - -export interface CheckOptions { - /** 실무자료 파일 층 */ - scope: HopyoScope; - key: string; - no: number; - /** 사전조합을 만들 층(없으면 파일 층) */ - target?: HopyoScope; - /** 만들 층 쓰기 권한 — 없으면 체크 · 만들기 없음(보기만) */ - canWrite: boolean; - /** 만들었음 — [로직 추가] 참조 줄 · 모달 닫기 */ - onBuilt?: (got: HopyoBuilt, hopyo: HopyoOne["호표"]) => void; -} - -/** 확인 창 몸 — 모달(`openHopyoCheck`)에 담김 */ -export function buildHopyoCheck(opt: CheckOptions): HTMLElement { - const root = el("div", { - className: "m02-hopyo m02-hopyo-check", - attrs: { "data-hopyo-check": `${opt.key}|${opt.no}` }, - }); - const target = opt.target ?? opt.scope; - const k = draftKey(opt.scope, opt.key, opt.no); - let d = loadDraft(k); - let got: HopyoOne | null = null; - let built: HopyoBuilt | null = null; - let seq = 0; - let timer = 0; - const ask = newest(); // 줄마다 최신 미리 셈만 - const askPrice = newest(); // 줄마다 최신 금액 미리 셈만 - const priced = new Map(); - let pending = false; - const keep = (): void => saveDraft(k, d); - - async function load(quiet = false): Promise { - const my = ++seq; - const stop = quiet ? () => {} : showLoading(root); - try { - const one = await readHopyoOne(opt.scope, opt.key, opt.no, opt.target); - if (my !== seq) return; - got = one; - const made = one.호표.만든?.품셈; // 705-3 — 만든 사전조합의 품셈 줄 = 첫 체크 - if (made && !hasDraft(k)) for (const l of one.호표.줄) d.체크[l.차례] = made.includes(l.차례); - draw(); - } catch (error) { - if (my === seq && !quiet) root.replaceChildren(loadingError(why(error), () => void load())); - } finally { - stop(); - } - } - - /** 대상 바꿈 → 캐시 · 체크 · 서버 미리 셈(최신 응답만) */ - function setParts(line: HopyoLine, parts: HopyoPart[]): void { - d.대상[line.차례] = parts; - d.체크[line.차례] = true; - delete d.뺌[line.차례]; - keep(); - draw(); - const mine = ask(line.차례); - guessHopyo(opt.scope, opt.key, opt.no, line.차례, parts.map(toTarget)).then((g) => { - if (!mine()) return; - if (Array.isArray(g.대상)) d.대상[line.차례] = g.대상; - if (g.품) d.품[line.차례] = g.품; - keep(); - draw(); - }, fail); - } - - function editOptions(line: HopyoLine, at: number): void { - const parts = partsOf(line, d); - const p = parts[at]; - if (!p?.로직) return; - fetchLogicInputs([p.로직]).then( - (res) => - openLaborInput({ - title: `${p.로직} ${p.이름 ?? ""}`.trim(), - logic: res.로직[0] ?? null, - values: (p.입력값 ?? {}) as never, - memo: null, - readOnly: !opt.canWrite, - done: (r) => - setParts( - line, - parts.map((x, i) => (i === at ? { ...x, 입력값: r.입력값 } : x)), - ), - }), - fail, - ); - } - - /** [원문 보기] — 엑셀 인용 조항 · 우리 로직 출처 두 갈래 */ - async function showSource(line: HopyoLine): Promise { - const excel = (line.근거?.출처 ?? []).map((x) => x.join(" ").trim()).filter(Boolean); - const codes = partsOf(line, d).flatMap((p) => (p.로직 ? [p.로직] : [])); - const ours = codes.length - ? (await fetchLogicInputs(codes).catch(() => ({ 로직: [] }))).로직.flatMap((x) => - x.출처 ? [`${x.로직} — ${x.출처}`] : [], - ) - : []; - openModal({ - title: `원문 — ${line.이름}`, - closeLabel: "닫기", - wide: true, - dialogClass: "m02-modal m02-hopyo__src", - mount: (body) => { - const part = (head: string, list: string[], cut: (s: string) => string): void => { - body.append(el("h4", { text: head })); - if (!list.length) body.append(note("없음")); - for (const s of list) { - const view = createSourceView({ - path: "/m02/logic-lib/source", - pdf: "/m02/logic-lib/source-pdf", - }); - view.show(cut(s)); - body.append(note(s), view.root); - } - }; - part("엑셀 인용 조항", excel, (s) => s); - part("우리 로직 출처", ours, (s) => s.split(" — ").slice(1).join(" — ")); - }, - }); - } - - /** 줄마다 금액 미리 셈(359-3) — 서버 calc(저장 안 함 · 줄마다 최신 응답만 · 문서가 같으면 다시 안 셈) */ - function price(line: HopyoLine): LinePrice | null { - const doc = draftDoc(line, partsOf(line, d)); - if (!doc) return null; - const sig = JSON.stringify(doc); - const have = priced.get(line.차례); - if (have?.sig === sig) return have.got; - const slot = { sig, got: {} as LinePrice }; - priced.set(line.차례, slot); - const mine = askPrice(line.차례); - calcLabor(target.layer, DRAFT_COLUMN, doc, {}, target.owner).then( - (calc) => mine() && ((slot.got = { calc }), soon()), - (error: unknown) => mine() && ((slot.got = { fail: why(error) }), soon()), - ); - return slot.got; - } - /** 응답 여럿 = 한 번 그림(뒤 탭에서도 — rAF 는 멈춤) */ - function soon(): void { - if (pending) return; - pending = true; - window.setTimeout(() => { - pending = false; - if (root.isConnected) draw(); - }, 0); - } - - const ctx = (): RowsContext => ({ - canWrite: opt.canWrite, - d, - price, - check: (line, on) => { - d.체크[line.차례] = on; - keep(); - draw(); - }, - out: (line, value) => { - if (value) d.뺌[line.차례] = value; - else delete d.뺌[line.차례]; - d.체크[line.차례] = !!value || d.체크[line.차례]; - keep(); - draw(); - }, - times: (line, at, value) => { - d.대상[line.차례] = partsOf(line, d).map((x, i) => (i === at ? { ...x, 배: value } : x)); - keep(); - draw(); - }, - choose: (line) => - openHopyoChoose({ - line, - openSheet: sheetAt, - done: (ts) => - setParts( - line, - ts.map((t) => ({ ...t, 입력값: t.입력값 ?? {}, 몫: t.몫 ?? null })), - ), - }), - source: (line) => void showSource(line), - sheet: sheetAt, - options: editOptions, - }); - const sheetAt = (행: [number, number]): void => - openSheet({ - scope: opt.scope, - key: opt.key, - 파일: got?.파일 ?? "", - canWrite: opt.canWrite, - target, - 단산행: 행, - }); - - /** 1 단위 금액(705-3) — 품셈 줄 = 우리 미리 셈 · 고정 줄 = 엑셀 · 셈 중 = null */ - function sumOf(lines: HopyoLine[], poom: (l: HopyoLine) => boolean): number | null { - let total = 0; - for (const l of lines) { - const p = poom(l) ? price(l) : null; - if (p && !p.calc && !p.fail) return null; - total += p?.calc?.반영합?.계 ?? l.금액.계 ?? 0; - } - return total; - } - - async function make(h: HopyoOne["호표"]): Promise { - const whole = !!h.통째 && (d.통째 ?? h.통째.체크); - const lines = whole ? [] : bodyOf(h.줄, d); - const poom = (l: HopyoLine): boolean => isChecked(l, d) && checkable(l, d) && !d.뺌[l.차례]; - const now = (l: HopyoLine): boolean => (h.만든?.품셈 ?? []).includes(l.차례); - const outs = h.줄.filter((l) => isChecked(l, d) && d.뺌[l.차례] === "안 씀").length; - const put = h.줄.filter(poom); - const diff = put.filter((l) => { - const q = d.품[l.차례] ?? l.맞춤.품; - return !!q && Object.values(q.같음).some((v) => !v); - }).length; - const after = sumOf(h.줄, poom); - const known = !!h.만든?.품셈; // 옛 사전조합(품셈 줄 기록 없음) = 지금 금액 모름 - const before = known ? sumOf(h.줄, now) : null; - const money = - `1 단위 ${known ? `지금 ${before === null ? "셈 중" : won(Math.round(before))} → ` : ""}` + - `바뀐 뒤 ${after === null ? "셈 중" : won(Math.round(after))} · 엑셀 ${won(h.금액.계)}`; - const ask = whole - ? `호표 통째로 만듦 — ${h.통째!.대상.map(targetText).join(" · ")}` - : `품셈 줄 ${put.length}${diff ? `(품 다름 ${diff})` : ""} · 고정 줄 ${h.줄.length - put.length - outs}` + - ` · 뺌 ${outs} — ${money}(미리 셈 · 끝수 차 있음) — ${L("M02_PrecomboTitle")} 생성`; - if (!(await showConfirmDialog(ask, "생성"))) return; - const from = { 층: opt.scope.layer, 주인: opt.scope.owner, 키: opt.key, 번: opt.no }; - showLoadingOverlay(); - try { - const res = await buildHopyo( - target, - from, - lines, - whole ? { 대상: h.통째!.대상.map(toTarget) } : null, - ); - dropDraft(k); - d = empty(); - built = res; - const miss = res.빠진줄.length; - showToast(`${res.참조.키} ${builtWord(res)}${miss ? ` — 빠진 줄 ${miss}` : ""}`, "success"); - document.dispatchEvent(new Event(HOPYO_BUILT)); - opt.onBuilt?.(res, h); - await load(true); - } catch (error) { - const at = { scope: opt.scope, target, key: opt.key, 파일: got?.파일 ?? "", 번: opt.no }; - if (!openLeftOf(error, at, () => ((d = loadDraft(k)), draw()))) fail(error); // 729-3 남은 줄 - } finally { - hideLoadingOverlay(); - } - } - - function builtBlock(res: HopyoBuilt): HTMLElement { - const c = res.비교; - const rows = res.빠진줄.map((x) => - el("tr", { - children: [ - td(x.이름), - td(x.규격), - td(x.수량 === null ? "" : formatNumber(x.수량), "m02-hopyo__num"), - td(x.단위), - td(won(x.금액), "m02-hopyo__num"), - td(x.까닭), - ], - }), - ); - return el("div", { - className: "m02-hopyo__block m02-hopyo__built", - attrs: { "data-built": res.참조.키 }, - children: [ - note(res.다시씀 ? `있던 ${res.참조.키} 를 확인한 내용으로 덮음` : `${res.참조.키} 만듦`), - ...(rows.length - ? [ - el("h4", { text: `빠진 줄 ${rows.length}` }), - el("table", { - className: "ui-general-block__table m02-hopyo__table", - children: [ - el("thead", { - children: [ - el("tr", { - children: ["품명", "규격", "수량", "단위", "엑셀 금액", "까닭"].map((t) => - el("th", { text: t }), - ), - }), - ], - }), - el("tbody", { children: rows }), - ], - }), - ] - : []), - note( - `값 비교(참고) — 엑셀 ${won(c.엑셀)} · 우리 ${won(c.우리 === null ? null : Math.round(c.우리))}` + - ` · 차이 ${won(c.차이)}${c.까닭 ? ` · ${c.까닭}` : ""}`, - ), - ], - }); - } - - const toItemButton = (h: HopyoOne["호표"], whole: boolean): HTMLElement => - createButton({ - label: "구조물 조합으로", - size: "sm", - onClick: () => - void toItem({ scope: opt.scope, target, key: opt.key, no: opt.no, hopyo: h, - lines: whole ? [] : bodyOf(h.줄, d), whole: whole ? { 대상: h.통째!.대상.map(toTarget) } : null }), // prettier-ignore - }); - - function draw(): void { - if (!got) return; - const h = got.호표; - const checked = h.줄.filter((l) => isChecked(l, d) && checkable(l, d)).length; - const calc = h.셈; - window.clearTimeout(timer); - if (calc && !calc.끝) - timer = window.setTimeout(() => root.isConnected && void load(true), POLL_MS); - const tools: HTMLElement[] = []; - if (opt.canWrite) { - const sure = sureLines(h.줄, d); - const all = createButton({ label: `맞음 · 별칭 일괄 체크 (${sure.length})`, size: "sm" }); - all.disabled = !sure.length; - all.addEventListener("click", () => { - for (const l of sure) d.체크[l.차례] = true; - keep(); - draw(); - }); - const whole = !!h.통째 && (d.통째 ?? h.통째.체크); - const go = createButton({ - label: `${L("M02_PrecomboTitle")} 생성`, - variant: "filled", - size: "sm", - }); - go.setAttribute("data-hopyo-make", ""); - go.addEventListener("click", () => void make(h)); - tools.push(all); - if (h.만든 && !whole) { - // 705-3 — 고정 ↔ 품셈 바꾸기(줄 하나 = 체크 하나 + [사전조합 생성]) - const poom = createButton({ label: `품셈으로 바꾸기 (${sure.length})`, size: "sm" }); - poom.setAttribute("data-hopyo-to-poom", ""); - poom.disabled = !sure.length; - poom.addEventListener("click", () => { - for (const l of sure) d.체크[l.차례] = true; - keep(); - draw(); - void make(h); - }); - const back = createButton({ label: "모두 고정으로", size: "sm" }); - back.setAttribute("data-hopyo-to-fixed", ""); - back.disabled = !h.만든.품셈?.length; - back.addEventListener("click", () => { - for (const l of h.줄) d.체크[l.차례] = false; - keep(); - draw(); - void make(h); - }); - tools.push(poom, back); - } - tools.push(go, toItemButton(h, whole)); // 704-4 — 사전조합 안 거치고 구조물 조합 - } - const head = el("div", { - className: "ui-toolbar m02-hopyo__bar", - children: [ - el("strong", { text: `제${h.번}호표 ${h.이름}` }), - note(h.규격), - note(h.단위), - note(`엑셀 단가 ${won(h.금액.계)}`), - note("금액 칸 위 = 엑셀 · 아래 = 우리(미리 셈)"), - note(`제안 ${h.맞춤[0]} / 줄 ${h.맞춤[1]} · 체크 ${checked}`), - ...(calc && !calc.끝 - ? [note(`숫자 판정 셈 중 ${calc.한}/${calc.모두}${calc.까닭 ? ` · ${calc.까닭}` : ""}`)] - : []), - ...(h.만든 - ? [ - note( - `만든 ${L("M02_PrecomboTitle")} ${h.만든.키}${h.만든.확인 ? " · 확인함" : ""}` + - (h.만든.품셈 - ? ` · 품셈 줄 ${h.만든.품셈.length} · 고정 줄 ${h.만든.고정 ?? 0}` - : ""), - ), - ] - : []), - createButton({ - label: "엑셀 보기", - size: "sm", - onClick: () => - openSheet({ - scope: opt.scope, - key: opt.key, - 파일: got!.파일, - canWrite: opt.canWrite, - target, - 호표: opt.no, - }), - }), - ...tools, - ], - }); - const whole = h.통째 - ? (() => { - const box = el("input", { - attrs: { type: "checkbox", "data-hopyo-whole": "" }, - }) as HTMLInputElement; - box.checked = d.통째 ?? h.통째.체크; - box.disabled = !opt.canWrite; - box.addEventListener("change", () => { - d.통째 = box.checked; - keep(); - draw(); - }); - return [ - el("label", { - className: "ui-toolbar m02-hopyo__bar m02-hopyo__whole", - children: [ - box, - note( - `통째 = ${h.통째.대상.map(targetText).join(" · ")}${h.통째.글 ? ` — ${h.통째.글}` : ""}`, - ), - ], - }), - ]; - })() - : []; - root.replaceChildren( - head, - ...whole, - ...(h.줄.length ? checkTables(h.줄, ctx()) : [note("구성 줄 없음")]), - ...(built ? [builtBlock(built)] : []), - ); - } - - void load(); - return root; -} - -/** 확인 창 모달 — [로직 추가] 「실무 호표」 · 엑셀 보기 창 호표 [수정] · 만들면 닫음 */ -export function openHopyoCheck(opt: CheckOptions & { title?: string }): void { - openModal({ - title: opt.title ?? `확인 — 제${opt.no}호표`, - closeLabel: "닫기", - wide: true, - dialogClass: "m02-modal m02-hopyo__check-modal", - mount: (body, close) => - body.append( - buildHopyoCheck({ - ...opt, - onBuilt: (got, h) => { - opt.onBuilt?.(got, h); - close(); - }, - }), - ), - }); -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_CheckRows.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_CheckRows.ts deleted file mode 100644 index 25b8d3209..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_CheckRows.ts +++ /dev/null @@ -1,373 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Hopyo_CheckRows.ts - * 확인 창 줄 표(PLAN 359-3) — 구조물 조합 일위대가 상세 꼴(`Labor_Rows` logicTable · matTable)에 품 비교 · 단추만 더함. - * · 로직 표 = [✓] · 차례 · 구분(엑셀 갈래 + 상태) · 이름(우리 로직 · 단위 환산 · 옵션 글) · 별칭(엑셀 품명 = 만들 때 별칭) · - * 수량산출(위 엑셀 / 아래 우리 = 엑셀 × 배) · 결과 단위 · 노무비 · 재료비 · 경비 · 합계 · 품 비교 · 단추 - * · 자재 표 = [✓] · 차례 · 코드 · 이름 · 규격 · 수량산출 · 단위 · 세 칸(자재구분 · 별도계상 · 운반비 · 글만) · - * 가격 다섯 · 엑셀 / 우리 금액 · 단추 - * · 금액 칸 = 위 작게 엑셀 · 아래 우리(서버 calc 미리 셈 · 저장 안 함) · 셈 중 「…」 - * · 단추 = [고르기] · [원문 보기] · [엑셀 보기] · [옵션](로직 대상마다) · 드롭다운 추가 · 로직없음 · 사용안함 - * (화면 글만 · 저장 값 = 빈 값 · 「우리에 없음」 · 「안 씀」 그대로) - * 줄 표 = `tableOf`(대상이 모두 자재 · 대상 없는 자재 · 별도계상 줄 = 자재 표). - * ========================================================================== */ - -import { createButton, el } from "@ui/ui_template_elements"; -import { t as L } from "@ui/ui_template_locale"; -import type { HopyoLine, HopyoPart } from "./M02_MasterTemplete_Hopyo_Api"; -import { - checkable, - compareText, - isChecked, - partsOf, - type CheckDraft, -} from "./M02_MasterTemplete_Hopyo_Check"; -import { inputsText, sourceText } from "./M02_MasterTemplete_Hopyo_Choose"; -import type { LaborCalc } from "./M02_MasterTemplete_Labor_Api"; -import { markCells, markHeads } from "./M02_MasterTemplete_Labor_MatMarks"; -import { moneyCell, PRICE_SLOTS } from "./M02_MasterTemplete_Labor_Rows"; -import { tc } from "./M02_MasterTemplete_Labor_Text"; -import { formatNumber, formatPrice } from "../common_util/common_util_money"; -import { tableOf } from "./logic/M02_MasterTemplete_Hopyo_Draft"; -import { unitView, type UnitView } from "./logic/M02_MasterTemplete_Hopyo_Unit"; - -/** 드롭다운 — 저장 값(서버 뺌 까닭) → 화면 글 */ -export const OUT_TEXT: [string, string][] = [ - ["", "추가"], - ["우리에 없음", "로직없음"], - ["안 씀", "사용안함"], -]; -const COSTS = ["노무비", "재료비", "경비", "계"] as const; -const COST_HEAD = { - 노무비: "Prev_Labor", - 재료비: "Prev_Material", - 경비: "Prev_Expense", - 계: "Prev_Sum", -} as const; - -/** 줄 하나 미리 셈 — 셈 중 = 둘 다 없음 */ -export interface LinePrice { - calc?: LaborCalc; - fail?: string; -} - -export interface RowsContext { - canWrite: boolean; - d: CheckDraft; - price: (line: HopyoLine) => LinePrice | null; - check: (line: HopyoLine, on: boolean) => void; - out: (line: HopyoLine, value: string) => void; - times: (line: HopyoLine, at: number, value: number | null) => void; - choose: (line: HopyoLine) => void; - source: (line: HopyoLine) => void; - sheet: (행: [number, number]) => void; - options: (line: HopyoLine, at: number) => void; -} - -const num = formatNumber; -const small = (text: string, cls = "m02-labrow__note"): HTMLElement => - el("small", { className: cls, text, attrs: { title: text } }); - -/** 금액 칸 — 위 엑셀(작게) · 아래 우리 */ -function pairCell( - excel: number | null | undefined, - ours: number | null | undefined, - wait: boolean, - label: string, -): HTMLElement { - const td = moneyCell(excel, ours, wait, label); - td.querySelector(".m02-labrow__base")?.setAttribute("title", "엑셀"); - td.querySelector(".m02-labrow__applied")?.setAttribute("title", "우리(미리 셈)"); - return td; -} - -/** 수량산출 — 위 엑셀 수량 · 아래 우리(엑셀 × 배 · 대상마다) */ -function qtyCell(line: HopyoLine, parts: HopyoPart[]): HTMLElement { - const ours = parts - .map((p) => (p.배 === null || line.수량 === null ? "—" : num(line.수량 * p.배))) - .join(" · "); - return el("td", { - className: "m02-labrow__qty", - attrs: { "data-label": tc("Labor_QtyCol") }, - children: [ - el("small", { - className: "m02-labrow__base", - text: line.수량 === null ? "—" : `${num(line.수량)} ${line.단위}`.trim(), - attrs: { title: "엑셀" }, - }), - el("span", { className: "m02-labrow__applied", text: ours || "—", attrs: { title: "우리" } }), - ], - }); -} - -const stateChip = (line: HopyoLine): HTMLElement => { - const m = line.맞춤; - const alias = m.별칭?.규격다름 ? " · 규격 다름" : m.별칭?.근거다름 ? " · 근거 다름" : ""; - const tags = [m.글, ...(m.꼬리표 ?? []), sourceText(line.근거)].filter(Boolean).join("\n"); - return el("span", { - className: `m02-hopyo__state is-${m.상태.replace(" ", "")}`, - text: `${m.상태}${alias}`, - attrs: { title: tags }, - }); -}; - -/** 단위 환산 칸 — 「엑셀 1 ㎡ = 우리 [ ] 본」(장부 화면도 같은 꼴) */ -export function unitField(input: HTMLInputElement, v: Pick): HTMLElement { - input.title = L("M02_HopyoUnitTip"); - const word = (text: string): HTMLElement => - el("span", { className: "m02-hopyo__unit-text", text }); - return el("span", { - className: "m02-hopyo__unit", - children: [ - word(L("M02_HopyoUnitLead").replace("{from}", v.from || "?")), - input, - ...(v.to ? [word(v.to)] : []), - ], - }); -} - -/** 대상마다 한 줄 — 키 · 이름 · 단위 환산(같음 = 글만 · 몫 줄 = 몫만 · 빈칸 · 추정 = 입력 칸) · 몫 */ -function partLines(line: HopyoLine, parts: HopyoPart[], ctx: RowsContext): HTMLElement[] { - return parts.map((p, at) => { - const v = unitView(p, line.단위, p.단위); - const times = el("input", { - className: "ui-input ui-input--sm m02-hopyo__times", - attrs: { type: "number", step: "any", min: "0", value: p.배 == null ? "" : String(p.배) }, - }) as HTMLInputElement; - times.disabled = !ctx.canWrite; - times.addEventListener("change", () => - ctx.times(line, at, times.value === "" ? null : Number(times.value)), - ); - const key = - p.로직 ?? p.자재 ?? (p.호표 !== undefined ? `제${p.호표}호표` : p.전국 ? "전국" : ""); - const note = v.estimated - ? v.thick !== null - ? L("M02_HopyoUnitThick").replace("{n}", String(v.thick)) - : L("M02_HopyoUnitEst") - : ""; - return el("div", { - className: "m02-hopyo__part", - attrs: { title: p.어디 ?? "" }, - children: [ - el("span", { className: "m02-labrow__key", text: key }), - el("strong", { text: p.이름 ?? "" }), - ...(v.mode === "same" ? [small(L("M02_HopyoUnitSame"))] : []), - ...(v.mode === "input" ? [unitField(times, v)] : []), - ...(note ? [small(note)] : []), - ...(p.몫 != null && p.몫 !== 1 ? [small(`몫 ${p.몫}`)] : []), - ], - }); - }); -} - -/** 이름 칸 덧글 — 옵션(칸=값 · 출처) · 못 가름 · 셈 못 함 까닭 */ -function nameNotes(line: HopyoLine, parts: HopyoPart[], price: LinePrice | null): HTMLElement[] { - const opts = parts - .filter((p) => p.로직) - .map((p) => inputsText(p.입력값 ?? {}, p.못읽음 ?? [], p.출처)) - .filter(Boolean); - if (line.맞춤.갈리는칸?.length) opts.push(`⚠ 못 가름: ${line.맞춤.갈리는칸.join(", ")}`); - const why = [ - price?.fail, - ...(price?.calc?.줄 ?? []).map((l) => l.까닭), - ...(price?.calc?.자재 ?? []).map((m) => m.까닭), - ].filter(Boolean) as string[]; - return [ - ...opts.map((t) => small(t, "m02-labrow__note m02-hopyo__opts")), - ...why.map((t) => small(`셈 못 함: ${t}`, "ui-text ui-text--error")), - ...(parts.some((p) => p.호표 !== undefined) ? [small("하위 호표 = 만든 뒤 셈")] : []), - ]; -} - -function checkCell(line: HopyoLine, ctx: RowsContext): HTMLElement { - const box = el("input", { - attrs: { type: "checkbox", "data-check": String(line.차례) }, - }) as HTMLInputElement; - box.checked = isChecked(line, ctx.d) && checkable(line, ctx.d); - box.disabled = !ctx.canWrite || !checkable(line, ctx.d); - box.addEventListener("change", () => ctx.check(line, box.checked)); - return el("td", { children: [box] }); -} - -function actsCell(line: HopyoLine, parts: HopyoPart[], ctx: RowsContext): HTMLElement { - const m = line.맞춤; - const ghost = (label: string, run: () => void): HTMLElement => - createButton({ label, variant: "ghost", size: "sm", onClick: run }); - const out = el("select", { - className: "ui-input ui-input--sm", - attrs: { - title: `로직없음 · 사용안함 — ${L("M02_PrecomboTitle")}에 안 넣고 장부에 남김`, - "data-out": "", - }, - children: OUT_TEXT.map(([value, text]) => el("option", { text, attrs: { value } })), - }) as HTMLSelectElement; - out.value = ctx.d.뺌[line.차례] ?? ""; - out.disabled = !ctx.canWrite || m.상태 === "%"; - out.addEventListener("change", () => ctx.out(line, out.value)); - const options = parts.flatMap((p, at) => { - if (!p.로직) return []; - const b = ghost("옵션", () => ctx.options(line, at)); - b.title = `${p.로직} 옵션`; - return [b]; - }); - return el("td", { - className: "m02-hopyo__acts", - children: [ - ...(ctx.canWrite && m.상태 !== "%" - ? [ghost(m.제안?.짝.length ? "고르기 · 내 제안" : "고르기", () => ctx.choose(line))] - : []), - ghost("원문 보기", () => ctx.source(line)), - ...(line.근거 ? [ghost("엑셀 보기", () => ctx.sheet(line.근거!.행))] : []), - ...options, - out, - ], - }); -} - -const rowOf = (line: HopyoLine, ctx: RowsContext, cells: HTMLElement[]): HTMLElement => { - const on = isChecked(line, ctx.d) && checkable(line, ctx.d); - return el("tr", { - className: `m02-labrow__row m02-hopyo__line is-${on ? "in" : "out"}`, - attrs: { "data-line": String(line.차례), "data-state": line.맞춤.상태 }, - children: [checkCell(line, ctx), ...cells, actsCell(line, partsOf(line, ctx.d), ctx)], - }); -}; - -function logicRow(line: HopyoLine, ctx: RowsContext): HTMLElement { - const parts = partsOf(line, ctx.d); - const price = ctx.price(line); - const wait = !!price && !price.calc && !price.fail; - const ours = price?.calc?.반영합; - const unit = price?.calc?.줄[0]?.분모 ?? parts[0]?.단위 ?? line.단위; - const compare = compareText(ctx.d.품[line.차례] ?? line.맞춤.품); - return rowOf(line, ctx, [ - el("td", { className: "m02-labrow__no", text: String(line.차례 + 1) }), - el("td", { className: "m02-labrow__kind", children: [line.갈래, stateChip(line)] }), - el("td", { - className: "m02-labrow__name m02-hopyo__parts", - children: [ - ...(parts.length ? partLines(line, parts, ctx) : [small("—", "ui-text ui-text--muted")]), - ...nameNotes(line, parts, price), - ], - }), - el("td", { - className: "m02-labrow__alias", - attrs: { title: `${line.이름} ${line.규격}`.trim() }, - children: [line.이름, ...(line.규격 ? [small(line.규격)] : [])], - }), - qtyCell(line, parts), - el("td", { className: "m02-labrow__unit", text: unit ?? "" }), - ...COSTS.map((c) => - pairCell(line.금액[c], price?.calc ? (ours?.[c] ?? null) : null, wait, tc(COST_HEAD[c])), - ), - el("td", { className: "m02-hopyo__compare", text: compare.split(" · ").join("\n") }), - ]); -} - -function matRow(line: HopyoLine, ctx: RowsContext): HTMLElement { - const parts = partsOf(line, ctx.d); - const price = ctx.price(line); - const wait = !!price && !price.calc && !price.fail; - const mats = price?.calc?.자재 ?? []; - const first = mats[0]; - const ours = mats.length ? mats.reduce((s, m) => s + (m.반영 ?? 0), 0) : null; - const show = (v: number | null | undefined): string => - wait ? "…" : v === null || v === undefined ? "—" : formatPrice(v); - const p = parts[0]; - return rowOf(line, ctx, [ - el("td", { className: "m02-labrow__no", text: String(line.차례 + 1) }), - el("td", { - className: "m02-labrow__code", - text: parts.map((x) => x.자재 ?? (x.전국 ? "전국" : "")).join(" · ") || "—", - }), - el("td", { - className: "m02-labrow__name m02-hopyo__parts", - children: [ - stateChip(line), - ...(parts.length ? partLines(line, parts, ctx) : []), - small(`엑셀 ${line.이름}`.trim()), - ...nameNotes(line, parts, price), - ], - }), - el("td", { - className: "m02-labrow__spec", - text: p?.규격 ?? line.규격, - attrs: { title: p?.규격 ?? line.규격 }, - }), - qtyCell(line, parts), - el("td", { className: "m02-labrow__unit", text: p?.단위 ?? line.단위 }), - ...markCells(p?.세칸 ?? { 별도계상: line.갈래 === "별도계상" ? "별도계상" : "직접계산" }), - ...PRICE_SLOTS.map((slot) => - el("td", { - className: `m02-labrow__num m02-labrow__price${first?.기준칸 === slot ? " is-base-price" : ""}`, - text: show(first?.가격?.[slot]), - attrs: { "data-label": slot, "data-price-slot": slot }, - }), - ), - pairCell(line.금액.계, price?.calc ? ours : null, wait, tc("Labor_AppliedSum")), - ]); -} - -const grid = (heads: string[], rows: HTMLElement[], kind: string): HTMLElement => - el("div", { - className: "ui-general-block__table-wrap ui-general-block__table-wrap--sticky-head", - children: [ - el("table", { - className: "ui-general-block__table m02-labrow m02-hopyo__table m02-hopyo__lines", - attrs: { "data-hopyo-table": kind }, - children: [ - el("thead", { - children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })], - }), - el("tbody", { children: rows }), - ], - }), - ], - }); - -/** 줄 표 둘 — 로직 표 · 자재 표(줄 없는 표는 안 그림) */ -export function checkTables(lines: HopyoLine[], ctx: RowsContext): HTMLElement[] { - const logic = lines.filter((l) => tableOf(l, partsOf(l, ctx.d)) === "logic"); - const mat = lines.filter((l) => !logic.includes(l)); - const out: HTMLElement[] = []; - if (logic.length) - out.push( - grid( - [ - "", - tc("Col_Order"), - tc("Sub"), - tc("Col_Name"), - tc("Col_Alias"), - tc("Labor_QtyCol"), - tc("Col_Unit"), - ...COSTS.map((c) => tc(COST_HEAD[c])), - "품 비교", - "", - ], - logic.map((l) => logicRow(l, ctx)), - "logic", - ), - ); - if (mat.length) - out.push( - el("h4", { className: "m02-hopyo__subhead", text: "자재" }), - grid( - [ - "", - tc("Col_Order"), - tc("Labor_Code"), - tc("Col_Name"), - tc("Labor_Spec"), - tc("Labor_QtyCol"), - tc("Labor_Unit"), - ...markHeads(), - ...PRICE_SLOTS, - `엑셀 / 우리 ${tc("Labor_AppliedSum")}`, - "", - ], - mat.map((l) => matRow(l, ctx)), - "mat", - ), - ); - return out; -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Choose.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Choose.ts deleted file mode 100644 index 7ee3b84dc..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Choose.ts +++ /dev/null @@ -1,383 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Hopyo_Choose.ts - * 일위대가 실무자료 고르기 창(PLAN 338-3 · 358 · 설계 4-2 · 옛 `Hopyo_View.choose` 를 옮김) + 글 도우미. - * · 위 = 엑셀 블록 품(직종 · 장비 · 자재) · 인용 조항 · [엑셀 보기](단가산출 그 행) - * · 후보 묶음(358-1 · 숨김 없음) = 1위 + 판정 / 출처 같음 / 이름 닮은 로직(옛 324 다섯) / 숫자만 닮음(접힘 · - * 뜻 낱말 안 겹치는 「후보」는 회색 끝) · 같은 이름이면 책(산림 · 건설) · 판정 꼬리표 · 근거 꼬리표(근거 맞음 초록) - * · 고름 = [후보](한 로직) · 또는 체크 + 몫 칸 → [일괄 적용(n)](조합) · 옵션은 부르는 쪽이 서버 `guess` 로 채움 - * · [다른 항목 검색](736-1) = 찾아 넣기 창(`Find_Pick`) 하나 · 품명 첫 뜻 낱말 · 기초 = 자재 아닌 줄 · 재료 = 자재 · 재료비 있는 줄(358-2) - * · 자재 줄(361-1) = 1위 / 전국(자리표 · 지역 고른 뒤 되돌림) / 지역 변형(접힘 · 하나 고르면 그 지역) / 그 밖 - * 저장 안 함 — 고른 것은 부르는 쪽 캐시(확인 창)로만 감. - * ========================================================================== */ - -import { createBadge } from "@ui/ui_template_badge"; -import { createButton, el } from "@ui/ui_template_elements"; -import { openModal } from "@ui/ui_template_modal"; -import type { - BuildTarget, - HopyoBasisHit, - HopyoLine, - HopyoLineBasis, - HopyoMatch, - HopyoQty, - HopyoTarget, -} from "./M02_MasterTemplete_Hopyo_Api"; -import { getActiveLayer, getActiveOwner } from "./M02_MasterTemplete_Api_Fetch"; -import { type FindKind, openFindPicker } from "./M02_MasterTemplete_Find_Pick"; -import { formatNumber } from "../common_util/common_util_money"; -import "./M02_MasterTemplete_Hopyo.css"; - -const MATERIAL = new Set(["자재", "별도계상"]); -const FOLD_TEXT: Record = { - "지역 변형": "하나 고르면 그 지역 · 「전국」으로 되돌림은 위", - 숫자: "이름 · 출처 안 닮음 · 숫자 지문만", -}; -const PRE_TIP = "미리 고름 — 엑셀 인용 조항 = 1위 기초 조항 · 누르기는 사용자"; -const note = (text: string): HTMLElement => - el("span", { className: "ui-text ui-text--muted", text, attrs: { "data-text": "14" } }); -const num = formatNumber; - -/** 대상 글 — 「이름 [단위]」 · 자재는 규격도 · 호표 = 「제N호표」 */ -export const targetText = (t: HopyoTarget | null | undefined): string => { - if (!t) return ""; - if (t.호표 !== undefined) return `제${t.호표}호표 ${t.단위 ? `[${t.단위}]` : ""}`.trim(); - const spec = t.규격 ? ` ${t.규격}` : ""; - return `${t.이름 ?? t.로직 ?? t.자재 ?? ""}${spec}${t.단위 ? ` [${t.단위}]` : ""}`; -}; - -/** 입력값 글 — 「이름=값」 · 칸 출처(블록 · 규격 · 별칭) · 못 가르는 칸 표시 */ -export const inputsText = ( - values: Record, - unread: string[], - from: Record = {}, -): string => - [ - Object.entries(values) - .map( - ([k, v]) => - `${k}=${formatNumber(v)}${from[k] && from[k] !== "자동값" ? `(${from[k]})` : ""}`, - ) - .join(", "), - unread.length ? `⚠ 못 가름: ${unread.join(", ")}` : "", - ] - .filter(Boolean) - .join(" · "); - -/** 만들기 결과 낱말 — 있던 것을 덮음 · 새로 만듦 */ -export const builtWord = (got: { 다시씀: boolean }): string => - got.다시씀 ? "있던 것을 확인한 내용으로 덮음" : "만듦"; - -/** 근거 맞음 — 조항 같음(앞 마디 아님) · 단위 맞음 · 직종 겹침 - * (줄 근거 직종이 보통인부뿐이거나 없으면 — 기계 등 — 직종은 안 봄) */ -export const basisOk = (b: HopyoBasisHit | undefined, line?: HopyoLineBasis): boolean => - !!b && - b.까닭.startsWith("조항 같음") && - b.단위 && - (b.직종.length > 0 || !(line?.직종 ?? []).some((x) => x !== "보통인부")); - -/** 후보 근거 꼬리표 — 「조항 건설 공통 3-2-1 · 직종 특별인부 · 단위 같음」 · 근거 없으면 "" */ -export const basisTag = (b: HopyoBasisHit | undefined): string => - b - ? [ - b.조항.length ? `조항 ${b.조항.join(", ")}` : "", - b.직종.length ? `직종 ${b.직종.join(", ")}` : "", - b.단위 ? "단위 같음" : "", - ] - .filter(Boolean) - .join(" · ") - : ""; - -/** 엑셀 근거 줄 — 「출처 건설표준품셈 3-2-1 · 직종 특별인부 · 단가산출 93–101행」 · 없으면 "" */ -export const sourceText = (b: HopyoLineBasis | undefined): string => - b - ? [ - b.출처.length ? `출처 ${b.출처.map((x) => x.join(" ").trim()).join(", ")}` : "", - b.직종.length ? `직종 ${b.직종.join(", ")}` : "", - `단가산출 ${b.행[0] === b.행[1] ? b.행[0] : `${b.행[0]}–${b.행[1]}`}행`, - ] - .filter(Boolean) - .join(" · ") - : ""; - -/** 품 글 — 「콘크리트공 0.1091 · 굴착기 0.29hr · 레미콘 1.02」 */ -export const qtyText = (q: HopyoQty | undefined): string => - q - ? [ - ...Object.entries(q.인력 ?? {}).map(([k, v]) => `${k} ${num(v)}`), - ...(q.장비 ?? []).map((x) => `${[x.갈래].flat().join("+")} ${num(x.시간)}hr`), - ...(q.자재 ?? []).map((x) => `${x.이름} ${num(x.수량)}`), - ].join(" · ") - : ""; - -/** [다른 항목 검색] 미리 넣을 글 — 품명 괄호 밖 첫 뜻 낱말(358-1 · 조항 번호는 모든 낱말 맞춤이라 0건 위험) */ -export function searchHint(line: Pick): string { - return /[가-힣A-Za-z]{2,}/.exec(line.이름.replace(/\([^()]*\)/g, " "))?.[0] ?? ""; -} - -/** 후보 묶음 하나 — 제목 · 접힘(숫자만 닮음) · 후보 */ -export interface CandGroup { - 제목: string; - 접힘: boolean; - 후보: HopyoTarget[]; -} - -/** 약한 후보 — 판정 「후보」 + 품명 뜻 낱말 안 겹침(회색 · 묶음 끝) */ -export const weakCand = (c: HopyoTarget): boolean => c.판정 === "후보" && c.뜻겹침 === false; - -/** 후보 묶음(358-1 · 숨김 없음 · 차례는 서버 그대로) — 1위 + 판정 / 출처 같음 / 이름 닮은 로직(옛 판 다섯) / - * 숫자만 닮음(접힘 · 약한 후보 뒤로) · 한 후보는 한 묶음에만 · 자재 줄 = 1위 / 그 밖 후보 */ -export function candGroups( - m: Pick, - material = false, -): CandGroup[] { - const all = m.후보 ?? []; - const seen = new Set(); - const take = (list: HopyoTarget[]): HopyoTarget[] => - list.filter((c) => { - const id = c.로직 ?? c.자재 ?? c.이름 ?? ""; - if (seen.has(id)) return false; - seen.add(id); - return true; - }); - const top = take(all.slice(0, 1)); - if (material) - return [ - { 제목: "1위", 접힘: false, 후보: top }, - { 제목: "전국", 접힘: false, 후보: take(all.filter((c) => c.전국)) }, - { 제목: "지역 변형", 접힘: true, 후보: take(all.filter((c) => c.변형)) }, - { 제목: "그 밖 후보", 접힘: false, 후보: take(all.slice(1)) }, - ].filter((g) => g.후보.length); - const named = m.이름후보 ?? []; - const source = take([...all.slice(1), ...named].filter((c) => c.출처같음)); - const name = take(named); - const rest = take(all.slice(1)); - rest.sort((a, b) => Number(weakCand(a)) - Number(weakCand(b))); - return [ - { 제목: "1위", 접힘: false, 후보: top }, - { 제목: "출처 같음", 접힘: false, 후보: source }, - { 제목: "이름 닮은 로직", 접힘: false, 후보: name }, - { 제목: "숫자만 닮음", 접힘: true, 후보: rest }, - ].filter((g) => g.후보.length); -} - -/** 같은 이름 후보 — 책(산림 · 건설)을 붙여 가를 이름 */ -export function sameNames(groups: CandGroup[]): Set { - const count = new Map(); - for (const c of groups.flatMap((g) => g.후보)) - count.set(c.이름 ?? "", (count.get(c.이름 ?? "") ?? 0) + 1); - return new Set([...count].filter(([name, n]) => name && n > 1).map(([name]) => name)); -} - -/** 후보 단추 글 — 「이름 [단위] · 키」 · 같은 이름이면 책 두 글자(산림 · 건설) */ -export const candText = (c: HopyoTarget, twins: Set): string => - `${targetText(c)} · ${c.로직 ?? c.자재 ?? "키 없음"}${twins.has(c.이름 ?? "") && c.책 ? ` · ${c.책.slice(0, 2)}` : ""}`; - -/** 자재 고름 — 자재 · 별도계상 줄 · 재료비 있는 단가산출 줄(358-2 · P.V.C파이프설치 등) */ -export const canPickMaterial = (line: Pick): boolean => - MATERIAL.has(line.갈래) || !!line.단가?.재료비; - -/** 736-2 [다른 항목 검색…] 탭 — 줄 갈래대로(서버 이음 칸 = 노임 LB · 기계 EQ · 자재 · 로직) */ -export const kindsOf = (line: Pick): FindKind[] => - line.갈래 === "노임" - ? ["labor"] - : line.갈래 === "기계" - ? ["machine"] - : [ - ...(MATERIAL.has(line.갈래) ? [] : (["logic"] as const)), - ...(canPickMaterial(line) ? (["material"] as const) : []), - ]; - -/** 고른 후보 → 만들기 대상(배 1 · 몫 있으면 배 = 몫) */ -export const pickedTargets = (picked: { c: HopyoTarget; 몫: number | null }[]): BuildTarget[] => - picked.map(({ c, 몫 }) => ({ - ...(c.로직 ? { 로직: c.로직 } : c.전국 ? { 전국: true } : { 자재: c.자재 }), - 입력값: {}, - 배: 몫 ?? 1, - 몫, - })); - -export interface ChooseOptions { - line: HopyoLine; - /** [엑셀 보기] — 단가산출 그 행 */ - openSheet?: (행: [number, number]) => void; - done: (targets: BuildTarget[]) => void; -} - -/** 고르기 창 — 엑셀 품 · 인용 조항 · 후보 묶음(하나 또는 여럿 · 몫) · 다른 로직 · 자재 검색 */ -export function openHopyoChoose(opt: ChooseOptions): void { - const { line } = opt; - const material = MATERIAL.has(line.갈래); - openModal({ - title: `고르기 — ${line.이름} ${line.규격}`.trim(), - closeLabel: "닫기", - dialogClass: "m02-modal m02-hopyo__choose", - mount: (body, close) => { - const finish = (targets: BuildTarget[]): void => { - close(); - opt.done(targets); - }; - const picked = new Map(); - const together = createButton({ label: "일괄 적용(0)", size: "sm", disabled: true }); - const paint = (): void => { - together.querySelector(".ui-btn__label")!.textContent = `일괄 적용(${picked.size})`; - together.disabled = picked.size < 2; - }; - together.addEventListener("click", () => - finish( - pickedTargets( - [...picked].map(([c, box]) => ({ c, 몫: box.value === "" ? null : Number(box.value) })), - ), - ), - ); - const groups = candGroups(line.맞춤, material); - // 739-1 내 제안 — 맨 위 · 한 번 눌러 짝 통째로(입력값 · 배 · 몫 그대로) - const mine = line.맞춤.제안?.짝.length ? line.맞춤.제안 : null; - const mineRow = mine - ? el("div", { - attrs: { "data-group": "내 제안" }, - children: [ - note("내 제안 — 승인 전(M01 별칭 장부 승인 뒤 자동)"), - el("div", { - className: "m02-hopyo__cand", - attrs: { "data-cand": "내 제안" }, - children: [ - createButton({ - label: mine.대상.map((c) => candText(c, new Set())).join(" + "), - variant: "ghost", - size: "sm", - onClick: () => - finish( - mine.짝.map((p) => ({ - ...(p.로직 ? { 로직: p.로직 } : { 자재: p.자재 }), - 입력값: p.입력값 ?? {}, - 배: p.배, - 몫: p.몫 ?? null, - })), - ), - }), - el("span", { className: "m02-hopyo__state is-별칭", text: "내 제안" }), - ], - }), - ], - }) - : null; - const twins = sameNames(groups); - const row = (c: HopyoTarget): HTMLElement => { - const share = el("input", { - className: "ui-input ui-input--sm m02-hopyo__times", - attrs: { type: "number", step: "0.05", min: "0", max: "1", placeholder: "몫" }, - }) as HTMLInputElement; - const box = el("input", { attrs: { type: "checkbox", title: "함께 고름(조합)" } }); - box.addEventListener("change", () => { - if ((box as HTMLInputElement).checked) picked.set(c, share); - else picked.delete(c); - paint(); - }); - const tag = basisTag(c.근거); - return el("div", { - className: `m02-hopyo__cand${weakCand(c) ? " is-weak" : ""}`, - attrs: { - "data-cand": c.로직 ?? c.자재 ?? (c.전국 ? "전국" : ""), - ...(c.미리 ? { "data-pre": "" } : {}), - }, - children: [ - ...(c.미리 ? [createBadge(PRE_TIP, "accent", { kind: "dot" })] : []), - box, - createButton({ - label: candText(c, twins), - variant: "ghost", - size: "sm", - onClick: () => finish(pickedTargets([{ c, 몫: null }])), - }), - ...(c.판정 - ? [ - el("span", { - className: `m02-hopyo__state is-${c.판정.replace(" ", "")}`, - text: c.판정, - }), - ] - : []), - ...(c.미리 ? [note("미리 고름")] : []), - ...(c.글 ? [note(c.글)] : []), - ...(weakCand(c) ? [note("이름 뜻 안 겹침")] : []), - ...(tag && c.근거 - ? [ - el("span", { - className: `m02-hopyo__basis${basisOk(c.근거, line.근거) ? " is-ok" : ""}`, - text: tag, - attrs: { title: `${c.근거.까닭} · 점수 ${c.근거.점수}` }, - }), - ] - : []), - share, - ], - }); - }; - const sections = groups.map((g) => { - const list = el("div", { className: "m02-hopyo__cands", children: g.후보.map(row) }); - const title = `${g.제목} (${g.후보.length})`; - if (!g.접힘) - return el("div", { attrs: { "data-group": g.제목 }, children: [note(title), list] }); - const fold = el("details", { - className: "m02-hopyo__group", - attrs: { "data-group": g.제목 }, - children: [ - el("summary", { text: `${title} — ${FOLD_TEXT[g.제목] ?? FOLD_TEXT.숫자}` }), - list, - ], - }); - return fold; - }); - const excel = qtyText(line.맞춤.품?.엑셀); - const basis = line.근거; - const head = el("div", { - className: "ui-toolbar m02-hopyo__bar m02-hopyo__source", - children: [ - ...(excel ? [note(`엑셀 품 — ${excel}`)] : []), - ...(basis ? [note(`엑셀 근거 — ${sourceText(basis)}`)] : []), - ...(basis && opt.openSheet - ? [ - createButton({ - label: "엑셀 보기", - variant: "ghost", - size: "sm", - onClick: () => opt.openSheet?.(basis.행), - }), - ] - : []), - ], - }); - // 736-1 · 736-2 찾아 넣기 창 하나 — 줄 갈래대로 탭(노임 = 인력 · 기계 = 기계 · 자재 = 재료 · 그 밖 = 기초 로직 · 재료비 있는 줄은 재료도) - const kinds = kindsOf(line); - const find = createButton({ - label: "다른 항목 검색…", - size: "sm", - onClick: () => { - close(); - openFindPicker({ - layer: getActiveLayer(), - owner: getActiveOwner(), - kinds, - query: searchHint(line.갈래 === "기계" ? { 이름: line.규격 || line.이름 } : line), // 기계 = 규격 낱말(굴착기) - logic: (x) => opt.done([{ 로직: x.키, 입력값: {}, 배: 1 }]), - material: (e) => opt.done([{ 자재: e.코드, 배: 1 }]), - element: (e) => opt.done([{ 노임: e.코드, 배: 1 }]), - machine: (e) => opt.done([{ 기계: e.코드, 배: 1 }]), - }); - }, - }); - const more = kinds.length ? [find] : []; - body.append( - ...(head.childElementCount ? [head] : []), - note( - groups.length - ? "후보 — 하나 고름 · 여럿은 체크(몫 칸) 뒤 [일괄 적용]" - : "가까운 것 없음 — 아래 검색", - ), - ...(mineRow ? [mineRow] : []), - ...sections, - el("div", { className: "ui-toolbar m02-hopyo__bar", children: [together, ...more] }), - ); - }, - }); -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Compare.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Compare.ts deleted file mode 100644 index 9779109f0..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Compare.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Hopyo_Compare.ts - * 불러오기 M01 요소 후보 단추(PLAN 746-2 · 요구 585 · 서버 746-1 `후보`) — - * · `elemChoices` = 안 이은 줄의 M01 요소 후보 셋 단추(누르면 부르는 쪽이 이음) - * · 옛 고름 대기 기초 줄 비교(`compareView`)는 778-5 D-5 줄 고름으로 걷음 - * 보이기만 — 판정 · 순위는 서버 그대로 · 고르기는 부르는 쪽. - * ========================================================================== */ - -import { createButton, el } from "@ui/ui_template_elements"; -import { t as L } from "@ui/ui_template_locale"; -import { formatPrice } from "../common_util/common_util_money"; -import type { ElemCand } from "./M02_MasterTemplete_Hopyo_Api"; - -/** 요소 후보 단추 줄(셋까지) — 누르면 `pick(후보)` */ -export function elemChoices(cands: ElemCand[], pick: (c: ElemCand) => void): HTMLElement | null { - if (!cands.length) return null; - const box = el("div", { className: "ui-toolbar", attrs: { "data-elem": "" } }); - box.title = L("M02_ElemTitle"); - for (const c of cands.slice(0, 3)) { - const price = c.단가 ? ` · ${formatPrice(c.단가)}` : ""; - const b = createButton({ - label: `${c.이름}${c.규격 ? ` ${c.규격}` : ""} [${c.단위 ?? ""}]${price}`, - size: 24, - variant: "ghost", - onClick: () => pick(c), - }); - b.dataset.elemKey = c.키; - b.title = `${c.키} · ${c.갈래}`; - box.append(b); - } - return box; -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Import.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Import.ts index e5360760f..4c4a2c52a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Import.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Import.ts @@ -2,37 +2,22 @@ * M02_MasterTemplete_Hopyo_Import.ts * 일위대가 불러오기 본 칸 보기(PLAN 785-4 · 요구 642-11 — 마법사 `Hopyo_Wizard` 갈음 · 가운데 상세 페이지 없음) — * 왼쪽 [불러오기] → 층 · 파일 고름 → 올림(`Hopyo_Side`) → 바로 이 보기: - * · 머리 막대 = 엑셀 보기 몸(`Hopyo_Sheet` — 시트 · 호표 · 검산 · 제안)의 머리에 층 · 안 이은 줄 배지 · - * [안 이은 줄 고르기] · [끝내기]를 덧붙임 - * · 아래 = 엑셀(탭 셋) + 오른쪽 호표 · 단가산출 목록(`Hopyo_Sheet` · `Hopyo_SheetSk`) · 만들기 = 목록 아래 막대 - * · 안 이은 줄 수 = 엑셀 보기가 읽은 호표 번으로 셈([끝내기] 전 「0」 오표시 흠 고침 — 빈 호표 번 안 씀) - * · [끝내기] = 남은 줄 다시 셈 → 있으면 막음(배지 · 남은 수 · 표) · 없으면 물음 → 서버가 이번 HP · 격자 · 맞춤셈 캐시 지움 · - * 만든 일위대가 · 단가산출(엑셀 사본 걷고 출처만) · 별칭 장부 남음 + * · 머리 막대 = 엑셀 보기 몸(`Hopyo_Sheet` — 시트 · 호표 · 검산 · 제안)의 머리에 층 · [끝내기]를 덧붙임 + * · 아래 = 엑셀(탭 셋) + 오른쪽 호표 · 단가산출 목록(`Hopyo_Sheet` · `Hopyo_SheetSk`) · 만들기 = 목록 아래 [옮기기] + * · [끝내기] = 물음(확인하고 안 옮긴 수 · 안 옮긴 단가산출 알림) → 서버가 이번 HP · 격자 · 블록 풀이 저장본 지움 · + * 만든 일위대가 · 단가산출(엑셀 사본 걷고 출처만) · 별칭 장부 남음 · 옛 「안 이은 줄」 띠는 785-7 에서 지움(판정 「고를 것」) * · 작업 중 = 이 화면 동안만(743-1) — 화면 떠남(`leave` · 페이지 닫음) = 이 화면에서 올리거나 이은 파일 지움 · * 하루 지난 것은 서버가 지움 · 엑셀 자료는 보관하지 않음(필요하면 다시 올림) * ========================================================================== */ -import { createBadge, type BadgeTone } from "@ui/ui_template_badge"; +import { createBadge } from "@ui/ui_template_badge"; import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { createText } from "@ui/ui_template_text"; import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; -import { - deleteHopyo, - readHopyo, - type HopyoFile, - type HopyoScope, - type HopyoView, -} from "./M02_MasterTemplete_Hopyo_Api"; -import { draftKey, hasDraft } from "./M02_MasterTemplete_Hopyo_Check"; +import { deleteHopyo, type HopyoFile, type HopyoScope } from "./M02_MasterTemplete_Hopyo_Api"; import type { HopyoSheetBody } from "./M02_MasterTemplete_Hopyo_Sheet"; -import { - countLeft, - leftCount, - openLeft, - type LeftMap, - type LeftScope, -} from "./M02_MasterTemplete_Hopyo_Left"; +import { draftKey, moveBody, parseDraft } from "./logic/M02_MasterTemplete_Hopyo_Picks"; import { accessFor, type ManageLayer } from "./M02_MasterTemplete_Layer"; import "./M02_MasterTemplete_Hopyo.css"; @@ -63,16 +48,13 @@ export interface HopyoImport { root: HTMLElement; /** 화면 떠남 — 이 화면에서 올리거나 이은 파일 지움(743-1) */ leave: () => void; - /** 고른 뒤 안 만든 확인 창 캐시가 있음 — 다른 엑셀로 옮기기 전 물음 */ + /** 확인하고 안 옮긴 판정 고름이 있음 — 다른 엑셀로 옮기기 전 물음 */ dirty: () => boolean; } export function buildHopyoImport(a: HopyoImportArgs): HopyoImport { const { layer, file } = a; const scope: HopyoScope = { layer, owner: null }; - const at: LeftScope = { scope, target: scope, key: file.키 }; - let nos: number[] = []; - let map: LeftMap | null = null; let held = true; let body: HopyoSheetBody | null = null; @@ -91,88 +73,26 @@ export function buildHopyoImport(a: HopyoImportArgs): HopyoImport { window.addEventListener("pagehide", leave); // ── 머리 막대에 덧붙일 것 ── - const badge = el("span", { attrs: { "data-hopyo-left": "" } }); const finish = createButton({ label: "끝내기", variant: "filled", size: 28, onClick: () => void end() }); // prettier-ignore finish.dataset.hopyoFinish = ""; - const onSk = (번: number): void => body?.showSk(번); - const pickBtn = createButton({ - label: L("M02_LeftOpen"), - variant: "ghost", - size: 28, - onClick: () => void openPick(), - }); - pickBtn.dataset.hopyoLeftOpen = ""; const layerNote = createBadge(`${layerText(layer)} 층`, "neutral"); - - /** 안 이은 줄 창 — 호표 번이 아직 없으면 먼저 읽음(빈 번으로 열면 「0」 오표시) */ - async function openPick(): Promise { - try { - await ensureNos(); - } catch (error) { - return void showToast(why(error), "error"); - } - openLeft({ ...at, 파일: file.파일, nos, onSk, ...(map ? { map } : {}) }, paint); - } - - async function ensureNos(): Promise { - if (!nos.length) nos = (await readHopyo(scope, file.키)).호표.map((h) => h.번); - } - - /** 배지 — 남은 줄 있으면 [끝내기] 막음 · 까닭 · 남은 수 */ - function paint(got: LeftMap | null): void { - map = got; - const n = got ? leftCount(got) : null; - const w = body?.skCount(); - const skLeft = w?.블록 ? `${L("M02_SkLeft").replace("{n}", String(w.블록)).replace("{m}", String(w.줄))} · ` : ""; // prettier-ignore - finish.disabled = !!n?.줄; - finish.title = n?.줄 ? skLeft + L("M02_LeftBlocked") : ""; - badge.dataset.left = n ? String(n.줄) : ""; - const tone: BadgeTone = !n ? "info" : n.줄 ? "warning" : "success"; - const text = !n - ? L("M02_LeftUnknown") - : n.줄 - ? L("M02_LeftBand").replace("{n}", String(n.줄)).replace("{h}", String(n.호표)) - : L("M02_LeftZero"); - badge.replaceChildren(createBadge(text, tone)); - } - paint(null); - - async function recount(): Promise { - await ensureNos(); - const got = await countLeft(at, nos, (i, all) => - badge.replaceChildren(createText(`${L("M02_LeftCounting")} ${i + 1} / ${all}`, "muted", 14)), - ); - paint(got); - return got; - } + /** 확인하고 안 옮긴 줄 · 블록 수(판정 고름 캐시 — 옮기면 캐시에서 빠짐) */ + const unmoved = (): number => + moveBody(parseDraft(sessionStorage.getItem(draftKey(layer, null, file.키))), scope).수; async function end(): Promise { finish.disabled = true; - let got: LeftMap; - try { - got = await recount(); - } catch (error) { - showToast(why(error), "error"); - finish.disabled = false; - return; - } - if (leftCount(got).줄) { - showToast(L("M02_LeftBlocked"), "warning"); - return openLeft({ ...at, 파일: file.파일, nos, onSk, map: got }, paint); - } - const loose = [...got.values()].filter( - (x) => !x.one.호표.만든?.확인 && hasDraft(draftKey(scope, file.키, x.one.호표.번)), - ).length; + const loose = unmoved(); const w = body?.skCount(); // 안 옮긴 단가산출(일위대가 밖 포함) — 묻기만 const ok = await showConfirmDialog( - `「${file.파일}」 불러오기를 끝냄 — 올린 파일(엑셀 결과 · 격자 · 맞춤 캐시)을 지움 · 만든 일위대가 · 단가산출(출처만 남김)과 별칭 장부는 남음 · 같은 엑셀은 필요하면 다시 올림` + - (loose ? ` · ${L("M02_LeftLoose").replace("{n}", String(loose))}` : "") + + `「${file.파일}」 불러오기를 끝냄 — 올린 파일(엑셀 결과 · 격자 · 블록 풀이)을 지움 · 만든 일위대가 · 단가산출(출처만 남김)과 별칭 장부는 남음 · 같은 엑셀은 필요하면 다시 올림` + + (loose ? ` · 확인하고 안 옮긴 줄 · 블록 ${loose} 개는 빠짐` : "") + (w?.블록 ? ` · ${L("M02_SkUnmoved").replace("{n}", String(w.블록)).replace("{m}", String(w.밖))}` : ""), // 780-2 막지 않음 "끝내기", ); - if (!ok) return paint(map); + if (!ok) return void (finish.disabled = false); try { const out = (await deleteHopyo(scope, file.키)) as { 만든?: number }; held = false; @@ -181,7 +101,8 @@ export function buildHopyoImport(a: HopyoImportArgs): HopyoImport { showToast(`끝냄 — 만든 일위대가 · 단가산출 ${out.만든 ?? 0} · 올린 파일 지움`, "success"); } catch (error) { showToast(why(error), "error"); - return paint(map); + finish.disabled = false; + return; } a.onEnded(); } @@ -196,9 +117,7 @@ export function buildHopyoImport(a: HopyoImportArgs): HopyoImport { 파일: file.파일, target: scope, canWrite: true, - headExtra: [layerNote, badge, pickBtn, finish], - onSkChange: () => paint(map), - onRead: (got: HopyoView) => void (nos = got.호표.map((h) => h.번)), + headExtra: [layerNote, finish], }); body = view; root.replaceChildren(...view.parts); @@ -208,6 +127,6 @@ export function buildHopyoImport(a: HopyoImportArgs): HopyoImport { return { root, leave, - dirty: () => nos.some((no) => hasDraft(draftKey(scope, file.키, no))), + dirty: () => unmoved() > 0, }; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Kids.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Kids.ts deleted file mode 100644 index de3aa001a..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Kids.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Hopyo_Kids.ts - * 일위대가 실무자료 만들기 몸 · 하위 호표 함께(PLAN 777-1 · 요구 621-4) — 호표가 다른 호표를 부르면 일위대가 둘 - * (하위 일위대가 따로 + 부모 줄 = 하위 사본). - * · bodyFor · wholeFor = 확인 창 캐시 위 체크 제안(짝 = 서버 `M02_Hopyo_Kids.proposal` · 캐시 빈 것) - * · unmadeKids = 고른 호표 줄이 부르는 확인 안 된 하위 호표(깊이 끝까지 · 하위 먼저) — [저장]이 그 캐시대로 함께 만듦 - * · buildWithKids = 확인 창 [생성] — 확인 안 된 하위를 그 캐시대로 먼저 → 부모(실패 = 「하위 제N호표 실패」) - * · 캐시 없는 하위도 서버가 부모 저장 때 제안대로 함께 만듦(`M02_Hopyo_Kids.first`) — 화면은 캐시 고름을 살림 - * · kidsText = 결과 글 「· 하위 제4호표 SC-11(사본 담음)」 · leftAt = 하위 실패의 남은 줄 = 그 하위 [고르기] - * ========================================================================== */ - -import { t as L } from "@ui/ui_template_locale"; -import { StaleError } from "./M02_MasterTemplete_Api_Fetch"; -import { - buildHopyo, - readHopyoOne, - type HopyoBuilt, - type HopyoOne, - type HopyoScope, -} from "./M02_MasterTemplete_Hopyo_Api"; -import { bodyOf, draftKey, loadDraft, partsOf, toTarget } from "./M02_MasterTemplete_Hopyo_Check"; - -type Body = Parameters[2]; -type Whole = Parameters[3]; -type From = Parameters[1]; - -/** 호표 하나의 몸 — 확인 창 캐시 위 체크 제안 · 통째면 줄 없음 */ -export function bodyFor(scope: HopyoScope, key: string, one: HopyoOne): Body { - const h = one.호표; - const d = loadDraft(draftKey(scope, key, h.번)); - return h.통째 && (d.통째 ?? h.통째.체크) ? [] : bodyOf(h.줄, d); -} -export function wholeFor(scope: HopyoScope, key: string, one: HopyoOne): Whole { - const h = one.호표; - const d = loadDraft(draftKey(scope, key, h.번)); - return h.통째 && (d.통째 ?? h.통째.체크) ? { 대상: h.통째.대상.map(toTarget) } : null; -} - -/** (줄, 통째) 몸 한 벌 */ -export const bodyPair = (scope: HopyoScope, key: string, one: HopyoOne): [Body, Whole] => [ - bodyFor(scope, key, one), - wholeFor(scope, key, one), -]; - -/** 호표 줄이 부르는 호표 번(체크 안 한 호표 줄 · 캐시에서 고른 호표 대상까지) */ -const kidNos = (scope: HopyoScope, key: string, one: HopyoOne): number[] => { - const d = loadDraft(draftKey(scope, key, one.호표.번)); - return one.호표.줄.flatMap((l) => - partsOf(l, d).flatMap((p) => (p.호표 !== undefined ? [p.호표] : [])), - ); -}; - -/** [저장] 할 호표 — 함께 만드는 하위 · 체크 줄(통째) 있음 · 하위를 부름(호표 줄 = 하위 사본) */ -export function readyFor(scope: HopyoScope, key: string, one: HopyoOne, picked: number[]): boolean { - const [lines, whole] = bodyPair(scope, key, one); - return ( - !picked.includes(one.호표.번) || !!lines.length || !!whole || !!kidNos(scope, key, one).length - ); -} - -/** 확인 안 된 하위 호표 보기 — 하위 먼저 · `ones` 와 이미 만든 하위(그 아래도) 뺌 */ -export async function unmadeKids( - scope: HopyoScope, - key: string, - target: HopyoScope, - ones: HopyoOne[], -): Promise { - const out: HopyoOne[] = []; - const seen = new Set(ones.map((one) => one.호표.번)); - async function visit(one: HopyoOne): Promise { - for (const no of kidNos(scope, key, one)) { - if (seen.has(no)) continue; - seen.add(no); - const kid = await readHopyoOne(scope, key, no, target); - if (kid.호표.만든) continue; - await visit(kid); - out.push(kid); - } - } - for (const one of ones) await visit(one); - return out; -} - -/** 하위 실패 — 까닭 「하위 제N호표 실패 — …」 · 남은 줄은 그 하위 몫(`leftAt`) */ -function kidFail(error: unknown, no: number): StaleError { - const left = (error as { detail?: { 남은줄?: unknown } } | null)?.detail?.남은줄; - const why = error instanceof Error ? error.message : String(error); - const text = `${L("M02_HopyoKidFail").replace("{n}", String(no))} — ${why}`; - return new StaleError(text, { 까닭: text, 하위: no, 하위남은줄: left ?? [] }); -} - -/** 확인 창 [생성] — 확인 안 된 하위를 그 캐시대로 먼저 만들고 부모 */ -export async function buildWithKids( - target: HopyoScope, - from: From, - 줄: Body, - 통째?: Whole, -): Promise { - const scope = { layer: from.층, owner: from.주인 }; - const me = await readHopyoOne(scope, from.키, from.번, target); - for (const kid of await unmadeKids(scope, from.키, target, [me])) { - const no = kid.호표.번; - const body = [bodyFor(scope, from.키, kid), wholeFor(scope, from.키, kid)] as const; - await buildHopyo(target, { ...from, 번: no }, ...body).catch((error: unknown) => { - throw kidFail(error, no); - }); - } - return buildHopyo(target, from, 줄, 통째); -} - -/** 하위 실패면 (그 하위 남은 줄 몸, 하위 번) · 아니면 그대로 — 결과 표 [고르기]가 그 하위를 엶 */ -export function leftAt(error: unknown, no: number): [unknown, number] { - const d = (error as { detail?: { 하위?: number; 하위남은줄?: unknown } } | null)?.detail; - return d?.하위 !== undefined ? [{ detail: { 남은줄: d.하위남은줄 } }, d.하위] : [error, no]; -} - -/** 결과 글 꼬리 — 같은 내용 재사용 · 하위 일위대가(사본 담음 · 함께 만듦) */ -export function kidsText(got: HopyoBuilt): string { - const more = got as HopyoBuilt & { - 재사용?: boolean; - 하위?: { 번: number; 키: string | null; 새로?: boolean }[]; - }; - const kids = (more.하위 ?? []).map((k) => { - const head = L("M02_HopyoKid") - .replace("{n}", String(k.번)) - .replace("{key}", k.키 ?? ""); - return ` · ${head}${k.새로 ? ` · ${L("M02_HopyoKidNew")}` : ""}`; - }); - return (more.재사용 ? ` · ${L("M02_HopyoReused")}` : "") + kids.join(""); -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts index 8392fb268..7aad4013f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts @@ -27,7 +27,6 @@ import { fetchLogicInputs } from "./M02_MasterTemplete_Labor_Api"; import { dateTimeText } from "../common_util/common_util_time"; import { openLaborInput } from "./M02_MasterTemplete_Labor_Input"; import { openFindPicker } from "./M02_MasterTemplete_Find_Pick"; -import { unitField } from "./M02_MasterTemplete_Hopyo_CheckRows"; import { formatNumber } from "../common_util/common_util_money"; import "./M02_MasterTemplete_Hopyo.css"; @@ -391,3 +390,18 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement { void load(); return root; } + +/** 단위 환산 칸 — 「엑셀 1 {from} =」 입력 「{to}」(옛 확인 창 `CheckRows.unitField` 옮김 · 785-7) */ +function unitField(input: HTMLInputElement, v: { from: string; to: string }): HTMLElement { + input.title = L("M02_HopyoUnitTip"); + const word = (text: string): HTMLElement => + el("span", { className: "m02-hopyo__unit-text", text }); + return el("span", { + className: "m02-hopyo__unit", + children: [ + word(L("M02_HopyoUnitLead").replace("{from}", v.from || "?")), + input, + ...(v.to ? [word(v.to)] : []), + ], + }); +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Left.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Left.ts deleted file mode 100644 index 93ae6bc21..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Left.ts +++ /dev/null @@ -1,327 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Hopyo_Left.ts - * 일위대가 불러오기 안 이은 줄(PLAN 729-3 · 요구서 내역19 1장 8 · 2장 4) — 만들면 남을 엑셀 줄 표 · 줄마다 고름. - * · 줄 표 = 서버 `/hopyo-unlinked`(저장 없음 · 몸 = 확인 창 캐시로 지을 만들기 몸) · 409 `{까닭, 남은줄}` 도 그 표 - * · 고름 = 확인 창 캐시(`Hopyo_Check` draft) 대상 · 뺌 — [저장] · [생성] 몸이 그대로 씀(새 저장 꼴 없음) - * [고르기] = 확인 창 고르기 창(후보 · 기초 · 자재 찾기 · 옵션은 서버 `guess`) · [찾기] = 기초 · 노임(LB) · 자재(MT) · - * [기계] = M01 기계(EQ) · % 줄 = 밑수 비목 · [우리에 없음] = 엑셀 숫자 그대로(사용자가 고른 것) - * · 단가산출 줄(참조 `단산`) = [단가산출 걸음에서 확인 ▸](`onSk` — 마법사 단가산출 칸 그 블록 펼침) — 기초 고르기 · SK 만들기 없음(778-5 D-3 · 줄로만) - * · 못옮김 남은 SK 를 쓰는 줄(729-4 `단산` · `SK` · `못옮김`) = [못옮김 잇기 (n)] → `Hopyo_Lost` 창 - * · 남은 수 = 안 만든 호표만(만든 호표는 서버가 남은 줄 있으면 안 만듦) — 마법사 [끝내기] 막음 · 띠 - * 계산 · 판정은 서버 — 화면은 보이기 · 고름만. - * ========================================================================== */ - -import { createBadge } from "@ui/ui_template_badge"; -import { createBanner } from "@ui/ui_template_banner"; -import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements"; -import { t as L } from "@ui/ui_template_locale"; -import { openModal } from "@ui/ui_template_modal"; -import { createTable, type Cell } from "@ui/ui_template_table"; -import { createText } from "@ui/ui_template_text"; -import { - guessHopyo, - leftHopyo, - leftOf, - readHopyoOne, - type BuildLine, - type HopyoLeft, - type HopyoLine, - type HopyoOne, - type HopyoPart, - type HopyoScope, - type LinkBase, - isElem, -} from "./M02_MasterTemplete_Hopyo_Api"; -import { bodyOf, draftKey, loadDraft, saveDraft, toTarget } from "./M02_MasterTemplete_Hopyo_Check"; -import { elemChoices } from "./M02_MasterTemplete_Hopyo_Compare"; -import { kindsOf, openHopyoChoose } from "./M02_MasterTemplete_Hopyo_Choose"; -import { openFindPicker } from "./M02_MasterTemplete_Find_Pick"; -import { openLost } from "./M02_MasterTemplete_Hopyo_Lost"; - -/** 파일 층 · 만들 층 · 파일 키 */ -export interface LeftScope { - scope: HopyoScope; - target: HopyoScope; - key: string; -} -/** 호표마다 남은 줄(그 호표 읽은 것과 함께) */ -export type LeftMap = Map; - -const NONE = "우리에 없음"; -const BASES: LinkBase[] = ["노무비", "재료비", "경비"]; -const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); - -/** 남은 줄 수 · 호표 수 */ -export const leftCount = (map: LeftMap): { 줄: number; 호표: number } => { - const nos = [...map.values()].filter((x) => x.left.length); - return { 줄: nos.reduce((n, x) => n + x.left.length, 0), 호표: nos.length }; -}; - -/** 캐시로 지을 만들기 몸 — 확인 창 · [저장]과 같은 규칙(통째 호표는 줄 없음) */ -export function draftLines(at: LeftScope, one: HopyoOne): BuildLine[] { - const h = one.호표; - const d = loadDraft(draftKey(at.scope, at.key, h.번)); - return h.통째 && (d.통째 ?? h.통째.체크) ? [] : bodyOf(h.줄, d); -} - -/** 호표 하나 다시 셈 — 서버 미리 보기(만든 호표 = 남은 줄 없음) */ -async function recount(at: LeftScope, no: number, map: LeftMap): Promise { - const one = await readHopyoOne(at.scope, at.key, no, at.target); - const from = { 층: at.scope.layer, 주인: at.scope.owner, 키: at.key, 번: no }; - const made = !!one.호표.만든?.확인; - const left = made ? [] : (await leftHopyo(at.target, from, draftLines(at, one))).남은줄; - map.set(no, { one, left }); -} - -/** 호표 여럿 남은 줄(차례로 · `step` = 몇째) — 실패한 호표는 까닭 글로 알림 */ -export async function countLeft( - at: LeftScope, - nos: number[], - step?: (done: number, all: number) => void, -): Promise { - const map: LeftMap = new Map(); - for (const [i, no] of nos.entries()) { - step?.(i, nos.length); - try { - await recount(at, no, map); - } catch (error) { - showToast(`제${no}호표 — ${why(error)}`, "warning"); - } - } - return map; -} - -/** 캐시에 한 줄 고름 — 대상 또는 뺌 「우리에 없음」(parts = null · 체크 켬) */ -export function put(at: LeftScope, no: number, 차례: number, parts: HopyoPart[] | null): void { - const k = draftKey(at.scope, at.key, no); - const d = loadDraft(k); - d.체크[차례] = true; - if (parts) { - d.대상[차례] = parts; - delete d.뺌[차례]; - } else { - d.뺌[차례] = NONE; - delete d.대상[차례]; - } - saveDraft(k, d); -} - -/** 고른 것 글 — 캐시 대상 · 뺌 */ -function pickedText(at: LeftScope, no: number, 차례: number): string { - const d = loadDraft(draftKey(at.scope, at.key, no)); - if (d.뺌[차례]) return d.뺌[차례]; - const parts = d.대상[차례] ?? []; - return parts - .map((p) => p.이름 ?? p.로직 ?? p.자재 ?? p.노임 ?? p.기계 ?? p.비율 ?? "") - .filter(Boolean) - .join(" + "); -} - -/** 남은 줄 창 — `map` 이 없으면 `nos` 를 서버에 물어 채움 · 닫을 때 `onClose(map)` */ -export function openLeft( - at: LeftScope & { 파일: string; nos?: number[]; map?: LeftMap; onSk?: (block: number) => void }, - onClose?: (map: LeftMap) => void, -): void { - const map: LeftMap = at.map ?? new Map(); - openModal({ - title: `${L("M02_LeftTitle")} — ${at.파일}`, - closeLabel: L("M02_Close"), - width: "1100px", - wide: true, - dialogClass: "m02-modal", - beforeClose: () => { - onClose?.(map); - return true; - }, - mount: (body, close) => { - const head = el("div"); - const list = el("div"); - body.append(createText(L("M02_LeftHint"), "hint", 14), head, list); - - const again = async (no: number): Promise => { - try { - await recount(at, no, map); - } catch (error) { - showToast(why(error), "error"); - } - draw(); - }; - const choose = (no: number, line: HopyoLine): void => - openHopyoChoose({ - line, - done: (ts) => { - const parts = ts.map((t) => ({ ...t, 입력값: t.입력값 ?? {}, 몫: t.몫 ?? null })); - guessHopyo(at.scope, at.key, no, line.차례, parts.map(toTarget)) - .then((g) => (Array.isArray(g.대상) ? g.대상 : parts)) - .catch(() => parts) - .then((got) => { - put(at, no, line.차례, got); - void again(no); - }); - }, - }); - const link = (no: number, 차례: number, part: Partial, name: string): void => { - put(at, no, 차례, [{ ...part, 이름: name, 입력값: {}, 배: 1 }]); - void again(no); - }; - const find = (no: number, line: HopyoLine): void => - openFindPicker({ - layer: at.target.layer, - owner: at.target.owner, - title: `${line.이름} ${line.규격}`.trim(), - kinds: kindsOf(line), // 736-2 줄 갈래대로 탭(노임 = 인력 · 기계 = 기계 · 자재 = 재료) - logic: (x) => link(no, line.차례, { 로직: x.키 }, x.이름), - element: (e) => - link(no, line.차례, e.종류 === "인력" ? { 노임: e.코드 } : { 자재: e.코드 }, e.이름), - machine: (e) => link(no, line.차례, { 기계: e.코드 }, e.이름), - }); - const actions = (no: number, row: HopyoLeft, line: HopyoLine | undefined): HTMLElement => { - const box = el("div", { className: "ui-toolbar" }); - const block = (line as { 참조?: { 종류?: string; 번?: number } } | undefined)?.참조; - if (at.onSk && block?.종류 === "단산" && block.번 !== undefined) { - const go = createButton({ - label: L("M02_LeftSkGo"), - size: 24, - onClick: () => { - close(); - onClose?.(map); - at.onSk!(block.번!); - }, - }); - go.dataset.leftSk = String(block.번); - box.append(go); - } - if (row.못옮김?.length && row.단산 !== undefined && row.SK) { - const lost = createButton({ - label: `${L("M02_LostTitle")} (${row.못옮김.length})`, - size: 24, - onClick: () => - openLost({ scope: at.scope, key: at.key, 번: row.단산!, sk: row.SK!, 이름: row.이름, - lost: row.못옮김!, done: () => void again(no) }), // prettier-ignore - }); - lost.dataset.leftLost = String(row.단산); - box.append(lost); - } - const elems = elemChoices(row.후보.filter(isElem), (c) => line && link(no, row.차례, { [c.갈래]: c.키 }, c.이름)); // prettier-ignore - if (elems) box.append(elems); - if (row.갈래 === "%" && line) { - const base = createSelectField({ - options: [{ value: "", text: L("M02_LeftBase") }].concat( - BASES.map((b) => ({ value: b, text: b })), - ), - compact: true, - size: 24, - ariaLabel: L("M02_LeftBase"), - onChange: (v) => v && link(no, row.차례, { 비율: v as LinkBase }, v), - }); - box.append(base.root); - } - if (line && row.갈래 !== "%") - box.append( - createButton({ label: L("M02_LeftChoose"), size: 24, variant: "ghost", onClick: () => choose(no, line) }), - createButton({ label: L("M02_LeftFind"), size: 24, variant: "ghost", onClick: () => find(no, line) }), - ); // prettier-ignore - const none = createButton({ - label: NONE, - size: 24, - variant: "ghost", - onClick: () => { - put(at, no, row.차례, null); - void again(no); - }, - }); - none.title = L("M02_LeftNoneTip"); - box.append(none); - return box; - }; - - const draw = (): void => { - const n = leftCount(map); - head.replaceChildren( - n.줄 - ? createBanner({ - tone: "warning", - text: L("M02_LeftBand").replace("{n}", String(n.줄)).replace("{h}", String(n.호표)), - }) - : createBanner({ tone: "success", text: L("M02_LeftZero") }), - ); - head.dataset.left = String(n.줄); - const rows: Cell[][] = []; - for (const [no, { one, left }] of [...map].sort((a, b) => a[0] - b[0])) - for (const row of left) { - const line = one.호표.줄.find((l) => l.차례 === row.차례); - rows.push([ - `제${no}호표`, - row.이름, - row.규격, - `${row.수량 ?? ""} ${row.단위}`.trim(), - createBadge(row.갈래, "warning"), - row.까닭, - pickedText(at, no, row.차례), - actions(no, row, line), - ]); - } - list.replaceChildren( - createTable( - [ - { label: L("M02_LeftHopyo"), width: "88px" }, - { label: L("M02_SangeunColName") }, - { label: L("M02_LeftSpec") }, - { label: L("M02_SangeunColQty"), width: "80px" }, - { label: L("M02_LeftKind"), width: "80px" }, - { label: L("M02_LeftWhy") }, - { label: L("M02_LeftPicked") }, - { label: L("M02_LeftPick") }, - ], - rows, - { density: "data", empty: L("M02_LeftZero") }, - ), - ); - }; - - if (at.map) draw(); - else { - list.replaceChildren(createText(L("M02_LeftCounting"), "muted", 14)); - void countLeft(at, at.nos ?? [], (i, all) => { - list.replaceChildren( - createText(`${L("M02_LeftCounting")} ${i + 1} / ${all}`, "muted", 14), - ); - }).then((got) => { - for (const [k, v] of got) map.set(k, v); - draw(); - }); - } - }, - }); -} - -/** 만들기 실패 까닭이 남은 줄이면 그 표를 엶(참) · 아니면 거짓(부르는 쪽이 까닭 글) */ -export function openLeftOf( - error: unknown, - at: LeftScope & { 파일: string; 번: number }, - onClose?: (map: LeftMap) => void, -): boolean { - const left = leftOf(error); - if (!left) return false; - void readHopyoOne(at.scope, at.key, at.번, at.target).then( - (one) => openLeft({ ...at, map: new Map([[at.번, { one, left }]]) }, onClose), - (e) => showToast(why(e), "error"), - ); - return true; -} - -/** 결과 표 칸 단추 — 남은 줄로 막힌 호표면 [고르기 (n)] · 아니면 null */ -export function leftButton( - error: unknown, - at: LeftScope & { 파일: string; 번: number }, -): HTMLButtonElement | null { - const left = leftOf(error); - if (!left) return null; - const go = createButton({ - label: `${L("M02_LeftChoose")} (${left.length})`, - size: 24, - onClick: () => void openLeftOf(error, at), - }); - go.dataset.leftOpen = String(at.번); - return go; -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Lost.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Lost.ts deleted file mode 100644 index 0b52cfeef..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Lost.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Hopyo_Lost.ts - * 단가산출 줄 찾기 단추 · 옛 못옮김 창(PLAN 729-4 · 778-5 D-5). - * · `lineFind` = 줄 묶음대로 찾아 넣기 창(`Find_Pick` 탭 하나 · 노임 = 인력 · 기계 = 기계 · 자재 = 재료) → - * 고름 `{노임|기계|자재: 키}` — 마법사 단가산출 줄 표(`Hopyo_SkLines`)가 씀 - * · `openLost` = 옛 SK(기초 + 엑셀 차이)의 남은 못옮김 보기만 — 잇기는 마법사 단가산출 칸에서 그 블록 줄 고름 → - * [확인] → [확인한 것 옮기기](서버 778-5 D-3 · 옛 `바탕` 몸 걷음) - * ========================================================================== */ - -import { createButton, showToast } from "@ui/ui_template_elements"; -import { t as L } from "@ui/ui_template_locale"; -import { openModal } from "@ui/ui_template_modal"; -import { createTable } from "@ui/ui_template_table"; -import { createText } from "@ui/ui_template_text"; -import { openFindPicker } from "./M02_MasterTemplete_Find_Pick"; -import type { HopyoScope, SangeunBuilt, SangeunLost } from "./M02_MasterTemplete_Hopyo_Api"; -import type { SkPick } from "./logic/M02_MasterTemplete_Hopyo_SkPick"; - -const KIND = { 노임: "labor", 기계: "machine", 자재: "material" } as const; - -/** 줄 찾기 단추 — 묶음 갈래 탭 하나 · 고르면 `done({갈래: 키})` */ -export function lineFind(opt: { - scope: HopyoScope; - 갈래: "노임" | "기계" | "자재"; - 이름: string; - 규격?: string; - done: (p: SkPick) => void; -}): HTMLButtonElement { - const go = createButton({ - label: opt.갈래 === "기계" ? L("M02_LeftMachine") : L("M02_LeftFind"), - size: 24, - variant: "ghost", - onClick: () => - openFindPicker({ - layer: opt.scope.layer, - owner: opt.scope.owner, - title: `${opt.이름} ${opt.규격 ?? ""}`.trim(), - kinds: [KIND[opt.갈래]], - query: opt.이름.split(/[\s(]/)[0] ?? "", - logic: () => showToast(L("M02_GridLinkNoLogic"), "info"), - element: (e) => opt.done({ [opt.갈래]: e.코드 } as SkPick), - machine: (e) => opt.done({ 기계: e.코드 }), - }), - }); - go.dataset.skFind = opt.갈래; - return go; -} - -export interface LostOptions { - scope: HopyoScope; - key: string; - /** 블록 번(단산N) */ - 번: number; - /** 그 블록 SK 키 */ - sk: string; - 이름?: string; - lost: SangeunLost[]; - /** 옛 꼴 그대로(부르는 쪽) — 보기만이라 안 부름 */ - done?: (got: SangeunBuilt) => void; -} - -/** 옛 못옮김 창 — 보기만 · 잇기 = 마법사 단가산출 칸 */ -export function openLost(opt: LostOptions): void { - openModal({ - title: `${L("M02_LostTitle")} — 단산${opt.번} ${opt.이름 ?? ""}`.trim(), - closeLabel: L("M02_Close"), - width: "960px", - dialogClass: "m02-modal", - mount: (body) => { - const rows = opt.lost.map((x) => [ - x.갈래, - x.이름, - x.규격 ?? "", - `${x.수량 ?? ""} ${x.단위 ?? ""}`.trim(), - x.글 ?? "", - ]); - body.append( - createText(L("M02_LostHint").replace("{n}", String(opt.번)), "hint", 14), - createTable( - [ - { label: L("M02_LeftKind"), width: "56px" }, - { label: L("M02_SangeunColName") }, - { label: L("M02_LeftSpec") }, - { label: L("M02_SangeunColQty"), width: "90px" }, - { label: L("M02_LeftWhy") }, - ], - rows, - { density: "data", empty: L("M02_LostZero") }, - ), - ); - }, - }); -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Pick.ts deleted file mode 100644 index 8d82f7339..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Pick.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Hopyo_Pick.ts - * [로직 추가] 창의 「실무 호표」 줄(PLAN 324-4 · 338-3) — 일위대가 문서 층 · 위 층 실무자료의 호표마다 한 줄(제안 n/m) · - * 고름 = 문서 층에 확인된 사전조합이 있으면 그것을 바로 참조 · 없으면 확인 창 모달(`Hopyo_Check` · 늦게 불러옴) → - * [확인하고 사전조합 만들기] → `lib.pick` 참조 줄(별칭 = 호표 품명) · 확인 없이 만들지 않음 · - * 파일마다 「엑셀로 보기 · 여러 호표」 줄(329 엑셀 보기 창 · 고른 호표마다 참조 줄). 프로젝트 층엔 없음. - * ========================================================================== */ - -import { showToast } from "@ui/ui_template_elements"; -import { t as L } from "@ui/ui_template_locale"; -import { listHopyoPicks, type HopyoPickRow } from "./M02_MasterTemplete_Hopyo_Api"; -import type { LibraryPick, PickExtra } from "./M02_MasterTemplete_Labor_Pick"; -import { tc } from "./M02_MasterTemplete_Labor_Text"; - -export const HOPYO_KIND = "hopyo"; -const LAYER_TEXT: Record = { system: "마스터", company: "회사", personal: "개인" }; -const ALIAS_MAX = 40; // 별칭 상한(44-6 · 서버 `ALIAS_MAX`) - -/** 서고 고르기에 「실무 호표」 덧줄 · 종류 고르기(단가산출 · 사전조합 · 실무 호표)를 붙임 */ -export function withHopyo(lib: LibraryPick): LibraryPick { - if (lib.layer === "project") return lib; - const scope = { layer: lib.layer, owner: lib.owner }; - return { - ...lib, - sangeun: true, // 717-4 — 일위대가 [로직 추가] 「단가산출」 탭 - kindFilter: lib.kindFilter ?? [ - { value: "logic", text: tc("Kind_Logic") }, - { value: "precombo", text: tc("Kind_Combo") }, - { value: HOPYO_KIND, text: "실무 호표" }, - ], - more: async () => { - const rows = await listHopyoPicks(scope); - const files = new Map(); - for (const row of rows) { - const key = `${row.층}|${row.주인 ?? ""}|${row.키}`; - files.set(key, [...(files.get(key) ?? []), row]); - } - return [ - ...rows.map((row) => hopyoExtra(row, lib)), - ...[...files].map(([key, list]) => sheetExtra(key, list[0], list.length, lib)), - ]; - }, - }; -} - -/** 파일 하나 — 엑셀 보기 창에서 여러 호표를 골라 한 번에(만드는 층 = 문서 층) */ -export function sheetExtra(key: string, row: HopyoPickRow, n: number, lib: LibraryPick): PickExtra { - return { - kind: HOPYO_KIND, - key: `${key}|엑셀`, - tag: "실무 호표", - name: "엑셀로 보기 · 여러 호표", - sub: `${row.파일} · ${LAYER_TEXT[row.층] ?? row.층}`, - unit: `호표 ${n}`, - onPick: () => - void import("./M02_MasterTemplete_Hopyo_Sheet").then((m) => - m.openHopyoSheet({ - scope: { layer: row.층, owner: row.주인 }, - key: row.키, - 파일: row.파일, - canWrite: true, - target: { layer: lib.layer, owner: lib.owner }, - onImport: (got, h) => - lib.pick(got.참조, got.이름, h.이름.slice(0, ALIAS_MAX).trim() || null), - }), - ), - }; -} - -/** 실무 호표 한 줄 */ -export function hopyoExtra(row: HopyoPickRow, lib: LibraryPick): PickExtra { - return { - kind: HOPYO_KIND, - key: `${row.층}|${row.주인 ?? ""}|${row.키}|${row.번}`, - tag: "실무 호표", - name: [row.이름, row.규격].filter(Boolean).join(" "), - sub: `${row.파일} · 제${row.번}호표 · ${LAYER_TEXT[row.층] ?? row.층}`, - unit: `${row.단위} · ${row.확인 ? `확인 ${row.확인.키}` : `제안 ${row.맞춤[0]}/${row.맞춤[1]}`}`, - onPick: () => void pickHopyo(row, lib), - }; -} - -/** 고름 — 확인된 사전조합이면 바로 참조 · 아니면 확인 창 모달 → 만든 뒤 참조 */ -export async function pickHopyo(row: HopyoPickRow, lib: LibraryPick): Promise { - const alias = row.이름.slice(0, ALIAS_MAX).trim() || null; - if (row.확인) { - showToast(`「${row.이름}」 → 확인한 ${L("M02_PrecomboTitle")} ${row.확인.키}`, "success"); - return lib.pick(row.확인, row.이름, alias); - } - try { - const check = await import("./M02_MasterTemplete_Hopyo_Check"); - check.openHopyoCheck({ - title: `확인 — ${row.파일} 제${row.번}호표 ${row.이름}`, - scope: { layer: row.층, owner: row.주인 }, - key: row.키, - no: row.번, - target: { layer: lib.layer, owner: lib.owner }, - canWrite: true, - onBuilt: (got) => lib.pick(got.참조, got.이름, alias), - }); - } catch (error) { - showToast(error instanceof Error ? error.message : String(error), "error"); - } -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts index 843613845..43ea0eef2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts @@ -1,36 +1,33 @@ /* ============================================================================= * M02_MasterTemplete_Hopyo_Sheet.ts * 일위대가 실무자료 엑셀 보기 몸(PLAN 329 · 338-3 · 359-1 · 359-2 · 785-4) — 본 칸 불러오기 보기(`Hopyo_Import`)가 바로 담음 · - * 머리 막대 = 시트 · 호표 수 · 검산 · 제안 n / 줄 m · 숫자 판정 셈 중 + 부르는 쪽 덧붙임(`headExtra` — 층 · 안 이은 줄 · [끝내기]) · + * 머리 막대 = 시트 · 호표 수 · 검산 · 제안 n / 줄 m(1차 판정) · 블록 풀이 셈 중 + 부르는 쪽 덧붙임(`headExtra` — 층 · [끝내기]) · * 왼쪽 = 올린 엑셀을 연 것처럼 읽기 전용 시트(A00 산출근거 스프레드시트 · 탭 셋 일위대가표 · 단가산출표 · 단가산출 상세 · - * 칠 · 범례 · 뛰기 · 뒤로 = `Hopyo_SheetNav` · 779-4 · 같은 파일 몸은 다시 씀 = 그 자리로 뜀 · 779-6) · - * 오른쪽 = 호표 목록(체크 · 제N호표 [수정] · 찾기 · 제안 n/m — 다 연결이면 녹색 · 뒤 셈 중이면 3 초마다 다시) + [저장] · + * 칠 · 범례 · 뛰기 · 뒤로 = `Hopyo_SheetNav` · 779-4 · 같은 파일 격자 다시 씀 · 779-6) · + * 오른쪽 = 호표 목록(제N호표 [수정] · 찾기 · 제안 n/m — 다 제안이면 녹색 · 뒤 셈 중이면 3 초마다 다시) · * 단가산출표 · 상세 탭이면 단가산출 목록으로 바뀜(780-2 · `Hopyo_SheetSk`). * · 목록 누름 → 시트에서 그 호표 영역(B열 ~ 끝 열) 고름 · 칸 누름 → 그 행 호표를 목록에서 강조 · 머리 행이면 영역 고름 * · [수정] = 시트 아래 줄 판(785-6 `Hopyo_Rows` · 줄 누름과 따로) · 단가산출 탭 블록 = 같은 자리 블록 판 · * 오른쪽 판 아래 [옮기기](확인한 줄 · 블록 · 고름 캐시 `createJudgeState` 한 벌) - * · [저장] · [모두 고정으로 들여오기] = `Hopyo_SheetMake` - * · 격자 = 서버 `/hopyo-grid/{키}` · 옛 파일(격자 없음)은 시트 자리에 까닭 글 · 목록 · 저장은 됨 - * · `buildHopyoSheet` = 몸(머리 · 띠 · 시트 + 목록) · `openHopyoSheet` = 모달 껍데기([로직 추가] 「실무 호표」 길 — 785-7 이 지움) + * · 격자 = 서버 `/hopyo-grid/{키}` · 옛 파일(격자 없음)은 시트 자리에 까닭 글 · 목록은 됨 + * · `buildHopyoSheet` = 몸(머리 · 띠 · 시트 + 목록) · 옛 [저장] · [모두 고정으로 들여오기] · 모달 껍데기는 785-7 에서 지움 + * (만들기 = [옮기기] · 모두 고정 서버 길은 B09 · 붙여넣기가 씀) * ⚠ 부르는 쪽은 `import()` 로 엶 · 스프레드시트도 열 때 불러옴(다른 화면 번들에 안 섞이게). * ========================================================================== */ import { createButton, el } from "@ui/ui_template_elements"; -import { createCheckbox } from "@ui/ui_template_checkbox"; import { createInputField } from "@ui/ui_template_input"; -import { openModal } from "@ui/ui_template_modal"; import type { Sheet } from "../A00_Common/spreadsheet/spreadsheet_types"; import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet"; import { + POLL_MS, readHopyo, type HopyoBrief, - type HopyoBuilt, type HopyoGrid, type HopyoSpan, type HopyoView, type HopyoScope, } from "./M02_MasterTemplete_Hopyo_Api"; -import { POLL_MS } from "./M02_MasterTemplete_Hopyo_Check"; import { createJudgeMove, createJudgeRows, @@ -39,17 +36,18 @@ import { import { lineOf } from "./logic/M02_MasterTemplete_Hopyo_Picks"; import * as Nav from "./M02_MasterTemplete_Hopyo_SheetNav"; import { createSheetSk, type SheetSk } from "./M02_MasterTemplete_Hopyo_SheetSk"; -import { note, why, wireMake } from "./M02_MasterTemplete_Hopyo_SheetMake"; import "./M02_MasterTemplete_Hopyo.css"; -export { buildOrder } from "./M02_MasterTemplete_Hopyo_SheetMake"; +const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); +const note = (text: string): HTMLElement => + el("span", { className: "ui-text ui-text--muted", text, attrs: { "data-text": "14" } }); export interface HopyoSheetOptions { /** 실무자료 파일 층 */ scope: HopyoScope; key: string; 파일: string; - /** 만들 층 쓰기 권한 — 없으면 체크 · 만들기 없음(보기만) */ + /** 만들 층 쓰기 권한 — 없으면 옮기기 없음(보기만) */ canWrite: boolean; /** 사전조합을 만들 층(없으면 `scope` · [로직 추가] = 문서 층) */ target?: HopyoScope; @@ -57,8 +55,6 @@ export interface HopyoSheetOptions { 호표?: number; /** 처음 보일 단가산출 행(330-3 근거 [엑셀 보기]) */ 단산행?: number | [number, number]; - /** 호표 하나를 만들었음 — [로직 추가] 참조 줄 */ - onImport?: (got: HopyoBuilt, hopyo: HopyoBrief) => void; /** 창을 닫았음(좌측 파일 줄 고름 표시) */ onClose?: () => void; /** 머리 막대 끝에 덧붙일 것(본 칸 불러오기 보기 — 층 · 안 이은 줄 · [끝내기]) */ @@ -74,8 +70,6 @@ export interface HopyoSheetBody { parts: HTMLElement[]; /** 몸을 걷음(마지막 창 손잡이 · 격자 · 스프레드시트) */ destroy: () => void; - /** 모달에서 앞으로 꺼낸 몸을 제자리로 — 했으면 참(닫기 대신) */ - lower: () => boolean; /** 안 옮긴 단가산출 블록 수(판정 전 null) */ skCount: () => { 블록: number; 밖: number; 줄: number } | null; /** 그 단가산출 블록으로 뜀 + 줄 판 엶 */ @@ -84,8 +78,6 @@ export interface HopyoSheetBody { const ILWI = "일위대가"; const DANSAN = ["단가산출", "단가산출근거"]; // 형식별 블록 시트(EST · STmate · 역할 id 없는 옛 격자) -/** 열린 창 손잡이(779-6) — 파일마다 · 다시 부르면 그 창이 앞으로 와 그 자리로 뜀 */ -const opened = new Map void>(); const sheetId = (o: HopyoSheetOptions): string => `${o.scope.layer}:${o.scope.owner ?? ""}:${o.key}`; @@ -125,32 +117,12 @@ export function lastColumn(sheet: Pick): string { export const rowsText = (행: number | [number, number]): string => typeof 행 === "number" ? `B${행}:F${행}` : `B${행[0]}:F${행[1]}`; -/** 모달 껍데기 — 같은 파일 몸이 이미 있으면(본 칸 · 모달) 그 몸을 그 자리로 뜀 */ -export function openHopyoSheet(opt: HopyoSheetOptions): void { - const id = sheetId(opt); - if (opened.has(id)) return opened.get(id)!(opt); - const view = buildHopyoSheet(opt); - openModal({ - title: `엑셀 보기 — ${opt.파일}`, - closeLabel: "닫기", - wide: true, - dialogClass: "m02-modal m02-hopyo-sheet__dialog", - beforeClose: async () => { - if (view.lower()) return false; // 꺼낸 창 = 닫기 대신 제자리로 - view.destroy(); - return true; - }, - mount: (body) => body.append(...view.parts), - }); -} - export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { const id = sheetId(opt); let sheet: SpreadsheetHandle | null = null; let nav: Nav.SheetNav | null = null; let sk: SheetSk | null = null; // 단가산출 탭 오른쪽 판(780-2) let detail = ""; // 단가산출 상세 시트 id - let above: Element[] = []; // 앞으로 꺼냈을 때 원래 위에 있던 창 let closed = false; let briefs: HopyoBrief[] = []; let spans: HopyoSpan[] = []; @@ -158,7 +130,6 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { let last = "O"; let at = { 시트: "", 칸: "", 범위: "" }; // 시트 고름 let current: number | null = opt.호표 ?? null; - const checked = new Set(); const items = new Map(); const target = opt.target ?? opt.scope; const state = createJudgeState(opt.scope, opt.key); // 판정 고름 캐시(785-6) @@ -203,13 +174,6 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { const search = searchField.input; const list = el("div", { className: "m02-hopyo-sheet__list" }); const count = note(""); - const go = createButton({ label: "저장", variant: "filled", size: "sm" }); - go.disabled = true; - go.setAttribute("data-hopyo-save", ""); - const fixAll = createButton({ label: "모두 고정으로 들여오기", size: "sm" }); - fixAll.setAttribute("data-hopyo-all-fixed", ""); - fixAll.disabled = true; // 엑셀을 다 읽은 뒤 켬 - const result = el("div", { className: "m02-hopyo-sheet__result" }); const move = createJudgeMove({ file: opt.scope, key: opt.key, @@ -224,10 +188,9 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { searchField.root, el("div", { className: "ui-toolbar m02-hopyo__bar", - children: [count, ...(opt.canWrite ? [fixAll, go] : [])], + children: [count], }), list, - result, move.root, ], }); @@ -235,7 +198,6 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { const finish = (): void => { if (closed) return; closed = true; - opened.delete(id); closeLines(); offPaint(); move.destroy(); @@ -245,25 +207,16 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { opt.onClose?.(); }; - const paintGo = (): void => { - go.querySelector(".ui-btn__label")!.textContent = checked.size - ? `저장 (${checked.size})` - : "저장"; - go.disabled = !checked.size; - }; - /** 머리 막대 — 시트 · 호표 수 · 검산 · 제안 · 셈 중 + 덧붙임 */ function paintHead(file: HopyoView): void { const wrong = file.검산.어긋남; - const c = file.셈 && !file.셈.끝 ? file.셈 : null; + const busy = !!file.셈 && !file.셈.끝; head.replaceChildren( note(`시트 ${file.시트}`), note(`호표 ${file.호표수}`), note(wrong.length ? `검산 어긋남 ${wrong.length}` : "검산 맞음"), note(`제안 ${file.맞춤[0]} / 줄 ${file.맞춤[1]}`), - ...(c - ? [note(`숫자 판정 셈 중 ${c.한}/${c.모두 ?? "?"}${c.까닭 ? ` · ${c.까닭}` : ""}`)] - : []), + ...(busy ? [note(`${file.셈?.까닭 ?? "블록 풀이"} 셈 중`)] : []), ...(opt.headExtra ?? []), ); opt.onRead?.(file); @@ -288,11 +241,6 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { const text = spanText(span, last); if (at.시트 !== ilwi || at.범위 !== text) sheet.select(text, ilwi); } - opened.set(id, (o) => { - above = Nav.raise(left); // 모달이 아니면 [] — 그 자리로만 뜀 - if (o.단산행 !== undefined && detail) nav?.jumpTo(detail, o.단산행); - else if (o.호표 !== undefined) focus(o.호표); - }); /** [수정] — 그 호표 자리 + 시트 아래 줄 판(줄 누름과 따로) */ const editButton = (h: HopyoBrief): HTMLElement => { @@ -330,23 +278,12 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { ); list.replaceChildren( ...shown.map((h) => { - const box = createCheckbox("", { - checked: checked.has(h.번), - onChange: (on) => { - if (on) checked.add(h.번); - else checked.delete(h.번); - paintGo(); - }, - }); - box.root.hidden = !opt.canWrite; - box.root.addEventListener("click", (e) => e.stopPropagation()); const linked = note(linkText(h)); linked.classList.toggle("is-linked", allLinked(h)); const row = el("div", { className: `m02-hopyo-sheet__item${h.번 === current ? " is-current" : ""}`, attrs: { "data-hopyo-no": String(h.번), title: `${h.이름} ${h.규격}`.trim() }, children: [ - box.root, el("span", { className: "m02-hopyo-sheet__no", text: `제${h.번}호표` }), editButton(h), el("span", { className: "m02-hopyo-sheet__name", text: `${h.이름} ${h.규격}`.trim() }), @@ -438,7 +375,6 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { ]); if (closed) return; briefs = file.호표; - fixAll.disabled = !briefs.length; spans = grid instanceof Error ? [] : grid.호표; paintHead(file); paintList(); @@ -456,25 +392,10 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { } } - wireMake({ - scope: opt.scope, - key: opt.key, - 파일: opt.파일, - target, - ...(opt.onImport ? { onImport: opt.onImport } : {}), - checked, - briefs: () => briefs, - closed: () => closed, - go, - fixAll, - result, - paintGo, - }); void load(); return { parts: [head, warn, el("div", { className: "m02-hopyo-sheet", children: [col, right] })], destroy: finish, - lower: () => Nav.lower(left, above.splice(0)), skCount: () => sk?.count() ?? null, showSk: (번) => sk?.show(번), }; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetMake.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetMake.ts deleted file mode 100644 index 0723e54b1..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetMake.ts +++ /dev/null @@ -1,256 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Hopyo_SheetMake.ts - * 엑셀 보기 몸의 만들기 셋(PLAN 785-4 · `Hopyo_Sheet` 에서 옮김 — 줄 바꿈 없음) — - * · [저장] = 체크한 호표 + 확인 안 된 하위 호표(777-1 · `Hopyo_Kids`)마다 체크 줄(확인 창 캐시 · 없으면 서버 제안)로 - * 하위 호표부터 차례로 `/hopyo-build` — 물음 한 번 · 체크 줄 없는 고른 호표 건너뜀 · 하위 실패 = 그 하위 [고르기] · - * 결과 표(사전조합 키 · 빠진 줄 · 값 차이) · `onImport` 가 있으면 호표마다 알림([로직 추가] 참조 줄) - * · [모두 고정으로 들여오기](705-3) = 물음 한 번 → 서버 `/hopyo-build-all`(호표 모두 · 하위 먼저 · 엑셀 숫자 그대로 · - * 이미 만든 호표 그대로) → 알림 「호표 n · 줄 m · 품셈 줄 a · 고정 줄 b · 빠짐 0 · 엑셀 합계 = 우리 합계 n/n」 + 다음 걸음 - * ========================================================================== */ - -import { - createButton, - el, - hideLoadingOverlay, - showConfirmDialog, - showLoadingOverlay, - showToast, -} from "@ui/ui_template_elements"; -import { t as L } from "@ui/ui_template_locale"; -import { - buildHopyo, - buildHopyoAll, - readHopyoOne, - type HopyoBrief, - type HopyoBuilt, - type HopyoBuiltAll, - type HopyoOne, - type HopyoScope, -} from "./M02_MasterTemplete_Hopyo_Api"; -import { - draftKey, - hasDraft, - HOPYO_BUILT, - loadDraft, - partsOf, -} from "./M02_MasterTemplete_Hopyo_Check"; -import { bodyPair, kidsText, leftAt, readyFor, unmadeKids } from "./M02_MasterTemplete_Hopyo_Kids"; -import { builtWord } from "./M02_MasterTemplete_Hopyo_Choose"; -import { leftButton } from "./M02_MasterTemplete_Hopyo_Left"; -import { openRelink, relinkKeys } from "./M02_MasterTemplete_Hopyo_Relink"; -import { formatPrice } from "../common_util/common_util_money"; - -export const why = (error: unknown): string => - error instanceof Error ? error.message : String(error); -export const won = (v: number | null | undefined): string => - v === null || v === undefined ? "" : formatPrice(v); -export const note = (text: string): HTMLElement => - el("span", { className: "ui-text ui-text--muted", text, attrs: { "data-text": "14" } }); - -/** 만들 차례 — 하위 호표(호표 참조 줄이 가리키는 고른 호표)부터 · 순환은 남은 차례대로 */ -export function buildOrder(ones: { 번: number; 아래: number[] }[]): number[] { - const left = new Map(ones.map((x) => [x.번, x.아래.filter((n) => n !== x.번)])); - const out: number[] = []; - while (left.size) { - const ready = [...left].find(([, kids]) => kids.every((n) => !left.has(n))); - const no = ready ? ready[0] : [...left.keys()][0]; - out.push(no); - left.delete(no); - } - return out; -} - -export interface MakeContext { - scope: HopyoScope; - key: string; - 파일: string; - /** 만들 층 */ - target: HopyoScope; - onImport?: (got: HopyoBuilt, hopyo: HopyoBrief) => void; - checked: Set; - briefs: () => HopyoBrief[]; - closed: () => boolean; - go: HTMLButtonElement; - fixAll: HTMLButtonElement; - result: HTMLElement; - paintGo: () => void; -} - -/** [저장] · [모두 고정으로 들여오기] 누름을 이음 */ -export function wireMake(c: MakeContext): void { - const { scope, key, target, go, fixAll, result } = c; - const table = (heads: string[], rows: HTMLElement[]): HTMLElement => - el("table", { - className: "ui-general-block__table m02-hopyo__table", - children: [ - el("thead", { - children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })], - }), - el("tbody", { children: rows }), - ], - }); - - const body = (one: HopyoOne) => bodyPair(scope, key, one); - const headCell = (no: number, name: string): HTMLElement => - el("td", { text: `제${no}호표 ${name}`.trim() }); - - /** [저장] — 물음 한 번 → 고른 호표마다 서버 보기 → 체크 줄 있는 것만 만들기(체크 줄 없으면 건너뜀) */ - async function save(): Promise { - const nos = [...c.checked].sort((a, b) => a - b); - const fresh = nos.filter((no) => !hasDraft(draftKey(scope, key, no))).length; - const ask = - `고른 호표 ${nos.length}개를 체크한 줄로 ${L("M02_PrecomboTitle")} 생성` + - (fresh ? ` — 확인 창에서 안 고친 ${fresh}개는 서버 제안 체크 줄` : ""); - if (!(await showConfirmDialog(ask, "저장"))) return; - go.disabled = true; - const ones: HopyoOne[] = []; - const skipped: HTMLElement[] = []; - showLoadingOverlay(); - try { - for (const no of nos) { - if (c.closed()) return; - try { - ones.push(await readHopyoOne(scope, key, no, target)); - } catch (error) { - skipped.push(el("tr", { children: [headCell(no, ""), el("td", { text: why(error) })] })); - } - } - ones.push(...(await unmadeKids(scope, key, target, ones))); // 777-1 하위 함께 - } finally { - hideLoadingOverlay(); - c.paintGo(); - } - const ready = ones.filter((one) => readyFor(scope, key, one, nos)); // 하위 함께 - for (const one of ones) - if (!ready.includes(one)) - skipped.push( - el("tr", { - children: [ - headCell(one.호표.번, one.호표.이름), - el("td", { text: "건너뜀 — 체크 줄 없음" }), - ], - }), - ); - await makeAll(ready, skipped); - } - - /** 하위 호표부터 차례로 만들기 — 하위 확인 전(409)은 안내 */ - async function makeAll(ones: HopyoOne[], skipped: HTMLElement[]): Promise { - const by = new Map(ones.map((one) => [one.호표.번, one])); - const order = buildOrder( - ones.map((one) => { - const d = loadDraft(draftKey(scope, key, one.호표.번)); - const kids = one.호표.줄.flatMap((l) => - partsOf(l, d).flatMap((p) => (p.호표 !== undefined ? [p.호표] : [])), - ); - return { 번: one.호표.번, 아래: kids }; - }), - ); - const rows: HTMLElement[] = []; - let ok = 0; - showLoadingOverlay(); - try { - for (const no of order) { - const one = by.get(no)!; - const h = c.briefs().find((x) => x.번 === no) ?? one.호표; - const cell = headCell(no, h.이름); - try { - const from = { 층: scope.layer, 주인: scope.owner, 키: key, 번: no }; - const got = await buildHopyo(target, from, ...body(one)); - ok += 1; - c.onImport?.(got, h); - rows.push( - el("tr", { - attrs: { "data-built": got.참조.키 }, - children: [ - cell, - el("td", { text: `${got.참조.키} · ${builtWord(got)}${kidsText(got)}` }), - el("td", { text: String(got.빠진줄.length), className: "m02-hopyo__num" }), - el("td", { text: won(got.비교.차이), className: "m02-hopyo__num" }), - ], - }), - ); - } catch (error) { - const [err, kid] = leftAt(error, no); // 하위 실패 = 그 하위 남은 줄 - const left = leftButton(err, { scope, target, key, 파일: c.파일, 번: kid }); // prettier-ignore - const text = `실패 — ${why(error)}`; - rows.push(el("tr", { children: [cell, el("td", { text }), ...(left ? [el("td", { children: [left] })] : [])] })); // prettier-ignore - } - } - } finally { - hideLoadingOverlay(); - } - if (ok) document.dispatchEvent(new Event(HOPYO_BUILT)); - result.replaceChildren( - table( - ["호표", `${L("M02_PrecomboTitle")}`, "빠진 줄", "값 차이(참고)"], - [...rows, ...skipped], - ), - ); - const all = order.length + skipped.length; - showToast(`만듦 ${ok} / ${all}`, ok === all ? "success" : "warning"); - } - go.addEventListener("click", () => void save()); - - /** 들여오기 알림 글(705-3) */ - const allText = (got: HopyoBuiltAll): string => - `호표 ${got.호표} · 줄 ${got.줄} · 품셈 줄 ${got.품셈} · 고정 줄 ${got.고정} · ` + - `하위 호표 참조 ${got.호표참조} · 빠짐 ${got.빠짐} · 엑셀 합계 = 우리 합계 ${got.같음}/${got.호표}` + - (got.그대로 ? ` · 이미 만든 호표 ${got.그대로} 그대로` : ""); - - /** [모두 고정으로 들여오기](705-3) — 물음 한 번 · 서버가 호표 모두를 하위부터(숫자 = 서버가 파일에서) */ - async function importAll(): Promise { - const ask = - `호표 ${c.briefs().length}개를 엑셀 숫자 그대로(고정 줄) ${L("M02_PrecomboTitle")}으로 들여옴 — ` + - "이미 만든 호표는 그대로 · 품셈 맞춤은 뒤에 호표 [수정]에서 [품셈으로 바꾸기]"; - if (!(await showConfirmDialog(ask, "들여오기"))) return; - fixAll.disabled = true; - showLoadingOverlay(); - try { - const from = { 층: scope.layer, 주인: scope.owner, 키: key }; - const got = await buildHopyoAll(target, from); - if (got.호표 > got.그대로) document.dispatchEvent(new Event(HOPYO_BUILT)); - const next = - "다음 — 호표 [수정]에서 [품셈으로 바꾸기]" + - (got.이음후보 - ? ` · 이음 후보 ${got.이음후보} 줄(이름은 같고 단가가 다름)은 엑셀 단가 그대로 — 아래 [다음 — 이 공사 단가로 다시 잇기]에서 바꿈` - : ""); - const rows = [ - ...got.다름.map((x) => - el("tr", { - children: [ - el("td", { text: `제${x.번}호표 ${x.이름}` }), - el("td", { - text: `엑셀 ${won(x.엑셀)} · 우리 ${won(x.우리)}`, - className: "m02-hopyo__num", - }), - el("td", { text: won(x.차이), className: "m02-hopyo__num" }), - ], - }), - ), - ...got.실패.map((x) => { - const left = leftButton({ detail: x }, { scope, target, key, 파일: c.파일, 번: x.번 }); // prettier-ignore - return el("tr", { children: [el("td", { text: `제${x.번}호표` }), el("td", { text: x.까닭 }), ...(left ? [el("td", { children: [left] })] : [])] }); // prettier-ignore - }), - ]; - const relink = createButton({ - label: "다음 — 이 공사 단가로 다시 잇기", - size: "sm", - onClick: () => void openRelink(relinkKeys((got.만듦 ?? []).map((x) => x.키))), - }); // 705-2 - result.replaceChildren( - note(allText(got)), - note(next), - relink, - ...(rows.length ? [table(["호표", "합계 다름 · 실패", "차이"], rows)] : []), - ); - showToast(allText(got), got.빠짐 || got.실패.length ? "warning" : "success"); - } catch (error) { - showToast(`실패 — ${why(error)}`, "error"); - } finally { - hideLoadingOverlay(); - fixAll.disabled = false; - } - } - fixAll.addEventListener("click", () => void importAll()); -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetNav.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetNav.ts index bdb5d1d2d..77b14dbc5 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetNav.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetNav.ts @@ -5,9 +5,9 @@ * 원본 시트 이름 · 그 행 칠 까닭 글(블록 「예상」 가름). * · 탭 = 서버 역할 id(779-1) 시트를 같은 글(일위대가표 · 단가산출표 · 단가산출 상세)로 · 원본 이름은 막대 * · 칠 = `logic/M02_MasterTemplete_Hopyo_SheetMap` 이 셈 · 격자 옵션 `칠`(779-3) 로 보이는 칸만 · A 열 ~ 끝 열 · 띠는 A 열만 - * · 다시 칠 = 목록 다시 읽음(`marks`) · 확인 창 캐시 바뀜(`HOPYO_DRAFT`) · 만들기(`HOPYO_BUILT` → 목록 · 블록 다시 읽음) + * · 다시 칠 = 목록 다시 읽음(`marks`) · 판정 화면 확인 바뀜(`repaint`) · 만들기(`HOPYO_BUILT` → 목록 · 블록 다시 읽음) * · 블록을 여러 줄이 부르면 [▸ 부르는 줄] 이 차례로 돌림 · 뛸 때마다 그 앞 자리를 쌓고 [◂ 뒤로] 가 꺼냄 · 두 번 누름 = 첫 단추 - * · 779-6 — 마지막 격자 다시 씀(`gridOf`) · 열린 창 앞으로 꺼냄 · 닫기 = 제자리로(`raise` · `lower`) + * · 779-6 — 마지막 격자 다시 씀(`gridOf`) * · 778-5 D-6 — 머리 막대 아래 띠(부품 `createBanner`) 「엑셀 셈 어긋남 n」 + [보기 i/m] = 상세 시트 그 행으로 차례로 뛰기 · * 그 행 까닭은 막대 글에 덧붙임 · 칠은 안 바꿈 * · 780-2 — `role(시트)` · `blockAt(시트, 행)`(단가산출표 목록 행 · 상세 블록 범위) · `spotOf(번, 역할)` 내보냄(오른쪽 단가산출 목록) @@ -21,9 +21,9 @@ import { t as L } from "@ui/ui_template_locale"; import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet"; import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet"; import { call } from "./M02_MasterTemplete_Api_Fetch"; -import { draftKey, hasDraft, HOPYO_BUILT, loadDraft } from "./M02_MasterTemplete_Hopyo_Check"; import { at, + HOPYO_BUILT, readHopyo, readHopyoGrid, type HopyoGrid, @@ -45,8 +45,6 @@ import { type Spot, } from "./logic/M02_MasterTemplete_Hopyo_SheetMap"; -/** 확인 창 캐시를 고쳤음(document 알림 · `Hopyo_Check.saveDraft` 가 같은 글로 냄) — 엑셀 보기 창 다시 칠 */ -export const HOPYO_DRAFT = "m02-hopyo-draft"; const TAB = { 일위대가표: "M02_SheetIlwi", 단가산출표: "M02_SheetList", 단가산출상세: "M02_SheetDetail" } as const; // prettier-ignore const enc = encodeURIComponent; @@ -69,23 +67,6 @@ export function gridOf(id: string, scope: HopyoScope, key: string): Promise void (kept = null); -/** 창(모달 바탕)을 맨 앞으로 — 원래 위에 있던 것들(제자리 표시) · 이미 맨 앞이면 [] */ -export function raise(node: Element): Element[] { - const back = node.closest(".ui-modal__backdrop"); - let above = back?.nextElementSibling ?? null; - while (above && !above.matches(".ui-modal__backdrop")) above = above.nextElementSibling; - if (!back || !above) return []; - document.body.append(back); - return [above]; -} -/** 꺼낸 창을 제자리로 — 했으면 참(닫기 대신) */ -export function lower(node: Element, above: Element[]): boolean { - const back = node.closest(".ui-modal__backdrop"); - if (!back || !above[0]?.isConnected) return false; - document.body.insertBefore(back, above[0]); - return true; -} - /** 역할 시트 탭 글 통일 — 역할 → 시트 id */ export function nameTabs(book: Workbook, grid: HopyoGrid): Partial> { const ids = (grid as HopyoGrid & GridMore).보기 ?? {}; @@ -126,10 +107,6 @@ export function createSheetNav(opt: SheetNavOptions) { const lastCol = new Map(); const colOf = (sheet: string): number => lastCol.get(sheet) ?? lastCol.set(sheet, toCol(opt.last(sheet))).get(sheet)!; - const draftOf = (no: number) => { - const k = draftKey(opt.scope, opt.key, no); - return hasDraft(k) ? loadDraft(k) : null; - }; const go = (range: string, sheet: string): void => void opt.sheet()?.select(range, sheet); const buttons = el("div", { className: "ui-toolbar m02-hopyo__bar" }); @@ -231,7 +208,7 @@ export function createSheetNav(opt: SheetNavOptions) { } function repaint(): void { - paint = paintOf(ix, briefs, blocks, draftOf, opt.judged); + paint = paintOf(ix, briefs, blocks, () => null, opt.judged); // 옛 확인 창 캐시 없음(785-7) opt.sheet()?.repaint(); paintBar(); } @@ -241,12 +218,10 @@ export function createSheetNav(opt: SheetNavOptions) { paintBand(); repaint(); } - const onDraft = (): void => repaint(); const onBuilt = (): void => { void readHopyo(opt.scope, opt.key).then((v) => (briefs = v.호표 as BriefMarks[]), () => {}).then(readBlocks); // prettier-ignore }; const onTwice = (): void => root.querySelector("[data-hopyo-jump]")?.click(); - document.addEventListener(HOPYO_DRAFT, onDraft); document.addEventListener(HOPYO_BUILT, onBuilt); opt.host.addEventListener("dblclick", onTwice); paintBar(); @@ -288,7 +263,6 @@ export function createSheetNav(opt: SheetNavOptions) { return sheet && rows ? { 시트: sheet, 행: rows } : null; }, destroy(): void { - document.removeEventListener(HOPYO_DRAFT, onDraft); document.removeEventListener(HOPYO_BUILT, onBuilt); opt.host.removeEventListener("dblclick", onTwice); }, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetSk.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetSk.ts index 5e05f2c3b..fe49c5f1c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetSk.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetSk.ts @@ -14,11 +14,10 @@ import { el } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { createListRow } from "@ui/ui_template_list_row"; import { createText } from "@ui/ui_template_text"; -import type { HopyoScope } from "./M02_MasterTemplete_Hopyo_Api"; +import { POLL_MS, type HopyoScope } from "./M02_MasterTemplete_Hopyo_Api"; import { createJudgeBlock, type JudgeState } from "./M02_MasterTemplete_Hopyo_Block"; import { createJudgeMove } from "./M02_MasterTemplete_Hopyo_Rows"; import { readJudge, type JudgeBlockBrief } from "./M02_MasterTemplete_Hopyo_JudgeApi"; -import { POLL_MS } from "./M02_MasterTemplete_Hopyo_Check"; import type { SheetNav } from "./M02_MasterTemplete_Hopyo_SheetNav"; export interface SheetSkOptions { diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SkApi.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SkApi.ts deleted file mode 100644 index a27bea442..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SkApi.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Hopyo_SkApi.ts - * 마법사 단가산출 길(PLAN 778-5 D-5 · 서버 D-3 `M02_Hopyo_Sangeun.build`) — `POST …/hopyo/{키}/sangeun` - * · 몸 `{확인: [{번, 고름?: {차례: 고름}}]}` — 빈 확인 = 판정만(안 만듦) · 확인한 블록만 단위 SK - * · 답 = 블록마다 줄 판정 · 만듦 · 못만듦(꼴 = `logic/M02_MasterTemplete_Hopyo_SkPick`) - * ========================================================================== */ - -import { call } from "./M02_MasterTemplete_Api_Fetch"; -import { at, type HopyoScope } from "./M02_MasterTemplete_Hopyo_Api"; -import type { SkJudged, SkSure } from "./logic/M02_MasterTemplete_Hopyo_SkPick"; - -/** 줄 판정 · 확인한 블록 옮기기 — 맞춤 셈 전 409 · 틀린 고름 400 · 없는 블록 404 */ -export const judgeSangeun = ( - scope: HopyoScope, - key: string, - 확인: SkSure[] = [], -): Promise => - call(at(scope, `/hopyo/${encodeURIComponent(key)}/sangeun`), { - method: "POST", - body: JSON.stringify({ 확인 }), - }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SkLines.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SkLines.ts deleted file mode 100644 index 88cc8a3bb..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SkLines.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Hopyo_SkLines.ts - * 마법사 단가산출 블록 펼침 = 줄 표(PLAN 778-5 D-5 · 요구 623 · 624) — 서버 줄 판정(D-3) + 캐시 고름 겹침. - * · 칸 = 묶음 · 이름 · 규격 · 매개값 · 상태 칩(이어짐 · 고를 것 · 못 이음 · 밖 · 뺌 — 엑셀 보기 칠과 한 규칙 - * `SheetMap.unitTone`) + 꼬리표(「기본 — 건설업 작업반장」 · 「내가 고름」) · 방법 글 · 우리 금액 · 엑셀 금액 · 차(참고) - * · 고름(밖 줄 빼고) = 서버 후보 단추 · [찾기](`Hopyo_Lost.lineFind` = 찾아 넣기 창) · 율 = 기준 고르기 · - * 하위 · 값 = 뺌만 · [뺌] + 까닭 · [고름 지움] — 고름은 부르는 쪽 캐시(`onPick`) - * · [보기] = 엑셀 보기 단가산출 상세 그 행(779-4 뛰기) - * 금액 · 판정은 서버 — 고른 줄 금액은 [확인한 것 옮기기] 뒤 서버가 다시 셈(그 전 「—」). - * ========================================================================== */ - -import { createBadge, type BadgeTone } from "@ui/ui_template_badge"; -import { createButton, createInputField, createSelectField, el } from "@ui/ui_template_elements"; -import { t as L } from "@ui/ui_template_locale"; -import { createTable, type Cell } from "@ui/ui_template_table"; -import { formatNumber, formatPrice } from "../common_util/common_util_money"; -import type { HopyoScope } from "./M02_MasterTemplete_Hopyo_Api"; -import { lineFind } from "./M02_MasterTemplete_Hopyo_Lost"; -import { unitTone, type Tone } from "./logic/M02_MasterTemplete_Hopyo_SheetMap"; -import { - BASES, - GROUP, - candPick, - candText, - dropOnly, - lineView, - pickable, - type SkBlock, - type SkLine, - type SkPick, -} from "./logic/M02_MasterTemplete_Hopyo_SkPick"; - -const BADGE: Record = { - success: "success", - warning: "warning", - info: "info", - danger: "danger", - muted: "neutral", -}; -const FIND = { L: "노임", X: "기계", M: "자재" } as const; -const won = (v: number | null | undefined): string => - v === null || v === undefined ? "—" : formatPrice(v); - -export interface SkLinesOptions { - scope: HopyoScope; - block: SkBlock; - picks: Record; - /** 고름 넣기 · 지움(null) */ - onPick: (차례: number, p: SkPick | null) => void; - /** 엑셀 보기 그 행 */ - onSheet?: (행: number) => void; -} - -/** 상태 칩 + 꼬리표 */ -function stateCell(line: SkLine, pick: SkPick | undefined): HTMLElement { - const v = lineView(line, pick); - const diff = !v.mine && line.차 !== null && line.차 !== undefined && Math.abs(line.차) >= 1; - const tone = v.상태 === "뺌" ? "muted" : unitTone(v.상태, diff); - const chip = createBadge(v.상태, BADGE[tone]); - chip.dataset.skState = v.상태; - const tags = v.꼬리표.map((t) => { - const tag = createBadge(t, t === "내가 고름" ? "accent" : "neutral"); - tag.dataset.skTag = t; - return tag; - }); - return el("div", { className: "ui-toolbar", children: [chip, ...tags] }); -} - -/** 고름 칸 — 후보 · 찾기 · 율 기준 · 뺌 + 까닭 · 지움 */ -function pickCell(opt: SkLinesOptions, line: SkLine, pick: SkPick | undefined): HTMLElement { - const box = el("div", { className: "ui-toolbar", attrs: { "data-sk-pick": String(line.차례) } }); - if (!pickable(line)) return box; - const put = (p: SkPick | null): void => opt.onPick(line.차례, p); - if (!dropOnly(line)) { - for (const c of line.후보.slice(0, 3)) { - const p = candPick(line.묶음, c); - if (!p) continue; - const b = createButton({ label: candText(c), size: 24, variant: "ghost", onClick: () => put(p) }); // prettier-ignore - b.dataset.skCand = String(line.차례); - b.title = c.키 ?? c.자재 ?? c.글 ?? ""; - box.append(b); - } - const kind = FIND[line.묶음 as keyof typeof FIND]; - if (kind) box.append(lineFind({ scope: opt.scope, 갈래: kind, 이름: line.이름 ?? "", 규격: line.규격, done: put })); // prettier-ignore - if (line.묶음 === "R") { - const base = createSelectField({ - options: [{ value: "", text: L("M02_SkBasePick") }].concat( - BASES.map((b) => ({ value: b, text: b })), - ), - compact: true, - size: 24, - ariaLabel: L("M02_SkBasePick"), - onChange: (v) => v && put({ 기준: v }), - }); - box.append(base.root); - } - } - const why = createInputField({ compact: true, size: 24, placeholder: L("M02_SkDropWhy") }); - why.input.dataset.skWhy = String(line.차례); - const drop = createButton({ - label: L("M02_SkDrop"), - size: 24, - variant: "ghost", - onClick: () => put({ 뺌: why.input.value.trim() || L("M02_SkDropDefault") }), - }); - drop.dataset.skDrop = String(line.차례); - box.append(why.root, drop); - if (pick) { - const clear = createButton({ label: L("M02_SkClear"), size: 24, variant: "link", onClick: () => put(null) }); // prettier-ignore - clear.dataset.skClear = String(line.차례); - box.append(clear); - } - return box; -} - -/** 블록 줄 표 */ -export function skLineTable(opt: SkLinesOptions): HTMLElement { - const rows: Cell[][] = opt.block.줄.map((line) => { - const pick = opt.picks[String(line.차례)]; - const v = lineView(line, pick); - const param = Object.entries(line.매개값) - .map(([k, n]) => `${k} ${formatNumber(n)}`) - .join(" · "); - const how = [line.방법, v.글].filter(Boolean).join(" · "); - const sheet = - line.행 !== null && opt.onSheet - ? createButton({ label: L("M02_SkSheet"), size: 24, variant: "link", onClick: () => opt.onSheet!(line.행!) }) // prettier-ignore - : ""; - if (sheet) sheet.dataset.skSheet = String(line.행); - return [ - GROUP[line.묶음] ?? line.묶음, - line.이름 ?? "", - line.규격, - param, - stateCell(line, pick), - how, - v.mine ? "—" : won(line.우리금액), - won(line.엑셀금액), - v.mine || line.차 === null || line.차 === undefined ? "—" : won(line.차), - pickCell(opt, line, pick), - sheet, - ]; - }); - const table = createTable( - [ - { label: L("M02_SkColGroup"), width: "48px" }, - { label: L("M02_SangeunColName") }, - { label: L("M02_LeftSpec") }, - { label: L("M02_SkColParam") }, - { label: L("M02_SkColState") }, - { label: L("M02_SkColHow") }, - { label: L("M02_SkColOurs"), align: "end" }, - { label: L("M02_SkColExcel"), align: "end" }, - { label: L("M02_SkColDiff"), align: "end" }, - { label: L("M02_SkColPick") }, - { label: L("M02_SkSheet"), width: "52px" }, - ], - rows, - { density: "data", empty: L("M02_SkNoLines") }, - ); - table.dataset.skLines = String(opt.block.번); - return table; -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ToItem.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ToItem.ts deleted file mode 100644 index f398fb636..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ToItem.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Hopyo_ToItem.ts - * [구조물 조합으로](PLAN 704-4 뒷부분) — 실무자료 확인 창 몸 그대로 구조물 조합 항목(SU)을 바로 만듦(사전조합 안 거침). - * 창 = 항목 이름(기본 = 호표 이름 · 규격) · 규격 묶음 제안(같은 이름 · 줄 구성 · 규격만 다른 호표 → 한 항목의 규격 열 · - * 기본 켬 · 끄면 이 호표 하나) · [만들기] → 서버 `/hopyo-item`(이 층 쓰기 · 「실무자료」 폴더) → 나무 다시 - * 숫자는 서버가 파일에서 — 화면은 체크 · 고름만 - * ========================================================================== */ - -import { - createButton, - el, - hideLoadingOverlay, - showLoadingOverlay, - showToast, -} from "@ui/ui_template_elements"; -import { createCheckbox } from "@ui/ui_template_checkbox"; -import { createInputField } from "@ui/ui_template_input"; -import { openModal } from "@ui/ui_template_modal"; -import { createText } from "@ui/ui_template_text"; -import { t as L } from "@ui/ui_template_locale"; -import { - buildHopyoItem, - hopyoSiblings, - type BuildLine, - type BuildTarget, - type HopyoOne, - type HopyoScope, -} from "./M02_MasterTemplete_Hopyo_Api"; -import { type SameExtra, withSameAsk } from "./M02_MasterTemplete_Library_SameAsk"; - -/** 항목을 만들었음(document 알림) — 구조물 조합 나무 · 옆 목록 다시 */ -export const ITEM_BUILT = "m02-hopyo-item-built"; - -export interface ToItemOptions { - scope: HopyoScope; - target: HopyoScope; - key: string; - no: number; - hopyo: HopyoOne["호표"]; - lines: BuildLine[]; - whole: { 대상: BuildTarget[] } | null; -} - -export async function toItem(o: ToItemOptions): Promise { - let sib: Awaited>; - try { - sib = await hopyoSiblings(o.scope, o.key, o.no); - } catch (error) { - showToast( - `묶음 제안 실패 — ${error instanceof Error ? error.message : String(error)}`, - "error", - ); - return; - } - const h = o.hopyo; - const one = [h.이름, h.규격].filter(Boolean).join(" "); - const bundle = sib.묶음.length > 1; - const name = createInputField({ label: "구조물 조합 이름", value: bundle ? h.이름 : one }); - const pack = bundle - ? createCheckbox( - `규격 열로 묶음 — ${sib.이름}: ${sib.묶음.map((r) => `${r.열}(제${r.번}호표)`).join(" · ")}`, - { - checked: true, - onChange: (on) => (name.input.value = on ? h.이름 : one), - }, - ) - : null; - openModal({ - title: `구조물 조합으로 — 제${o.no}호표`, - closeLabel: "취소", - width: "40rem", - mount: (body, close, actions) => { - const go = createButton({ - label: "만들기", - onClick: async () => { - const label = name.input.value.trim(); - if (!label) return name.setError("이름이 빔"); - const from = { 층: o.scope.layer, 주인: o.scope.owner, 키: o.key, 번: o.no }; - const with_ = pack?.input.checked - ? sib.묶음.map((r) => r.번).filter((n) => n !== o.no) - : []; - // 같은 내용 항목이 있으면 물음(733-3) — 물을 때는 가림막을 걷음 - const build = async (extra: SameExtra) => { - showLoadingOverlay(); - try { - return await buildHopyoItem(o.target, from, o.lines, o.whole, with_, label, extra); - } finally { - hideLoadingOverlay(); - } - }; - try { - const got = await withSameAsk(build); - if (!got) return; - close(); - document.dispatchEvent(new Event(ITEM_BUILT)); - showToast( - got.씀 - ? `같은 내용 구조물 조합 「${got.이름}」 씀 — 새로 안 만듦` - : `구조물 조합 「${label}」 만듦 — 「실무자료」 폴더 · 줄 ${got.줄수}` + - ((got.열수 ?? 0) > 1 ? ` · 규격 열 ${got.열수}` : ""), - "success", - 6000, - ); - } catch (error) { - showToast( - `못 만듦 — ${error instanceof Error ? error.message : String(error)}`, - "error", - ); - } - }, - }); - actions.append(go); - body.append( - createText( - `체크한 줄 = 품셈 줄 · 나머지 = 고정 줄(엑셀 숫자 그대로) — ${L("M02_PrecomboTitle")}을 거치지 않고 구조물 조합 항목을 바로 만듦`, - "muted", - ), - name.root, - ...(pack ? [el("div", { children: [pack.root] })] : []), - ); - }, - }); -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_WizardSk.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_WizardSk.ts deleted file mode 100644 index 23285dd56..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_WizardSk.ts +++ /dev/null @@ -1,352 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Hopyo_WizardSk.ts - * 단가산출 목록 판(PLAN 778-5 D-5 · 780-2 · 785-4 — 엑셀 보기 오른쪽 · 요구 621 · 622 · 623 · 624 · 627) — 모두 줄로만 · 자동 없음. - * · 열면 서버 줄 판정(`POST …/sangeun` 빈 확인 = 안 만듦 · D-3) → 블록 표: 단산N · 부름 칸 · 판정(「옮길 수 있음」 · - * 「고를 줄 n」 — 캐시 고름 겹침) · 만든 SK · [확인] 칩(고를 줄 0 일 때만 켜짐) · [줄 n] = 시트 아래 줄 판(`lines` 자리) 에 - * 줄 표(`Hopyo_SkLines` · 넓은 꼴) · 금액 · 근거는 줄 title - * · 맞춤 셈 중(409)이면 3 초마다 다시 판정(끝나면 저절로 표) - * · 고름 · 확인 = 캐시 `m02-hopyo-sk:{층}:{주인}:{키}`(`SkPick` 꼴) — 누를 때 서버에 안 감 · 자동 저장 없음 - * · [확인한 것 옮기기 (n)] = 몸 `{확인: [{번, 고름}]}` → 서버가 M01 지금 값으로 다시 셈 · 단위 SK(확인 안 한 블록 안 만듦) · - * 만든 블록은 확인에서 뺌 · 못만듦 = 띠(까닭 그대로) · 「모두 확인」 단추 없음 - * · `count()` = 아직 안 옮긴 블록 · 그중 일위대가 밖 · 고를 줄([끝내기] 글) - * · 부름 칸(「일위대가 n 곳」 · 「일위대가 밖」) · 거르기(전체 · 안 · 밖 · 고를 줄 있음) · 블록 줄 누름 = `onRow` · - * `mark(번)` 강조 · `readOnly` = 옮기기 숨김 · 알림 `SK_SYNC`(같은 캐시 열쇠 · 고름 바뀜 → 다시 그림) · 옮김 뒤 `HOPYO_BUILT` - * ========================================================================== */ - -import { createBadge } from "@ui/ui_template_badge"; -import { createBanner } from "@ui/ui_template_banner"; -import { createButton, el, showToast } from "@ui/ui_template_elements"; -import { t as L } from "@ui/ui_template_locale"; -import { tableRow, tableShell, type Column } from "@ui/ui_template_table"; -import { createText } from "@ui/ui_template_text"; -import { createToggle } from "@ui/ui_template_toggle"; -import { createSegment } from "@ui/ui_template_segment"; -import { createPanelHead } from "@ui/ui_template_panel_head"; -import { StaleError } from "./M02_MasterTemplete_Api_Fetch"; -import { HOPYO_BUILT, POLL_MS } from "./M02_MasterTemplete_Hopyo_Check"; -import { formatPrice } from "../common_util/common_util_money"; -import type { HopyoScope } from "./M02_MasterTemplete_Hopyo_Api"; -import { judgeSangeun } from "./M02_MasterTemplete_Hopyo_SkApi"; -import { skLineTable } from "./M02_MasterTemplete_Hopyo_SkLines"; -import { - GOOD, - canConfirm, - filterBlocks, - outside, - picksOf, - readDraft, - setPick, - setSure, - sureBody, - tidy, - verdictOf, - waiting, - type SkBlock, - type SkDraft, - type SkFilter, - type SkJudged, -} from "./logic/M02_MasterTemplete_Hopyo_SkPick"; - -const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); -const won = (v: number | null | undefined): string => - v === null || v === undefined ? "—" : formatPrice(v); - -export const skDraftKey = (scope: HopyoScope, key: string): string => - `m02-hopyo-sk:${scope.layer}:${scope.owner ?? ""}:${key}`; - -export interface SkStep { - root: HTMLElement; - /** 아직 안 옮긴 블록 · 그중 일위대가 밖 · 고를 줄 — 판정 전 null */ - count: () => { 블록: number; 밖: number; 줄: number } | null; - /** 그 블록 펼치고 화면에 보임(안 이은 줄 창 [단가산출 걸음에서 확인]) */ - show: (번: number) => void; - /** 그 블록 줄 강조 · 보이게(펼치지 않음 · 거른 줄이면 거르기 풂) — null = 강조 지움 */ - mark: (번: number | null) => void; -} - -export interface SkStepOptions { - /** 줄 판 자리(시트 아래) — [줄 n] 이 여기에 그 블록 줄 표를 그림 · 없으면 [줄 n] 없음 */ - lines?: HTMLElement; - /** 블록 줄 누름(단추 밖) */ - onRow?: (번: number) => void; - /** 옮기기 숨김(만들 층 ≠ 파일 층 · 쓰기 권한 없음) */ - readOnly?: boolean; -} - -/** 맞춤 알림 — detail `{dk, from, got?}` */ -export const SK_SYNC = "m02-hopyo-sk"; -type SyncDetail = { dk: string; from: object; got?: SkJudged }; - -/** ② 단가산출 칸 — `파일` = 엑셀 보기 창 머리 · `onChange` = 판정 · 옮김 뒤(끝내기 글) */ -export function sangeunStep( - scope: HopyoScope, - key: string, - 파일: string, - onChange?: () => void, - o: SkStepOptions = {}, -): SkStep { - const dk = skDraftKey(scope, key); - const me = {}; - const load = (): SkDraft => readDraft(sessionStorage.getItem(dk)); - const save = (d: SkDraft): void => sessionStorage.setItem(dk, JSON.stringify(d)); - const tell = (more: Partial = {}): void => - void document.dispatchEvent(new CustomEvent(SK_SYNC, { detail: { dk, from: me, ...more } })); // prettier-ignore - let got: SkJudged | null = null; - let fails: SkJudged["못만듦"] = []; - let current: number | null = null; // 강조 블록 - let filter: SkFilter = "all"; - let shown: number | null = null; // 줄 판에 보인 블록 - const out = el("div", { className: "m02-hopyo", attrs: { "data-wiz-sk": "" } }); - const seg = createSegment({ - options: [ - { value: "all", label: L("M02_SkFilterAll") }, - { value: "in", label: L("M02_SkFilterIn") }, - { value: "out", label: L("M02_SkFilterOut") }, - { value: "open", label: L("M02_SkFilterOpen") }, - ], - value: filter, - size: 24, - ariaLabel: L("M02_SkFilter"), - onChange: (v) => { - filter = v as SkFilter; - draw(); - }, - }); - seg.root.dataset.skFilter = ""; - const again = createButton({ label: L("M02_SkJudge"), variant: "ghost", size: 28, onClick: () => void judge() }); // prettier-ignore - again.dataset.skJudge = ""; - const move = createButton({ label: L("M02_SkMove").replace("{n}", "0"), variant: "filled", size: 28, disabled: true, onClick: () => void run() }); // prettier-ignore - move.title = L("M02_SkMoveTip"); - move.dataset.skMove = ""; - const root = el("div", { - className: "m02-hopyo", - children: [ - seg.root, - out, - el("div", { className: "ui-toolbar", children: o.readOnly ? [again] : [move, again] }), - ], - }); - /** 다른 벌이 고름 · 옮김을 알림 — 떨어진 벌은 듣기 그침 */ - const onSync = (e: Event): void => { - const x = (e as CustomEvent).detail; - if (!root.isConnected) return document.removeEventListener(SK_SYNC, onSync); - if (x.dk !== dk || x.from === me || !got) return; - if (x.got) { - got = x.got; - fails = []; - } - draw(); - }; - document.addEventListener(SK_SYNC, onSync); - - const sheet = (행: number): void => - void import("./M02_MasterTemplete_Hopyo_Sheet") - .then((m) => - m.openHopyoSheet({ scope, key, 파일, canWrite: true, target: scope, 단산행: 행 }), - ) - .catch((error: unknown) => showToast(why(error), "error")); - - async function judge(): Promise { - again.disabled = true; - out.replaceChildren(createText(L("M02_SkJudging"), "muted", 14)); - try { - got = await judgeSangeun(scope, key); - fails = []; - draw(); - } catch (error) { - out.replaceChildren(createBanner({ tone: "warning", text: why(error) })); - if (error instanceof StaleError) - window.setTimeout(() => root.isConnected && void judge(), POLL_MS); // 맞춤 셈 중 — 끝나면 저절로 - } finally { - again.disabled = false; - onChange?.(); - } - } - - async function run(): Promise { - if (!got) return; - const body = sureBody(load(), got.판정); - if (!body.length) return showToast(L("M02_SkSureFirst"), "warning"); - move.disabled = true; - try { - const next = await judgeSangeun(scope, key, body); - const made = new Set(next.만듦.map((x) => x.번)); - const d = load(); - save({ ...d, 확인: d.확인.filter((n) => !made.has(n)) }); - got = next; - fails = next.못만듦; - tell({ got: next }); - if (made.size) document.dispatchEvent(new Event(HOPYO_BUILT)); // 열린 엑셀 보기 칠 · 좌측 나무 다시 - showToast( - L("M02_SkMoved") - .replace("{made}", String(made.size)) - .replace("{bad}", String(fails.length)), - fails.length ? "warning" : "success", - ); - draw(); - } catch (error) { - showToast(why(error), "error"); - draw(); - } finally { - onChange?.(); - } - } - - const cols: Column[] = [ - { label: L("M02_SkColBlock") }, - { label: L("M02_SkColCall"), width: "80px" }, - { label: L("M02_SkColVerdict"), width: "84px" }, - { label: L("M02_SkColMade"), width: "72px" }, - { label: L("M02_SkSure"), width: "56px" }, - { label: "", width: "56px" }, - ]; - - /** 부름 칸 — 「일위대가 n 곳」(title 호표 번) · 「일위대가 밖」 · 옛 서버(칸 없음) = — */ - function callCell(b: SkBlock): HTMLElement | string { - if (!b.부름) return "—"; - const out = outside(b); - const word = out ? L("M02_SkCallOut") : L("M02_SkCallIn").replace("{n}", String(b.부름.length)); // prettier-ignore - const badge = createBadge(word, out ? "neutral" : "info"); - badge.title = b.부름.map((n) => `제${n}호표`).join(" · "); - badge.dataset.skCall = out ? "out" : "in"; - return badge; - } - - /** 블록 한 줄 + 펼친 줄 표 */ - function blockRows(b: SkBlock, d: SkDraft): HTMLTableRowElement[] { - const picks = picksOf(d, b.번); - const word = verdictOf(b, picks); - const verdict = createBadge(word, word === GOOD ? "success" : "info"); - verdict.dataset.skVerdict = String(b.번); - const basis = b.근거 ? createBadge(`${L("M02_SkBasisTag")} ${b.근거.이름 ?? b.근거.로직}`, "neutral") : "—"; // prettier-ignore - if (basis instanceof HTMLElement) - basis.title = [b.근거!.로직, b.근거!.판정].filter(Boolean).join(" · "); - const made = b.만든 ? createBadge(b.만든, "success") : "—"; - const ok = canConfirm(b, picks); - const sure = createToggle({ - label: L("M02_SkSure"), - kind: "chip", - size: 24, - checked: d.확인.includes(b.번), - disabled: !ok, - onChange: (on) => { - save(setSure(load(), b, on)); - draw(); - tell(); - }, - }); - sure.root.dataset.skSure = String(b.번); - sure.root.title = ok - ? L("M02_SkSureTip") - : L(b.줄.length ? "M02_SkSureOff" : "M02_SkSureEmpty"); - const lines = createButton({ - label: `${L("M02_SkOpen")} ${b.줄.length}`, - size: 24, - variant: shown === b.번 ? "filled" : "ghost", - onClick: () => showLines(shown === b.번 ? null : b.번), - }); - lines.dataset.skOpen = String(b.번); - lines.hidden = !o.lines; - const name = `단산${b.번} ${b.이름 ?? ""}${b.단위 ? ` [${b.단위}]` : ""}`; - const head = tableRow(cols, [name, callCell(b), verdict, made, sure.root, lines]); - head.dataset.skBlock = String(b.번); - if (b.번 === current) head.classList.add("is-current"); - head.title = `${name} · ${L("M02_SkColOurs")} ${won(b.우리금액)} · ${L("M02_SkColExcel")} ${won(b.엑셀금액)}`; // prettier-ignore - head.addEventListener("click", (e) => { - if (!(e.target as Element).closest("button, label, input")) o.onRow?.(b.번); - }); - return [head]; - } - - /** 줄 판 — 그 블록 줄 표(넓은 꼴) · 고름은 같은 캐시 · null = 닫음 */ - function showLines(번: number | null): void { - shown = 번; - draw(); // 줄 판 · 목록 단추 칠을 한 번에 - } - function drawLines(): void { - const host = o.lines; - if (!host) return; - const b = shown === null ? null : got?.판정.find((x) => x.번 === shown); - host.hidden = !b; - if (!b) return host.replaceChildren(); - const close = createButton({ label: L("M02_Close"), size: 24, variant: "ghost", onClick: () => showLines(null) }); // prettier-ignore - host.replaceChildren( - createPanelHead({ title: `단산${b.번} ${b.이름 ?? ""}`, level: 4, divider: true, actions: [close] }).root, // prettier-ignore - skLineTable({ - scope, - block: b, - picks: picksOf(load(), b.번), - onPick: (at, p) => { - save(tidy(setPick(load(), b.번, at, p), got!.판정)); - draw(); - tell(); - }, - onSheet: sheet, - }), - ); - } - - function draw(): void { - if (!got) return; - const d = tidy(load(), got.판정); - save(d); - const blocks = got.판정; - const good = blocks.filter((b) => canConfirm(b, picksOf(d, b.번))).length; - const made = blocks.filter((b) => b.만든).length; - const sum = L("M02_SkSummary") - .replace("{b}", String(blocks.length)) - .replace("{ok}", String(good)) - .replace("{wait}", String(blocks.length - good)) - .replace("{made}", String(made)) - .replace("{sure}", String(d.확인.length)); - const { root: table, tbody } = tableShell(cols, { density: "data" }); - const shown = filterBlocks(blocks, d, filter); - for (const b of shown) tbody.append(...blockRows(b, d)); - const band = fails.length - ? [ - createBanner({ - tone: "warning", - text: L("M02_SkFailed").replace("{n}", String(fails.length)), - details: fails.map((x) => `단산${x.번} ${x.이름 ?? ""} — ${x.까닭}`).join("\n"), - }), - ] - : []; - const far = blocks.filter(outside).length; - const where = blocks.some((b) => b.부름) ? ` · ${L("M02_SkSummaryIn").replace("{in}", String(blocks.length - far)).replace("{out}", String(far))}` : ""; // prettier-ignore - out.replaceChildren( - createText(sum + where, "muted", 14), - ...band, - shown.length ? table : createText(L("M02_SkNone"), "muted", 14), - ); - move.disabled = !d.확인.length; - move.querySelector(".ui-btn__label")!.textContent = L("M02_SkMove").replace( - "{n}", - String(d.확인.length), - ); - drawLines(); - onChange?.(); - } - - void judge(); - return { - root, - count: () => (got ? waiting(load(), got.판정) : null), - show: (번) => { - showLines(번); - out.querySelector(`[data-sk-block="${번}"]`)?.scrollIntoView({ block: "center" }); - }, - mark: (번) => { - out.querySelector("tr.is-current")?.classList.remove("is-current"); - current = 번; - if (번 === null || !got) return; - if (!out.querySelector(`[data-sk-block="${번}"]`) && filter !== "all") { - filter = "all"; - seg.set("all"); - draw(); - } - const row = out.querySelector(`[data-sk-block="${번}"]`); - row?.classList.add("is-current"); - row?.scrollIntoView({ block: "nearest" }); - }, - }; -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index bfb4b5a62..cc698a99f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -56,7 +56,7 @@ import { openQtyModal } from "./M02_MasterTemplete_Labor_QtyModal"; import { openRefEdit } from "./M02_MasterTemplete_Labor_RefEdit"; import { openRowSangeun } from "./M02_MasterTemplete_Labor_SangeunModal"; import { afterRowDrag } from "./M02_MasterTemplete_Labor_RowDrag"; -import { openLogicPicker, withHopyo } from "./M02_MasterTemplete_Labor_Pick"; +import { openLogicPicker, withSangeun } from "./M02_MasterTemplete_Labor_Pick"; import { logicMatRows } from "./M02_MasterTemplete_Labor_LogicMats"; import { setLogicMark, setMark } from "./M02_MasterTemplete_Labor_MatMarks"; import { logicTable, qtyShort } from "./M02_MasterTemplete_Labor_Rows"; @@ -542,7 +542,7 @@ export function mountLaborDetail( void loadLogics().then(() => changed()); }, // 사전조합도 같은 창 · 같은 목록(51-11 · 자기 자신 · 순환은 [저장] 때 서버 422) - withHopyo(library), + withSangeun(library), ), }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Paste.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Paste.ts index 9b1f8242e..1a1792952 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Paste.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Paste.ts @@ -13,7 +13,7 @@ import { openModal } from "@ui/ui_template_modal"; import { createDataTable } from "@ui/ui_template_data_table"; import { createText } from "@ui/ui_template_text"; import { hideLoadingOverlay, showLoadingOverlay } from "@ui/ui_template_toast"; -import { HOPYO_BUILT } from "./M02_MasterTemplete_Hopyo_Check"; +import { HOPYO_BUILT } from "./M02_MasterTemplete_Hopyo_Api"; import { pageScope, pasteHopyo, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts index b55ae5b88..3a4199dec 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts @@ -5,12 +5,11 @@ * 프로젝트 · 카드엔 서고 줄 없음 · 거름 = 로직 품셈 분류 + 사전조합 새 분류(149-2 · 구조물군 › 공통 + 종류) * 단가연결(251 · B08 거리이정표 · B09 설계내역서) = 종류 고르기(단가산출 · 사전조합 · 덧붙인 줄 종류) + * 덧붙일 줄(구조물 조합 · B09 호표) — `lib.extra` · `lib.kindFilter` · 늦게 받는 덧줄 `lib.more` - * (324-4 실무 호표 · `withHopyo`) * 704-1 단가산출 줄 = 기준 목록 표(품명 · 규격 · 단위 · 금액 · 출처 · 대표 규격 펼침 · 공종 거름 · * 실무 낱말 = 서버가 별칭에 덧붙임) · 금액 = 서버 M01 엔진(`/logic-lib/standard`) · 오기 전엔 로직 줄만 · * 줄 누름 = 계산서 보기(704-2 · `Labor_Sheet`) → [담기] = 지금 담기 동작(`done(로직)`) * 727-1 기초 = 로직마다 한 줄(`logicHeads` — 옵션 줄을 고르면 옵션을 버리던 흠 걷음 · 옵션은 담은 뒤 고름) - * 721-2 `lib.sangeun`(M02 일위대가 [로직 추가] · `withHopyo`) = 찾아 넣기 창(`Find_Pick` · 탭 M01 전체 · 단가산출 · + * 721-2 `lib.sangeun`(M02 일위대가 [로직 추가] · `withSangeun`) = 찾아 넣기 창(`Find_Pick` · 탭 M01 전체 · 단가산출 · * 일위대가 · 실무 호표 줄 없음) · M01 인력 · 재료 = `lib.fixed`(일위대가 고정 줄 · 없으면 회색 막힘) · * B09 · 불러오기는 이 창 그대로 * ========================================================================== */ @@ -47,7 +46,9 @@ import { upper, } from "./M02_MasterTemplete_Find_Pick"; -export { withHopyo } from "./M02_MasterTemplete_Hopyo_Pick"; +/** M02 일위대가 [로직 추가] = 찾아 넣기 창(721-2 `sangeun` · 프로젝트 층 그대로 · 옛 「실무 호표」 덧줄은 785-7 에서 지움) */ +export const withSangeun = (lib: LibraryPick): LibraryPick => + lib.layer === "project" ? lib : { ...lib, sangeun: true }; /** 구분 보이는 글(707 낱말) — 값(저장 글)은 그대로 */ const CLASS_TEXT: Record = { 실무자료: "실무 엑셀 일위대가" }; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_RowModal.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_RowModal.ts index ff6ea9c92..8a5e0eecc 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_RowModal.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_RowModal.ts @@ -14,7 +14,7 @@ import { createButton, el, showToast } from "@ui/ui_template_elements"; import { askConfirm, openModal } from "@ui/ui_template_modal"; import { createText } from "@ui/ui_template_text"; import type { ItemLogic } from "./M02_MasterTemplete_Api_Fetch"; -import { HOPYO_BUILT } from "./M02_MasterTemplete_Hopyo_Check"; +import { HOPYO_BUILT } from "./M02_MasterTemplete_Hopyo_Api"; import type { LaborCalc, LaborMat, LaborRow, MatMarkKey } from "./M02_MasterTemplete_Labor_Api"; import { openLaborInput } from "./M02_MasterTemplete_Labor_Input"; import { logicMatRows } from "./M02_MasterTemplete_Labor_LogicMats"; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts index 0193d557a..84100eba3 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts @@ -38,7 +38,7 @@ import { laborBlock } from "./M02_MasterTemplete_Labor_Grid"; import { tipBorn } from "./M02_MasterTemplete_Labor_Born"; import { unlinkedBar } from "./M02_MasterTemplete_Labor_Unlinked"; import { pasteInto } from "./M02_MasterTemplete_Labor_Paste"; -import { openLogicPicker, withHopyo } from "./M02_MasterTemplete_Labor_Pick"; +import { openLogicPicker, withSangeun } from "./M02_MasterTemplete_Labor_Pick"; import { openRefEdit } from "./M02_MasterTemplete_Labor_RefEdit"; import { openMatModal, openRowModal } from "./M02_MasterTemplete_Labor_RowModal"; import { tc } from "./M02_MasterTemplete_Labor_Text"; @@ -383,7 +383,7 @@ export function mountLaborTable( return showToast(tc("Labor_Already").replace("{value}", `${x.키} ${x.이름}`), "error"); cur.담은로직.push({ 로직: x.키, 별칭: null, 메모: null, 입력값: {}, 수량: { ...ONE } }); void loadLogics().then(() => changed()); - }, withHopyo(library)), + }, withSangeun(library)), }); // 정보 칸 — 이름 · 구분 · 상세구분 · 단위 · 본뜬 diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index 99cafeb84..8c5e47051 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -52,7 +52,6 @@ import { } from "./M02_MasterTemplete_TreePanel_Roots"; import { canEdit, createMain, newItemButton, type Selection } from "./M02_MasterTemplete_UI_Main"; import { buildSide } from "./M02_MasterTemplete_UI_Side"; -import { ITEM_BUILT } from "./M02_MasterTemplete_Hopyo_ToItem"; import { precomboList, type PrecomboScope } from "./M02_MasterTemplete_UI_Side_Precombo"; import { sangeunList } from "./M02_MasterTemplete_UI_Side_Sangeun"; import { buildTwoWay, pickedName } from "./M02_MasterTemplete_ItemPick"; @@ -203,16 +202,6 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise => { - await Promise.all([side.refresh(), tree.refresh()]); - const now = main.current(); - const shown = now?.kind === "table" && now.name === STRUCTURE_TABLE; - if (shown && (await main.settle()) !== false) await main.openTable(layer, null, true); - }; - const onItem = (): void => - main.root.isConnected ? void afterItem() : document.removeEventListener(ITEM_BUILT, onItem); - document.addEventListener(ITEM_BUILT, onItem); const main = createMain( () => isAdmin, () => void Promise.all([side.refresh(), tree.refresh()]), diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts index e06795503..f8e6c996d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts @@ -32,7 +32,7 @@ import { type TemplateInfo, } from "./M02_MasterTemplete_Api_Fetch"; import { STRUCTURE_TABLE } from "./M02_MasterTemplete_Structure"; -import { HOPYO_BUILT } from "./M02_MasterTemplete_Hopyo_Check"; +import { HOPYO_BUILT } from "./M02_MasterTemplete_Hopyo_Api"; import { buildHopyoSection } from "./M02_MasterTemplete_Hopyo_Side"; import type { Selection } from "./M02_MasterTemplete_UI_Main"; import { buildPrecomboSection, type PrecomboOptions } from "./M02_MasterTemplete_UI_Side_Precombo"; diff --git a/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_Draft.ts b/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_Draft.ts deleted file mode 100644 index 64395df5a..000000000 --- a/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_Draft.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_Draft.ts - * 확인 창 금액 미리 보기 꼴(PLAN 359-3) — 엑셀 줄 하나 + 지금 대상 → 일위대가 문서 한 벌(서버 `/labor/{열}/calc` 에 - * 보냄 · 저장 안 함 · 금액은 서버가 셈) · 줄이 들어갈 표(로직 표 · 자재 표). - * 정본 = 서버 `M02_Hopyo_Build.Builder.build` 의 `put`([사전조합 생성] 때 다시 셈) — 같은 규칙: - * 우리 수량 = 엑셀 수량 × 배 · 로직 = {로직, 입력값, 수량 {손}} · 자재 = {코드, 수량 {손}, 별도계상 = - * 갈래 별도계상이면 「별도계상」 아니면 「직접계산」} · 같은 자재 = 수량 더함. - * 「전국」 자재(361-1 · 키 · 단가 없음) = 자재 표 · 문서엔 안 넣음(Build = 본뜬.전국자재 · 금액 없음). - * 미리 보기만 다른 것: 호표 대상(하위 사전조합 — 만든 뒤 셈) · 배 없는 대상은 뺌 · - * 같은 로직 둘째는 뺌(Build = 한 줄 사전조합으로 감쌈 · 한 문서에 같은 로직 둘 = 서버 400). - * 화면 · fetch · sessionStorage 없음(`CLAUDE.md` 4장). - * ========================================================================== */ - -import type { HopyoLine, HopyoPart } from "../M02_MasterTemplete_Hopyo_Api"; -import type { LaborDoc, LaborMat, LaborRow } from "../M02_MasterTemplete_Labor_Api"; - -export type CheckTable = "logic" | "mat"; - -/** 엑셀 갈래 중 자재 표로 가는 것(대상 없을 때) */ -const MAT_KINDS = ["자재", "별도계상"]; - -/** 줄이 들어갈 표 — 대상이 모두 자재(전국 포함)면 자재 표 · 대상 없으면 갈래(자재 · 별도계상) · 그 밖 로직 표 */ -export function tableOf(line: Pick, parts: HopyoPart[]): CheckTable { - if (parts.length) return parts.every((p) => !!p.자재 || !!p.전국) ? "mat" : "logic"; - return MAT_KINDS.includes(line.갈래) ? "mat" : "logic"; -} - -/** 엑셀 줄 + 대상 → 미리 셈 문서 · 셀 것이 없으면 null(호표 대상뿐 · 배 없음 · 대상 없음) */ -export function draftDoc( - line: Pick, - parts: HopyoPart[], -): LaborDoc | null { - const rows: LaborRow[] = []; - const mats = new Map(); - const apart = line.갈래 === "별도계상" ? "별도계상" : "직접계산"; - for (const p of parts) { - if (p.배 === null || p.호표 !== undefined) continue; - const qty = (line.수량 ?? 0) * p.배; - if (p.자재) { - const have = mats.get(p.자재)?.수량; - const hand = (have ? have.손 : 0) + qty; - mats.set(p.자재, { - 코드: p.자재, - 이름: null, - 규격: null, - 단위: null, - 수량: { 손: hand }, - 별도계상: apart, - }); - } else if (p.로직 && !rows.some((r) => r.로직 === p.로직)) { - const values = (p.입력값 ?? {}) as LaborRow["입력값"]; - rows.push({ 로직: p.로직, 별칭: null, 메모: null, 입력값: values, 수량: { 손: qty } }); - } - } - if (!rows.length && !mats.size) return null; - return { 종류: "일위대가", 열: "", 단위: null, 담은로직: rows, 자재: [...mats.values()] }; -} diff --git a/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SheetMap.ts b/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SheetMap.ts index 0466aa207..cc48246df 100644 --- a/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SheetMap.ts +++ b/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SheetMap.ts @@ -1,11 +1,10 @@ /* ============================================================================= * M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SheetMap.ts * 엑셀 보기 창 세 보기 칠 · 뛸 곳(PLAN 779-4 · 요구서 621 3장 4 · 5) — 순수 셈. - * · 재료 = 서버 격자 `보기 · 목록 · 줄 · 호표 · 단산`(779-1 `M02_Hopyo_Grid`) · 목록 `줄상태 · 만든`(779-2 `Match.view`) · + * · 재료 = 서버 격자 `보기 · 목록 · 줄 · 호표 · 단산`(779-1 `M02_Hopyo_Grid`) · 목록 `줄상태 · 만든`(785-7 `Judge.view` 1차 판정) · * 블록 `[{번, 판정, SK, 갈래, 못옮김}]`(779-2 `M02_Hopyo_Marks` — 칠 꼴 표가 그 머리) · 확인 창 캐시 고름 * · 칠 = success 이어짐 · warning 이어짐 · 엑셀과 다름 · info 고를 것 · danger 못 이음 · muted 밖 + mine(내가 고름 띠) - * · 「이어짐」 = 확인 창과 한 규칙(`Hopyo_Check` `isChecked && checkable` · 캐시 고름이 이김) — 캐시에 대상이 없으면 - * 서버 체크 제안(서버가 배 확실할 때만 켬)을 그대로 믿음 + * · 「이어짐」 = 1차 판정 「제안 있음」 · 판정 화면 확인함 · 만든 SC(옛 확인 창 캐시 `draftOf` 는 785-7 부터 늘 null) * · 호표 머리 행 = 그 호표 줄 칠 중 가장 나쁜 것 · 블록 = 만든 SK 갈래 › 없으면 부르는 첫 줄 판정 「예상」 · 안 불림 = 밖 * · 블록 안 줄(779-5) = 블록 `줄 [[행, 상태, 차]]`(Marks · D-2 줄 잇기 상태) → 그 출처 행만 칠 · 머리 행 = 줄 가장 나쁜 칠 · * 「엑셀과 다름」 = 이어졌는데 금액 참고 차만 있을 때 · 줄 재료 없으면 옛 블록 칠(범위 통째) @@ -63,7 +62,7 @@ export interface Miss { 줄: { 행: number | null; 이름: string; 묶음: string; 차: number | null; 까닭: string }[]; 까닭: string; } -/** 확인 창 캐시 고름(`Hopyo_Check` `CheckDraft` 중 칠에 쓰는 것) */ +/** 옛 확인 창 캐시 고름 꼴(785-7 부터 부르는 쪽이 늘 null) */ export interface PickDraft { 체크: Record; 대상: Record; diff --git a/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SkPick.ts b/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SkPick.ts deleted file mode 100644 index 736103c41..000000000 --- a/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SkPick.ts +++ /dev/null @@ -1,242 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SkPick.ts - * 마법사 단가산출 줄 고름(PLAN 778-5 D-5 · 요구 621 · 622 · 623 · 624) — 순수 셈. - * · 재료 = 서버 `POST …/hopyo/{키}/sangeun` 답 `판정 [{번 … 줄 […]}]`(D-3 `M02_Hopyo_Sangeun.build`) - * · 고름 캐시 `{고름 {번: {차례: 고름}}, 확인 [번]}` — 줄 고름이 서버 상태를 덮음(꼬리표 「내가 고름」) - * · 블록 [확인] = 남은 고를 줄 0 일 때만 · 몸 = `{확인: [{번, 고름?}]}`(확인한 블록만 · 자동 확인 없음) - * · 금액 · 판정 정본은 서버 — 여기는 캐시를 겹쳐 보이기 · 몸 짓기만 - * · 780-2(요구 627) — 블록 `부름 [호표 번]`(빈 = 일위대가 밖 · 칸 없음 = 옛 서버 = 안) · 줄 0 = 「줄 없음」 확인 막음 · - * 거르기(모두 · 안 · 밖 · 고를 줄 있음) - * 화면 · fetch · sessionStorage 없음(`CLAUDE.md` 4장). - * ========================================================================== */ - -export type SkGroup = "L" | "X" | "M" | "R" | "S" | "E"; - -/** 줄 고름 — 노임 LB · 기계 EQ · 자재 MT · 율 기준 · 뺌 + 까닭 */ -export type SkPick = - | { 노임: string } - | { 기계: string } - | { 자재: string; 별도계상?: boolean } - | { 기준: SkBase; 비목?: string } - | { 뺌: string }; - -/** 율 기준 — 묶음 · 비목 · 줄 목록(그 엑셀 행 줄) */ -export type SkBase = string | { 행: number[] }; - -/** 서버 후보 — 노임 · 기계 = `키` · 자재 = `자재` · 율 = `기준` */ -export interface SkCand { - 키?: string; - 자재?: string; - 기준?: SkBase; - 비목?: string; - 갈래?: string; - 이름?: string | null; - 규격?: string; - 단위?: string; - 구분?: string; - 단가?: number | null; - 글?: string; -} - -export interface SkLine { - 차례: number; - 행: number | null; - 묶음: SkGroup; - 이름: string | null; - 규격: string; - 매개값: Record; - 엑셀금액: number | null; - 상태: string; - 방법: string; - 단위: unknown; - 후보: SkCand[]; - 꼬리표: string[]; - 글: string; - 우리금액?: number | null; - 차?: number | null; -} - -export interface SkBlock { - 번: number; - 이름: string | null; - 단위: string | null; - 판정: string; - 고를줄: number; - 우리금액: number | null; - 엑셀금액: number | null; - 근거: { 로직: string; 이름: string | null; 판정: string | null } | null; - 만든: string | null; - 줄: SkLine[]; - /** 부르는 일위대가 호표 번(780-1) — [] = 일위대가 밖 · 없음 = 옛 서버(안) */ - 부름?: number[]; -} - -export interface SkJudged { - 블록: number; - 판정: SkBlock[]; - 만듦: { 번: number; 이름: string | null; 키: string; 갈래: string; 못옮김: unknown[] }[]; - 못만듦: { 번: number; 이름: string | null; 까닭: string }[]; - 그대로: number; - 같은것씀: { 번: number; 키: string }[]; - 위층같음: { 번: number; 층: string; 주인: string | null; 키: string }[]; -} - -/** 확인 목록 한 칸(서버 몸) */ -export interface SkSure { - 번: number; - 고름?: Record; -} - -/** 캐시 꼴 — 블록 번 · 줄 차례는 JSON 열쇠라 글 */ -export interface SkDraft { - 고름: Record>; - 확인: number[]; -} - -export const MINE = "내가 고름"; -export const GOOD = "옮길 수 있음"; -export const NO_LINES = "줄 없음"; -export const LINKED = "이어짐"; -export const PICK = "고를 것"; -export const OUT = "밖"; -export const DROP = "뺌"; -export const GROUP: Record = { - L: "노무", - X: "기계", - M: "자재", - R: "율", - S: "하위", - E: "값", -}; -/** 율 기준 고르기 칸 */ -export const BASES = ["인력", "재료", "노무비", "재료비", "경비"]; -const DONE = new Set([LINKED, OUT, DROP]); - -export const emptyDraft = (): SkDraft => ({ 고름: {}, 확인: [] }); - -/** 캐시 글 → 꼴(틀리면 빈 것) */ -export function readDraft(raw: string | null): SkDraft { - try { - const got = JSON.parse(raw ?? "") as Partial | null; - const picks = got?.고름 && typeof got.고름 === "object" ? got.고름 : {}; - const sure = Array.isArray(got?.확인) ? got.확인.filter((n) => Number.isInteger(n)) : []; - return { 고름: picks, 확인: sure }; - } catch { - return emptyDraft(); - } -} - -/** 고른 것 글 — 키 · 율 기준 · 뺌 까닭 */ -export function pickText(p: SkPick): string { - if ("뺌" in p) return `${DROP} · ${p.뺌}`; - if ("기준" in p) { - const base = typeof p.기준 === "string" ? p.기준 : `${p.기준.행.join(" · ")} 행`; - return `기준 ${base}${p.비목 ? ` · ${p.비목}` : ""}`; - } - return "노임" in p ? p.노임 : "기계" in p ? p.기계 : p.자재; -} - -/** 줄 보이기 — 캐시 고름이 서버 상태를 덮음 */ -export function lineView( - line: SkLine, - pick: SkPick | undefined, -): { 상태: string; 꼬리표: string[]; 글: string; mine: boolean } { - if (!pick) return { 상태: line.상태, 꼬리표: line.꼬리표, 글: line.글, mine: false }; - return { 상태: "뺌" in pick ? DROP : LINKED, 꼬리표: [MINE], 글: pickText(pick), mine: true }; -} - -/** 그 블록 캐시 고름 `{차례: 고름}` */ -export const picksOf = (d: SkDraft, 번: number): Record => d.고름[String(번)] ?? {}; - -/** 남은 고를 줄 수 — 이어짐 · 밖 · 뺌 아닌 줄(캐시 고름 겹침) · 줄 0 블록 = 1(서버 「줄 없음」 고를줄 1) */ -export const leftLines = (b: SkBlock, picks: Record): number => - b.줄.length ? b.줄.filter((l) => !DONE.has(lineView(l, picks[String(l.차례)]).상태)).length : 1; - -/** [확인] 켜짐 — 고를 줄 0 */ -export const canConfirm = (b: SkBlock, picks: Record): boolean => - leftLines(b, picks) === 0; - -/** 블록 판정 글 — 캐시 겹친 뒤 「옮길 수 있음」 · 「고를 줄 n」 · 「줄 없음」 */ -export function verdictOf(b: SkBlock, picks: Record): string { - if (!b.줄.length) return NO_LINES; - const n = leftLines(b, picks); - return n ? `고를 줄 ${n}` : GOOD; -} - -/** 줄 고를 수 있나 — 밖 줄 = 아님 · 하위 · 값 = 뺌만 */ -export const pickable = (l: SkLine): boolean => l.상태 !== OUT; -export const dropOnly = (l: SkLine): boolean => l.묶음 === "S" || l.묶음 === "E"; - -/** 서버 후보 → 고름(그 줄 묶음 꼴 · 아니면 null) */ -export function candPick(group: SkGroup, c: SkCand): SkPick | null { - if (group === "L" && c.키?.startsWith("LB")) return { 노임: c.키 }; - if (group === "X" && c.키?.startsWith("EQ")) return { 기계: c.키 }; - if (group === "M" && c.자재) return { 자재: c.자재 }; - if (group === "R" && c.기준 !== undefined && c.기준 !== null) - return { 기준: c.기준, ...(c.비목 ? { 비목: c.비목 } : {}) }; - return null; -} - -/** 후보 글 — 「이름 규격 [단위]」 · 율 = 기준 */ -export function candText(c: SkCand): string { - if (c.기준 !== undefined) return pickText({ 기준: c.기준, ...(c.비목 ? { 비목: c.비목 } : {}) }); - const spec = c.규격 ? ` ${c.규격}` : ""; - const unit = c.단위 ? ` [${c.단위}]` : ""; - return `${c.이름 ?? c.키 ?? c.자재 ?? ""}${spec}${unit}${c.구분 ? ` · ${c.구분}` : ""}`; -} - -/** 캐시 정리 — 고를 줄 남은 블록 · 이미 옮겨 판정 없는 블록은 확인에서 뺌 */ -export function tidy(d: SkDraft, blocks: SkBlock[]): SkDraft { - const by = new Map(blocks.map((b) => [b.번, b])); - const sure = d.확인.filter((n) => { - const b = by.get(n); - return !!b && canConfirm(b, picksOf(d, n)); - }); - return { ...d, 확인: [...new Set(sure)].sort((a, b) => a - b) }; -} - -/** [확인한 것 옮기기] 몸 — 확인한 블록만 · 고름은 그 블록 줄에 있는 차례만 */ -export function sureBody(d: SkDraft, blocks: SkBlock[]): SkSure[] { - const by = new Map(blocks.map((b) => [b.번, b])); - return tidy(d, blocks).확인.map((n) => { - const have = new Set(by.get(n)!.줄.map((l) => l.차례)); - const picks = Object.entries(picksOf(d, n)).filter(([at]) => have.has(Number(at))); - return picks.length - ? { 번: n, 고름: Object.fromEntries(picks.map(([at, p]) => [Number(at), p])) } - : { 번: n }; - }); -} - -/** 일위대가 밖 블록 — `부름` 빈 배열 */ -export const outside = (b: SkBlock): boolean => !!b.부름 && !b.부름.length; - -/** 마법사 [끝내기] 글 재료 — 아직 안 옮긴 블록 수 · 그중 일위대가 밖 · 그 블록 고를 줄 합 */ -export function waiting(d: SkDraft, blocks: SkBlock[]): { 블록: number; 밖: number; 줄: number } { - const open = blocks.filter((b) => !b.만든); - const 줄 = open.reduce((n, b) => n + leftLines(b, picksOf(d, b.번)), 0); - return { 블록: open.length, 밖: open.filter(outside).length, 줄 }; -} - -export type SkFilter = "all" | "in" | "out" | "open"; -/** 거르기 — 모두 · 일위대가 안 · 일위대가 밖 · 고를 줄 있음(안 옮긴 블록 · 캐시 겹침) */ -export function filterBlocks(blocks: SkBlock[], d: SkDraft, f: SkFilter): SkBlock[] { - if (f === "in") return blocks.filter((b) => !outside(b)); - if (f === "out") return blocks.filter(outside); - if (f === "open") return blocks.filter((b) => !b.만든 && leftLines(b, picksOf(d, b.번)) > 0); - return blocks; -} - -/** 줄 고름 넣기 · 지움(null) — 새 캐시 */ -export function setPick(d: SkDraft, 번: number, 차례: number, p: SkPick | null): SkDraft { - const mine = { ...picksOf(d, 번) }; - if (p) mine[String(차례)] = p; - else delete mine[String(차례)]; - return { ...d, 고름: { ...d.고름, [String(번)]: mine } }; -} - -/** [확인] 켬 · 끔 — 고를 줄 남으면 안 켬 */ -export function setSure(d: SkDraft, b: SkBlock, on: boolean): SkDraft { - const rest = d.확인.filter((n) => n !== b.번); - const ok = on && canConfirm(b, picksOf(d, b.번)); - return { ...d, 확인: ok ? [...rest, b.번].sort((x, y) => x - y) : rest }; -} diff --git a/resources/tester/helper_729_3_hopyo_left.cjs b/resources/tester/helper_729_3_hopyo_left.cjs deleted file mode 100644 index 494b14420..000000000 --- a/resources/tester/helper_729_3_hopyo_left.cjs +++ /dev/null @@ -1,94 +0,0 @@ -/* 729-3 안 이은 줄 화면 헬퍼 — `Hopyo_Left` · `Hopyo_Check` · `Api_Fetch` · `Labor_Unlinked` 도우미를 - * 트랜스파일해 Node 로 돌림 · fetch 대역(부른 길 · 몸 모음 · 409 몸) · sessionStorage 대역 · - * stdin = {one, made, left, fixes} · stdout = JSON. */ -const fs = require("fs"); -const path = require("path"); -const { makeLoader, memoryStorage } = require("./helper_ts_stub_loader.cjs"); - -const ROOT = path.join(__dirname, "..", ".."); -const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api", ROUTES: {} } }; -const STUB_FILES = [ - "M02_MasterTemplete_Labor_Pick", - "M02_Material_Pick", - "M02_MasterTemplete_Find_Pick", - "M02_MasterTemplete_Labor_Api", - "M02_MasterTemplete_Labor_Input", - "M01_MasterData_UI_LogicLab_Source", - "M01_MasterData_UI_Logic_Api", - "M02_MasterTemplete_Hopyo_CheckRows", - "M02_MasterTemplete_Find_Pick", -]; -const stub = new Proxy({}, { get: (_t, k) => (k === "currentLanguageIndex" ? () => 0 : () => {}) }); -const { loadTs } = makeLoader({ - root: ROOT, - requireFor: (full) => (request) => { - if (STUBS[request]) return STUBS[request]; - if (STUB_FILES.some((name) => request.endsWith(name))) return stub; - if (request.startsWith(".") && !request.endsWith(".css")) - return loadTs(`${path.resolve(path.dirname(full), request)}.ts`); - return stub; - }, -}); -global.sessionStorage = memoryStorage(); -global.document = { dispatchEvent: () => true }; // 캐시 고침 알림(779-4) 대역 - -(async () => { - const input = JSON.parse(fs.readFileSync(0, "utf8")); - const calls = []; - global.fetch = async (url, init = {}) => { - const body = init.body ? JSON.parse(init.body) : null; - calls.push({ url, method: init.method ?? "GET", body }); - if (url.includes("/hopyo-build")) - return { status: 409, ok: false, json: async () => ({ detail: { 까닭: "남음", 남은줄: input.left } }) }; // prettier-ignore - if (url.includes("/hopyo-unlinked")) { - const done = new Set((body.줄 ?? []).map((x) => x.차례)); - const left = input.left.filter((x) => !done.has(x.차례)); - return { status: 200, ok: true, json: async () => ({ 남은줄: left, 수: left.length }) }; - } - const no = Number(url.split("?")[0].split("/").pop()); - const one = { ...input.one, 호표: { ...input.one.호표, 번: no, 만든: input.made.includes(no) ? { 키: "SC-1", 확인: true } : null } }; // prettier-ignore - return { status: 200, ok: true, json: async () => one }; - }; - const M02 = path.join(ROOT, "M02_MasterTemplete"); - const check = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Check.ts")); - const left = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Left.ts")); - const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Api.ts")); - const grid = loadTs(path.join(M02, "M02_MasterTemplete_Labor_Unlinked.ts")); - const scope = { layer: "personal", owner: null }; - const at = { scope, target: scope, key: "HP-000001" }; - - // 만들기 409 → 남은 줄 · 까닭 글 - let stale = null; - try { - await api.buildHopyo(scope, { 층: "personal", 주인: null, 키: "HP-000001", 번: 2 }, []); - } catch (error) { - stale = { message: error.message, left: api.leftOf(error), name: error.constructor.name }; - } - const before = await left.countLeft(at, [1, 2]); - // 줄마다 고름 — 노임 · 기계 · 비율 · 우리에 없음 · 로직 - const [a, b, c, d, e] = input.left; - left.put(at, 2, a.차례, [{ 노임: "LB000002", 이름: "보통인부", 입력값: {}, 배: 1 }]); - left.put(at, 2, b.차례, [{ 기계: "EQ000069", 이름: "덤프트럭", 입력값: {}, 배: 1 }]); - left.put(at, 2, c.차례, [{ 비율: "노무비", 이름: "노무비", 입력값: {}, 배: 1 }]); - left.put(at, 2, d.차례, null); - const half = await left.countLeft(at, [1, 2]); - left.put(at, 2, e.차례, [{ 로직: "GF000113", 이름: "되메우기", 입력값: {}, 배: 1 }]); - const after = await left.countLeft(at, [1, 2]); - const body = left.draftLines(at, after.get(2).one); - const count = (m) => left.leftCount(m); - process.stdout.write( - JSON.stringify({ - stale, - before: count(before), - half: count(half), - after: count(after), - body, - unlinkedCalls: calls.filter((x) => x.url.includes("/hopyo-unlinked")).map((x) => x.url), - lastBody: calls.filter((x) => x.url.includes("/hopyo-unlinked")).pop().body, - fixes: grid.unlinkedFixes({ 담은로직: input.fixes }).map((x) => x.이름), - }), - ); -})().catch((error) => { - process.stderr.write(String(error?.stack ?? error)); - process.exit(1); -}); diff --git a/resources/tester/helper_778_5_sk_pick.cjs b/resources/tester/helper_778_5_sk_pick.cjs index 3fb08cace..a433c596b 100644 --- a/resources/tester/helper_778_5_sk_pick.cjs +++ b/resources/tester/helper_778_5_sk_pick.cjs @@ -1,12 +1,10 @@ -/* 778-5 D-5 마법사 단가산출 줄 고름 헬퍼 — `logic/Hopyo_SkPick`(순수) · `Hopyo_SkApi`(몸) 를 트랜스파일해 Node 로 돌림 · - * fetch 대역(부른 길 · 몸 모음) · stdin = {blocks} · stdout = JSON. - * 780-2 — stdin `{sheetSk: {...}}` = 엑셀 보기 오른쪽 판 바꾸기(`Hopyo_SheetSk`) · 단가산출 칸 · 시트 막대 · 판은 가짜. */ +/* 780-2 · 785-6 엑셀 보기 오른쪽 판 바꾸기 헬퍼 — stdin `{sheetSk: {...}}` = `Hopyo_SheetSk` 를 트랜스파일해 Node 로 돌림 · + * 단가산출 칸 · 시트 막대 · 판은 가짜 · stdout = JSON. 옛 마법사 줄 고름(`SkPick` · `SkApi`)은 785-7 에서 지움. */ const fs = require("fs"); const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); const ROOT = path.join(__dirname, "..", ".."); -const made = []; // 가짜 단가산출 칸 — 지은 인자 · 강조 const fakeEl = () => ({ hidden: false, attrs: {}, @@ -64,7 +62,7 @@ const STUBS = { }, }, "./M02_MasterTemplete_Hopyo_Block": { createJudgeBlock: (o) => ({ ...o, destroy() {} }) }, - "./M02_MasterTemplete_Hopyo_Check": { POLL_MS: 3000 }, + "./M02_MasterTemplete_Hopyo_Api": { POLL_MS: 3000 }, "./M02_MasterTemplete_Hopyo_Rows": { createJudgeMove: () => ({ root: fakeEl(), destroy() {} }) }, "@config/config_frontend": { API_BASE_URL: "/api", ROUTES: {} }, "@ui/ui_template_elements": { @@ -78,17 +76,6 @@ const STUBS = { }, }), }, - "./M02_MasterTemplete_Hopyo_WizardSk": { - sangeunStep: (scope, key, file, onChange, o) => { - const step = { - args: { scope, key, file, o: { ...o, onRow: !!o.onRow } }, - marks: [], - onRow: o.onRow, - }; - made.push(step); - return { root: fakeEl(), mark: (n) => step.marks.push(n) }; - }, - }, }; const stub = new Proxy({}, { get: () => () => {} }); const { loadTs } = makeLoader({ @@ -104,87 +91,7 @@ global.sessionStorage = { getItem: () => null, setItem: () => {}, removeItem: () (async () => { const input = JSON.parse(fs.readFileSync(0, "utf8")); - if (input.sheetSk) return process.stdout.write(JSON.stringify(await sheetSk(input.sheetSk))); - const { blocks } = input; - const calls = []; - global.fetch = async (url, init = {}) => { - calls.push({ - url, - method: init.method ?? "GET", - body: init.body ? JSON.parse(init.body) : null, - }); - return { status: 200, ok: true, json: async () => ({ 판정: blocks, 만듦: [], 못만듦: [] }) }; - }; - const M02 = path.join(ROOT, "M02_MasterTemplete"); - const sk = loadTs(path.join(M02, "logic", "M02_MasterTemplete_Hopyo_SkPick.ts")); - const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_SkApi.ts")); - const [a, b, c] = blocks; // a = 고를 줄 2 · b = 옮길 수 있음 · c = 못 이음(하위) - const out = {}; - let d = sk.emptyDraft(); - out.start = blocks.map((x) => ({ - 번: x.번, - 판정: sk.verdictOf(x, {}), - 확인켜짐: sk.canConfirm(x, {}), - })); - out.emptyBody = sk.sureBody(d, blocks); // 아무것도 안 눌렀으면 몸 비움(자동 확인 없음) - d = sk.setSure(d, a, true); // 고를 줄 남은 블록 = 안 켜짐 - out.sureBlocked = d.확인; - d = sk.setPick(d, a.번, 1, { 노임: "LB000099" }); // 작업반장 → 다른 노임 - out.half = { - 판정: sk.verdictOf(a, sk.picksOf(d, a.번)), - 켜짐: sk.canConfirm(a, sk.picksOf(d, a.번)), - }; - d = sk.setPick(d, a.번, 2, { 자재: "MT000123" }); - d = sk.setPick(d, a.번, 3, { 뺌: "우리에 없음" }); - out.view = a.줄.map((l) => sk.lineView(l, sk.picksOf(d, a.번)[String(l.차례)])); - out.full = { - 판정: sk.verdictOf(a, sk.picksOf(d, a.번)), - 켜짐: sk.canConfirm(a, sk.picksOf(d, a.번)), - }; - d = sk.setSure(d, a, true); - d = sk.setSure(d, b, true); - d = sk.setSure(d, c, true); // 못 이음 남음 = 안 켜짐 - out.sure = d.확인; - out.body = sk.sureBody(d, blocks); - out.wait = sk.waiting(d, blocks); - // 고름 지우면 확인에서 빠짐(tidy) - out.tidy = sk.tidy(sk.setPick(d, a.번, 2, null), blocks).확인; - // 캐시 글 왕복 · 틀린 글 - out.round = sk.readDraft(JSON.stringify(d)); - out.bad = sk.readDraft("{엉터리"); - out.cand = { - L: sk.candPick("L", { 키: "LB000001", 이름: "작업반장" }), - X: sk.candPick("X", { 키: "EQ000019", 이름: "굴착기" }), - M: sk.candPick("M", { 자재: "MT000001", 이름: "시멘트" }), - R: sk.candPick("R", { 기준: "노무비", 비목: "경비" }), - Mbad: sk.candPick("M", { 키: "LB000001" }), - text: sk.candText({ 이름: "합판", 규격: "12mm", 단위: "장" }), - }; - await api.judgeSangeun({ layer: "personal", owner: null }, "HP-000001"); - await api.judgeSangeun({ layer: "personal", owner: null }, "HP-000001", out.body); - out.calls = calls; - // 780-2 일위대가 안 · 밖 · 줄 0 블록 — 거르기 · 판정 · 확인 막음 · 끝내기 글 재료 - const more = [ - { ...a, 부름: [3, 7] }, - { ...b, 부름: [] }, - { ...b, 번: 9, 부름: [], 줄: [] }, - { ...c, 부름: [12], 만든: "SK-000001" }, - ]; - const e = more[2]; - let d2 = sk.setSure(sk.emptyDraft(), e, true); - out.empty = { - 판정: sk.verdictOf(e, {}), - 켜짐: sk.canConfirm(e, {}), - 확인: d2.확인, - 몸: sk.sureBody({ 고름: {}, 확인: [9] }, more), - }; - d2 = sk.emptyDraft(); - out.filter = Object.fromEntries( - ["all", "in", "out", "open"].map((f) => [f, sk.filterBlocks(more, d2, f).map((x) => x.번)]), - ); - out.old = sk.filterBlocks(blocks, d2, "out").length; // 옛 서버(부름 없음) = 모두 안 - out.wait2 = sk.waiting(d2, more); - process.stdout.write(JSON.stringify(out)); + process.stdout.write(JSON.stringify(await sheetSk(input.sheetSk))); })().catch((error) => { process.stderr.write(String(error?.stack ?? error)); process.exit(1); diff --git a/resources/tester/helper_m02_hopyo_check.cjs b/resources/tester/helper_m02_hopyo_check.cjs deleted file mode 100644 index 28c36b80d..000000000 --- a/resources/tester/helper_m02_hopyo_check.cjs +++ /dev/null @@ -1,315 +0,0 @@ -/* M02 일위대가 실무자료 확인 창(338-3) 헬퍼 — `Hopyo_Check` · `Hopyo_Choose` · `Hopyo_Ledger` · `Hopyo_Sheet` - * 도우미와 `Hopyo_Api` 길을 트랜스파일해 Node 로 돌림 · fetch 대역이 부른 길 · 몸을 모음 · sessionStorage 대역 · - * stdin = {lines, rows, ledger} · stdout = {check, choose, ledger, sheet, calls, pick} JSON. */ -const fs = require("fs"); -const path = require("path"); -const { makeLoader } = require("./helper_ts_stub_loader.cjs"); - -const ROOT = path.join(__dirname, "..", ".."); -const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api" } }; -// DOM · 모듈 메타를 쓰는 창은 대역(도우미 · 길만 시험) -const STUB_FILES = [ - "M02_MasterTemplete_Labor_Pick", - "M02_Material_Pick", - "M02_MasterTemplete_Find_Pick", - "M02_MasterTemplete_Labor_Api", - "M02_MasterTemplete_Labor_Input", - "M01_MasterData_UI_LogicLab_Source", - "M02_MasterTemplete_Hopyo_Left", // 남은 줄 창(729-3 · DOM · 찾기 창) - "M02_MasterTemplete_Hopyo_CheckRows", // 표 그리기(DOM · Labor_Rows) — 359-3 -]; -const opened = []; -const stub = new Proxy( - {}, - { - get: (_t, k) => - k === "currentLanguageIndex" - ? () => 0 - : k === "openModal" - ? (o) => opened.push(o.title) - : () => {}, - }, -); -const { loadTs } = makeLoader({ - root: ROOT, - requireFor: (full) => (request) => { - if (STUBS[request]) return STUBS[request]; - if (STUB_FILES.some((name) => request.endsWith(name))) return stub; - if (request.startsWith(".") && !request.endsWith(".css")) - return loadTs(`${path.resolve(path.dirname(full), request)}.ts`); - return stub; - }, -}); - -const store = new Map(); -global.sessionStorage = { - getItem: (k) => (store.has(k) ? store.get(k) : null), - setItem: (k, v) => store.set(k, String(v)), - removeItem: (k) => store.delete(k), -}; -global.document = { dispatchEvent: () => true }; // 캐시 고침 알림(779-4) 대역 - -let out0 = null; -(async () => { - const input = JSON.parse(fs.readFileSync(0, "utf8")); - const calls = []; - global.fetch = async (url, init = {}) => { - calls.push({ - url, - method: init.method ?? "GET", - body: init.body ? JSON.parse(init.body) : null, - }); - const body = url.includes("/hopyo-picks") || url.includes("/hopyo-ledger") ? { 줄: [] } : {}; - return { status: 200, ok: true, json: async () => body }; - }; - const M02 = path.join(ROOT, "M02_MasterTemplete"); - const check = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Check.ts")); - const choose = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Choose.ts")); - const ledger = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Ledger.ts")); - const sheet = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Sheet.ts")); - const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Api.ts")); - const pick = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Pick.ts")); - const draft = loadTs(path.join(M02, "logic", "M02_MasterTemplete_Hopyo_Draft.ts")); - if (input.drafts) { - // 359-3 — 실물 줄 → 미리 셈 문서 · 표(서버 calc 에 그대로 보냄) - const d = check.loadDraft("drafts"); - out0 = input.drafts.map((l) => { - const parts = check.partsOf(l, d); - return { 차례: l.차례, table: draft.tableOf(l, parts), doc: draft.draftDoc(l, parts) }; - }); - process.stdout.write(JSON.stringify(out0)); - return; - } - if (input.real) { - // 실물 맞춤 응답(338-4) — 서버 줄을 그대로 먹여 체크 · 몸 · 빠질 줄 · 품 비교 · 글 - const lines = input.real; - const d = check.loadDraft("real"); - out0 = { - body: check.bodyOf(lines, d), - leftOut: check.leftOut(lines, d), - sure: check.sureLines(lines, d).map((l) => l.차례), - lines: lines.map((l) => { - const p = check.partsOf(l, d)[0]; - return { - 이름: l.이름, - 상태: l.맞춤.상태, - checkable: check.checkable(l, d), - checked: check.isChecked(l, d), - compare: check.compareText(l.맞춤.품), - qty: choose.qtyText(l.맞춤.품?.엑셀), - target: choose.targetText(p), - inputs: p ? choose.inputsText(p.입력값 ?? {}, p.못읽음 ?? [], p.출처 ?? {}) : "", - cands: (l.맞춤.후보 ?? []).map((c) => [ - choose.targetText(c), - c.판정 ?? "", - choose.basisTag(c.근거), - ]), - }; - }), - }; - process.stdout.write(JSON.stringify(out0)); - return; - } - const { lines } = input; - const scope = { layer: "personal", owner: null }; - const key = check.draftKey(scope, "HP-000001", 3); - - // 서버 제안 그대로 → 사용자가 만짐(체크 끔 · 뺌 · 고른 대상) → 캐시 다시 읽음 - const fresh = check.loadDraft(key); - const suggested = check.bodyOf(lines, fresh); - const d = check.loadDraft(key); - d.체크[1] = false; // 맞음 줄 체크 끔 - d.뺌[3] = "우리에 없음"; - d.체크[3] = true; - d.대상[4] = [{ 로직: "GF000211", 이름: "고임돌", 입력값: { 유형: "보통" }, 배: 0.1, 몫: 0.1 }]; - d.체크[4] = true; - check.saveDraft(key, d); - const again = check.loadDraft(key); - const ask = check.newest(); - const a = ask(1); - const b = ask(1); - const c = ask(2); - const out = { - check: { - key, - suggested, - edited: check.bodyOf(lines, again), - leftOut: check.leftOut(lines, again), - checkable: lines.map((l) => check.checkable(l, fresh)), - sure: check.sureLines(lines, fresh).map((l) => l.차례), - compare: check.compareText(lines[1].맞춤.품), - compareMany: check.compareText(lines[3].맞춤.품), - toTarget: [ - check.toTarget({ 호표: 46, 이름: "고화매트", 입력값: {}, 배: 2.3 }), - check.toTarget({ 자재: "MT1", 이름: "레미콘", 입력값: {}, 배: 1, 몫: null }), - check.toTarget({ 전국: true, 이름: "레미콘(전국)-관급", 입력값: {}, 배: 1, 몫: null }), - ], - newest: [a(), b(), c()], - dropped: (check.dropDraft(key), store.has(key)), - }, - choose: { - hint: [ - choose.searchHint({ - 이름: "레미콘타설(장비사용)", - 근거: { 출처: [["건설품셈", "6-1-1"]], 직종: [], 행: [1, 1] }, - }), - choose.searchHint({ 이름: "(소)고임돌 채집 및 운반" }), - ], - picked: choose.pickedTargets([ - { c: { 로직: "GC000206" }, 몫: null }, - { c: { 로직: "GF000211" }, 몫: 0.1 }, - ]), - inputs: choose.inputsText({ 소량감률: 50, 지역: "서울" }, ["측압"], { - 소량감률: "블록", - 지역: "자동값", - }), - word: [choose.builtWord({ 다시씀: false }), choose.builtWord({ 다시씀: true })], - qty: choose.qtyText(lines[1].맞춤.품.엑셀), - target: choose.targetText({ 호표: 46, 단위: "㎡" }), - // 358-1 후보 묶음 — 1위 / 출처 같음 / 이름 닮은 로직 / 숫자만 닮음(약한 후보 끝) · 같은 이름 책 - groups: (() => { - const m = { - 후보: [ - { 로직: "A", 이름: "되메우기", 판정: "맞음", 뜻겹침: true, 책: "산림품셈" }, - { 로직: "B", 이름: "야면석 채집", 판정: "후보", 뜻겹침: false }, - { - 로직: "C", - 이름: "되메우기", - 판정: "후보", - 뜻겹침: true, - 출처같음: true, - 책: "건설품셈", - }, - { 로직: "D", 이름: "표토펴기", 판정: "다른 점", 뜻겹침: false }, - ], - 이름후보: [ - { 로직: "A", 이름: "되메우기" }, - { 로직: "E", 이름: "되메우기(노상재)" }, - ], - }; - const g = choose.candGroups(m); - const twins = choose.sameNames(g); - return { - groups: g.map((x) => [x.제목, x.접힘, x.후보.map((c) => c.로직)]), - text: g.flatMap((x) => x.후보).map((c) => choose.candText(c, twins)), - material: choose - .candGroups({ 후보: [{ 자재: "M1" }, { 자재: "M2" }] }, true) - .map((x) => x.제목), - // 361-1 지역만 다른 자재 — 1위 전국 · 지역 변형 접힘 · 고르면 그 지역 · 전국 되돌림 - national: (() => { - const all = [ - { 전국: true, 이름: "레미콘(전국)-관급", 규격: "25-21-120", 단위: "㎥" }, - { 자재: "M1", 이름: "레미콘(서울)-사급", 변형: true }, - { 자재: "M2", 이름: "레미콘(관급)-강원도(강릉시)", 변형: true }, - ]; - const g = choose.candGroups({ 후보: all }, true); - const id = (c) => c.자재 ?? (c.전국 ? "전국" : ""); - return { - groups: g.map((x) => [x.제목, x.접힘, x.후보.map(id)]), - text: choose.candText(all[0], new Set()), - region: choose.pickedTargets([{ c: all[2], 몫: null }]), - back: choose.pickedTargets([{ c: all[0], 몫: null }]), - hit: choose - .candGroups({ 후보: [all[2], all[0], all[1]] }, true) - .map((x) => [x.제목, x.후보.map(id)]), - }; - })(), - pick: [ - choose.canPickMaterial({ 갈래: "단가산출", 단가: { 재료비: 10 } }), - choose.canPickMaterial({ 갈래: "단가산출", 단가: { 재료비: null } }), - choose.canPickMaterial({ 갈래: "별도계상", 단가: {} }), - ], - }; - })(), - }, - ledger: (() => { - const rows = input.ledger; - const edits = { - "가|b|m3": null, // 지움 - "나||m3": { 원래: { 품명: "나", 규격: "", 단위: "m3" }, 갈래: "안 씀", 짝: [] }, - "다||개": ledger.entryOf(rows[2]), // 위 층 → 내 층 - }; - const merged = ledger.mergeRows(rows, edits, { layer: "personal", owner: "7/42" }); - return { - key: ledger.ledgerKey({ layer: "company", owner: "7" }), - merged: merged.map((r) => [r.열쇠, r.층, r.고침, !!r.고친, r.갈래]), - filter: ledger.filterRows(merged, "MT1", "").map((r) => r.열쇠), - kind: ledger.filterRows(merged, "", "안 씀").map((r) => r.열쇠), - pair: ledger.pairText({ 로직: "GC000198", 입력값: { 소량감률: 50 }, 배: 1, 몫: 0.1 }), - entry: ledger.entryOf({ ...rows[0], 갈래: "우리에 없음" }), - }; - })(), - draft: (() => { - // 359-3 — 미리 셈 문서(우리 수량 = 엑셀 × 배 · 별도계상 · 같은 자재 더함 · 같은 로직 둘째 · 호표 · 배 없음 뺌) - const L = { 로직: "GC000198", 입력값: { 소량감률: 50 }, 배: 2 }; - const M = { 자재: "MT1", 입력값: {}, 배: 1 }; - return { - logic: draft.draftDoc({ 수량: 0.5, 갈래: "단가산출" }, [ - L, - { ...L, 배: 3 }, - { 호표: 46, 입력값: {}, 배: 1 }, - { 로직: "GF000101", 입력값: {}, 배: null }, - ]), - mat: draft.draftDoc({ 수량: 2, 갈래: "별도계상" }, [M, { ...M, 배: 0.5 }]), - direct: draft.draftDoc({ 수량: null, 갈래: "자재" }, [M]).자재[0], - none: [ - draft.draftDoc({ 수량: 1, 갈래: "단가산출" }, []), - draft.draftDoc({ 수량: 1, 갈래: "호표" }, [{ 호표: 3, 입력값: {}, 배: 1 }]), - draft.draftDoc({ 수량: 1, 갈래: "별도계상" }, [{ 전국: true, 입력값: {}, 배: 1 }]), - ], - table: [ - draft.tableOf({ 갈래: "단가산출" }, [M]), - draft.tableOf({ 갈래: "자재" }, [L]), - draft.tableOf({ 갈래: "자재" }, [L, M]), - draft.tableOf({ 갈래: "별도계상" }, []), - draft.tableOf({ 갈래: "%" }, []), - draft.tableOf({ 갈래: "별도계상" }, [{ 전국: true, 입력값: {}, 배: 1 }]), - ], - }; - })(), - sheet: { - order: sheet.buildOrder([ - { 번: 45, 아래: [46] }, - { 번: 46, 아래: [] }, - { 번: 3, 아래: [] }, - { 번: 7, 아래: [8] }, - { 번: 8, 아래: [7] }, - ]), - linked: [ - { 맞춤: [2, 2] }, - { 맞춤: [1, 2] }, - { 맞춤: [0, 0] }, - { 맞춤: [3, 2] }, - { 맞춤: [1, 4], 연결할: 3, 연결된: 3 }, // 358-3 두 칸이 앞섬(% · 노임 줄 밖) - { 맞춤: [3, 3], 연결할: 3, 연결된: 2 }, - { 맞춤: [0, 1], 연결할: 0, 연결된: 0 }, - ].map(sheet.allLinked), - }, - }; - // 길 — 만들기(줄 · 통째) · 호표 하나(만들 층) · 미리 셈 · 장부 - const from = { 층: "system", 주인: null, 키: "HP-000001", 번: 3 }; - const target = { layer: "company", owner: "7" }; - await api.buildHopyo(target, from, suggested); - await api.buildHopyo(target, from, [], { 대상: [{ 로직: "GF000225", 배: 1 }] }); - await api.readHopyoOne({ layer: "system", owner: null }, "HP-000001", 3, target); - await api.guessHopyo(scope, "HP-000001", 3, 4, [ - { 로직: "GF000211", 입력값: {}, 배: 0.1, 몫: 0.1 }, - ]); - await api.readLedger(target); - await api.saveLedger(target, { "가|b|m3": null }); - out.calls = calls.splice(0); - // [로직 추가] 실무 호표 — 확인된 것 = 바로 참조 · 아닌 것 = 확인 창(만들기 길 안 부름) - const picked = []; - const lib = { - layer: "company", - owner: "7", - pick: (ref, name, alias) => picked.push({ ref, name, alias }), - }; - for (const row of input.rows) await pick.pickHopyo(row, lib); - await new Promise((r) => setTimeout(r, 0)); - out.pick = { picked, calls: calls.splice(0), opened }; - process.stdout.write(JSON.stringify(out)); -})().catch((error) => { - process.stderr.write(String(error?.stack ?? error)); - process.exit(1); -}); diff --git a/resources/tester/helper_m02_hopyo_pick.cjs b/resources/tester/helper_m02_hopyo_pick.cjs deleted file mode 100644 index 4f0bdba9f..000000000 --- a/resources/tester/helper_m02_hopyo_pick.cjs +++ /dev/null @@ -1,115 +0,0 @@ -/* M02 일위대가 실무자료(324-4 · 338-3) 헬퍼 — `M02_MasterTemplete_Hopyo_Pick.ts`(고르기 창 「실무 호표」 덧줄) · - * `M02_MasterTemplete_Hopyo_Choose.ts` 글 도우미 · `M02_MasterTemplete_Hopyo_Sheet.ts` 엑셀 보기 창 도우미(329 · 행 → 호표)를 - * 트랜스파일해 Node 로 돌림 · fetch 대역이 부른 길 · 몸을 모음 · - * stdin = {rows, built} · stdout = {calls, kinds, rows, picked, project, view, sheet} JSON. */ -const fs = require("fs"); -const path = require("path"); -const { makeLoader } = require("./helper_ts_stub_loader.cjs"); - -const ROOT = path.join(__dirname, "..", ".."); -const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api" } }; -// 고르기 창 · 자재 창 · 입력 창 · 원문 창은 DOM · 모듈 메타를 씀 — 대역(글 도우미만 시험) -const STUB_FILES = [ - "M02_MasterTemplete_Labor_Pick", - "M02_Material_Pick", - "M02_MasterTemplete_Find_Pick", - "M02_MasterTemplete_Labor_Api", - "M02_MasterTemplete_Labor_Input", - "M01_MasterData_UI_LogicLab_Source", - "M02_MasterTemplete_Hopyo_Left", // 남은 줄 창(729-3 · DOM · 찾기 창) -]; -const stub = new Proxy({}, { get: (_t, k) => (k === "currentLanguageIndex" ? () => 0 : () => {}) }); -const { loadTs } = makeLoader({ - root: ROOT, - requireFor: (full) => (request) => { - if (STUBS[request]) return STUBS[request]; - if (STUB_FILES.some((name) => request.endsWith(name))) return stub; - if (request.startsWith(".") && !request.endsWith(".css")) - return loadTs(`${path.resolve(path.dirname(full), request)}.ts`); - return stub; - }, -}); - -(async () => { - const input = JSON.parse(fs.readFileSync(0, "utf8")); - const calls = []; - global.fetch = async (url, init = {}) => { - calls.push({ - url, - method: init.method ?? "GET", - body: init.body ? JSON.parse(init.body) : null, - }); - const body = url.includes("/hopyo-picks") ? { 줄: input.rows } : input.built; - return { status: 200, ok: true, json: async () => body }; - }; - const M02 = path.join(ROOT, "M02_MasterTemplete"); - const pick = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Pick.ts")); - const picked = []; - const lib = { - layer: "company", - owner: "7", - pick: (ref, name, alias) => picked.push({ ref, name, alias }), - }; - const wrapped = pick.withHopyo(lib); - const rows = (await wrapped.more()).map(({ onPick: _, ...r }) => r); - await pick.pickHopyo(input.rows[0], lib); - const project = pick.withHopyo({ ...lib, layer: "project" }); - const view = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Choose.ts")); - const sheet = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Sheet.ts")); - const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Api.ts")); - await api.readHopyoGrid({ layer: "company", owner: "7" }, "HP-000001"); - const spans = [ - { 번: 3, 이름: "임도표지석", 행: [20, 24] }, - { 번: 4, 이름: "배수관", 행: [25, 30] }, - ]; - const out = { - calls, - kinds: wrapped.kindFilter.map((k) => k.value), - rows, - picked, - project: project.more === undefined && project.kindFilter === undefined, - view: { - target: [ - view.targetText({ 로직: "GF000101", 이름: "구조물터파기", 단위: "㎥" }), - view.targetText({ 자재: "MT1", 이름: "레미콘", 규격: "25-21-120", 단위: "㎥" }), - view.targetText({ 호표: 46, 단위: "㎡" }), - ], - inputs: view.inputsText({ 횟수: "4회사용시", 지역: "서울" }, ["보정작업"]), - }, - basis: (() => { - const hit = { - 조항: ["건설 공통 3-2-1"], - 직종: ["특별인부"], - 단위: true, - 점수: 1.65, - 까닭: "조항 같음 · 직종 특별인부 · 단위 맞음", - }; - const half = { ...hit, 직종: [] }; - const trade = { 출처: [["건설품셈", "3-2-1"]], 직종: ["특별인부", "보통인부"], 행: [1, 1] }; - const plain = { ...trade, 직종: ["보통인부"] }; - return { - ok: [hit, half, { ...hit, 단위: false }, { ...hit, 까닭: "앞 마디 같음" }, undefined].map( - (b) => view.basisOk(b, trade), - ), - // 엑셀 직종이 보통인부뿐 · 없음(기계) = 직종 안 봄 - plain: [view.basisOk(half, plain), view.basisOk(half, { ...trade, 직종: [] })], - tag: [hit, half, undefined].map(view.basisTag), - source: [ - view.sourceText({ 출처: [["건설표준품셈", "3-2-1"]], 직종: ["특별인부"], 행: [93, 101] }), - view.sourceText({ 출처: [], 직종: [], 행: [7, 7] }), - view.sourceText(undefined), - ], - }; - })(), - sheet: { - at: [19, 20, 22, 24, 25, 30, 31].map((row) => sheet.hopyoAtRow(spans, row)), - span: sheet.spanText([20, 24], sheet.lastColumn({ 칸: { A1: {}, N24: {} }, 열: { O: {} } })), - last: [sheet.lastColumn({ 칸: { Z9: {}, AA1: {} } }), sheet.lastColumn({ 칸: {} })], - rows: [sheet.rowsText(4345), sheet.rowsText([93, 101])], - }, - }; - process.stdout.write(JSON.stringify(out)); -})().catch((error) => { - process.stderr.write(String(error?.stack ?? error)); - process.exit(1); -}); diff --git a/resources/tester/helper_m02_hopyo_seam.cjs b/resources/tester/helper_m02_hopyo_seam.cjs deleted file mode 100644 index 2970532c3..000000000 --- a/resources/tester/helper_m02_hopyo_seam.cjs +++ /dev/null @@ -1,58 +0,0 @@ -/* M02 일위대가 실무자료 이음매(329 · 330) 헬퍼 — 실제 서버 응답(격자 · 호표 맞춤)을 - * `M02_MasterTemplete_Hopyo_Sheet.ts`(행 → 호표 · 영역 A1) · `M02_MasterTemplete_Hopyo_Choose.ts` - * (근거 꼬리표 · 엑셀 근거 줄 · 근거 맞음)에 먹임 · - * stdin = {grid, lines, all} · stdout = {at, span, rows, lines: [{이름, source, tag, ok}], - * count: {top}} JSON — count = 「후보」 줄 1순위 근거 맞음. */ -const fs = require("fs"); -const path = require("path"); -const { makeLoader } = require("./helper_ts_stub_loader.cjs"); - -const ROOT = path.join(__dirname, "..", ".."); -const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api" } }; -const STUB_FILES = [ - "M02_MasterTemplete_Labor_Pick", - "M02_Material_Pick", - "M02_MasterTemplete_Find_Pick", - "M02_MasterTemplete_Labor_Api", - "M02_MasterTemplete_Labor_Input", - "M01_MasterData_UI_LogicLab_Source", - "M02_MasterTemplete_Hopyo_Left", // 남은 줄 창(729-3 · DOM · 찾기 창) -]; -const stub = new Proxy({}, { get: (_t, k) => (k === "currentLanguageIndex" ? () => 0 : () => {}) }); -const { loadTs } = makeLoader({ - root: ROOT, - requireFor: (full) => (request) => { - if (STUBS[request]) return STUBS[request]; - if (STUB_FILES.some((name) => request.endsWith(name))) return stub; - if (request.startsWith(".") && !request.endsWith(".css")) - return loadTs(`${path.resolve(path.dirname(full), request)}.ts`); - return stub; - }, -}); - -const input = JSON.parse(fs.readFileSync(0, "utf8")); -const M02 = path.join(ROOT, "M02_MasterTemplete"); -const sheet = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Sheet.ts")); -const view = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Choose.ts")); -const { grid } = input; -const ilwi = grid.책.시트.find((s) => s.이름 === "일위대가"); -const three = grid.호표.find((h) => h.번 === 3); -const out = { - at: [19, 20, 22, 24, 25, 26].map((row) => sheet.hopyoAtRow(grid.호표, row)), - span: sheet.spanText(three.행, sheet.lastColumn(ilwi)), - rows: sheet.rowsText(grid.단산.find((b) => b.번 === 93).행), - lines: input.lines.map((line) => { - const top = (line.맞춤.후보 ?? [])[0]; - return { - 이름: line.이름, - source: view.sourceText(line.근거), - tag: view.basisTag(top?.근거), - ok: view.basisOk(top?.근거, line.근거), - }; - }), -}; -const open = (input.all ?? []).filter((line) => line.맞춤.상태 === "후보"); -out.count = { - top: open.filter((line) => view.basisOk((line.맞춤.후보 ?? [])[0]?.근거, line.근거)).length, -}; -process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_627_sheet_sk.py b/resources/tester/test_627_sheet_sk.py index 63b27d9ce..a96abb845 100644 --- a/resources/tester/test_627_sheet_sk.py +++ b/resources/tester/test_627_sheet_sk.py @@ -2,7 +2,7 @@ 서버 없이 잡음: 탭(역할) 따라 판 바꿈(일위대가표 = 호표 목록 · 단가산출 탭 = 단가산출 목록 · 칸은 처음 한 번만 지음) · 시트 행 → 목록 강조 · 목록 줄 → 지금 탭의 블록 자리 · 만들 층 ≠ 파일 층 = 보기만 · 거르기(전체 · 안 · 밖 · 고를 줄 있음) · -줄 0 블록 = 「줄 없음」 확인 막음 · 마법사 칸과 같은 캐시 열쇠(두 벌 고름 맞춤) · [끝내기] = 막지 않고 물음창 글만 · +[끝내기] = 막지 않고 물음창 글만 · 화면 = 부품 상자만 · 700 줄. """ @@ -14,7 +14,6 @@ from pathlib import Path HERE = os.path.dirname(__file__) M02 = Path(__file__).resolve().parents[2] / "M02_MasterTemplete" -from test_778_5_sk_pick_screen import BLOCKS as sys_blocks # noqa: E402 def _node(payload: dict) -> dict: @@ -71,24 +70,11 @@ def test_다른_층_권한_없음_보기만(): assert got["blocks"][0]["readOnly"] is True -def test_거르기_줄0_막음_끝내기_글(): - got = _node({"blocks": sys_blocks}) - assert got["empty"] == {"판정": "줄 없음", "켜짐": False, "확인": [], "몸": []} - assert got["filter"] == {"all": [2, 5, 9, 7], "in": [2, 7], "out": [5, 9], "open": [2, 9]} - assert got["old"] == 0 # 옛 서버(부름 없음) = 모두 일위대가 안 - assert got["wait2"] == {"블록": 3, "밖": 2, "줄": 3} # 만든 블록 7 뺌 · 줄 0 블록 = 고를 줄 1 - - def _src(name: str) -> str: return (M02 / f"M02_MasterTemplete_Hopyo_{name}.ts").read_text(encoding="utf-8") def test_캐시_공유_알림_칠_다시(): - wiz = _src("WizardSk") - assert wiz.count("skDraftKey(scope, key)") == 1 # 두 벌 모두 같은 열쇠(층 · 주인 · 파일 키) - assert "SK_SYNC" in wiz and "tell({ got: next })" in wiz # 옮김 = 받은 판정 그대로 다른 벌에 - assert wiz.count("tell();") == 2 # [확인] · 줄 고름 바뀜 - assert "new Event(HOPYO_BUILT)" in wiz # 옮김 뒤 열린 엑셀 보기 칠 다시 sheet = _src("Sheet") assert "createSheetSk(" in sheet and "sk?.at(s);" in sheet side = _src("SheetSk") @@ -96,17 +82,17 @@ def test_캐시_공유_알림_칠_다시(): nav = _src("SheetNav") assert "blockAt(sheet: string, row: number)" in nav and "spotOf(no: number, role: Role)" in nav wizard = _src("Import") - assert 'L("M02_SkUnmoved")' in wizard and "finish.disabled = !!n?.줄;" in wizard # 밖 = 안 막음 + assert 'L("M02_SkUnmoved")' in wizard and "leftCount" not in wizard # 밖 = 안 막음 · 옛 남은 줄 막음 걷음 tone = (M02 / "logic" / "M02_MasterTemplete_Hopyo_SheetMap.ts").read_text(encoding="utf-8") assert '글: "일위대가 밖"' in tone def test_부품_상자만_700줄(): - for name in ("SheetSk", "Sheet", "SheetMake", "SheetNav", "WizardSk", "Import", "Side"): + for name in ("SheetSk", "Sheet", "SheetNav", "Import", "Side"): src = _src(name) assert len(src.splitlines()) <= 700, name assert not re.search(r"#[0-9a-fA-F]{3,6}\b|rgb\(", src), name - for name in ("SheetSk", "WizardSk", "Import"): + for name in ("SheetSk", "Import"): assert not re.search(r'el\("(button|input|select)"| opt.ref?.(x.ref, x.제목 || x.이름))" in find - assert "sangeun: true," in _text( - M02 / "M02_MasterTemplete_Hopyo_Pick.ts" - ) # 일위대가 [로직 추가] → 찾아 넣기 창 + assert "sangeun: true }" in _text( + M02 / "M02_MasterTemplete_Labor_Pick.ts" + ) # 일위대가 [로직 추가] → 찾아 넣기 창(785-7 `withSangeun`) store_ts = _text(ROOT / "B09_Estimation" / "B09_Estimation_UI_Store.ts") assert '["sangeun", "단가산출"],' in store_ts and "sangeun: sangeunItems," in store_ts # 더한줄 고르기 — 715 이음 꼴 셋(인력 · 재료 = 요소 키) diff --git a/resources/tester/test_720_m02_folder_layer.py b/resources/tester/test_720_m02_folder_layer.py index d8b759f7e..112a5c7e6 100644 --- a/resources/tester/test_720_m02_folder_layer.py +++ b/resources/tester/test_720_m02_folder_layer.py @@ -89,7 +89,7 @@ def test_구조물_조합_층_제자리_바꿈(): assert "topHost.replaceChildren(" in build and "treeHost.replaceChildren(" in build assert "tree: [treeHost]," in page and "top: topHost," in page assert "() => isAdmin," in page and 'isSystem: user?.role === "SYSTEM_ADMIN",' in page - assert "main.root.isConnected ? void afterItem()" in page # 나무를 갈아도 듣기 그대로 + assert "afterItem" not in page # 785-7 [구조물 조합으로](옛 확인 창 몫) 듣기 걷음 side = _text("M02_MasterTemplete_UI_Side.ts") assert "const drawing = section(" in side and "practice" not in side # 732-1 # 도면 양식 칸 = 누구나 · 단추만 관리자(720-4 덧) diff --git a/resources/tester/test_721_find_pick.py b/resources/tester/test_721_find_pick.py index c2eb4be8b..11263c483 100644 --- a/resources/tester/test_721_find_pick.py +++ b/resources/tester/test_721_find_pick.py @@ -44,7 +44,7 @@ def test_721_2_부르는_곳_셋만_새_창(): assert "logicOnly" not in _text(M02 / "M02_MasterTemplete_Sangeun_Detail.ts") # 727-2 assert "logicOnly" not in find assert "openFindPicker({" in _text(M02 / "M02_MasterTemplete_Sangeun_Pick.ts") - assert "sangeun: true," in _text(M02 / "M02_MasterTemplete_Hopyo_Pick.ts") # 일위대가 + assert "sangeun: true }" in labor_pick # 일위대가(785-7 `withSangeun`) # 옛 창 그대로 — B09 단가 연결은 sangeun 없이 부름(736-1 불러오기 · 별칭 장부는 찾아 넣기 창으로) for f in ( ROOT / "B09_Estimation" / "B09_Estimation_UI_Link.ts", @@ -52,7 +52,7 @@ def test_721_2_부르는_곳_셋만_새_창(): ): text = _text(f) assert "openLogicPicker(" in text and "sangeun:" not in text, f.name - for f in ("Hopyo_Choose", "Hopyo_Ledger"): + for f in ("Hopyo_Ledger",): text = _text(M02 / f"M02_MasterTemplete_{f}.ts") assert "openFindPicker({" in text and "openLogicPicker" not in text.split("*/", 1)[1], f # 탭 셋 · 찾기 칸 하나 · 거름 고르기 없음 · M01 = 로직 · 인력 · 재료(기계 · 호표 없음) @@ -126,11 +126,7 @@ def test_736_1_항목_추가_찾기_창_하나(): find = _text(M02 / "M02_MasterTemplete_Find_Pick.ts") assert 'kind === "재료" && opt.material' in find and "? opt.material" in find assert '"logic" | "labor" | "material" | "machine" | "oil" | "sk" | "pre"' in find # 785-3 oil - # 불러오기 엑셀 연결 = 기초(자재 아닌 줄) · 재료(자재 · 재료비 줄) · 별칭 장부 = 기초 · 재료 둘 - choose = _text(M02 / "M02_MasterTemplete_Hopyo_Choose.ts") - assert "다른 로직 검색" not in choose and "다른 자재 검색" not in choose - assert 'MATERIAL.has(line.갈래) ? [] : (["logic"] as const)' in choose - assert 'canPickMaterial(line) ? (["material"] as const) : []' in choose + # 별칭 장부 = 기초 · 재료 둘(옛 확인 창 고르기는 785-7 에서 지움) assert 'kinds: ["logic", "material"],' in _text(M02 / "M02_MasterTemplete_Hopyo_Ledger.ts") @@ -143,14 +139,6 @@ def test_736_2_빠진_길_둘_신규_자재_기계_탭(): assert 'opt.machine && ok("machine")' in find pick = _text(M02 / "M02_Material_Pick.ts") assert "export function openNewMaterial(" in pick and "{ owner }," in pick - choose = _text(M02 / "M02_MasterTemplete_Hopyo_Choose.ts") - assert 'line.갈래 === "노임"' in choose and '["labor"]' in choose and '["machine"]' in choose - assert "element: (e) => opt.done([{ 노임: e.코드, 배: 1 }])," in choose - assert "machine: (e) => opt.done([{ 기계: e.코드, 배: 1 }])," in choose - left = _text(M02 / "M02_MasterTemplete_Hopyo_Left.ts") - assert "kinds: kindsOf(line)" in left and "pickMachine" not in left - lost = _text(M02 / "M02_MasterTemplete_Hopyo_Lost.ts") - assert '"machine"' in lost and "pickMachine" not in lost unlinked = _text(M02 / "M02_MasterTemplete_Labor_Unlinked.ts") assert 'machine: (e) => done("기계", e.코드)' in unlinked and "pickMachine" not in unlinked diff --git a/resources/tester/test_729_3_hopyo_left_screen.py b/resources/tester/test_729_3_hopyo_left_screen.py deleted file mode 100644 index 07c1cc8c9..000000000 --- a/resources/tester/test_729_3_hopyo_left_screen.py +++ /dev/null @@ -1,138 +0,0 @@ -"""일위대가 불러오기 마법사 안 이은 줄 화면(PLAN 729-3 · 요구서 내역19 1장 8 · 2장 4). - -헬퍼(`helper_729_3_hopyo_left.cjs` · fetch · sessionStorage 대역)로 진짜 TS 를 돌림 — -만들기 409 `{까닭, 남은줄}` = StaleError 까닭 글 + 남은 줄 · 남은 수 = 안 만든 호표만(만든 호표는 안 물음) · -줄마다 고름(노임 · 기계 · 비율 · 우리에 없음 · 로직) = 확인 창 캐시 → 만들기 몸 그대로 · 0 될 때까지 · -일위대가표 안 이은 고정 줄(비율 · 이음 있는 줄 뺌) · 고름 대기 후보 글. -소스: [끝내기] = 남은 줄 다시 셈 → 있으면 막음(띠 · 표) · 단가산출 칸 · 목록 열 · 화면 요소 = 부품 상자 · 700 줄. -""" - -import json -import os -import re -import subprocess -from pathlib import Path - -HERE = os.path.dirname(os.path.abspath(__file__)) -M02 = Path(HERE).parents[1] / "M02_MasterTemplete" - - -def _line(no: int, kind: str = "자재", state: str = "후보") -> dict: - match = {"열쇠": f"줄{no}", "상태": state, "체크": False, "대상": []} - return {"차례": no, "이름": f"줄{no}", "규격": "", "수량": 1, "단위": "㎥", "갈래": kind, - "단가": {}, "금액": {}, "비고": "", "맞춤": match} # fmt: skip - - -def _left(no: int, kind: str) -> dict: - return {"차례": no, "이름": f"줄{no}", "규격": "", "단위": "㎥", "수량": 1, "갈래": kind, - "까닭": "이을 것 없음", "후보": []} # fmt: skip - - -LINES = [_line(0, "노임"), _line(1, "기계"), _line(2, "%", "%"), _line(3), _line(4, "단가산출")] -LEFT = [_left(0, "노임"), _left(1, "기계"), _left(2, "%"), _left(3, "자재"), _left(4, "단가산출")] -FIXES = [ - {"고정": {"이름": "안이음", "단가": {}, "이음": {"노임": None, "자재": None, "기계": None}}}, - { - "고정": { - "이름": "이음", - "단가": {}, - "이음": {"노임": "LB000002", "자재": None, "기계": None}, - } - }, - {"고정": {"이름": "비율", "단가": {}, "비율": {"기준": "노무비", "비목": "경비"}}}, - {"로직": "GF000113"}, -] - - -def _run() -> dict: - payload = { - "one": {"키": "HP-000001", "파일": "북부청.xls", "호표": {"이름": "x", "줄": LINES}}, - "made": [1], - "left": LEFT, - "fixes": FIXES, - } - proc = subprocess.run( - ["node", os.path.join(HERE, "helper_729_3_hopyo_left.cjs")], - input=json.dumps(payload, ensure_ascii=False), - capture_output=True, - text=True, - encoding="utf-8", - check=False, - ) - assert proc.returncode == 0, proc.stderr - return json.loads(proc.stdout) - - -def test_남은_줄_고름_몸_막힘_판정(): - got = _run() - # 만들기 409 = StaleError · 까닭 글 · 남은 줄 그대로 - assert got["stale"]["name"] == "StaleError" and got["stale"]["message"] == "남음" - assert got["stale"]["left"] == LEFT - # 만든 호표(1) 는 안 물음 · 안 만든 호표(2) 만 남은 줄 - assert got["before"] == {"줄": 5, "호표": 1} - assert all(u.endswith("/layers/personal/hopyo-unlinked") for u in got["unlinkedCalls"]) - assert len(got["unlinkedCalls"]) == 3 # 셈 세 번 × 안 만든 호표 하나 - assert got["half"] == {"줄": 1, "호표": 1} - assert got["after"] == {"줄": 0, "호표": 0} - # 고른 것 = 만들기 몸(서버 이음 대상 꼴 · 입력값 없음 · 배 1) - body = {x["차례"]: x for x in got["body"]} - assert body[0] == {"차례": 0, "대상": [{"노임": "LB000002", "배": 1}]} - assert body[1] == {"차례": 1, "대상": [{"기계": "EQ000069", "배": 1}]} - assert body[2] == {"차례": 2, "대상": [{"비율": "노무비", "배": 1}]} # % 줄 = 밑수 고른 것만 - assert body[3] == {"차례": 3, "뺌": "우리에 없음"} - assert body[4] == {"차례": 4, "대상": [{"로직": "GF000113", "입력값": {}, "배": 1}]} - assert got["lastBody"]["줄"] == got["body"] and got["lastBody"]["번"] == 2 - # 일위대가표 띠 = 이음 · 비율 없는 고정 줄만 - assert got["fixes"] == ["안이음"] - - -def _text(name: str) -> str: - return (M02 / name).read_text(encoding="utf-8") - - -def test_불러오기_끝내기_막음_단가산출_칸_목록_열(): - wizard = _text("M02_MasterTemplete_Hopyo_Import.ts") - end = wizard.split("async function end()")[1] - assert end.index("await recount()") < end.index("showConfirmDialog(") # 끝내기 전 다시 셈 - assert "if (leftCount(got).줄)" in end and "openLeft(" in end # 남으면 막음 · 표 - assert "finish.disabled = !!n?.줄;" in wizard # 배지 · 남은 수 · [끝내기] 막음 - assert ( - "body?.skCount()" in wizard and "/sangeun`" not in wizard - ) # 단가산출 칸 = 엑셀 보기 오른쪽 판 - # 785-4 흠 — [안 이은 줄 고르기] · [끝내기] 모두 엑셀 보기가 읽은 호표 번으로 셈(빈 번 「0」 오표시 없음) - pick = wizard.split("async function openPick()")[1].split("async function ensureNos")[0] - assert "await ensureNos();" in pick and "openLeft(" in pick - assert "nos = got.호표.map((h) => h.번)" in wizard - assert "ensureNos" in wizard.split("async function recount()")[1] - sk = _text( - "M02_MasterTemplete_Hopyo_WizardSk.ts" - ) # 778-5 D-5 — 줄 고름 · 확인한 것만(옛 기초 고름 걷음) - assert "judgeSangeun(scope, key, body)" in sk - lost = _text("M02_MasterTemplete_Hopyo_Lost.ts") # 옛 못옮김 창 = 보기만(옛 `바탕` 몸 걷음) - assert "makeSangeun" not in lost and "loadLinks" not in lost - assert "row.못옮김?.length && row.단산 !== undefined && row.SK" in _text( - "M02_MasterTemplete_Hopyo_Left.ts" - ) - left = _text( - "M02_MasterTemplete_Hopyo_Left.ts" - ) # 778-5 D-3 — 기초 고르기 · SK 만들기 걷고 걸음 이동 - assert "makeSangeun" not in left and 'L("M02_LeftSkGo")' in left and "at.onSk" in left - lib = _text("M02_MasterTemplete_Library_List.ts") - assert 'L("M02_LibUnlinked"), (r) => (r.안이은줄' in lib - assert "unlinkedBar(doc, edit)" in _text("M02_MasterTemplete_Labor_Grid.ts") - assert "unlinkedBar(cur, edit &&" in _text("M02_MasterTemplete_Labor_Table.ts") # 일위대가표 띠 - left = _text("M02_MasterTemplete_Hopyo_Left.ts") - # 일반 사용자 길 — 기계 고름은 찾아 넣기 창(736-2)이 `/logic-lib/elements` 로 - assert "M01_MasterData_UI_Logic_Api" not in left - assert "/logic-lib/elements?group=" in _text("M02_MasterTemplete_Find_Pick.ts") - for name in ( - "M02_MasterTemplete_Hopyo_Left.ts", - "M02_MasterTemplete_Hopyo_WizardSk.ts", - "M02_MasterTemplete_Hopyo_Import.ts", - "M02_MasterTemplete_Labor_Unlinked.ts", - "M02_MasterTemplete_Hopyo_Lost.ts", - ): - src = _text(name) - assert len(src.splitlines()) <= 700, name - assert 'el("button"' not in src and 'el("input"' not in src, name # 부품 상자만 - assert not re.search(r"#[0-9a-fA-F]{3,6}|rgb\(", src), name # 색 = 공통 값 diff --git a/resources/tester/test_739_1_alias_central.py b/resources/tester/test_739_1_alias_central.py index 7e04a28e5..cc774367e 100644 --- a/resources/tester/test_739_1_alias_central.py +++ b/resources/tester/test_739_1_alias_central.py @@ -83,18 +83,3 @@ def _lines(client, no): def _auto(client, no): """장부가 자동으로 정한 줄 수.""" return sum(1 for m in _lines(client, no).values() if m["상태"] in FROM_LEDGER) - - -def test_화면_내_제안_글(): - """고르기 창 맨 위 「내 제안」(한 번 눌러 짝 통째) · 장부 화면 = 내 제안 목록 · 승인 칸.""" - from pathlib import Path - - m02 = Path(__file__).resolve().parents[2] / "M02_MasterTemplete" - choose = (m02 / "M02_MasterTemplete_Hopyo_Choose.ts").read_text(encoding="utf-8") - assert choose.index("...(mineRow ? [mineRow] : [])") < choose.index("...sections") - assert '"data-group": "내 제안"' in choose and "mine.짝.map((p)" in choose - rows = (m02 / "M02_MasterTemplete_Hopyo_CheckRows.ts").read_text(encoding="utf-8") - assert '"고르기 · 내 제안"' in rows - book = (m02 / "M02_MasterTemplete_Hopyo_Ledger.ts").read_text(encoding="utf-8") - assert 'MINE_TITLE = "내 제안 목록"' in book and '"승인",' in book - assert "createBadge(r.상태, STATE_TONE[r.상태])" in book diff --git a/resources/tester/test_746_2_hopyo_compare_screen.py b/resources/tester/test_746_2_hopyo_compare_screen.py deleted file mode 100644 index c9cbb3bdf..000000000 --- a/resources/tester/test_746_2_hopyo_compare_screen.py +++ /dev/null @@ -1,33 +0,0 @@ -"""746-2 지킴 — 불러오기 고르기 창 후보 블록 줄 비교 · 요소 후보 화면(요구 585). - -서버 없이 소스로 잡음: 안 이은 줄 창이 요소 후보 단추를 붙임(고름 대기 줄 비교 · 못옮김 잇기는 778-5 D-5 줄 고름으로 걷음) · -안 이은 줄 창의 기초 후보 칸에 요소 후보(`키`)가 섞여 들어가지 않음 · 서버 응답 칸 이름 · 부품 상자만 · 700 줄. -""" - -import re -from pathlib import Path - -M02 = Path(__file__).resolve().parents[2] / "M02_MasterTemplete" - - -def _src(name: str) -> str: - return (M02 / f"M02_MasterTemplete_Hopyo_{name}.ts").read_text(encoding="utf-8") - - -def test_안_이은_줄_창이_요소_후보_단추를_붙임(): - left = _src("Left") - assert "elemChoices(row.후보.filter(isElem)" in left - assert "SangeunCand" not in left # 778-5 D-3 — 기초 고르기 걷음 - - -def test_서버_응답_칸_이름_그대로(): - api = _src("Api") - assert "못옮김: SangeunLost[]" in api and "export interface ElemCand" in api - assert "SangeunCompare" not in api and "export function compareView" not in _src("Compare") - - -def test_부품_상자만_날_요소_없음_700줄_안(): - src = _src("Compare") - assert not re.search(r'createElement\("(button|input)"| dict: - proc = subprocess.run( - ["node", os.path.join(HERE, "helper_778_5_sk_pick.cjs")], - input=json.dumps({"blocks": BLOCKS}, ensure_ascii=False), - capture_output=True, - text=True, - encoding="utf-8", - check=False, - ) - assert proc.returncode == 0, proc.stderr - return json.loads(proc.stdout) - - -def test_판정_확인_켜짐_조건(): - got = _run() - assert got["start"] == [ - {"번": 2, "판정": "고를 줄 2", "확인켜짐": False}, - {"번": 5, "판정": "옮길 수 있음", "확인켜짐": True}, - {"번": 7, "판정": "고를 줄 1", "확인켜짐": False}, - ] - assert got["sureBlocked"] == [] # 고를 줄 남은 블록 = [확인] 안 켜짐 - assert got["half"] == {"판정": "고를 줄 2", "켜짐": False} # 이어진 줄 바꿈은 고를 줄 안 줄임 - assert got["full"] == {"판정": "옮길 수 있음", "켜짐": True} - assert got["sure"] == [2, 5] # 하위 못 이음 블록 7 = 안 켜짐 - - -def test_줄_표_꼴_내가_고름이_덮음(): - view = _run()["view"] - assert view[0] == {"상태": "이어짐", "꼬리표": [], "글": "", "mine": False} - assert view[1] == {"상태": "이어짐", "꼬리표": ["내가 고름"], "글": "LB000099", "mine": True} - assert view[2]["상태"] == "이어짐" and view[2]["글"] == "MT000123" - assert view[3] == { - "상태": "뺌", - "꼬리표": ["내가 고름"], - "글": "뺌 · 우리에 없음", - "mine": True, - } - assert view[4]["상태"] == "밖" - - -def test_몸_꼴_이음_줄_자동_확인_없음(): - got = _run() - assert got["emptyBody"] == [] # 누른 것 없음 = 몸 비움 - picks = {"1": {"노임": "LB000099"}, "2": {"자재": "MT000123"}, "3": {"뺌": "우리에 없음"}} - assert got["body"] == [{"번": 2, "고름": picks}, {"번": 5}] - first, second = got["calls"] - assert first["method"] == "POST" and first["body"] == {"확인": []} # 판정만 - assert first["url"].endswith("/layers/personal/hopyo/HP-000001/sangeun") - assert second["body"] == {"확인": got["body"]} - assert got["wait"] == { - "블록": 3, - "밖": 0, - "줄": 1, - } # 안 옮긴 블록 셋 · 블록 7 하위 줄 · 옛 서버 = 밖 없음 - assert got["tidy"] == [5] # 고름 지우면 그 블록 확인 빠짐 - assert got["round"]["확인"] == [2, 5] and got["bad"] == {"고름": {}, "확인": []} - - -def test_후보_고름_꼴(): - cand = _run()["cand"] - assert cand["L"] == {"노임": "LB000001"} and cand["X"] == {"기계": "EQ000019"} - assert cand["M"] == {"자재": "MT000001"} and cand["R"] == {"기준": "노무비", "비목": "경비"} - assert cand["Mbad"] is None and cand["text"] == "합판 12mm [장]" - - -def _src(name: str) -> str: - return (M02 / f"M02_MasterTemplete_Hopyo_{name}.ts").read_text(encoding="utf-8") - - -def test_화면_자동_없음_부품_상자만(): - wiz = _src("WizardSk") - assert "judgeSangeun(scope, key)" in wiz # 열 때 = 판정만 - assert "judgeSangeun(scope, key, body)" in wiz and "sureBody(load(), got.판정)" in wiz - assert "모두" not in wiz.split("*/", 1)[1] and "setSure(" in wiz # 확인은 블록마다만 - assert 'kind: "chip"' in wiz and "disabled: !ok" in wiz # [확인] 칩 · 고를 줄 0 일 때만 - assert "단산행: 행" in wiz # 엑셀 보기 뛰기 - lines = _src("SkLines") - assert "lineFind(" in lines and "unitTone(" in lines # 찾기 창 · 엑셀 칠과 한 규칙 - wizard = _src("Import") - assert 'L("M02_SkLeft")' in wizard and "body?.skCount()" in wizard # [끝내기] 막힘 글 - for name in ("WizardSk", "SkLines", "SkApi", "Lost", "Compare"): - src = _src(name) - assert len(src.splitlines()) <= 700, name - assert not re.search(r'el\("(button|input|select)"| h.번)" in imp - assert "ensureNos" in imp.split("async function recount()")[1] - # ② 「맞춤 셈 중」(409) = 3 초마다 저절로 다시 판정 - sk = _src("WizardSk") - assert "error instanceof StaleError" in sk and "void judge()" in sk and "POLL_MS" in sk - # ③ 좁은 판 줄 짓눌림 — [줄 n] = 시트 아래 줄 판(넓은 줄 표) · 판 안 펼침 없음 - assert "skLineTable({" in sk.split("function drawLines()")[1] - assert "is-sub" not in sk and "o.narrow" not in sk +def test_줄_판_자리_좁은_폭(): + # 셈 중(블록 풀이) = 3 초마다 다시 읽음 · 줄 판 = 시트 아래 넓은 자리 · 좁은 폭 = 줄 판 높이 바닥 + assert "POLL_MS" in _src("SheetSk") and "POLL_MS" in _src("Sheet") sheet = _src("Sheet") assert 'attrs: { "data-hopyo-lines": "" }' in sheet and "openLines," in sheet assert "openLines: (make: (host: HTMLElement)" in _src("SheetSk") # 785-6 줄 판 자리 한 손잡이 + css = (M02 / "M02_MasterTemplete_Hopyo.css").read_text(encoding="utf-8") + narrow = css.split("/* 좁은 폭(785-7 · 390)")[1].split("/*")[0] + assert ".m02-hopyo-sheet__lines:not([hidden])" in narrow and "min-height: 16rem;" in narrow def test_끝내기_머리_막대_끝냄_뒤_빈_본_칸(): imp = _src("Import") end = imp.split("async function end()")[1] - assert end.index("await recount()") < end.index("showConfirmDialog(") + assert "unmoved()" in end.split("showConfirmDialog(")[0] # 확인하고 안 옮긴 수 = 물음 글만(막지 않음) assert "deleteHopyo(scope, file.키)" in end and "a.onEnded();" in end side = _src("Side") assert "onEnded: () => {" in side and "void refresh();" in side @@ -73,19 +66,19 @@ def test_끝내기_머리_막대_끝냄_뒤_빈_본_칸(): def test_부품_상자만_700줄_늘리지_않음(): - for name in ("Import", "Side", "Sheet", "SheetMake", "SheetSk", "WizardSk"): + for name in ("Import", "Side", "Sheet", "SheetSk"): src = _src(name) assert len(src.splitlines()) <= 700, name assert not re.search(r"#[0-9a-fA-F]{3,6}\b|rgb\(", src), name # 색 = 공통 값 - for name in ("Import", "SheetMake", "SheetSk", "WizardSk"): + for name in ("Import", "SheetSk"): assert not re.search(r'el\("(button|input|select)"| str: return (M02 / name).read_text(encoding="utf-8") -def _node(helper: str, payload: dict) -> dict: - proc = subprocess.run( - ["node", os.path.join(HERE, helper)], - input=json.dumps(payload, ensure_ascii=False), - capture_output=True, - text=True, - encoding="utf-8", - check=False, - ) - assert proc.returncode == 0, proc.stderr - return json.loads(proc.stdout) - - -def _line(no, state, check, parts, **extra): - match = {"열쇠": f"줄{no}||m3", "상태": state, "체크": check, "대상": parts, **extra} - return {"차례": no, "이름": f"줄{no}", "규격": "", "수량": 1, "단위": "㎥", "갈래": "단가산출", - "맞춤": match} # fmt: skip - - -def _check() -> dict: - lines = [ - _line(0, "후보", False, [{"자재": "MT004258", "입력값": {}, "배": 1}]), - _line(1, "맞음", True, [SURE], 품=QTY), - _line(2, "별칭", True, [{"로직": "GF000101", "입력값": {}, "배": 1}]), - _line(3, "다른 점", False, [{"로직": "GF000103", "입력값": {}, "배": 1}], 품=QTY_MANY), - _line(4, "후보", False, []), - _line(5, "%", False, []), - _line(6, "호표", True, [{"호표": 46, "입력값": {}, "배": 2.3}]), - _line(7, "노임", False, [{"로직": "GF000300", "입력값": {}, "배": None}]), - ] - ledger = [ - {"열쇠": "가|b|m3", "원래": {"품명": "가", "규격": "b", "단위": "m3"}, "갈래": "짝", - "짝": [{"로직": "GC000198", "입력값": {}, "배": 1, "몫": None}], "층": "personal", - "주인": "7/42", "고침": True}, - {"열쇠": "나||m3", "원래": {"품명": "나", "규격": "", "단위": "m3"}, "갈래": "짝", - "짝": [{"로직": "GF000101", "입력값": {}, "배": 1, "몫": None}], "층": "personal", - "주인": "7/42", "고침": True}, - {"열쇠": "다||개", "원래": {"품명": "다", "규격": "", "단위": "개"}, "갈래": "짝", - "짝": [{"자재": "MT1", "입력값": {}, "배": 1, "몫": None}], "층": "system", - "주인": None, "고침": False}, - ] # fmt: skip - rows = [ - {**ROW, "확인": {"층": "company", "주인": "7", "키": "SC-000009"}}, - {**ROW, "번": 4, "이름": "배수관", "확인": None}, - ] - return _node("helper_m02_hopyo_check.cjs", {"lines": lines, "ledger": ledger, "rows": rows}) - - -def test_확인_창_체크한_줄만_몸(): - got = _check()["check"] - assert got["key"] == "m02-hopyo-check:personal::HP-000001:3" - assert got["suggested"] == [ # 서버 체크 제안 그대로 · 배 없는 줄 · % 줄 · 후보는 안 감 - {"차례": 1, "대상": [{"로직": "GC000198", "입력값": {"소량감률": 50}, "배": 1}]}, - {"차례": 2, "대상": [{"로직": "GF000101", "입력값": {}, "배": 1}]}, - {"차례": 6, "대상": [{"호표": 46, "배": 2.3}]}, - ] - assert got["edited"] == [ # 캐시 — 맞음 줄 체크 끔 · 뺌 · 고른 대상(몫 · 배) - {"차례": 2, "대상": [{"로직": "GF000101", "입력값": {}, "배": 1}]}, - {"차례": 3, "뺌": "우리에 없음"}, - { - "차례": 4, - "대상": [{"로직": "GF000211", "입력값": {"유형": "보통"}, "배": 0.1, "몫": 0.1}], - }, - {"차례": 6, "대상": [{"호표": 46, "배": 2.3}]}, - ] - assert got["leftOut"] == {"확인 안 함": 3, "뺌": 1, "% 줄": 1} - assert got["checkable"] == [True, True, True, True, False, False, True, False] - assert got["sure"] == [1, 2] # [맞음 · 별칭 일괄 체크] - assert got["compare"] == "콘크리트공 0.1091 → 0.1091 ✓ · 굴착기 0.29 → 0.31 ≠" - assert got["compareMany"] == "굴착기 0.1594 → 0.0889 ≠" # 같은 장비 여러 줄 = 합쳐 한 줄 - assert got["toTarget"] == [ - {"호표": 46, "배": 2.3}, - {"자재": "MT1", "배": 1}, - {"전국": True, "배": 1}, # 361-1 — 키 없음 · 이름은 서버가 - ] - assert got["newest"] == [False, True, True] # 같은 줄 늦은 응답 버림 · 다른 줄은 따로 - assert got["dropped"] is False # 만들기 성공 = 캐시 지움 - - -def test_고르기_창_도우미(): - got = _check()["choose"] - assert got["hint"] == ["레미콘타설", "고임돌"] # 품명 첫 뜻 낱말(358-1 · 조항 번호 아님) - assert got["groups"]["groups"] == [ - ["1위", False, ["A"]], - ["출처 같음", False, ["C"]], - ["이름 닮은 로직", False, ["E"]], - ["숫자만 닮음", True, ["D", "B"]], # 뜻 낱말 안 겹치는 「후보」 = 끝 - ] - assert got["groups"]["text"][:2] == ["되메우기 · A · 산림", "되메우기 · C · 건설"] - assert got["groups"]["material"] == ["1위", "그 밖 후보"] - nat = got["groups"]["national"] # 361-1 - assert nat["groups"] == [["1위", False, ["전국"]], ["지역 변형", True, ["M1", "M2"]]] - assert nat["text"] == "레미콘(전국)-관급 25-21-120 [㎥] · 키 없음" - assert nat["region"] == [{"자재": "M2", "입력값": {}, "배": 1, "몫": None}] - assert nat["back"] == [{"전국": True, "입력값": {}, "배": 1, "몫": None}] - assert nat["hit"] == [ # 줄 글 지역 = 1위 · 전국 = 되돌릴 자리 · 남은 변형 접힘 - ["1위", ["M2"]], - ["전국", ["전국"]], - ["지역 변형", ["M1"]], - ] - assert got["groups"]["pick"] == [True, False, True] # 재료비 있는 단가산출 줄도 자재 검색 - assert got["picked"] == [ - {"로직": "GC000206", "입력값": {}, "배": 1, "몫": None}, - {"로직": "GF000211", "입력값": {}, "배": 0.1, "몫": 0.1}, - ] - assert got["inputs"] == "소량감률=50(블록), 지역=서울 · ⚠ 못 가름: 측압" - assert got["word"] == ["만듦", "있던 것을 확인한 내용으로 덮음"] - assert got["qty"] == "콘크리트공 0.1091 · 굴착기 0.29hr" - assert got["target"] == "제46호표 [㎡]" - - -def test_별칭_장부_화면_합침(): - got = _check()["ledger"] - assert got["key"] == "m02-hopyo-ledger:company:7" - assert got["merged"] == [ - ["나||m3", "personal", True, True, "안 씀"], # 고친 줄 - ["다||개", "personal", True, True, "짝"], # 위 층 → 내 층으로 고쳐 씀 - ["다||개", "system", False, False, "짝"], # 위 층 = 그대로 읽기만 - ] # 지운 줄(가) 없음 - assert got["filter"] == ["다||개", "다||개"] and got["kind"] == ["나||m3"] - assert got["pair"] == "GC000198 × 1 · 몫 0.1 · 소량감률=50" - assert got["entry"]["짝"] == [] and "층" not in got["entry"] - - -def test_길_만들기_통째_만들_층_미리_셈_장부(): - calls = _check()["calls"] - build = "/api/m02/layers/company/hopyo-build?owner=7" - head = {"층": "system", "주인": None, "키": "HP-000001", "번": 3} - assert calls[0]["url"] == build and calls[0]["body"]["줄"][0]["차례"] == 1 - assert {k: calls[0]["body"][k] for k in head} == head and "새로" not in calls[0]["body"] - assert calls[1]["body"] == {**head, "통째": {"대상": [{"로직": "GF000225", "배": 1}]}} - assert calls[2] == { - "url": "/api/m02/layers/system/hopyo/HP-000001/3?to=company&to_owner=7", - "method": "GET", - "body": None, - } - assert calls[3]["url"] == "/api/m02/layers/personal/hopyo/HP-000001/3/guess" - assert calls[3]["body"]["대상"][0]["몫"] == 0.1 - assert calls[4]["url"] == "/api/m02/layers/company/hopyo-ledger?owner=7" - assert calls[5]["method"] == "PUT" and calls[5]["body"] == {"줄": {"가|b|m3": None}} - - -def test_로직_추가_확인된_것은_바로_참조_아니면_확인_창(): - got = _check()["pick"] - assert got["picked"] == [ - { - "ref": {"층": "company", "주인": "7", "키": "SC-000009"}, - "name": "임도표지석", - "alias": "임도표지석", - } - ] - assert got["calls"] == [] # 확인 없이 만들지 않음 - assert got["opened"] == ["확인 — 북부청.xls 제4호표 배수관"] - - -def test_엑셀_보기_창_하위_호표_먼저(): - got = _check()["sheet"] - assert got["order"] == [46, 45, 3, 7, 8] # 순환은 남은 차례 - # 다 연결만 녹색 · 0/0 색 없음(359-2) · 358-3 연결할 · 연결된 두 칸이 있으면 그것 - assert got["linked"] == [True, False, False, False, True, False, False] - - -def _pick() -> dict: - row = {**ROW, "확인": {"층": "company", "주인": "7", "키": "SC-000009"}} - return _node("helper_m02_hopyo_pick.cjs", {"rows": [row], "built": {}}) - - -def test_고르기_창_실무_호표_줄(): - got = _pick() - assert got["calls"][0] == { - "url": "/api/m02/layers/company/hopyo-picks?owner=7", - "method": "GET", - "body": None, - } - assert got["kinds"] == ["logic", "precombo", "hopyo"] - assert got["rows"] == [ - { - "kind": "hopyo", - "key": "system||HP-000001|3", - "tag": "실무 호표", - "name": "임도표지석 910×600×150", - "sub": "북부청.xls · 제3호표 · 마스터", - "unit": "개소 · 확인 SC-000009", - }, - { - "kind": "hopyo", - "key": "system||HP-000001|엑셀", - "tag": "실무 호표", - "name": "엑셀로 보기 · 여러 호표", - "sub": "북부청.xls · 마스터", - "unit": "호표 1", - }, - ] - assert got["picked"][0]["ref"]["키"] == "SC-000009" - assert got["project"] is True # 프로젝트 층 = 덧줄 없음 - - -def test_글_도우미와_근거_꼬리표(): - got = _pick() - view = got["view"] - assert view["target"] == ["구조물터파기 [㎥]", "레미콘 25-21-120 [㎥]", "제46호표 [㎡]"] - assert view["inputs"] == "횟수=4회사용시, 지역=서울 · ⚠ 못 가름: 보정작업" - basis = got["basis"] - assert basis["ok"] == [True, False, False, False, False] # 조항 같음 · 직종 · 단위 모두 - assert basis["plain"] == [True, True] # 줄 직종이 보통인부뿐 · 없음 = 직종 안 봄 - assert basis["tag"] == [ - "조항 건설 공통 3-2-1 · 직종 특별인부 · 단위 같음", - "조항 건설 공통 3-2-1 · 단위 같음", - "", - ] - assert basis["source"] == [ - "출처 건설표준품셈 3-2-1 · 직종 특별인부 · 단가산출 93–101행", - "단가산출 7행", - "", - ] - - -def test_엑셀_보기_창_행과_호표(): - got = _pick() - assert got["calls"][1] == { - "url": "/api/m02/layers/company/hopyo-grid/HP-000001?owner=7", - "method": "GET", - "body": None, - } - sheet = got["sheet"] - assert sheet["at"] == [None, 3, 3, 3, 4, 4, None] # 22행 → 제3호표 · 범위 밖 = 없음 - assert sheet["span"] == "B20:O24" # 제3호표 영역 = B열 ~ 끝 열(열 폭 정보 O) - assert sheet["last"] == ["AA", "A"] - assert sheet["rows"] == ["B4345:F4345", "B93:F101"] # 단가산출 행 B~F 열 - - -# ── 소스 ────────────────────────────────────────────────────────────── -def test_옛_즉시_확정과_바로_만들기_없음(): - texts = {name: _text(f"M02_MasterTemplete_{name}.ts") for name in NEW} - every = "\n".join(texts.values()) - for gone in ("saveHopyoMemory", "hopyo-memory", "HopyoMemory", "memoryOf", "basisPick"): - assert gone not in every, gone - for label in ( - "같음 모두 확정", - "근거 맞음 모두 확정", - '"확정"', - '"사전조합 만들기"', - '"불러오기" 옛 단추', # 718-1 — 「불러오기」 = 칸 이름 · 마법사 단추라 낱말은 뺌 - ): - assert label not in every, label - users = sorted(name for name, text in texts.items() if "buildHopyo(" in text) - assert users == [ - "Hopyo_Check", - "Hopyo_SheetMake", - ] # 확인 창 · 엑셀 보기 [저장] 물음 뒤(785-4 만들기 셋 = SheetMake) - check = texts["Hopyo_Check"] - assert '`${L("M02_PrecomboTitle")} 생성`' in check and "맞음 · 별칭 일괄 체크" in check - assert "확인하고" not in check # 359-4 명사형 - assert "showLoadingOverlay()" in check and "dropDraft(k)" in check - assert "m02-hopyo-check:" in check and "POLL_MS" in check - sheet = texts["Hopyo_SheetMake"] - # 359-2 — 펼침 표 · [확인 창 열기] · [이대로 n개 만들기] 없음 · [저장] = 물음 한 번 뒤 체크 줄로 만들기 - for gone in ("확인 창 열기", "이대로", "review(", "확인하고 만들기"): - assert gone not in sheet, gone - save = sheet.split("async function save(")[1].split("async function makeAll(")[0] - assert 'showConfirmDialog(ask, "저장")' in save and "hasDraft(" in save - assert "건너뜀 — 체크 줄 없음" in save and "await makeAll(ready, skipped);" in save - assert ( - "buildOrder(" in sheet and "unmadeKids(" in sheet and "leftAt(" in sheet - ) # 하위 먼저 · 함께(777-1) - sheet = texts["Hopyo_Sheet"] # 목록 · [수정] = 엑셀 보기 몸 - edit = sheet.split("const editButton =")[1].split("function paintList(")[0] - assert ( - '"수정"' in edit and "e.stopPropagation();" in edit and "createJudgeRows({" in edit - ) # 785-6 줄 판 - row = sheet.split("function paintList(")[1] - assert row.index("m02-hopyo-sheet__no") < row.index("editButton(h)") < row.index("__name") - assert 'linked.classList.toggle("is-linked", allLinked(h));' in row - pick = texts["Hopyo_Pick"] - assert "if (row.확인)" in pick and 'import("./M02_MasterTemplete_Hopyo_Check")' in pick - assert not (M02 / "M02_MasterTemplete_Hopyo_View.ts").exists() # 본 칸 표 없음(359-1) - ledger = texts["Hopyo_Ledger"] - assert "m02-hopyo-ledger:" in ledger and "saveLedger(" in ledger - for gone in ("발주처", "회사로 올리기", "쓰인 횟수"): - assert gone not in ledger.split("*/", 1)[1] # 머리 글 「안 둠」 밖 - - def test_좌측_판_자리와_권한(): side = _text("M02_MasterTemplete_UI_Side.ts") assert 'import { buildHopyoSection } from "./M02_MasterTemplete_Hopyo_Side";' in side @@ -375,182 +44,6 @@ def test_좌측_판_자리와_권한(): assert gone not in imp + hopyo_side, gone # 중간 걸음 · 단가산출 체크 없음 -def test_로직_추가_한_곳과_파일_규칙(): - detail = _text("M02_MasterTemplete_Labor_Detail.ts") - assert len(detail.splitlines()) <= 700 # 700 줄 넘지 않음(717-3 나눔으로 줄어듦) - assert "withHopyo(library)," in detail - pick = _text("M02_MasterTemplete_Labor_Pick.ts") - assert "more?: () => Promise;" in pick and "...later" in pick - assert "if (pre?.query) finder.search.value = pre.query;" in pick # 찾기 글 미리 넣음 - for name in NEW: - text = _text(f"M02_MasterTemplete_{name}.ts") - assert len(text.splitlines()) <= 700, name - css = _text("M02_MasterTemplete_Hopyo.css") - assert "var(--color-" in css and not re.search(r"#[0-9a-fA-F]{3,6}\b", css) - for state in ("맞음", "별칭", "다른점", "후보"): - assert f".m02-hopyo__state.is-{state}" in css - assert '"./M02_MasterTemplete_Hopyo.css"' in _text("M02_MasterTemplete_Hopyo_Check.ts") - - -def test_엑셀_보기_창_여는_자리와_늦게_불러옴(): - lazy = 'import("./M02_MasterTemplete_Hopyo_Sheet")' - side = _text("M02_MasterTemplete_Hopyo_Import.ts") - assert "return show(pick, got, true);" in _text( - "M02_MasterTemplete_Hopyo_Side.ts" - ) # 올린 직후 바로 본 칸 - assert lazy in side and "m.buildHopyoSheet({" in side # 본 칸 몸 = 늦게 불러옴 - check = _text("M02_MasterTemplete_Hopyo_Check.ts") - assert lazy in check and 'label: "엑셀 보기"' in check and "호표: opt.no" in check - assert "엑셀에서 보기" not in check - assert "단산행: 행" in check - pick = _text("M02_MasterTemplete_Hopyo_Pick.ts") - assert lazy in pick - static = r'^import (?!type )[^(;]*"\./M02_MasterTemplete_Hopyo_Sheet"' - for text in (side, pick, check): - assert not re.search(static, text, re.M) # 정적 import 없음(타입만) - sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts") - assert 'import("../A00_Common/spreadsheet/spreadsheet")' in sheet - assert not re.search( - r'^import \{[^}]*\} from "\.\./A00_Common/spreadsheet/spreadsheet"', sheet, re.M - ) - assert "readOnly: true" in sheet and "wide: true" in sheet - - -def test_실물_응답_대역_확인_창(): - """338-4 — 가 창 실물 응답(`fixtures/m02_hopyo_view_sample.json`)을 확인 창 · 고르기 창 도우미에 - 그대로 먹임 — 체크 = 서버 제안 · 몸 꼴 · 품 비교 · 옵션 출처 · 후보 판정 꼬리표.""" - sample = json.loads((Path(HERE) / "fixtures" / "m02_hopyo_view_sample.json").read_bytes()) - lines = sample["호표"]["호표"]["줄"] - got = _node("helper_m02_hopyo_check.cjs", {"real": lines}) - by = {x["이름"]: x for x in got["lines"]} - remicon = by["레미콘타설(장비사용)"] - assert remicon["상태"] == "맞음" and remicon["checked"] and remicon["checkable"] - assert ( - remicon["compare"] - == "콘크리트공 0.1091 → 0.1091 ✓ · 보통인부 0.03636 → 0.03636 ✓ · 굴착기 0.29 → 0.2909 ✓" - ) - assert ( - remicon["inputs"] - == "타설방법=장비사용타설(규격), 구조물=철근구조물(규격), 소량감률=50(블록)" - ) - assert remicon["qty"] == "콘크리트공 0.1091 · 보통인부 0.03636 · 굴착기 0.29hr" - assert remicon["cands"][0][1] == "맞음" and remicon["cands"][0][2].startswith("조항 건설품셈") - rebar = by["철근가공조립(현장)"] - assert rebar["상태"] == "다른 점" and not rebar["checked"] and rebar["checkable"] - assert [x["차례"] for x in got["body"]] == [x["차례"] for x in lines if x["맞춤"]["체크"]] - assert all({"로직", "입력값", "배"} <= set(t) for x in got["body"] for t in x["대상"]) - assert got["sure"] == [x["차례"] for x in lines if x["맞춤"]["상태"] == "맞음"] - - -def test_확인_창_손질_339(): - """339 덧고침 — ① 줄 표 꼴(한글 낱말 줄바꿈 · 옵션 · 품 비교 한 줄씩 · 단추 세로) · 장부 품명 · 규격 폭 - ② 만들기 뒤 좌측 사전조합 목록 다시 ③ 엑셀 보기 창 목록 셈 끝까지 다시 읽음 · 펼침 표 숫자 판정 칸.""" - css = _text("M02_MasterTemplete_Hopyo.css") - assert "word-break: keep-all;" in css and "white-space: pre-line;" in css - assert ".m02-hopyo__lines .m02-hopyo__acts > *" in css - assert "nth-child(9)" not in css # 359-3 — 칸 차례 대신 칸 이름 - assert ".m02-hopyo-ledger__table td:nth-child(1)" in css - assert "m02-hopyo-ledger__table" in _text("M02_MasterTemplete_Hopyo_Ledger.ts") - rows = _text("M02_MasterTemplete_Hopyo_CheckRows.ts") - assert r'text: compare.split(" · ").join("\n")' in rows # 품 비교 = 항목마다 한 줄 - assert "m02-hopyo__opts" in rows # 옵션 = 항목마다 한 줄 - check = _text("M02_MasterTemplete_Hopyo_Check.ts") - make = check.split("async function make(")[1].split("function builtBlock(")[0] - assert make.index("document.dispatchEvent(new Event(HOPYO_BUILT));") < make.index("opt.onBuilt") - side = _text("M02_MasterTemplete_UI_Side.ts") - assert "document.addEventListener(HOPYO_BUILT, onBuilt);" in side - onb = side.split("const onBuilt")[1].split(";")[0] - assert "precombo.refresh()" in onb and "sangeun.refresh()" in onb # 717-2a — 둘 다 새로고침 - sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts") - assert "poll(file);" in sheet and "poll(next);" in sheet and "POLL_MS" in sheet - assert "if (ok) document.dispatchEvent(new Event(HOPYO_BUILT));" in _text( - "M02_MasterTemplete_Hopyo_SheetMake.ts" - ) - - -def test_엑셀_보기_머리_막대_셈_끝까지_다시_읽음_359(): - """339 덧고침 2 → 359-1 · 785-4 — 옛 본 칸 표 몫(시트 · 호표 수 · 검산 · 「제안 n / 줄 m」 · 「셈 중」)은 - 엑셀 보기 몸 머리(부르는 쪽 덧붙임 `headExtra` — 층 · 안 이은 줄 · [끝내기]) · 셈 중이면 3 초마다 다시 읽음(머리 · 목록 함께).""" - sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts") - head = sheet.split("function paintHead(")[1].split("/** 목록 강조")[0] - for word in ("시트 ", "호표 ", "검산 어긋남", "검산 맞음", "제안 ", "숫자 판정 셈 중"): - assert word in head, word - assert "...(opt.headExtra ?? [])" in head and "opt.onRead?.(file);" in head - assert "dropButton" not in sheet and "onDeleted" not in sheet # [삭제] 걷음 = [끝내기] 한 길 - poll = sheet.split("function poll(")[1].split("async function load(")[0] - assert "paintHead(next);" in poll and "paintList();" in poll and "poll(next);" in poll - - -def test_확인_창_표_일위대가_상세_꼴_359(): - """359-3 — 로직 표 · 자재 표(일위대가 상세 꼴) + 품 비교 · 단추 · 드롭다운 글(저장 값 그대로) · - 금액 = 줄마다 서버 calc 미리 셈(`logic/` 문서 꼴 · Build 와 같은 수량 규칙).""" - got = _check()["draft"] - assert got["logic"] == { - "종류": "일위대가", - "열": "", - "단위": None, - "담은로직": [ # 같은 로직 둘째 · 호표 · 배 없음 = 뺌 - {"로직": "GC000198", "별칭": None, "메모": None, "입력값": {"소량감률": 50}, - "수량": {"손": 1}}, - ], - "자재": [], - } # fmt: skip - assert got["mat"]["자재"] == [ # 같은 자재 = 2 × 1 + 2 × 0.5 - {"코드": "MT1", "이름": None, "규격": None, "단위": None, "수량": {"손": 3}, - "별도계상": "별도계상"}, - ] # fmt: skip - assert got["direct"]["별도계상"] == "직접계산" and got["direct"]["수량"] == {"손": 0} - assert got["none"] == [None, None, None] # 전국 자재(361-1) = 키 없어 미리 셈 없음 - assert got["table"] == ["mat", "logic", "logic", "mat", "logic", "mat"] - rows = _text("M02_MasterTemplete_Hopyo_CheckRows.ts") - assert '["", "추가"],' in rows and '["우리에 없음", "로직없음"],' in rows - assert '["안 씀", "사용안함"],' in rows and "뺌 — " not in rows - for word in ('"고르기"', '"원문 보기"', '"엑셀 보기"', '"옵션"', '"품 비교"'): - assert word in rows, word - assert 'import { moneyCell, PRICE_SLOTS } from "./M02_MasterTemplete_Labor_Rows";' in rows - assert "...markCells(p?.세칸 ?? { 별도계상:" in rows # 361-2 서버 세 칸 먼저 - assert "...markHeads()," in rows - for head in ("Col_Order", "Sub", "Col_Name", "Col_Alias", "Labor_QtyCol", "Col_Unit"): - assert f'tc("{head}")' in rows, head - check = _text("M02_MasterTemplete_Hopyo_Check.ts") - assert "calcLabor(target.layer, DRAFT_COLUMN, doc, {}, target.owner)" in check - assert "checkTables(h.줄, ctx())" in check and "draftDoc(line, partsOf(line, d))" in check - assert "function lineRow(" not in check and "function partCell(" not in check - draft = _text("logic/M02_MasterTemplete_Hopyo_Draft.ts") - assert "M02_Hopyo_Build" in draft # 정본 짝 표시 - code = draft.split("*/", 1)[1] # 머리 글 밖 - for gone in ("document", "fetch(", "sessionStorage", "@ui/"): - assert gone not in code, gone - - -def test_명사형_단추와_별칭_장부_풀이_359(): - """359-4 — 별칭 장부 뜻 한 줄(좌측 단추 title · 화면 머리) · M02 실무자료 단추 = 명사형 · - 갈래 글 = 확인 창 드롭다운과 같음(저장 값 그대로).""" - ledger = _text("M02_MasterTemplete_Hopyo_Ledger.ts") - hint = "엑셀 품명 ↔ 우리 로직 · 자재 짝을 기억해 두었다가 같은 품명이 나오면 먼저 제안" - assert hint in ledger and "`${LEDGER_HINT}" in ledger - side = _text("M02_MasterTemplete_Hopyo_Side.ts") - assert "title: LEDGER_HINT" in side - assert 'label: master ? "삭제" : "거두기"' in ledger # 739-1 내 제안 = 거두기 - for label in ('"변경 취소"', '"항목 추가"', '"내 층 복사"'): - assert f"label: {label}" in ledger, label - assert '"우리에 없음": "로직없음"' in ledger and '"안 씀": "사용안함"' in ledger - mine = ("Hopyo_Side", "Hopyo_Sheet", "Hopyo_Check", "Hopyo_CheckRows", "Hopyo_Ledger") - mine += ("Hopyo_Import", "Hopyo_SheetMake") - every = "\n".join(_text(f"M02_MasterTemplete_{n}.ts") for n in mine) - for gone in ( - "고친 것 버림", - "로직 더함", - "내 층으로 고쳐 씀", - "확인하고 사전조합 만들기", - "엑셀 올리기", - "엑셀에서 보기", - "모두 체크 (", - 'label: "지우기"', - ): - assert gone not in every, gone - - def test_372_2_저장은_개인_층에_삭제는_화면_층_권한(): """718-1 — 만들 층 = 올린 층 · [끝내기] 로 올린 파일 지움 → 빈 본 칸(785-4: 엑셀 보기 [삭제] 걷음) · 743-1 — 떠남(화면 바뀜 · 페이지 닫음) = 쥔 파일 지움.""" @@ -562,77 +55,3 @@ def test_372_2_저장은_개인_층에_삭제는_화면_층_권한(): assert 'addEventListener("pagehide", leave)' in imp and " leave,\n" in imp panel = _text("M02_MasterTemplete_Hopyo_Side.ts") assert panel.count("open?.imp?.leave()") == 2 and panel.count("open.imp?.leave()") == 1 - - -def test_779_세_보기_칠_뛰기_창_다시_씀(): - """779-4 — 탭 = 서버 역할 id(이름으로 안 찾음) · 칠 = 공용 격자 `칠` · 범례 부품 · 색은 공통 값 · - 확인 창 캐시 고침 = 다시 칠 알림 · 779-6 — 같은 파일 창은 다시 씀(앞으로 · 제자리) · 격자 다시 안 받음.""" - sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts") - nav = _text("M02_MasterTemplete_Hopyo_SheetNav.ts") - check = _text("M02_MasterTemplete_Hopyo_Check.ts") - assert "Nav.nameTabs(book, grid)" in sheet and "칠: nav?.fill" in sheet - assert "nav?.at(s);" in sheet and "nav?.marks(briefs);" in sheet - assert "[detail, ...DANSAN]" in sheet # 단산행 = 역할 id 먼저(STmate 「단가산출근거」) - assert "if (opened.has(id)) return opened.get(id)!(opt);" in sheet - assert "Nav.gridOf(id, opt.scope, opt.key)" in sheet and "readHopyoGrid" not in sheet - assert "above = Nav.raise(left);" in sheet and "Nav.lower(left, above.splice(0))" in sheet - assert "createLegend([" in nav and 'kind: "line"' in nav - assert re.findall(r'color: "([^"]+)"', nav) == [ - f"var(--color-{x})" - for x in ("success", "warning", "info", "danger", "text-muted", "accent") - ] - assert 'HOPYO_DRAFT = "m02-hopyo-draft"' in nav - save = check.split("export function saveDraft(")[1].split("export function dropDraft(")[0] - assert 'document.dispatchEvent(new Event("m02-hopyo-draft"));' in save - for name in ( - "M02_MasterTemplete_Hopyo_SheetNav.ts", - "logic/M02_MasterTemplete_Hopyo_SheetMap.ts", - ): - text = _text(name) - assert ( - 'el("button"' not in text - and 'el("input"' not in text - and "#" not in text.split("*/", 1)[1] - ) - pure = _text("logic/M02_MasterTemplete_Hopyo_SheetMap.ts").split("*/", 1)[1] - for word in ("document", "fetch", "sessionStorage", "import "): - assert word not in pure, word - assert ( - len(check.splitlines()) <= 663 - and len(_text("M02_MasterTemplete_Hopyo_Api.ts").splitlines()) <= 676 - ) - - -def test_단위_환산_칸_보이기_784_2(): - """784-2 — 배 1 · 단위 같음 = 칸 숨김 · 몫 줄 = 몫만 · 빈칸 · 추정 · 단위 다름 = 입력 칸 + 두 단위 글 재료 · - 화면(CheckRows · Ledger)은 「배」 대신 「단위 환산」 · 「엑셀 1 ㎡ = 우리 [ ] 본」 꼴.""" - cases = [ - {"part": {"배": 1, "환산": {"배": 1, "갈래": "같음"}}, "from": "㎡", "to": "㎡"}, - {"part": {"배": 1, "환산": {"배": 1, "갈래": "두 단위"}}, "from": "공/㎥", "to": "공㎥"}, - {"part": {"배": 0.1, "몫": 0.1, "환산": {"배": 1, "갈래": "같음"}}, "from": "㎥", "to": "㎥"}, - {"part": {"배": None, "환산": {"배": None, "갈래": "입력"}}, "from": "㎡", "to": "본"}, - {"part": {"배": 1, "환산": {"배": None, "갈래": "입력"}}, "from": "m", "to": "본"}, - {"part": {"배": 0.2, "환산": {"배": 0.2, "갈래": "추정"}}, "from": "㎡", "to": "㎥"}, - {"part": {"배": 100, "환산": {"배": 100, "갈래": "단위표"}}, "from": "m", "to": "cm"}, - {"part": {"배": 0.01, "몫": 0.1, "환산": {"배": 0.1, "갈래": "단위표"}}, "from": "a", "to": "b"}, - {"part": {"배": 1}, "from": "m2", "to": "㎡"}, - {"part": {"배": 1}, "from": "m", "to": "본"}, - {"part": {"배": 1, "몫": 1, "환산": {"배": 1, "갈래": "같음"}}, "from": "㎡", "to": "㎡"}, - ] # fmt: skip - got = _node("helper_m02_hopyo_unit.cjs", {"cases": cases}) - assert [g["mode"] for g in got] == [ - "same", "same", "quota", "input", "input", "input", "input", "input", "same", "input", "same", - ] # fmt: skip - assert (got[3]["from"], got[3]["to"]) == ("㎡", "본") # 「엑셀 1 ㎡ = 우리 [ ] 본」 - assert got[5]["estimated"] and got[5]["thick"] == 0.2 # 「두께 0.2m 로 추정」 - assert not got[3]["estimated"] and got[3]["thick"] is None - rows = _text("M02_MasterTemplete_Hopyo_CheckRows.ts") - assert "unitView(p, line.단위, p.단위)" in rows and "M02_HopyoUnitSame" in rows - assert "배 — 우리 수량" not in rows - ledger = _text("M02_MasterTemplete_Hopyo_Ledger.ts") - assert "unitField(times, { from: r.원래.단위" in ledger and 'title: "배"' not in ledger - pure = _text("logic/M02_MasterTemplete_Hopyo_Unit.ts").split("*/", 1)[1] - for word in ("document", "fetch", "sessionStorage", "import "): - assert word not in pure, word - for name in ("CheckRows", "Ledger"): - assert len(_text(f"M02_MasterTemplete_Hopyo_{name}.ts").splitlines()) <= 700