Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1

This commit is contained in:
2026-10-12 21:55:31 +09:00
5 changed files with 143 additions and 26 deletions
+4 -3
View File
@@ -66,9 +66,9 @@ def used_keys(folder: Path) -> list[str]:
return seen
def linked_resource_keys(project_root: str | Path) -> list[str]:
def linked_resource_keys(project_root: str | Path, keep: set[str] | None = None) -> list[str]:
"""연결된 사본 전부가 부른 자원 키 — 구조물 id 차례 → 설계내역서 서고 사본(204-2) ·
겹침 없음."""
겹침 없음 · `keep` = 화면 캐시 줄이 잇는 서고 사본만(296 · 지운 줄의 사본 뺌)."""
from B09_Estimation.B09_Estimation_Bill_Library import copies
base = structures_dir(project_root)
@@ -78,7 +78,8 @@ def linked_resource_keys(project_root: str | Path) -> list[str]:
if "일위대가" in linked.get("부분", [])
]
out: list[str] = []
for folder in [*folders, *(f for f, _ in copies(project_root))]:
libs = [f for f, _ in copies(project_root) if keep is None or f.name in keep]
for folder in [*folders, *libs]:
for key in used_keys(folder):
if key not in out:
out.append(key)
+7 -3
View File
@@ -182,14 +182,17 @@ def _convert(master, key: str) -> dict[str, Any]:
return material_input(row)
def refresh_book(keys: list[str]) -> dict[str, Any]:
"""[단가 갱신] — 수요 키 전부를 지금 M01 값으로 새로 굳힘."""
def refresh_book(keys: list[str], book: dict[str, Any] | None = None) -> dict[str, Any]:
"""[단가 갱신] — 수요 키 전부를 지금 M01 값으로 새로 굳힘 ·
M01 원본이 지워진 키(296) = 옛 책 줄 그대로(지우지도 바꾸지도 않음)."""
master = _master()
want = demand(master, keys)
rows = {
key: _convert(master, key)
for key in [*want["labor"], *want["machines"], *want["materials"]]
}
old = (book or {}).get("rows") or {}
rows.update({key: old[key] for key in want["missing"] if key in old})
return {
"made": datetime.now().isoformat(timespec="seconds"),
"rows": rows,
@@ -202,7 +205,8 @@ 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)로 견줌
# 저장 꼴(JSON)로 견줌 · M01 원본이 지워진 줄 = 옛 줄 그대로(낡음 까닭에서 빠짐 · 296)
fresh = json.loads(json.dumps(refresh_book(keys, book), ensure_ascii=False))
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())
+25 -16
View File
@@ -4,8 +4,9 @@
입력(굳힌 단가 · 수요 · B08 수량) + 사용자 값 → 표 전부
PUT /api/projects/{id}/estimation {user}
[저장] — 사용자 값 · 책에 없는 새 단가 줄만 굳힘 → 서버가 정본으로 다시 셈
POST /api/projects/{id}/estimation/prices
[단가 갱신] — 수요 단가 전부를 지금 M01 값으로 새로 굳힘
POST /api/projects/{id}/estimation/prices {user?}
[단가 갱신] — 수요 단가 전부를 지금 M01 값으로 새로 굳힘 · user = 화면 캐시(296 ·
지운 줄 · 그 사본은 수요에서 뺌 · 저장은 안 함) · M01 원본이 지워진 줄 = 굳힌 값 그대로
POST /api/projects/{id}/estimation/logic-preview {code, given, rate, picks}
「운반 단가」 모달 계산 절차(253-3) — 그 로직 한 산근을 프로젝트 단가로 풀어 글 줄 · 단가 ·
저장 안 함(`render_sangeun_text` 를 `Sangeun.Solver` 로) · `blocks` = 칸 나눔(277-2)
@@ -138,26 +139,29 @@ def haul_user(rows: list[dict[str, Any]]) -> dict[str, Any]:
}
def calc_user(root: Path) -> dict[str, Any]:
"""셈 · 수요 · 사본 정리용 사용자 값 — 저장 값 + 거리이정표 운반 연결 · 거리(화면엔 안 보냄) ·
운반 사본 연결 = 지금 거리의 사본(263-2)."""
user = {**store.read_user(root), **haul_user(haul_routes(root))}
def calc_user(root: Path, draft: dict[str, Any] | None = None) -> dict[str, Any]:
"""셈 · 수요 · 사본 정리용 사용자 값 — 저장 값(draft = 화면 캐시 · 296) + 거리이정표 운반 연결 ·
거리(화면엔 안 보냄) · 운반 사본 연결 = 지금 거리의 사본(263-2)."""
user = {**(store.read_user(root) if draft is None else draft), **haul_user(haul_routes(root))}
return library.resolve_hauls(root, user)
async def _demand(root: Path) -> list[str]:
async def _demand(root: Path, draft: dict[str, Any] | None = None) -> list[str]:
"""단가 수요 키 — 연결된 사본 + 설계내역서 손 줄 로직(166-2) +
거리이정표 운반 연결(251-5)."""
user = await asyncio.to_thread(calc_user, root) # 운반 값 사본 먼저(263-2)
keys = await asyncio.to_thread(linked_resource_keys, root)
거리이정표 운반 연결(251-5) · draft = 화면 캐시(296 · 서고 사본 = 그 줄이 잇는 것만)."""
user = await asyncio.to_thread(calc_user, root, draft) # 운반 값 사본 먼저(263-2)
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)]
async def payload(project_id: UUID, root: Path) -> dict[str, Any]:
async def payload(
project_id: UUID, root: Path, draft: dict[str, Any] | None = None
) -> dict[str, Any]:
notes: list[str] = []
b08 = await _b08(project_id, notes)
keys = await _demand(root)
keys = await _demand(root, draft)
if not keys:
notes.append(
"연결된 구조물 조합 사본 없음 — 노무 · 경비 · 중기 단가 줄은 사본이 연결되면 생김"
@@ -213,12 +217,17 @@ async def save_estimation(project_id: UUID, body: dict[str, Any] = Body(...)) ->
@router.post("/{project_id}/estimation/prices")
async def refresh_prices(project_id: UUID) -> dict[str, Any]:
async def refresh_prices(
project_id: UUID, body: dict[str, Any] | None = Body(None)
) -> dict[str, Any]:
root = await _project_root(project_id)
keys = await _demand(root)
book = await asyncio.to_thread(prices.refresh_book, keys)
raw = (body or {}).get("user")
draft = store.clean_user(raw) if isinstance(raw, dict) else None # 296 화면 캐시(저장 안 함)
keys = await _demand(root, draft)
old = await asyncio.to_thread(store.read_book, root)
book = await asyncio.to_thread(prices.refresh_book, keys, old)
await asyncio.to_thread(store.write_book, root, book)
return await payload(project_id, root)
return await payload(project_id, root, draft)
class PreviewBody(BaseModel):
+17 -4
View File
@@ -312,7 +312,14 @@ export function renderB09Estimation(root: HTMLElement): void {
show(current);
};
const run = async (path: string, method: string, done: string, init?: Row): Promise<void> => {
// keep = 화면 캐시 그대로(296 [업데이트] — 지운 줄 · 고친 칸을 안 되살림)
const run = async (
path: string,
method: string,
done: string,
init?: Row,
keep = false,
): Promise<void> => {
if (!projectId) return;
showLoadingOverlay();
try {
@@ -320,8 +327,8 @@ export function renderB09Estimation(root: HTMLElement): void {
method,
...(init ? { body: JSON.stringify(init) } : {}),
});
clearState("b09-estimation", projectId);
apply(next, true);
if (!keep) clearState("b09-estimation", projectId);
apply(next, !keep);
showToast(done, "success");
} catch (error) {
showToast(`하지 못했습니다 — ${String(error)}`, "error");
@@ -340,7 +347,13 @@ export function renderB09Estimation(root: HTMLElement): void {
label: "업데이트",
variant: "ghost",
onClick: () =>
void run("/estimation/prices", "POST", "단가를 지금 M01 값으로 새로 굳혔습니다."),
void run(
"/estimation/prices",
"POST",
"단가를 지금 M01 값으로 새로 굳혔습니다.",
{ user },
true,
),
});
refresh.dataset.b09Refresh = "";
const check = createCheckPanel(refresh, (link) => show(link.tab, link.key));
@@ -0,0 +1,90 @@
"""296 B09 [업데이트] — 지워진 복사 정보는 업데이트에서 뺌(요구 190-4).
- ① B09 에서 지운 복사 항목 = 화면 캐시(user)를 같이 보냄 → 그 줄이 안 잇는 서고 사본 =
수요에서 뺌 · 책에 안 살아남 · 캐시 그대로(지운 줄 안 되살림) · 저장은 안 함
- ② M01 원본이 지워진 키 = 옛 책 줄 그대로(지우지도 바꾸지도 않음) · 낡음 까닭에서 빠짐
- 나머지 = 지금처럼 M01 값으로 새로 굳힘 · user 안 보내면 옛 길 그대로
임시 층(견본 마스터 · 임시 storage) · 실제 마스터 · 프로젝트는 안 씀.
"""
import asyncio
import json
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 B09_Estimation import B09_Estimation_Bill_Library as library # noqa: E402
from B09_Estimation import B09_Estimation_Prices as prices # noqa: E402
from B09_Estimation import B09_Estimation_Router as router # noqa: E402
from B09_Estimation import B09_Estimation_Store as store_b09 # noqa: E402
from B09_Estimation.B09_Estimation_Linked import linked_resource_keys # noqa: E402
from resources.tester.test_204_2_b09_bill_library import COMBO, world # noqa: E402, F401
GONE = "LB999999" # M01 에 없는 키(원본 지워짐)
def _json(book):
return json.loads(json.dumps(book, ensure_ascii=False))
def _refresh(monkeypatch, root, body):
seen = {}
async def fake_root(_):
return root
async def fake_payload(_, at, draft=None):
seen["draft"] = draft
return {}
monkeypatch.setattr(router, "_project_root", fake_root)
monkeypatch.setattr(router, "payload", fake_payload)
asyncio.run(router.refresh_prices("00000000-0000-0000-0000-000000000001", body))
return store_b09.read_book(root), seen["draft"]
def test_지운_복사_항목_업데이트_뒤_안_살아남(world, monkeypatch): # noqa: F811
root = world["root"]
a = library.import_copy(root, "precombo", "system", None, world["key"])["폴더"]
b = library.import_copy(root, "combo", "system", None, COMBO)["폴더"]
keep_a = set(prices.refresh_book(linked_resource_keys(root, {a}))["rows"])
every = set(prices.refresh_book(linked_resource_keys(root))["rows"])
assert keep_a < every # b 사본만 부르는 줄이 있음
# 화면 캐시 = b 줄을 지움(아직 [저장] 안 함 · 사본 폴더는 남음)
ref = {"tab": "dg", "key": "DG:x", "lib": a}
user = {"bill": {"v": 2, "rows": [{"key": "hand:1", "name": "줄", "qty": 1, "ref": ref}]}}
book, draft = _refresh(monkeypatch, root, {"user": user})
assert set(book["rows"]) == keep_a # 지운 사본 단가 줄 안 살아남
assert draft["bill"]["rows"][0]["ref"]["lib"] == a # 응답 셈도 캐시로
assert not (store_b09.folder(root) / store_b09.USER).exists() # 저장 안 함
assert {f.name for f, _ in library.copies(root)} == {a, b} # 사본 정리는 [저장] 몫
assert prices.book_stale(_json(book), linked_resource_keys(root, {a})) is None
# user 안 보냄 = 옛 길(다 쓴 사본 전부)
book, draft = _refresh(monkeypatch, root, None)
assert set(book["rows"]) == every and draft is None
def test_M01_원본_없는_항목_값_그대로_낡음_까닭_빠짐():
keys = ["EQ000004", GONE] # 불도저 · 조종원 · 경유(test_144 견본) + 원본 없는 키
old = _json(prices.refresh_book(["EQ000004"]))
kept = {"key": GONE, "code": "9999", "name": "지워진 직종", "price": 123.0}
old["rows"][GONE] = kept
old["rows"]["LB000048"] = {**old["rows"]["LB000048"], "price": 1} # M01 에서 바뀐 단가
assert prices.book_stale(old, keys) == "M01 에서 바뀐 단가 1" # 원본 없는 줄 = 까닭에 없음
new = _json(prices.refresh_book(keys, old))
assert new["rows"][GONE] == kept # 지우지도 바꾸지도 않음
assert new["rows"]["LB000048"]["price"] not in (None, 1) # 나머지 = 새로 굳힘
assert set(new["rows"]) == set(old["rows"])
assert prices.book_stale(new, keys) is None
assert GONE not in prices.refresh_book(keys)["rows"] # 옛 책 없으면 줄 없음(옛 길)
def test_화면_업데이트_캐시_보냄_그대로_둠():
shell = (ROOT / "B09_Estimation/B09_Estimation_UI_Shell.ts").read_text("utf-8")
at = shell.index('"/estimation/prices",')
assert "{ user },\n true," in shell[at : at + 200]
assert 'if (!keep) clearState("b09-estimation", projectId);' in shell
assert "apply(next, !keep);" in shell