# DB 구조 설계 제안서 **작성일:** 2026-07-05 **대상:** 임도 설계 및 견적 자동화 웹앱 (프로젝트 기반 멀티테넌트) **기술 스택:** PostgreSQL v17 + PostGIS v3.4 / asyncpg / FastAPI --- ## 1. 설계 원칙 ### 1.1 멀티테넌트 아키텍처 - **프로젝트 단위 데이터 격리:** 각 사용자의 프로젝트는 별도의 논리적 네임스페이스로 관리 - **하이브리드 저장소:** - **DB:** 사용자, 프로젝트 메타데이터, 설계 결과(경로, 단면, 수량) 저장 - **파일시스템:** 원본 입력파일(LAS/TIF/DXF), 중간 산출물(mesh/타일), 최종 산출물(DXF/Excel/PDF) 저장 - **스토리지 경로:** `storage/{회사명}/{사용자명}/{프로젝트ID}/` 하위로 자동 생성 ### 1.2 데이터 계층 분리 ``` [입력] LAS, TFW, PRJ, TIF → [분석] DEM/Mesh/필터 → [설계] 경로, 단면 → [산출] DXF, Excel (storage) (DB + storage) (DB + storage) (storage) ``` ### 1.3 공간 데이터 전용 설계 - PostGIS 기하학적 타입 활용 (geometry, geography) - 좌표계 일관성: EPSG 코드 저장 후 필요 시 변환 - 벡터 데이터(경로, 경계선) ↔ 래스터 샘플링 간 추적 가능하도록 설계 --- ## 2. 핵심 Entity 및 관계 ### 2.1 사용자 및 인증 (users, user_sessions) ``` users ├── id (PK) ├── email (UNIQUE) ├── password_hash ├── name ├── company (FK → companies) ├── role (admin, user, guest) ├── created_at, updated_at companies ├── id (PK) ├── name (UNIQUE) ├── created_by (FK → users) ├── members [] (users, M:N via user_company_roles) ├── created_at, updated_at user_sessions (토큰 저장 — optional, Redis 권장) ├── id (PK) ├── user_id (FK → users) ├── token ├── expires_at ``` **설계 의도:** - 회사 단위로 프로젝트, 사용자를 그룹화 - 회사별 권한 분리 (협업 확장성) - 향후 팀 공유 프로젝트 기능 추가 가능 ### 2.2 프로젝트 메타데이터 (projects, project_versions) ``` projects ├── id (PK, UUID) ├── user_id (FK → users) — 소유자 ├── company_id (FK → companies) ├── name ├── region (지역명: 예 "울진군 금강송면") ├── road_type (간선임도, 지선임도, 산불진화임도, 계류보전) ├── project_year (사업 연도, INT) ├── estimated_length_m (추정 연장) ├── memo ├── status (NEW, ANALYZING, WF1_COMPLETE, WF2_COMPLETE, ... , CONFIRMED, DONE) ├── crs_epsg (좌표계, INT — 예: 5178) ├── bbox (GEOMETRY(Polygon)) — 전체 프로젝트 범위 ├── created_at, updated_at ├── deleted_at (soft delete) project_versions (버전 관리 — 선택) ├── id (PK) ├── project_id (FK → projects) ├── version_num ├── snapshot_at (스냅샷 시점) ├── status (저장된 상태) └── data (JSONB — 설계 데이터 스냅샷) ``` **설계 의도:** - 프로젝트 생명주기 추적 (NEW → WF1 → WF2 → ... → CONFIRMED) - 좌표계 메타데이터 저장 (변환 오류 방지) - 버전 관리는 (선택) — 회원가입 초기엔 미필수, 협업 필요 시 추가 ### 2.3 입력 파일 관리 (input_files) ``` input_files ├── id (PK) ├── project_id (FK → projects) ├── file_type (las, tif, tfw, prj, dxf, dwg, other) ├── original_filename ├── stored_path (storage/{...}/raw/{file_type}/{filename}) ├── file_size_mb ├── upload_by (FK → users) ├── upload_at ├── crs_epsg (파일이 가진 좌표계) ├── metadata (JSONB — 해상도, 데이터 범위, 포인트 수 등) └── status (UPLOADED, PROCESSED, ARCHIVED) point_cloud_metadata ├── id (PK) ├── input_file_id (FK → input_files, LAS만) ├── point_count (INT) ├── min_z, max_z, mean_z (높이) ├── x_min, x_max, y_min, y_max (공간 범위) ├── densitiy_per_sqm (밀도) └── classification_summary (JSONB — {ground: N, vegetation: N, building: N, ...}) ``` **설계 의도:** - 입력 파일의 생명주기 추적 - 포인트클라우드 메타데이터로 전처리 여부 판단 - storage 폴더와 DB 간 일관성 확보 ### 2.4 지표면 모델 및 분석 결과 (surface_models, terrain_layers) ``` surface_models (WF1 출력) ├── id (PK) ├── project_id (FK → projects) ├── model_type (dem_grid, tin, mesh_triangulated, contour_lines) ├── source_file_id (FK → input_files, LAS) ├── status (PROCESSING, COMPLETE, FAILED) ├── crs_epsg ├── resolution_m (래스터 경우, NULL if 벡터) ├── stored_path (storage/{...}/processed/surface/) ├── bounds (GEOMETRY(Polygon)) — 모델 범위 ├── metadata (JSONB — 필터링 파라미터, 생성 시각 등) ├── created_at, completed_at terrain_layers ├── id (PK) ├── surface_model_id (FK → surface_models) ├── layer_name (지표, 제1층, 제2층 등 15종) ├── geometry_type (POINTCLOUD, GRID, MESH, CONTOUR) ├── stored_path (GeoJSON / GeoTIFF / LAS 등) └── statistics (JSONB — min_z, max_z, mean_slope, etc.) ``` **설계 의도:** - WF1 분석 결과(DEM, TIN, mesh 등)의 출처와 메타데이터 추적 - 층(layer)별로 렌더링 최적화 가능 - 재분석 시 기존 결과와 비교 가능 ### 2.5 경로 설계 (routes, route_points, route_statistics) ``` routes (WF2 출력) ├── id (PK) ├── project_id (FK → projects) ├── status (DRAFT, CONFIRMED, ARCHIVED) ├── start_point (GEOMETRY(Point)) — 지형 위 실제 좌표 ├── end_point (GEOMETRY(Point)) ├── start_chainage_m, end_chainage_m (측점) ├── total_length_m ├── grade_percent[] (JSONB array — 각 구간 종단 경사도) ├── constraints (JSONB — max_grade, min_radius, avoidance_zones) ├── algorithm_params (JSONB — 비용함수 가중치, 계산 시간 등) ├── computed_at └── geometry (GEOMETRY(LineString, Z) — 3D 경로) route_points (경로 포인트 샘플, 웹 렌더링용) ├── id (PK) ├── route_id (FK → routes) ├── chainage_m (측점) ├── geometry (GEOMETRY(Point, Z)) ├── elevation_m, slope_percent └── sequence_num route_statistics ├── id (PK) ├── route_id (FK → routes) ├── min_slope, max_slope, mean_slope ├── cut_volume_m3, fill_volume_m3 ├── tree_cutting_volume (목재 추정) └── cost_score (알고리즘 점수) ``` **설계 의도:** - 경로의 기하학적 정보(3D LineString) + 측점 기반 추적 - 재계산 시 이전 결과와 버전 비교 가능 - 사용자가 수정한 경로도 저장 가능 (draft ↔ confirmed) ### 2.6 종단면 및 횡단면 (longitudinal_sections, cross_sections) ``` longitudinal_sections ├── id (PK) ├── project_id (FK → projects) ├── route_id (FK → routes) ├── computed_at ├── geometry (GEOMETRY(LineString, Z)) — 경로 따라가며 샘플링한 표고 ├── data (JSONB) │ ├── chainages [0, 20, 40, ...] │ ├── elevations [100.5, 102.3, ...] │ ├── grades [2.5, 1.8, ...] │ └── design_elevations [100.0, 102.0, ...] (설계 기준) └── stored_path (storage/{...}/sections/longitudinal.json) cross_sections ├── id (PK) ├── project_id (FK → projects) ├── route_id (FK → routes) ├── chainage_m (측점) ├── sequence_num (1st, 2nd, ...) ├── geometry (GEOMETRY(LineString, Z)) — 지형 횡단면 ├── geometry_design (GEOMETRY(LineString, Z)) — 설계 횡단면 ├── data (JSONB) │ ├── left_slope, right_slope │ ├── width_m │ ├── cut_volume_m3, fill_volume_m3 │ ├── structures [] (낙석방지책, 돌붙임 등) │ └── notes └── stored_path (storage/{...}/sections/cross_{chainage}.json) ``` **설계 의도:** - 종단면: 경로 따라 세로 방향 표고 추적 - 횡단면: 20m 간격 가로 방향 단면 + 설계 기준 - JSONB로 유연한 추가 메타데이터 저장 ### 2.7 설계 및 구조물 (design_details, structures, quantity_items) ``` structures (WF4 구조물 라이브러리) ├── id (PK) ├── project_id (FK → projects) ├── cross_section_id (FK → cross_sections) ├── structure_type (낙석방지책, 돌붙임, 계간수로, 낙차공, etc.) ├── chainage_m, location (LEFT, CENTER, RIGHT) ├── length_m, width_m, height_m (기본 치수) ├── material (강재, 콘크리트, 목재, 돌 등) ├── quantity (개수) ├── unit_price (단가) ├── geometry (GEOMETRY(Polygon)) — 3D 구조물 배치 ├── design_notes (JSONB) └── last_modified_by (FK → users) quantity_items (WF5 수량 산출) ├── id (PK) ├── project_id (FK → projects) ├── category (토공, 구조물, 포장, 배수, 녹화, 안전시설) ├── item_name (예: "절토 일반", "낙석방지책 설치" 등) ├── unit (m3, 개, m, m2) ├── quantity_design (설계 수량) ├── quantity_actual (실제 수량, 사용자 수정 가능) ├── unit_price (단가) ├── total_price (수량 × 단가) ├── standard_reference (설계 기준 참고 자료) ├── computed_at └── data (JSONB — 계산 과정 메모) ``` **설계 의도:** - 구조물은 횡단면별로 배치 - 수량 항목은 자동 계산 + 사용자 수정 가능 - 단가와 기준 추적으로 감시/감독 기록 남김 ### 2.8 산출물 관리 (outputs, output_files) ``` outputs ├── id (PK) ├── project_id (FK → projects) ├── output_type (estimation_excel, drawing_dxf, report_pdf, all_bundle) ├── status (GENERATING, COMPLETE, FAILED) ├── generated_by (FK → users) ├── generated_at ├── version (프로젝트 확정 단계에서 자동 증가) ├── metadata (JSONB) │ ├── template_used │ ├── company_name, project_name │ ├── total_cost │ └── generation_time_sec └── created_at output_files ├── id (PK) ├── output_id (FK → outputs) ├── file_type (xlsx, pdf, dxf, dwg, json, zip) ├── original_filename ├── stored_path (storage/{...}/outputs/{output_type}/{filename}) ├── file_size_mb ├── created_at └── download_count (통계) ``` **설계 의도:** - 산출물 생성 이력 추적 - 같은 프로젝트 다중 버전 관리 (재산출 가능) - 다운로드 통계로 사용 현황 파악 ### 2.9 변경 이력 및 감시 (audit_logs, change_logs) ``` audit_logs (접근 제어 감시) ├── id (PK) ├── user_id (FK → users) ├── action (CREATE, READ, UPDATE, DELETE, EXPORT) ├── entity_type (projects, routes, structures, etc.) ├── entity_id ├── timestamp ├── ip_address └── details (JSONB) change_logs (설계 변경 기록) ├── id (PK) ├── project_id (FK → projects) ├── changed_by (FK → users) ├── changed_at ├── entity_type (routes, cross_sections, structures) ├── entity_id ├── old_value (JSONB) ├── new_value (JSONB) └── reason (사용자 입력 선택) ``` **설계 의도:** - 보안 감시 (audit_logs) - 설계 변경 추적 및 롤백 가능성 (change_logs) --- ## 3. 파일시스템 디렉토리 구조 ``` storage/ ├── {company_slug}/ │ ├── {user_slug}/ │ │ └── {project_uuid}/ │ │ ├── raw/ # 입력 원본 │ │ │ ├── las/ │ │ │ ├── tif/ │ │ │ ├── prj/ │ │ │ ├── tfw/ │ │ │ └── dwg_dxf/ │ │ ├── processed/ # 중간 산출물 (DB에 메타데이터만 저장) │ │ │ ├── surface/ │ │ │ │ ├── dem_grid_2m.tif │ │ │ │ ├── tin_mesh.obj / .gltf │ │ │ │ └── contours.geojson │ │ │ ├── points/ │ │ │ │ ├── ground_filtered.las │ │ │ │ └── ground_sampled_10m.ply │ │ │ └── tiles/ │ │ │ └── (3D Tiles / MVT 렌더링 최적화) │ │ ├── sections/ # 단면 데이터 (JSON) │ │ │ ├── longitudinal.json │ │ │ ├── cross_0000m.json │ │ │ ├── cross_0020m.json │ │ │ └── ... │ │ └── outputs/ # 최종 산출물 │ │ ├── estimation/ │ │ │ ├── v1_2025-07-05.xlsx │ │ │ ├── v2_2025-07-10.xlsx │ │ │ └── ... │ │ ├── drawings/ │ │ │ ├── v1_base.dxf │ │ │ ├── v1_detailed.dxf │ │ │ └── ... │ │ ├── reports/ │ │ │ ├── v1_report.pdf │ │ │ └── ... │ │ └── bundles/ │ │ ├── v1_20250705.zip # 완전 묶음 │ │ └── ... │ └── {project_uuid2}/ │ └── ... ├── temp/ # 임시 처리 파일 (일정 기간 후 삭제) └── cache/ # 렌더링 캐시 (3D Tiles, 이미지 타일) ``` --- ## 4. 주요 쿼리 패턴 및 인덱스 전략 ### 4.1 자주 사용할 조회 패턴 ```sql -- 사용자의 전체 프로젝트 목록 (상태별 필터) SELECT * FROM projects WHERE user_id = :user_id AND deleted_at IS NULL ORDER BY created_at DESC; -- 프로젝트의 최신 경로 SELECT * FROM routes WHERE project_id = :project_id AND status IN ('CONFIRMED', 'DRAFT') ORDER BY computed_at DESC LIMIT 1; -- 경로의 횡단면 일괄 조회 (웹 렌더링) SELECT chainage_m, geometry FROM cross_sections WHERE route_id = :route_id AND status = 'CONFIRMED' ORDER BY chainage_m; -- 수량 항목별 비용 집계 SELECT category, SUM(total_price) as subtotal FROM quantity_items WHERE project_id = :project_id GROUP BY category; -- 공간 범위 쿼리 (지도 렌더링) SELECT id, geometry FROM routes WHERE project_id = :project_id AND ST_Intersects(geometry, ST_GeomFromText(:bbox_wkt)); ``` ### 4.2 권장 인덱스 ```sql -- 사용자별 프로젝트 조회 CREATE INDEX idx_projects_user_deleted ON projects(user_id, deleted_at); -- 프로젝트별 경로 최신순 CREATE INDEX idx_routes_project_computed ON routes(project_id, computed_at DESC); -- 경로의 횡단면 측점 순 CREATE INDEX idx_cross_sections_route_chainage ON cross_sections(route_id, chainage_m); -- PostGIS 공간 인덱스 CREATE INDEX idx_routes_geometry ON routes USING GIST(geometry); CREATE INDEX idx_cross_sections_geometry ON cross_sections USING GIST(geometry); -- 수량 항목 범위 쿼리 CREATE INDEX idx_quantity_project_category ON quantity_items(project_id, category); -- 감시 로그 시간순 CREATE INDEX idx_audit_logs_timestamp ON audit_logs(user_id, timestamp DESC); ``` --- ## 5. API 라우터 구조 (FastAPI) ### 5.1 라우터 계층화 ``` routers/ ├── auth.py # 인증 (로그인, 회원가입, 토큰 갱신) ├── users.py # 사용자 프로필 ├── companies.py # 회사 관리 ├── projects.py # 프로젝트 CRUD + 상태 관리 ├── files.py # 파일 업로드/다운로드 ├── workflows/ │ ├── wf1_surface.py # WF1: 지표면 분석 (LAS → DEM/mesh) │ ├── wf2_route.py # WF2: 경로 설계 │ ├── wf3_sections.py # WF3: 종횡단 생성 │ ├── wf4_design.py # WF4: 상세 설계 (구조물) │ ├── wf5_quantity.py # WF5: 수량 산출 │ └── wf6_outputs.py # WF6: 산출물 생성 (Excel/PDF/DXF) ├── analysis.py # 분석 결과 조회 (읽기 전용) └── admin.py # 관리자 기능 (감시 로그 등) ``` ### 5.2 주요 엔드포인트 예시 ``` POST /api/auth/register 사용자 가입 POST /api/auth/login 로그인 GET /api/users/me 현재 사용자 정보 PATCH /api/users/{user_id} 프로필 수정 POST /api/projects 프로젝트 생성 GET /api/projects 사용자 프로젝트 목록 GET /api/projects/{proj_id} 프로젝트 상세 PATCH /api/projects/{proj_id} 프로젝트 수정 (이름, 메모 등) POST /api/projects/{proj_id}/files/upload 파일 업로드 GET /api/projects/{proj_id}/files 파일 목록 POST /api/projects/{proj_id}/wf1/analyze WF1 시작 GET /api/projects/{proj_id}/wf1/status WF1 진행률 GET /api/projects/{proj_id}/wf1/surface-models 생성된 표면 POST /api/projects/{proj_id}/wf2/calculate-route WF2 경로 계산 GET /api/projects/{proj_id}/wf2/routes 경로 목록 PATCH /api/projects/{proj_id}/wf2/routes/{route_id} 경로 수정 GET /api/projects/{proj_id}/wf3/cross-sections 횡단면 목록 GET /api/projects/{proj_id}/wf3/sections/{ch} 특정 측점 상세 POST /api/projects/{proj_id}/wf4/structures 구조물 추가 PATCH /api/projects/{proj_id}/wf4/structures/{id} 구조물 수정 GET /api/projects/{proj_id}/wf5/quantities 수량 항목 PATCH /api/projects/{proj_id}/wf5/quantities/{id} 수량 수정 POST /api/projects/{proj_id}/wf6/generate-output 산출물 생성 GET /api/projects/{proj_id}/outputs 산출물 목록 GET /api/projects/{proj_id}/outputs/{id}/download 다운로드 POST /api/projects/{proj_id}/confirm 프로젝트 확정 ``` --- ## 6. 비동기 작업 및 백그라운드 처리 (Celery / asyncio) ### 6.1 장시간 작업 큐 ``` 작업 목록: 1. WF1 분석 (LAS 필터링 → 지형 변환) — 수십 분 2. WF2 경로 계산 (최적화 알고리즘) — 수 분 3. WF6 산출물 생성 (DXF/Excel/PDF) — 수 분 구현 방식: - Celery + Redis (프로덕션) 또는 - FastAPI BackgroundTasks + asyncio (초기 단계) 상태 추적: - jobs 테이블에 task_id, status, progress, error_msg 저장 - WebSocket 또는 polling으로 진행률 클라이언트에 전송 ``` --- ## 7. 마이그레이션 및 테이블 생성 전략 ### 7.1 alembic 기반 버전 관리 ``` alembic/ ├── versions/ │ ├── 001_initial_schema.py (users, companies, projects) │ ├── 002_spatial_tables.py (routes, cross_sections, PostGIS) │ ├── 003_audit_and_logs.py (audit_logs, change_logs) │ └── ... └── env.py ``` ### 7.2 초기 마이그레이션 단계 1. 사용자 / 회사 / 프로젝트 (로그인 후 필수) 2. 입력 파일 메타데이터 (파일 업로드 후 필수) 3. 경로 / 단면 / 구조물 (워크플로우 실행 후 필수) 4. 감시 로그 (선택, 나중에 추가 가능) --- ## 8. 데이터 일관성 및 검증 규칙 ### 8.1 데이터 무결성 제약 ```sql -- 외래키 제약 ALTER TABLE projects ADD CONSTRAINT fk_projects_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT; -- Unique 제약 ALTER TABLE users ADD CONSTRAINT uq_users_email UNIQUE(email); ALTER TABLE companies ADD CONSTRAINT uq_companies_name UNIQUE(name); -- Check 제약 (상태 머신) ALTER TABLE projects ADD CONSTRAINT check_project_status CHECK (status IN ('NEW', 'ANALYZING', 'WF1_COMPLETE', ..., 'DONE')); ``` ### 8.2 좌표계 검증 ```python # Python 유효성 검사 from pyproj import CRS def validate_crs(epsg_code: int): try: crs = CRS.from_epsg(epsg_code) return True except: raise ValueError(f"Invalid EPSG code: {epsg_code}") # 좌표 변환 시 예외 처리 def transform_coordinates(src_epsg, tgt_epsg, coords): transformer = Transformer.from_crs(f"EPSG:{src_epsg}", f"EPSG:{tgt_epsg}") return transformer.transform(coords.x, coords.y) ``` ### 8.3 입력 파일 검증 ```python # 파일 타입 검증 ALLOWED_EXTENSIONS = { 'las': laspy.read, 'tif': rasterio.open, 'prj': lambda f: f.read().decode('utf-8'), 'dxf': ezdxf.readfile, } # 파일 크기 제한 (config에서 읽기) MAX_FILE_SIZE_MB = 500 def validate_upload(file, file_type): if file.size > MAX_FILE_SIZE_MB * 1024 * 1024: raise ValueError(f"File too large: {file.size / 1024 / 1024:.1f}MB") ``` --- ## 9. 초기 vs 장기 구현 로드맵 ### Phase 1 (초기, 회원가입 MVP — 2-3주) **필수 테이블:** users, companies, projects, input_files, audit_logs **API:** 인증, 프로젝트 CRUD, 파일 업로드 **워크플로우:** 파일 입력(B03)까지만 DB 연동 ### Phase 2 (WF1-WF3, 분석 — 4-6주) **추가 테이블:** surface_models, routes, cross_sections, change_logs **API:** WF1 분석 시작, 경로 계산, 단면 조회 **백그라운드:** Celery 작업 큐 도입 ### Phase 3 (WF4-WF6, 산출물 — 6-8주) **추가 테이블:** structures, quantity_items, outputs, output_files **API:** 구조물 추가, 수량 산출, 산출물 생성 **기능:** Excel/PDF/DXF 자동 생성 ### Phase 4 (협업 및 감시, 선택) **추가 기능:** 팀 공유 프로젝트, 권한 세분화, 비용 청구 **감시:** audit_logs 기반 컴플라이언스 리포트 --- ## 10. 보안 및 성능 고려사항 ### 10.1 보안 - **인증:** JWT 토큰 (httponly cookie) - **권한:** user_id 기반 row-level security (RLS) + 애플리케이션 검증 - **파일 접근:** 서명된 URL 또는 토큰 검증 - **감시:** audit_logs에 모든 수정 기록 - **SQL injection 방지:** ORM (SQLAlchemy) + 파라미터화된 쿼리 ### 10.2 성능 최적화 - **쿼리:** 인덱스 전략 (위 4.2 참고) - **캐싱:** Redis에 자주 조회하는 메타데이터 캐시 (설정값, 사용자 권한 등) - **파일 저장소:** 클라우드 스토리지 옵션 (AWS S3, GCS) 고려 - **3D 렌더링:** LOD/타일링으로 대용량 지형 최적화 - **비동기:** 장시간 작업은 백그라운드 큐에서 처리 ### 10.3 백업 및 복구 - **DB 백업:** 일 1회 자동 백업 (point-in-time recovery) - **파일 백업:** 중요 산출물은 별도 저장소에 복제 - **버전 관리:** change_logs로 프로젝트 상태 롤백 가능 --- ## 11. 검토 항목 및 다음 단계 ### 체크리스트 - [ ] ERD(Entity-Relationship Diagram) 작성 및 리뷰 - [ ] 좌표계 변환 로직 검증 (EPSG 코드) - [ ] 파일 저장소 정책 확정 (로컬 vs 클라우드) - [ ] 권한 모델 세밀화 (팀 공유 필요성 판단) - [ ] 비용 청구 로직 정의 (필요 시) - [ ] 성능 테스트 (대용량 파일, 대량 사용자) - [ ] 법적 데이터 보관 기간 확인 ### 다음 작업 1. **ERD 다이어그램 생성** — dbdiagram.io 또는 draw.io 2. **alembic 마이그레이션 초안 작성** — Phase 1 테이블부터 3. **FastAPI 모델(Pydantic) 정의** — 입력/출력 스키마 4. **API 문서(OpenAPI)** — Swagger 자동 생성 5. **테스트 DB 구성** — 로컬 PostgreSQL + 샘플 데이터 --- ## 12. 참고: 구현 언어 & 도구 | 영역 | 도구 | 용도 | |------|------|------| | **ORM** | SQLAlchemy 2.0 + asyncpg | async DB 쿼리 | | **마이그레이션** | Alembic | DB 버전 관리 | | **검증** | Pydantic v2 | 입력 데이터 검증 | | **공간 쿼리** | GeoAlchemy2 + PostGIS | 기하학 연산 | | **비동기** | Celery + Redis 또는 FastAPI BackgroundTasks | 장시간 작업 | | **파일 처리** | laspy, rasterio, geopandas, ezdxf | 공간 데이터 | | **문서 생성** | openpyxl, reportlab, ezdxf | Excel/PDF/DXF 출력 | --- **이 제안서는 기초 설계 단계입니다. 다음 리뷰에서 변경, 추가, 삭제 사항을 정리한 후 ERD 및 마이그레이션 코드로 구체화하겠습니다.**