Merge remote-tracking branch 'origin/dev' into sub_laptop_8
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
"""B09 설계내역서 셈(PLAN 145-3) — 화면 없음 · 저장 안 함 · 파일 · M01 안 읽음.
|
"""B09 설계내역서 셈(PLAN 145-3) — 화면 없음 · 저장 안 함 · 파일 · M01 안 읽음.
|
||||||
|
|
||||||
⚠ 짝 = `B09_Estimation_Calc_Bill.ts`(같은 식 · 같은 차례 · 같은 끝수) — 한쪽을 고치면 다른 쪽도 ·
|
⚠ 짝 = `B09_Estimation_Calc_Bill.ts`(같은 식 · 같은 차례 · 같은 끝수) — 한쪽을 고치면 다른 쪽도 ·
|
||||||
거울 시험 `resources/tester/test_145_3_b09_bill.py` · `test_182_b09_bill_level.py`(레벨).
|
거울 시험 `resources/tester/test_145_3_b09_bill.py` · `test_182_b09_bill_level.py`(레벨) · `test_184_2_b09_bill_free.py`(자유 편집).
|
||||||
|
|
||||||
실무 엑셀 설계내역서 꼴 · 공종 나무 = 엑셀 꼴 고정(`SECTIONS` · 공구 안 나눔):
|
실무 엑셀 설계내역서 꼴 · 공종 나무 = 엑셀 꼴 고정(`SECTIONS` · 공구 안 나눔):
|
||||||
1 토공사(1.1~1.6) · 2 구조물공(2.1~2.3) · 3 운반공 · 4 자재대 · 관급자재대(제외금액)
|
1 토공사(1.1~1.6) · 2 구조물공(2.1~2.3) · 3 운반공 · 4 자재대 · 관급자재대(제외금액)
|
||||||
@@ -12,17 +12,34 @@
|
|||||||
조달수수료 = ROUNDDOWN(관급 순자재대 × 율 ÷ 100, 0)(율 = 사용자 입력 · 빈칸 시작)
|
조달수수료 = ROUNDDOWN(관급 순자재대 × 율 ÷ 100, 0)(율 = 사용자 입력 · 빈칸 시작)
|
||||||
제목 줄 = 아래 항목 금액 합(엑셀 SUMIF — 빈 줄은 0 이 아니라 「빈 줄 n」 으로 셈)
|
제목 줄 = 아래 항목 금액 합(엑셀 SUMIF — 빈 줄은 0 이 아니라 「빈 줄 n」 으로 셈)
|
||||||
빈칸 = 수량이나 단가를 낸 원천이 없으면 그 줄 금액 None + 「연결 없음」 목록.
|
빈칸 = 수량이나 단가를 낸 원천이 없으면 그 줄 금액 None + 「연결 없음」 목록.
|
||||||
손질(166-2) = 사용자 값 `user.bill` — `qty{줄 키: 수}` 자동 줄 수량 덮어쓰기(키 지우면 자동) ·
|
단가 = `ref_price(ref)` 하나 — dg = 일위대가목록표 · sg = 단가산출근거목록표 ·
|
||||||
`rows[]` 손 줄(`hand:` · 공종 · `after` 앞 줄 키 뒤에 끼움 · 명칭 · 규격 · 단위 · 수량 ·
|
|
||||||
연결 `ref`) ·
|
|
||||||
단가 = `ref_price(ref)` 하나 — dg = 일위대가목록표 · sg = 단가산출근거목록표 ·
|
|
||||||
logic = M01 로직 새 산근(서버가 `sg_hand{줄 키: 산근 키}` 로 줌 · 그전엔 빈칸 「[저장] 뒤」) ·
|
logic = M01 로직 새 산근(서버가 `sg_hand{줄 키: 산근 키}` 로 줌 · 그전엔 빈칸 「[저장] 뒤」) ·
|
||||||
그 밖 = `inputs.hopyo` · 비고 = 「대가n」 · 「산근 n호표」(155-3 꼴).
|
그 밖 = `inputs.hopyo` · 비고 = 「대가n」 · 「산근 n호표」(155-3 꼴).
|
||||||
레벨(182) = 손 줄 `kind: "group"`(묶음 줄 · 수량 · 연결 없음) · `parent`(묶음 손 줄 키 · 없음 = 공종 바로
|
자유 편집(184) = 기본 로직 표 = 초기값(저장 안 함) · 사용자 값 `user.bill` v2 = 덮개(계약 — 184-3 화면):
|
||||||
아래) · 줄마다 `level`(1 = 1 토공사 · 2 = 1.1 · 3 = 그 안 항목 · 손 묶음 + 1) ·
|
{"v": 2,
|
||||||
묶음 금액 = 자손 항목 합 · 공종 합 · `sums` = 항목만(묶음 줄 안 셈 · 원가계산서 그대로).
|
"rows": [표 차례 전체(order · 부모 먼저 · 자손 이어서) {
|
||||||
원가계산서로 넘길 합 = `sums`(직접재료비 · 직접노무비 · 산출경비 · 관급 순자재대 — 145-4 이음 꼴 ·
|
"key": 줄 키, "parent": 묶음 줄 키(`sec:*` · `hand:*`) | 없음 = 맨 위,
|
||||||
빈 줄이 있으면 None) · 화면 표시용 = `totals`(아는 금액 합 · 빈 줄 수 · 조달수수료).
|
자동 줄(`sec:*` · `earth:` · `st:` · `haul:` · `mat:`) = 고친 칸만 — 칸 없음 = 자동(B08 따라감) ·
|
||||||
|
값 = 지킴 · null = 일부러 빈칸 · 칸 = name · spec · unit · qty · ref
|
||||||
|
(제목 `sec:*` = name 만 · `mat:` = ref 없음 · `fee` = 덮개 없음 · 차례에 안 넣음 — `sec:G` 끝 고정)
|
||||||
|
새 줄(`hand:*`) = 모든 칸 + "kind": "group"(묶음 · 수량 · 연결 없음) | 없음 = 항목 ·
|
||||||
|
"cost": "direct"(맨 위 새 묶음 · 없음도 direct)}],
|
||||||
|
"gone": [지운 자동 줄 키 — `sec:G` · `fee` · `mat:` 는 안 받음(자재 줄 주인 = 대비표 표시)]}
|
||||||
|
덮개 없음 = 기본 표 그대로 · [초기화] = `bill` 지움 ·
|
||||||
|
`rows` 에 없고 `gone` 에도 없는 자동 줄(B08 새 열 · 대비표 새 표시) = 기본 부모 끝 + 비고 「새 자동 줄」
|
||||||
|
(기본 부모가 없으면 표 끝 맨 위) · 원천이 안 낸 키 = 안 보임 ·
|
||||||
|
부모 못 찾음 · 묶음 아님 · 돎 = 자동 줄 기본 부모 / 새 줄 맨 위 · `sec:G` = 맨 위 고정 ·
|
||||||
|
관급 자재 줄 = `sec:G` 밖 못 감(밖이면 `sec:G` 아래) · 깊이 5 넘음 = 레벨 4 조상 아래.
|
||||||
|
옛 꼴(v 없음 · 166-2 · 182) = `qty{줄 키: 수}` 수량 덮개 + `rows[]` 손 줄(`section` · `after` 앞 줄
|
||||||
|
뒤에 끼움 · `parent` = 같은 공종 손 묶음) — 그대로 읽음(화면 첫 손질 때 v2 로 굳힘).
|
||||||
|
셈 출력 줄 칸(계약) = kind("title" 자동 묶음 · "group" 새 묶음 · "item") · key · name(번호 뺀 이름) ·
|
||||||
|
parent(실제 부모 키 · None = 맨 위) · level(1~5 = 깊이) · no(레벨 1 · 2 묶음 차례 번호 「1」 · 「1.1」 ·
|
||||||
|
관급 · 항목 = "") · cost("direct" | "supply" = 맨 위 조상 갈래 · `sec:G` 만 supply) ·
|
||||||
|
auto(True = 자동 줄) | hand(True = 새 줄) · auto_{name|spec|unit|qty|ref}(고친 칸만 = 자동값) ·
|
||||||
|
section(가장 가까운 제목 공종 · 없으면 None).
|
||||||
|
묶음 금액 = 자손 항목 합 + 「빈 줄 n」 · 구조물 블록 줄 키 꼬리 = 대가 블록 키(대가 번호 밀림에 안 흔들림).
|
||||||
|
원가계산서로 넘길 합 = `sums`(cost direct 항목의 직접재료비 · 직접노무비 · 산출경비 · supply 항목의 관급
|
||||||
|
순자재대 — 145-4 이음 꼴 · 빈 줄이 있으면 None) · 화면 표시용 = `totals`(아는 금액 합 · 빈 줄 수 · 조달수수료).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@@ -3,17 +3,16 @@
|
|||||||
* B09 설계내역서 셈(PLAN 145-3) — 화면 없음 · 저장 안 함 · import 는 짝 바탕 셈 하나(시험이 이어 붙임).
|
* B09 설계내역서 셈(PLAN 145-3) — 화면 없음 · 저장 안 함 · import 는 짝 바탕 셈 하나(시험이 이어 붙임).
|
||||||
*
|
*
|
||||||
* ⚠ 짝 = `B09_Estimation_Calc_Bill.py`(같은 식 · 같은 차례 · 같은 끝수) — 한쪽을 고치면 다른 쪽도 ·
|
* ⚠ 짝 = `B09_Estimation_Calc_Bill.py`(같은 식 · 같은 차례 · 같은 끝수) — 한쪽을 고치면 다른 쪽도 ·
|
||||||
* 거울 시험 `resources/tester/test_145_3_b09_bill.py` · `test_182_b09_bill_level.py`(레벨).
|
* 거울 시험 `resources/tester/test_145_3_b09_bill.py` · `test_182_b09_bill_level.py`(레벨) · `test_184_2_b09_bill_free.py`(자유 편집).
|
||||||
*
|
*
|
||||||
* 공종 나무 = 엑셀 꼴 고정(1 토공사 · 2 구조물공 · 3 운반공 · 4 자재대 · 관급자재대(제외금액)) · 공구 안 나눔
|
* 공종 나무 = 엑셀 꼴 고정(1 토공사 · 2 구조물공 · 3 운반공 · 4 자재대 · 관급자재대(제외금액)) · 공구 안 나눔
|
||||||
* 항목 = 서버가 B08 수량으로 짠 줄 + 대비표 「별도」(4 자재대) · 「관급」(관급자재대) 자재 + 조달수수료
|
* 항목 = 서버가 B08 수량으로 짠 줄 + 대비표 「별도」(4 자재대) · 「관급」(관급자재대) 자재 + 조달수수료
|
||||||
* 금액 = ROUNDDOWN(단가 × 수량, 0) 비목마다 · 조달수수료 = ROUNDDOWN(순자재대 × 율 ÷ 100, 0)
|
* 금액 = ROUNDDOWN(단가 × 수량, 0) 비목마다 · 조달수수료 = ROUNDDOWN(순자재대 × 율 ÷ 100, 0)
|
||||||
* 제목 줄 = 아래 항목 금액 합(빈 줄은 「빈 줄 n」) · 원가계산서로 넘길 합 = sums(빈 줄 있으면 null)
|
* 묶음 금액 = 자손 항목 합(빈 줄은 「빈 줄 n」) · 원가계산서로 넘길 합 = sums(빈 줄 있으면 null)
|
||||||
* 손질(166-2) = `user.bill` — qty{줄 키: 수} 자동 줄 수량 덮어쓰기 · rows[] 손 줄(after 뒤에 끼움) ·
|
|
||||||
* 단가 = `refPrice(ref)` 하나(dg 대가 목록 · sg 산근 목록 · logic = 서버 sg_hand 산근 · 그 밖 hopyo)
|
* 단가 = `refPrice(ref)` 하나(dg 대가 목록 · sg 산근 목록 · logic = 서버 sg_hand 산근 · 그 밖 hopyo)
|
||||||
* 레벨(182) = 손 줄 `kind: "group"`(묶음 줄 · 수량 · 연결 없음) · `parent`(묶음 손 줄 키 · 없음 = 공종 바로 아래) ·
|
* 자유 편집(184) = `user.bill` v2 덮개 {v: 2, rows: [표 차례 · 고친 칸 · 새 줄], gone: [지운 자동 줄]} ·
|
||||||
* 줄마다 `level`(1 = 1 토공사 · 2 = 1.1 · 3 = 그 안 항목 · 손 묶음 + 1) · 묶음 금액 = 자손 항목 합 ·
|
* 옛 꼴(qty · 손 줄 after · parent) 그대로 읽음 · 저장 꼴 · 출력 줄 칸(계약) = 짝 .py 머리
|
||||||
* 공종 합 · sums = 항목만(묶음 줄 안 셈 · 원가계산서 그대로)
|
* 출력 줄 칸 = kind · key · name · parent · level · no · cost · auto | hand · auto_{칸} · section
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
[저장] 때 책에 없는 새 줄만 더함(있는 줄 안 바꿈)
|
[저장] 때 책에 없는 새 줄만 더함(있는 줄 안 바꿈)
|
||||||
user_values.json 사용자 값 {"materials": {자재 키: {applied?, supply?, quote?, quote_page?,
|
user_values.json 사용자 값 {"materials": {자재 키: {applied?, supply?, quote?, quote_page?,
|
||||||
pick?}}, "estimate": {…},
|
pick?}}, "estimate": {…},
|
||||||
"bill": {"qty": {줄 키: 수}, "rows": [손 줄]}}(166-2 설계내역서 손질)
|
"bill": {"v": 2, "rows": [표 차례 · 고친 칸 · 새 줄], "gone": [키]}}
|
||||||
|
(184 설계내역서 덮개 — 꼴은 `B09_Estimation_Calc_Bill` 머리 ·
|
||||||
|
옛 꼴 {"qty", "rows"}(166-2 · 182)도 그대로 다듬음)
|
||||||
— 고친 칸만(키가 있으면 그 값 · null = 빈칸 · 없으면 기본)
|
— 고친 칸만(키가 있으면 그 값 · null = 빈칸 · 없으면 기본)
|
||||||
|
|
||||||
quotes/<자재 키>/ 견적서 파일(166-4 · 여러 개) — 폴더가 목록(키의 못 쓰는 글자는 %XX) ·
|
quotes/<자재 키>/ 견적서 파일(166-4 · 여러 개) — 폴더가 목록(키의 못 쓰는 글자는 %XX) ·
|
||||||
|
|||||||
@@ -44,6 +44,12 @@
|
|||||||
gap: var(--spacing-8);
|
gap: var(--spacing-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 185-2 보기 단추 글 한 줄 */
|
||||||
|
.b09-estimation__bar .ui-btn {
|
||||||
|
flex: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
/* 182 설계내역서 레벨 — 글자 크기(줄 높이 28px 고정 → 16px 까지) · 첫 칸 들여쓰기 1em × (레벨 − 1) */
|
/* 182 설계내역서 레벨 — 글자 크기(줄 높이 28px 고정 → 16px 까지) · 첫 칸 들여쓰기 1em × (레벨 − 1) */
|
||||||
.b09-estimation .ui-data-table__table tbody tr.is-lv1 td {
|
.b09-estimation .ui-data-table__table tbody tr.is-lv1 td {
|
||||||
font-size: var(--text-body);
|
font-size: var(--text-body);
|
||||||
@@ -112,3 +118,16 @@
|
|||||||
background: color-mix(in srgb, var(--color-accent) 22%, var(--color-canvas));
|
background: color-mix(in srgb, var(--color-accent) 22%, var(--color-canvas));
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 185-1 총괄 · 원가계산서 레벨 — 글자 크기 = 위 is-lvN · 들여쓰기 = 명칭 칸(is-indent) · 앞 칸(is-flat)은 그대로 */
|
||||||
|
.b09-estimation .ui-data-table__table tbody tr[class*="is-lv"] td.is-flat:first-child {
|
||||||
|
padding-left: var(--spacing-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.b09-estimation .ui-data-table__table tbody tr.is-lv2 td.is-indent {
|
||||||
|
padding-left: calc(var(--spacing-8) + 1em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.b09-estimation .ui-data-table__table tbody tr.is-lv3 td.is-indent {
|
||||||
|
padding-left: calc(var(--spacing-8) + 2em);
|
||||||
|
}
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ function numberFormat(spec: TabTable, key: string): (v: number | null | undefine
|
|||||||
/** 이 화면 안 보기(기억이 안 돼도 바꾼 보기 유지) */
|
/** 이 화면 안 보기(기억이 안 돼도 바꾼 보기 유지) */
|
||||||
const chosen = new Map<TabId, GroupView>();
|
const chosen = new Map<TabId, GroupView>();
|
||||||
|
|
||||||
/** 묶음 탭 — 머리(안내 + 보기 단추) · 고른 보기(카드 · 표) */
|
/** 묶음 탭 — 카드 = 안내 + 카드 판(보기 단추는 가로 열 수 줄) · 표 = 머리(안내 + 보기 단추) + 표 */
|
||||||
export function renderGroupPanel(o: {
|
export function renderGroupPanel(o: {
|
||||||
tab: TabId;
|
tab: TabId;
|
||||||
spec: TabTable;
|
spec: TabTable;
|
||||||
@@ -199,9 +199,6 @@ export function renderGroupPanel(o: {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
toggle.dataset.b09GroupView = view;
|
toggle.dataset.b09GroupView = view;
|
||||||
const bar = document.createElement("div");
|
|
||||||
bar.className = "b09-estimation__bar";
|
|
||||||
bar.append(...(o.hint ? [o.hint] : []), toggle);
|
|
||||||
const at = o.focus === undefined ? -1 : rowIndex(o.spec.rows, o.focus);
|
const at = o.focus === undefined ? -1 : rowIndex(o.spec.rows, o.focus);
|
||||||
if (view === "cards") {
|
if (view === "cards") {
|
||||||
const first = o.spec.columns[0]?.key ?? "";
|
const first = o.spec.columns[0]?.key ?? "";
|
||||||
@@ -213,10 +210,14 @@ export function renderGroupPanel(o: {
|
|||||||
focus: at,
|
focus: at,
|
||||||
money: numberFormat(o.spec, "sum"),
|
money: numberFormat(o.spec, "sum"),
|
||||||
qty: numberFormat(o.spec, "qty"),
|
qty: numberFormat(o.spec, "qty"),
|
||||||
|
actions: [toggle], // 185-2 가로 열 수 단추와 한 줄
|
||||||
});
|
});
|
||||||
cards.dataset.b09Table = o.tab;
|
cards.dataset.b09Table = o.tab;
|
||||||
return [bar, cards];
|
return [...(o.hint ? [o.hint] : []), cards];
|
||||||
}
|
}
|
||||||
|
const bar = document.createElement("div");
|
||||||
|
bar.className = "b09-estimation__bar";
|
||||||
|
bar.append(...(o.hint ? [o.hint] : []), toggle);
|
||||||
const range = pickRange(o.spec.rows, o.focus);
|
const range = pickRange(o.spec.rows, o.focus);
|
||||||
const table = o.makeTable((i) => pickClass(range, i));
|
const table = o.makeTable((i) => pickClass(range, i));
|
||||||
if (at >= 0) requestAnimationFrame(() => table.focusRow(at));
|
if (at >= 0) requestAnimationFrame(() => table.focusRow(at));
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
* 고칠 칸 = 요율 · 손입력(기초액 · 이윤 보정 · 부가세 조정 · 조달수수료율) ·
|
* 고칠 칸 = 요율 · 손입력(기초액 · 이윤 보정 · 부가세 조정 · 조달수수료율) ·
|
||||||
* 엑셀에 없는 현행 비목 = 줄만(빈칸)
|
* 엑셀에 없는 현행 비목 = 줄만(빈칸)
|
||||||
* 원가계산서 = 총괄 값 거울 + 구성비 · 고칠 칸 = 간접재료비 · 작업설 · 줄 누름 = 총괄 그 줄로
|
* 원가계산서 = 총괄 값 거울 + 구성비 · 고칠 칸 = 간접재료비 · 작업설 · 줄 누름 = 총괄 그 줄로
|
||||||
|
* 레벨(185-1) = 줄 `level`(화면이 정함 · 줄 짜임이 이 파일에 있음 · 셈 짝은 값만) → 182 is-lvN 글자 크기 ·
|
||||||
|
* 들여쓰기 = 명칭 칸(is-indent) · 앞 칸(is-flat)은 그대로 · 1단 = 굵게(is-title)
|
||||||
|
* 총괄 = 가.~바. 소계 1 · 그 밖 2 · 원가계산서 = 구분만 1 · 비목 소계 2 · 세목 3
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import type { DataRow } from "@ui/ui_template_data_table";
|
import type { DataRow } from "@ui/ui_template_data_table";
|
||||||
@@ -74,6 +77,14 @@ function clickEstimate(ctx: TabContext, rowKey: string, column: string, from: un
|
|||||||
ctx.setUser({ ...ctx.user, estimate });
|
ctx.setUser({ ...ctx.user, estimate });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 185-1 레벨 꼴 — 줄 class · 칸 class(들여쓰기 = 명칭 칸 · 앞 칸 그대로) */
|
||||||
|
const levelClass = (r: DataRow | undefined): string | null =>
|
||||||
|
r ? `is-lv${r.level}${r.level === 1 ? " is-title" : ""}` : null;
|
||||||
|
const indentClass =
|
||||||
|
(first: string, name: string) =>
|
||||||
|
(_i: number, key: string): string | null =>
|
||||||
|
key === name ? "is-indent" : key === first ? "is-flat" : null;
|
||||||
|
|
||||||
function rateText(formula: string, rate: Num): string {
|
function rateText(formula: string, rate: Num): string {
|
||||||
return rate === null ? `${formula}(요율 빈칸)` : formula.replace("율", `${rate}%`);
|
return rate === null ? `${formula}(요율 빈칸)` : formula.replace("율", `${rate}%`);
|
||||||
}
|
}
|
||||||
@@ -113,6 +124,7 @@ export const buildTotal: TabBuild = (ctx): TabTable => {
|
|||||||
rate: null,
|
rate: null,
|
||||||
hand: null,
|
hand: null,
|
||||||
candidate: "",
|
candidate: "",
|
||||||
|
level: part.endsWith(".") ? 1 : 2,
|
||||||
...extra,
|
...extra,
|
||||||
});
|
});
|
||||||
const rateRow = ([key, name, rt, formula]: [string, string, string, string], no: number) =>
|
const rateRow = ([key, name, rt, formula]: [string, string, string, string], no: number) =>
|
||||||
@@ -197,6 +209,8 @@ export const buildTotal: TabBuild = (ctx): TabTable => {
|
|||||||
],
|
],
|
||||||
rows,
|
rows,
|
||||||
frozen: 2,
|
frozen: 2,
|
||||||
|
rowClass: (i) => levelClass(rows[i]),
|
||||||
|
cellClass: indentClass("part", "name"),
|
||||||
hint:
|
hint:
|
||||||
"요율 = M01 조달청 제비율에서 자동(맨 위 고르는 값 · 공종 토목) — 칸에 적으면 사용자 값이 앞섬 · 지우거나 출처 칸 누르면 자동 · " +
|
"요율 = M01 조달청 제비율에서 자동(맨 위 고르는 값 · 공종 토목) — 칸에 적으면 사용자 값이 앞섬 · 지우거나 출처 칸 누르면 자동 · " +
|
||||||
"2024 엑셀 요율과 연도 차로 다를 수 있음 · 자동도 없으면 그 줄 · 그 줄을 쓰는 소계 빈칸 · 끝수 = 엑셀 그대로(줄마다 원 버림 · 관급 천원 올림) · " +
|
"2024 엑셀 요율과 연도 차로 다를 수 있음 · 자동도 없으면 그 줄 · 그 줄을 쓰는 소계 빈칸 · 끝수 = 엑셀 그대로(줄마다 원 버림 · 관급 천원 올림) · " +
|
||||||
@@ -262,6 +276,7 @@ export const buildCost: TabBuild = (ctx): TabTable => {
|
|||||||
share: share[key] ?? null,
|
share: share[key] ?? null,
|
||||||
note: formula[key] ?? "",
|
note: formula[key] ?? "",
|
||||||
link: link ?? "",
|
link: link ?? "",
|
||||||
|
level: !part ? 1 : name === "소계" ? 2 : 3,
|
||||||
}));
|
}));
|
||||||
return {
|
return {
|
||||||
columns: [
|
columns: [
|
||||||
@@ -274,6 +289,8 @@ export const buildCost: TabBuild = (ctx): TabTable => {
|
|||||||
],
|
],
|
||||||
rows,
|
rows,
|
||||||
frozen: 3,
|
frozen: 3,
|
||||||
|
rowClass: (i) => levelClass(rows[i]),
|
||||||
|
cellClass: indentClass("group", "name"),
|
||||||
hint: "총괄설계내역서 값 거울 · 구성비 = 총공사비 대비 · 고칠 칸 = 간접재료비 · 작업설(직접재료비가 그만큼 바뀌고 재료비 소계는 그대로 — 엑셀 꼴) · 줄 누름 = 총괄 그 줄",
|
hint: "총괄설계내역서 값 거울 · 구성비 = 총공사비 대비 · 고칠 칸 = 간접재료비 · 작업설(직접재료비가 그만큼 바뀌고 재료비 소계는 그대로 — 엑셀 꼴) · 줄 누름 = 총괄 그 줄",
|
||||||
editable: (i, key) => key === "amount" && String(rows[i]?.key) in HAND,
|
editable: (i, key) => key === "amount" && String(rows[i]?.key) in HAND,
|
||||||
onEdit: (i, key, value) => editEstimate(ctx, String(rows[i]?.key), key, value),
|
onEdit: (i, key, value) => editEstimate(ctx, String(rows[i]?.key), key, value),
|
||||||
|
|||||||
@@ -47,3 +47,8 @@
|
|||||||
## 추가 (182장 뒤 · 23번 보충)
|
## 추가 (182장 뒤 · 23번 보충)
|
||||||
31. 설계 내역서의 내용은 기본적으로 들어간 내용은 고정이 아님. 초기에 기본적으로 만드는 로직은 있지만 이걸 사용자가 수정할 수 있어야하며 추가하는 행과 삭제도 자유로워야함. 다시 말해 자유롭게 작성이 되고 단가 산출과 일위대가에 연결할 수 있어야한다는 것임.
|
31. 설계 내역서의 내용은 기본적으로 들어간 내용은 고정이 아님. 초기에 기본적으로 만드는 로직은 있지만 이걸 사용자가 수정할 수 있어야하며 추가하는 행과 삭제도 자유로워야함. 다시 말해 자유롭게 작성이 되고 단가 산출과 일위대가에 연결할 수 있어야한다는 것임.
|
||||||
32. 추가로 특정 셀의 텍스트 입력으로 마우스 우클릭이 브라우져 옵션이 보임. 우리프로그램에서는 우클릭으로 브라우져 옵션보이기는 없는 상태임.참고
|
32. 추가로 특정 셀의 텍스트 입력으로 마우스 우클릭이 브라우져 옵션이 보임. 우리프로그램에서는 우클릭으로 브라우져 옵션보이기는 없는 상태임.참고
|
||||||
|
33. 총관설계 내역서에도 행들이 레벨 구분이 되어야함.
|
||||||
|
34. 박스보기가 구현되어 있는 버튼에 줄바꿈이 존재함. 1행으로 존재하게 변경하고 같이 존제하는 가로 개수 설정의 버튼들도 같은 행에 존재하게 해줘.
|
||||||
|
35. 자재단가대비표의에서 견적항목의 경우 단가를 수기입력하는 정보들만 견적서를 첨부하는 글자가 보이게 반영
|
||||||
|
36. 자재단가대비표의 출처는 있어야하는 정보가 맞으나 현재 페이지에서는 중요한 정보가 아니니 출처정보는 마우스커서를 올린 셀의 정보를 보이게 하는데 숫자들의 의미를 모름 풀어서 짧게 정보를 보여줘.
|
||||||
|
37. 4번 요청에 이어 프로그램 전역에서 호버기능으로 추가정보를 보여줄때의 공용양식이 있었으면 함.
|
||||||
|
|||||||
@@ -146,13 +146,14 @@ def test_화면_보기_바꿈_누름_이동():
|
|||||||
"\nconst o = { tab: 'dg', spec, hint: null, focus: 'DG1|1', goto: (l) => gone.push(l),"
|
"\nconst o = { tab: 'dg', spec, hint: null, focus: 'DG1|1', goto: (l) => gone.push(l),"
|
||||||
" redraw: () => redraws++, makeTable: () => ({ root: document.createElement('table'),"
|
" redraw: () => redraws++, makeTable: () => ({ root: document.createElement('table'),"
|
||||||
" focusRow: (i) => focused.push(i) }) };"
|
" focusRow: (i) => focused.push(i) }) };"
|
||||||
"\nconst [bar, view] = renderGroupPanel(o);"
|
"\nconst [view] = renderGroupPanel(o);"
|
||||||
"\nconst label = bar.children[0].textContent;"
|
"\nconst btn = view.querySelector('[data-b09-group-view]');"
|
||||||
|
"\nconst label = btn.textContent;"
|
||||||
"\nview.querySelector('[data-at=\"2\"]').click();"
|
"\nview.querySelector('[data-at=\"2\"]').click();"
|
||||||
"\nview.querySelector('[data-at=\"0\"]').click();"
|
"\nview.querySelector('[data-at=\"0\"]').click();"
|
||||||
"\nconst lit = view.querySelector('[data-at=\"3\"]').classList.contains('is-focus');"
|
"\nconst lit = view.querySelector('[data-at=\"3\"]').classList.contains('is-focus');"
|
||||||
"\nconst box = view.querySelector('.ui-group-card').classList.contains('is-pick');"
|
"\nconst box = view.querySelector('.ui-group-card').classList.contains('is-pick');"
|
||||||
"\nbar.children[0].click();"
|
"\nbtn.click();"
|
||||||
"\nconst [bar2, view2] = renderGroupPanel(o);"
|
"\nconst [bar2, view2] = renderGroupPanel(o);"
|
||||||
"\nconsole.log(JSON.stringify({ label, table: view.dataset.b09Table, gone, lit, box, redraws,"
|
"\nconsole.log(JSON.stringify({ label, table: view.dataset.b09Table, gone, lit, box, redraws,"
|
||||||
" label2: bar2.children[0].textContent, tag2: view2.tagName, focused,"
|
" label2: bar2.children[0].textContent, tag2: view2.tagName, focused,"
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""185-1 B09 총괄설계내역서 · 공사원가계산서 줄 레벨 — 줄 `level` · 줄 class(182 is-lvN) · 칸 class · CSS.
|
||||||
|
|
||||||
|
레벨 = 화면이 정함(줄 짜임이 `B09_Estimation_UI_Tabs_Total.ts` 에 있음 · 셈 짝은 값만 냄 → 거울 없음).
|
||||||
|
총괄 = 가.~바. 소계 1(굵게) · 그 밖 2 · 원가계산서 = 구분만 1 · 비목 소계 2 · 세목 3.
|
||||||
|
들여쓰기 = 명칭 칸(is-indent) · 앞 칸(is-flat)은 그대로.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
B09 = ROOT / "B09_Estimation"
|
||||||
|
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||||
|
|
||||||
|
SUMS = {"material": 194234417, "labor": 346800554, "expense": 169324165, "supply_net": 69474220}
|
||||||
|
|
||||||
|
BODY = f"""
|
||||||
|
const user = {{ estimate: {{}} }};
|
||||||
|
const ctx = {{ sheets: {{ total: computeTotal({json.dumps(SUMS)}, user.estimate) }}, payload: {{}}, user, setUser: () => {{}} }};
|
||||||
|
const pack = (t) => t.rows.map((r, i) => [r.key, r.level, t.rowClass(i), t.cellClass(i, "name"), t.cellClass(i, t.columns[0].key), t.cellClass(i, "amount")]);
|
||||||
|
console.log(JSON.stringify({{ total: pack(buildTotal(ctx)), cost: pack(buildCost(ctx)) }}));
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _run() -> dict:
|
||||||
|
names = ("Calc_Base", "Calc_Total", "UI_Tabs", "UI_Tabs_Total")
|
||||||
|
script = "".join(
|
||||||
|
IMPORTS.sub("", (B09 / f"B09_Estimation_{n}.ts").read_text(encoding="utf-8")) for n in names
|
||||||
|
)
|
||||||
|
result = subprocess.run(
|
||||||
|
["node", "--experimental-strip-types", "--no-warnings",
|
||||||
|
"--input-type=module-typescript"],
|
||||||
|
input=script + BODY, capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||||
|
) # fmt: skip
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
return json.loads(result.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def test_총괄_레벨():
|
||||||
|
rows = {k: (lv, cls) for k, lv, cls, *_ in _run()["total"]}
|
||||||
|
top = ["direct", "net_cost", "da", "supply_price", "contract", "grand"]
|
||||||
|
assert all(rows[k] == (1, "is-lv1 is-title") for k in top)
|
||||||
|
for k in ["in_price", "in_env", "indirect_labor", "machine", "retirement", "overhead", "vat",
|
||||||
|
"supply"]: # fmt: skip
|
||||||
|
assert rows[k] == (2, "is-lv2"), k
|
||||||
|
assert {lv for lv, _ in rows.values()} == {1, 2}
|
||||||
|
|
||||||
|
|
||||||
|
def test_원가계산서_레벨():
|
||||||
|
rows = {k: (lv, cls) for k, lv, cls, *_ in _run()["cost"]}
|
||||||
|
for k in ["overhead", "profit", "supply_price", "vat", "contract", "supply", "grand"]:
|
||||||
|
assert rows[k] == (1, "is-lv1 is-title"), k
|
||||||
|
for k in ["material", "labor", "expense_sum"]:
|
||||||
|
assert rows[k] == (2, "is-lv2"), k
|
||||||
|
for k in ["direct_material", "scrap", "direct_labor", "indirect_labor", "expense", "safety"]:
|
||||||
|
assert rows[k] == (3, "is-lv3"), k
|
||||||
|
|
||||||
|
|
||||||
|
def test_칸_class_명칭만_들여씀():
|
||||||
|
out = _run()
|
||||||
|
for tab in ("total", "cost"):
|
||||||
|
for _k, _lv, _cls, name, first, amount in out[tab]:
|
||||||
|
assert (name, first, amount) == ("is-indent", "is-flat", None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_CSS_레벨_규칙():
|
||||||
|
css = (B09 / "B09_Estimation_UI_Page.css").read_text(encoding="utf-8")
|
||||||
|
assert re.search(
|
||||||
|
r'tr\[class\*="is-lv"\] td\.is-flat:first-child \{\s*padding-left: var\(--spacing-8\);', css
|
||||||
|
)
|
||||||
|
for lv, em in ((2, 1), (3, 2)):
|
||||||
|
assert re.search(
|
||||||
|
rf"tr\.is-lv{lv} td\.is-indent \{{\s*padding-left: calc\(var\(--spacing-8\) \+ {em}em\);",
|
||||||
|
css,
|
||||||
|
)
|
||||||
|
# 글자 크기 = 182 규칙 재사용(줄 높이 28px → 16px 까지)
|
||||||
|
assert re.search(r"tr\.is-lv1 td \{\s*font-size: var\(--text-body\);", css)
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
"""185-2 B09 묶음 탭 도구 줄 — 보기 바꿈 단추 글 한 줄 · 가로 열 수 단추와 같은 줄.
|
||||||
|
|
||||||
|
- 카드 보기 = 보기 바꿈 단추가 가로 열 수 줄(`ui-group-cards__bar`) 안 · 표 보기 = 머리 줄(`b09-estimation__bar`)
|
||||||
|
- 단추 · 라벨 = `white-space: nowrap` · 도구 줄 = 줄바꿈 없음(`flex-wrap: nowrap`)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
if str(ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from resources.tester.helper_b09_group_cards import COMMON, GROUPS, RAW, TABS, run # noqa: E402
|
||||||
|
|
||||||
|
B09 = "B09_Estimation/B09_Estimation_UI_Tabs_"
|
||||||
|
DG = {"id": "DG1", "no": 1, "name": "찰쌓기", "unit": "m2", "lines": [], "sum": 0}
|
||||||
|
HINT = "(() => { const h = document.createElement('p'); h.className = 'hint'; return h; })()"
|
||||||
|
|
||||||
|
|
||||||
|
def _block(css: str, selector: str) -> str:
|
||||||
|
m = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", css)
|
||||||
|
assert m, selector
|
||||||
|
return m.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _panel(view: str, hint: str):
|
||||||
|
"""고른 보기 · 안내 글로 그린 뒤 보기 바꿈 단추가 든 줄 · 그 줄의 열 수 단추."""
|
||||||
|
return run(
|
||||||
|
[COMMON, TABS, f"{B09}Labor.ts", GROUPS],
|
||||||
|
f"\nconst spec = buildDaegaBlocks({{ sheets: {{ daega_blocks: [{json.dumps(DG, ensure_ascii=False)}] }} }});"
|
||||||
|
f"\nchosen.set('dg', '{view}');"
|
||||||
|
f"\nconst o = {{ tab: 'dg', spec, hint: {hint}, goto: () => {{}}, redraw: () => {{}},"
|
||||||
|
" makeTable: () => ({ root: document.createElement('table'), focusRow: () => {} }) };"
|
||||||
|
"\nconst out = renderGroupPanel(o);"
|
||||||
|
"\nconst top = out.find((e) => e.all('[data-b09-group-view]').length || e.dataset.b09GroupView);"
|
||||||
|
"\nconst btn = top.dataset.b09GroupView ? top : top.querySelector('[data-b09-group-view]');"
|
||||||
|
"\nconst bar = btn.closest('.ui-group-cards__bar') ?? btn.closest('.b09-estimation__bar') ?? top;"
|
||||||
|
"\nconsole.log(JSON.stringify({ n: out.length, first: out[0].className, label: btn.textContent,"
|
||||||
|
" bar: bar.className, cols: bar.all('[data-group-cols]').map((b) => b.dataset.groupCols) }));",
|
||||||
|
RAW,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_카드_보기_단추가_가로_열수_줄에():
|
||||||
|
out = _panel("cards", "null")
|
||||||
|
assert out["bar"] == "ui-group-cards__bar" and out["label"] == "표로 보기"
|
||||||
|
assert out["cols"] == ["1", "2", "3", "4"] # 같은 줄에 열 수 단추 넷
|
||||||
|
assert out["n"] == 1 # 안내 글 없으면 카드 판 하나뿐
|
||||||
|
|
||||||
|
|
||||||
|
def test_카드_보기_안내_글은_카드_위에_따로():
|
||||||
|
out = _panel("cards", HINT)
|
||||||
|
assert out["n"] == 2 and out["first"] == "hint"
|
||||||
|
assert out["bar"] == "ui-group-cards__bar"
|
||||||
|
|
||||||
|
|
||||||
|
def test_표_보기_단추는_머리_줄():
|
||||||
|
out = _panel("table", "null")
|
||||||
|
assert out["bar"] == "b09-estimation__bar" and out["label"] == "박스보기"
|
||||||
|
assert out["cols"] == [] # 표 보기 = 열 수 단추 없음
|
||||||
|
|
||||||
|
|
||||||
|
def test_CSS_한_줄_nowrap():
|
||||||
|
cards = (ROOT / "ui_template" / "ui_template_group_cards.css").read_text("utf-8")
|
||||||
|
assert "flex-wrap: nowrap" in _block(cards, ".ui-group-cards__bar")
|
||||||
|
item = _block(
|
||||||
|
cards,
|
||||||
|
".ui-group-cards__bar .ui-btn,\n.ui-group-cards__label,\n.ui-group-cards__count",
|
||||||
|
)
|
||||||
|
assert "white-space: nowrap" in item and "flex: none" in item
|
||||||
|
page = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8")
|
||||||
|
assert "white-space: nowrap" in _block(page, ".b09-estimation__bar .ui-btn")
|
||||||
|
assert "actions: [toggle]" in (ROOT / GROUPS).read_text("utf-8")
|
||||||
@@ -10,11 +10,19 @@
|
|||||||
|
|
||||||
.ui-group-cards__bar {
|
.ui-group-cards__bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-8);
|
gap: var(--spacing-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 185-2 도구 줄 = 한 줄 · 단추 · 글 줄바꿈 없음 */
|
||||||
|
.ui-group-cards__bar .ui-btn,
|
||||||
|
.ui-group-cards__label,
|
||||||
|
.ui-group-cards__count {
|
||||||
|
flex: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.ui-group-cards__cols {
|
.ui-group-cards__cols {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user