260705_1
This commit is contained in:
@@ -0,0 +1,682 @@
|
|||||||
|
# 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 및 마이그레이션 코드로 구체화하겠습니다.**
|
||||||
@@ -0,0 +1,499 @@
|
|||||||
|
# DB 스키마 (테이블 및 열 목록)
|
||||||
|
|
||||||
|
## 1. 사용자 & 인증 그룹
|
||||||
|
|
||||||
|
### 1-1. users 테이블
|
||||||
|
```
|
||||||
|
users
|
||||||
|
├── id (INT, PK) — 사용자 고유 번호
|
||||||
|
├── email (TEXT, UNIQUE) — 로그인 이메일
|
||||||
|
├── password_hash (TEXT) — 비밀번호 암호화 저장
|
||||||
|
├── name (TEXT) — 사용자 이름
|
||||||
|
├── company_id (INT, FK → companies.id) — 소속 회사
|
||||||
|
├── role (TEXT) — 역할: admin, user, guest
|
||||||
|
├── created_at (TIMESTAMP) — 가입일
|
||||||
|
├── updated_at (TIMESTAMP) — 수정일
|
||||||
|
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1-2. companies 테이블
|
||||||
|
```
|
||||||
|
companies
|
||||||
|
├── id (INT, PK) — 회사 고유 번호
|
||||||
|
├── name (TEXT, UNIQUE) — 회사명
|
||||||
|
├── created_by (INT, FK → users.id) — 회사 생성자
|
||||||
|
├── created_at (TIMESTAMP) — 생성일
|
||||||
|
└── updated_at (TIMESTAMP) — 수정일
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 프로젝트 그룹
|
||||||
|
|
||||||
|
### 2-1. projects 테이블
|
||||||
|
```
|
||||||
|
projects
|
||||||
|
├── id (UUID, PK) — 프로젝트 고유 ID (예: 550e8400-e29b-41d4-a716-446655440000)
|
||||||
|
├── user_id (INT, FK → users.id) — 프로젝트 소유자
|
||||||
|
├── company_id (INT, FK → companies.id) — 소속 회사
|
||||||
|
├── name (TEXT) — 프로젝트명 (예: "2025년 산불진화임도")
|
||||||
|
├── region (TEXT) — 지역명 (예: "울진군 금강송면")
|
||||||
|
├── road_type (TEXT) — 임도 종류: 간선임도, 지선임도, 산불진화임도, 계류보전
|
||||||
|
├── project_year (INT) — 사업 연도 (예: 2025)
|
||||||
|
├── estimated_length_m (FLOAT) — 추정 연장 (미터)
|
||||||
|
├── memo (TEXT) — 비고/메모
|
||||||
|
├── status (TEXT) — 상태: NEW, FILE_UPLOADED, WF1_ANALYZING, WF1_COMPLETE, WF2_COMPLETE, ... , CONFIRMED, DONE
|
||||||
|
├── crs_epsg (INT) — 좌표계 (예: 5178 = 한국 표준)
|
||||||
|
├── bbox (GEOMETRY) — 프로젝트 전체 범위 (다각형)
|
||||||
|
├── created_at (TIMESTAMP) — 생성일
|
||||||
|
├── updated_at (TIMESTAMP) — 수정일
|
||||||
|
├── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
|
||||||
|
└── storage_path (TEXT) — 파일시스템 경로 (예: "storage/회사/사용자/project_uuid")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2-2. project_versions 테이블 (선택사항)
|
||||||
|
```
|
||||||
|
project_versions
|
||||||
|
├── id (INT, PK)
|
||||||
|
├── project_id (UUID, FK → projects.id)
|
||||||
|
├── version_num (INT) — 버전 번호 (1, 2, 3, ...)
|
||||||
|
├── snapshot_at (TIMESTAMP) — 스냅샷 시점
|
||||||
|
├── status (TEXT) — 저장된 상태
|
||||||
|
├── data (JSONB) — 설계 데이터 스냅샷 (JSON 형식)
|
||||||
|
└── created_at (TIMESTAMP)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 파일 & 입력 데이터 그룹
|
||||||
|
|
||||||
|
### 3-1. input_files 테이블
|
||||||
|
```
|
||||||
|
input_files
|
||||||
|
├── id (INT, PK) — 파일 고유 번호
|
||||||
|
├── project_id (UUID, FK → projects.id) — 속한 프로젝트
|
||||||
|
├── file_type (TEXT) — 파일 종류: las, tif, tfw, prj, dxf, dwg, other
|
||||||
|
├── original_filename (TEXT) — 원래 파일명 (예: "cloud_merged.las")
|
||||||
|
├── stored_path (TEXT) — 저장 경로 (예: "storage/.../raw/las/cloud_merged.las")
|
||||||
|
├── file_size_mb (FLOAT) — 파일 크기 (MB)
|
||||||
|
├── upload_by (INT, FK → users.id) — 업로드한 사용자
|
||||||
|
├── upload_at (TIMESTAMP) — 업로드 날짜
|
||||||
|
├── crs_epsg (INT) — 파일 좌표계 (예: 5178)
|
||||||
|
├── metadata (JSONB) — 파일 메타데이터 (해상도, 범위, 포인트 수 등)
|
||||||
|
│ ├── resolution_m (좌표계 기준 해상도)
|
||||||
|
│ ├── data_range (데이터 범위)
|
||||||
|
│ └── ...
|
||||||
|
└── status (TEXT) — 상태: UPLOADED, PROCESSED, ARCHIVED
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3-2. point_cloud_metadata 테이블
|
||||||
|
```
|
||||||
|
point_cloud_metadata
|
||||||
|
├── id (INT, PK)
|
||||||
|
├── input_file_id (INT, FK → input_files.id) — LAS 파일 참조
|
||||||
|
├── point_count (INT) — 포인트 개수
|
||||||
|
├── min_z (FLOAT) — 최저 높이
|
||||||
|
├── max_z (FLOAT) — 최고 높이
|
||||||
|
├── mean_z (FLOAT) — 평균 높이
|
||||||
|
├── x_min (FLOAT) — 최소 경도
|
||||||
|
├── x_max (FLOAT) — 최대 경도
|
||||||
|
├── y_min (FLOAT) — 최소 위도
|
||||||
|
├── y_max (FLOAT) — 최대 위도
|
||||||
|
├── density_per_sqm (FLOAT) — 단위 면적당 포인트 밀도
|
||||||
|
└── classification_summary (JSONB) — 분류 요약
|
||||||
|
├── ground (지표면 포인트)
|
||||||
|
├── vegetation (식생)
|
||||||
|
├── building (건물)
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 지표면 & 분석 결과 그룹
|
||||||
|
|
||||||
|
### 4-1. surface_models 테이블 (WF1 출력)
|
||||||
|
```
|
||||||
|
surface_models
|
||||||
|
├── id (INT, PK) — 지표면 모델 고유 번호
|
||||||
|
├── project_id (UUID, FK → projects.id)
|
||||||
|
├── model_type (TEXT) — 모델 종류: dem_grid, tin, mesh_triangulated, contour_lines
|
||||||
|
├── source_file_id (INT, FK → input_files.id) — 원본 LAS 파일
|
||||||
|
├── status (TEXT) — 상태: PROCESSING, COMPLETE, FAILED
|
||||||
|
├── crs_epsg (INT) — 좌표계
|
||||||
|
├── resolution_m (FLOAT) — 해상도 (래스터인 경우, NULL 가능)
|
||||||
|
├── stored_path (TEXT) — 저장 경로 (예: "storage/.../processed/surface/dem.tif")
|
||||||
|
├── bounds (GEOMETRY) — 모델 범위 (다각형)
|
||||||
|
├── metadata (JSONB) — 생성 파라미터
|
||||||
|
│ ├── filter_type (필터링 방식)
|
||||||
|
│ ├── creation_time (생성 시각)
|
||||||
|
│ └── ...
|
||||||
|
├── created_at (TIMESTAMP)
|
||||||
|
└── completed_at (TIMESTAMP)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4-2. terrain_layers 테이블
|
||||||
|
```
|
||||||
|
terrain_layers
|
||||||
|
├── id (INT, PK)
|
||||||
|
├── surface_model_id (INT, FK → surface_models.id)
|
||||||
|
├── layer_name (TEXT) — 레이어명 (예: "지표", "제1층", "제2층" 등)
|
||||||
|
├── geometry_type (TEXT) — 기하 종류: POINTCLOUD, GRID, MESH, CONTOUR
|
||||||
|
├── stored_path (TEXT) — 저장 경로 (GeoJSON, GeoTIFF, LAS 등)
|
||||||
|
└── statistics (JSONB) — 통계
|
||||||
|
├── min_z, max_z, mean_slope
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 경로 & 설계 그룹
|
||||||
|
|
||||||
|
### 5-1. routes 테이블 (WF2 출력)
|
||||||
|
```
|
||||||
|
routes
|
||||||
|
├── id (INT, PK) — 경로 고유 번호
|
||||||
|
├── project_id (UUID, FK → projects.id)
|
||||||
|
├── status (TEXT) — 상태: DRAFT, CONFIRMED, ARCHIVED
|
||||||
|
├── start_point (GEOMETRY(Point)) — 시작점 좌표 (3D)
|
||||||
|
├── end_point (GEOMETRY(Point)) — 종료점 좌표 (3D)
|
||||||
|
├── start_chainage_m (FLOAT) — 시작점 측점 (m)
|
||||||
|
├── end_chainage_m (FLOAT) — 종료점 측점 (m)
|
||||||
|
├── total_length_m (FLOAT) — 총 연장 (m)
|
||||||
|
├── grade_percent (JSONB array) — 각 구간 종단 경사도 (%)
|
||||||
|
│ └── [2.5, 1.8, 3.2, ...]
|
||||||
|
├── constraints (JSONB) — 설계 제약조건
|
||||||
|
│ ├── max_grade (% 최대경사도)
|
||||||
|
│ ├── min_radius (m 최소곡선반경)
|
||||||
|
│ ├── avoidance_zones ([폴리곤 배열])
|
||||||
|
│ └── ...
|
||||||
|
├── algorithm_params (JSONB) — 알고리즘 파라미터
|
||||||
|
│ ├── cost_weights (비용함수 가중치)
|
||||||
|
│ └── ...
|
||||||
|
├── geometry (GEOMETRY(LineString, Z)) — 3D 경로 좌표 열
|
||||||
|
├── computed_at (TIMESTAMP) — 계산 완료 날짜
|
||||||
|
└── created_at (TIMESTAMP)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5-2. route_points 테이블 (웹 렌더링용 샘플)
|
||||||
|
```
|
||||||
|
route_points
|
||||||
|
├── id (INT, PK)
|
||||||
|
├── route_id (INT, FK → routes.id)
|
||||||
|
├── chainage_m (FLOAT) — 측점 (m)
|
||||||
|
├── geometry (GEOMETRY(Point, Z)) — 포인트 좌표
|
||||||
|
├── elevation_m (FLOAT) — 높이
|
||||||
|
├── slope_percent (FLOAT) — 경사도 (%)
|
||||||
|
└── sequence_num (INT) — 순서번호
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5-3. route_statistics 테이블
|
||||||
|
```
|
||||||
|
route_statistics
|
||||||
|
├── id (INT, PK)
|
||||||
|
├── route_id (INT, FK → routes.id)
|
||||||
|
├── min_slope (FLOAT) — 최소 경사도 (%)
|
||||||
|
├── max_slope (FLOAT) — 최대 경사도 (%)
|
||||||
|
├── mean_slope (FLOAT) — 평균 경사도 (%)
|
||||||
|
├── cut_volume_m3 (FLOAT) — 절토량 (m³)
|
||||||
|
├── fill_volume_m3 (FLOAT) — 성토량 (m³)
|
||||||
|
├── tree_cutting_volume (FLOAT) — 목재 절감 추정량
|
||||||
|
└── cost_score (FLOAT) — 알고리즘 점수
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 종단면 & 횡단면 그룹
|
||||||
|
|
||||||
|
### 6-1. longitudinal_sections 테이블 (WF3 출력)
|
||||||
|
```
|
||||||
|
longitudinal_sections
|
||||||
|
├── id (INT, PK) — 종단면 고유 번호
|
||||||
|
├── project_id (UUID, FK → projects.id)
|
||||||
|
├── route_id (INT, FK → routes.id)
|
||||||
|
├── computed_at (TIMESTAMP) — 계산 날짜
|
||||||
|
├── geometry (GEOMETRY(LineString, Z)) — 종단면 지형 좌표열
|
||||||
|
├── data (JSONB) — 상세 데이터
|
||||||
|
│ ├── chainages ([0, 20, 40, 60, ...]) — 측점 배열 (m)
|
||||||
|
│ ├── elevations ([100.5, 102.3, 101.8, ...]) — 지표 표고 배열
|
||||||
|
│ ├── grades ([2.5, 1.8, 1.0, ...]) — 경사도 배열 (%)
|
||||||
|
│ └── design_elevations ([100.0, 102.0, 101.5, ...]) — 설계 표고
|
||||||
|
└── stored_path (TEXT) — 저장 경로 (예: "storage/.../sections/longitudinal.json")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6-2. cross_sections 테이블 (WF3 출력)
|
||||||
|
```
|
||||||
|
cross_sections
|
||||||
|
├── id (INT, PK) — 횡단면 고유 번호
|
||||||
|
├── project_id (UUID, FK → projects.id)
|
||||||
|
├── route_id (INT, FK → routes.id)
|
||||||
|
├── chainage_m (FLOAT) — 측점 (예: 0, 20, 40, ... m)
|
||||||
|
├── sequence_num (INT) — 횡단면 순번 (1번째, 2번째, ...)
|
||||||
|
├── geometry (GEOMETRY(LineString, Z)) — 지형 횡단선 좌표열
|
||||||
|
├── geometry_design (GEOMETRY(LineString, Z)) — 설계 횡단선 좌표열
|
||||||
|
├── data (JSONB) — 상세 데이터
|
||||||
|
│ ├── left_slope (FLOAT, %) — 좌측 사면 경사
|
||||||
|
│ ├── right_slope (FLOAT, %) — 우측 사면 경사
|
||||||
|
│ ├── width_m (FLOAT) — 노폭
|
||||||
|
│ ├── cut_volume_m3 (FLOAT) — 절토량
|
||||||
|
│ ├── fill_volume_m3 (FLOAT) — 성토량
|
||||||
|
│ ├── structures ([]) — 이 단면 내 구조물 ID 배열
|
||||||
|
│ └── notes (TEXT) — 메모
|
||||||
|
└── stored_path (TEXT) — 저장 경로 (예: "storage/.../sections/cross_0020m.json")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 설계 & 구조물 그룹
|
||||||
|
|
||||||
|
### 7-1. structures 테이블 (WF4 출력)
|
||||||
|
```
|
||||||
|
structures
|
||||||
|
├── id (INT, PK) — 구조물 고유 번호
|
||||||
|
├── project_id (UUID, FK → projects.id)
|
||||||
|
├── cross_section_id (INT, FK → cross_sections.id)
|
||||||
|
├── structure_type (TEXT) — 종류: 낙석방지책, 돌붙임, 계간수로, 낙차공, 등
|
||||||
|
├── chainage_m (FLOAT) — 측점 (m)
|
||||||
|
├── location (TEXT) — 위치: LEFT, CENTER, RIGHT
|
||||||
|
├── length_m (FLOAT) — 길이
|
||||||
|
├── width_m (FLOAT) — 폭
|
||||||
|
├── height_m (FLOAT) — 높이
|
||||||
|
├── material (TEXT) — 재료: 강재, 콘크리트, 목재, 돌 등
|
||||||
|
├── quantity (INT) — 개수
|
||||||
|
├── unit_price (FLOAT) — 단가
|
||||||
|
├── geometry (GEOMETRY(Polygon)) — 3D 배치 도형
|
||||||
|
├── design_notes (JSONB) — 설계 노트
|
||||||
|
├── last_modified_by (INT, FK → users.id) — 마지막 수정자
|
||||||
|
└── created_at (TIMESTAMP)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7-2. quantity_items 테이블 (WF5 출력)
|
||||||
|
```
|
||||||
|
quantity_items
|
||||||
|
├── id (INT, PK) — 수량 항목 고유 번호
|
||||||
|
├── project_id (UUID, FK → projects.id)
|
||||||
|
├── category (TEXT) — 대분류: 토공, 구조물, 포장, 배수, 녹화, 안전시설
|
||||||
|
├── item_name (TEXT) — 항목명 (예: "절토 일반", "낙석방지책 설치")
|
||||||
|
├── unit (TEXT) — 단위: m3, 개, m, m2
|
||||||
|
├── quantity_design (FLOAT) — 설계 수량
|
||||||
|
├── quantity_actual (FLOAT) — 실제 수량 (사용자 수정 가능)
|
||||||
|
├── unit_price (FLOAT) — 단가
|
||||||
|
├── total_price (FLOAT) — 소계 (quantity_actual × unit_price)
|
||||||
|
├── standard_reference (TEXT) — 기준 참고 자료
|
||||||
|
├── computed_at (TIMESTAMP) — 계산 날짜
|
||||||
|
└── data (JSONB) — 계산 과정 메모
|
||||||
|
├── formula (계산식)
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 산출물 & 문서 그룹
|
||||||
|
|
||||||
|
### 8-1. outputs 테이블 (WF6 출력)
|
||||||
|
```
|
||||||
|
outputs
|
||||||
|
├── id (INT, PK) — 산출물 세트 고유 번호
|
||||||
|
├── project_id (UUID, FK → projects.id)
|
||||||
|
├── output_type (TEXT) — 종류: estimation_excel, drawing_dxf, report_pdf, all_bundle
|
||||||
|
├── status (TEXT) — 상태: GENERATING, COMPLETE, FAILED
|
||||||
|
├── generated_by (INT, FK → users.id) — 생성자
|
||||||
|
├── generated_at (TIMESTAMP) — 생성 날짜
|
||||||
|
├── version (INT) — 버전 (프로젝트 확정 시 자동 증가)
|
||||||
|
└── metadata (JSONB) — 메타데이터
|
||||||
|
├── template_used (사용한 양식)
|
||||||
|
├── company_name (회사명)
|
||||||
|
├── project_name (프로젝트명)
|
||||||
|
├── total_cost (총 비용)
|
||||||
|
└── generation_time_sec (생성 소요 시간)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8-2. output_files 테이블
|
||||||
|
```
|
||||||
|
output_files
|
||||||
|
├── id (INT, PK) — 파일 고유 번호
|
||||||
|
├── output_id (INT, FK → outputs.id)
|
||||||
|
├── file_type (TEXT) — 파일 형식: xlsx, pdf, dxf, dwg, json, zip
|
||||||
|
├── original_filename (TEXT) — 파일명 (예: "견적서_v1.xlsx")
|
||||||
|
├── stored_path (TEXT) — 저장 경로 (예: "storage/.../outputs/estimation/견적서_v1.xlsx")
|
||||||
|
├── file_size_mb (FLOAT) — 파일 크기
|
||||||
|
├── created_at (TIMESTAMP) — 생성 날짜
|
||||||
|
└── download_count (INT) — 다운로드 횟수
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 변경 이력 & 감시 그룹
|
||||||
|
|
||||||
|
### 9-1. audit_logs 테이블 (보안 감시)
|
||||||
|
```
|
||||||
|
audit_logs
|
||||||
|
├── id (INT, PK) — 로그 고유 번호
|
||||||
|
├── user_id (INT, FK → users.id) — 행동 수행자
|
||||||
|
├── action (TEXT) — 행동: CREATE, READ, UPDATE, DELETE, EXPORT, DOWNLOAD
|
||||||
|
├── entity_type (TEXT) — 대상 타입: projects, routes, structures, outputs 등
|
||||||
|
├── entity_id (TEXT) — 대상 ID
|
||||||
|
├── timestamp (TIMESTAMP) — 시간
|
||||||
|
├── ip_address (TEXT) — IP 주소
|
||||||
|
└── details (JSONB) — 상세 정보
|
||||||
|
├── old_value (변경 전 값)
|
||||||
|
├── new_value (변경 후 값)
|
||||||
|
└── reason (변경 사유)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9-2. change_logs 테이블 (설계 변경 기록)
|
||||||
|
```
|
||||||
|
change_logs
|
||||||
|
├── id (INT, PK) — 변경 고유 번호
|
||||||
|
├── project_id (UUID, FK → projects.id)
|
||||||
|
├── changed_by (INT, FK → users.id) — 변경자
|
||||||
|
├── changed_at (TIMESTAMP) — 변경 날짜
|
||||||
|
├── entity_type (TEXT) — 대상 타입: routes, cross_sections, structures, quantity_items
|
||||||
|
├── entity_id (INT) — 대상 ID
|
||||||
|
├── old_value (JSONB) — 변경 전 값
|
||||||
|
├── new_value (JSONB) — 변경 후 값
|
||||||
|
└── reason (TEXT) — 변경 사유
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 테이블 간 관계도 (폴더처럼 표현)
|
||||||
|
|
||||||
|
```
|
||||||
|
users (사용자)
|
||||||
|
├── ↓ FK: company_id
|
||||||
|
├─→ companies (회사)
|
||||||
|
│ ├── ↓ FK
|
||||||
|
│ └─→ projects (프로젝트)
|
||||||
|
│ ├── ↓ FK
|
||||||
|
│ ├─→ input_files (입력 파일)
|
||||||
|
│ │ └─→ point_cloud_metadata (포인트클라우드 메타)
|
||||||
|
│ ├─→ surface_models (지표면 모델)
|
||||||
|
│ │ └─→ terrain_layers (지형 레이어)
|
||||||
|
│ ├─→ routes (경로)
|
||||||
|
│ │ ├─→ route_points (경로 포인트)
|
||||||
|
│ │ ├─→ route_statistics (경로 통계)
|
||||||
|
│ │ ├─→ longitudinal_sections (종단면)
|
||||||
|
│ │ └─→ cross_sections (횡단면)
|
||||||
|
│ │ └─→ structures (구조물)
|
||||||
|
│ ├─→ quantity_items (수량 항목)
|
||||||
|
│ ├─→ outputs (산출물)
|
||||||
|
│ │ └─→ output_files (산출물 파일)
|
||||||
|
│ ├─→ project_versions (프로젝트 버전)
|
||||||
|
│ ├─→ audit_logs (감시 로그)
|
||||||
|
│ └─→ change_logs (변경 로그)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 파일시스템 경로와 DB 링크
|
||||||
|
|
||||||
|
```
|
||||||
|
storage/
|
||||||
|
├── {company_slug}/
|
||||||
|
│ └── {user_slug}/
|
||||||
|
│ └── {project_id}/ ← projects.storage_path
|
||||||
|
│ ├── raw/ ← input_files.stored_path
|
||||||
|
│ │ ├── las/
|
||||||
|
│ │ ├── tif/
|
||||||
|
│ │ ├── tfw/
|
||||||
|
│ │ ├── prj/
|
||||||
|
│ │ └── dxf/
|
||||||
|
│ ├── processed/ ← surface_models.stored_path, terrain_layers.stored_path
|
||||||
|
│ │ ├── surface/
|
||||||
|
│ │ ├── points/
|
||||||
|
│ │ └── tiles/
|
||||||
|
│ ├── sections/ ← longitudinal_sections.stored_path, cross_sections.stored_path
|
||||||
|
│ │ ├── longitudinal.json
|
||||||
|
│ │ ├── cross_0000m.json
|
||||||
|
│ │ ├── cross_0020m.json
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── outputs/ ← output_files.stored_path
|
||||||
|
│ ├── estimation/
|
||||||
|
│ ├── drawings/
|
||||||
|
│ ├── reports/
|
||||||
|
│ └── bundles/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 핵심 쿼리 예제
|
||||||
|
|
||||||
|
### 예제 1: 사용자의 모든 프로젝트 조회
|
||||||
|
```sql
|
||||||
|
SELECT p.id, p.name, p.status, p.created_at
|
||||||
|
FROM projects p
|
||||||
|
WHERE p.user_id = :user_id AND p.deleted_at IS NULL
|
||||||
|
ORDER BY p.created_at DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 예제 2: 프로젝트의 최신 경로 조회
|
||||||
|
```sql
|
||||||
|
SELECT r.id, r.total_length_m, r.geometry, r.status
|
||||||
|
FROM routes r
|
||||||
|
WHERE r.project_id = :project_id
|
||||||
|
ORDER BY r.computed_at DESC
|
||||||
|
LIMIT 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 예제 3: 경로의 모든 횡단면 조회
|
||||||
|
```sql
|
||||||
|
SELECT cs.id, cs.chainage_m, cs.geometry, cs.data
|
||||||
|
FROM cross_sections cs
|
||||||
|
WHERE cs.route_id = :route_id
|
||||||
|
ORDER BY cs.chainage_m;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 예제 4: 프로젝트 총 비용 계산
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
SUM(quantity_actual * unit_price) as total_cost,
|
||||||
|
category
|
||||||
|
FROM quantity_items
|
||||||
|
WHERE project_id = :project_id
|
||||||
|
GROUP BY category;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 예제 5: 사용자의 감시 로그 (최근 30일)
|
||||||
|
```sql
|
||||||
|
SELECT action, entity_type, timestamp, details
|
||||||
|
FROM audit_logs
|
||||||
|
WHERE user_id = :user_id AND timestamp > NOW() - INTERVAL '30 days'
|
||||||
|
ORDER BY timestamp DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. 데이터 타입 가이드
|
||||||
|
|
||||||
|
| 타입 | 설명 | 예제 |
|
||||||
|
|------|------|------|
|
||||||
|
| INT | 정수 | 1, 2, 100 |
|
||||||
|
| FLOAT | 실수 | 1.5, 2.25, 100.75 |
|
||||||
|
| TEXT | 문자열 (가변) | "프로젝트명", "울진군" |
|
||||||
|
| TIMESTAMP | 날짜 + 시간 | 2025-07-05 15:30:00 |
|
||||||
|
| BOOLEAN | 참/거짓 | TRUE, FALSE |
|
||||||
|
| UUID | 36자 고유 ID | 550e8400-e29b-41d4-a716-446655440000 |
|
||||||
|
| JSONB | JSON 데이터 | {"key": "value", "array": [1,2,3]} |
|
||||||
|
| GEOMETRY | 지리 좌표 | POINT(127.5 37.5), LINESTRING(...) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 요약
|
||||||
|
|
||||||
|
**총 17개 테이블:**
|
||||||
|
1. users, companies
|
||||||
|
2. projects, project_versions
|
||||||
|
3. input_files, point_cloud_metadata
|
||||||
|
4. surface_models, terrain_layers
|
||||||
|
5. routes, route_points, route_statistics
|
||||||
|
6. longitudinal_sections, cross_sections
|
||||||
|
7. structures, quantity_items
|
||||||
|
8. outputs, output_files
|
||||||
|
9. audit_logs, change_logs
|
||||||
|
|
||||||
|
**다음 단계:**
|
||||||
|
- [ ] PostgreSQL에 위 테이블 생성
|
||||||
|
- [ ] 관계(FK) 설정
|
||||||
|
- [ ] 인덱스 추가
|
||||||
|
- [ ] Alembic 마이그레이션 코드 작성
|
||||||
|
- [ ] Python ORM(SQLAlchemy) 모델 정의
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
# 프론트엔드 구현 계획서 (frontend_plan.md)
|
|
||||||
|
|
||||||
**작성일:** 2026-07-05
|
|
||||||
**상태:** 계획 수립 완료
|
|
||||||
**목표:** POC → 정식 프로덕션 프론트엔드 구축 (백엔드 독립적 구현)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 전략 및 원칙
|
|
||||||
|
|
||||||
### 1.1 핵심 방침
|
|
||||||
- **UI-First 접근:** 기존 프로그램의 UI가 없으므로 **프론트엔드를 먼저 완성**
|
|
||||||
- **템플릿 기반 개발:** `ui_template/` 3개 파일(theme.css, locale.ts, elements.ts)만 사용
|
|
||||||
- **백엔드 독립성:** 각 페이지는 **Mock 데이터로 독립 실행 가능**하게 구현
|
|
||||||
- **점진적 마이그레이션:** 백엔드 구현 후 API 연결은 **페이지별 선택적으로**
|
|
||||||
|
|
||||||
### 1.2 기술 스택 (frontend.md 기반)
|
|
||||||
- **언어:** TypeScript / HTML5 Canvas (WebCAD)
|
|
||||||
- **스타일:** CSS 변수만 참조 (하드코딩 금지)
|
|
||||||
- **다국어:** `ui_template_locale.ts` 배열 방식 `[한국어, 영어]`
|
|
||||||
- **컴포넌트:** `ui_template_elements.ts` 팩토리 함수 (createButton, createInputField 등)
|
|
||||||
- **상태 관리:** Vanilla TS (프레임워크 미사용, 가볍고 빠름)
|
|
||||||
|
|
||||||
### 1.3 행동 제약 (frontend.md §1~4)
|
|
||||||
1. **색상/간격:** 모두 `var(--...)`로만 참조 (Hex/px 하드코딩 금지)
|
|
||||||
2. **컴포넌트:** `ui_template_elements.ts` 규격만 사용 (임의 스타일링 금지)
|
|
||||||
3. **텍스트:** **모든** 문자열은 `ui_template_locale.ts`에 **선(先) 등록**, 후(後) 컴포넌트에서 참조
|
|
||||||
4. **이벤트 핸들러:** `on[페이지]_[기능]_[액션]` 명명 (예: `onA01_Home_NewsLink_Click`)
|
|
||||||
5. **API 호출:** 호출 직전 `showLoadingOverlay()`, 완료 후 `hideLoadingOverlay()`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 페이지 구조 (18페이지 + 워크플로우)
|
|
||||||
|
|
||||||
### Phase A: 로그인 전 페이지 (A01~A08) — 2주
|
|
||||||
각 페이지는 2개 파일 구성:
|
|
||||||
- `A0X_PageName_UI_Page.ts` — HTML 구조 + 컴포넌트 조립 + 이벤트 핸들링
|
|
||||||
- `A0X_PageName_UI_Style.css` — 페이지 고유 스타일 (theme.css 변수만 사용)
|
|
||||||
|
|
||||||
#### A01_Home (홈)
|
|
||||||
- **목표:** 프로그램 소개, 최신 뉴스, 로그인/회원가입 링크
|
|
||||||
- **레이아웃:** 싱글 컬럼 (3단 레이아웃 미적용)
|
|
||||||
- **컴포넌트:** 제목, 뉴스 카드 그리드, CTA 버튼 쌍 (filled + ghost)
|
|
||||||
- **상태:** 뉴스 목록 (Mock 배열)
|
|
||||||
|
|
||||||
#### A02_ProgDetail (프로그램 상세)
|
|
||||||
- **목표:** 프로그램 기능 소개, 스크린샷, FAQ
|
|
||||||
- **레이아웃:** 싱글 컬럼, 섹션 구분
|
|
||||||
- **컴포넌트:** 아코디언 (FAQ), 탭 (기능별 설명)
|
|
||||||
|
|
||||||
#### A03_CompDetail (회사 상세)
|
|
||||||
- **목표:** 회사 정보, 팀, 연락처
|
|
||||||
- **컴포넌트:** 정보 카드, 팀 멤버 그리드
|
|
||||||
|
|
||||||
#### A04_NewsHistory (뉴스/이력)
|
|
||||||
- **목표:** 공지사항, 버전 이력 조회
|
|
||||||
- **컴포넌트:** 타임라인, 검색/필터
|
|
||||||
|
|
||||||
#### A05_EduDetail (교육)
|
|
||||||
- **목표:** 사용자 가이드, 튜토리얼 링크
|
|
||||||
- **컴포넌트:** 비디오 임베드, 문서 링크 그리드
|
|
||||||
|
|
||||||
#### A06_Login (로그인) ⭐ 핵심
|
|
||||||
- **목표:** 이메일/비밀번호 입력, JWT 토큰 저장 (Mock)
|
|
||||||
- **필드:** 이메일(email), 비밀번호(password)
|
|
||||||
- **유효성 검사:** 이메일 형식, 비밀번호 길이 (프론트 1차)
|
|
||||||
- **성공 시:** 토큰을 `localStorage`에 저장, B01_AccountDetail로 리다이렉트
|
|
||||||
- **상태 관리:** 로그인 상태 플래그 (`isLoggedIn`)
|
|
||||||
|
|
||||||
#### A07_Register (회원가입) ⭐ 핵심
|
|
||||||
- **목표:** 신규 사용자 계정 생성
|
|
||||||
- **필드:** 이메일, 비밀번호, 비밀번호 확인, 약관 동의
|
|
||||||
- **유효성:** 이메일 형식, 비밀번호 강도(8자 이상, 대문자/숫자 포함), 비밀번호 일치
|
|
||||||
- **성공 시:** 계정 생성 토스트 + A06_Login으로 이동
|
|
||||||
|
|
||||||
#### A08_Support (기술 지원)
|
|
||||||
- **목표:** 문의 양식 제출
|
|
||||||
- **필드:** 이름, 이메일, 주제, 메시지
|
|
||||||
- **제출 시:** 토스트 메시지 (Mock 성공)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase B: 로그인 후 기초 (B01~B03) — 1.5주
|
|
||||||
JWT 인증 미들웨어 필수. 토큰 없으면 A01로 리다이렉트.
|
|
||||||
|
|
||||||
#### B01_AccountDetail (계정 상세)
|
|
||||||
- **목표:** 사용자 정보 조회/수정
|
|
||||||
- **필드:** 이름, 이메일, 전화, 회사명 (수정 토글)
|
|
||||||
- **섹션:** 계정 정보, 구독 상태, 다운로드 이력
|
|
||||||
|
|
||||||
#### B02_ProjRegister (프로젝트 등록)
|
|
||||||
- **목표:** 신규 프로젝트 생성
|
|
||||||
- **필드:** 프로젝트명, 고객사명, 프로젝트 설명
|
|
||||||
- **성공 시:** 프로젝트 ID 생성, 목록에 추가 (Mock), B03_FileInput으로 이동
|
|
||||||
|
|
||||||
#### B03_FileInput (파일 입력)
|
|
||||||
- **목표:** LAS, DEM, DWG 파일 업로드
|
|
||||||
- **UI:** Drag & Drop 박스 + 파일 목록 테이블
|
|
||||||
- **유효성:** 파일 확장자 검증 (.las, .dem, .dwg), 파일 크기 제한 (config_frontend.ts 참조)
|
|
||||||
- **성공 시:** 파일 목록 테이블 갱신, B04_wf1_Surface로 이동 가능
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase C: 워크플로우 (B04~B09) — 3주
|
|
||||||
**3단 레이아웃 (frontend.md §2) 준수 필수:**
|
|
||||||
- 상단: 페이지 타이틀 + 6단계 진행바
|
|
||||||
- 좌측: 입력 폼 + 파라미터 조정 (너비 고정 320px)
|
|
||||||
- 우측: WebCAD 뷰어 또는 그리드 (나머지 전체)
|
|
||||||
|
|
||||||
#### B04_wf1_Surface (지표면 분석)
|
|
||||||
- **상단:** "지표면 분석" 타이틀, 진행 Step 1/6 하이라이트
|
|
||||||
- **좌측:**
|
|
||||||
- 파일 선택 드롭다운 (B03에서 업로드한 LAS 목록)
|
|
||||||
- 메쉬 파라미터: Grid Size (숫자), Smoothing (슬라이더)
|
|
||||||
- "분석 실행" 버튼 (filled)
|
|
||||||
- **우측:**
|
|
||||||
- Canvas 영역 (WebGL 3D 뷰어 자리표)
|
|
||||||
- 메쉬 정보 카드 (vertices, faces, bounds)
|
|
||||||
- **Mock:** 더미 메쉬 데이터 로드
|
|
||||||
|
|
||||||
#### B05_wf2_Route (경로 설계)
|
|
||||||
- **좌측:**
|
|
||||||
- 지표면 선택 (B04 결과)
|
|
||||||
- 경로점 추가 모드 토글
|
|
||||||
- 경로 정보 (길이, 고도차)
|
|
||||||
- **우측:**
|
|
||||||
- 2D 평면도 (Canvas, SVG 오버레이로 클릭 가능)
|
|
||||||
- 경로점 시각화
|
|
||||||
- **Mock:** 기본 경로 1개 로드
|
|
||||||
|
|
||||||
#### B06_wf3_ProfileCross (종횡단 생성)
|
|
||||||
- **좌측:**
|
|
||||||
- 경로 선택
|
|
||||||
- 프로필 타입 선택 (종단도 / 횡단도)
|
|
||||||
- 샘플링 거리 (숫자)
|
|
||||||
- **우측:**
|
|
||||||
- 2D 그래프 영역 (SVG 선그래프)
|
|
||||||
- 기울기, 높이차 등 정보
|
|
||||||
|
|
||||||
#### B07_wf4_DesignDetail (상세 설계)
|
|
||||||
- **좌측:**
|
|
||||||
- 포장 재료 선택
|
|
||||||
- 두께, 기울기, 배수 입력
|
|
||||||
- Whitebox 토양 분석 옵션
|
|
||||||
- **우측:**
|
|
||||||
- 단면도 (3D 미니 렌더링 또는 단면 다이어그램)
|
|
||||||
|
|
||||||
#### B08_wf5_Quantity (수량 산출)
|
|
||||||
- **좌측:**
|
|
||||||
- 설계 정보 요약
|
|
||||||
- 수량 계산 버튼
|
|
||||||
- **우측:**
|
|
||||||
- 테이블 (포장재, 흙 깍기/쌓기 수량)
|
|
||||||
- 합계 행
|
|
||||||
|
|
||||||
#### B09_wf6_Estimation (견적/문서)
|
|
||||||
- **좌측:**
|
|
||||||
- 단가 입력 (포장재, 인건비 등)
|
|
||||||
- PDF/DWG 생성 버튼
|
|
||||||
- **우측:**
|
|
||||||
- 견적 요약 테이블 (항목, 수량, 단가, 금액)
|
|
||||||
- 총액
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase D: 최종 페이지 (B10~B11) — 1주
|
|
||||||
|
|
||||||
#### B10_Payment (결재)
|
|
||||||
- **목표:** 견적 검토 및 승인
|
|
||||||
- **UI:** 견적 미리보기 + 승인/반려 버튼
|
|
||||||
|
|
||||||
#### B11_Status (상태/다운로드)
|
|
||||||
- **목표:** 프로젝트 최종 상태 조회
|
|
||||||
- **UI:** 진행 상태 배지, 산출물 다운로드 링크 테이블
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 파일 구조 및 네이밍
|
|
||||||
|
|
||||||
```
|
|
||||||
A01_Home/
|
|
||||||
├── A01_Home_UI_Page.ts # HTML 구조, 컴포넌트, 이벤트 핸들러
|
|
||||||
└── A01_Home_UI_Style.css # 페이지 고유 스타일
|
|
||||||
|
|
||||||
B04_wf1_Surface/
|
|
||||||
├── B04_wf1_Surface_UI_View.ts # WebCAD 뷰어 + 정보 카드
|
|
||||||
├── B04_wf1_Surface_UI_Form.ts # 좌측 입력 폼
|
|
||||||
├── B04_wf1_Surface_UI_Style.css # 통합 스타일
|
|
||||||
└── (API 라우터는 나중에)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 다국어 등록 전략
|
|
||||||
|
|
||||||
각 페이지 구현 시 **선(先) 등록** 프로토콜:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ui_template_locale.ts에 먼저 추가
|
|
||||||
export const ui_locales = {
|
|
||||||
// ...
|
|
||||||
A01_Home_Title: ["홈", "Home"],
|
|
||||||
A01_Home_NewsSection: ["최신 소식", "Latest News"],
|
|
||||||
// ...
|
|
||||||
};
|
|
||||||
|
|
||||||
// A01_Home_UI_Page.ts에서 참조
|
|
||||||
const title = ui_locales.A01_Home_Title[currentLanguageIndex];
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Mock 데이터 및 상태 관리
|
|
||||||
|
|
||||||
각 페이지는 간단한 **로컬 상태 객체**로 관리:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// A01_Home_UI_Page.ts
|
|
||||||
const state = {
|
|
||||||
news: [
|
|
||||||
{ id: 1, title: "뉴스 1", date: "2026-07-05" },
|
|
||||||
{ id: 2, title: "뉴스 2", date: "2026-07-04" },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
로그인 상태는 **글로벌 플래그**:
|
|
||||||
```typescript
|
|
||||||
// 모든 페이지에서 참조
|
|
||||||
const isLoggedIn = localStorage.getItem('authToken') !== null;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 구현 순서 및 일정
|
|
||||||
|
|
||||||
### Week 1: 로그인 전 기초 (A01, A06, A07)
|
|
||||||
1. **A01_Home** — 소개 페이지 (카드, 그리드)
|
|
||||||
2. **A06_Login** — 로그인 폼 + localStorage 토큰 저장
|
|
||||||
3. **A07_Register** — 회원가입 폼 + 유효성 검사
|
|
||||||
|
|
||||||
### Week 2: 로그인 전 상세 (A02~A05, A08)
|
|
||||||
4. **A02_ProgDetail** — 기능 소개 (아코디언, 탭)
|
|
||||||
5. **A03_CompDetail** — 회사 정보
|
|
||||||
6. **A04_NewsHistory** — 뉴스 목록 + 필터
|
|
||||||
7. **A05_EduDetail** — 튜토리얼
|
|
||||||
8. **A08_Support** — 문의 양식
|
|
||||||
|
|
||||||
### Week 3: 로그인 후 기초 (B01~B03)
|
|
||||||
9. **B01_AccountDetail** — 사용자 계정
|
|
||||||
10. **B02_ProjRegister** — 프로젝트 생성
|
|
||||||
11. **B03_FileInput** — 파일 업로드 + 드래그 드롭
|
|
||||||
|
|
||||||
### Week 4~5: 워크플로우 기초 (B04~B06)
|
|
||||||
12. **B04_wf1_Surface** — 지표면 분석 (3단 레이아웃, Canvas 뷰어)
|
|
||||||
13. **B05_wf2_Route** — 경로 설계 (2D 맵)
|
|
||||||
14. **B06_wf3_ProfileCross** — 종횡단 (그래프)
|
|
||||||
|
|
||||||
### Week 6: 워크플로우 후반 (B07~B09)
|
|
||||||
15. **B07_wf4_DesignDetail** — 상세 설계
|
|
||||||
16. **B08_wf5_Quantity** — 수량 산출 (테이블)
|
|
||||||
17. **B09_wf6_Estimation** — 견적 (테이블)
|
|
||||||
|
|
||||||
### Week 7: 최종 (B10~B11)
|
|
||||||
18. **B10_Payment** — 결재
|
|
||||||
19. **B11_Status** — 상태/다운로드
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 검증 및 마무리
|
|
||||||
|
|
||||||
- **포맷팅:** 각 페이지 완성 후 `npx prettier --write` 실행
|
|
||||||
- **테스트:** 브라우저에서 각 페이지 수동 테스트 (Mock 데이터 로드 확인)
|
|
||||||
- **Git 커밋:** 페이지별 커밋 (예: `"feat: implement A01_Home page"`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 백엔드 연결 (나중)
|
|
||||||
|
|
||||||
프론트엔드 완성 후, 각 페이지별로 **선택적 API 연결:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// A06_Login_UI_Page.ts에서
|
|
||||||
// 현재: localStorage에 Mock 토큰 저장
|
|
||||||
// 나중: fetchAPI("/api/a06/login", { email, password })로 교체
|
|
||||||
```
|
|
||||||
|
|
||||||
이 전략으로 **프론트엔드 완성도를 먼저 높이고**, 백엔드는 병렬로 또는 선택적으로 구축 가능합니다.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. 참고 자료
|
|
||||||
|
|
||||||
- **design.md:** Wiza 스타일 (색상, 타이포그래피, 컴포넌트)
|
|
||||||
- **frontend.md:** UI 제약 조건 (테마 변수, 컴포넌트, i18n, 이벤트 명명)
|
|
||||||
- **ui_template/:** 3개 핵심 파일 (theme.css, locale.ts, elements.ts)
|
|
||||||
- **migration_plan.md:** 전체 5 Phase 계획 (이 계획은 Phase A~D = frontend 먼저)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**다음 단계:** Week 1 시작 → A01_Home 페이지 구현
|
|
||||||
+28
-14
@@ -19,8 +19,9 @@ my-project/
|
|||||||
│ └── structure.md # 본 파일
|
│ └── structure.md # 본 파일
|
||||||
├── A00_Common/ # 프론트엔드 공통 인프라 (부트스트랩, 라우팅, 셸)
|
├── A00_Common/ # 프론트엔드 공통 인프라 (부트스트랩, 라우팅, 셸)
|
||||||
│ ├── main.ts # 앱 진입점 (스타일 주입 → 테마 복원 → 셸 → 라우터)
|
│ ├── main.ts # 앱 진입점 (스타일 주입 → 테마 복원 → 셸 → 라우터)
|
||||||
│ ├── router.ts # 해시 기반 SPA 라우터 + 인증 가드 + 지연 로딩
|
│ ├── router.ts # 해시 기반 SPA 라우터 + 인증 가드 + 지연 로딩 (A·B 전 라우트 등록)
|
||||||
│ ├── app_shell.ts # 공통 헤더/푸터 (로고, 네비, 언어·테마·로그인 토글)
|
│ ├── app_shell.ts # 공통 헤더/푸터 (로고, 네비, 언어·테마·로그인 토글)
|
||||||
|
│ ├── b_page_scaffold.ts # B 그룹 공통 스캐폴드 (헤더+준비중 안내 / 워크플로우 셸 래퍼)
|
||||||
│ └── vite-env.d.ts # Vite 클라이언트 타입 선언
|
│ └── vite-env.d.ts # Vite 클라이언트 타입 선언
|
||||||
├── common_util/ # 공통 유틸리티 (공통 기능 코드)
|
├── common_util/ # 공통 유틸리티 (공통 기능 코드)
|
||||||
│ ├── common_util_validate.ts # 프론트 1차 유효성 검사 (이메일/빈값/최소길이)
|
│ ├── common_util_validate.ts # 프론트 1차 유효성 검사 (이메일/빈값/최소길이)
|
||||||
@@ -76,19 +77,32 @@ my-project/
|
|||||||
│ ├── A08_Support_UI_Page.ts
|
│ ├── A08_Support_UI_Page.ts
|
||||||
│ └── A08_Support_UI_Style.css
|
│ └── A08_Support_UI_Style.css
|
||||||
│
|
│
|
||||||
├── B01_AccountDetail/ # 로그인 후 01: 계정 상세
|
│ # ※ B01·B02 본문 완성 / B03~B11 헤더·셸만 (본문은 0_old 참고 후 구체화)
|
||||||
├── B02_ProjRegister/ # 로그인 후 02: 프로젝트 등록
|
├── B01_AccountDetail/ # 로그인 후 01: 계정 상세 (기본정보 수정 + 비밀번호 변경)
|
||||||
├── B03_FileInput/ # 로그인 후 03: 파일 입력
|
│ ├── B01_AccountDetail_UI_Page.ts
|
||||||
├── B04_wf1_Surface/ # 로그인 후 04: 1차 workflow (지표면 모델 분석)
|
│ └── B01_AccountDetail_UI_Style.css
|
||||||
│ ├── B04_wf1_Surface_UI_View.ts
|
├── B02_ProjRegister/ # 로그인 후 02: 프로젝트 등록 (기본정보 입력 폼)
|
||||||
│ └── B04_wf1_Surface_Calc_Mesh.py
|
│ ├── B02_ProjRegister_UI_Page.ts
|
||||||
├── B05_wf2_Route/ # 로그인 후 05: 2차 workflow (경로설계)
|
│ └── B02_ProjRegister_UI_Style.css
|
||||||
├── B06_wf3_ProfileCross/ # 로그인 후 06: 3차 workflow (종횡단 생성)
|
├── B03_FileInput/ # 로그인 후 03: 파일 입력 (헤더+준비중 안내)
|
||||||
├── B07_wf4_DesignDetail/ # 로그인 후 07: 4차 workflow (상세설계)
|
│ ├── B03_FileInput_UI_Page.ts
|
||||||
├── B08_wf5_Quantity/ # 로그인 후 08: 5차 workflow (수량 산출)
|
│ └── B03_FileInput_UI_Style.css
|
||||||
├── B09_wf6_Estimation/ # 로그인 후 09: 6차 workflow (견적/문서)
|
├── B04_wf1_Surface/ # 로그인 후 04: 1차 workflow (지표면 모델 분석) — 워크플로우 셸
|
||||||
├── B10_Payment/ # 로그인 후 10: 결재 페이지
|
│ └── B04_wf1_Surface_UI_Page.ts
|
||||||
└── B11_Status/ # 로그인 후 11: 상태 출력 페이지 (결재완료/다운로드 등)
|
├── B05_wf2_Route/ # 로그인 후 05: 2차 workflow (경로설계) — 워크플로우 셸
|
||||||
|
│ └── B05_wf2_Route_UI_Page.ts
|
||||||
|
├── B06_wf3_ProfileCross/ # 로그인 후 06: 3차 workflow (종횡단 생성) — 워크플로우 셸
|
||||||
|
│ └── B06_wf3_ProfileCross_UI_Page.ts
|
||||||
|
├── B07_wf4_DesignDetail/ # 로그인 후 07: 4차 workflow (상세설계) — 워크플로우 셸
|
||||||
|
│ └── B07_wf4_DesignDetail_UI_Page.ts
|
||||||
|
├── B08_wf5_Quantity/ # 로그인 후 08: 5차 workflow (수량 산출) — 워크플로우 셸
|
||||||
|
│ └── B08_wf5_Quantity_UI_Page.ts
|
||||||
|
├── B09_wf6_Estimation/ # 로그인 후 09: 6차 workflow (견적/문서) — 워크플로우 셸
|
||||||
|
│ └── B09_wf6_Estimation_UI_Page.ts
|
||||||
|
├── B10_Payment/ # 로그인 후 10: 결재 페이지 (헤더+준비중 안내)
|
||||||
|
│ └── B10_Payment_UI_Page.ts
|
||||||
|
└── B11_Status/ # 로그인 후 11: 상태 출력 페이지 (헤더+준비중 안내)
|
||||||
|
└── B11_Status_UI_Page.ts
|
||||||
│
|
│
|
||||||
├── main.py # FastAPI 애플리케이션 진입점
|
├── main.py # FastAPI 애플리케이션 진입점
|
||||||
├── requirements.txt # Python 의존성 (pip)
|
├── requirements.txt # Python 의존성 (pip)
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ venv/
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
# Node / Frontend (루트 기반 구조)
|
# Node / Frontend (루트 기반 구조)
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* b_page_scaffold.ts
|
||||||
|
* B 그룹 페이지 공통 스캐폴드 — "헤더 + 준비 중 안내" 및 워크플로우 셸 래퍼
|
||||||
|
*
|
||||||
|
* B03~B06 등 본문 미구현 페이지에서 헤더/푸터(공통 셸)를 제외한 콘텐츠 영역을
|
||||||
|
* 일관된 형태로 채우기 위한 헬퍼. 실제 본문은 0_old 참고하여 추후 구체화.
|
||||||
|
*
|
||||||
|
* 제약 준수 (frontend.md):
|
||||||
|
* - 문구는 호출측에서 locale 참조 후 문자열로 전달 (§3).
|
||||||
|
* - 공통 워크플로우 셸(createWorkflowShell) 재사용 (§2 3단 레이아웃).
|
||||||
|
* - 색상/간격은 theme.css 변수만 사용 (§1).
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||||
|
import { createWorkflowShell } from "@ui/ui_template_elements";
|
||||||
|
|
||||||
|
/** locale 헬퍼 */
|
||||||
|
function L(key: keyof typeof ui_locales): string {
|
||||||
|
return ui_locales[key][currentLanguageIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCAFFOLD_STYLE_ID = "b-page-scaffold-style";
|
||||||
|
|
||||||
|
/** "준비 중" 안내 블록 생성 (B_Content_Pending 문구 표시) */
|
||||||
|
function buildPendingBlock(): HTMLDivElement {
|
||||||
|
const block = document.createElement("div");
|
||||||
|
block.className = "b-pending";
|
||||||
|
const icon = document.createElement("div");
|
||||||
|
icon.className = "b-pending__icon";
|
||||||
|
icon.textContent = "🚧";
|
||||||
|
const msg = document.createElement("p");
|
||||||
|
msg.className = "b-pending__msg";
|
||||||
|
msg.textContent = L("B_Content_Pending");
|
||||||
|
block.append(icon, msg);
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
* 1. 단순 헤더 + 준비 중 안내 (비(非)워크플로우 페이지: B03 등)
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
export interface PendingContentOptions {
|
||||||
|
/** 페이지 루트 클래스 (예: "b03-file") */
|
||||||
|
pageClass: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderPendingContent(root: HTMLElement, opts: PendingContentOptions): void {
|
||||||
|
injectScaffoldStyles();
|
||||||
|
const page = document.createElement("div");
|
||||||
|
page.className = `b-scaffold ${opts.pageClass}`;
|
||||||
|
|
||||||
|
const header = document.createElement("div");
|
||||||
|
header.className = "b-scaffold__header";
|
||||||
|
const title = document.createElement("h1");
|
||||||
|
title.className = "b-scaffold__title";
|
||||||
|
title.textContent = opts.title;
|
||||||
|
const subtitle = document.createElement("p");
|
||||||
|
subtitle.className = "b-scaffold__subtitle";
|
||||||
|
subtitle.textContent = opts.subtitle;
|
||||||
|
header.append(title, subtitle);
|
||||||
|
|
||||||
|
page.append(header, buildPendingBlock());
|
||||||
|
root.append(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
* 2. 워크플로우 셸(3단 레이아웃) + 준비 중 안내 (B04~B06 등)
|
||||||
|
* 좌측 패널·우측 뷰어 모두 준비 중 상태로 채운다.
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
export interface PendingWorkflowOptions {
|
||||||
|
/** 상단 페이지 타이틀 (i18n 결과) */
|
||||||
|
title: string;
|
||||||
|
/** 진행 단계 라벨 배열 (i18n 결과) */
|
||||||
|
steps: string[];
|
||||||
|
/** 현재 활성 단계 인덱스 */
|
||||||
|
activeStep: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderPendingWorkflow(root: HTMLElement, opts: PendingWorkflowOptions): void {
|
||||||
|
injectScaffoldStyles();
|
||||||
|
const shell = createWorkflowShell({
|
||||||
|
title: opts.title,
|
||||||
|
steps: opts.steps,
|
||||||
|
activeStep: opts.activeStep,
|
||||||
|
});
|
||||||
|
shell.root.classList.add("b-scaffold-wf");
|
||||||
|
|
||||||
|
// 좌측/우측 모두 준비 중 안내로 채움 (본문은 추후 구체화)
|
||||||
|
shell.leftPanel.append(buildPendingBlock());
|
||||||
|
shell.rightArea.append(buildPendingBlock());
|
||||||
|
|
||||||
|
root.append(shell.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 워크플로우 스텝 라벨 6종 (locale 결과) — B04~B09 공용.
|
||||||
|
* 기존 WF_Step_* 키를 재사용 (중복 등록 방지, frontend.md §3). */
|
||||||
|
export function workflowSteps(): string[] {
|
||||||
|
return [
|
||||||
|
L("WF_Step_Surface"),
|
||||||
|
L("WF_Step_Route"),
|
||||||
|
L("WF_Step_ProfileCross"),
|
||||||
|
L("WF_Step_DesignDetail"),
|
||||||
|
L("WF_Step_Quantity"),
|
||||||
|
L("WF_Step_Estimation"),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
* 스타일 주입 (theme.css 변수만 사용)
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
const SCAFFOLD_CSS = `
|
||||||
|
.b-scaffold {
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--spacing-40) var(--spacing-24);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-24);
|
||||||
|
}
|
||||||
|
.b-scaffold__header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-8);
|
||||||
|
}
|
||||||
|
.b-scaffold__title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: var(--text-heading);
|
||||||
|
color: var(--color-plum-velvet);
|
||||||
|
}
|
||||||
|
.b-scaffold__subtitle {
|
||||||
|
font-size: var(--text-body-sm);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
.b-scaffold-wf { height: calc(100vh - 64px - 56px); }
|
||||||
|
|
||||||
|
.b-pending {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spacing-16);
|
||||||
|
min-height: 240px;
|
||||||
|
padding: var(--spacing-40);
|
||||||
|
border: 1px dashed var(--color-border);
|
||||||
|
border-radius: var(--radius-cards);
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
.b-pending__icon { font-size: 40px; line-height: 1; }
|
||||||
|
.b-pending__msg { font-size: var(--text-body-sm); text-align: center; }
|
||||||
|
`;
|
||||||
|
|
||||||
|
function injectScaffoldStyles(): void {
|
||||||
|
if (document.getElementById(SCAFFOLD_STYLE_ID)) return;
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.id = SCAFFOLD_STYLE_ID;
|
||||||
|
style.textContent = SCAFFOLD_CSS;
|
||||||
|
document.head.append(style);
|
||||||
|
}
|
||||||
@@ -35,6 +35,29 @@ const routeTable: Partial<Record<RoutePath, () => Promise<PageRenderer>>> = {
|
|||||||
(await import("../A07_Register/A07_Register_UI_Page")).renderA07Register,
|
(await import("../A07_Register/A07_Register_UI_Page")).renderA07Register,
|
||||||
[ROUTES.A08_SUPPORT]: async () =>
|
[ROUTES.A08_SUPPORT]: async () =>
|
||||||
(await import("../A08_Support/A08_Support_UI_Page")).renderA08Support,
|
(await import("../A08_Support/A08_Support_UI_Page")).renderA08Support,
|
||||||
|
// 로그인 후 (B 그룹)
|
||||||
|
[ROUTES.B01_ACCOUNT]: async () =>
|
||||||
|
(await import("../B01_AccountDetail/B01_AccountDetail_UI_Page")).renderB01AccountDetail,
|
||||||
|
[ROUTES.B02_PROJ_REGISTER]: async () =>
|
||||||
|
(await import("../B02_ProjRegister/B02_ProjRegister_UI_Page")).renderB02ProjRegister,
|
||||||
|
[ROUTES.B03_FILE_INPUT]: async () =>
|
||||||
|
(await import("../B03_FileInput/B03_FileInput_UI_Page")).renderB03FileInput,
|
||||||
|
[ROUTES.B04_WF1_SURFACE]: async () =>
|
||||||
|
(await import("../B04_wf1_Surface/B04_wf1_Surface_UI_Page")).renderB04Surface,
|
||||||
|
[ROUTES.B05_WF2_ROUTE]: async () =>
|
||||||
|
(await import("../B05_wf2_Route/B05_wf2_Route_UI_Page")).renderB05Route,
|
||||||
|
[ROUTES.B06_WF3_PROFILE_CROSS]: async () =>
|
||||||
|
(await import("../B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Page")).renderB06ProfileCross,
|
||||||
|
[ROUTES.B07_WF4_DESIGN_DETAIL]: async () =>
|
||||||
|
(await import("../B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Page")).renderB07DesignDetail,
|
||||||
|
[ROUTES.B08_WF5_QUANTITY]: async () =>
|
||||||
|
(await import("../B08_wf5_Quantity/B08_wf5_Quantity_UI_Page")).renderB08Quantity,
|
||||||
|
[ROUTES.B09_WF6_ESTIMATION]: async () =>
|
||||||
|
(await import("../B09_wf6_Estimation/B09_wf6_Estimation_UI_Page")).renderB09Estimation,
|
||||||
|
[ROUTES.B10_PAYMENT]: async () =>
|
||||||
|
(await import("../B10_Payment/B10_Payment_UI_Page")).renderB10Payment,
|
||||||
|
[ROUTES.B11_STATUS]: async () =>
|
||||||
|
(await import("../B11_Status/B11_Status_UI_Page")).renderB11Status,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 로그인 여부 (토큰 존재 확인) */
|
/** 로그인 여부 (토큰 존재 확인) */
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B01_AccountDetail_UI_Page.ts
|
||||||
|
* 로그인 후 01: 계정 상세 (기본 정보 수정 + 비밀번호 변경)
|
||||||
|
*
|
||||||
|
* 제약 준수 (frontend.md):
|
||||||
|
* - 모든 문구는 ui_template_locale.ts 참조 (하드코딩 금지, §3).
|
||||||
|
* - 색상/간격은 theme.css 변수 + 공통 컴포넌트만 사용 (§1, §2).
|
||||||
|
* - 이벤트 핸들러는 onB01_[기능]_[액션] 명명 (§4).
|
||||||
|
* - 백엔드 전송 전 1차 유효성 검사 (§4).
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||||
|
import { createButton, createInputField, createCard, showToast } from "@ui/ui_template_elements";
|
||||||
|
import { isBlank } from "@util/common_util_validate";
|
||||||
|
import "./B01_AccountDetail_UI_Style.css";
|
||||||
|
|
||||||
|
/** locale 헬퍼 */
|
||||||
|
function L(key: keyof typeof ui_locales): string {
|
||||||
|
return ui_locales[key][currentLanguageIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 비밀번호 최소 길이 (A07 회원가입과 동일 기준) */
|
||||||
|
const PASSWORD_MIN_LENGTH = 8;
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
* 페이지 진입점
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
export function renderB01AccountDetail(root: HTMLElement): void {
|
||||||
|
const page = document.createElement("div");
|
||||||
|
page.className = "b01-account";
|
||||||
|
|
||||||
|
const header = document.createElement("div");
|
||||||
|
header.className = "b01-account__header";
|
||||||
|
const title = document.createElement("h1");
|
||||||
|
title.className = "b01-account__title";
|
||||||
|
title.textContent = L("B01_Account_Title");
|
||||||
|
const subtitle = document.createElement("p");
|
||||||
|
subtitle.className = "b01-account__subtitle";
|
||||||
|
subtitle.textContent = L("B01_Account_Subtitle");
|
||||||
|
header.append(title, subtitle);
|
||||||
|
|
||||||
|
page.append(header, buildProfileCard(), buildSecurityCard());
|
||||||
|
root.append(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
* 기본 정보 카드 (회사/이름/이메일/연락처)
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
function buildProfileCard(): HTMLDivElement {
|
||||||
|
// 회사명·이메일은 읽기 전용(가입 시 확정), 이름·연락처만 수정 가능
|
||||||
|
const companyField = createInputField({
|
||||||
|
label: L("B01_Account_Field_Company"),
|
||||||
|
value: "",
|
||||||
|
});
|
||||||
|
companyField.input.readOnly = true;
|
||||||
|
companyField.input.classList.add("b01-account__readonly");
|
||||||
|
|
||||||
|
const emailField = createInputField({
|
||||||
|
label: L("B01_Account_Field_Email"),
|
||||||
|
type: "email",
|
||||||
|
value: "",
|
||||||
|
});
|
||||||
|
emailField.input.readOnly = true;
|
||||||
|
emailField.input.classList.add("b01-account__readonly");
|
||||||
|
|
||||||
|
const nameField = createInputField({
|
||||||
|
label: L("B01_Account_Field_Name"),
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const phoneField = createInputField({
|
||||||
|
label: L("B01_Account_Field_Phone"),
|
||||||
|
placeholder: L("B01_Account_Field_Phone_Placeholder"),
|
||||||
|
type: "text",
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveBtn = createButton({
|
||||||
|
label: L("B01_Account_Save_Profile"),
|
||||||
|
variant: "filled",
|
||||||
|
onClick: onB01_Profile_Save_Click,
|
||||||
|
});
|
||||||
|
saveBtn.classList.add("b01-account__submit");
|
||||||
|
|
||||||
|
function onB01_Profile_Save_Click(): void {
|
||||||
|
nameField.setError();
|
||||||
|
if (isBlank(nameField.input.value)) {
|
||||||
|
nameField.setError(L("B01_Account_Error_Required"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO: PATCH /api/b01/profile 연동 (로딩 오버레이 + 성공/실패 처리)
|
||||||
|
showToast(L("B01_Account_Success_Profile"), "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
const grid = document.createElement("div");
|
||||||
|
grid.className = "b01-account__grid";
|
||||||
|
grid.append(companyField.root, emailField.root, nameField.root, phoneField.root);
|
||||||
|
|
||||||
|
return createCard({
|
||||||
|
title: L("B01_Account_Section_Profile"),
|
||||||
|
body: [grid, saveBtn],
|
||||||
|
raised: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
* 보안 카드 (비밀번호 변경)
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
function buildSecurityCard(): HTMLDivElement {
|
||||||
|
const currentPwField = createInputField({
|
||||||
|
label: L("B01_Account_Field_CurrentPw"),
|
||||||
|
type: "password",
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const newPwField = createInputField({
|
||||||
|
label: L("B01_Account_Field_NewPw"),
|
||||||
|
type: "password",
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const confirmPwField = createInputField({
|
||||||
|
label: L("B01_Account_Field_ConfirmPw"),
|
||||||
|
type: "password",
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveBtn = createButton({
|
||||||
|
label: L("B01_Account_Save_Password"),
|
||||||
|
variant: "ghost",
|
||||||
|
onClick: onB01_Password_Save_Click,
|
||||||
|
});
|
||||||
|
saveBtn.classList.add("b01-account__submit");
|
||||||
|
|
||||||
|
function onB01_Password_Save_Click(): void {
|
||||||
|
currentPwField.setError();
|
||||||
|
newPwField.setError();
|
||||||
|
confirmPwField.setError();
|
||||||
|
|
||||||
|
const current = currentPwField.input.value;
|
||||||
|
const next = newPwField.input.value;
|
||||||
|
const confirm = confirmPwField.input.value;
|
||||||
|
|
||||||
|
// 1차 유효성: 빈 값 검사
|
||||||
|
let hasError = false;
|
||||||
|
if (isBlank(current)) {
|
||||||
|
currentPwField.setError(L("B01_Account_Error_Required"));
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
if (isBlank(next)) {
|
||||||
|
newPwField.setError(L("B01_Account_Error_Required"));
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
if (isBlank(confirm)) {
|
||||||
|
confirmPwField.setError(L("B01_Account_Error_Required"));
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
if (hasError) return;
|
||||||
|
|
||||||
|
// 길이 검사
|
||||||
|
if (next.length < PASSWORD_MIN_LENGTH) {
|
||||||
|
newPwField.setError(L("B01_Account_Error_PwLength"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 일치 검사
|
||||||
|
if (next !== confirm) {
|
||||||
|
confirmPwField.setError(L("B01_Account_Error_PwMismatch"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: PATCH /api/b01/password 연동 (로딩 오버레이 + 성공/실패 처리)
|
||||||
|
currentPwField.input.value = "";
|
||||||
|
newPwField.input.value = "";
|
||||||
|
confirmPwField.input.value = "";
|
||||||
|
showToast(L("B01_Account_Success_Password"), "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
const grid = document.createElement("div");
|
||||||
|
grid.className = "b01-account__grid";
|
||||||
|
grid.append(currentPwField.root, newPwField.root, confirmPwField.root);
|
||||||
|
|
||||||
|
return createCard({
|
||||||
|
title: L("B01_Account_Section_Security"),
|
||||||
|
body: [grid, saveBtn],
|
||||||
|
raised: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B01_AccountDetail_UI_Style.css
|
||||||
|
* 계정 상세 페이지 전용 스타일 (theme.css 변수만 사용)
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
.b01-account {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--spacing-40) var(--spacing-24);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.b01-account__header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.b01-account__title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: var(--text-heading);
|
||||||
|
color: var(--color-plum-velvet);
|
||||||
|
}
|
||||||
|
|
||||||
|
.b01-account__subtitle {
|
||||||
|
font-size: var(--text-body-sm);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2열 필드 그리드 (좁은 화면에서는 1열) */
|
||||||
|
.b01-account__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: var(--spacing-16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.b01-account__readonly {
|
||||||
|
background-color: var(--color-paper);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.b01-account__submit {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-top: var(--spacing-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.b01-account__grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B02_ProjRegister_UI_Page.ts
|
||||||
|
* 로그인 후 02: 프로젝트 등록 (신규 임도 설계 프로젝트 기본 정보 입력)
|
||||||
|
*
|
||||||
|
* 제약 준수 (frontend.md):
|
||||||
|
* - 모든 문구는 ui_template_locale.ts 참조 (하드코딩 금지, §3).
|
||||||
|
* - 색상/간격은 theme.css 변수 + 공통 컴포넌트만 사용 (§1, §2).
|
||||||
|
* - 이벤트 핸들러는 onB02_[기능]_[액션] 명명 (§4).
|
||||||
|
* - 백엔드 전송 전 1차 유효성 검사 (§4).
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||||
|
import { createButton, createInputField, createCard, showToast } from "@ui/ui_template_elements";
|
||||||
|
import { isBlank } from "@util/common_util_validate";
|
||||||
|
import { navigateTo } from "../A00_Common/router";
|
||||||
|
import { ROUTES } from "@config/config_frontend";
|
||||||
|
import "./B02_ProjRegister_UI_Style.css";
|
||||||
|
|
||||||
|
/** locale 헬퍼 */
|
||||||
|
function L(key: keyof typeof ui_locales): string {
|
||||||
|
return ui_locales[key][currentLanguageIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** select 필드 핸들 (공통 .ui-field/.ui-input 스타일 상속) */
|
||||||
|
interface SelectFieldHandle {
|
||||||
|
root: HTMLDivElement;
|
||||||
|
select: HTMLSelectElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 드롭다운 필드 생성 (공통 컴포넌트에 없어 로컬 정의, 공통 스타일 상속) */
|
||||||
|
function createSelectField(
|
||||||
|
label: string,
|
||||||
|
options: { value: string; text: string }[],
|
||||||
|
): SelectFieldHandle {
|
||||||
|
const root = document.createElement("div");
|
||||||
|
root.className = "ui-field";
|
||||||
|
|
||||||
|
const labelEl = document.createElement("label");
|
||||||
|
labelEl.className = "ui-field__label";
|
||||||
|
labelEl.textContent = label;
|
||||||
|
|
||||||
|
const select = document.createElement("select");
|
||||||
|
select.className = "ui-input b02-proj__select";
|
||||||
|
for (const opt of options) {
|
||||||
|
const optionEl = document.createElement("option");
|
||||||
|
optionEl.value = opt.value;
|
||||||
|
optionEl.textContent = opt.text;
|
||||||
|
select.append(optionEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
root.append(labelEl, select);
|
||||||
|
return { root, select };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
* 페이지 진입점
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
export function renderB02ProjRegister(root: HTMLElement): void {
|
||||||
|
const page = document.createElement("div");
|
||||||
|
page.className = "b02-proj";
|
||||||
|
|
||||||
|
const header = document.createElement("div");
|
||||||
|
header.className = "b02-proj__header";
|
||||||
|
const title = document.createElement("h1");
|
||||||
|
title.className = "b02-proj__title";
|
||||||
|
title.textContent = L("B02_Proj_Title");
|
||||||
|
const subtitle = document.createElement("p");
|
||||||
|
subtitle.className = "b02-proj__subtitle";
|
||||||
|
subtitle.textContent = L("B02_Proj_Subtitle");
|
||||||
|
header.append(title, subtitle);
|
||||||
|
|
||||||
|
// 입력 필드
|
||||||
|
const nameField = createInputField({
|
||||||
|
label: L("B02_Proj_Field_Name"),
|
||||||
|
placeholder: L("B02_Proj_Field_Name_Placeholder"),
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const regionField = createInputField({
|
||||||
|
label: L("B02_Proj_Field_Region"),
|
||||||
|
placeholder: L("B02_Proj_Field_Region_Placeholder"),
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const roadTypeField = createSelectField(L("B02_Proj_Field_RoadType"), [
|
||||||
|
{ value: "main", text: L("B02_Proj_RoadType_Main") },
|
||||||
|
{ value: "branch", text: L("B02_Proj_RoadType_Branch") },
|
||||||
|
{ value: "fire", text: L("B02_Proj_RoadType_Fire") },
|
||||||
|
{ value: "stream", text: L("B02_Proj_RoadType_Stream") },
|
||||||
|
]);
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const yearField = createInputField({
|
||||||
|
label: L("B02_Proj_Field_Year"),
|
||||||
|
type: "number",
|
||||||
|
value: String(currentYear),
|
||||||
|
min: 2000,
|
||||||
|
max: currentYear + 5,
|
||||||
|
});
|
||||||
|
const lengthField = createInputField({
|
||||||
|
label: L("B02_Proj_Field_Length"),
|
||||||
|
placeholder: L("B02_Proj_Field_Length_Placeholder"),
|
||||||
|
type: "number",
|
||||||
|
min: 0,
|
||||||
|
});
|
||||||
|
const memoField = createInputField({
|
||||||
|
label: L("B02_Proj_Field_Memo"),
|
||||||
|
placeholder: L("B02_Proj_Field_Memo_Placeholder"),
|
||||||
|
type: "text",
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitBtn = createButton({
|
||||||
|
label: L("B02_Proj_Submit"),
|
||||||
|
variant: "filled",
|
||||||
|
onClick: onB02_Proj_Submit_Click,
|
||||||
|
});
|
||||||
|
submitBtn.classList.add("b02-proj__submit");
|
||||||
|
|
||||||
|
function onB02_Proj_Submit_Click(): void {
|
||||||
|
nameField.setError();
|
||||||
|
regionField.setError();
|
||||||
|
|
||||||
|
// 1차 유효성: 필수값 검사
|
||||||
|
let hasError = false;
|
||||||
|
if (isBlank(nameField.input.value)) {
|
||||||
|
nameField.setError(L("B02_Proj_Error_Required"));
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
if (isBlank(regionField.input.value)) {
|
||||||
|
regionField.setError(L("B02_Proj_Error_Required"));
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
if (hasError) return;
|
||||||
|
|
||||||
|
// TODO: POST /api/b02/project 연동 (로딩 오버레이 + 성공/실패 처리).
|
||||||
|
// 성공 시 반환된 프로젝트 ID로 storage 폴더 생성 후 파일 입력으로 이동.
|
||||||
|
showToast(L("B02_Proj_Success"), "success");
|
||||||
|
navigateTo(ROUTES.B03_FILE_INPUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
const grid = document.createElement("div");
|
||||||
|
grid.className = "b02-proj__grid";
|
||||||
|
grid.append(
|
||||||
|
nameField.root,
|
||||||
|
regionField.root,
|
||||||
|
roadTypeField.root,
|
||||||
|
yearField.root,
|
||||||
|
lengthField.root,
|
||||||
|
memoField.root,
|
||||||
|
);
|
||||||
|
|
||||||
|
const card = createCard({ body: [grid, submitBtn], raised: true });
|
||||||
|
page.append(header, card);
|
||||||
|
root.append(page);
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B02_ProjRegister_UI_Style.css
|
||||||
|
* 프로젝트 등록 페이지 전용 스타일 (theme.css 변수만 사용)
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
.b02-proj {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--spacing-40) var(--spacing-24);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.b02-proj__header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.b02-proj__title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: var(--text-heading);
|
||||||
|
color: var(--color-plum-velvet);
|
||||||
|
}
|
||||||
|
|
||||||
|
.b02-proj__subtitle {
|
||||||
|
font-size: var(--text-body-sm);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2열 필드 그리드 (좁은 화면에서는 1열) */
|
||||||
|
.b02-proj__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: var(--spacing-16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* select는 공통 .ui-input 상속 + 드롭다운 화살표 여백 확보 */
|
||||||
|
.b02-proj__select {
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, transparent 50%, var(--color-slate) 50%),
|
||||||
|
linear-gradient(135deg, var(--color-slate) 50%, transparent 50%);
|
||||||
|
background-position:
|
||||||
|
calc(100% - 18px) 50%,
|
||||||
|
calc(100% - 13px) 50%;
|
||||||
|
background-size:
|
||||||
|
5px 5px,
|
||||||
|
5px 5px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
padding-right: var(--spacing-32);
|
||||||
|
}
|
||||||
|
|
||||||
|
.b02-proj__submit {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-top: var(--spacing-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.b02-proj__grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B03_FileInput_UI_Page.ts
|
||||||
|
* 로그인 후 03: 파일 입력 (지형·포인트클라우드·도면 업로드)
|
||||||
|
*
|
||||||
|
* ⚠️ 본문(업로드 드롭존/파일 목록)은 준비 중 — 헤더/안내만 구현.
|
||||||
|
* 실제 업로드 UI는 0_old 참고하여 추후 구체화.
|
||||||
|
*
|
||||||
|
* 제약 준수 (frontend.md): 문구는 locale 참조(§3), 공통 컴포넌트 사용(§2).
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||||
|
import { renderPendingContent } from "../A00_Common/b_page_scaffold";
|
||||||
|
import "./B03_FileInput_UI_Style.css";
|
||||||
|
|
||||||
|
/** locale 헬퍼 */
|
||||||
|
function L(key: keyof typeof ui_locales): string {
|
||||||
|
return ui_locales[key][currentLanguageIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
* 페이지 진입점
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
export function renderB03FileInput(root: HTMLElement): void {
|
||||||
|
renderPendingContent(root, {
|
||||||
|
pageClass: "b03-file",
|
||||||
|
title: L("B03_File_Title"),
|
||||||
|
subtitle: L("B03_File_Subtitle"),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B03_FileInput_UI_Style.css
|
||||||
|
* 파일 입력 페이지 전용 스타일 (theme.css 변수만 사용)
|
||||||
|
* ⚠️ 본문(업로드 드롭존) 준비 중 — 현재는 공통 스캐폴드 스타일에 위임.
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
/* 본문 구현 시 .b03-file 하위에 업로드 드롭존/파일 목록 스타일 추가 예정 */
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B04_wf1_Surface_UI_Page.ts
|
||||||
|
* 로그인 후 04: 1차 워크플로우 (지표면 모델 분석)
|
||||||
|
*
|
||||||
|
* ⚠️ 좌측 입력 패널 / 우측 WebCAD 뷰어 본문은 준비 중 — 워크플로우 셸(헤더+
|
||||||
|
* 스텝바 = 3단 레이아웃)만 구성. 실제 본문은 0_old 참고하여 추후 구체화.
|
||||||
|
*
|
||||||
|
* 제약 준수 (frontend.md §2 3단 레이아웃): createWorkflowShell 재사용.
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||||
|
import { renderPendingWorkflow, workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||||
|
|
||||||
|
/** locale 헬퍼 */
|
||||||
|
function L(key: keyof typeof ui_locales): string {
|
||||||
|
return ui_locales[key][currentLanguageIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
* 페이지 진입점
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
export function renderB04Surface(root: HTMLElement): void {
|
||||||
|
renderPendingWorkflow(root, {
|
||||||
|
title: L("B04_Surface_Title"),
|
||||||
|
steps: workflowSteps(),
|
||||||
|
activeStep: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B05_wf2_Route_UI_Page.ts
|
||||||
|
* 로그인 후 05: 2차 워크플로우 (경로 설계)
|
||||||
|
*
|
||||||
|
* ⚠️ 좌측 입력 패널 / 우측 WebCAD 뷰어 본문은 준비 중 — 워크플로우 셸(헤더+
|
||||||
|
* 스텝바 = 3단 레이아웃)만 구성. 실제 본문은 0_old 참고하여 추후 구체화.
|
||||||
|
*
|
||||||
|
* 제약 준수 (frontend.md §2 3단 레이아웃): createWorkflowShell 재사용.
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||||
|
import { renderPendingWorkflow, workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||||
|
|
||||||
|
/** locale 헬퍼 */
|
||||||
|
function L(key: keyof typeof ui_locales): string {
|
||||||
|
return ui_locales[key][currentLanguageIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
* 페이지 진입점
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
export function renderB05Route(root: HTMLElement): void {
|
||||||
|
renderPendingWorkflow(root, {
|
||||||
|
title: L("B05_Route_Title"),
|
||||||
|
steps: workflowSteps(),
|
||||||
|
activeStep: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B06_wf3_ProfileCross_UI_Page.ts
|
||||||
|
* 로그인 후 06: 3차 워크플로우 (종·횡단 생성)
|
||||||
|
*
|
||||||
|
* ⚠️ 좌측 입력 패널 / 우측 WebCAD 뷰어 본문은 준비 중 — 워크플로우 셸(헤더+
|
||||||
|
* 스텝바 = 3단 레이아웃)만 구성. 실제 본문은 0_old 참고하여 추후 구체화.
|
||||||
|
*
|
||||||
|
* 제약 준수 (frontend.md §2 3단 레이아웃): createWorkflowShell 재사용.
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||||
|
import { renderPendingWorkflow, workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||||
|
|
||||||
|
/** locale 헬퍼 */
|
||||||
|
function L(key: keyof typeof ui_locales): string {
|
||||||
|
return ui_locales[key][currentLanguageIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
* 페이지 진입점
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
export function renderB06ProfileCross(root: HTMLElement): void {
|
||||||
|
renderPendingWorkflow(root, {
|
||||||
|
title: L("B06_Profile_Title"),
|
||||||
|
steps: workflowSteps(),
|
||||||
|
activeStep: 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B07_wf4_DesignDetail_UI_Page.ts
|
||||||
|
* 로그인 후 07: 4차 워크플로우 (상세 설계)
|
||||||
|
*
|
||||||
|
* ⚠️ 본문 준비 중 — 워크플로우 셸(헤더+스텝바)만 구성. 추후 구체화.
|
||||||
|
* 제약 준수 (frontend.md §2 3단 레이아웃): createWorkflowShell 재사용.
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||||
|
import { renderPendingWorkflow, workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||||
|
|
||||||
|
/** locale 헬퍼 */
|
||||||
|
function L(key: keyof typeof ui_locales): string {
|
||||||
|
return ui_locales[key][currentLanguageIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
* 페이지 진입점
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
export function renderB07DesignDetail(root: HTMLElement): void {
|
||||||
|
renderPendingWorkflow(root, {
|
||||||
|
title: L("B07_Design_Title"),
|
||||||
|
steps: workflowSteps(),
|
||||||
|
activeStep: 3,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B08_wf5_Quantity_UI_Page.ts
|
||||||
|
* 로그인 후 08: 5차 워크플로우 (수량 산출)
|
||||||
|
*
|
||||||
|
* ⚠️ 본문 준비 중 — 워크플로우 셸(헤더+스텝바)만 구성. 추후 구체화.
|
||||||
|
* 제약 준수 (frontend.md §2 3단 레이아웃): createWorkflowShell 재사용.
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||||
|
import { renderPendingWorkflow, workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||||
|
|
||||||
|
/** locale 헬퍼 */
|
||||||
|
function L(key: keyof typeof ui_locales): string {
|
||||||
|
return ui_locales[key][currentLanguageIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
* 페이지 진입점
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
export function renderB08Quantity(root: HTMLElement): void {
|
||||||
|
renderPendingWorkflow(root, {
|
||||||
|
title: L("B08_Quantity_Title"),
|
||||||
|
steps: workflowSteps(),
|
||||||
|
activeStep: 4,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B09_wf6_Estimation_UI_Page.ts
|
||||||
|
* 로그인 후 09: 6차 워크플로우 (견적·문서)
|
||||||
|
*
|
||||||
|
* ⚠️ 본문 준비 중 — 워크플로우 셸(헤더+스텝바)만 구성. 추후 구체화.
|
||||||
|
* 제약 준수 (frontend.md §2 3단 레이아웃): createWorkflowShell 재사용.
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||||
|
import { renderPendingWorkflow, workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||||
|
|
||||||
|
/** locale 헬퍼 */
|
||||||
|
function L(key: keyof typeof ui_locales): string {
|
||||||
|
return ui_locales[key][currentLanguageIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
* 페이지 진입점
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
export function renderB09Estimation(root: HTMLElement): void {
|
||||||
|
renderPendingWorkflow(root, {
|
||||||
|
title: L("B09_Estimation_Title"),
|
||||||
|
steps: workflowSteps(),
|
||||||
|
activeStep: 5,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B10_Payment_UI_Page.ts
|
||||||
|
* 로그인 후 10: 결재 페이지 (견적 확인 + 결재 진행)
|
||||||
|
*
|
||||||
|
* ⚠️ 본문(견적 요약/결재 수단) 준비 중 — 헤더/안내만 구성. 추후 구체화.
|
||||||
|
* 제약 준수 (frontend.md): 문구는 locale 참조(§3), 공통 스캐폴드 사용(§2).
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||||
|
import { renderPendingContent } from "../A00_Common/b_page_scaffold";
|
||||||
|
|
||||||
|
/** locale 헬퍼 */
|
||||||
|
function L(key: keyof typeof ui_locales): string {
|
||||||
|
return ui_locales[key][currentLanguageIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
* 페이지 진입점
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
export function renderB10Payment(root: HTMLElement): void {
|
||||||
|
renderPendingContent(root, {
|
||||||
|
pageClass: "b10-payment",
|
||||||
|
title: L("B10_Payment_Title"),
|
||||||
|
subtitle: L("B10_Payment_Subtitle"),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B11_Status_UI_Page.ts
|
||||||
|
* 로그인 후 11: 상태 출력 페이지 (결재완료/문서생성/다운로드)
|
||||||
|
*
|
||||||
|
* ⚠️ 본문(진행 상태/결과물 다운로드) 준비 중 — 헤더/안내만 구성. 추후 구체화.
|
||||||
|
* 제약 준수 (frontend.md): 문구는 locale 참조(§3), 공통 스캐폴드 사용(§2).
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||||
|
import { renderPendingContent } from "../A00_Common/b_page_scaffold";
|
||||||
|
|
||||||
|
/** locale 헬퍼 */
|
||||||
|
function L(key: keyof typeof ui_locales): string {
|
||||||
|
return ui_locales[key][currentLanguageIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
* 페이지 진입점
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
export function renderB11Status(root: HTMLElement): void {
|
||||||
|
renderPendingContent(root, {
|
||||||
|
pageClass: "b11-status",
|
||||||
|
title: L("B11_Status_Title"),
|
||||||
|
subtitle: L("B11_Status_Subtitle"),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -332,6 +332,114 @@ export const ui_locales = {
|
|||||||
],
|
],
|
||||||
A08_Support_Error_Required: ["모든 항목을 입력하세요.", "Please fill in all fields."],
|
A08_Support_Error_Required: ["모든 항목을 입력하세요.", "Please fill in all fields."],
|
||||||
A08_Support_Error_Email: ["이메일 형식이 올바르지 않습니다.", "Invalid email format."],
|
A08_Support_Error_Email: ["이메일 형식이 올바르지 않습니다.", "Invalid email format."],
|
||||||
|
|
||||||
|
/* ===========================================================================
|
||||||
|
* 로그인 후 (B 그룹)
|
||||||
|
* ======================================================================== */
|
||||||
|
|
||||||
|
/* 워크플로우 진행 단계 라벨은 상단 WF_Step_* 키를 재사용 (중복 등록 방지). */
|
||||||
|
|
||||||
|
/* --- 공통: 콘텐츠 미구현 안내 (B03~B06 본문 자리표시) --- */
|
||||||
|
B_Content_Pending: [
|
||||||
|
"이 영역의 상세 기능은 준비 중입니다.",
|
||||||
|
"The detailed features for this area are in preparation.",
|
||||||
|
],
|
||||||
|
|
||||||
|
/* --- B01_AccountDetail 계정 상세 --- */
|
||||||
|
B01_Account_Title: ["내 계정", "My Account"],
|
||||||
|
B01_Account_Subtitle: [
|
||||||
|
"계정 정보와 소속 회사를 확인하고 수정할 수 있습니다.",
|
||||||
|
"View and edit your account information and company.",
|
||||||
|
],
|
||||||
|
B01_Account_Section_Profile: ["기본 정보", "Profile"],
|
||||||
|
B01_Account_Section_Security: ["보안", "Security"],
|
||||||
|
B01_Account_Field_Company: ["회사명", "Company"],
|
||||||
|
B01_Account_Field_Name: ["이름", "Name"],
|
||||||
|
B01_Account_Field_Email: ["이메일", "Email"],
|
||||||
|
B01_Account_Field_Phone: ["연락처", "Phone"],
|
||||||
|
B01_Account_Field_Phone_Placeholder: ["연락처를 입력하세요", "Enter phone number"],
|
||||||
|
B01_Account_Field_CurrentPw: ["현재 비밀번호", "Current password"],
|
||||||
|
B01_Account_Field_NewPw: ["새 비밀번호", "New password"],
|
||||||
|
B01_Account_Field_ConfirmPw: ["새 비밀번호 확인", "Confirm new password"],
|
||||||
|
B01_Account_Save_Profile: ["기본 정보 저장", "Save profile"],
|
||||||
|
B01_Account_Save_Password: ["비밀번호 변경", "Change password"],
|
||||||
|
B01_Account_Success_Profile: ["기본 정보가 저장되었습니다.", "Profile has been saved."],
|
||||||
|
B01_Account_Success_Password: ["비밀번호가 변경되었습니다.", "Password has been changed."],
|
||||||
|
B01_Account_Error_Required: ["필수 항목을 입력하세요.", "Please fill in required fields."],
|
||||||
|
B01_Account_Error_PwMismatch: ["새 비밀번호가 일치하지 않습니다.", "New passwords do not match."],
|
||||||
|
B01_Account_Error_PwLength: [
|
||||||
|
"비밀번호는 8자 이상이어야 합니다.",
|
||||||
|
"Password must be at least 8 characters.",
|
||||||
|
],
|
||||||
|
|
||||||
|
/* --- B02_ProjRegister 프로젝트 등록 --- */
|
||||||
|
B02_Proj_Title: ["프로젝트 등록", "Register Project"],
|
||||||
|
B02_Proj_Subtitle: [
|
||||||
|
"새 임도 설계 프로젝트의 기본 정보를 입력하세요.",
|
||||||
|
"Enter the basic information for a new forest road project.",
|
||||||
|
],
|
||||||
|
B02_Proj_Field_Name: ["프로젝트명", "Project name"],
|
||||||
|
B02_Proj_Field_Name_Placeholder: [
|
||||||
|
"예: 2025년 산불진화임도(기번8)",
|
||||||
|
"e.g. 2025 Fire-suppression Road (No.8)",
|
||||||
|
],
|
||||||
|
B02_Proj_Field_Region: ["사업 지역", "Region"],
|
||||||
|
B02_Proj_Field_Region_Placeholder: ["예: 울진군 금강송면", "e.g. Uljin-gun"],
|
||||||
|
B02_Proj_Field_RoadType: ["임도 종류", "Road type"],
|
||||||
|
B02_Proj_RoadType_Main: ["간선임도", "Main road"],
|
||||||
|
B02_Proj_RoadType_Branch: ["지선임도", "Branch road"],
|
||||||
|
B02_Proj_RoadType_Fire: ["산불진화임도", "Fire-suppression road"],
|
||||||
|
B02_Proj_RoadType_Stream: ["계류보전", "Stream conservation"],
|
||||||
|
B02_Proj_Field_Year: ["사업 연도", "Project year"],
|
||||||
|
B02_Proj_Field_Length: ["예상 연장 (m)", "Estimated length (m)"],
|
||||||
|
B02_Proj_Field_Length_Placeholder: ["예상 노선 길이", "Estimated route length"],
|
||||||
|
B02_Proj_Field_Memo: ["비고", "Notes"],
|
||||||
|
B02_Proj_Field_Memo_Placeholder: ["추가 메모 (선택)", "Additional notes (optional)"],
|
||||||
|
B02_Proj_Submit: ["프로젝트 생성", "Create project"],
|
||||||
|
B02_Proj_Success: [
|
||||||
|
"프로젝트가 생성되었습니다. 파일 입력 단계로 이동합니다.",
|
||||||
|
"Project created. Moving to the file input step.",
|
||||||
|
],
|
||||||
|
B02_Proj_Error_Required: ["필수 항목을 입력하세요.", "Please fill in required fields."],
|
||||||
|
|
||||||
|
/* --- B03_FileInput 파일 입력 --- */
|
||||||
|
B03_File_Title: ["파일 입력", "File Input"],
|
||||||
|
B03_File_Subtitle: [
|
||||||
|
"지형·포인트클라우드·도면 파일을 업로드하세요.",
|
||||||
|
"Upload terrain, point cloud, and drawing files.",
|
||||||
|
],
|
||||||
|
|
||||||
|
/* --- B04_wf1_Surface 지표면 모델 분석 --- */
|
||||||
|
B04_Surface_Title: ["1차 · 지표면 모델 분석", "Step 1 · Surface Analysis"],
|
||||||
|
|
||||||
|
/* --- B05_wf2_Route 경로 설계 --- */
|
||||||
|
B05_Route_Title: ["2차 · 경로 설계", "Step 2 · Route Design"],
|
||||||
|
|
||||||
|
/* --- B06_wf3_ProfileCross 종·횡단 생성 --- */
|
||||||
|
B06_Profile_Title: ["3차 · 종·횡단 생성", "Step 3 · Profile & Cross-section"],
|
||||||
|
|
||||||
|
/* --- B07_wf4_DesignDetail 상세 설계 --- */
|
||||||
|
B07_Design_Title: ["4차 · 상세 설계", "Step 4 · Detailed Design"],
|
||||||
|
|
||||||
|
/* --- B08_wf5_Quantity 수량 산출 --- */
|
||||||
|
B08_Quantity_Title: ["5차 · 수량 산출", "Step 5 · Quantity Takeoff"],
|
||||||
|
|
||||||
|
/* --- B09_wf6_Estimation 견적·문서 --- */
|
||||||
|
B09_Estimation_Title: ["6차 · 견적·문서", "Step 6 · Estimation & Documents"],
|
||||||
|
|
||||||
|
/* --- B10_Payment 결재 --- */
|
||||||
|
B10_Payment_Title: ["결재", "Payment"],
|
||||||
|
B10_Payment_Subtitle: [
|
||||||
|
"산출된 견적을 확인하고 결재를 진행하세요.",
|
||||||
|
"Review the estimate and proceed with payment.",
|
||||||
|
],
|
||||||
|
|
||||||
|
/* --- B11_Status 상태 출력 --- */
|
||||||
|
B11_Status_Title: ["처리 상태", "Status"],
|
||||||
|
B11_Status_Subtitle: [
|
||||||
|
"결재·문서 생성 상태를 확인하고 결과물을 내려받으세요.",
|
||||||
|
"Check payment and document status, and download results.",
|
||||||
|
],
|
||||||
} as const satisfies Record<string, LocaleEntry>;
|
} as const satisfies Record<string, LocaleEntry>;
|
||||||
|
|
||||||
export type LocaleKey = keyof typeof ui_locales;
|
export type LocaleKey = keyof typeof ui_locales;
|
||||||
|
|||||||
Reference in New Issue
Block a user