"""프로젝트 영구저장소 경로 유틸리티.""" import os from pathlib import PurePosixPath from config.config_system import PROJECT_STORAGE_STAGE_DIRS, STORAGE_BASE_DIR PROJECT_STORAGE_LAYOUT_V2 = ( ("B03_FileInput", "input"), ("B04_wf1_Surface", "processed"), ("B04_wf1_Surface", "models"), ("B05_wf2_Route", "route"), ("B06_wf3_ProfileCross", "longitudinal"), ("B06_wf3_ProfileCross", "cross_sections"), ("B07_wf4_DesignDetail", "structures"), ("B08_wf5_Quantity", "quantities"), ("B09_wf6_Estimation", "v1"), ) def get_project_stage_path(project_root: str, stage: str) -> str: """프로젝트 루트 아래의 허용된 워크플로우 단계 폴더를 생성한다.""" if stage not in PROJECT_STORAGE_STAGE_DIRS: raise ValueError(f"허용되지 않은 프로젝트 저장 단계입니다: {stage}") root = os.path.abspath(project_root) path = os.path.abspath(os.path.join(root, stage)) if os.path.commonpath((root, path)) != root: raise ValueError("단계 저장 경로가 프로젝트 루트를 벗어났습니다.") os.makedirs(path, exist_ok=True) return path def ensure_project_storage_layout(project_root: str) -> None: """신규 워크플로우 저장소 하위 폴더를 누락분만 생성한다.""" root = os.path.abspath(project_root) for stage, subdir in PROJECT_STORAGE_LAYOUT_V2: if stage not in PROJECT_STORAGE_STAGE_DIRS: raise ValueError(f"허용되지 않은 프로젝트 저장 단계입니다: {stage}") path = os.path.abspath(os.path.join(root, stage, subdir)) if os.path.commonpath((root, path)) != root: raise ValueError("단계 저장 경로가 프로젝트 루트를 벗어났습니다.") os.makedirs(path, exist_ok=True) def resolve_stored_project_path(relative_path: str) -> str: """DB의 storage 기준 상대 경로를 검증해 실제 프로젝트 경로로 변환한다.""" normalized = PurePosixPath(relative_path.replace("\\", "/")) if normalized.is_absolute() or ".." in normalized.parts: raise ValueError("프로젝트 저장 경로는 안전한 상대 경로여야 합니다.") if not normalized.parts or normalized.parts[0] != "storage": raise ValueError("프로젝트 저장 경로는 storage/로 시작해야 합니다.") storage_root = os.path.abspath(STORAGE_BASE_DIR) path = os.path.abspath(os.path.join(storage_root, *normalized.parts[1:])) if os.path.commonpath((storage_root, path)) != storage_root or path == storage_root: raise ValueError("프로젝트 저장 경로가 저장소 루트를 벗어났습니다.") os.makedirs(path, exist_ok=True) ensure_project_storage_layout(path) return path