라이다 원본은 업로드에 오래 걸려 프로젝트 정보 확정 전에 미리 올릴 수 있어야 한다.
계정에 묶인 임시 보관함을 만들고, 나중에 만든 프로젝트로 자료를 옮겨 쓴다.
저장·DB
- storage/tmp/{user_id}/{batch_id}/ 아래에 프로젝트 저장소와 동일한 구조를 써서
청크 저장·병합 엔진(resolve_upload_destination/merge_upload_chunks)을 그대로 재사용
- 010_temp_upload.sql: temp_upload_batches / temp_upload_files 신설,
upload_sessions.project_id NULL 허용 + temp_batch_id 추가(FK명 조회 후 재생성)
- config: TEMP_UPLOAD_DIR_NAME / TEMP_UPLOAD_RETENTION_DAYS(30) /
TEMP_UPLOAD_CLEANUP_INTERVAL_HOURS(6)
백엔드
- B03_FileInput_Router_Temp.py: 묶음 생성·목록·삭제, 일반/청크 업로드, finalize,
이어올리기 상태 조회, 프로젝트 연결(attach)
- attach: 파일 이동 후 input_files 등록, stage 0 완료, WF1·자동 설계 체인 트리거
- common_util_temp_cleanup.py: 완료 시각 기준 만료분 주기 삭제(서버 시작 시 1회 포함)
프론트엔드
- B01 대시보드 임시 보관함 섹션: 프로젝트 등록과 같은 폼 + 보관 목록.
진행률은 모달이 아니라 리스트 행에 표시, 새로고침 후 이어올리기 지원
- B03 업로드 컨테이너 내부 불러오기 버튼과 선택 모달.
완료된 묶음만 노출하고, 선택 후 업로드를 누르면 이동과 분석으로 이어짐
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
767 lines
50 KiB
Python
767 lines
50 KiB
Python
"""
|
||
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(__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", str(30 * 1024)))
|
||
UPLOAD_MAX_FILES = int(os.getenv("UPLOAD_MAX_FILES", "5"))
|
||
UPLOAD_CHUNK_SIZE_BYTES = int(os.getenv("UPLOAD_CHUNK_SIZE_BYTES", str(1024 * 1024 * 1024)))
|
||
UPLOAD_ALLOWED_EXT = [".csv", ".las", ".laz", ".tif", ".tfw", ".prj", ".dxf"]
|
||
CHUNK_TEMP_DIR = os.getenv("CHUNK_TEMP_DIR", "B03_FileInput/chunks_temp")
|
||
CHUNK_RETENTION_HOURS = int(os.getenv("CHUNK_RETENTION_HOURS", "24"))
|
||
|
||
# 프로젝트 생성 전 임시 보관함 — 계정에 묶어 자료를 먼저 올려 두는 공간.
|
||
# 라이다 원본이 수십 GB라 보관 기간이 길면 디스크를 금방 채운다. 보관 기간과 정리 주기는
|
||
# 운영하며 조정할 값이라 여기서 관리한다(2026-08-08 사용자 지시).
|
||
TEMP_UPLOAD_DIR_NAME = os.getenv("TEMP_UPLOAD_DIR_NAME", "tmp")
|
||
TEMP_UPLOAD_RETENTION_DAYS = int(os.getenv("TEMP_UPLOAD_RETENTION_DAYS", "30"))
|
||
TEMP_UPLOAD_CLEANUP_INTERVAL_HOURS = int(os.getenv("TEMP_UPLOAD_CLEANUP_INTERVAL_HOURS", "6"))
|
||
MERGE_TIMEOUT_SECONDS = int(os.getenv("MERGE_TIMEOUT_SECONDS", "3600"))
|
||
SEND_ANALYSIS_COMPLETION_EMAIL = (
|
||
os.getenv("SEND_ANALYSIS_COMPLETION_EMAIL", "True").lower() == "true"
|
||
)
|
||
# WF1 자동 확정 후 B05 기본 경로 → B06 기본 횡단 설계까지 서버가 이어서 계산·확정한다
|
||
# (2026-08-04 사용자 확정). 프로젝트에 경로가 이미 있으면 체인은 스스로 건너뛴다.
|
||
AUTO_DESIGN_CHAIN_ENABLED = os.getenv("AUTO_DESIGN_CHAIN_ENABLED", "True").lower() == "true"
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 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(",")
|
||
)
|
||
# dtm을 먼저 빌드해야 TIN 등고선 사전 캐시가 dtm footprint를 참조할 수 있다 (PLAN D-6)
|
||
SURFACE_MODEL_PRECOMPUTE = tuple(
|
||
os.getenv("SURFACE_MODEL_PRECOMPUTE", "dtm,tin,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", "500000"))
|
||
|
||
# 표현별 파라미터 (old 버전 검증값 기준 — PLAN F)
|
||
SURFACE_TIN_MAX_INPUT_POINTS = int(os.getenv("SURFACE_TIN_MAX_INPUT_POINTS", "500000"))
|
||
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", "10000")
|
||
)
|
||
SURFACE_IMPLICIT_SMOOTHING = float(os.getenv("SURFACE_IMPLICIT_SMOOTHING", "0.1"))
|
||
SURFACE_MESHFREE_MAX_MODEL_POINTS = int(os.getenv("SURFACE_MESHFREE_MAX_MODEL_POINTS", "500000"))
|
||
SURFACE_MESHFREE_POINT_RADIUS_M = float(os.getenv("SURFACE_MESHFREE_POINT_RADIUS_M", "0.15"))
|
||
|
||
# 스무딩 파라미터
|
||
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", "1.0"))
|
||
SURFACE_CONTOUR_GRID_RESOLUTION_M = float(os.getenv("SURFACE_CONTOUR_GRID_RESOLUTION_M", "1.0"))
|
||
|
||
# 일반 사용자 WF1 자동 확정 기본값
|
||
SURFACE_CONFIRM_DEFAULT_FILTER = os.getenv("SURFACE_CONFIRM_DEFAULT_FILTER", "csf")
|
||
SURFACE_CONFIRM_DEFAULT_METHOD = os.getenv("SURFACE_CONFIRM_DEFAULT_METHOD", "dtm")
|
||
SURFACE_CONFIRM_DEFAULT_SMOOTH = (
|
||
os.getenv("SURFACE_CONFIRM_DEFAULT_SMOOTH", "True").lower() == "true"
|
||
)
|
||
|
||
|
||
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-3-1. 배수유역 격자 해석 파라미터 (B05 WF2 — 2026-07-31 전면 재설계)
|
||
#
|
||
# 도엽 등고선 TIN 보간 → 웅덩이 채움 → D8 물 방향 → 도로에서 상류 BFS(포인터 더블링)
|
||
# 순서로 유역을 정한다. 능선을 따로 탐지하지 않는다 — 도로로 물이 도달하는지 여부가
|
||
# 유일한 판정 기준이며, 그 경계가 곧 능선이다.
|
||
# 라이다 DEM은 노선 주변만 커버해 유역 산정에 부족하므로 쓰지 않는다(사용자 지시).
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 해석 격자 한 변(m). 작을수록 정밀하나 셀 수가 제곱으로 늘어난다.
|
||
DRAINAGE_GRID_SIZE_M = float(os.getenv("DRAINAGE_GRID_SIZE_M", "1.0"))
|
||
# 1차 배수유역 반경(m). 도로 교차점 상류로 이어진 세류망과 계획 노선을 각각 이 반경으로
|
||
# 버퍼해 합친 범위가 1차 영역이며, 그 bbox가 해석 격자다.
|
||
# 노선 버퍼가 필요한 이유: 세류 교차가 없는 구간의 도로도 격자 안에 있어야 그 구간 사면이
|
||
# 유역으로 잡힌다. 세류망 선정이 정확해진 뒤 다시 포함했다(2026-07-31 사용자 지시).
|
||
DRAINAGE_INITIAL_RADIUS_M = float(os.getenv("DRAINAGE_INITIAL_RADIUS_M", "100.0"))
|
||
# 활성 셀이 격자 최외곽에 닿았을 때 한 번에 넓히는 폭(m).
|
||
DRAINAGE_EXPAND_STEP_M = float(os.getenv("DRAINAGE_EXPAND_STEP_M", "200.0"))
|
||
# 확장 반복 상한. 경계 링이 전부 비활성이 되면 그 전에 스스로 멈춘다(안전핀).
|
||
DRAINAGE_MAX_EXPAND_ROUNDS = int(os.getenv("DRAINAGE_MAX_EXPAND_ROUNDS", "6"))
|
||
# 최외곽 적색 셀 주변을 한 회차에 넓히는 폭(m). 좁을수록 유역 경계가 정밀하나 회차가 늘어난다.
|
||
DRAINAGE_RED_EXPAND_BAND_M = float(os.getenv("DRAINAGE_RED_EXPAND_BAND_M", "50.0"))
|
||
# 적색 확장 반복 상한. 새로 추가한 셀에 적색이 없으면 그 전에 스스로 멈춘다(안전핀).
|
||
DRAINAGE_RED_EXPAND_MAX_ROUNDS = int(os.getenv("DRAINAGE_RED_EXPAND_MAX_ROUNDS", "20"))
|
||
# 격자 셀 수 권장 상한. 넘으면 **경고만** 남기고 그대로 계산한다 — 격자 크기 자동 조절은
|
||
# 하지 않는다(2026-07-31 사용자 지시). 느리면 위 DRAINAGE_GRID_SIZE_M을 직접 올린다.
|
||
DRAINAGE_MAX_GRID_CELLS = int(os.getenv("DRAINAGE_MAX_GRID_CELLS", "16000000"))
|
||
# 해석용 **도로 굽기 폭**(m) — 설계 도로폭이 아니다.
|
||
#
|
||
# 설계 도로폭은 등급별로 따로 있다(B05 `/sections/road-widths`: 간선 3.0 / 지선 3.0 / 작업로 2.5m).
|
||
# 여기 값은 노선을 격자에 구울 때만 쓰는 해석 파라미터다. 1m 격자에서 도로를 1셀 선으로 구우면
|
||
# D8 대각 이동이 도로를 건너뛰어 상류 물이 도로를 지나쳐 버리므로, 3셀(=3m) 이상 두께가 필요하다.
|
||
# 그래서 설계폭과 연동하지 않고 4m로 둔다(2026-08-01 사용자 확인). 값을 바꾸면 재분석해야 한다.
|
||
DRAINAGE_ROAD_WIDTH_M = float(os.getenv("DRAINAGE_ROAD_WIDTH_M", "4.0"))
|
||
# 계획선 최저점보다 이만큼 아래인 등고선은 상류 기여가 불가능하므로 보간에서 제외한다.
|
||
DRAINAGE_CONTOUR_MARGIN_M = float(os.getenv("DRAINAGE_CONTOUR_MARGIN_M", "10.0"))
|
||
# 이보다 짧은 등고선 파편은 노이즈로 보고 버린다. 봉우리 폐합 등고선은 이 값 이상이면 남는다.
|
||
DRAINAGE_CONTOUR_MIN_LENGTH_M = float(os.getenv("DRAINAGE_CONTOUR_MIN_LENGTH_M", "20.0"))
|
||
# 등고선 정점 재샘플 간격(m). 조밀할수록 TIN이 정확하나 Delaunay 비용이 커진다.
|
||
DRAINAGE_CONTOUR_RESAMPLE_M = float(os.getenv("DRAINAGE_CONTOUR_RESAMPLE_M", "5.0"))
|
||
# TIN 삼각망을 만들 때 격자 범위 밖으로 남길 여유(m). 도엽 전체 등고선을 다 물면 삼각망
|
||
# 비용만 커지고 결과는 같다. 여유가 0이면 격자 가장자리가 TIN 밖으로 나가 NaN이 된다.
|
||
DRAINAGE_CONTOUR_CLIP_MARGIN_M = float(os.getenv("DRAINAGE_CONTOUR_CLIP_MARGIN_M", "100.0"))
|
||
# 평탄면 해소용 미세 경사(m/셀). 채움 후 흐름 방향이 없는 셀에 출구 쪽 경사를 만들어 준다.
|
||
DRAINAGE_FLAT_EPSILON_M = float(os.getenv("DRAINAGE_FLAT_EPSILON_M", "0.001"))
|
||
# 관 매설 최대 간격(m). 이 간격을 넘으면 흐름 강도가 가장 큰 지점에 관을 보충한다.
|
||
DRAINAGE_PIPE_MAX_SPACING_M = float(os.getenv("DRAINAGE_PIPE_MAX_SPACING_M", "100.0"))
|
||
# 관끼리 이보다 가까우면 같은 계곡으로 보고 하나로 합친다.
|
||
DRAINAGE_PIPE_MIN_SPACING_M = float(os.getenv("DRAINAGE_PIPE_MIN_SPACING_M", "5.0"))
|
||
# 측구 흐름(도로 셀 → 담당 관) 판정용 종단 계획선 샘플 간격(m).
|
||
DRAINAGE_DITCH_SAMPLE_M = float(os.getenv("DRAINAGE_DITCH_SAMPLE_M", "1.0"))
|
||
# 유역 폴리곤 단순화 허용오차(m). 격자 계단 경계를 매끄럽게 줄여 응답 크기를 낮춘다.
|
||
DRAINAGE_POLYGON_SIMPLIFY_M = float(os.getenv("DRAINAGE_POLYGON_SIMPLIFY_M", "2.0"))
|
||
# 이 면적(㎡) 미만의 유역 조각은 버린다(격자 노이즈 제거).
|
||
DRAINAGE_MIN_BASIN_AREA_M2 = float(os.getenv("DRAINAGE_MIN_BASIN_AREA_M2", "100.0"))
|
||
# 격자 해석 결과 캐시 파일명. 프로젝트 저장소의 B04_PreProcess/drainage/ 아래에 놓인다.
|
||
DRAINAGE_CACHE_DIRNAME = "drainage"
|
||
DRAINAGE_CACHE_FILENAME = "watershed_grid.npz"
|
||
# 분석 응답 자체를 그대로 담아 두는 파일. 재산정하지 않는 한 이걸 그대로 돌려준다 —
|
||
# 저장 배열에서 응답을 다시 조립하면 원본과 어긋날 여지가 생긴다(2026-07-31 사용자 지시).
|
||
DRAINAGE_RESPONSE_FILENAME = "00_watershed_response.json"
|
||
# 세부유역 산출물. 관 목록이 정해져야 나오므로 해석 산출물(01~03)과 번호를 이어 붙인다.
|
||
DRAINAGE_DETAIL_FILENAME = "04_detailed_basins.geojson"
|
||
|
||
# ── 관 매설 지점 편집분 ──
|
||
# 해석 산출물(01~03)은 다시 돌리면 덮어써도 되지만 사용자가 찍은 관은 그러면 안 된다.
|
||
# 같은 폴더 아래 편집분 전용 칸을 따로 두고 B04(관리자)·B05(사용자)가 같은 파일을 본다.
|
||
DRAINAGE_EDITS_DIRNAME = "edits"
|
||
DRAINAGE_PIPE_POINTS_FILENAME = "pipe_points.json"
|
||
|
||
# ── B05용 평균 흐름 화살표 ──
|
||
# 셀 화살표는 1m라 축소하면 경향이 안 보인다. 이 크기의 블록으로 묶어 방향을 평균한다.
|
||
DRAINAGE_ARROW_BLOCK_M = float(os.getenv("DRAINAGE_ARROW_BLOCK_M", "10.0"))
|
||
# 화살표끼리 최소 이 간격을 두고 솎아낸다. 촘촘하면 도면이 지저분해진다.
|
||
DRAINAGE_ARROW_SPACING_M = float(os.getenv("DRAINAGE_ARROW_SPACING_M", "40.0"))
|
||
# 블록 안에서 화살표를 낼 수 있는 셀이 이 비율 미만이면 건너뛴다(가장자리 조각 방지).
|
||
DRAINAGE_ARROW_MIN_COVERAGE = float(os.getenv("DRAINAGE_ARROW_MIN_COVERAGE", "0.5"))
|
||
# 방향 일치도 하한(원형 평균 결과 길이 0~1). 블록 안 방향이 제각각이면 평균이 무의미하므로 버린다.
|
||
DRAINAGE_ARROW_MIN_AGREEMENT = float(os.getenv("DRAINAGE_ARROW_MIN_AGREEMENT", "0.7"))
|
||
# 단계별 검증 산출물은 같은 폴더에 `{번호}_{단계}.geojson` + `manifest.json`으로 쌓인다.
|
||
# 파일명 규칙은 B05_Profile_Engine_Watershed_Export.STAGES가 유일한 정의처다.
|
||
|
||
# ── 확률강우량 (map.wamis.go.kr 등우선도) ──
|
||
# 전국 공통 등우선 GeoJSON 원본을 이 폴더에 1회 내려받아 영구 캐시한다(약 20~30MB).
|
||
# 근거·검증: docs/raw/guidelines/2026-08-05_임도_배수설계_규정_조사.md 5절.
|
||
WAMIS_CONTOUR_CACHE_DIR = PROJECT_ROOT / "resources" / "wamis_contours"
|
||
# 프로젝트별 내삽 결과 파일명. drainage/ 아래에 놓인다.
|
||
DRAINAGE_RAINFALL_FILENAME = "rainfall_table.json"
|
||
|
||
# ── 배수 유효직경 계산 계수 (2026-08-05 사용자 확정, 위 규정 조사 문서 6절) ──
|
||
# 합리식 유출계수 C. 산악지 준용 0.8 — 필요 시 사용자가 이 값을 고친다.
|
||
DRAINAGE_RUNOFF_COEFFICIENT = float(os.getenv("DRAINAGE_RUNOFF_COEFFICIENT", "0.8"))
|
||
# 설계유량 배수. 시행규칙 별표2 (가): 최대홍수유출량의 2.0배 이상.
|
||
DRAINAGE_DESIGN_FLOW_FACTOR = float(os.getenv("DRAINAGE_DESIGN_FLOW_FACTOR", "2.0"))
|
||
# 설계빈도(년). 별표2 (가) 1호: 100년빈도 확률강우량.
|
||
DRAINAGE_DESIGN_RETURN_PERIOD_YR = int(os.getenv("DRAINAGE_DESIGN_RETURN_PERIOD_YR", "100"))
|
||
# 홍수도달시간 하한(분). 국도건설공사 설계실무요령·도로배수지침 "강우지속기간 5분 원칙".
|
||
DRAINAGE_TC_MIN_MINUTES = float(os.getenv("DRAINAGE_TC_MIN_MINUTES", "5.0"))
|
||
# 관 경사(도). 시공 중 수시 변경되므로 지형 산출 대신 실무 대푯값 10도로 고정(사용자 제시).
|
||
DRAINAGE_PIPE_SLOPE_DEG = float(os.getenv("DRAINAGE_PIPE_SLOPE_DEG", "10.0"))
|
||
# Manning 조도계수(파형강관).
|
||
DRAINAGE_MANNING_N = float(os.getenv("DRAINAGE_MANNING_N", "0.024"))
|
||
# 관내 유속 허용범위(m/s). 도로배수지침 0.8~3.0 — 상한 클램프가 안전측(관이 커진다).
|
||
DRAINAGE_VELOCITY_MIN_MS = float(os.getenv("DRAINAGE_VELOCITY_MIN_MS", "0.8"))
|
||
DRAINAGE_VELOCITY_MAX_MS = float(os.getenv("DRAINAGE_VELOCITY_MAX_MS", "3.0"))
|
||
# 원형관 통수단면 비율. 도로설계요령: 관 단면의 70%만 통수 고려.
|
||
DRAINAGE_FLOW_AREA_RATIO = float(os.getenv("DRAINAGE_FLOW_AREA_RATIO", "0.7"))
|
||
# 세월교 검토 문턱(mm). 유효직경이 관 최대 규격(파형강관 D2000)을 넘으면 관이 아니라
|
||
# 세월교·물넘이·교량 대상이다 — 「임도설치 및 관리 등에 관한 규정」 제12조: 계류 횡단
|
||
# 구간은 배수구 막힘 우려가 없는 물넘이 포장(세월교) 또는 교량으로 설계(2026-08-05 조사).
|
||
DRAINAGE_BRIDGE_THRESHOLD_MM = float(os.getenv("DRAINAGE_BRIDGE_THRESHOLD_MM", "2000"))
|
||
|
||
# ── B05 구조물 옵션 (드롭다운 목록·기본값, 2026-08-05 사용자 확정) ──
|
||
# 프론트는 이 값을 /api 설정 응답 또는 빌드타임 복사로 받아 쓴다. 수정은 여기서만 한다.
|
||
STRUCTURE_TYPES = ("배관", "기성막이", "대피로", "기타")
|
||
STRUCTURE_DEFAULT_TYPE = "배관"
|
||
PIPE_DIAMETERS_MM = {
|
||
"이중벽관": (150, 200, 250, 300, 400, 450, 500, 600, 700, 800, 900, 1000, 1200, 1500),
|
||
"삼중벽관": (200, 250, 300, 400, 450, 500, 600, 700, 800, 900, 1000, 1200, 1500),
|
||
"파형강관": (
|
||
150,
|
||
200,
|
||
250,
|
||
300,
|
||
350,
|
||
400,
|
||
450,
|
||
500,
|
||
600,
|
||
700,
|
||
800,
|
||
900,
|
||
1000,
|
||
1200,
|
||
1350,
|
||
1500,
|
||
1650,
|
||
1800,
|
||
2000,
|
||
),
|
||
}
|
||
PIPE_DEFAULT_TYPE = "파형강관"
|
||
# 자동 지정 기본 관경. 별표2 (나) 예외 하한 800mm(사용자 결정 — 계산값이 더 크면 바로 위 규격).
|
||
PIPE_DEFAULT_DIAMETER_MM = int(os.getenv("PIPE_DEFAULT_DIAMETER_MM", "800"))
|
||
ESCAPE_ROUTE_WIDTHS_M = (1.5, 2.0, 2.5, 3.0)
|
||
ESCAPE_ROUTE_DEFAULT_WIDTH_M = 2.0
|
||
STRUCTURE_ETC_DEFAULT_NAME = "기타 구조물"
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 5-4. 종횡단 생성 파라미터 (B06 WF3)
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
SECTION_STATION_INTERVAL_M = float(os.getenv("SECTION_STATION_INTERVAL_M", "20.0"))
|
||
# 초기 파이프라인(파일입력→B06) 샘플 반폭 — 표시 반폭보다 넉넉히 뽑아 두면 사용자가
|
||
# 표시 반폭을 이 안에서 바꿀 때 재계산이 필요 없다(2026-08-06 사용자 확정, 기준 20m).
|
||
# 표시 반폭이 이 값을 넘을 때만 B05부터 재생성한다. 향후 사용자 평균 설정 보고 조정.
|
||
SECTION_CROSS_HALF_WIDTH_M = float(os.getenv("SECTION_CROSS_HALF_WIDTH_M", "20.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_VERTICAL_EXAGGERATION = float(os.getenv("SECTION_VERTICAL_EXAGGERATION", "1.0"))
|
||
SECTION_INCLUDE_ENDPOINT = os.getenv("SECTION_INCLUDE_ENDPOINT", "True").lower() == "true"
|
||
FOREST_ROAD_MIN_WIDTH_M = {"trunk": 3.0, "branch": 3.0, "work": 2.5}
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 5-4-1. 표준 횡단면 단일 진실 원천 (도면 판독값, 2026-07-24 사용자 확정)
|
||
#
|
||
# 출처: `08 표준횡단도면(울진 울진 대흥 산65 외2(3공구)).bmp` 좌측 영역 판독값.
|
||
# 지반그룹 3종: soil(토사) / rock(암, 리핑·발파 공유) / paved(포장).
|
||
# 단위: 길이 m, 경사비 수평:수직=ratio:1, 횡단경사 %.
|
||
#
|
||
# 이 상수가 **표준 횡단면 기하의 유일한 정의처**다. B06 설정 패널 기본값과
|
||
# 절·성토 단면적 엔진(B06_Section_Engine_Design)이 모두 여기를 읽으며,
|
||
# 아래 5-4-2의 파생 상수 외에 같은 값을 별도로 정의하지 않는다.
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
STANDARD_CROSS_SECTION = {
|
||
"soil": {
|
||
"road_width_m": 3.0, # 노폭(차도)
|
||
"shoulder_left_m": 0.5, # 노견(좌)
|
||
"shoulder_right_m": 0.5, # 노견(우)
|
||
# 측구(상단폭/저폭/깊이). 토사 = 900/300/300mm.
|
||
"ditch": {"top_width_m": 0.9, "bottom_width_m": 0.3, "depth_m": 0.3},
|
||
"cross_slope_pct": {"min": 3.0, "max": 5.0}, # 횡단경사(측구방향)
|
||
"fill_slope_ratio": 1.2, # 성토 1:1.2
|
||
"cut_slope_ratio": 1.0, # 절토 1:1.0
|
||
},
|
||
"rock": { # 리핑암·발파암 공유
|
||
"road_width_m": 3.0,
|
||
"shoulder_left_m": 0.5,
|
||
"shoulder_right_m": 0.5,
|
||
# 일반 측구 = 690/300/300mm.
|
||
"ditch": {"top_width_m": 0.69, "bottom_width_m": 0.3, "depth_m": 0.3},
|
||
# L형 측구 = 폭500 × 깊이100mm (횡단면도에서 일반/L형 중 선택).
|
||
"ditch_l_type": {"width_m": 0.5, "depth_m": 0.1},
|
||
"cross_slope_pct": {"min": 3.0, "max": 3.0},
|
||
"fill_slope_ratio": 1.2,
|
||
"cut_slope_ratio": 0.4, # 절토(암) 1:0.4 (규정 1:0.3 이상)
|
||
},
|
||
"paved": { # 포장: 토사와 동일 기하 + 횡단경사만 다름
|
||
"road_width_m": 3.0,
|
||
"shoulder_left_m": 0.5,
|
||
"shoulder_right_m": 0.5,
|
||
"ditch": {"top_width_m": 0.9, "bottom_width_m": 0.3, "depth_m": 0.3},
|
||
"cross_slope_pct": {"min": 1.5, "max": 2.0},
|
||
"fill_slope_ratio": 1.2,
|
||
"cut_slope_ratio": 1.0,
|
||
# 포장층 두께(도면 미표기 — 임도 콘크리트 포장 실무 표준 0.2m, 패널에서 편집 가능).
|
||
"pavement_thickness_m": 0.2,
|
||
},
|
||
}
|
||
# 암 경계선(지면선 복사 — 계획선 아님) 기본 오프셋과 상/하 제어 스텝(m).
|
||
STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M = -0.5
|
||
STANDARD_ROCK_BOUNDARY_STEP_M = 0.1
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 5-4-2. 횡단 표준단면 엔진 파생 상수 (B06 WF3)
|
||
#
|
||
# 전부 위 5-4-1 STANDARD_CROSS_SECTION에서 파생한다(중복 정의 금지).
|
||
# 지반유형 라벨은 3종(토사/리핑암/발파암)이나, 표준단면 기하는 토사/암반 2종
|
||
# 프리셋만 존재한다(리핑암·발파암은 절토경사·측구 동일, 단가만 B07/B09에서 구분).
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
_SCS_SOIL = STANDARD_CROSS_SECTION["soil"]
|
||
# 노면폭(노견 포함) = 0.5 + 3.0 + 0.5 = 4.0m. env로만 개별 재정의 가능.
|
||
SECTION_ROADBED_WIDTH_M = float(
|
||
os.getenv(
|
||
"SECTION_ROADBED_WIDTH_M",
|
||
str(
|
||
_SCS_SOIL["road_width_m"] + _SCS_SOIL["shoulder_left_m"] + _SCS_SOIL["shoulder_right_m"]
|
||
),
|
||
)
|
||
)
|
||
SECTION_CARRIAGEWAY_WIDTH_M = float(
|
||
os.getenv("SECTION_CARRIAGEWAY_WIDTH_M", str(_SCS_SOIL["road_width_m"]))
|
||
)
|
||
# 성토 경사(수평:수직 = ratio:1). 지반유형 무관 고정.
|
||
SECTION_FILL_SLOPE_RATIO = float(
|
||
os.getenv("SECTION_FILL_SLOPE_RATIO", str(_SCS_SOIL["fill_slope_ratio"]))
|
||
)
|
||
# 지반유형(저장 라벨) → 기하 프리셋 키. 견적 단가 구분은 라벨 자체로 유지한다.
|
||
SECTION_GROUND_TYPE_PRESET = {
|
||
"soil": "soil",
|
||
"ripping_rock": "rock",
|
||
"blasting_rock": "rock",
|
||
}
|
||
# 단면유형: 좌절/우절(편절편성), 양절, 양성. 좌=양(+)offset, 우=음(-)offset.
|
||
SECTION_MODES = ("left_cut", "right_cut", "both_cut", "both_fill")
|
||
SECTION_DITCH_SIDES = ("left", "right")
|
||
# 측구 형식: 일반(사다리꼴) / L형(암 구간 전용 선택지).
|
||
SECTION_DITCH_TYPES = ("standard", "l_type")
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 5-4-3. 토량환산계수 (B06 유토곡선)
|
||
#
|
||
# loose(L) = 흐트러진 상태 토량 / 자연상태 토량 (팽창률)
|
||
# compacted(C) = 다져진 상태 토량 / 자연상태 토량 (다짐률)
|
||
#
|
||
# 유토곡선은 운반계획 도면이므로 **다짐상태 기준**으로 계산한다.
|
||
# "운반거리의 산정 시에 모든 수량은 다짐상태로 환산하여 계산하고,
|
||
# 내역서에 적용하는 수량은 자연상태로 한다."
|
||
# (2021년도 국도건설공사 설계실무 요령 / 2016 건설공사 표준품셈 계열)
|
||
#
|
||
# 아래 값은 표준품셈 토량환산계수표의 암종별 범위를 B06 지반유형 3종에 대응시킨
|
||
# 제안값이며 임도 전용 고시 수치가 아니다.
|
||
# 토사 ← 풍화토(L 1.10~1.25 / C 0.80~0.90) ~ 점토(L 1.20~1.35 / C 0.75~0.90)
|
||
# 리핑암 ← 풍화암(L 1.30~1.35 / C 1.00~1.15) ~ 연암(L 1.30~1.50 / C 1.00~1.30) 하단
|
||
# 발파암 ← 보통암(L 1.55~1.70 / C 1.20~1.40) 중앙
|
||
# 표준품셈도 "토질 시험하여 적용하는 것을 원칙으로 하되 소량인 경우 환산계수표에
|
||
# 따를 수 있다"고 하므로 현장별 조정이 전제다. 값을 바꾸려면 여기만 고치면 된다.
|
||
#
|
||
# 이 상수가 **토량환산계수의 유일한 정의처**다(타 파일 중복 정의 금지).
|
||
# 분석 근거: docs/raw/2026-08-02_유토곡선_3공구_분석.md 8절
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
EARTHWORK_CONVERSION_FACTORS = {
|
||
"soil": {"loose": 1.25, "compacted": 0.90},
|
||
"ripping_rock": {"loose": 1.35, "compacted": 1.15},
|
||
"blasting_rock": {"loose": 1.60, "compacted": 1.30},
|
||
}
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 5-4-5. 토공 운반장비 선정 거리 경계 (B06 유토곡선 운반계획)
|
||
#
|
||
# 평균운반거리(m)로 운반장비를 고른다. 위에서부터 순서대로 검사해 `max_distance_m`
|
||
# **이하**면 그 장비를 쓰고, 마지막 항목(None)이 나머지를 전부 받는다.
|
||
#
|
||
# 종무대(무대운반) ≤ 20m < 도쟈(불도저 압토) ≤ 70m < 덤프트럭
|
||
#
|
||
# 2026-08-02 사용자 확정값(도쟈 50 → 70m). 유토곡선의 장비 경계현은 이 값을
|
||
# **수평 현의 길이**로 읽는다 — 현 길이가 20m가 되는 높이 위쪽이 종무대 몫,
|
||
# 70m가 되는 높이까지가 도쟈 몫, 그 아래가 덤프 몫이다.
|
||
# 현장·발주처 기준에 따라 달라질 수 있으므로
|
||
# **이 상수가 거리 경계의 유일한 정의처**이며 여기만 고치면 된다.
|
||
#
|
||
# key는 도면 표기와 다음과 같이 대응한다: free_haul=종무대, dozer=도쟈, dump_truck=덤프.
|
||
# 화면 표기 문구는 하드코딩 금지 규칙에 따라 ui_locales에서 따로 관리한다.
|
||
# 분석 근거: docs/raw/2026-08-02_유토곡선_3공구_분석.md
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
EARTHWORK_HAUL_EQUIPMENT_LIMITS_M = (
|
||
("free_haul", 20.0),
|
||
("dozer", 70.0),
|
||
("dump_truck", None),
|
||
)
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 5-4-2. 자연방토 판정 경사 (B06 유토곡선)
|
||
#
|
||
# 자연방토 = 남는 흙을 성토사면 아래로 흘려보내 **운반비를 세지 않는** 처리.
|
||
# 판정 기준은 **성토측 자연 지반의 경사**다(2026-08-02 사용자 확정). 지반이 이보다 가파르면
|
||
# 부어 놓은 흙이 쌓이지 않고 스스로 흘러내리므로 따로 운반하지 않는다.
|
||
#
|
||
# 값은 rise/run(무차원). 1/1.5 = 0.667 ≈ 33.7°로, 토사 안식각 상한이자 표준 성토 경사 1:1.5와
|
||
# 같은 자리다. 현장·발주처 기준에 따라 달라질 수 있으므로 **이 상수가 유일한 정의처**다.
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
NATURAL_SPOIL_MIN_GROUND_SLOPE = 1.0 / 1.5
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 5-5. 종단 계획선(계획고) 설계 기준 (B05 WF2)
|
||
#
|
||
# 출처: 「임도설치 및 관리 등에 관한 규정」[별표 1-2] 임도의 설계 및 시설기준.
|
||
# 기준은 임도등급이 아니라 `설계속도 × 지형구분(일반/특수)`으로 규정되어 있어
|
||
# 등급은 grade_to_design_speed로 간접 매핑한다.
|
||
# 위 5-3의 경로탐색(평면) 기준(FOREST_ROAD_MAX_GRADE 등)과는 별개의 값이므로
|
||
# 서로 혼용하지 않는다.
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
FOREST_ROAD_PROFILE_CRITERIA = {
|
||
# 설계속도(km/h)별 법정 기준
|
||
"design_speed": {
|
||
40: {
|
||
"max_grade_pct": {"normal": 7.0, "special": 10.0},
|
||
"max_reverse_grade_pct": 5.0,
|
||
"min_vertical_radius_m": 450.0,
|
||
"min_curve_length_m": 40.0,
|
||
},
|
||
30: {
|
||
"max_grade_pct": {"normal": 8.0, "special": 12.0},
|
||
"max_reverse_grade_pct": 5.0,
|
||
"min_vertical_radius_m": 250.0,
|
||
"min_curve_length_m": 30.0,
|
||
},
|
||
20: {
|
||
"max_grade_pct": {"normal": 9.0, "special": 14.0},
|
||
"max_reverse_grade_pct": 5.0,
|
||
"min_vertical_radius_m": 100.0,
|
||
"min_curve_length_m": 20.0,
|
||
},
|
||
},
|
||
# 임도등급 → 기본 설계속도. 작업임도는 별표에 종단 기준이 없어 20km/h를 준용한다.
|
||
"grade_to_design_speed": {"trunk": 40, "branch": 30, "work": 20},
|
||
# 특수지형에서 기준 적용이 어려운 경우 노면포장 시에 한하여 허용되는 상한
|
||
"paved_exception_grade_pct": 18.0,
|
||
# 비포장 도로이면서 종단기울기 대수차가 이 값 이하이면 종단곡선을 두지 않는다
|
||
"vertical_curve_skip_delta_pct": 5.0,
|
||
# 곡선 중첩 방지용 최소 직선(tangent) 길이 (규정 외 실무 기본값)
|
||
"min_tangent_length_m": 20.0,
|
||
# 절·성토 균형 구역 기본 길이 (None이면 노선 전체를 1구역으로 본다)
|
||
"balance_segment_length_m": None,
|
||
# 주 진행방향 자동 판정 기준: |시종점 고도차| / (최고−최저) 가 이 값 이상이면
|
||
# 한 방향으로 오르내리는 노선으로 보고 반대 방향에 역기울기 상한을 적용한다.
|
||
# V자(계곡 횡단)·Λ자(능선 통과) 노선은 이 비율이 낮아 역기울기 판정을 하지 않는다.
|
||
"main_direction_monotone_ratio": 0.5,
|
||
}
|
||
GRADE_TERRAIN_TYPES = ("normal", "special")
|
||
# 주 진행방향: auto(자동 판정) / ascending(상행) / descending(하행) / none(역기울기 미적용)
|
||
GRADE_MAIN_DIRECTIONS = ("auto", "ascending", "descending", "none")
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 5-6. 종단 계획선 선형(직선 + 측점 위 종단곡선) 및 편집 정책 (B05 WF2)
|
||
#
|
||
# 위 5-5의 FOREST_ROAD_PROFILE_CRITERIA는 법정 기준값이고, 여기는 계획선을
|
||
# "지반 추종 직선 분할 + 측점 위 종단곡선"으로 만들고 사용자가 0.1m 단위로
|
||
# 편집하는 동작을 제어하는 실무 정책값이다. 서로 혼용하지 않는다.
|
||
#
|
||
# 변화점(PVI)은 반드시 기준 측점 위에만 놓이며, 종단곡선은 그 측점을 중심으로
|
||
# 대칭 배치되어 곡선 좌우에 직선 구간이 반드시 남는다.
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
FOREST_ROAD_PROFILE_ALIGNMENT = {
|
||
# 절·성토 균형 허용 오차: |절토 − 성토| / max(절토, 성토) (%)
|
||
# 5% 미만이면 지반 추종이 왜곡되어 불필요한 변화점이 늘고, 15%를 넘으면
|
||
# 사토·객토 운반 물량 부담이 커진다. 10%를 실무 균형점으로 둔다.
|
||
"balance_tolerance_percent": 10.0,
|
||
# 종단곡선 기본 **반경** R = 측점간격 × 이 비율 (변화점 대칭 배치).
|
||
# R을 1차 값으로 두어야 계획고를 편집해도 R이 흔들리지 않는다(곡선길이 L이 대신 변한다).
|
||
# L = R × |기울기 대수차 A(비율)|
|
||
# 주의: 측점간격 20m 기준 R=8m이면 A=5%일 때 L=0.4m, A=15%일 때 L=1.2m로
|
||
# 도면상 곡선이 거의 드러나지 않는다. 곡선을 뚜렷하게 보려면 이 비율을 크게 올린다
|
||
# (예: 20.0 → R=400m, A=10%일 때 L=40m).
|
||
"curve_radius_ratio": 0.40,
|
||
# **배관 정착 계획선(1차 로직)의 기본 종단곡선 길이 L(m).**
|
||
# 이 선형은 변화점 사이 대수차 A가 작아(1~3%가 흔하다) 반경 R을 기준으로 잡으면
|
||
# L = R × A 가 변화점마다 3~50m로 널뛰고, 작은 쪽은 도면에서 직선과 구분되지 않는다
|
||
# (2026-08-03 사용자 지적). 그래서 **L을 기준값으로 고정**하고 R = L / A 로 역산한다.
|
||
# 결과적으로 호 길이가 변화점마다 같아 도면이 고르게 읽힌다.
|
||
# 인접 직선이 짧아 `curve_tangent_max_ratio`에 걸리면 자동으로 줄고 경고가 남는다.
|
||
# 호를 더/덜 길게 보이려면 여기만 고친다.
|
||
"pipe_anchor_curve_length_m": 15.0,
|
||
# 인접 직선 길이 대비 곡선 반쪽이 점유할 수 있는 최대 비율.
|
||
# 0.45면 짧은 쪽 직선의 55%가 항상 직선으로 남아 좌우 곡선이 겹치지 않는다.
|
||
"curve_tangent_max_ratio": 0.45,
|
||
# 법정 예외(비포장 & 대수차 5% 이하)를 실제 기하에서도 곡선 생략으로 적용할지.
|
||
# 규정 다-(3)-(다)는 "두지 않을 수 있다"는 허용 조항이며, 임도 실무 도면은 대수차가
|
||
# 작아도 변화점을 원곡선으로 처리한다. 기본값 False = 곡선을 항상 삽입하고
|
||
# 해당 구간에는 "생략 가능" 표시만 남긴다(True로 바꾸면 곡선을 실제로 뺀다).
|
||
"curve_skip_legal_exception": False,
|
||
# 변화점(PVI) 추가 페널티 (직선 분할 DP 목적함수, 단위 m²).
|
||
# 변화점 하나를 늘리려면 잔차제곱합이 이 값 이상 개선되어야 채택된다.
|
||
"pvi_penalty_m2": 25.0,
|
||
# 직선 분할 시 한 구간이 가질 수 있는 최소 측점 개수 (짧은 토막 방지)
|
||
"min_segment_stations": 2,
|
||
# 사용자 편집 스텝(m). 그래프 상·하단 버튼 1클릭당 계획고 이동량.
|
||
"edit_step_m": 0.1,
|
||
# 법정 종단기울기 상한 초과 시 동작: "warn"(경고만) | "block"(편집 차단)
|
||
"grade_violation_policy": "warn",
|
||
}
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 6. 저장소 경로
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
STORAGE_BASE_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "storage")
|
||
|
||
# 1:5,000 수치지형도 도엽 저장 폴더명 — 프로젝트 영구저장소 하위
|
||
# (storage/{회사}/{사용자}/{프로젝트ID}/B04_PreProcess/processed/map_sheets/)
|
||
MAP_SHEETS_DIRNAME = "map_sheets"
|
||
MAP_SHEETS_INDEX_FILENAME = "map_sheets_index.json"
|
||
|
||
# 배경 지도(위성·하이브리드·백지도) 확보 범위 = 계획노선이 걸치는 **기준 도엽**의 도곽.
|
||
# 1:5,000 도엽 1매는 약 2.2km × 2.3km다. 주변 도엽은 받지 않는다(2026-08-01 사용자 지시).
|
||
#
|
||
# 도엽 1매를 zoom 18로 받으면 한 변이 19타일(4,864px)이라 파일이 지나치게 커진다.
|
||
# 아래 한도 안에 들어오는 가장 선명한 zoom을 자동으로 고른다(브이월드 타일 눈금 그대로).
|
||
SURFACE_MAP_MAX_TILES_PER_SIDE = 16
|
||
SURFACE_MAP_MAX_ZOOM = 18
|
||
SURFACE_MAP_MIN_ZOOM = 14
|
||
|
||
# 브이월드 지도서비스 도엽 자동 다운로드 — 세션 만료 시 id/pw로 자동 재로그인 (.env)
|
||
VWORLD_LOGIN_ID = os.getenv("VWORLD_LOGIN_ID", "")
|
||
VWORLD_LOGIN_PW = os.getenv("VWORLD_LOGIN_PW", "")
|
||
# 선택: 수동 발급 세션 쿠키 (예: "JSESSIONID=..; SSCSID=.."). 비어 있으면 자동 로그인 사용.
|
||
VWORLD_SESSION_COOKIE = os.getenv("VWORLD_SESSION_COOKIE", "")
|
||
|
||
# 시스템 리소스 로그 (루트 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_PreProcess",
|
||
"B05_Profile",
|
||
"B06_Section",
|
||
"B08_DesignDetail",
|
||
"B07_Quantity",
|
||
"B09_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")
|
||
DEVICE_TOKEN_COOKIE_NAME = os.getenv("DEVICE_TOKEN_COOKIE_NAME", "device_token")
|
||
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", "")
|
||
APP_PUBLIC_BASE_URL = os.getenv(
|
||
"APP_PUBLIC_BASE_URL",
|
||
os.getenv("AISLO_BASE_URL", "http://localhost:8000"),
|
||
).rstrip("/")
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 8. CORS (프론트엔드 도메인)
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
CORS_ORIGINS = os.getenv("CORS_ORIGINS", "http://localhost:5173,http://localhost:8000").split(",")
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 9. 환경 정보
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
ENVIRONMENT = os.getenv("ENVIRONMENT", "development") # development, staging, production
|