diff --git a/.agent/db_design_proposal.md b/.agent/db_design_proposal.md new file mode 100644 index 0000000..6ba52b4 --- /dev/null +++ b/.agent/db_design_proposal.md @@ -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 및 마이그레이션 코드로 구체화하겠습니다.** diff --git a/.agent/db_schema_simple.md b/.agent/db_schema_simple.md new file mode 100644 index 0000000..e5de3d4 --- /dev/null +++ b/.agent/db_schema_simple.md @@ -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) 모델 정의 diff --git a/.agent/frontend_plan.md b/.agent/frontend_plan.md deleted file mode 100644 index 93846d8..0000000 --- a/.agent/frontend_plan.md +++ /dev/null @@ -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 페이지 구현 diff --git a/.agent/structure.md b/.agent/structure.md index 720a941..af70c85 100644 --- a/.agent/structure.md +++ b/.agent/structure.md @@ -19,8 +19,9 @@ my-project/ │ └── structure.md # 본 파일 ├── A00_Common/ # 프론트엔드 공통 인프라 (부트스트랩, 라우팅, 셸) │ ├── main.ts # 앱 진입점 (스타일 주입 → 테마 복원 → 셸 → 라우터) -│ ├── router.ts # 해시 기반 SPA 라우터 + 인증 가드 + 지연 로딩 +│ ├── router.ts # 해시 기반 SPA 라우터 + 인증 가드 + 지연 로딩 (A·B 전 라우트 등록) │ ├── app_shell.ts # 공통 헤더/푸터 (로고, 네비, 언어·테마·로그인 토글) +│ ├── b_page_scaffold.ts # B 그룹 공통 스캐폴드 (헤더+준비중 안내 / 워크플로우 셸 래퍼) │ └── vite-env.d.ts # Vite 클라이언트 타입 선언 ├── common_util/ # 공통 유틸리티 (공통 기능 코드) │ ├── common_util_validate.ts # 프론트 1차 유효성 검사 (이메일/빈값/최소길이) @@ -76,19 +77,32 @@ my-project/ │ ├── A08_Support_UI_Page.ts │ └── A08_Support_UI_Style.css │ -├── B01_AccountDetail/ # 로그인 후 01: 계정 상세 -├── B02_ProjRegister/ # 로그인 후 02: 프로젝트 등록 -├── B03_FileInput/ # 로그인 후 03: 파일 입력 -├── B04_wf1_Surface/ # 로그인 후 04: 1차 workflow (지표면 모델 분석) -│ ├── B04_wf1_Surface_UI_View.ts -│ └── B04_wf1_Surface_Calc_Mesh.py -├── B05_wf2_Route/ # 로그인 후 05: 2차 workflow (경로설계) -├── B06_wf3_ProfileCross/ # 로그인 후 06: 3차 workflow (종횡단 생성) -├── B07_wf4_DesignDetail/ # 로그인 후 07: 4차 workflow (상세설계) -├── B08_wf5_Quantity/ # 로그인 후 08: 5차 workflow (수량 산출) -├── B09_wf6_Estimation/ # 로그인 후 09: 6차 workflow (견적/문서) -├── B10_Payment/ # 로그인 후 10: 결재 페이지 -└── B11_Status/ # 로그인 후 11: 상태 출력 페이지 (결재완료/다운로드 등) +│ # ※ B01·B02 본문 완성 / B03~B11 헤더·셸만 (본문은 0_old 참고 후 구체화) +├── B01_AccountDetail/ # 로그인 후 01: 계정 상세 (기본정보 수정 + 비밀번호 변경) +│ ├── B01_AccountDetail_UI_Page.ts +│ └── B01_AccountDetail_UI_Style.css +├── B02_ProjRegister/ # 로그인 후 02: 프로젝트 등록 (기본정보 입력 폼) +│ ├── B02_ProjRegister_UI_Page.ts +│ └── B02_ProjRegister_UI_Style.css +├── B03_FileInput/ # 로그인 후 03: 파일 입력 (헤더+준비중 안내) +│ ├── B03_FileInput_UI_Page.ts +│ └── B03_FileInput_UI_Style.css +├── B04_wf1_Surface/ # 로그인 후 04: 1차 workflow (지표면 모델 분석) — 워크플로우 셸 +│ └── B04_wf1_Surface_UI_Page.ts +├── 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 애플리케이션 진입점 ├── requirements.txt # Python 의존성 (pip) diff --git a/.gitignore b/.gitignore index 4997fe1..1eb568f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ venv/ __pycache__/ *.pyc .pytest_cache/ +.env +.env.* +!.env.example # Node / Frontend (루트 기반 구조) node_modules/ diff --git a/A00_Common/b_page_scaffold.ts b/A00_Common/b_page_scaffold.ts new file mode 100644 index 0000000..d8b4e98 --- /dev/null +++ b/A00_Common/b_page_scaffold.ts @@ -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); +} diff --git a/A00_Common/router.ts b/A00_Common/router.ts index 0f94645..a518990 100644 --- a/A00_Common/router.ts +++ b/A00_Common/router.ts @@ -35,6 +35,29 @@ const routeTable: Partial Promise>> = { (await import("../A07_Register/A07_Register_UI_Page")).renderA07Register, [ROUTES.A08_SUPPORT]: async () => (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, }; /** 로그인 여부 (토큰 존재 확인) */ diff --git a/B01_AccountDetail/B01_AccountDetail_UI_Page.ts b/B01_AccountDetail/B01_AccountDetail_UI_Page.ts new file mode 100644 index 0000000..2083218 --- /dev/null +++ b/B01_AccountDetail/B01_AccountDetail_UI_Page.ts @@ -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, + }); +} diff --git a/B01_AccountDetail/B01_AccountDetail_UI_Style.css b/B01_AccountDetail/B01_AccountDetail_UI_Style.css new file mode 100644 index 0000000..c22fba1 --- /dev/null +++ b/B01_AccountDetail/B01_AccountDetail_UI_Style.css @@ -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; + } +} diff --git a/B02_ProjRegister/B02_ProjRegister_UI_Page.ts b/B02_ProjRegister/B02_ProjRegister_UI_Page.ts new file mode 100644 index 0000000..2714822 --- /dev/null +++ b/B02_ProjRegister/B02_ProjRegister_UI_Page.ts @@ -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); +} diff --git a/B02_ProjRegister/B02_ProjRegister_UI_Style.css b/B02_ProjRegister/B02_ProjRegister_UI_Style.css new file mode 100644 index 0000000..233f8f2 --- /dev/null +++ b/B02_ProjRegister/B02_ProjRegister_UI_Style.css @@ -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; + } +} diff --git a/B03_FileInput/B03_FileInput_UI_Page.ts b/B03_FileInput/B03_FileInput_UI_Page.ts new file mode 100644 index 0000000..afe96b0 --- /dev/null +++ b/B03_FileInput/B03_FileInput_UI_Page.ts @@ -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"), + }); +} diff --git a/B03_FileInput/B03_FileInput_UI_Style.css b/B03_FileInput/B03_FileInput_UI_Style.css new file mode 100644 index 0000000..b301952 --- /dev/null +++ b/B03_FileInput/B03_FileInput_UI_Style.css @@ -0,0 +1,7 @@ +/* ============================================================================= + * B03_FileInput_UI_Style.css + * 파일 입력 페이지 전용 스타일 (theme.css 변수만 사용) + * ⚠️ 본문(업로드 드롭존) 준비 중 — 현재는 공통 스캐폴드 스타일에 위임. + * ========================================================================== */ + +/* 본문 구현 시 .b03-file 하위에 업로드 드롭존/파일 목록 스타일 추가 예정 */ diff --git a/B04_wf1_Surface/B04_wf1_Surface_UI_Page.ts b/B04_wf1_Surface/B04_wf1_Surface_UI_Page.ts new file mode 100644 index 0000000..09a7e7d --- /dev/null +++ b/B04_wf1_Surface/B04_wf1_Surface_UI_Page.ts @@ -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, + }); +} diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Page.ts b/B05_wf2_Route/B05_wf2_Route_UI_Page.ts new file mode 100644 index 0000000..3117c90 --- /dev/null +++ b/B05_wf2_Route/B05_wf2_Route_UI_Page.ts @@ -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, + }); +} diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Page.ts b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Page.ts new file mode 100644 index 0000000..bad7fb9 --- /dev/null +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Page.ts @@ -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, + }); +} diff --git a/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Page.ts b/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Page.ts new file mode 100644 index 0000000..1a02bf7 --- /dev/null +++ b/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Page.ts @@ -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, + }); +} diff --git a/B08_wf5_Quantity/B08_wf5_Quantity_UI_Page.ts b/B08_wf5_Quantity/B08_wf5_Quantity_UI_Page.ts new file mode 100644 index 0000000..c69673f --- /dev/null +++ b/B08_wf5_Quantity/B08_wf5_Quantity_UI_Page.ts @@ -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, + }); +} diff --git a/B09_wf6_Estimation/B09_wf6_Estimation_UI_Page.ts b/B09_wf6_Estimation/B09_wf6_Estimation_UI_Page.ts new file mode 100644 index 0000000..d80b323 --- /dev/null +++ b/B09_wf6_Estimation/B09_wf6_Estimation_UI_Page.ts @@ -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, + }); +} diff --git a/B10_Payment/B10_Payment_UI_Page.ts b/B10_Payment/B10_Payment_UI_Page.ts new file mode 100644 index 0000000..fdb51b8 --- /dev/null +++ b/B10_Payment/B10_Payment_UI_Page.ts @@ -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"), + }); +} diff --git a/B11_Status/B11_Status_UI_Page.ts b/B11_Status/B11_Status_UI_Page.ts new file mode 100644 index 0000000..d74db68 --- /dev/null +++ b/B11_Status/B11_Status_UI_Page.ts @@ -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"), + }); +} diff --git a/ui_template/ui_template_locale.ts b/ui_template/ui_template_locale.ts index dbc6bd1..41be913 100644 --- a/ui_template/ui_template_locale.ts +++ b/ui_template/ui_template_locale.ts @@ -332,6 +332,114 @@ export const ui_locales = { ], A08_Support_Error_Required: ["모든 항목을 입력하세요.", "Please fill in all fields."], 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; export type LocaleKey = keyof typeof ui_locales;