Merge remote-tracking branch 'origin/sub_laptop_5' into sub_laptop_2
# Conflicts: # B04_PreProcess/B04_PreProcess_Router_Basins.py # B08_Quantity/B08_Quantity_Router_Summary.py
This commit is contained in:
@@ -46,6 +46,7 @@ from M02_MasterTemplete.M02_Item_Source import (
|
||||
)
|
||||
|
||||
#: 길의 종류 이름 → 복사본 파일
|
||||
PARTS = ("도면", "면적", "일위대가") # 목록 `부분` — 구조물 조합 사본이 가진 것
|
||||
KINDS = {
|
||||
"도면": "도면.json",
|
||||
"면적": "면적.json",
|
||||
@@ -115,6 +116,9 @@ def list_items(project_root: str | Path) -> list[dict[str, Any]]:
|
||||
"구조물": manifest.get("구조물") or [],
|
||||
"고침": bool(manifest.get("고침")),
|
||||
"값있음": bool(manifest.get("값있음")),
|
||||
"가져온이": manifest.get("가져온이"),
|
||||
# 부분(142-2) — 복사본에 있는 파일만(원본 대조 없이)
|
||||
"부분": [name for name in PARTS if (child / KINDS[name]).is_file()],
|
||||
}
|
||||
)
|
||||
return out
|
||||
@@ -227,8 +231,11 @@ def _kept_picks(picks: Any, labor: dict[str, Any]) -> dict[str, Any]:
|
||||
return out
|
||||
|
||||
|
||||
def reapply(project_root: str | Path, folder: str) -> dict[str, Any]:
|
||||
"""재적용 — 원본(층 · 마스터)을 다시 가져옴 · 입력값 · 고른자재는 같은 키면 살림.
|
||||
def reapply(
|
||||
project_root: str | Path, folder: str, who: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""재적용 — 원본(층 · 마스터)을 다시 가져옴 · 입력값 · 고른자재는 같은 키면 살림 ·
|
||||
`who` = 가져온이 새로(142-2).
|
||||
|
||||
원본과 달라 덮인 파일(도면 · 면적)은 `덮임` 에 알림."""
|
||||
target = folder_of(project_root, folder)
|
||||
@@ -258,6 +265,8 @@ def reapply(project_root: str | Path, folder: str) -> dict[str, Any]:
|
||||
# 원본을 다시 가져왔으니 시트 고침은 없음 — 살린 입력값 · 고른 자재는 `값있음` 으로 따로 알림
|
||||
kept = bool(kept_values or files[PICKS_FILE])
|
||||
fresh = {**manifest, "판": source, "원본판": source, "가져온날": stamp, "고침": False}
|
||||
if who:
|
||||
fresh["가져온이"] = who
|
||||
fresh["값있음"] = kept
|
||||
fresh.pop("고친날", None)
|
||||
atomic_write_json(target / MANIFEST_NAME, fresh)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""프로젝트에서 개인 층으로 — [개인에 저장](save-as) · 프로젝트에서 새 항목(new) (PLAN 26-2b).
|
||||
|
||||
개인 층 새 항목 = 구조물집계표 새 열 하나 + 빈 도면 · 면적산출(번호 · SS 키는 저장소가
|
||||
줌). [개인에 저장]은 구조물 복사본의 열 정의 · 도면 · 면적을 담음 —
|
||||
줌). [개인에 저장]은 구조물 복사본의 열 정의 · 도면 · 면적 · 일위대가 문서를 담음 —
|
||||
단가 묶음 · 입력값 · 고른자재는 프로젝트 몫이라 안 가져감. 쓰기는 지금 층 저장소 함수(`store.at` ·
|
||||
`store.write`) — 나무 고침(`M02_MasterTemplete_Tree`)과
|
||||
같은 집계표 저장 한 줄기(경로 · 머리 줄 수 맞춤 · 번호 · SS 장부)를 탐(PLAN 26-2a).
|
||||
@@ -19,7 +19,7 @@ from M02_MasterTemplete import M02_MasterTemplete_Items as items_store
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Tree as tree
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
from M02_MasterTemplete.M02_Item_Source import read_json
|
||||
from M02_MasterTemplete.M02_Item_Source import LABOR_FILE, read_json
|
||||
|
||||
#: 복사본 파일 → 층 양식 종류
|
||||
PART_KINDS = (("도면.json", "structure"), ("면적.json", "area"))
|
||||
@@ -89,6 +89,12 @@ def create_item(
|
||||
body = store._server_values(body)
|
||||
with store._LOCK:
|
||||
atomic_write_json(store._path(kind, new_id), body)
|
||||
labor = (read_json(copy / LABOR_FILE) or {}).get("줄") if copy else None
|
||||
# 일위대가 문서도(142-2) — 옛 UA 조합 줄은 문서가 아니라 안 담음 · 번호 · 열은 새 열 것
|
||||
# ponytail: 참조(단가승격 · 사전조합)는 키 그대로 — 개인 층이 못 읽는 참조는 계산 때
|
||||
# 까닭으로 멈춤(참조사본까지 옮기려면 `Labor_Ref.carry` 길)
|
||||
if isinstance(labor, dict) and labor.get("종류") == "일위대가":
|
||||
store.copy_from("labor", new_id, new_id, body=labor)
|
||||
ss = next(r for r in store.read_combos()["줄"] if r.get("열") == new_id)
|
||||
return {"key": ss["키"], "열": new_id, "이름": tree.name_of(heads)}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
([재적용]은 사용자가)
|
||||
- 지정을 다른 항목으로 바꾸면 새로 가져옴 · 구조물을 지우거나 지정을 풀면 복사본을 지움 —
|
||||
프로젝트에서 고친 흔적(`manifest.고침`)이 있으면 지우지 않고 `_지난/` 에 옮겨 둠
|
||||
- 관 지점을 옮겨도 같은 항목이면 폴더 이름만 바꿈(142-2) · 옵션 그룹 이름 · 가져온이도 굳힘
|
||||
- 옛 항목 폴더(`templates/items/{층}_{주인}_{키}/` · 25-2b)는 그 항목을 쓰는 구조물마다 복사해
|
||||
옮겨 받음(고친 값 유지)
|
||||
"""
|
||||
@@ -54,6 +55,8 @@ LOCAL_FILES = (VALUES_FILE, PICKS_FILE)
|
||||
FOLDER_PATTERN = re.compile(
|
||||
rf"^(?:pipe@\d+(?:\.\d+)?(?:__g[0-9a-z]{{1,16}})?|{STRUCTURE_ID_RULE})$"
|
||||
)
|
||||
#: 관 지점 복사본 — (측점, 그룹 꼬리)
|
||||
PIPE_FOLDER = re.compile(r"^pipe@(\d+(?:\.\d+)?)((?:__g[0-9a-z]{1,16})?)$")
|
||||
|
||||
|
||||
def structures_dir(project_root: str | Path) -> Path:
|
||||
@@ -221,16 +224,100 @@ def migrate_items(project_root: str | Path) -> list[str]:
|
||||
return moved
|
||||
|
||||
|
||||
def sync_snapshots(project_root: str | Path) -> dict[str, list[str]]:
|
||||
"""구조물 복사본을 지정에 맞춤 — `{만듦, 지움, 못만듦, 옮겨받음}` 폴더 이름."""
|
||||
def rename_moved(base: Path, wanted: dict[str, tuple[str, dict[str, Any]]]) -> list[str]:
|
||||
"""관 지점을 옮긴 뒤 [저장] — 새 자리(`pipe@새 측점`)에 복사본이 없고 안 쓰게 된 옛 자리
|
||||
복사본이 같은 항목 · 같은 그룹 꼬리면 폴더 이름만 바꿈(가까운 측점 먼저 · 142-2) — 가져온날 ·
|
||||
고침 · 입력값 그대로. 바꾼 것 `옛 이름→새 이름`."""
|
||||
if not base.is_dir():
|
||||
return []
|
||||
|
||||
def spot(name: str) -> tuple[float, str] | None:
|
||||
hit = PIPE_FOLDER.match(name)
|
||||
return (float(hit[1]), hit[2]) if hit else None
|
||||
|
||||
new = [
|
||||
(name, at, item)
|
||||
for name, (_, item) in wanted.items()
|
||||
if (at := spot(name)) and not (base / name / MANIFEST_NAME).is_file()
|
||||
]
|
||||
old = [
|
||||
(child.name, at, manifest)
|
||||
for child in base.iterdir()
|
||||
if child.is_dir()
|
||||
and child.name not in wanted
|
||||
and (at := spot(child.name))
|
||||
and isinstance(manifest := read_json(child / MANIFEST_NAME), dict)
|
||||
]
|
||||
# ponytail: 같은 항목 관 둘을 함께 옮기면 가까운 측점끼리 짝 — 고침 · 입력값이 다를 때만 엇갈림
|
||||
pairs = sorted(
|
||||
(abs(a[0] - b[0]), new_name, old_name)
|
||||
for new_name, a, item in new
|
||||
for old_name, b, manifest in old
|
||||
if a[1] == b[1] and same_item(manifest, item)
|
||||
)
|
||||
used: set[str] = set()
|
||||
out: list[str] = []
|
||||
for _, new_name, old_name in pairs:
|
||||
target = safe_folder(base, new_name)
|
||||
if new_name in used or old_name in used or target is None:
|
||||
continue
|
||||
if target.is_dir(): # 끊긴 쓰기(manifest 없음) 자리
|
||||
retire(target)
|
||||
(base / old_name).rename(target)
|
||||
manifest = read_json(target / MANIFEST_NAME) or {}
|
||||
atomic_write_json(target / MANIFEST_NAME, {**manifest, "구조물": [wanted[new_name][0]]})
|
||||
used.update((new_name, old_name))
|
||||
out.append(f"{old_name}→{new_name}")
|
||||
return out
|
||||
|
||||
|
||||
def group_names() -> dict[str, str]:
|
||||
"""옵션 그룹 id → 이름(지금 정의) — 복사본 manifest `그룹` 으로 굳힘(142-2)."""
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Groups import groups_of
|
||||
|
||||
return {g["id"]: g["name"] for rows in groups_of().values() for g in rows}
|
||||
|
||||
|
||||
def copied_groups(
|
||||
project_root: str | Path | None,
|
||||
owner: str,
|
||||
links: dict[str, Any],
|
||||
defined: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""그 구조물의 옵션 그룹 — 지금 정의 + 복사본에 굳힌 정의(이름 · 사라진 그룹도 · 142-2 ·
|
||||
층이 바뀌어도 프로젝트 그대로). 굳힌 것 없으면 지금 정의 그대로."""
|
||||
if project_root is None:
|
||||
return defined
|
||||
base = structures_dir(project_root)
|
||||
ids = {g["id"] for g in defined}
|
||||
out = []
|
||||
for group in [*defined, *({"id": gid} for gid in links if gid not in ids)]:
|
||||
manifest = read_json(
|
||||
base / folder_name(f"{owner}{GROUP_JOIN}{group['id']}") / MANIFEST_NAME
|
||||
)
|
||||
kept = manifest.get("그룹") if isinstance(manifest, dict) else None
|
||||
if isinstance(kept, dict) and kept.get("name"):
|
||||
out.append({**group, "name": kept["name"]})
|
||||
elif group["id"] in ids:
|
||||
out.append(group)
|
||||
return out
|
||||
|
||||
|
||||
def sync_snapshots(
|
||||
project_root: str | Path, who: dict[str, Any] | None = None
|
||||
) -> dict[str, list[str]]:
|
||||
"""구조물 복사본을 지정에 맞춤 — `{만듦, 지움, 못만듦, 옮겨받음, 이름바꿈}` 폴더 이름 ·
|
||||
`who` = 가져온이(manifest · 142-2)."""
|
||||
moved = migrate_items(project_root)
|
||||
base = structures_dir(project_root)
|
||||
wanted = {
|
||||
folder_name(owner): (owner, item) for owner, item in assignments(project_root).items()
|
||||
}
|
||||
renamed = rename_moved(base, wanted)
|
||||
made: list[str] = []
|
||||
failed: list[str] = []
|
||||
removed: list[str] = []
|
||||
names: dict[str, str] | None = None
|
||||
for folder, (owner, item) in sorted(wanted.items()):
|
||||
target = safe_folder(base, folder)
|
||||
if target is None:
|
||||
@@ -239,12 +326,20 @@ def sync_snapshots(project_root: str | Path) -> dict[str, list[str]]:
|
||||
continue
|
||||
manifest = read_json(target / MANIFEST_NAME)
|
||||
if isinstance(manifest, dict) and same_item(manifest, item):
|
||||
if who and not manifest.get("가져온이"): # 미리 가져온 것 — 저장한 이가 가져온이
|
||||
atomic_write_json(target / MANIFEST_NAME, {**manifest, "가져온이": who})
|
||||
continue
|
||||
if target.is_dir(): # 다른 항목으로 바뀜 — 옛 것은 지우거나(고침이면) `_지난` 에
|
||||
retire(target)
|
||||
removed.append(folder)
|
||||
extra: dict[str, Any] = {"가져온이": who} if who else {}
|
||||
gid = owner.partition(GROUP_JOIN)[2]
|
||||
if gid:
|
||||
names = group_names() if names is None else names
|
||||
if gid in names:
|
||||
extra["그룹"] = {"id": gid, "name": names[gid]}
|
||||
try:
|
||||
write_snapshot(target, item, owner, {})
|
||||
write_snapshot(target, item, owner, extra)
|
||||
except (LookupError, ValueError) as error:
|
||||
logger.warning("집계 항목 복사본을 못 만듦 (%s): %s", folder, error)
|
||||
failed.append(folder)
|
||||
@@ -255,17 +350,28 @@ def sync_snapshots(project_root: str | Path) -> dict[str, list[str]]:
|
||||
if child.is_dir() and not child.name.startswith("_") and child.name not in wanted:
|
||||
retire(child)
|
||||
removed.append(child.name)
|
||||
return {"만듦": made, "지움": removed, "못만듦": failed, "옮겨받음": moved}
|
||||
out = {"만듦": made, "지움": removed, "못만듦": failed, "옮겨받음": moved}
|
||||
return {**out, "이름바꿈": renamed} if renamed else out
|
||||
|
||||
|
||||
def safe_sync(project_root: str | Path) -> None:
|
||||
"""저장 뒤 부름 — 실패해도 저장은 그대로(복사본은 다음 저장이 다시 맞춤)."""
|
||||
def importer(session: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
"""세션 → manifest 가져온이 `{id, 이름}`(142-2)."""
|
||||
if not isinstance(session, dict) or session.get("user_id") is None:
|
||||
return None
|
||||
return {"id": int(session["user_id"]), "이름": session.get("name")}
|
||||
|
||||
|
||||
def safe_sync(project_root: str | Path, who: dict[str, Any] | None = None) -> list[str]:
|
||||
"""저장 뒤 부름 — 실패해도 저장은 그대로(복사본은 다음 저장이 다시 맞춤) · 못 만든 복사본
|
||||
폴더 이름을 돌려줌(저장 응답 → [저장] 알림 · 142-2 · 통째 실패 = `["*"]`)."""
|
||||
try:
|
||||
result = sync_snapshots(project_root)
|
||||
result = sync_snapshots(project_root, who)
|
||||
if any(result.values()):
|
||||
logger.info("집계 항목 복사본: %s", result)
|
||||
return result["못만듦"]
|
||||
except Exception:
|
||||
logger.exception("집계 항목 복사본 맞춤 실패 (저장은 유지): %s", project_root)
|
||||
return ["*"]
|
||||
|
||||
|
||||
def complete_old(folder: Path) -> bool:
|
||||
|
||||
@@ -422,9 +422,11 @@ class Expand:
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
|
||||
lines = [one for one in doc.get("담은로직") or [] if isinstance(one, dict)]
|
||||
kept = doc.get("알약") # 프로젝트 참조사본 = 가져온 때 굳힌 알약(142-2) · 층 = 지금 M01
|
||||
pills = kept if isinstance(kept, list) else None
|
||||
by_logic = {
|
||||
code: pill
|
||||
for pill in combo_store.combo_pills({"담은로직": lines})["물량"]
|
||||
for pill in pills or combo_store.combo_pills({"담은로직": lines})["물량"]
|
||||
for code in pill["로직"]
|
||||
}
|
||||
inner, rows, why = [], [], None
|
||||
@@ -478,13 +480,24 @@ def frozen(doc: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
continue
|
||||
got = read_ref(ref)
|
||||
if got is not None:
|
||||
out[ref_id(ref)] = got
|
||||
out[ref_id(ref)] = with_pills(got)
|
||||
walk(got.get("담은로직"), depth + 1)
|
||||
|
||||
walk(doc.get("담은로직"), 1)
|
||||
return out
|
||||
|
||||
|
||||
def with_pills(doc: dict[str, Any]) -> dict[str, Any]:
|
||||
"""사전조합 굳힘 — 안 로직 줄 알약(배 · 단위)도 그때 M01 값으로(142-2 · 사본 셈이 지금 M01 을
|
||||
안 읽게)."""
|
||||
if doc.get("종류") != TITLE["precombo"]:
|
||||
return doc
|
||||
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||
|
||||
lines = [one for one in doc.get("담은로직") or [] if isinstance(one, dict)]
|
||||
return {**doc, "알약": combo_store.combo_pills({"담은로직": lines})["물량"]}
|
||||
|
||||
|
||||
def frozen_logics(copies: dict[str, Any]) -> list[str]:
|
||||
"""굳힌 단가승격 · 사전조합이 담은 로직 키 — 프로젝트 단가 묶음에 넣음."""
|
||||
return [
|
||||
|
||||
@@ -29,6 +29,7 @@ from common_util.common_util_summary_item import parse_summary_item
|
||||
from M02_MasterTemplete import M02_Item_Copy as copies
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Router_Layers as layer_routes
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete.M02_Item_Snapshot import importer
|
||||
from M02_MasterTemplete.M02_Item_Source import item_name
|
||||
|
||||
router = APIRouter(prefix="/api/projects", tags=["M02 Project Items"])
|
||||
@@ -162,7 +163,8 @@ async def write_item_file(
|
||||
async def reapply_item(
|
||||
project_id: str, folder: str, session: dict[str, Any] = Depends(verify_session)
|
||||
) -> dict[str, Any]:
|
||||
return await _run(copies.reapply, await _root(session, project_id), folder)
|
||||
root = await _root(session, project_id)
|
||||
return await _run(copies.reapply, root, folder, importer(session)) # 가져온이(142-2)
|
||||
|
||||
|
||||
@router.post("/{project_id}/items/{folder}/save-as")
|
||||
|
||||
@@ -140,14 +140,15 @@ def collect_items(
|
||||
"group_items": point.group_items,
|
||||
}
|
||||
)
|
||||
return _with_group_items(items)
|
||||
return _with_group_items(items, project_root)
|
||||
|
||||
|
||||
def _with_group_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
def _with_group_items(items: list[dict[str, Any]], project_root=None) -> list[dict[str, Any]]:
|
||||
"""옵션 그룹(M02 PLAN 46)마다 한 건 더 — 같은 구조물 · 옵션 · 자리에 그 그룹이 고른 항목 ·
|
||||
복사본 `{구조물 id}__{그룹 id}`. 정의된 그룹만(미지정 그룹 = 「미지정」 알림에 셈) · 정의가 사라진
|
||||
그룹 연결은 기록에만 남고 표에 안 들어감."""
|
||||
복사본 `{구조물 id}__{그룹 id}`. 그룹 = 지금 정의 + 복사본에 굳힌 정의(142-2 · 이름 · 사라진
|
||||
그룹도) · 미지정 그룹 = 「미지정」 알림에 셈."""
|
||||
from common_util.common_util_summary_item import GROUP_JOIN
|
||||
from M02_MasterTemplete.M02_Item_Snapshot import copied_groups
|
||||
from M02_MasterTemplete.M02_Table_Fill_Spec import container_rows
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Groups import groups_of
|
||||
|
||||
@@ -156,7 +157,8 @@ def _with_group_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
for item in items:
|
||||
links = item.pop("group_items", None) or {}
|
||||
out.append(item)
|
||||
for group in defined.get(item["type_id"], []):
|
||||
mine = defined.get(item["type_id"], [])
|
||||
for group in copied_groups(project_root, item["복사본"], links, mine):
|
||||
out.append(
|
||||
{
|
||||
**item,
|
||||
|
||||
@@ -357,3 +357,14 @@
|
||||
.m02-sod__specpreview {
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
/* 미리보기 상자 이름표 [삭제] — B05 상자 안 작은 단추 틀과 같은 꼴(136-3 · M02 는 B05 CSS 를 안 읽음) */
|
||||
.m02-sod__block .b05-structure__group > legend > button {
|
||||
box-sizing: border-box;
|
||||
height: 26px;
|
||||
min-height: 0;
|
||||
padding-block: 0;
|
||||
padding-inline: var(--spacing-8);
|
||||
font-size: var(--text-caption);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user