"""B02_ProjRegister aiomysql Raw SQL 저장소.""" from __future__ import annotations from datetime import datetime from pathlib import Path from typing import Any from uuid import uuid4 import aiomysql from common_util.common_util_json import atomic_write_json from common_util.common_util_storage import PROJECT_STORAGE_LAYOUT_V2 from common_util.common_util_workflow import load_project_workflow from config.config_db import get_db_pool from config.config_system import STORAGE_BASE_DIR def _build_project_storage(company_id: int, user_id: int, project_id: str) -> tuple[str, Path]: relative_path = f"storage/{company_id}/{user_id}/{project_id}" storage_root = Path(STORAGE_BASE_DIR).resolve() project_root = (storage_root / str(company_id) / str(user_id) / project_id).resolve() if storage_root not in (project_root, *project_root.parents): raise ValueError("프로젝트 저장 경로가 저장소 루트를 벗어났습니다.") return relative_path, project_root def _initialize_project_storage(project_root: Path, project_id: str) -> None: for stage, subdir in PROJECT_STORAGE_LAYOUT_V2: (project_root / stage / subdir).mkdir(parents=True, exist_ok=True) atomic_write_json(project_root / "workflow.json", load_project_workflow(project_root)) atomic_write_json( project_root / "project_manifest.json", { "project_id": project_id, "storage_version": 2, "created_at": datetime.utcnow().isoformat(timespec="seconds"), "stages": [f"{stage}/{subdir}" for stage, subdir in PROJECT_STORAGE_LAYOUT_V2], }, ) async def create_project( *, user_id: int, company_id: int, name: str, region: str, road_type: str, project_year: int, estimated_length_m: float | None, memo: str | None, ) -> dict[str, Any]: """신규 프로젝트를 DB에 저장하고 워크플로우 저장소를 초기화한다.""" project_id = str(uuid4()) storage_path, project_root = _build_project_storage(company_id, user_id, project_id) now = datetime.utcnow() pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: try: await connection.begin() await cursor.execute( """ INSERT INTO projects ( id, user_id, company_id, name, region, road_type, project_year, estimated_length_m, memo, status, crs_epsg, storage_path, created_at, updated_at ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, 'NEW', 5178, %s, %s, %s) """, ( project_id, user_id, company_id, name, region, road_type, project_year, estimated_length_m, memo, storage_path, now, now, ), ) await cursor.execute( """INSERT INTO system_audit_logs (user_id, action, resource_type, resource_id) VALUES (%s, 'PROJECT_CREATE', 'project', NULL)""", (user_id,), ) _initialize_project_storage(project_root, project_id) await connection.commit() except Exception: await connection.rollback() raise await cursor.execute( """ SELECT id AS project_id, name, region, road_type, project_year, estimated_length_m, memo, status, storage_path, DATE_FORMAT(created_at, '%%Y-%%m-%%dT%%H:%%i:%%s') AS created_at FROM projects WHERE id = %s """, (project_id,), ) row = await cursor.fetchone() if not row: raise RuntimeError("생성된 프로젝트를 다시 조회할 수 없습니다.") return dict(row) async def get_project_by_id(project_id: str) -> dict[str, Any] | None: """프로젝트 단건 조회.""" pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await cursor.execute( """ SELECT id AS project_id, user_id, company_id, name, region, road_type, project_year, estimated_length_m, memo, status, storage_path, DATE_FORMAT(created_at, '%%Y-%%m-%%dT%%H:%%i:%%s') AS created_at FROM projects WHERE id = %s AND deleted_at IS NULL """, (project_id,), ) return await cursor.fetchone()