feat(B09): 좌측 「데이터 점검」 묶음 — 이름 · 설명 지움 · [업데이트] · 낡음 점 배지 · 연결 점검 탭을 좌측 목록으로(203-1)
- 묶음 이름 「원가 단가」 → 「데이터 점검」 · 굳힌 시각 안내 글 지움(공용 호버로) - [단가 갱신] → [업데이트] · 서버 book_stale(지금 M01 로 굳힌 책과 견줌) 까닭 있으면 점 배지 - 「연결 점검」 탭 = 좌측 목록(누르면 그 탭 그 줄 · 까닭 = 공용 호버) · 위 탭 줄에서 뺌 · 첫 탭 = 공사원가계산서 - test_203_1 새로 · test_144 · test_181_1 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
@@ -197,6 +198,23 @@ def refresh_book(keys: list[str]) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def book_stale(book: dict[str, Any] | None, keys: list[str]) -> str | None:
|
||||
"""203-1 [업데이트] 배지 — 지금 M01 로 새로 굳힌 책(`refresh_book`)과 견줌 · 다른 것 한 줄(같으면 None)."""
|
||||
if book is None:
|
||||
return "단가 아직 안 굳힘"
|
||||
fresh = json.loads(json.dumps(refresh_book(keys), ensure_ascii=False)) # 저장 꼴(JSON)로 견줌
|
||||
rows = book.get("rows") or {}
|
||||
new = sum(key not in rows for key in fresh["rows"])
|
||||
changed = sum(key in rows and rows[key] != row for key, row in fresh["rows"].items())
|
||||
parts = [
|
||||
f"새 단가 줄 {new}" if new else "",
|
||||
f"M01 에서 바뀐 단가 {changed}" if changed else "",
|
||||
"운전사 계수 바뀜" if book.get("driver_factor") != fresh["driver_factor"] else "",
|
||||
"환율 바뀜" if book.get("fx") != fresh["fx"] else "",
|
||||
]
|
||||
return " · ".join(p for p in parts if p) or None
|
||||
|
||||
|
||||
def extend_book(book: dict[str, Any] | None, keys: list[str]) -> dict[str, Any]:
|
||||
"""[저장] — 책에 없는 새 줄만 더함(있는 줄 · 계수 · 환율은 안 바꿈) · 책이 없으면 새로 굳힘."""
|
||||
if book is None:
|
||||
|
||||
@@ -121,6 +121,8 @@ async def payload(project_id: UUID, root: Path) -> dict[str, Any]:
|
||||
"inputs": inputs,
|
||||
"user": user,
|
||||
"book": {"made": book.get("made")} if book else None,
|
||||
# 203-1 [업데이트] 배지 까닭(지금 M01 과 다른 것 한 줄 · 같으면 None)
|
||||
"stale": await asyncio.to_thread(prices.book_stale, book, keys),
|
||||
"sheets": sheets,
|
||||
"rate_candidates": rate_candidates(),
|
||||
"rate_table": rate_table(),
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/* =============================================================================
|
||||
* B09_Estimation_UI_Check.ts
|
||||
* B09 좌측 「데이터 점검」 묶음(PLAN 203-1) — [업데이트] · 연결 점검(옛 「연결 점검」 탭 내용).
|
||||
*
|
||||
* [업데이트] = 단가를 지금 M01 값으로 새로 굳힘 · 서버가 낡았다고 보면(`payload.stale` 까닭 한 줄)
|
||||
* 단추에 점 배지(`is-pending`) · 굳힌 시각 · 까닭 = 공용 호버
|
||||
* 연결 점검 = 「연결 없음」 줄 목록(누르면 그 탭 그 줄로 · 까닭 = 공용 호버) + 서버 알림 · M01 에 없는 키
|
||||
* ========================================================================== */
|
||||
|
||||
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
||||
import type { Row } from "./B09_Estimation_Calc_Base";
|
||||
import { tabName, type Link, type TabId } from "./B09_Estimation_UI_Tabs";
|
||||
|
||||
export interface CheckItem {
|
||||
link: Link;
|
||||
tab: string;
|
||||
name: string;
|
||||
why: string;
|
||||
}
|
||||
|
||||
/** 빠진 연결 줄 · 알림 글(서버 알림 + M01 에 없는 키) */
|
||||
export function checkItems(sheets: Row, payload: Row): { items: CheckItem[]; notes: string[] } {
|
||||
const gaps: Row[] = sheets.gaps || [];
|
||||
const items = gaps.map((g) => ({
|
||||
link: { tab: g.tab as TabId, key: g.key },
|
||||
tab: tabName(g.tab),
|
||||
name: String(g.name ?? ""),
|
||||
why: String(g.why ?? ""),
|
||||
}));
|
||||
const notes: string[] = [...(payload.notes || [])];
|
||||
const missing: string[] = payload.inputs?.missing || [];
|
||||
if (missing.length) notes.push(`M01 에 없는 키 ${missing.length} — ${missing.join(" · ")}`);
|
||||
return { items, notes };
|
||||
}
|
||||
|
||||
/** [업데이트] 호버 글 — 굳힌 시각 + 낡은 까닭 */
|
||||
export function refreshTip(payload: Row | null): string {
|
||||
const made = payload?.book?.made;
|
||||
const when = made ? `단가 굳힘 ${String(made).replace("T", " ")}` : "단가 아직 안 굳힘";
|
||||
const stale = payload?.stale;
|
||||
return `${when} · 누르면 지금 M01 값으로 새로 굳힘${stale ? ` · 업데이트 필요 — ${stale}` : ""}`;
|
||||
}
|
||||
|
||||
export interface CheckPanel {
|
||||
/** 묶음 안 연결 점검 자리 */
|
||||
list: HTMLElement;
|
||||
draw: (payload: Row, sheets: Row) => void;
|
||||
}
|
||||
|
||||
export function createCheckPanel(refresh: HTMLElement, goto: (link: Link) => void): CheckPanel {
|
||||
let payload: Row | null = null;
|
||||
let items: CheckItem[] = [];
|
||||
attachHoverInfo(refresh, () => refreshTip(payload));
|
||||
const list = document.createElement("div");
|
||||
list.className = "b09-check";
|
||||
attachHoverInfo(list, (row) => items[Number(row.dataset.i)]?.why, "[data-i]");
|
||||
|
||||
const draw = (next: Row, sheets: Row): void => {
|
||||
payload = next;
|
||||
refresh.classList.toggle("is-pending", Boolean(next.stale));
|
||||
const made = checkItems(sheets, next);
|
||||
items = made.items;
|
||||
const head = document.createElement("p");
|
||||
head.className = "b09-check__head";
|
||||
head.textContent = `연결 없음 ${items.length}곳`;
|
||||
const rows = document.createElement("div");
|
||||
rows.className = "ui-sidebar-list";
|
||||
rows.append(
|
||||
...items.map((item, i) => {
|
||||
const row = document.createElement("button");
|
||||
row.type = "button";
|
||||
row.className = "ui-sidebar-row";
|
||||
row.dataset.i = String(i);
|
||||
const name = document.createElement("span");
|
||||
name.textContent = item.name;
|
||||
const tab = document.createElement("span");
|
||||
tab.className = "b09-check__tab";
|
||||
tab.textContent = item.tab;
|
||||
row.append(name, tab);
|
||||
row.addEventListener("click", () => goto(item.link));
|
||||
return row;
|
||||
}),
|
||||
);
|
||||
const notes = made.notes.map((text) => {
|
||||
const p = document.createElement("p");
|
||||
p.className = "b09-check__note";
|
||||
p.textContent = text;
|
||||
return p;
|
||||
});
|
||||
list.replaceChildren(head, ...(items.length ? [rows] : []), ...notes);
|
||||
};
|
||||
return { list, draw };
|
||||
}
|
||||
@@ -209,3 +209,34 @@
|
||||
.b09-estimation .ui-data-table__table tbody td.is-edited {
|
||||
background: color-mix(in srgb, var(--color-warning) 22%, var(--color-canvas));
|
||||
}
|
||||
|
||||
/* 203-1 좌측 「데이터 점검」 — [업데이트] 필요 = 단추 위 점 배지(B05 [3D 업데이트] 꼴) */
|
||||
.b09-options .ui-btn.is-pending {
|
||||
position: relative;
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.b09-options .ui-btn.is-pending::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-warning);
|
||||
}
|
||||
|
||||
/* 연결 점검 목록 — 이름 · 탭(작게) · 알림 글 */
|
||||
.b09-check__head,
|
||||
.b09-check__note {
|
||||
margin: var(--spacing-8) 0 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.b09-check__tab {
|
||||
flex: none;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* 원본 = 기번3 공통 원가산출 엑셀 · 탭 하나 = 화면 표 하나(`…_UI_Tabs` 차례) · 옛 원가 코드 안 봄
|
||||
* 자료 = GET …/estimation(입력 · 사용자 값 · 서버가 셈한 표) · 조작 = 캐시(`b09-estimation`) +
|
||||
* 짝 계산(`…_Calc_*.ts`)으로 바로 다시 셈 · [저장] = PUT → 서버 정본 · [단가 갱신] = POST …/prices
|
||||
* 짝 계산(`…_Calc_*.ts`)으로 바로 다시 셈 · [저장] = PUT → 서버 정본 · [업데이트] = POST …/prices
|
||||
* 호표 칸 누름 = 그 탭 그 줄로 이동 · 자동 저장 없음
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -31,9 +31,9 @@ import { computeEstimation } from "./B09_Estimation_Calc_Bill";
|
||||
import { computeLabor, daegaPrice } from "./B09_Estimation_Calc_Labor";
|
||||
import { computeTotal } from "./B09_Estimation_Calc_Total";
|
||||
import { createTabBars } from "./B09_Estimation_UI_TabBar";
|
||||
import { createCheckPanel } from "./B09_Estimation_UI_Check";
|
||||
import { buildDaegaBlocks, buildDaegaList } from "./B09_Estimation_UI_Tabs_Labor";
|
||||
import {
|
||||
buildCheck,
|
||||
rowIndex,
|
||||
TABS,
|
||||
type Link,
|
||||
@@ -70,7 +70,6 @@ function L(key: keyof typeof ui_locales): string {
|
||||
const LATER: Record<string, string> = {};
|
||||
|
||||
const BUILDERS: Partial<Record<TabId, TabBuild>> = {
|
||||
check: buildCheck,
|
||||
dg_list: buildDaegaList,
|
||||
dg: buildDaegaBlocks,
|
||||
bill: buildBill,
|
||||
@@ -152,7 +151,7 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
let payload: Row | null = null;
|
||||
let user: Row = {};
|
||||
let sheets: Row = {};
|
||||
let current: TabId = "check";
|
||||
let current: TabId = TABS[0][0];
|
||||
|
||||
// 181-1 위 탭(md) · 아래 탭(sm) 두 줄 — 줄 그리기 = `…_UI_TabBar`
|
||||
const bars = createTabBars((tab) => show(tab));
|
||||
@@ -163,11 +162,10 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
body.className = "b09-estimation";
|
||||
body.append(bars.top, bars.sub, panel);
|
||||
|
||||
const made = document.createElement("p");
|
||||
made.className = "b09-estimation__hint";
|
||||
|
||||
const recompute = (): void => {
|
||||
if (payload) sheets = computeSheets(payload, user);
|
||||
if (!payload) return;
|
||||
sheets = computeSheets(payload, user);
|
||||
check.draw(payload, sheets); // 203-1 좌측 연결 점검 · [업데이트] 배지
|
||||
};
|
||||
|
||||
const setUser = (next: Row): void => {
|
||||
@@ -266,9 +264,6 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
const draft = projectId && !fromServer ? readState<Row>("b09-estimation", projectId) : null;
|
||||
user = draft ?? next.user ?? {};
|
||||
recompute();
|
||||
made.textContent = next.book?.made
|
||||
? `단가 굳힘 ${String(next.book.made).replace("T", " ")} — [단가 갱신] 때만 새로`
|
||||
: "단가 아직 안 굳힘 — [저장] 이나 [단가 갱신] 때 M01 값을 굳힘";
|
||||
show(current);
|
||||
};
|
||||
|
||||
@@ -297,12 +292,13 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
});
|
||||
save.dataset.b09Save = "";
|
||||
const refresh = createButton({
|
||||
label: "단가 갱신",
|
||||
label: "업데이트",
|
||||
variant: "ghost",
|
||||
onClick: () =>
|
||||
void run("/estimation/prices", "POST", "단가를 지금 M01 값으로 새로 굳혔습니다."),
|
||||
});
|
||||
refresh.dataset.b09Refresh = "";
|
||||
const check = createCheckPanel(refresh, (link) => show(link.tab, link.key));
|
||||
// 184-3 내역 초기화 — 설계내역서 덮개(user.bill) 지우고 바로 저장 → 기본 로직 내역
|
||||
const reset = createButton({
|
||||
label: "내역 초기화",
|
||||
@@ -322,8 +318,8 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
section.className = "ui-collapsible ui-sidebar-section ui-sidebar-card";
|
||||
const heading = document.createElement("h3");
|
||||
heading.className = "ui-collapsible__title";
|
||||
heading.textContent = "원가 단가";
|
||||
section.append(heading, made, refresh);
|
||||
heading.textContent = "데이터 점검";
|
||||
section.append(heading, refresh, check.list);
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "ui-sidebar-actions";
|
||||
actions.append(save, reset);
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
* B09_Estimation_UI_Tabs.ts
|
||||
* B09 탭 목록 · 표 짜기 공용(PLAN 144-1) — 탭 하나 = 화면 표 하나(실무 엑셀 탭 차례).
|
||||
*
|
||||
* 탭 = 연결 점검(목차 · 안내) · 원가 · 총괄 · 내역 · 대가 둘 · 산근 둘 · 기초 · 중기 둘 · 단가 넷 · 집계표 넷
|
||||
* 탭 = 원가 · 총괄 · 내역 · 대가 둘 · 산근 둘 · 기초 · 중기 둘 · 단가 넷 · 집계표 넷
|
||||
* 표 = 공용 `createDataTable` · 「연결 없음」 칸 = 빈칸 · 호표 칸 누름 = 그 탭 그 줄로 이동(`link`)
|
||||
* 탭별 표 = `…_UI_Tabs_Base`(144) · `…_UI_Tabs_Cost`(145) · 여기는 차례 · 꼴 · 연결 점검만
|
||||
* 탭별 표 = `…_UI_Tabs_Base`(144) · `…_UI_Tabs_Cost`(145) · 여기는 차례 · 꼴만
|
||||
* 연결 점검(옛 첫 탭) = 좌측 「데이터 점검」 묶음(203-1 `…_UI_Check`)
|
||||
* ========================================================================== */
|
||||
|
||||
import type { MapContextMenuItem } from "@ui/ui_template_context_menu";
|
||||
@@ -12,7 +13,6 @@ import type { DataColumn, DataRow } from "@ui/ui_template_data_table";
|
||||
import type { Row } from "./B09_Estimation_Calc_Base";
|
||||
|
||||
export type TabId =
|
||||
| "check"
|
||||
| "cost"
|
||||
| "total"
|
||||
| "bill"
|
||||
@@ -34,7 +34,6 @@ export type TabId =
|
||||
|
||||
/** 엑셀 탭 차례 — [id, 탭 이름] */
|
||||
export const TABS: Array<[TabId, string]> = [
|
||||
["check", "연결 점검"],
|
||||
["cost", "공사원가계산서"],
|
||||
["total", "총괄설계내역서"],
|
||||
["bill", "설계내역서"],
|
||||
@@ -59,7 +58,6 @@ export const tabName = (id: string): string => TABS.find(([t]) => t === id)?.[1]
|
||||
|
||||
/** 위 탭(181-1) — [위 탭 이름, 아래 탭들] · 아래 탭 하나뿐 = 위 탭 단독 · 첫 아래 탭 = 위 탭 키 */
|
||||
export const TAB_GROUPS: Array<[string, TabId[]]> = [
|
||||
["연결 점검", ["check"]],
|
||||
["공사원가계산서", ["cost"]],
|
||||
["총괄설계내역서", ["total"]],
|
||||
["설계내역서", ["bill"]],
|
||||
@@ -156,36 +154,3 @@ export function parseNumber(value: string): number | null | undefined {
|
||||
export function rowIndex(rows: DataRow[], key: string): number {
|
||||
return rows.findIndex((row) => row.key === key);
|
||||
}
|
||||
|
||||
/* ── 연결 점검(목차 · 안내) ───────────────────────────────────────────── */
|
||||
|
||||
export const buildCheck: TabBuild = ({ sheets, payload }): TabTable => {
|
||||
const gaps: Row[] = sheets.gaps || [];
|
||||
const rows: DataRow[] = gaps.map((g, i) => ({
|
||||
no: i + 1,
|
||||
key: `${g.tab}|${g.key}|${i}`,
|
||||
tab: tabName(g.tab),
|
||||
name: g.name,
|
||||
why: g.why,
|
||||
}));
|
||||
const notes: string[] = [...(payload.notes || [])];
|
||||
const missing: string[] = payload.inputs?.missing || [];
|
||||
if (missing.length) notes.push(`M01 에 없는 키 ${missing.length} — ${missing.join(" · ")}`);
|
||||
return {
|
||||
columns: [
|
||||
qty("no", "번호", 0),
|
||||
text("tab", "탭", 150),
|
||||
text("name", "명칭", 260),
|
||||
text("why", "까닭", 320),
|
||||
],
|
||||
rows,
|
||||
frozen: 1,
|
||||
hint:
|
||||
`연결 없음 ${gaps.length}곳 — 줄을 누르면 그 탭 그 줄로 · 빈칸 = 값을 낸 원천이 없음(0 을 채우지 않음)` +
|
||||
(notes.length ? ` · ${notes.join(" · ")}` : ""),
|
||||
link: (i) => {
|
||||
const g = gaps[i];
|
||||
return g ? { tab: g.tab as TabId, key: g.key } : null;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -51,14 +51,14 @@ export const buildLabor: TabBuild = ({ sheets, payload }) =>
|
||||
priceList(
|
||||
sheets.labor || [],
|
||||
"단가(원/인)",
|
||||
`노임 = 굳힌 단가(${madeText(payload)}) · M01 인력 · 바꾸려면 [단가 갱신]`,
|
||||
`노임 = 굳힌 단가(${madeText(payload)}) · M01 인력 · 바꾸려면 [업데이트]`,
|
||||
);
|
||||
|
||||
export const buildExpense: TabBuild = ({ sheets, payload }) =>
|
||||
priceList(
|
||||
sheets.expense || [],
|
||||
"기준가(천원)",
|
||||
`기계 손료 기준가 = 굳힌 단가(${madeText(payload)}) · M01 기계 가격 · 바꾸려면 [단가 갱신]`,
|
||||
`기계 손료 기준가 = 굳힌 단가(${madeText(payload)}) · M01 기계 가격 · 바꾸려면 [업데이트]`,
|
||||
);
|
||||
|
||||
function madeText(payload: Row): string {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* =============================================================================
|
||||
* B09_Estimation_UI_Tabs_Sangeun.ts
|
||||
* B09 단가산출근거 · 단가산출근거목록표 탭(PLAN 145-1) — 값 = 서버 `B09_Estimation_Sangeun`
|
||||
* (서버 단독 · 이 탭엔 조작 없음 · [저장] · [단가 갱신] 때 서버가 다시 셈).
|
||||
* (서버 단독 · 이 탭엔 조작 없음 · [저장] · [업데이트] 때 서버가 다시 셈).
|
||||
*
|
||||
* 목록표 = 산근 호표 한 줄(합계 · 노무비 · 재료비 · 경비 · 1 단위) · 줄 누름 = 그 블록
|
||||
* 풀이표(155-5 · 엑셀 단가산출근거 꼴) = 공종 · 산출내역(서버가 지은 텍스트 수식 한 칸) · 합계 ·
|
||||
|
||||
@@ -24,7 +24,7 @@ const HINT =
|
||||
/** 안내 + 「연결 없음 n」(전개 안 한 내역 항목 — 조합 밖 토공 · 운반 산근 등) */
|
||||
const hint = (payload: Row, extra = ""): string => {
|
||||
const n = (payload.summary || {}).unlinked || 0;
|
||||
return `${HINT}${extra}${n ? ` · 연결 없음 ${n} — 전개 안 함(연결 점검 탭)` : ""}`;
|
||||
return `${HINT}${extra}${n ? ` · 연결 없음 ${n} — 전개 안 함(좌측 데이터 점검)` : ""}`;
|
||||
};
|
||||
|
||||
/** 줄이 없을 때 표 위 맨 앞 안내 */
|
||||
|
||||
Reference in New Issue
Block a user