- common_util_project_copy.rekey_id_files: 코리도 이름 corridor_{노선 id:04d} → 새 노선 id(겹침 대비 임시 이름 거침) · 짝 없는 옛 코리도 지움 · B06_Section/sampling.json surface_model_id → 새 지표면 id
- _insert_rows · _insert_full 이 (지표면 · 노선) id 짝을 함께 돌려줌 · full 복제 · --replace · --storage-only 셋 다 _fix_files 에서 rekey
- id_maps_by_order: 이미 복제된 두 프로젝트의 id 짝(차례 · 행 수 다르면 ValueError) — storage-only 가 씀
- storage 경로 함수 전수: id 박힌 이름 = 코리도뿐(B08 이정표 그림 이름 = 거래처 상자 id · 추천 저장본 = 해시) · 내용 = sampling.json 지표면 id(초기값 스냅샷 지표면 id 는 종전대로)
- 지킴 시험 test_761_2: 노선 id 짝 단정 · rekey(이름 겹침 · 짝 없음 지움 · sampling)
- 바꿔 넣은 B 13 개 고침(tmp 스크립트 · 차례 짝 + 내용 칸 대조): 코리도 셋씩 새 id · corridor_0370 지움 · sampling 911 → 각 새 id
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
164 lines
6.7 KiB
Python
164 lines
6.7 KiB
Python
"""프로젝트 자리 바꿔 넣기 — 원본 내용을 대상 프로젝트 id 자리에 넣음 (요구 603 · PLAN 761-2).
|
|
|
|
`common_util_project_copy` 의 full 복제와 같은 표 · 같은 id 다시 잇기 · 같은 파일 치환을 쓰되
|
|
새 프로젝트를 만들지 않고 **대상 행을 그대로 둠**(id · 이름 · 주인 · 참여자 · 종류 · 회사 ·
|
|
담당자 · 저장 자리 · 만든 때). 대상의 옛 행(복제 표 목록)은 지우고 원본 행으로 채움 ·
|
|
storage 폴더는 원본과 똑같이 맞춤(robocopy /MIR — 원본에 없는 파일 지움).
|
|
|
|
넣기 전 대상 행을 `backup_dir/<대상 id>.json` 에 덤프(되돌림 재료) · storage 는 백업 안 함.
|
|
원본은 읽기만.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import aiomysql
|
|
|
|
from common_util.common_util_project_copy import (
|
|
KIND_LABELS,
|
|
_copy_folder,
|
|
_fix_files,
|
|
_insert_rows,
|
|
_source_rows,
|
|
_tree_files,
|
|
_uncovered,
|
|
_write_manifest,
|
|
)
|
|
from common_util.common_util_storage import resolve_stored_project_path
|
|
from common_util.common_util_time import utc_now_text
|
|
|
|
#: 대상 projects 행에서 그대로 두는 칸 — 나머지 칸은 원본 값
|
|
KEEP_PROJECT_COLUMNS = (
|
|
"id",
|
|
"name",
|
|
"user_id",
|
|
"company_id",
|
|
"project_kind",
|
|
"storage_path",
|
|
"created_at",
|
|
"deleted_at",
|
|
"pm_user_id",
|
|
"field_lead_user_id",
|
|
"designer_user_id",
|
|
"logo_asset_id",
|
|
"signature_asset_id",
|
|
)
|
|
#: 대상에서 지우는 표(자식 → 부모) — projects · project_members 는 둠
|
|
ROUTE_CHILD_TABLES = ("cross_sections", "longitudinal_sections", "route_statistics", "route_points")
|
|
PROJECT_TABLES = (
|
|
"routes",
|
|
"terrain_layers",
|
|
"surface_models",
|
|
"processed_point_cloud",
|
|
"input_files",
|
|
"project_workflow_stages",
|
|
"plan_route_years",
|
|
"plan_route_assignees",
|
|
)
|
|
|
|
|
|
def merged_project_row(source: dict[str, Any], target: dict[str, Any]) -> dict[str, Any]:
|
|
"""대상 projects 행에 쓸 값 — 유지 칸은 대상 · 나머지는 원본(id 뺀 SET 몫)."""
|
|
return {
|
|
key: (target[key] if key in KEEP_PROJECT_COLUMNS else value)
|
|
for key, value in source.items()
|
|
if key != "id" and key != "updated_at"
|
|
}
|
|
|
|
|
|
async def _delete_target_rows(cursor, target_id: str, rows: dict[str, list]) -> dict[str, int]:
|
|
"""대상 옛 행 지움(자식 → 부모) → 표 → 지운 행 수."""
|
|
removed: dict[str, int] = {}
|
|
route_ids = [route["id"] for route in rows["routes"]]
|
|
for table in ROUTE_CHILD_TABLES:
|
|
removed[table] = 0
|
|
if route_ids:
|
|
holders = ", ".join(["%s"] * len(route_ids))
|
|
removed[table] = await cursor.execute(
|
|
f"DELETE FROM `{table}` WHERE route_id IN ({holders})", route_ids
|
|
)
|
|
for table in PROJECT_TABLES:
|
|
if table == "terrain_layers":
|
|
model_ids = [model["id"] for model in rows["surface_models"]]
|
|
removed[table] = 0
|
|
if model_ids:
|
|
holders = ", ".join(["%s"] * len(model_ids))
|
|
removed[table] = await cursor.execute(
|
|
f"DELETE FROM terrain_layers WHERE surface_model_id IN ({holders})", model_ids
|
|
)
|
|
continue
|
|
removed[table] = await cursor.execute(
|
|
f"DELETE FROM `{table}` WHERE project_id = %s", (target_id,)
|
|
)
|
|
return removed
|
|
|
|
|
|
async def replace_project(
|
|
connection, source_id: str, target_id: str, *, backup_dir: Path
|
|
) -> dict[str, Any]:
|
|
"""원본 내용을 대상 id 자리에 넣음 → 보고(표 행 수 · 지운 행 수 · 백업 자리)."""
|
|
if source_id == target_id:
|
|
raise ValueError("원본과 대상이 같음")
|
|
async with connection.cursor(aiomysql.DictCursor) as cursor:
|
|
rows = await _source_rows(cursor, source_id)
|
|
skipped = await _uncovered(cursor, source_id, rows)
|
|
old = await _source_rows(cursor, target_id)
|
|
left = await _uncovered(cursor, target_id, old)
|
|
source, target = rows["projects"][0], old["projects"][0]
|
|
if source.get("project_kind") != target.get("project_kind"):
|
|
raise ValueError(
|
|
f"종류가 다름: 원본 {source.get('project_kind')} · 대상 {target.get('project_kind')}"
|
|
)
|
|
source_root = Path(resolve_stored_project_path(source["storage_path"]))
|
|
target_root = Path(resolve_stored_project_path(target["storage_path"]))
|
|
# /MIR 은 대상 폴더를 원본과 똑같이 지움 — 자리가 대상 id 폴더일 때만
|
|
if target_root.name != target_id or target_root.resolve() == source_root.resolve():
|
|
raise ValueError(f"대상 저장 자리가 이상함: {target_root}")
|
|
|
|
if skipped or left:
|
|
print("⚠ 복제 목록 밖 표(안 뜸 · 대상 행 그대로):", {"원본": skipped, "대상": left})
|
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
backup = backup_dir / f"{target_id}.json"
|
|
backup.write_text(json.dumps(old, ensure_ascii=False, default=str), encoding="utf-8")
|
|
print(f"대상 행 백업: {backup}")
|
|
|
|
# 1) 파일 먼저 — 실패하면 DB 를 건드리지 않은 채로 끝남
|
|
_copy_folder(source_root, target_root, mirror=True)
|
|
# 2) DB — 지우기 · projects 칸 · 다시 넣기 한 트랜잭션
|
|
await connection.begin()
|
|
try:
|
|
async with connection.cursor(aiomysql.DictCursor) as cursor:
|
|
removed = await _delete_target_rows(cursor, target_id, old)
|
|
values = merged_project_row(source, target)
|
|
sets = ", ".join(f"`{key}` = %s" for key in values)
|
|
await cursor.execute(
|
|
f"UPDATE projects SET {sets} WHERE id = %s", (*values.values(), target_id)
|
|
)
|
|
model_map, route_map = await _insert_rows(cursor, rows, target_id)
|
|
await connection.commit()
|
|
except Exception:
|
|
await connection.rollback()
|
|
raise
|
|
# 3) 파일 안 옛 프로젝트 id · 초기값 스냅샷 지표면 id · 노선 id 이름 파일
|
|
_fix_files(target_root, source_id, target_id, model_map, route_map)
|
|
tables = {name: len(items) for name, items in rows.items()}
|
|
_write_manifest(
|
|
target_root,
|
|
{
|
|
"scope": "replace",
|
|
"kind": KIND_LABELS.get(source.get("project_kind"), source.get("project_kind")),
|
|
"source_project_id": source_id,
|
|
"project_id": target_id,
|
|
"copied_at": utc_now_text(),
|
|
"tables": tables,
|
|
"removed": removed,
|
|
"skipped_tables": skipped,
|
|
"files": _tree_files(target_root),
|
|
},
|
|
)
|
|
print(f"자리 바꿔 넣기 완료: {target['name']} ({target_id}) ← {source['name']}")
|
|
return {"tables": tables, "removed": removed, "skipped": skipped, "backup": str(backup)}
|