feat(B09 데이터 점검): [업데이트] 한 곳 — 단가 다시 굳힘 + 원본 바뀐 구조물 사본 재적용 · 미리 보기 창 (PLAN 703-3)
- POST …/estimation/prices = 연결된 구조물 사본 중 원본이 바뀐 것만 재적용(B09_Estimation_Refresh · 막힌 사본 = 알림 · 나머지 함) → 수요 다시 → 단가 책 다시 굳힘 · 덮개 그대로 · 답 notes 첫 줄 「단가 줄 N 바뀜 · 사본 K · 고친 칸 M 그대로」 - POST …/estimation/prices/preview = 바뀌는 단가 줄(새 · 값 바뀜) · 다시 가져올 사본 · 지킬 고친 칸 · 노임 판 전 → 후(쓰지 않음) - 단가 책에 노임 판(labor_edition · 구분마다 M01 인력 판) · 책 견줌 book_diff(book_stale 도 같이 씀) - UI_Check = [업데이트] 누름을 먼저 잡아 미리 보기 창 → [적용] 이면 원래 단추 일 · 호버 글 - 시험 test_703_3_b09_refresh(미리 보기 수 = 적용 수 · 판 · 재적용 원본 바뀐 것만 · 덮개 그대로) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -198,6 +198,31 @@ def refresh_book(keys: list[str], book: dict[str, Any] | None = None) -> dict[st
|
||||
"rows": rows,
|
||||
"driver_factor": driver_factor(master),
|
||||
"fx": fx_inputs(master),
|
||||
"labor_edition": labor_editions(keys), # 703-3 미리 보기 「노임 판 전 → 후」
|
||||
}
|
||||
|
||||
|
||||
def labor_editions(keys: list[str]) -> dict[str, str]:
|
||||
"""노임 판(703-3) — 수요 노무 줄 구분(건설업 · 산림 …)마다 M01 `인력.json` 그 구분 `판`."""
|
||||
files, master = m01.loaded()
|
||||
groups = (files.get("인력.json") or {}).get("구분") or {}
|
||||
out: dict[str, str] = {}
|
||||
for key in demand(master, keys)["labor"]:
|
||||
kind = str((_row(master, key) or {}).get("구분") or "")
|
||||
if (groups.get(kind) or {}).get("판"):
|
||||
out[kind] = str(groups[kind]["판"])
|
||||
return out
|
||||
|
||||
|
||||
def book_diff(old: dict[str, Any] | None, new: dict[str, Any]) -> dict[str, int | bool]:
|
||||
"""두 책 견줌(저장 꼴 JSON) — 새 줄 · 바뀐 줄 수 · 운전사 계수 · 환율 바뀜(203-1 · 703-3)."""
|
||||
new = json.loads(json.dumps(new, ensure_ascii=False))
|
||||
rows = (old or {}).get("rows") or {}
|
||||
return {
|
||||
"new": sum(key not in rows for key in new["rows"]),
|
||||
"changed": sum(key in rows and rows[key] != row for key, row in new["rows"].items()),
|
||||
"factor": (old or {}).get("driver_factor") != new["driver_factor"],
|
||||
"fx": (old or {}).get("fx") != new["fx"],
|
||||
}
|
||||
|
||||
|
||||
@@ -206,15 +231,12 @@ def book_stale(book: dict[str, Any] | None, keys: list[str]) -> str | None:
|
||||
if book is None:
|
||||
return "단가 아직 안 굳힘"
|
||||
# 저장 꼴(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())
|
||||
diff = book_diff(book, refresh_book(keys, book))
|
||||
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 "",
|
||||
f"새 단가 줄 {diff['new']}" if diff["new"] else "",
|
||||
f"M01 에서 바뀐 단가 {diff['changed']}" if diff["changed"] else "",
|
||||
"운전사 계수 바뀜" if diff["factor"] else "",
|
||||
"환율 바뀜" if diff["fx"] else "",
|
||||
]
|
||||
return " · ".join(p for p in parts if p) or None
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""B09 [업데이트] 한 곳(PLAN 703-3) — 단가 책 다시 굳힘 + 원본이 바뀐 구조물 사본 [재적용]을 함께.
|
||||
|
||||
미리 보기(`preview`) = 바뀌는 단가 줄(새 줄 + 바뀐 줄 · 지금 수요) · 다시 가져올 구조물 사본
|
||||
(연결된 것 중 원본이 바뀐 것만 · 원본 없음 = 그대로) · 지킬 고친 칸(덮개 칸 수 · 702-4 —
|
||||
재적용 · 새 판 뒤에도 남음) · 노임 판(굳힌 판 → 지금 M01 판 · 옛 책 = 굳힌 날)
|
||||
적용(`reapply_changed` → 부르는 쪽이 수요 다시 → `prices.refresh_book`) — 사본 먼저(수요가 바뀜) ·
|
||||
덮개(사용자 값)는 안 건드림 · 사본 하나가 막혀도 나머지는 함(막힌 것 = 알림).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from B09_Estimation import B09_Estimation_Prices as prices
|
||||
from B09_Estimation.B09_Estimation_Calc_Labor import COPY
|
||||
from M02_MasterTemplete import M02_Item_Copy as item_copy
|
||||
from M02_MasterTemplete.M02_Item_Snapshot import linked_snapshots
|
||||
|
||||
|
||||
def changed_copies(root: str | Path) -> list[dict[str, Any]]:
|
||||
"""연결된 구조물 사본 중 원본(층 · 마스터)이 바뀐 것 `[{폴더, 이름}]`."""
|
||||
linked = {one["폴더"] for one in linked_snapshots(root).values()}
|
||||
return [
|
||||
{"폴더": it["폴더"], "이름": it["이름"] or it["폴더"]}
|
||||
for it in item_copy.list_items(root)
|
||||
if it["폴더"] in linked and it["마스터바뀜"]
|
||||
]
|
||||
|
||||
|
||||
def cover_cells(cover: dict[str, Any] | None) -> int:
|
||||
"""고친 칸 수 — 덮개 칸(품 · 수량 · 단가) · 복사본 표지 뺌."""
|
||||
return sum(
|
||||
len(cell) for lines in (cover or {}).values() for cid, cell in lines.items() if cid != COPY
|
||||
)
|
||||
|
||||
|
||||
def edition_text(editions: dict[str, str] | None) -> str:
|
||||
return " · ".join(f"{kind} {day[:7]}" for kind, day in sorted((editions or {}).items()))
|
||||
|
||||
|
||||
def preview(
|
||||
root: str | Path, book: dict[str, Any] | None, keys: list[str], cover: dict | None
|
||||
) -> dict[str, Any]:
|
||||
diff = prices.book_diff(book, prices.refresh_book(keys, book))
|
||||
before = (book or {}).get("labor_edition")
|
||||
made = str((book or {}).get("made") or "")[:10]
|
||||
return {
|
||||
"단가줄": diff["new"] + diff["changed"],
|
||||
"새줄": diff["new"],
|
||||
"바뀐줄": diff["changed"],
|
||||
"계수": diff["factor"],
|
||||
"환율": diff["fx"],
|
||||
"사본": changed_copies(root),
|
||||
"고친칸": cover_cells(cover),
|
||||
"노임판": {
|
||||
"전": edition_text(before) if before else (f"굳힌 날 {made}" if made else "안 굳힘"),
|
||||
"후": edition_text(prices.labor_editions(keys)),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def reapply_changed(root: str | Path, who: dict[str, Any] | None = None) -> dict[str, list]:
|
||||
"""원본이 바뀐 연결 사본 재적용 — `{다시: [이름], 막힘: [이름 — 까닭]}`."""
|
||||
done, stuck = [], []
|
||||
for one in changed_copies(root):
|
||||
try:
|
||||
item_copy.reapply(root, one["폴더"], who)
|
||||
done.append(one["이름"])
|
||||
except (LookupError, ValueError, KeyError, OSError) as error:
|
||||
stuck.append(f"{one['이름']} — {error}")
|
||||
return {"다시": done, "막힘": stuck}
|
||||
@@ -11,8 +11,13 @@
|
||||
원가 · 공기) · 단가 책 · `estimation` 설정 구획 지움 · 서고 사본 정리 · 견적서 파일 남김 →
|
||||
답 = GET 꼴(기준 = 지금 B08 저장본 + 지금 M01 단가) · 단계 = [저장]과 같음
|
||||
POST /api/projects/{id}/estimation/prices {user?}
|
||||
[단가 갱신] — 수요 단가 전부를 지금 M01 값으로 새로 굳힘 · user = 화면 캐시(296 ·
|
||||
지운 줄 · 그 사본은 수요에서 뺌 · 저장은 안 함) · M01 원본이 지워진 줄 = 굳힌 값 그대로
|
||||
[업데이트] — 원본이 바뀐 구조물 사본 재적용(703-3 · `B09_Estimation_Refresh`) →
|
||||
수요 단가 전부를 지금 M01 값으로 새로 굳힘 · user = 화면 캐시(296 · 지운 줄 · 그 사본은
|
||||
수요에서 뺌 · 저장은 안 함) · M01 원본이 지워진 줄 = 굳힌 값 그대로 · 덮개 그대로 ·
|
||||
답 notes 첫 줄 = 바뀐 수
|
||||
POST /api/projects/{id}/estimation/prices/preview {user?}
|
||||
[업데이트] 미리 보기(703-3) — 바뀌는 단가 줄 · 다시 가져올 사본 · 지킬 고친 칸 ·
|
||||
노임 판(쓰지 않음)
|
||||
POST /api/projects/{id}/estimation/logic-preview {code, given, rate, picks}
|
||||
「운반 단가」 모달 계산 절차(253-3) — 그 로직 한 산근을 프로젝트 단가로 풀어 글 줄 · 단가 ·
|
||||
저장 안 함(`render_sangeun_text` 를 `Sangeun.Solver` 로) · `blocks` = 칸 나눔(277-2)
|
||||
@@ -26,7 +31,8 @@
|
||||
「운반 단가」 모달(263-2) — 담은 로직 줄 입력 · 값(읽기만 · 권한 = library 길과 같음)
|
||||
POST /api/projects/{id}/estimation/daega/personal {cover, name?, overwrite?, user?}
|
||||
「내 자료집에 저장」(703-2) — 펼친 일위대가 → 개인 층 사전조합
|
||||
(`B09_Estimation_Daega_Personal`) · user = 화면 캐시 덮개 · 같은 이름 = 409 {같은이름} → 덮어쓰기(overwrite = 키) · 새 이름
|
||||
(`B09_Estimation_Daega_Personal`) · user = 화면 캐시 덮개 ·
|
||||
같은 이름 = 409 {같은이름} → 덮어쓰기(overwrite = 키) · 새 이름
|
||||
운반 사본 연결 = 셈 때마다 거리로 사본 다시 고름(`Bill_Library.resolve_hauls` · `fix_keys`).
|
||||
|
||||
저장 자리 = 프로젝트 안 `B09_Estimation/v1/`(`B09_Estimation_Store`) ·
|
||||
@@ -58,6 +64,7 @@ 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_Personal as personal
|
||||
from B09_Estimation import B09_Estimation_Prices as prices
|
||||
from B09_Estimation import B09_Estimation_Refresh as refresh
|
||||
from B09_Estimation import B09_Estimation_Sangeun as sangeun
|
||||
from B09_Estimation import B09_Estimation_Store as store
|
||||
from B09_Estimation.B09_Estimation_Bill_Items import bill_items, hopyo_prices
|
||||
@@ -265,16 +272,44 @@ async def estimation_reset(project_id: UUID) -> dict[str, Any]:
|
||||
|
||||
@router.post("/{project_id}/estimation/prices")
|
||||
async def refresh_prices(
|
||||
project_id: UUID, body: dict[str, Any] | None = Body(None)
|
||||
project_id: UUID,
|
||||
body: dict[str, Any] | None = Body(None),
|
||||
session: dict[str, Any] = Depends(verify_session),
|
||||
) -> dict[str, Any]:
|
||||
"""[업데이트] 한 곳(703-3) — 원본이 바뀐 구조물 사본 재적용 → 단가 책 다시 굳힘 ·
|
||||
덮개 그대로."""
|
||||
root = await _project_root(project_id)
|
||||
raw = (body or {}).get("user")
|
||||
draft = store.clean_user(raw) if isinstance(raw, dict) else None # 296 화면 캐시(저장 안 함)
|
||||
again = await asyncio.to_thread(refresh.reapply_changed, root, importer(session))
|
||||
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, draft)
|
||||
out = await payload(project_id, root, draft)
|
||||
diff = prices.book_diff(old, book)
|
||||
cells = refresh.cover_cells((draft or out.get("user") or {}).get("daega_cover"))
|
||||
out["notes"] = [
|
||||
f"업데이트 — 단가 줄 {diff['new'] + diff['changed']} 바뀜 · 구조물 사본 "
|
||||
f"{len(again['다시'])} 다시 가져옴 · 고친 칸 {cells} 그대로",
|
||||
*(f"재적용 못 함 {why}" for why in again["막힘"]),
|
||||
*(out.get("notes") or []),
|
||||
]
|
||||
return out
|
||||
|
||||
|
||||
@router.post("/{project_id}/estimation/prices/preview")
|
||||
async def refresh_preview(
|
||||
project_id: UUID, body: dict[str, Any] | None = Body(None)
|
||||
) -> dict[str, Any]:
|
||||
"""[업데이트] 미리 보기(703-3) — 바뀌는 단가 줄 · 다시 가져올 사본 · 지킬 고친 칸 · 노임 판."""
|
||||
root = await _project_root(project_id)
|
||||
raw = (body or {}).get("user")
|
||||
draft = store.clean_user(raw) if isinstance(raw, dict) else None
|
||||
keys = await _demand(root, draft)
|
||||
book = await asyncio.to_thread(store.read_book, root)
|
||||
user = draft if draft is not None else await asyncio.to_thread(store.read_user, root)
|
||||
return await asyncio.to_thread(refresh.preview, root, book, keys, user.get("daega_cover"))
|
||||
|
||||
|
||||
class PreviewBody(BaseModel):
|
||||
|
||||
@@ -4,15 +4,23 @@
|
||||
*
|
||||
* [업데이트] = 단가를 지금 M01 값으로 새로 굳힘 · 서버가 낡았다고 보면(`payload.stale` 까닭 한 줄)
|
||||
* 단추에 점 배지(`is-pending`) · 굳힌 시각 · 까닭 = 공용 호버
|
||||
* 미리 보기 고리(703-3) = [업데이트] 누름을 먼저 잡아 POST …/estimation/prices/preview(화면 캐시) →
|
||||
* 「바뀌는 단가 줄 N · 다시 가져올 구조물 사본 K · 고친 칸 M 지킴 · 노임 판 전 → 후」 창 → [적용] =
|
||||
* 원래 단추 일(서버가 사본 재적용 + 단가 다시 굳힘 · 덮개 그대로) · 닫기 = 안 함
|
||||
* 연결 점검 = 「빈 줄 n」(설계내역서 금액 모르는 줄 · 안내 · 집계표와 같은 수 · 701-2) 목록 +
|
||||
* 그 밖 확인 목록(일위대가 · 단가산출 · 재료 등 · 탭 · 키마다 하나)(누르면 그 탭 그 줄로 · 까닭 = 공용 호버) + 서버 알림 · M01 에 없는 키
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { createButton, showToast } from "@ui/ui_template_elements";
|
||||
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import type { Row } from "./B09_Estimation_Calc_Base";
|
||||
import { dateTimeText } from "../common_util/common_util_time";
|
||||
import { readState } from "../A00_Common/b_page_state";
|
||||
import { currentProjectId } from "../A00_Common/b_project_switch";
|
||||
import { blankRows, tabName, type Link, type TabId } from "./B09_Estimation_UI_Tabs";
|
||||
|
||||
export interface CheckItem {
|
||||
@@ -59,7 +67,65 @@ export function refreshTip(payload: Row | null): string {
|
||||
const made = payload?.book?.made;
|
||||
const when = made ? `단가 굳힘 ${dateTimeText(String(made))}` : "단가 아직 안 굳힘";
|
||||
const stale = payload?.stale;
|
||||
return `${when} · 누르면 지금 M01 값으로 새로 굳힘${stale ? ` · 업데이트 필요 — ${stale}` : ""}`;
|
||||
return `${when} · 누르면 미리 보기 → [적용] = 지금 M01 값으로 새로 굳힘 · 원본 바뀐 구조물 사본 재적용 · 고친 칸 그대로${stale ? ` · 업데이트 필요 — ${stale}` : ""}`;
|
||||
}
|
||||
|
||||
/** [업데이트] 미리 보기 글 줄(703-3) */
|
||||
export function previewLines(got: Row): string[] {
|
||||
const copies: Row[] = got.사본 || [];
|
||||
const edition = got.노임판 || {};
|
||||
return [
|
||||
`바뀌는 단가 줄 ${got.단가줄 ?? 0}(새 줄 ${got.새줄 ?? 0} · 값 바뀜 ${got.바뀐줄 ?? 0})` +
|
||||
`${got.계수 ? " · 운전사 계수 바뀜" : ""}${got.환율 ? " · 환율 바뀜" : ""}`,
|
||||
`다시 가져올 구조물 사본 ${copies.length}${copies.length ? ` — ${copies.map((c) => c.이름).join(" · ")}` : ""}`,
|
||||
`고친 칸 ${got.고친칸 ?? 0} 지킴(일위대가 덮개 · 새 판 · 재적용 뒤에도 그대로)`,
|
||||
`노임 판 ${edition.전 || "—"} → ${edition.후 || "—"}`,
|
||||
];
|
||||
}
|
||||
|
||||
/** [업데이트] 누름 앞 미리 보기 — [적용] 이면 원래 단추 일 · 닫기 = 안 함 */
|
||||
function attachPreview(refresh: HTMLElement, payload: () => Row | null): void {
|
||||
let pass = false;
|
||||
refresh.addEventListener(
|
||||
"click",
|
||||
(event) => {
|
||||
if (pass) return void (pass = false);
|
||||
event.stopImmediatePropagation();
|
||||
const projectId = currentProjectId();
|
||||
const user = readState<Row>("b09-estimation", projectId) ?? payload()?.user ?? {};
|
||||
void fetch(`${API_BASE_URL}/projects/${projectId}/estimation/prices/preview`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ user }),
|
||||
})
|
||||
.then(async (response) => {
|
||||
const got = (await response.json()) as Row;
|
||||
if (!response.ok) throw new Error(String(got?.detail ?? response.status));
|
||||
openModal({
|
||||
title: "업데이트 미리 보기",
|
||||
closeLabel: "닫기",
|
||||
width: "40rem",
|
||||
mount: (body, close, head) => {
|
||||
const apply = createButton({
|
||||
label: "적용",
|
||||
variant: "filled",
|
||||
onClick: () => {
|
||||
close();
|
||||
pass = true;
|
||||
refresh.click();
|
||||
},
|
||||
});
|
||||
apply.dataset.b09RefreshApply = "";
|
||||
head.append(apply);
|
||||
body.append(...previewLines(got).map((line) => createText(line)));
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((error) => showToast(`미리 보기 못 함 — ${String(error)}`, "error"));
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
export interface CheckPanel {
|
||||
@@ -72,6 +138,7 @@ export function createCheckPanel(refresh: HTMLElement, goto: (link: Link) => voi
|
||||
let payload: Row | null = null;
|
||||
let items: CheckItem[] = [];
|
||||
attachHoverInfo(refresh, () => refreshTip(payload));
|
||||
attachPreview(refresh, () => payload);
|
||||
const box = document.createElement("div");
|
||||
box.className = "b09-check";
|
||||
attachHoverInfo(box, (row) => items[Number(row.dataset.i)]?.why, "[data-i]");
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""703-3 B09 [업데이트] 한 곳 — 단가 책 다시 굳힘 + 원본이 바뀐 구조물 사본 재적용.
|
||||
|
||||
- 미리 보기 수(`Refresh.preview`) = 적용 뒤 책 견줌(`Prices.book_diff`) 수 · 노임 판 전 → 후 · 고친 칸 수
|
||||
- 재적용 = 연결된 사본 중 원본이 바뀐 것만 · 하나가 막혀도 나머지 · 덮개(사용자 값) 안 건드림
|
||||
"""
|
||||
|
||||
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_Prices as prices # noqa: E402
|
||||
from B09_Estimation import B09_Estimation_Refresh as refresh # noqa: E402
|
||||
from B09_Estimation import B09_Estimation_Store as store # noqa: E402
|
||||
|
||||
KEYS = ["LB000001", "LB000033"] # 작업반장(건설업) · 석공
|
||||
|
||||
|
||||
def test_책_견줌_미리보기_수_적용_수_같음(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(refresh, "changed_copies", lambda root: [])
|
||||
new = prices.refresh_book(KEYS)
|
||||
assert new["labor_edition"] == prices.labor_editions(KEYS) and new["labor_edition"]
|
||||
old = json.loads(json.dumps(new))
|
||||
old["rows"]["LB000001"] = {**old["rows"]["LB000001"], "price": 1} # 옛 판 값
|
||||
del old["rows"]["LB000033"] # 새 줄
|
||||
old["labor_edition"] = {"건설업": "2025-09-01"}
|
||||
cover = {
|
||||
"S:a": {"x#0": {"qty": 1, "labor": 2}},
|
||||
"K:b": {"@copy": {"from": "S:a"}, "y#0": {"qty": 3}},
|
||||
}
|
||||
got = refresh.preview(tmp_path, old, KEYS, cover)
|
||||
after = prices.refresh_book(KEYS, old)
|
||||
diff = prices.book_diff(old, after)
|
||||
assert (got["새줄"], got["바뀐줄"]) == (diff["new"], diff["changed"]) == (1, 1)
|
||||
assert got["단가줄"] == 2 and got["고친칸"] == 3 and got["사본"] == []
|
||||
assert got["노임판"]["전"] == "건설업 2025-09"
|
||||
assert got["노임판"]["후"].startswith("건설업 2026")
|
||||
old.pop("labor_edition")
|
||||
assert refresh.preview(tmp_path, old, KEYS, None)["노임판"]["전"].startswith("굳힌 날 ")
|
||||
assert prices.book_diff(after, prices.refresh_book(KEYS, after)) == {
|
||||
"new": 0,
|
||||
"changed": 0,
|
||||
"factor": False,
|
||||
"fx": False,
|
||||
}
|
||||
assert prices.book_stale(after, KEYS) is None
|
||||
|
||||
|
||||
def test_재적용_원본_바뀐_사본만_막혀도_나머지_덮개_그대로(tmp_path, monkeypatch):
|
||||
cover = {"S:a": {"x#0": {"qty": 0.3}}}
|
||||
store.write_user(tmp_path, {"daega_cover": cover})
|
||||
monkeypatch.setattr(
|
||||
refresh,
|
||||
"linked_snapshots",
|
||||
lambda root: {"p1": {"폴더": "a"}, "p2": {"폴더": "b"}, "p3": {"폴더": "c"}},
|
||||
)
|
||||
items = [
|
||||
{"폴더": "a", "이름": "가", "마스터바뀜": True},
|
||||
{"폴더": "b", "이름": "나", "마스터바뀜": False}, # 원본 그대로 = 안 함
|
||||
{"폴더": "c", "이름": "다", "마스터바뀜": True},
|
||||
{"폴더": "z", "이름": "끊김", "마스터바뀜": True}, # 연결 안 됨 = 안 함
|
||||
]
|
||||
monkeypatch.setattr(refresh.item_copy, "list_items", lambda root: items)
|
||||
calls = []
|
||||
|
||||
def fake(root, folder, who=None):
|
||||
calls.append(folder)
|
||||
if folder == "c":
|
||||
raise LookupError("원본 층 없음")
|
||||
|
||||
monkeypatch.setattr(refresh.item_copy, "reapply", fake)
|
||||
assert [c["이름"] for c in refresh.changed_copies(tmp_path)] == ["가", "다"]
|
||||
got = refresh.reapply_changed(tmp_path)
|
||||
assert calls == ["a", "c"]
|
||||
assert got == {"다시": ["가"], "막힘": ["다 — 원본 층 없음"]}
|
||||
assert store.read_user(tmp_path)["daega_cover"] == cover
|
||||
Reference in New Issue
Block a user