- common_util_project_copy_replace: 대상 옛 행(복제 표 목록) 지우고 원본 행으로 채움 · projects 는 유지 칸 밖만 원본 값 · storage 를 원본과 똑같이(robocopy /MIR) · 넣기 전 대상 행 덤프(--backup-dir) - common_util_project_copy: 행 넣기를 _insert_rows 로 떼어 full 복제 · 바꿔 넣기가 함께 씀 · robocopy 출력 글자 깨짐 무시 - 지킴 시험 test_761_2_clone_replace(유지 칸 · 지우는 표 = 복제 표 · id 다시 잇기) - test_532_1 낡은 단정 고침: 지표면 지워진 옛 노선의 NULL(ON DELETE SET NULL)을 새 id 밖 값으로 잘못 셈 - 실제 한 번: 가이드 → 임시 복제 → 그 자리에 --replace → 행 수 · 파일 같음 · 추천 4 꺼내짐 → 앱 영구 삭제 길로 지움 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
115 lines
5.3 KiB
Python
115 lines
5.3 KiB
Python
"""프로젝트 통째 복제 — **검증용 프로젝트를 창마다 하나씩 뜨는 도구** (2026-09-08).
|
|
|
|
본체 = `common_util/common_util_project_copy.py`(full 범위) · 이 파일은 CLI 껍데기.
|
|
|
|
⚠ **왜 저장소에 두나** — 네 창이 **같은 원본에서 같은 방법으로** 떠야 수치를 견줄 수 있다.
|
|
`tmp/` 는 창마다 따로라 서로 못 봄(2026-09-08 랩탑 두 창이 막힌 자리).
|
|
|
|
⚠ **원본은 읽기만 한다** — SELECT 뿐이고 원본 행·파일에 쓰지 않는다.
|
|
⚠ 다만 **복제 중에 원본이 바뀌면 창마다 복제본이 달라진다** — 원본을 얼린 뒤 뜰 것.
|
|
|
|
무엇을 뜨나
|
|
DB projects · project_members · input_files · processed_point_cloud · surface_models ·
|
|
terrain_layers · routes · route_points · route_statistics · longitudinal_sections ·
|
|
cross_sections · project_workflow_stages · (D) plan_route_years · plan_route_assignees —
|
|
종류(B · D · E)는 project_kind 로 판별 · D 설계 내용 = 폴더 파일(아래 파일째 따라감)
|
|
⚠ 자동증가 id 는 **새로 받고** 참조(외래키)를 새 id 로 다시 이어 붙인다.
|
|
파일 저장소 폴더 통째(robocopy). 경로가 프로젝트 루트 기준 상대라 그대로 쓰인다.
|
|
치환 파일 안에 박힌 **옛 프로젝트 UUID**, 초기값 스냅숏의 **surface_model_id**.
|
|
|
|
⚠ **DB 만 뜨거나 파일만 떠서는 안 된다** — 수량(B08)은 DB(측점·횡단)를 보고,
|
|
구조물·관 정본은 파일(`structures.json`·`pipe_points.json`)이라 **둘 다** 있어야 화면이 선다.
|
|
|
|
쓰는 법
|
|
./venv/Scripts/python.exe db_management/tools_clone_project.py <원본 UUID> "<새 이름>"
|
|
[--owner <사용자 id>] [--members <id,id>] (없으면 원본 주인 · 참여자 그대로)
|
|
셈만(쓰지 않음 — 종류 · 표마다 행 수 · 목록 밖 표 · 폴더 크기)
|
|
./venv/Scripts/python.exe db_management/tools_clone_project.py <원본 UUID> - --dry-run
|
|
자리 바꿔 넣기(대상 id · 이름 · 주인 · 참여자 · 종류 유지 · 대상 옛 행 지우고 원본으로 채움 ·
|
|
storage 를 원본과 똑같이(/MIR) · 넣기 전 대상 행을 --backup-dir/<대상 id>.json 에 덤프)
|
|
./venv/Scripts/python.exe db_management/tools_clone_project.py <원본 UUID> - \
|
|
--replace <대상 UUID> [--backup-dir tmp/clone_replace_bk]
|
|
storage 만(DB 는 이미 복제됨 — 다른 PC 의 storage 를 맞출 때)
|
|
./venv/Scripts/python.exe db_management/tools_clone_project.py \\
|
|
<원본 UUID> <새 UUID> --storage-only
|
|
"""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
|
|
|
import aiomysql
|
|
|
|
from common_util.common_util_project_copy import clone_full, clone_storage_only, dry_run
|
|
from common_util.common_util_project_copy_replace import replace_project
|
|
from config.config_db import DB_HOST, DB_NAME, DB_PASSWORD, DB_PORT, DB_USER
|
|
|
|
|
|
def _args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description="프로젝트를 통째로 복제한다(원본은 읽기만).")
|
|
parser.add_argument("source_id", help="원본 프로젝트 UUID")
|
|
parser.add_argument(
|
|
"new_name",
|
|
nargs="?",
|
|
help="새 프로젝트 이름(--storage-only 면 새 프로젝트 UUID · --dry-run 이면 없어도 됨)",
|
|
)
|
|
parser.add_argument(
|
|
"--storage-only",
|
|
action="store_true",
|
|
help="DB 는 읽기만 · storage 폴더 복사와 파일 안 id 바꿈만",
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run", action="store_true", help="쓰지 않고 표마다 행 수 · 폴더 크기만"
|
|
)
|
|
parser.add_argument("--replace", metavar="TARGET_ID", help="원본 내용을 넣을 대상 UUID")
|
|
parser.add_argument(
|
|
"--backup-dir", default="tmp/clone_replace_bk", help="--replace 전 대상 행 덤프 폴더"
|
|
)
|
|
parser.add_argument("--owner", type=int, help="새 주인 사용자 id(없으면 원본 주인)")
|
|
parser.add_argument("--members", help="참여자 사용자 id 쉼표 목록(없으면 원본 참여자)")
|
|
args = parser.parse_args(argv)
|
|
if not args.dry_run and not args.replace and not args.new_name:
|
|
parser.error("new_name 필요")
|
|
return args
|
|
|
|
|
|
async def main(argv: list[str] | None = None) -> str | None:
|
|
args = _args(argv)
|
|
connection = await aiomysql.connect(
|
|
host=DB_HOST,
|
|
port=DB_PORT,
|
|
user=DB_USER,
|
|
password=DB_PASSWORD,
|
|
db=DB_NAME,
|
|
charset="utf8mb4",
|
|
)
|
|
try:
|
|
if args.dry_run:
|
|
await dry_run(connection, args.source_id)
|
|
return None
|
|
if args.replace:
|
|
await replace_project(
|
|
connection, args.source_id, args.replace, backup_dir=Path(args.backup_dir)
|
|
)
|
|
return args.replace
|
|
if args.storage_only:
|
|
await clone_storage_only(connection, args.source_id, args.new_name)
|
|
return None
|
|
members = (
|
|
[int(user) for user in args.members.split(",") if user.strip()]
|
|
if args.members is not None
|
|
else None
|
|
)
|
|
return await clone_full(
|
|
connection, args.source_id, args.new_name, owner=args.owner, members=members
|
|
)
|
|
finally:
|
|
connection.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|