feat(B09 펼친 일위대가 붙여넣기): Ctrl+V = 지금 호표 끝 고정 줄 · 이 공사 단가로 다시 잇기 (PLAN 705-2)
- 펼친 일위대가 표를 누르고 Ctrl+V → 서버 /estimation/daega/paste(붙인 글 · M02 와 같은 해석 · 열 짝 물음 · 기억) → 덮개 @add 줄(캐시 · [저장] 때 서버가 다시 셈) - 셈 짝(파이썬 · TS) — @add 줄을 그 cover 블록 끝에 더함(복사본 앞 · 뒤 · 같은 cid 한 번) · 짝 잃음에서 뺌 · 저장 정리 clean_added - 붙인 줄 이음 키 = 단가 책 수요(added_keys) · 답에 그 캐시로 셈한 원가 자료 - 알림 누름 = 다시 잇기 창(후보 = M02 이음 짝 · 단가 = 이 공사 단가 책 · 바꿈 = 캐시 + 서버 셈) - 시험: 거울(파이썬 = TS) · 같은 글 두 번 = 「 (2)」 가름 · 정리 · 수요 키 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,8 @@
|
||||
짝 잃은 덮개(그 블록 · 줄 없음) = `daega_orphans` [{cover, cid}] — 지우지 않고 알림만.
|
||||
복사본(703-2) = 덮개 `K:…` 의 `@copy` {from · name · spec} — 원본 cover 블록을 본뜬 새 호표
|
||||
(id `DG:K:…` · 번호 = 끝 · 줄 · 기본값 = 원본 · 덮개 = 제 것) · 원본 없음 = 짝 잃음 {cover, cid: @copy}.
|
||||
붙인 줄(705-2 · 엑셀 붙여넣기) = 덮개 `@add` [블록 줄 `fixed` · `pct` · cid `@add|…`] — 그 cover 블록 끝에 더함
|
||||
(복사본 앞 · 뒤 두 번 · 같은 cid 는 한 번 · 복사본이 원본 붙인 줄을 이어받음) · 고친 칸 덮개는 다른 줄과 같음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -35,6 +37,7 @@ PARTS = ("labor", "material", "expense")
|
||||
COVER_KEYS = ("qty", *PARTS)
|
||||
TAB = "dg"
|
||||
COPY = "@copy"
|
||||
ADD = "@add"
|
||||
WHY = {
|
||||
"labor": "노임 단가 없음",
|
||||
"material": "자재 적용 단가 없음",
|
||||
@@ -182,6 +185,26 @@ def with_copies(blocks: list[dict[str, Any]], cover: dict[str, Any]) -> list[dic
|
||||
return out
|
||||
|
||||
|
||||
def with_added(blocks: list[dict[str, Any]], cover: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""붙인 줄(705-2) — 블록 cover 의 `@add` 줄을 끝에 더함(이미 있는 cid 는 뺌)."""
|
||||
out = []
|
||||
for b in blocks:
|
||||
add = (cover.get(b.get("cover")) or {}).get(ADD)
|
||||
have = {ln.get("cid") for ln in b["lines"]}
|
||||
more = [ln for ln in add if ln.get("cid") not in have] if isinstance(add, list) else []
|
||||
out.append({**b, "lines": [*b["lines"], *more]} if more else b)
|
||||
return out
|
||||
|
||||
|
||||
def added_keys(cover: dict[str, Any] | None) -> list[str]:
|
||||
"""붙인 줄이 잇는 단가 책 키(705-2 · 수요에 더함)."""
|
||||
out: list[str] = []
|
||||
for lines in (cover or {}).values():
|
||||
for ln in (lines or {}).get(ADD) or []:
|
||||
out += [v for v in (ln.get("link") or {}).values() if v and v not in out]
|
||||
return out
|
||||
|
||||
|
||||
def compute_labor(
|
||||
daega: dict[str, Any] | None,
|
||||
sheets: dict[str, Any],
|
||||
@@ -201,7 +224,7 @@ def compute_labor(
|
||||
"sangeun": sangeun or {},
|
||||
}
|
||||
cover = cover or {}
|
||||
blocks = with_copies(blocks, cover)
|
||||
blocks = with_added(with_copies(with_added(blocks, cover), cover), cover)
|
||||
by_id = {b["id"]: b for b in blocks}
|
||||
no = {b["id"]: n for n, b in enumerate(blocks, 1)}
|
||||
done: dict[str, dict[str, Any]] = {}
|
||||
@@ -252,7 +275,9 @@ def compute_labor(
|
||||
{"cover": key, "cid": cid}
|
||||
for key, lines in cover.items()
|
||||
for cid in lines
|
||||
if cid not in cids.get(key, ()) and not (cid == COPY and f"DG:{key}" in by_id)
|
||||
if cid not in cids.get(key, ())
|
||||
and not (cid == COPY and f"DG:{key}" in by_id)
|
||||
and cid != ADD
|
||||
]
|
||||
return {
|
||||
"daega_blocks": out_blocks,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
* 있는 칸만 셈 값 대신 · 줄 base(품셈 값) · covered(고친 칸) · 짝 잃은 덮개 = daega_orphans(알림만)
|
||||
* 복사본(703-2) = 덮개 K:… 의 @copy {from · name · spec} — 원본 cover 블록을 본뜬 새 호표
|
||||
* (id DG:K:… · 번호 = 끝 · 줄 · 기본값 = 원본 · 덮개 = 제 것) · 원본 없음 = 짝 잃음 {cover, cid: @copy}
|
||||
* 붙인 줄(705-2) = 덮개 @add [블록 줄 fixed · pct · cid @add|…] — 그 cover 블록 끝에 더함(복사본 앞 · 뒤 · 같은 cid 한 번)
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
@@ -36,6 +37,8 @@ const PARTS = ["labor", "material", "expense"] as const;
|
||||
const COVER_KEYS = ["qty", ...PARTS] as const;
|
||||
const TAB = "dg";
|
||||
export const COPY = "@copy";
|
||||
/** 붙인 줄 덮개 열쇠(705-2) */
|
||||
export const ADD = "@add";
|
||||
const WHY: Record<string, string> = {
|
||||
labor: "노임 단가 없음",
|
||||
material: "자재 적용 단가 없음",
|
||||
@@ -183,6 +186,16 @@ function withCopies(blocks: Row[], covers: Row): Row[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 붙인 줄(705-2) — 블록 cover 의 @add 줄을 끝에 더함(이미 있는 cid 는 뺌) */
|
||||
function withAdded(blocks: Row[], covers: Row): Row[] {
|
||||
return blocks.map((b) => {
|
||||
const add: unknown = (covers[b.cover] || {})[ADD];
|
||||
const have = new Set((b.lines as Row[]).map((ln) => ln.cid));
|
||||
const more = Array.isArray(add) ? (add as Row[]).filter((ln) => !have.has(ln.cid)) : [];
|
||||
return more.length ? { ...b, lines: [...b.lines, ...more] } : b;
|
||||
});
|
||||
}
|
||||
|
||||
/** 대가 탭 둘 — daega_blocks(일위대가표) · daega(목록표) · daega_owners + 바탕 gaps 이어 붙임 */
|
||||
export function computeLabor(
|
||||
daega: Row | null | undefined,
|
||||
@@ -191,7 +204,10 @@ export function computeLabor(
|
||||
covers: Row = {},
|
||||
): Row {
|
||||
covers = covers || {};
|
||||
const blocks: Row[] = withCopies((daega || {}).blocks || [], covers);
|
||||
const blocks: Row[] = withAdded(
|
||||
withCopies(withAdded((daega || {}).blocks || [], covers), covers),
|
||||
covers,
|
||||
);
|
||||
const index = (rows: Row[] | undefined): Row =>
|
||||
Object.fromEntries((rows || []).map((r) => [r.key, r]));
|
||||
const books: Row = {
|
||||
@@ -244,7 +260,7 @@ export function computeLabor(
|
||||
const orphans: Row[] = [];
|
||||
for (const [key, lines] of Object.entries(covers))
|
||||
for (const cid of Object.keys(lines || {}))
|
||||
if (!cids.get(key)?.has(cid) && !(cid === COPY && `DG:${key}` in byId))
|
||||
if (!cids.get(key)?.has(cid) && !(cid === COPY && `DG:${key}` in byId) && cid !== ADD)
|
||||
orphans.push({ cover: key, cid });
|
||||
return {
|
||||
daega_blocks: outBlocks,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"""B09 펼친 일위대가 엑셀 붙여넣기(PLAN 705-2) — 붙인 글 → 지금 호표 끝 고정 줄(덮개 `@add`).
|
||||
|
||||
글 해석 · 고정 줄 = M02 와 한 벌(`M02_Hopyo_Paste.read_text` · `fixed_lines` · 705-3 이음) · 블록 줄 꼴 =
|
||||
`Labor_Source.fixed_line`(fixed · 비율 = pct) · cid `@add|고정|이름|규격|단위`(같은 이름 = 「 (2)」 가름) ·
|
||||
여러 호표를 붙여도 지금 호표에 줄로만 · 사용자 값(화면 캐시)에 더해 돌려줌 — 저장은 B09 [저장](서버가 다시 셈).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from B09_Estimation import B09_Estimation_Store as store
|
||||
from B09_Estimation.B09_Estimation_Calc_Labor import ADD
|
||||
from B09_Estimation.B09_Estimation_Labor_Source import fixed_line
|
||||
from M02_MasterTemplete import M02_Hopyo_Paste as hp
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
|
||||
|
||||
def paste(user: dict[str, Any], cover: str, text: str, cols: Any) -> dict[str, Any]:
|
||||
"""→ `{갈래: 물음, 열, 보기}` · `{갈래: 줄, 줄, 엑셀, 이음, 이음후보, user}` — 틀린 글 = ValueError."""
|
||||
parsed = hp.read_text(text, cols)
|
||||
if parsed["물음"]:
|
||||
look = [[c[:24] for c in row[:16]] for row in hp.cells(text)[:8]]
|
||||
return {"갈래": "물음", "열": parsed["열"], "짐작": True, "보기": look}
|
||||
user = store.clean_user(user)
|
||||
lines = dict((user.get("daega_cover") or {}).get(cover) or {})
|
||||
have = list(lines.get(ADD) or [])
|
||||
taken = {str(x["cid"]).removeprefix(f"{ADD}|") for x in have}
|
||||
got = hp.fixed_lines(parsed, taken)
|
||||
new = [
|
||||
{**fixed_line(x["고정"], x["수량"]["손"]), "cid": f"{ADD}|{fixed.fixed_id(x['고정'])}"}
|
||||
for x in got["줄"]
|
||||
]
|
||||
lines[ADD] = [*have, *new]
|
||||
user = store.clean_user(
|
||||
{**user, "daega_cover": {**(user.get("daega_cover") or {}), cover: lines}}
|
||||
)
|
||||
head = {k: got[k] for k in ("엑셀", "이음", "이음후보")}
|
||||
return {"갈래": "줄", "열": parsed["열"], "줄": len(new), **head, "user": user}
|
||||
@@ -71,6 +71,7 @@ from B08_Quantity.B08_Quantity_Router_Material import material_sheets
|
||||
from B08_Quantity.B08_Quantity_Router_Milestone import read_data as read_milestone
|
||||
from B08_Quantity.B08_Quantity_Router_Milestone import site_info
|
||||
from B09_Estimation import B09_Estimation_Bill_Library as library
|
||||
from B09_Estimation import B09_Estimation_Daega_Paste as daega_paste
|
||||
from B09_Estimation import B09_Estimation_Daega_Personal as personal
|
||||
from B09_Estimation import B09_Estimation_Past as past
|
||||
from B09_Estimation import B09_Estimation_Prices as prices
|
||||
@@ -80,7 +81,7 @@ from B09_Estimation import B09_Estimation_Store as store
|
||||
from B09_Estimation.B09_Estimation_Bill_Items import bill_items, hopyo_prices
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import compute_base
|
||||
from B09_Estimation.B09_Estimation_Calc_Bill import compute_estimation
|
||||
from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor, daega_price
|
||||
from B09_Estimation.B09_Estimation_Calc_Labor import added_keys, compute_labor, daega_price
|
||||
from B09_Estimation.B09_Estimation_Calc_Total import compute_total
|
||||
from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs
|
||||
from B09_Estimation.B09_Estimation_Linked import linked_resource_keys
|
||||
@@ -209,7 +210,8 @@ async def _demand(root: Path, draft: dict[str, Any] | None = None) -> list[str]:
|
||||
keep = None if draft is None else library.used_folders(user)
|
||||
keys = await asyncio.to_thread(linked_resource_keys, root, keep)
|
||||
hand = await asyncio.to_thread(sangeun.hand_resource_keys, user)
|
||||
return [*keys, *(k for k in hand if k not in keys)]
|
||||
hand = [*hand, *added_keys(user.get("daega_cover"))] # 705-2 붙인 줄 이음
|
||||
return [*keys, *dict.fromkeys(k for k in hand if k not in keys)]
|
||||
|
||||
|
||||
async def payload(
|
||||
@@ -497,6 +499,34 @@ async def save_personal(
|
||||
}
|
||||
|
||||
|
||||
class DaegaPasteBody(BaseModel):
|
||||
"""705-2 — `cover` = 펼친 호표 덮개 자리 · `글` = 붙인 글 그대로 · `열` = 기억한 열 짝 · `user` = 화면 캐시."""
|
||||
|
||||
cover: str = Field(min_length=1, max_length=200)
|
||||
글: str = Field(max_length=400_000)
|
||||
열: dict[str, int] | None = None
|
||||
user: dict[str, Any] | None = None
|
||||
|
||||
|
||||
@router.post("/{project_id}/estimation/daega/paste")
|
||||
async def daega_paste_run(
|
||||
project_id: UUID, body: DaegaPasteBody, session: dict[str, Any] = Depends(verify_session)
|
||||
) -> dict[str, Any]:
|
||||
"""[Ctrl+V] 펼친 일위대가 — 지금 호표 끝에 고정 줄(저장 안 함) · 답 = 새 사용자 값 + 그 값으로 셈한 원가
|
||||
자료(붙인 줄 이음 키가 단가 책 수요에 듦) · 물음 = 열 짝."""
|
||||
root = await _project_root(project_id)
|
||||
raw = (
|
||||
body.user if isinstance(body.user, dict) else await asyncio.to_thread(store.read_user, root)
|
||||
)
|
||||
try:
|
||||
got = await asyncio.to_thread(daega_paste.paste, raw, body.cover, body.글, body.열)
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=400, detail=str(error)) from error
|
||||
if got["갈래"] == "줄":
|
||||
got["payload"] = await payload(project_id, root, got["user"])
|
||||
return got
|
||||
|
||||
|
||||
def _past_rows(projects: list[dict[str, Any]], here: str) -> list[dict[str, Any]]:
|
||||
out = []
|
||||
for other in projects:
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"daega_cover": {블록 cover: {줄 cid: {qty?, labor?, material?, expense?}}}
|
||||
(702-2 일위대가 고친 칸 덮개 — 수만 · 셈 = `B09_Estimation_Calc_Labor`)
|
||||
+ 복사본 `K:…` 의 `@copy` {from · name · spec}(703-2 · 글만)
|
||||
+ 붙인 줄 `@add` [블록 줄 fixed · pct](705-2 · `clean_added`)
|
||||
|
||||
quotes/<자재 키>/ 견적서 파일(166-4 · 여러 개) — 폴더가 목록(키의 못 쓰는 글자는 %XX) ·
|
||||
읽을 때 materials[키].files = 이름들 · 사용자 값으로는 저장 안 함
|
||||
@@ -32,7 +33,7 @@ from urllib.parse import unquote
|
||||
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import SOURCES, SUPPLY
|
||||
from B09_Estimation.B09_Estimation_Calc_Bill import SECTIONS
|
||||
from B09_Estimation.B09_Estimation_Calc_Labor import COVER_KEYS
|
||||
from B09_Estimation.B09_Estimation_Calc_Labor import ADD, COVER_KEYS, PARTS
|
||||
|
||||
try: # 252 공기 초안(draft 폴더 · 지우면 공기 값 없이)
|
||||
from B09_Estimation.draft.B09_Draft_Schedule_Source import clean_schedule
|
||||
@@ -235,6 +236,10 @@ def clean_cover(raw: Any) -> dict[str, Any]:
|
||||
kept = {}
|
||||
for cid, cell in (lines if isinstance(lines, dict) else {}).items():
|
||||
cell = cell if isinstance(cell, dict) else {}
|
||||
if cid == ADD:
|
||||
if got := clean_added(lines.get(ADD)):
|
||||
kept[cid] = got
|
||||
continue
|
||||
if cid == "@copy":
|
||||
if _text(cell.get("from")):
|
||||
kept[cid] = {k: _text(cell[k]) for k in ("from", "name", "spec") if k in cell}
|
||||
@@ -247,6 +252,33 @@ def clean_cover(raw: Any) -> dict[str, Any]:
|
||||
return out
|
||||
|
||||
|
||||
ADD_MAX = 500
|
||||
|
||||
|
||||
def clean_added(raw: Any) -> list[dict[str, Any]]:
|
||||
"""붙인 줄(705-2) — fixed = 이름 · 규격 · 단위 · 수량 · 붙인 단가(own) · 이음 키(link) · pct = 비목 · 밑 ·
|
||||
율 · cid `@add|…` 만 · 모르는 칸 버림 · 숫자 아닌 단가 = 0."""
|
||||
out: list[dict[str, Any]] = []
|
||||
for ln in raw[:ADD_MAX] if isinstance(raw, list) else []:
|
||||
if not isinstance(ln, dict) or not str(ln.get("cid") or "").startswith(f"{ADD}|"):
|
||||
continue
|
||||
head = {k: _text(ln.get(k))[:200] for k in ("cid", "name", "spec", "unit")}
|
||||
if ln.get("kind") == "pct" and ln.get("part") in PARTS:
|
||||
base = str(ln.get("base") or "")
|
||||
if base.removeprefix("part:") in PARTS:
|
||||
out.append({**head, "kind": "pct", "part": ln["part"], "base": base,
|
||||
"qty": _num(ln.get("qty"))}) # fmt: skip
|
||||
continue
|
||||
if ln.get("kind") != "fixed":
|
||||
continue
|
||||
own, link = ln.get("own") or {}, ln.get("link") or {}
|
||||
out.append({**head, "kind": "fixed", "qty": _num(ln.get("qty")),
|
||||
"own": {p: _num(own.get(p)) or 0 for p in PARTS if isinstance(own, dict)},
|
||||
"link": {k: _text(link.get(k))[:40] or None for k in ("labor", "material", "machine")
|
||||
if isinstance(link, dict)}}) # fmt: skip
|
||||
return out
|
||||
|
||||
|
||||
def clean_user(raw: Any) -> dict[str, Any]:
|
||||
"""사용자 값 다듬기 — 모르는 칸 버림 · 수 칸은 수 · null(빈칸)만."""
|
||||
raw = raw if isinstance(raw, dict) else {}
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
* 고친 칸 = 칸 색(`is-edited`) · 「품셈 값」 칸 = 셈 값 · 우클릭 [품셈 값으로] · 빈칸 입력 = 되돌림
|
||||
* 일위대가 연결(dg) 없는 줄 = 펼치지 않음 · 고른 줄 = 이 파일이 기억(다시 그려도 그대로)
|
||||
* 우클릭 덧메뉴(703-2) = 복사해 새 일위대가 · 내 자료집에 저장 · 어디 쓰였나(`…_Bill_Fold_Menu`)
|
||||
* 엑셀 붙여넣기(705-2) = 표를 누르고 Ctrl+V → 서버가 붙인 글을 읽어 지금 호표 끝 고정 줄(덮개 `@add` · 캐시 ·
|
||||
* [저장] 때 서버가 다시 셈) · 열 짝 = M02 와 같은 물음 · 기억 · 알림 누름 = 「이 공사 단가로 다시 잇기」(이 공사 단가 책)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
@@ -22,6 +24,16 @@ import type { Row } from "./B09_Estimation_Calc_Base";
|
||||
import { money, parseNumber, qty, text, type TabContext } from "./B09_Estimation_UI_Tabs";
|
||||
import { attachRowMenu } from "./B09_Estimation_UI_Tabs_Bill_Edit";
|
||||
import { foldMenu } from "./B09_Estimation_UI_Tabs_Bill_Fold_Menu";
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { hideLoadingOverlay, showLoadingOverlay, showToast } from "@ui/ui_template_toast";
|
||||
import { currentProjectId } from "../A00_Common/b_project_switch";
|
||||
import { ADD } from "./B09_Estimation_Calc_Labor";
|
||||
import { withCols } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Paste";
|
||||
import {
|
||||
openRelink,
|
||||
type RelinkSource,
|
||||
} from "../M02_MasterTemplete/M02_MasterTemplete_Hopyo_Relink";
|
||||
import { relinkRows, type PasteCols } from "../M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api";
|
||||
|
||||
/** 고칠 칸 → 덮개 칸 */
|
||||
const FIELD: Record<string, string> = {
|
||||
@@ -210,9 +222,126 @@ function foldCard(
|
||||
],
|
||||
});
|
||||
card.classList.add("b09-fold");
|
||||
if (block.cover) {
|
||||
table.root.tabIndex = 0; // 705-2 — 눌러 고르면 Ctrl+V 받음
|
||||
card.addEventListener("paste", (ev) => {
|
||||
if ((ev.target as Element | null)?.closest("input, textarea")) return;
|
||||
const text = ev.clipboardData?.getData("text/plain") ?? "";
|
||||
if (!text.trim()) return;
|
||||
ev.preventDefault();
|
||||
void pasteFold(ctx, block.cover, text);
|
||||
});
|
||||
}
|
||||
return card;
|
||||
}
|
||||
|
||||
async function post(path: string, body: Row): Promise<Row> {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/projects/${currentProjectId()}/estimation/${path}`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
);
|
||||
const got = ((await response.json().catch(() => null)) as Row | null) ?? {};
|
||||
if (!response.ok)
|
||||
throw new Error(typeof got.detail === "string" ? got.detail : `HTTP ${response.status}`);
|
||||
return got;
|
||||
}
|
||||
|
||||
const COST_PART = { 노무비: "labor", 재료비: "material", 경비: "expense" } as const;
|
||||
|
||||
/** 붙인 줄 다시 잇기(705-2) — 후보 = 서버(M02 이음 짝) · 단가 = 이 공사 단가 책 · 바꿈 = 캐시 + 서버 셈 */
|
||||
function relinkFold(ctx: TabContext, cover: string): RelinkSource {
|
||||
const added = (): Row[] => ((ctx.user.daega_cover || {})[cover] || {})[ADD] || [];
|
||||
const book = (kind: "노임" | "자재", key: string): number | null | undefined =>
|
||||
(ctx.payload.inputs?.[kind === "노임" ? "labor" : "materials"] || []).find(
|
||||
(r: Row) => r.key === key,
|
||||
)?.price;
|
||||
return {
|
||||
title: "이 공사 단가로 다시 잇기 — 붙인 줄",
|
||||
priceLabel: "이 공사 단가",
|
||||
price: book,
|
||||
rows: () =>
|
||||
relinkRows(
|
||||
{ layer: "personal", owner: null },
|
||||
{
|
||||
문서: {
|
||||
담은로직: added()
|
||||
.filter((ln) => ln.kind === "fixed")
|
||||
.map((ln) => ({
|
||||
고정: {
|
||||
이름: ln.name,
|
||||
규격: ln.spec,
|
||||
단위: ln.unit,
|
||||
단가: Object.fromEntries(
|
||||
Object.entries(COST_PART).map(([k, p]) => [k, ln.own?.[p] ?? 0]),
|
||||
),
|
||||
이음: { 노임: ln.link?.labor ?? null, 자재: ln.link?.material ?? null },
|
||||
},
|
||||
수량: { 손: ln.qty },
|
||||
})),
|
||||
},
|
||||
},
|
||||
),
|
||||
apply: async (changes) => {
|
||||
const lines: Row[] = JSON.parse(JSON.stringify(added()));
|
||||
const back = [];
|
||||
for (const c of changes) {
|
||||
const ln = lines.find((x) => x.cid === `${ADD}|${c.줄}`);
|
||||
if (!ln) continue;
|
||||
back.push({
|
||||
키: "",
|
||||
줄: c.줄,
|
||||
노임: ln.link?.labor ?? null,
|
||||
자재: ln.link?.material ?? null,
|
||||
});
|
||||
ln.link = { ...(ln.link || {}) };
|
||||
if ("노임" in c) ln.link.labor = c.노임 ?? null;
|
||||
if ("자재" in c) ln.link.material = c.자재 ?? null;
|
||||
}
|
||||
const all: Row = JSON.parse(JSON.stringify(ctx.user.daega_cover || {}));
|
||||
all[cover] = { ...(all[cover] || {}), [ADD]: lines };
|
||||
const user = { ...ctx.user, daega_cover: all };
|
||||
Object.assign(ctx.payload, await post("calc", { user })); // 새 이음 키 = 이 공사 단가 책 수요
|
||||
ctx.setUser(user);
|
||||
return { back, note: `바뀐 줄 ${back.length} — [저장]으로 남김` };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 붙여넣기(705-2) — 붙인 글 그대로 서버로 · 답 = 새 캐시 + 그 캐시로 셈한 원가 자료 */
|
||||
async function pasteFold(ctx: TabContext, cover: string, text: string): Promise<void> {
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
const got = await withCols(
|
||||
(cols: PasteCols | null) =>
|
||||
post("daega/paste", {
|
||||
cover,
|
||||
글: text,
|
||||
...(cols ? { 열: cols } : {}),
|
||||
user: ctx.user,
|
||||
}) as Promise<Row & { 갈래: "줄" }>,
|
||||
);
|
||||
if (!got) return;
|
||||
Object.assign(ctx.payload, got.payload);
|
||||
ctx.setUser(got.user);
|
||||
const won = Math.round(Number(got.엑셀) || 0).toLocaleString("ko-KR");
|
||||
showToast(
|
||||
`고정 줄 ${got.줄} 붙임 · 엑셀 합계 ${won} · 이음 ${got.이음} · 후보 ${got.이음후보} — [저장]으로 남김 · 이 알림을 누르면 이 공사 단가로 다시 잇기`,
|
||||
"success",
|
||||
9000,
|
||||
() => void openRelink(relinkFold(ctx, cover)),
|
||||
);
|
||||
} catch (error) {
|
||||
showToast(`붙여넣기 실패 — ${error instanceof Error ? error.message : String(error)}`, "error");
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
/** 펼침 고리 — 설계내역서 표 root 줄 누름 = 그 줄 펼침 · 돌려준 자리 = 표 아래 */
|
||||
export function attachBillFold(ctx: TabContext, raw: DataRow[], table: HTMLElement): HTMLElement {
|
||||
const host = document.createElement("div");
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
층은 임시 폴더(공용 storage 안 씀).
|
||||
"""
|
||||
|
||||
import json
|
||||
import warnings
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
@@ -214,3 +215,61 @@ def test_다시_잇기_열린_문서_후보만(client):
|
||||
doc = {"담은로직": [{"고정": fx, "수량": {"손": 0.5}}]}
|
||||
(row,) = client.post(f"{RELINK}/rows", json={"문서": doc}).json()["줄"]
|
||||
assert row["키"] is None and row["후보"][0]["비목"] == "노임" and row["후보"][0]["단가"] > 1
|
||||
|
||||
|
||||
# ── B09 펼친 일위대가 붙여넣기(덮개 @add) ─────────────────────────────────────
|
||||
def _b09():
|
||||
from resources.tester import test_145_2_b09_labor_calc as b09
|
||||
|
||||
return b09
|
||||
|
||||
|
||||
def _added(user):
|
||||
from B09_Estimation import B09_Estimation_Daega_Paste as dp
|
||||
|
||||
text = "\n".join(
|
||||
[
|
||||
"석공\t\t0.08\t인\t258,935\t20,714.8\t258,935\t20,714.8\t\t\t\t\t",
|
||||
"잡재료\t\t1\t식\t1,000\t1,000\t\t\t1,000\t1,000\t\t\t",
|
||||
"공구손료\t노무비의\t3\t%\t20,714.8\t621.4\t\t\t\t\t621.4\t621.4\t",
|
||||
]
|
||||
)
|
||||
cols = {"품명": 0, "규격": 1, "수량": 2, "단위": 3, "금액": 5, "노": 6, "재": 8, "경": 10}
|
||||
return dp.paste(user, "C:DG9", text, cols)
|
||||
|
||||
|
||||
def test_B09_붙인_줄_거울_정리_수요():
|
||||
from B09_Estimation.B09_Estimation_Calc_Labor import ADD, added_keys, compute_labor
|
||||
from B09_Estimation.B09_Estimation_Store import clean_user
|
||||
|
||||
b09 = _b09()
|
||||
got = _added({})
|
||||
assert got["갈래"] == "줄" and got["줄"] == 3 and got["엑셀"] == 22335
|
||||
cover = got["user"]["daega_cover"]
|
||||
lines = cover["C:DG9"][ADD]
|
||||
assert [ln["kind"] for ln in lines] == ["fixed", "fixed", "pct"]
|
||||
assert all(ln["cid"].startswith("@add|고정|") for ln in lines)
|
||||
again = _added(got["user"]) # 같은 글 두 번 = 이름 「 (2)」 가름 · 줄 더함
|
||||
names = [ln["name"] for ln in again["user"]["daega_cover"]["C:DG9"][ADD]]
|
||||
assert names[3:] == ["석공 (2)", "잡재료 (2)", "공구손료 (2)"]
|
||||
assert clean_user(again["user"]) == again["user"] # 저장 정리 그대로
|
||||
bad = clean_user(
|
||||
{"daega_cover": {"C:x": {ADD: [{"cid": "x", "kind": "fixed"}, {"kind": "?"}]}}}
|
||||
)
|
||||
assert "daega_cover" not in bad # cid 머리 틀림 · 모르는 갈래 버림
|
||||
linked = {**lines[0], "link": {"labor": "L00005", "material": None, "machine": None}}
|
||||
cover = {"C:DG9": {ADD: [linked, *lines[1:]], linked["cid"]: {"qty": 0.1}}}
|
||||
assert added_keys(cover) == ["L00005"]
|
||||
blocks = json.loads(json.dumps(b09.EXCEL))
|
||||
for b in blocks["blocks"]:
|
||||
b["cover"] = f"C:{b['id']}"
|
||||
for n, ln in enumerate(b["lines"]):
|
||||
ln["cid"] = f"{ln['kind']}:{ln.get('key')}#{n}"
|
||||
py = json.loads(json.dumps(compute_labor(blocks, b09.SHEETS, b09.SANGEUN, cover)))
|
||||
ts = b09._ts(blocks, b09.SHEETS, b09.SANGEUN, cover)
|
||||
assert b09._same(py, ts["out"])
|
||||
dg9 = b09._block(py, "DG9")
|
||||
stone = next(ln for ln in dg9["lines"] if ln["cid"] == linked["cid"])
|
||||
assert stone["prices"]["labor"] == 258935 and stone["qty"] == 0.1 # 이은 단가 · 덮개
|
||||
pct = next(ln for ln in dg9["lines"] if ln["kind"] == "pct" and ln["cid"].startswith(ADD))
|
||||
assert pct["amounts"]["expense"] > 0 and py["daega_orphans"] == []
|
||||
|
||||
Reference in New Issue
Block a user