""" config_system.py 백엔드 작동 제어 및 알고리즘 파라미터 모든 제어 변수는 여기서 관리. 코드 내 하드코딩 금지. """ import os from pathlib import Path from dotenv import load_dotenv PROJECT_ROOT = Path(__file__).resolve().parent.parent load_dotenv(PROJECT_ROOT / ".env") os.environ["PYTHONUTF8"] = "1" # ───────────────────────────────────────────────────────────────────────── # 1. FastAPI 서버 # ───────────────────────────────────────────────────────────────────────── SERVER_HOST = os.getenv("SERVER_HOST", "0.0.0.0") SERVER_PORT = int(os.getenv("SERVER_PORT", "8000")) DEBUG = os.getenv("DEBUG", "False").lower() == "true" # 정적 파일 서빙 (프론트엔드 빌드 결과) STATIC_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "node_modules", ".build") STATIC_URL = "/static" # ───────────────────────────────────────────────────────────────────────── # 2. 로깅 # ───────────────────────────────────────────────────────────────────────── LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") # ───────────────────────────────────────────────────────────────────────── # 3. 데이터베이스 (MariaDB) # ───────────────────────────────────────────────────────────────────────── DB_HOST = os.getenv("DB_HOST", "localhost") DB_PORT = int(os.getenv("DB_PORT", "3306")) DB_NAME = os.getenv("DB_NAME", "aislo_db") DB_USER = os.getenv("DB_USER", "aislo") DB_PASSWORD = os.getenv("DB_PASSWORD", "aislo") # asyncmy 연결 풀 설정 DB_POOL_MIN = int(os.getenv("DB_POOL_MIN", "5")) DB_POOL_MAX = int(os.getenv("DB_POOL_MAX", "20")) # ───────────────────────────────────────────────────────────────────────── # 4. 파일 업로드 제한 # ───────────────────────────────────────────────────────────────────────── UPLOAD_MAX_MB = int(os.getenv("UPLOAD_MAX_MB", "500")) UPLOAD_MAX_FILES = int(os.getenv("UPLOAD_MAX_FILES", "20")) UPLOAD_CHUNK_SIZE_BYTES = int(os.getenv("UPLOAD_CHUNK_SIZE_BYTES", str(1024 * 1024))) UPLOAD_ALLOWED_EXT = [".las", ".laz", ".dem", ".tif", ".tiff", ".tfw", ".prj", ".dxf", ".dwg"] # ───────────────────────────────────────────────────────────────────────── # 5. 지형 분석 알고리즘 파라미터 # ───────────────────────────────────────────────────────────────────────── # Trimesh 메쉬 생성 MESH_GRID_SIZE = float(os.getenv("MESH_GRID_SIZE", "1.0")) # 미터 단위 MESH_SMOOTHING_ITERATIONS = int(os.getenv("MESH_SMOOTHING_ITERATIONS", "0")) SURFACE_LAS_CHUNK_SIZE = int(os.getenv("SURFACE_LAS_CHUNK_SIZE", "500000")) SURFACE_DEFAULT_RGB_VALUE = int(os.getenv("SURFACE_DEFAULT_RGB_VALUE", "128")) SURFACE_GRID_CELL_SIZE_M = float(os.getenv("SURFACE_GRID_CELL_SIZE_M", "2.0")) SURFACE_GRID_HEIGHT_THRESHOLD_M = float(os.getenv("SURFACE_GRID_HEIGHT_THRESHOLD_M", "1.5")) # CSF (Cloth Simulation Filter) 지면 분류 파라미터 SURFACE_CSF_CLOTH_RESOLUTION_M = float(os.getenv("SURFACE_CSF_CLOTH_RESOLUTION_M", "1.5")) SURFACE_CSF_RIGIDNESS = int(os.getenv("SURFACE_CSF_RIGIDNESS", "1")) SURFACE_CSF_TIME_STEP = float(os.getenv("SURFACE_CSF_TIME_STEP", "0.65")) SURFACE_CSF_CLASS_THRESHOLD_M = float(os.getenv("SURFACE_CSF_CLASS_THRESHOLD_M", "0.5")) SURFACE_CSF_ITERATIONS = int(os.getenv("SURFACE_CSF_ITERATIONS", "150")) SURFACE_CSF_SLOPE_SMOOTH = os.getenv("SURFACE_CSF_SLOPE_SMOOTH", "True").lower() == "true" SURFACE_CSF_SLOPE_SMOOTH_THRESHOLD_M = float( os.getenv("SURFACE_CSF_SLOPE_SMOOTH_THRESHOLD_M", "1.8") ) # PMF (Progressive Morphological Filter) 지면 분류 파라미터 SURFACE_PMF_CELL_SIZE_M = float(os.getenv("SURFACE_PMF_CELL_SIZE_M", "2.0")) SURFACE_PMF_MAX_WINDOW_SIZE = int(os.getenv("SURFACE_PMF_MAX_WINDOW_SIZE", "40")) SURFACE_PMF_INITIAL_WINDOW_SIZE = int(os.getenv("SURFACE_PMF_INITIAL_WINDOW_SIZE", "3")) SURFACE_PMF_SLOPE = float(os.getenv("SURFACE_PMF_SLOPE", "1.0")) SURFACE_PMF_MAX_DISTANCE_M = float(os.getenv("SURFACE_PMF_MAX_DISTANCE_M", "2.5")) # RANSAC (Local plane fitting) 지면 분류 파라미터 SURFACE_RANSAC_DISTANCE_THRESHOLD_M = float(os.getenv("SURFACE_RANSAC_DISTANCE_THRESHOLD_M", "0.3")) SURFACE_RANSAC_N = int(os.getenv("SURFACE_RANSAC_N", "3")) SURFACE_RANSAC_ITERATIONS = int(os.getenv("SURFACE_RANSAC_ITERATIONS", "100")) SURFACE_RANSAC_LOCAL_GRID_SIZE_M = float(os.getenv("SURFACE_RANSAC_LOCAL_GRID_SIZE_M", "10.0")) SURFACE_RANSAC_SEED = int(os.getenv("SURFACE_RANSAC_SEED", "42")) # ───────────────────────────────────────────────────────────────────────── # 5-2. 지표면 모델 생성 파라미터 (TIN/DTM/NURBS/implicit/meshfree) # ───────────────────────────────────────────────────────────────────────── # 빌드 대상 지면 필터·표현 방식 SURFACE_MODEL_SOURCE_FILTERS = tuple( os.getenv("SURFACE_MODEL_SOURCE_FILTERS", "grid_min_z,csf,pmf").split(",") ) SURFACE_MODEL_PRECOMPUTE = tuple( os.getenv("SURFACE_MODEL_PRECOMPUTE", "tin,dtm,nurbs,implicit,meshfree").split(",") ) SURFACE_MODEL_SMOOTHING_METHODS = tuple( os.getenv("SURFACE_MODEL_SMOOTHING_METHODS", "dtm,tin").split(",") ) SURFACE_MODEL_SYNC_TIMEOUT_SECONDS = int(os.getenv("SURFACE_MODEL_SYNC_TIMEOUT_SECONDS", "0")) # footprint(외곽) 산출 SURFACE_FOOTPRINT_RESOLUTION_M = float(os.getenv("SURFACE_FOOTPRINT_RESOLUTION_M", "1.0")) SURFACE_FOOTPRINT_GAP_CLOSE_M = float(os.getenv("SURFACE_FOOTPRINT_GAP_CLOSE_M", "1.0")) SURFACE_BOUNDARY_INSET_M = float(os.getenv("SURFACE_BOUNDARY_INSET_M", "1.0")) SURFACE_KEEP_LARGEST_FOOTPRINT = ( os.getenv("SURFACE_KEEP_LARGEST_FOOTPRINT", "True").lower() == "true" ) SURFACE_TILE_SIZE_M = float(os.getenv("SURFACE_TILE_SIZE_M", "50.0")) SURFACE_MAX_PREVIEW_VERTICES = int(os.getenv("SURFACE_MAX_PREVIEW_VERTICES", "120000")) # 표현별 파라미터 SURFACE_TIN_MAX_INPUT_POINTS = int(os.getenv("SURFACE_TIN_MAX_INPUT_POINTS", "200000")) SURFACE_DTM_GRID_RESOLUTION_M = float(os.getenv("SURFACE_DTM_GRID_RESOLUTION_M", "1.0")) SURFACE_NURBS_DEGREE = int(os.getenv("SURFACE_NURBS_DEGREE", "3")) SURFACE_NURBS_PATCH_SIZE_M = float(os.getenv("SURFACE_NURBS_PATCH_SIZE_M", "50.0")) SURFACE_NURBS_CONTROL_POINTS_PER_AXIS = int( os.getenv("SURFACE_NURBS_CONTROL_POINTS_PER_AXIS", "16") ) SURFACE_IMPLICIT_MAX_POINTS_PER_TILE = int( os.getenv("SURFACE_IMPLICIT_MAX_POINTS_PER_TILE", "20000") ) SURFACE_IMPLICIT_SMOOTHING = float(os.getenv("SURFACE_IMPLICIT_SMOOTHING", "0.5")) SURFACE_MESHFREE_MAX_MODEL_POINTS = int(os.getenv("SURFACE_MESHFREE_MAX_MODEL_POINTS", "300000")) SURFACE_MESHFREE_POINT_RADIUS_M = float(os.getenv("SURFACE_MESHFREE_POINT_RADIUS_M", "0.5")) # 스무딩 파라미터 SURFACE_SMOOTHING_DTM_SIGMA_M = float(os.getenv("SURFACE_SMOOTHING_DTM_SIGMA_M", "0.5")) SURFACE_SMOOTHING_DTM_SPLINE_SMOOTH = float(os.getenv("SURFACE_SMOOTHING_DTM_SPLINE_SMOOTH", "0.0")) SURFACE_SMOOTHING_DTM_PREVIEW_RESOLUTION_M = float( os.getenv("SURFACE_SMOOTHING_DTM_PREVIEW_RESOLUTION_M", "0.5") ) SURFACE_SMOOTHING_TIN_TAUBIN_ITERATIONS = int( os.getenv("SURFACE_SMOOTHING_TIN_TAUBIN_ITERATIONS", "10") ) SURFACE_SMOOTHING_TIN_TAUBIN_LAMBDA = float(os.getenv("SURFACE_SMOOTHING_TIN_TAUBIN_LAMBDA", "0.5")) SURFACE_SMOOTHING_TIN_TAUBIN_MU = float(os.getenv("SURFACE_SMOOTHING_TIN_TAUBIN_MU", "-0.53")) # 등고선 파라미터 SURFACE_CONTOUR_INTERVAL_M = float(os.getenv("SURFACE_CONTOUR_INTERVAL_M", "5.0")) SURFACE_CONTOUR_GRID_RESOLUTION_M = float(os.getenv("SURFACE_CONTOUR_GRID_RESOLUTION_M", "1.0")) def build_surface_model_config() -> dict: """지표면 모델 파이프라인이 사용하는 config dict를 조립한다.""" return { "source_filters": list(SURFACE_MODEL_SOURCE_FILTERS), "precompute": list(SURFACE_MODEL_PRECOMPUTE), "smoothing_methods": tuple(SURFACE_MODEL_SMOOTHING_METHODS), "sync_timeout_seconds": SURFACE_MODEL_SYNC_TIMEOUT_SECONDS, "footprint_resolution_meters": SURFACE_FOOTPRINT_RESOLUTION_M, "footprint_gap_close_meters": SURFACE_FOOTPRINT_GAP_CLOSE_M, "boundary_inset_meters": SURFACE_BOUNDARY_INSET_M, "keep_largest_footprint": SURFACE_KEEP_LARGEST_FOOTPRINT, "tile_size_meters": SURFACE_TILE_SIZE_M, "max_preview_vertices": SURFACE_MAX_PREVIEW_VERTICES, "tin_max_input_points": SURFACE_TIN_MAX_INPUT_POINTS, "dtm_grid_resolution_meters": SURFACE_DTM_GRID_RESOLUTION_M, "nurbs_degree": SURFACE_NURBS_DEGREE, "nurbs_patch_size_meters": SURFACE_NURBS_PATCH_SIZE_M, "nurbs_control_points_per_axis": SURFACE_NURBS_CONTROL_POINTS_PER_AXIS, "implicit_max_points_per_tile": SURFACE_IMPLICIT_MAX_POINTS_PER_TILE, "implicit_smoothing": SURFACE_IMPLICIT_SMOOTHING, "meshfree_max_model_points": SURFACE_MESHFREE_MAX_MODEL_POINTS, "meshfree_point_radius_meters": SURFACE_MESHFREE_POINT_RADIUS_M, "smoothing_dtm_sigma_meters": SURFACE_SMOOTHING_DTM_SIGMA_M, "smoothing_dtm_spline_smooth": SURFACE_SMOOTHING_DTM_SPLINE_SMOOTH, "smoothing_dtm_preview_resolution_meters": SURFACE_SMOOTHING_DTM_PREVIEW_RESOLUTION_M, "smoothing_tin_taubin_iterations": SURFACE_SMOOTHING_TIN_TAUBIN_ITERATIONS, "smoothing_tin_taubin_lambda": SURFACE_SMOOTHING_TIN_TAUBIN_LAMBDA, "smoothing_tin_taubin_mu": SURFACE_SMOOTHING_TIN_TAUBIN_MU, "contour_interval_meters": SURFACE_CONTOUR_INTERVAL_M, "contour_grid_resolution_meters": SURFACE_CONTOUR_GRID_RESOLUTION_M, } # ───────────────────────────────────────────────────────────────────────── # 5-3. 경로 설계 파라미터 (B05 WF2) # ───────────────────────────────────────────────────────────────────────── # 비용 함수 가중치 ROUTE_W_DIST = float(os.getenv("ROUTE_W_DIST", "1.0")) ROUTE_W_GRADE = float(os.getenv("ROUTE_W_GRADE", "2.0")) ROUTE_W_SIDE = float(os.getenv("ROUTE_W_SIDE", "1.5")) ROUTE_W_CURVE = float(os.getenv("ROUTE_W_CURVE", "0.5")) ROUTE_W_AVOID = float(os.getenv("ROUTE_W_AVOID", "10.0")) ROUTE_WEIGHT_MAX = float(os.getenv("ROUTE_WEIGHT_MAX", "1000.0")) # 격자·경사·비용 제약 ROUTE_MAX_GRADE = float(os.getenv("ROUTE_MAX_GRADE", "0.14")) ROUTE_MAX_GRADE_PAVED = float(os.getenv("ROUTE_MAX_GRADE_PAVED", "0.18")) ROUTE_GRID_RES_M = float(os.getenv("ROUTE_GRID_RES_M", "2.0")) ROUTE_AVOID_DEFAULT_RADIUS_M = float(os.getenv("ROUTE_AVOID_DEFAULT_RADIUS_M", "25.0")) ROUTE_DEFAULT_GRADE_CLASS = os.getenv("ROUTE_DEFAULT_GRADE_CLASS", "trunk") ROUTE_MAX_COST_CELLS = int(os.getenv("ROUTE_MAX_COST_CELLS", "4000000")) ROUTE_REQUIRED_POINT_TOLERANCE_M = float(os.getenv("ROUTE_REQUIRED_POINT_TOLERANCE_M", "1.0")) ROUTE_GRADE_CLASSES = ("trunk", "branch", "work") # 임도 등급별 종단경사 한계 및 최소 곡선반지름 FOREST_ROAD_MAX_GRADE = {"trunk": 0.26, "branch": 0.28, "work": 0.40} FOREST_ROAD_MIN_CURVE_R_M = {"trunk": 12.0, "branch": 10.0, "work": 6.0} # 대안(정속경사) 파라미터 ROUTE_ALT_MIN_GRADE = float(os.getenv("ROUTE_ALT_MIN_GRADE", "0.08")) ROUTE_ALT_MAX_GRADE = float(os.getenv("ROUTE_ALT_MAX_GRADE", "0.14")) ROUTE_ALT_GRADE_TOLERANCE = float(os.getenv("ROUTE_ALT_GRADE_TOLERANCE", "0.005")) # 지형 skeleton (능선/계곡) 파라미터 SKELETON_VALLEY_ACC_THRESHOLD_CELLS = int(os.getenv("SKELETON_VALLEY_ACC_THRESHOLD_CELLS", "500")) SKELETON_MAIN_VALLEY_ACC_THRESHOLD_CELLS = int( os.getenv("SKELETON_MAIN_VALLEY_ACC_THRESHOLD_CELLS", "5000") ) SKELETON_RIDGE_ACC_THRESHOLD_CELLS = int(os.getenv("SKELETON_RIDGE_ACC_THRESHOLD_CELLS", "500")) SKELETON_MAIN_RIDGE_ACC_THRESHOLD_CELLS = int( os.getenv("SKELETON_MAIN_RIDGE_ACC_THRESHOLD_CELLS", "5000") ) SKELETON_NODE_SPACING_M = float(os.getenv("SKELETON_NODE_SPACING_M", "10.0")) # ───────────────────────────────────────────────────────────────────────── # 5-4. 종횡단 생성 파라미터 (B06 WF3) # ───────────────────────────────────────────────────────────────────────── SECTION_STATION_INTERVAL_M = float(os.getenv("SECTION_STATION_INTERVAL_M", "20.0")) SECTION_CROSS_HALF_WIDTH_M = float(os.getenv("SECTION_CROSS_HALF_WIDTH_M", "15.0")) SECTION_CROSS_SAMPLE_INTERVAL_M = float(os.getenv("SECTION_CROSS_SAMPLE_INTERVAL_M", "0.5")) SECTION_LONG_SAMPLE_INTERVAL_M = float(os.getenv("SECTION_LONG_SAMPLE_INTERVAL_M", "1.0")) SECTION_INCLUDE_ENDPOINT = os.getenv("SECTION_INCLUDE_ENDPOINT", "True").lower() == "true" # ───────────────────────────────────────────────────────────────────────── # 6. 저장소 경로 # ───────────────────────────────────────────────────────────────────────── STORAGE_BASE_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "storage") # 시스템 리소스 로그 (루트 log 폴더에 단일 파일, 1개월 보관) LOG_BASE_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "log") RESOURCE_LOG_PATH = os.path.join(LOG_BASE_DIR, "system_resources.log") RESOURCE_SAMPLE_INTERVAL_SEC = 120 # 계측 주기: 2분 RESOURCE_LOG_RETENTION_DAYS = 30 # 로그 보관: 1개월 PROJECT_STORAGE_STAGE_DIRS = frozenset( { "B03_FileInput", "B04_wf1_Surface", "B05_wf2_Route", "B06_wf3_ProfileCross", "B07_wf4_DesignDetail", "B08_wf5_Quantity", "B09_wf6_Estimation", } ) # 프로젝트별 저장 구조: storage/[고객사]/[사용자]/[프로젝트ID]/ def get_project_storage_path(company: str, user: str, project_id: str) -> str: """검증된 식별자로 프로젝트 저장 경로를 생성한다.""" segments = (company, user, project_id) for segment in segments: if ( not segment or segment in {".", ".."} or os.path.isabs(segment) or "/" in segment or "\\" in segment ): raise ValueError("저장 경로 식별자는 비어 있거나 경로 구분자를 포함할 수 없습니다.") storage_root = os.path.abspath(STORAGE_BASE_DIR) path = os.path.abspath(os.path.join(storage_root, *segments)) if os.path.commonpath((storage_root, path)) != storage_root: raise ValueError("프로젝트 저장 경로가 저장소 루트를 벗어났습니다.") os.makedirs(path, exist_ok=True) return path # ───────────────────────────────────────────────────────────────────────── # 7. 인증 및 이메일 # ───────────────────────────────────────────────────────────────────────── SESSION_COOKIE_NAME = os.getenv("SESSION_COOKIE_NAME", "session_id") SESSION_MAX_AGE_SECONDS = int(os.getenv("SESSION_MAX_AGE_SECONDS", "43200")) SESSION_IDLE_TIMEOUT_SECONDS = int(os.getenv("SESSION_IDLE_TIMEOUT_SECONDS", "14400")) SESSION_COOKIE_SECURE = os.getenv("SESSION_COOKIE_SECURE", "True").lower() == "true" PASSWORD_BCRYPT_ROUNDS = int(os.getenv("PASSWORD_BCRYPT_ROUNDS", "12")) OTP_VALID_MINUTES = int(os.getenv("EMAIL_OTP_VALID_MINUTES", "5")) EMAIL_REVERIFY_DAYS = int(os.getenv("EMAIL_REVERIFY_DAYS", "90")) SMTP_HOST = os.getenv("SMTP_HOST", "smtp.gmail.com") SMTP_PORT = int(os.getenv("SMTP_PORT", "587")) SMTP_USERNAME = os.getenv("SMTP_USERNAME", "") SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "") SMTP_USE_TLS = os.getenv("SMTP_USE_TLS", "True").lower() == "true" EMAIL_FROM_ADDRESS = os.getenv("EMAIL_FROM_ADDRESS", SMTP_USERNAME) EMAIL_FROM_NAME = os.getenv("EMAIL_FROM_NAME", "Aislo Support") EMAIL_RETRY_COUNT = int(os.getenv("EMAIL_RETRY_COUNT", "3")) EMAIL_RETRY_DELAY_SECONDS = int(os.getenv("EMAIL_RETRY_DELAY_SECONDS", "2")) EMAIL_NOTIFICATION_ENABLED = os.getenv("EMAIL_NOTIFICATION_ENABLED", "True").lower() == "true" ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "") # ───────────────────────────────────────────────────────────────────────── # 8. CORS (프론트엔드 도메인) # ───────────────────────────────────────────────────────────────────────── CORS_ORIGINS = os.getenv("CORS_ORIGINS", "http://localhost:5173,http://localhost:8000").split(",") # ───────────────────────────────────────────────────────────────────────── # 9. 환경 정보 # ───────────────────────────────────────────────────────────────────────── ENVIRONMENT = os.getenv("ENVIRONMENT", "development") # development, staging, production