fix(b09): [업데이트] 때 지운 복사 정보 뺌 — 화면 캐시 기준 수요 · M01 원본 없는 줄 굳힌 값 그대로 (296-1)

- [업데이트] = 화면 캐시(user)를 같이 보냄 · 캐시 그대로 둠(지운 줄 안 되살림 · 저장 안 함)
- 서버 수요 = 캐시 줄이 잇는 서고 사본만(지운 줄의 사본 단가 줄 안 살아남)
- M01 원본이 지워진 키 = 옛 책 줄 그대로 · 낡음 까닭에서 빠짐
- 시험 test_296_b09_update_skip_deleted

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
This commit is contained in:
2026-10-12 21:54:43 +09:00
co-authored by Claude Opus 5.5
parent df574665b1
commit cf396d93ff
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));