This commit is contained in:
2026-07-15 18:33:33 +09:00
parent 6b1583103f
commit f533082537
201 changed files with 19776 additions and 563 deletions
+200
View File
@@ -0,0 +1,200 @@
# 프로젝트 행동지침 및 기술 스택 명세 (agent.md)
## 프로젝트 & DB 정보
### 📌 프로그램명
**Aislo (아이슬로)**
- **의미**: AI (인공지능) + Slotti (핀란드어: 임도/산림 도로)
- **콘셉트**: 산림의 미래를 열어가는 인공지능 경로 설계 솔루션
---
## 1. 기술 환경 및 기본 스택 (Technical Stack Baseline)
프로젝트의 코드를 작성, 테스트 또는 검증할 환경 명확화 및 호환 기준 제시.
### A. 백엔드 프레임워크 (Python)
* **Runtime:** Python v3.12.7
* **Framework:** FastAPI / Pydantic
* **Geometry/GIS Engine:** Trimesh, Whitebox, Geopandas, Shapely, Rasterio, Laspy
### B. 데이터베이스 (MariaDB)
* **DBMS:** MariaDB v10.6+
* **Character Set:** utf8mb4 (한글 완벽 지원)
* **Collation:** utf8mb4_unicode_ci
* **DB Driver (비동기):** `aiomysql` (순수 Python, Windows 호환. asyncmy는 Cython 빌드 필요로 미채택)
* **쿼리 방식:** Raw SQL (ORM 사용 금지)
* **공간 데이터:** JSON 기반 저장 (MariaDB는 PostGIS 미지원)
### C. 프론트엔드 & WebCAD (TypeScript & WebGL)
* **Language/Runtime:** TypeScript / Node.js
* **Rendering:** HTML5 Canvas 및 WebGL 기반 WebCAD 시스템 구현
### D. 인코딩 및 다국어 표준
* **인코딩:** 텍스트 파일은 UTF-8 표준으로 통일
---
## 2. 문맥 라우팅 및 문서 구조 (Context Routing)
코드 작성 및 기술 검증 시 문서 우선순위에 따른 계층적 레퍼런스 필수 읽기.
* **1단계 (필수):** `.agent/structure.md` 읽기 후 신규 폴더 생성 기준 분석.
* **2단계 (선택 읽기):** 프로젝트 구현에 필요한 세부 기술 명세 읽기.
* **그룹 A (UI, 스타일, 컴포넌트, WebCAD):** `.agent/frontend.md` 필수 추가 읽기.
* **그룹 B (알고리즘, 저장, DB, MariaDB 값 제어):** `.agent/backend.md` 필수 추가 읽기.
* **그룹 C (DB 구조 변경):** DB 구조 변경 시 `.agent/db_schema.md` 선택적 읽기.
---
## 3. 코드 작성 기본 제약 (Constraints)
* **구조 준수:** `.agent/structure.md` 트리 구조 엄격 준수.
* **700줄 제한:** 단일 파일 코드 작성/수정 시 700줄 이상이 될 경우 사전 공지. 기능별 파일 분할 후 `structure.md` 갱신 필수.
* **안전 보관:** 백엔드 작업 시 보안 저장소 경로 명시 및 임의 삭제 방지 필수.
* **경로 활용:** 단계별 페이지 기반 폴더 구조(B03~B09)와 DB 경로 열의 기준은 `.agent/db_schema_simple.md`의 「파일시스템 경로와 DB 링크」를 따른다.
* **일관성 검증:** 설계 단계에서부터 DB 설계, 파일 경로, API 라우팅이 모두 동일한 워크플로우 기준으로 통일되어야 한다. (불일치 발생 시 설계 단계에서 재논의 필수)
* **코드 포맷팅 (자동화 도구):** 프로젝트의 코드 작성이 완료되는 시, 반드시 프로젝트 루트에서 각 언어별 포맷터를 재실행하여 스타일을 통일해야 한다.
* *Python 포맷팅 명령어:* `ruff format [파일명]``ruff check --fix [파일명]` 실행
* *TypeScript/CSS 포맷팅 명령어:* `npx prettier --write [파일명]` 실행
* **외부 라이브러리 활용:** 외부 라이브러리 활용 시 프로젝트에 맞춰 호환 라이브러리 선정하고, 불필요한 함수는 작성하지 않도록 최소화.
* **서버실행은 사용자가 실행:** 필요한 경우 의존성 설치까지는 OK, 단 서버실행은 사용자가 진행
---
## 4. 데이터베이스 상세 명세 (Database Specification)
### 4.1 스키마 기본 정보
- **DB 명:** `aislo_db`
- **DBMS:** MariaDB v10.6+
- **인코딩:** utf8mb4_unicode_ci
- **테이블 수:** 18개
- **드라이버:** aiomysql (비동기)
### 4.2 파일 경로 추적 (Path Tracking)
**모든 중간 산출물의 파일 경로를 DB에 기록:**
- `input_files.raw_file_path` — 원본 입력 파일
- `processed_point_cloud.converted_file_path` — 변환된 포인트클라우드
- `surface_models.model_file_path` — 지표면 모델
- `routes.route_data_path` — 경로 데이터
- `longitudinal_sections.longitudinal_file_path` — 종단면
- `cross_sections.cross_section_file_path` — 횡단면
- `structures.structure_data_path` — 구조물 배치
- `quantity_items.quantity_data_path` — 수량 항목
- `outputs.outputs_directory_path` — 산출물 폴더
- `output_files.output_file_path` — 개별 산출 파일
### 4.3 저장소 구조 (Workflow-based Folder Structure)
```
storage/{company_slug}/{user_slug}/{project_id}/
├── B03_FileInput/input/ (WF0: 원본 입력)
├── B04_wf1_Surface/processed/ (WF1: 변환된 포인트클라우드 & 모델)
├── B05_wf2_Route/route/ (WF2: 경로 설계)
├── B06_wf3_ProfileCross/ (WF3: 종단면 & 횡단면)
├── B07_wf4_DesignDetail/structures/ (WF4: 구조물 배치)
├── B08_wf5_Quantity/quantities/ (WF5: 수량 산출)
└── B09_wf6_Estimation/v1,v2,.../ (WF6: 최종 산출물)
```
### 4.4 공간 데이터 처리 (MariaDB 특성)
- **지하형 기하 데이터:** GEOMETRY 타입 미지원 → JSON으로 저장
- **좌표 저장 예:** `{"type": "Point", "coordinates": [127.5, 37.5]}`
- **경로 저장 예:** `{"type": "LineString", "coordinates": [[127.5, 37.5], [127.6, 37.6]]}`
- **애플리케이션 처리:** Python의 Shapely, Geopandas에서 JSON 파싱 후 기하 연산
### 4.5 역할 기반 접근 제어 (RBAC - Role-Based Access Control)
**3가지 사용자 역할:**
| 역할 | 영문 | 설명 | 접근 권한 |
|------|------|------|---------|
| 시스템 관리자 | SYSTEM_ADMIN | 최고 관리자 | 전체 회사/사용자/프로젝트 조회 및 관리 |
| 회사 관리자 | ADMIN | 회사의 주요 아이디 (초기 로그인) | 회사 내 팀원/프로젝트 관리, 가입 요청 승인 |
| 일반 사용자 | USER | 팀원 | 개인 프로젝트 생성/수정, 워크플로우 진행 |
**회사 마스터 (is_master) 플래그:**
- TRUE: 회사 생성자 또는 ADMIN 역할 (팀원 승인/거부 권한)
- FALSE: 일반 팀원
**사용자 상태 생명주기:**
```
PENDING_EMAIL (임시)
↓ (이메일 인증)
NO_COMPANY (로그인 가능, 회사 미연결)
↓ (회사 생성 또는 참여 신청)
PENDING (승인 대기) 또는 ACTIVE (회사 연결 완료)
ACTIVE (모든 기능 접근)
↓ (퇴사/비활성화)
INACTIVE 또는 REJECTED
```
---
## 5. 워크플로우 6단계 (6-Stage Workflow)
```
WF0: B03_FileInput (파일 입력)
↓ (LAS, TIF, TFW, PRJ, DXF 업로드)
WF1: B04_wf1_Surface (지표면 분석)
↓ (DEM, TIN, 포인트클라우드 변환)
WF2: B05_wf2_Route (경로 설계)
↓ (최적 경로 계산)
WF3: B06_wf3_ProfileCross (종횡단 생성)
↓ (종단면, 횡단면 생성)
WF4: B07_wf4_DesignDetail (상세 설계)
↓ (구조물 배치)
WF5: B08_wf5_Quantity (수량 산출)
↓ (수량 항목 계산)
WF6: B09_wf6_Estimation (견적·문서)
↓ (Excel, PDF, DXF 생성)
```
---
## 6. 주요 문서 참고 순서
1. **계획서 읽기:** 구현 전 반드시 읽을 문서
- `.agent/plan_register_split.md` — B01_Dashboard 설계 및 역할별 기능
- `.agent/plan_register_split_validation.md` — 검증 항목 및 테스트 체크리스트
2. **구조 설계:** `.agent/structure.md` 읽기
- 폴더/파일 명명 규칙
- 전체 디렉토리 트리 구조
3. **DB 스키마:** `.agent/db_schema.md` 읽기
- 사용자/회사/조직 테이블 (5개)
- 프로젝트 및 워크플로우 테이블
- 테이블 관계도
4. **백엔드 구현:** `.agent/backend.md` 읽기
- 아키텍처 및 라우팅
- DB 및 MariaDB 설정
- 인증 시스템 (로그인/역할/인가)
- 역할 기반 접근 제어 (RBAC)
5. **프론트 구현:** `.agent/frontend.md` 읽기
- UI 요소 및 레이아웃 제약
- 인증 및 라우팅 가드
- 역할 기반 대시보드 렌더링
6. **마이그레이션:** `.agent/migration_plan.md` 읽기 (필요 시)
7. **프로젝트 정보:** `.agent/project_info.md` 읽기 (필요 시)
---
## 7. 요약
| 항목 | 값 |
|------|-----|
| **프로그램명** | Aislo (아이슬로) |
| **DB 명** | aislo_db |
| **DBMS** | MariaDB v10.6+ |
| **인코딩** | utf8mb4_unicode_ci |
| **Backend** | Python 3.12+ / FastAPI |
| **Frontend** | TypeScript / Node.js |
| **드라이버** | aiomysql |
| **워크플로우** | 6단계 (WF0~WF6) |
| **테이블 수** | 23개 (조직/인증 5개 포함) |
| **폴더 구조** | 워크플로우 기반 (B03~B09) |
| **인증 방식** | HttpOnly Secure 세션 쿠키 |
| **사용자 역할** | SYSTEM_ADMIN, ADMIN, USER (3가지) |
| **대시보드** | B01_Dashboard (역할별 동적 렌더링) |
+300
View File
@@ -0,0 +1,300 @@
# 백엔드 제어 명세서 (backend.md)
## 1. 아키텍처 및 라우팅 (Architecture)
* **수직 통합:** 백엔드 로직은 페이지 폴더(`A01_`, `B01_` 등)별로 격리. 거대 단일 모듈 금지.
* **파일 분할:** 파일 수 제한 없음. 코드 집중 방지를 위해 기능별 분할 자율화.
* **파일명 규칙:** `[폴더명]_Router.py`, `[폴더명]_Engine.py`, `[폴더명]_Schema.py` 형태 준수.
* **루트 main.py:** 라우터 등록(`include_router`)만 수행. 비즈니스 로직 작성 금지.
---
## 2. DB 및 MariaDB (Database)
* **비동기 통신:** `async/await``aiomysql` 드라이버 사용 강제. (asyncmy는 Cython 빌드 필요로 Windows 환경 미채택)
* **쿼리 방식:** ORM 사용 금지. **Raw SQL** 작성 원칙. 파라미터 바인딩은 `%s` 플레이스홀더 사용.
* **커넥션/트랜잭션:** 풀에서 `pool.acquire()`로 커넥션 확보 후 `connection.cursor()`로 커서 생성. 다건 쓰기는 `connection.begin()``commit()` / 예외 시 `rollback()`.
* **자동 증가 ID:** `INSERT``cursor.lastrowid`로 조회 (PostgreSQL의 `RETURNING id` 미지원).
* **공간 데이터:** GEOMETRY 타입 미지원. 좌표, 다각형, 경로 등은 JSON 형식으로 저장 후 애플리케이션에서 처리.
---
## 3. 설정 및 저장소 (Config & Storage)
* **하드코딩 금지:** 제어 변수/파라미터 하드코딩 금지. `config/config_system.py`에서 `import` 필수.
* **물리 파일 격리:** 포인트 클라우드, 분석 중간 파일, 메쉬, 임시 커서는 DB 저장 금지.
* **저장 경로:** `storage/[고객사명]/[사용자명]/[프로젝트ID]/`에 물리 파일 저장 후 DB에는 경로만 기록.
* **단계별 루트 강제:** 프로젝트 저장소 내부는 실제 워크플로우 페이지명과 동일한 `B03_FileInput/` ~ `B09_wf6_Estimation/` 폴더로 분리한다. `raw/`, `processed/`, `computed/`를 프로젝트 루트의 공용 폴더로 만들지 않는다.
* **경로 정의 우선순위:** 단계별 세부 폴더와 DB 경로 열의 기준은 `.agent/db_schema_simple.md`의 「파일시스템 경로와 DB 링크」를 따른다.
* **경로 생성 책임:** 페이지별 백엔드는 자기 단계 폴더만 생성·수정한다. 다른 단계의 산출물을 직접 삭제하거나 덮어쓰지 않고 stale 상태를 통해 재계산 필요성을 전파한다.
* **DB 저장 형식:** DB에는 프로젝트 루트 기준 상대 경로를 기록하고, 실제 파일 접근 시 설정의 저장소 루트와 안전하게 결합한다. 사용자 입력 경로를 직접 결합하거나 절대 경로를 DB에 저장하지 않는다.
* **MariaDB 특성:**
- 공간 기하 데이터(좌표, 폴리곤, 경로 등)는 GEOMETRY 타입 미지원 → JSON 또는 TEXT로 저장
- 예: `{"type": "Point", "coordinates": [127.5, 37.5]}` (GeoJSON 형식)
- 애플리케이션(Python)에서 JSON 파싱 후 기하 연산 처리
---
## 4. 검증 및 예외 처리 (Error Handling)
* **데이터 검증:** 모든 JSON 요청은 라우터 진입 전 Pydantic 모델로 타입/범위 필수 검증.
* **예외 포획:** 모든 지형/메쉬 연산은 `try-except` 필수 처리.
* **반환 포맷:** 에러 발생 시 `{"status": "error", "message": "원인"}` 표준 포맷 반환.
---
## 5. 워크플로우 상태 동기화 (Workflow State Sync)
### 5.1 다중 브라우저 동시 작업 원칙
* **브라우저 독립성:** 같은 계정/프로젝트를 여러 브라우저에서 동시 접근 가능. 각 브라우저는 서버 데이터 기반 독립 작동.
* **클라이언트 상태 격리:** `localStorage`는 브라우저별 격리(도메인 단위 공유). 공유 데이터는 항상 서버 영구저장소 우선.
* **영구저장소 설계:** 계산 결과는 `.agent/db_schema_simple.md`에 정의된 페이지별 단계 폴더에 저장한다.
* `B03_FileInput/` — 원본 입력 및 파일 메타데이터
* `B04_wf1_Surface/` — 변환 포인트클라우드, 지표면 모델 및 분석 결과
* `B05_wf2_Route/` — 경로, 경로점 및 설계 파라미터
* `B06_wf3_ProfileCross/` — 종단·횡단 결과 및 인덱스
* **워크플로우 상태:** 여러 단계가 공유하는 `workflow.json`의 실제 위치는 저장소 경로 유틸에서 단일하게 정의하며, 원자적 쓰기를 적용한다. 단계별 결과 파일의 위치를 프로젝트 루트의 `result_*.json` 이름으로 추정하지 않는다.
### 5.2 Stale 상태 감지 및 전파
* **workflow.json 구조:**
```json
{
"current_stage": "route",
"completed": ["scan", "surface"],
"stale_from": "route",
"stage1_confirmed": {...}
}
```
* **Stale 발생 조건:**
* 상위 단계(예: surface) 재계산 → 하위 단계(route, section) 자동으로 `stale_from` 설정
* 다른 브라우저에서 변경 감지 → 기존 결과 무효화
* **상태 업데이트 함수:**
```python
def _patch_workflow_stale(project_id: str, stale_from: str | None) -> None:
"""workflow.json의 stale_from 필드만 원자적 업데이트"""
wf_path = get_project_workflow_path(project_id)
# 기존 상태 유지하며 stale_from만 변경
```
### 5.3 클라이언트 폴링 패턴 (Polling)
* **주기:** GET `/api/projects/{project_id}/workflow` — 3초마다 호출
* **응답:** workflow.json 전체 반환 (현재 단계, 완료 이력, stale 상태 포함)
* **클라이언트 동작:**
1. stale 감지 → 사용자에게 선택지 제시
2. "계속하기" → 기존 결과 유지 (위험 경고)
3. "재계산하기" → 상위 단계 재실행 (모든 하위 결과 초기화)
* **구현 예시:**
```typescript
setInterval(async () => {
const workflow = await fetch(`/api/projects/${projectId}/workflow`);
if (workflow.stale_from && workflow.stale_from !== currentStage) {
handleStaleness(workflow.stale_from); // 사용자에게 알림
}
}, 3000);
```
### 5.4 WebSocket 확장 (미래 선택사항)
* **실시간 알림:** 폴링 대신 WebSocket으로 업그레이드 가능 (응답성 향상)
* **전송 메시지:**
```json
{
"type": "workflow_changed",
"project_id": "...",
"stale_from": "route",
"changed_by": "other_browser"
}
```
* **마이그레이션 시기:** 사용자 수 증가 또는 실시간성 요구 시 적용
---
## 6. 로그인 및 인증 시스템 (Authentication & Authorization)
### 6.1 이메일 기반 OTP 인증 (Email OTP)
* **OTP 생성:** 6자리 난수 (000000~999999)
* **저장 방식:** DB에 bcrypt/PBKDF2 해시값 저장 (평문 절대 금지)
* **유효 시간:** 5분 (config에서 설정 가능)
* **발송:** Google SMTP (개발) → AWS SES (상용화) 전환 예정
* **재시도 로직:** 최대 3회 (지수 백오프: 2초, 4초, 8초)
### 6.2 비밀번호 정책 (Password Policy)
* **최소 길이:** 8자 이상
* **권장 구성:** 대문자/소문자/숫자/특수문자 (강제 아님, Google 기준)
* **해시 알고리즘:** bcrypt (라운드 12 이상) 또는 PBKDF2
* **저장 방식:** 해시값만 DB에 저장 (평문 절대 금지)
### 6.3 세션 관리 (Session Management) - 확정: Secure HttpOnly 세션 쿠키
* **방식:** Secure HttpOnly 세션 쿠키 (JWT 아님)
* **쿠키 이름:** `session_id`
* **유효 시간:** 12시간
* **Secure 플래그:** true (HTTPS만 전송, 개발 환경 localhost에서는 false)
* **HttpOnly 플래그:** true (JavaScript 접근 불가)
* **SameSite:** Lax (CSRF 방지)
* **저장소:** MariaDB `sessions` 테이블
* **활동 감지:** 마지막 요청으로부터 4시간 (파일 업로드 등 장시간 작업 고려)
* **세션 만료 판정:**
```python
if (current_time - last_activity_at > 4시간) or (current_time - created_at > 12시간):
→ 세션 자동 만료
```
* **만료 처리:** 프론트엔드는 만료 5분 전 알림, 만료 후 모든 요청 거부 (401 Unauthorized)
* **DB 풀 설정:** `autocommit=True` (pooled 커넥션의 트랜잭션 스냅샷 문제 방지 — 새 세션 INSERT 후 다른 커넥션에서 즉시 조회 가능)
### 6.4 로그인 이력 기록 (Audit Log)
* **기록 항목:**
- `user_id`, `email`, `login_timestamp`, `user_agent` (브라우저/OS), `status` (SUCCESS/FAILURE)
* **보관 기간:** 1년 (자동 삭제)
* **무차별 대입 방지:** 5회 이상 실패 시 계정 15분 잠금
* **IP 주소:** 법적 이슈로 수집하지 않음 (권장)
### 6.4.1 사용자 상태 생명주기 (User Status Lifecycle)
* **PENDING_EMAIL:** 회원가입 후 이메일 인증 전 (임시)
* **NO_COMPANY:** 이메일 인증 완료, 회사 미연결 (로그인 가능, B01_Dashboard 접근 가능, B02~B11 워크플로우 차단)
* **PENDING:** 회사 참여 신청 후 ADMIN 승인 대기 (로그인 가능, B01_Dashboard 접근 가능, B02~B11 워크플로우 차단)
* **ACTIVE:** 회사 연결 완료, 모든 기능 접근 가능 (B01_Dashboard + B02~B11)
* **INACTIVE:** 퇴사/계정 휴활성화
* **REJECTED:** 회사 참여 신청 거부
### 6.5 회사 연결 필수화 (Mandatory Company Linking)
* **접근 제어:**
- B02~B11 워크플로우 라우터에 `Depends(require_company)` 의존성 추가
- `session["company_id"] is None` 이면 403 Forbidden 반환 (`"회사 연결이 필요합니다."`)
* **라우팅 가드 (프론트):**
- B02~B11 진입 시 `fetchSessionUser()` → `company_id` 확인
- 없으면 B01_ACCOUNT로 리다이렉트
* **회사 생성/연결:**
- `POST /api/account/company/create`: 신규 회사 생성 후 사용자를 MASTER로 자동 연결 (status='ACTIVE')
- `POST /api/account/company/join`: 기존 회사 참여 신청 (status='PENDING', 마스터 승인 필요)
### 6.6 권한 체크 (Authorization)
* **마스터 권한:**
- 팀원 목록 조회, 승인/거부, 내보내기
- 회사 정보 수정, 구독 관리
- 마스터 대시보드 접근
* **팀원 권한:**
- 프로젝트 생성/수정/조회
- 워크플로우 실행 (B03~B09)
- 개인 프로필 수정
* **시스템관리자 권한:**
- 모든 회사/사용자 데이터 접근 (슈퍼유저)
- 회사/사용자 생성/수정/삭제
- 관리자 대시보드 (모니터링, Q&A 관리)
* **검증 방식:** 라우터 진입 전 JWT/토큰 검증, 필요시 데이터베이스에서 권한 확인
### 6.7 로그인 기반 인증 갱신 (Login-based Auth Renewal)
* **목적:** 활성 사용자의 인증 유지, 비활성 사용자 자동 차단
* **방식:** 로그인 시 인증 유효 기간 자동 연장 (3개월)
* **구현:**
- `users.last_login` → 마지막 로그인 시간 업데이트
- `users.auth_expires_at` → 매 로그인 시 NOW() + 3개월로 설정
* **인증 확인:**
- auth_expires_at > NOW() → 인증 유효 (바로 대시보드 이동)
- auth_expires_at <= NOW() → 재인증 모달 표시
* **장점:**
- 활동적인 사용자: 지속적으로 인증 유지
- 휴면 사용자: 자동으로 재인증 요구
- 일관된 정책: 고정 3개월이 아닌 활동 기반 연장
### 6.8 역할 기반 접근 제어 (Role-Based Access Control)
**사용자 역할 (users.role)**
- `SYSTEM_ADMIN`: 시스템 관리자 (최고 권한)
- `ADMIN`: 회사 관리자 (회사 내 관리권 한정)
- `USER`: 일반 사용자
### 6.9 대시보드 권한 검증 및 감시 로깅 (B01_Dashboard Authorization & Audit)
* **권한 검증 헬퍼 필수:** `.agent/plan_dashboard_management.md` 섹션 9의 Python 헬퍼 함수 구현
- `can_edit_project(user, project)` - 프로젝트 수정 권한
- `can_delete_project(user)` - 프로젝트 삭제 권한 (SYSTEM_ADMIN만)
- `can_change_role(user, target_user, new_role)` - 역할 변경 권한
- ADMIN이 SYSTEM_ADMIN으로 변경 시도 시 즉시 False 반환
- ADMIN은 USER ↔ ADMIN만 변경 가능 (같은 회사만)
- `can_manage_automation(user, automation)` - 자동화 로직 관리 권한 (USER 차단)
- `is_last_admin(company_id, exclude_user_id)` - 회사의 마지막 ADMIN 확인
* **백엔드 권한 재검증 필수:** 모든 수정/삭제/역할변경 API에서 권한 재검증
- 클라이언트 UI 신뢰 금지 (프론트엔드는 UI만 제어)
- 권한 없는 사용자가 API 직접 호출 시도 → 403 Forbidden
- 권한 오류 메시지: `{"status": "error", "message": "Access denied"}`
* **감시 로깅 (audit_logs):** 다음 행동들을 기록
- 프로젝트 삭제: `action="DELETE_PROJECT"`, `resource_type="project"`
- 사용자 역할 변경: `action="CHANGE_ROLE_TO_{NEW_ROLE}"`, `resource_type="user"`
- 사용자 삭제: `action="DELETE_USER"`, `resource_type="user"`
- 자동화 로직 삭제: `action="DELETE_AUTOMATION"`, `resource_type="automation"`
* **USER 자동화 조회 권한 차단:** `/api/projects/{pid}/automations` GET
- 요청 사용자의 role이 USER인 경우 403 Forbidden 반환
- 메시지: `"Automation logic viewing is not allowed for users"`
* **마지막 ADMIN 보호:** 사용자 삭제 API에서 체크
- `is_last_admin(company_id, exclude_user_id=user_id)` 호출
- True 반환 시 400 Bad Request: `"Cannot delete the last admin of the company"`
- 역할 변경 API에서도 동일 로직 적용
**마스터 플래그 (users.is_master)**
- `TRUE`: 회사 생성자 또는 ADMIN 역할 (팀원 승인/거부 권한)
- `FALSE`: 일반 팀원
**접근 제어 규칙:**
| 기능 | SYSTEM_ADMIN | ADMIN | USER |
|------|-------------|-------|------|
| 전체 회사 조회 | ✅ | ✗ | ✗ |
| 전체 사용자 조회 | ✅ | ✗ | ✗ |
| 사용자 역할 변경 | ✅ | ✗ | ✗ |
| 회사 생성 | ✅ | ✅ | ✅ |
| 회사 팀원 관리 | ✅ | ✅ (자신 회사만) | ✗ |
| 가입 요청 승인 | ✅ | ✅ (자신 회사만) | ✗ |
| 시스템 로그 조회 | ✅ | ✗ | ✗ |
| 프로젝트 생성 | ✅ | ✅ | ✅ |
| 프로젝트 조회 | ✅ (전체) | ✅ (회사별) | ✅ (개인) |
### 6.9 이메일 발송 (Email Infrastructure)
* **SMTP 설정:**
- **개발:** Google Gmail SMTP (umsangdon@gmail.com)
- **상용화:** Google Workspace 또는 AWS SES로 전환
* **발송 유형:**
- OTP 인증 코드
- 가입 확인 메일
- 팀원 가입 승인 요청 (ADMIN 수신)
- 팀원 승인/거부 결과 (사용자에게)
- 회사 참여 신청 알림 (SYSTEM_ADMIN 수신)
- 로그인 기반 인증 갱신 알림
* **구현:** `common_util/common_util_email.py` 모듈
- 비동기 발송 (asyncio.create_task)
- 재시도 로직 (최대 3회)
- 템플릿 렌더링 (common_util_email_templates.py)
* **자세한 내용:** `.agent/email_infrastructure_plan.md` 참고
---
## 7. 시스템 리소스 모니터링 (System Resource Monitoring)
### 7.1 계측 백그라운드 태스크
* **구현:** `common_util/common_util_resource_monitor.py`의 `sample_resources_loop()`
* **등록:** `main.py` lifespan에서 `asyncio.create_task`로 시작, 종료 시 cancel (기존 `cleanup_expired_sessions` 패턴과 동일)
* **계측 주기:** 2분 (`config_system.RESOURCE_SAMPLE_INTERVAL_SEC = 120`)
* **계측 항목:** CPU(`psutil.cpu_percent`), 메모리(`psutil.virtual_memory().percent`), 디스크(`shutil.disk_usage`)
* **방어:** 계측 예외는 로깅 후 무시하여 앱 안정성 유지
### 7.2 이중 기록 (DB + 로그 파일)
* **DB 기록:** `system_resources` 테이블에 INSERT (활성 세션/프로젝트 수 포함) — 대시보드 그래프 조회 소스
* **로그 파일:** 루트 `log/system_resources.log` 단일 파일에 탭 구분 1줄 append
- 형식: `<ISO timestamp>\tcpu=<v>\tmem=<v>\tdisk=<v>\tstorage_mb=<v>`
- `log/`는 `.gitignore` 처리 (런타임 생성물)
### 7.3 1개월 롤링 보관 (Retention)
* **보관 기간:** 30일 (`config_system.RESOURCE_LOG_RETENTION_DAYS = 30`)
* **DB:** 매 계측 시 `timestamp < NOW() - 30일` 행 DELETE
* **로그 파일:** 매 계측 시 30일 경과 줄 제거 후 새 줄 추가(오래된 줄 삭제 + 신규 줄 추가 방식) → 파일 크기 무한 증가 방지
### 7.4 리소스 API
* **엔드포인트:** `GET /api/dashboard/admin/resources?days=30` (SYSTEM_ADMIN 전용, 범위 1~90일)
* **응답:**
- `current`: 요청 시점 실시간 계측값 (그래프 미저장)
- `history`: `system_resources` 테이블의 최근 N일 시계열 (그래프 렌더링용, **다운샘플링 적용**)
- `stats`: 활성 사용자/프로젝트/저장소 지표
* **다운샘플링 (조회 시):**
- 30일 원본은 약 21,600점(2분 간격) → 그대로 전송 시 페이로드 과다(~4MB)
- SQL에서 시간버킷 평균으로 축소: `bucket = max(120초, days*86400 / 300)` → 최대 약 300점
- `GROUP BY FLOOR(UNIX_TIMESTAMP(timestamp) / bucket)` + `AVG(...)`로 집계
- 계측 간격(2분)보다 버킷이 작으면 원본 해상도 유지 (하루 이하 조회 시)
- 효과: 30일 조회 페이로드 ~4MB → ~55KB 수준
+851
View File
@@ -0,0 +1,851 @@
# DB 스키마 (테이블 및 열 목록)
**최종 업데이트:** 2026-07-10
**DB 명:** `aislo_db` (MariaDB v10.6+, utf8mb4_unicode_ci)
**총 테이블 수:** 34개 (33개 현재 운영 + 1개 향후 사용)
---
## 1. 사용자 & 인증 & 조직 그룹
### 1-1. users 테이블
```
users
├── id (INT, PK) — 사용자 고유 번호
├── email (VARCHAR(255), UNIQUE) — 로그인 이메일
├── password_hash (VARCHAR(255)) — 비밀번호 암호화 저장
├── name (VARCHAR(255)) — 사용자 이름
├── position (VARCHAR(100), NULL) — 직급 (예: 과장, 대리, 사원)
├── department (VARCHAR(100), NULL) — 부서명 (예: 설계팀, 영업팀)
├── phone (VARCHAR(20), NULL) — 연락처 (예: 010-1234-5678)
├── company_id (INT, FK → companies.id, NULL) — 소속 회사
├── role (ENUM) — 역할: SYSTEM_ADMIN, ADMIN, USER (기본값: USER)
├── is_master (TINYINT(1)) — 회사 마스터 여부 (기본값: 0)
├── status (ENUM) — 상태: PENDING_EMAIL, NO_COMPANY, PENDING, ACTIVE, INACTIVE, REJECTED (기본값: PENDING_EMAIL)
├── last_login (DATETIME, NULL) — 마지막 로그인 시간
├── auth_expires_at (DATETIME, NULL) — 인증 유효 만료 시간 (3개월 주기)
├── last_email_verified_at (TIMESTAMP, NULL) — 마지막 이메일 인증 시간
├── login_failures (INT) — 연속 로그인 실패 횟수 (기본값: 0, 보안)
├── last_failed_at (TIMESTAMP, NULL) — 마지막 로그인 실패 시간
├── account_locked_until (TIMESTAMP, NULL) — 계정 잠금 해제 시간
├── created_at (TIMESTAMP) — 가입일
├── updated_at (TIMESTAMP) — 수정일
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
**상태 생명주기:**
- PENDING_EMAIL: 회원가입 후 이메일 인증 전
- NO_COMPANY: 이메일 인증 완료, 회사 미연결 (로그인 가능, B02~B11 차단)
- PENDING: 회사 참여 신청 후 관리자 승인 대기
- ACTIVE: 회사 연결 완료, 모든 기능 접근 가능
- INACTIVE: 퇴사/계정 비활성화
- REJECTED: 회사 참여 신청 거부
### 1-2. companies 테이블
```
companies
├── id (INT, PK) — 회사 고유 번호
├── name (VARCHAR(255), UNIQUE) — 회사명
├── business_registration_number (VARCHAR(20), UNIQUE, NULL) — 사업자등록번호
├── business_address (VARCHAR(255), NULL) — 사업장 주소
├── business_owner (VARCHAR(100), NULL) — 사업주 이름
├── business_status (VARCHAR(50), NULL) — 기업 상태
├── company_code (VARCHAR(20), NULL) — 회사 코드
├── phone_number (VARCHAR(20), NULL) — 회사 연락처
├── representative_name (VARCHAR(100), NULL) — 대표자명
├── master_user_id (INT, NULL) — 마스터 사용자 ID
├── status (ENUM) — 상태: ACTIVE, INACTIVE, SUSPENDED (기본값: ACTIVE)
├── subscription_status (ENUM) — 구독 상태: FREE, TRIAL, PAID, EXPIRED (기본값: FREE)
├── created_by (INT, FK → users.id, NULL) — 회사 생성자
├── created_at (TIMESTAMP) — 생성일
├── updated_at (TIMESTAMP) — 수정일
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
### 1-3. join_requests 테이블 (가입 신청 - 향후 사용)
```
join_requests
├── id (INT, PK) — 요청 고유 번호
├── user_id (INT, FK → users.id) — 신청한 사용자
├── company_id (INT, FK → companies.id) — 신청 대상 회사
├── requested_at (TIMESTAMP) — 신청 시간
├── status (ENUM) — 상태: PENDING, APPROVED, REJECTED (기본값: PENDING)
├── reviewed_by (INT, FK → users.id, NULL) — 검토한 관리자
├── reviewed_at (TIMESTAMP, NULL) — 검토 시간
├── review_comment (TEXT, NULL) — 검토 의견/거부 사유
└── UNIQUE KEY (user_id, company_id) — 중복 신청 방지
```
**동작 (향후 구현 예정):**
- 사용자 가입 신청 → status = PENDING 생성
- 관리자 승인 → status = APPROVED, users.status = ACTIVE, users.company_id 세팅
- 관리자 거부 → status = REJECTED, review_comment에 거부 사유 기록, 이메일 발송
### 1-5. email_otps 테이블 (이메일 인증)
```
email_otps
├── id (INT, PK) — OTP 고유 번호
├── email (VARCHAR(255)) — 이메일 주소
├── otp_code (VARCHAR(10)) — OTP 코드
├── purpose (VARCHAR(50)) — 목적: REGISTRATION, PASSWORD_RESET, EMAIL_VERIFICATION
├── expires_at (TIMESTAMP) — 만료 시간
├── verified_at (TIMESTAMP, NULL) — 인증 시간
├── created_at (TIMESTAMP) — 생성일
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
### 1-6. sessions 테이블 (세션 관리)
```
sessions
├── id (INT, PK) — 세션 고유 번호
├── user_id (INT, FK → users.id) — 사용자 ID
├── session_token (VARCHAR(255), UNIQUE) — 세션 토큰
├── expires_at (TIMESTAMP) — 세션 만료 시간
├── created_at (TIMESTAMP) — 생성일
├── last_accessed_at (TIMESTAMP, NULL) — 마지막 접근 시간
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
### 1-7. trusted_devices 테이블 (장치 신뢰 관리)
```
trusted_devices
├── id (INT, PK) — 장치 고유 번호
├── user_id (INT, FK → users.id) — 사용자 ID
├── device_fingerprint (VARCHAR(255)) — 장치 지문
├── device_name (VARCHAR(100), NULL) — 장치명
├── ip_address (VARCHAR(50)) — IP 주소
├── user_agent (TEXT) — 브라우저/OS 정보
├── trusted_until (TIMESTAMP, NULL) — 신뢰 만료 시간
├── created_at (TIMESTAMP) — 생성일
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
### 1-8. user_consents 테이블 (사용자 동의 관리)
```
user_consents
├── id (INT, PK) — 동의 고유 번호
├── user_id (INT, FK → users.id) — 사용자 ID
├── consent_type (VARCHAR(100)) — 동의 종류: TERMS, PRIVACY, MARKETING, etc
├── consented (TINYINT(1)) — 동의 여부 (1 = 동의, 0 = 미동의)
├── consent_version (VARCHAR(20)) — 약관 버전
├── created_at (TIMESTAMP) — 생성일
├── updated_at (TIMESTAMP) — 수정일
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
### 1-9. login_logs 테이블 (로그인 로그)
```
login_logs
├── id (INT, PK) — 로그 고유 번호
├── user_id (INT, FK → users.id, NULL) — 사용자 ID
├── email (VARCHAR(255), NULL) — 로그인 시도 이메일
├── status (VARCHAR(50)) — 상태: SUCCESS, FAILURE, LOCKED
├── ip_address (VARCHAR(50), NULL) — IP 주소
├── user_agent (TEXT, NULL) — 브라우저/OS 정보
├── reason (TEXT, NULL) — 실패 사유
├── created_at (TIMESTAMP) — 기록 시간
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
### 1-10. activity_logs 테이블 (활동 로그)
```
activity_logs
├── id (INT, PK) — 로그 고유 번호
├── user_id (INT, FK → users.id, NULL) — 사용자 ID
├── action (VARCHAR(100)) — 행동: LOGIN, LOGOUT, FILE_UPLOAD, PROJECT_CREATE, etc
├── entity_type (VARCHAR(100), NULL) — 대상 타입: project, file, route, etc
├── entity_id (VARCHAR(50), NULL) — 대상 ID
├── description (TEXT, NULL) — 행동 설명
├── ip_address (VARCHAR(50), NULL) — IP 주소
├── created_at (TIMESTAMP) — 기록 시간
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
### 1-11. audit_logs 테이블 (감시 로그 - 보안)
```
audit_logs
├── id (INT, PK) — 로그 고유 번호
├── user_id (INT, FK → users.id) — 행동 수행자
├── action (VARCHAR(100)) — 행동: CREATE, READ, UPDATE, DELETE, EXPORT, DOWNLOAD
├── entity_type (VARCHAR(100)) — 대상 타입: projects, routes, structures, outputs
├── entity_id (VARCHAR(50)) — 대상 ID
├── details (LONGTEXT, NULL) — 상세 정보 (JSON 형식)
├── ip_address (VARCHAR(50), NULL) — IP 주소
├── timestamp (TIMESTAMP) — 기록 시간
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
### 1-12. system_audit_logs 테이블 (시스템 감시)
```
system_audit_logs
├── id (INT, PK) — 로그 고유 번호
├── user_id (INT, FK → users.id) — 행동 수행자
├── action (VARCHAR(50)) — 행동: LOGIN, LOGOUT, USER_CREATE, COMPANY_CREATE, ROLE_CHANGE
├── resource_type (VARCHAR(50)) — 대상 타입: user, company, project, join_request
├── resource_id (INT, NULL) — 대상 ID
├── ip_address (VARCHAR(50), NULL) — IP 주소
├── user_agent (TEXT, NULL) — 브라우저/OS 정보
├── timestamp (TIMESTAMP) — 기록 시간
└── INDEX (timestamp, user_id) — 성능 최적화
```
### 1-13. system_resources 테이블 (시스템 리소스 모니터링)
```
system_resources
├── id (INT, PK) — 기록 고유 번호
├── cpu_usage_percent (FLOAT, NULL) — CPU 사용률 (%)
├── memory_usage_percent (FLOAT, NULL) — 메모리 사용률 (%)
├── disk_usage_percent (FLOAT, NULL) — 디스크 사용률 (%)
├── active_user_count (INT, NULL) — 활성 사용자 수
├── active_project_count (INT, NULL) — 활성 프로젝트 수
├── total_storage_mb (FLOAT, NULL) — 전체 저장소 사용량 (MB)
├── timestamp (TIMESTAMP) — 기록 시간
└── INDEX (timestamp) — 시계열 조회 최적화
```
**리소스 측정 방식:**
- **현재값 실측:** 요청 시점 실시간 측정 (DB 저장 없이 응답에 포함)
- **이력 조회:** `system_resources` 테이블에서 기본 30일치 시계열 조회
- **활성 지표:** active_user_count는 유효 세션 수, active_project_count는 미삭제 프로젝트 수 실시간 집계
### 1-14. support_requests 테이블 (고객 지원)
```
support_requests
├── id (INT, PK) — 요청 고유 번호
├── user_id (INT, FK → users.id) — 신청자
├── title (VARCHAR(255)) — 제목
├── description (LONGTEXT) — 설명
├── category (VARCHAR(100), NULL) — 카테고리
├── priority (VARCHAR(50), NULL) — 우선순위: LOW, MEDIUM, HIGH, URGENT
├── status (VARCHAR(50)) — 상태: OPEN, IN_PROGRESS, RESOLVED, CLOSED
├── assigned_to (INT, FK → users.id, NULL) — 담당자
├── resolution (LONGTEXT, NULL) — 해결 내용
├── created_at (TIMESTAMP) — 생성일
├── updated_at (TIMESTAMP) — 수정일
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
---
## 2. 프로젝트 그룹
### 2-1. projects 테이블
```
projects
├── id (CHAR(36), PK) — 프로젝트 고유 ID (UUID)
├── user_id (INT, FK → users.id) — 프로젝트 소유자
├── company_id (INT, FK → companies.id) — 소속 회사
├── name (VARCHAR(255)) — 프로젝트명 (예: "2025년 산불진화임도")
├── region (VARCHAR(100), NULL) — 지역명 (예: "울진군 금강송면")
├── road_type (VARCHAR(100), NULL) — 임도 종류: 간선임도, 지선임도, 산불진화임도, 계류보전
├── project_year (INT, NULL) — 사업 연도 (예: 2025)
├── estimated_length_m (FLOAT, NULL) — 추정 연장 (미터)
├── memo (TEXT, NULL) — 비고/메모
├── status (VARCHAR(50), NULL) — 상태: NEW, FILE_UPLOADED, WF1_ANALYZING, WF1_COMPLETE, ... (기본값: NEW)
├── crs_epsg (INT, NULL) — 좌표계 (예: 5178 = 한국 표준, 기본값: 5178)
├── storage_path (VARCHAR(500), NULL) — 파일시스템 경로 (예: "storage/company/user/project_uuid")
├── created_at (TIMESTAMP) — 생성일
├── updated_at (TIMESTAMP) — 수정일
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
### 2-2. project_versions 테이블 (프로젝트 버전 관리)
```
project_versions
├── id (INT, PK) — 버전 고유 번호
├── project_id (CHAR(36), FK → projects.id) — 프로젝트 ID
├── version_num (INT) — 버전 번호 (1, 2, 3, ...)
├── status (VARCHAR(50), NULL) — 저장된 상태
├── data (LONGTEXT, NULL) — 설계 데이터 스냅샷 (JSON 형식)
├── snapshot_at (TIMESTAMP) — 스냅샷 시점
└── created_at (TIMESTAMP) — 생성일
```
### 2-3. project_automations 테이블 (프로젝트 자동화 규칙)
```
project_automations
├── id (INT, PK) — 자동화 고유 번호
├── project_id (CHAR(36), FK → projects.id) — 프로젝트 ID
├── name (VARCHAR(100)) — 자동화 규칙명
├── logic_type (VARCHAR(50)) — 로직 타입: trigger_on_stage_complete, auto_notify, etc
├── config_json (LONGTEXT) — 자동화 설정 (JSON)
├── status (ENUM) — 상태: DRAFT, ACTIVE, INACTIVE (기본값: DRAFT)
├── created_by (INT, FK → users.id) — 생성자
├── updated_by (INT, FK → users.id, NULL) — 마지막 수정자
├── last_executed_at (DATETIME, NULL) — 마지막 실행 시간
├── created_at (TIMESTAMP) — 생성일
├── updated_at (TIMESTAMP) — 수정일
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
---
## 3. 파일 & 업로드 관리 그룹
### 3-1. upload_sessions 테이블 (파일 업로드 세션)
```
upload_sessions
├── id (INT, PK) — 업로드 세션 고유 번호
├── user_id (INT, FK → users.id) — 업로드자
├── project_id (CHAR(36), FK → projects.id) — 프로젝트 ID
├── session_id (VARCHAR(255), UNIQUE) — 세션 ID
├── file_name (VARCHAR(255)) — 파일명
├── file_size_mb (FLOAT) — 파일 크기
├── chunk_count (INT) — 청크 개수
├── uploaded_chunks (INT) — 업로드된 청크 수
├── status (VARCHAR(50)) — 상태: PENDING, IN_PROGRESS, COMPLETED, FAILED (기본값: PENDING)
├── started_at (TIMESTAMP) — 시작 시간
├── completed_at (TIMESTAMP, NULL) — 완료 시간
└── expires_at (TIMESTAMP) — 세션 만료 시간
```
### 3-2. upload_chunks 테이블 (파일 청크 관리)
```
upload_chunks
├── id (INT, PK) — 청크 고유 번호
├── upload_session_id (INT, FK → upload_sessions.id) — 업로드 세션 ID
├── chunk_index (INT) — 청크 순서
├── chunk_size_mb (FLOAT) — 청크 크기
├── checksum (VARCHAR(255), NULL) — 체크섬 (무결성 검사)
├── status (VARCHAR(50)) — 상태: PENDING, UPLOADED, VERIFIED (기본값: PENDING)
├── uploaded_at (TIMESTAMP, NULL) — 업로드 시간
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
### 3-3. input_files 테이블 (원본 입력 파일)
```
input_files
├── id (INT, PK) — 파일 고유 번호
├── project_id (CHAR(36), FK → projects.id) — 속한 프로젝트
├── file_type (VARCHAR(50), NULL) — 파일 종류: las, tif, tfw, prj, dxf, dwg, other
├── original_filename (VARCHAR(255)) — 원래 파일명 (예: "cloud_merged.las")
├── raw_file_path (VARCHAR(500)) — 원본 저장 경로 (예: "storage/.../raw/las/cloud_merged.las")
├── file_size_mb (FLOAT, NULL) — 파일 크기 (MB)
├── upload_by (INT, FK → users.id, NULL) — 업로드한 사용자
├── upload_at (TIMESTAMP) — 업로드 날짜
├── crs_epsg (INT, NULL) — 파일 좌표계 (예: 5178)
├── metadata (LONGTEXT, NULL) — 파일 메타데이터 (JSON)
├── status (VARCHAR(50), NULL) — 상태: UPLOADED, PROCESSED, ARCHIVED (기본값: UPLOADED)
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
---
## 4. 지표면 분석 & 처리 그룹
### 4-1. processed_point_cloud 테이블 (변환된 포인트클라우드)
```
processed_point_cloud
├── id (INT, PK) — 변환 데이터 고유 번호
├── input_file_id (INT, FK → input_files.id) — 원본 LAS 파일 참조
├── project_id (CHAR(36), FK → projects.id) — 프로젝트 ID
├── process_type (VARCHAR(50), NULL) — 변환 방식: filtered, sampled, classified
├── processed_file_path (VARCHAR(500), NULL) — 변환된 파일 경로
├── converted_format (VARCHAR(50), NULL) — 변환 포맷: las, ply, laz 등
├── converted_file_path (VARCHAR(500), NULL) — 변환 포맷 저장 경로
├── point_count (INT, NULL) — 포인트 개수
├── min_z (FLOAT, NULL) — 최저 높이
├── max_z (FLOAT, NULL) — 최고 높이
├── mean_z (FLOAT, NULL) — 평균 높이
├── x_min (FLOAT, NULL) — 최소 경도
├── x_max (FLOAT, NULL) — 최대 경도
├── y_min (FLOAT, NULL) — 최소 위도
├── y_max (FLOAT, NULL) — 최대 위도
├── density_per_sqm (FLOAT, NULL) — 단위 면적당 포인트 밀도
├── classification_summary (LONGTEXT, NULL) — 분류 요약 (JSON)
├── processing_params (LONGTEXT, NULL) — 변환 파라미터 (JSON)
├── processed_at (TIMESTAMP) — 변환 완료 날짜
├── status (VARCHAR(50), NULL) — 상태: PROCESSING, COMPLETE, FAILED (기본값: PROCESSING)
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
### 4-2. surface_models 테이블 (지표면 모델)
```
surface_models
├── id (INT, PK) — 지표면 모델 고유 번호
├── project_id (CHAR(36), FK → projects.id) — 프로젝트 ID
├── model_type (VARCHAR(50), NULL) — 모델 종류: dem_grid, tin, mesh_triangulated, contour_lines
├── source_file_id (INT, FK → input_files.id, NULL) — 원본 LAS 파일
├── processed_cloud_id (INT, FK → processed_point_cloud.id, NULL) — 변환된 포인트클라우드 참조
├── status (VARCHAR(50), NULL) — 상태: PROCESSING, COMPLETE, FAILED (기본값: PROCESSING)
├── crs_epsg (INT, NULL) — 좌표계
├── resolution_m (FLOAT, NULL) — 해상도 (래스터인 경우)
├── model_file_path (VARCHAR(500), NULL) — 모델 저장 경로 (예: "storage/.../surface/dem.tif")
├── generation_params (LONGTEXT, NULL) — 생성 파라미터 (JSON)
├── created_at (TIMESTAMP) — 생성일
├── completed_at (TIMESTAMP, NULL) — 완료 시간
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
### 4-3. terrain_layers 테이블 (지형 레이어)
```
terrain_layers
├── id (INT, PK) — 레이어 고유 번호
├── surface_model_id (INT, FK → surface_models.id) — 지표면 모델 참조
├── layer_name (VARCHAR(100), NULL) — 레이어명 (예: "지표", "제1층", "제2층")
├── geometry_type (VARCHAR(50), NULL) — 기하 종류: POINTCLOUD, GRID, MESH, CONTOUR
├── layer_file_path (VARCHAR(500), NULL) — 레이어 파일 저장 경로
├── file_format (VARCHAR(50), NULL) — 파일 형식: geojson, geotiff, ply, obj 등
├── file_size_mb (FLOAT, NULL) — 파일 크기
├── statistics (LONGTEXT, NULL) — 통계 (JSON)
├── created_at (TIMESTAMP) — 생성일
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
---
## 5. 경로 & 설계 그룹
### 5-1. routes 테이블 (경로 설계)
```
routes
├── id (INT, PK) — 경로 고유 번호
├── project_id (CHAR(36), FK → projects.id) — 프로젝트 ID
├── surface_model_id (INT, FK → surface_models.id, NULL) — 기반 지표면 모델
├── status (VARCHAR(50), NULL) — 상태: DRAFT, CONFIRMED, ARCHIVED (기본값: DRAFT)
├── start_chainage_m (FLOAT, NULL) — 시작점 측점 (m)
├── end_chainage_m (FLOAT, NULL) — 종료점 측점 (m)
├── total_length_m (FLOAT, NULL) — 총 연장 (m)
├── grade_percent (LONGTEXT, NULL) — 각 구간 종단 경사도 (JSON array)
├── constraints (LONGTEXT, NULL) — 설계 제약조건 (JSON)
├── algorithm_params (LONGTEXT, NULL) — 알고리즘 파라미터 (JSON)
├── route_data_path (VARCHAR(500), NULL) — 경로 데이터 저장 경로 (GeoJSON)
├── computed_at (TIMESTAMP, NULL) — 계산 완료 날짜
├── created_at (TIMESTAMP) — 생성일
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
**참고:** 경로 좌표는 JSON 형식으로 `route_data_path` 파일에 저장됨 (GeoJSON LineString)
### 5-2. route_points 테이블 (경로 샘플 포인트)
```
route_points
├── id (INT, PK) — 포인트 고유 번호
├── route_id (INT, FK → routes.id) — 경로 ID
├── chainage_m (FLOAT, NULL) — 측점 (m)
├── elevation_m (FLOAT, NULL) — 높이
├── slope_percent (FLOAT, NULL) — 경사도 (%)
├── sequence_num (INT, NULL) — 순서번호
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
### 5-3. route_statistics 테이블 (경로 통계)
```
route_statistics
├── id (INT, PK) — 통계 고유 번호
├── route_id (INT, FK → routes.id) — 경로 ID
├── min_slope (FLOAT, NULL) — 최소 경사도 (%)
├── max_slope (FLOAT, NULL) — 최대 경사도 (%)
├── mean_slope (FLOAT, NULL) — 평균 경사도 (%)
├── cut_volume_m3 (FLOAT, NULL) — 절토량 (m³)
├── fill_volume_m3 (FLOAT, NULL) — 성토량 (m³)
├── tree_cutting_volume (FLOAT, NULL) — 목재 절감 추정량
└── cost_score (FLOAT, NULL) — 알고리즘 점수
```
---
## 6. 종단면 & 횡단면 그룹
### 6-1. longitudinal_sections 테이블 (종단면)
```
longitudinal_sections
├── id (INT, PK) — 종단면 고유 번호
├── project_id (CHAR(36), FK → projects.id) — 프로젝트 ID
├── route_id (INT, FK → routes.id) — 경로 ID
├── status (VARCHAR(50), NULL) — 상태: DRAFT, CONFIRMED (기본값: DRAFT)
├── data (LONGTEXT, NULL) — 상세 데이터 (JSON)
│ ├── chainages (측점 배열, m)
│ ├── elevations (지표 표고 배열)
│ ├── grades (경사도 배열, %)
│ └── design_elevations (설계 표고 배열)
├── longitudinal_file_path (VARCHAR(500), NULL) — 저장 경로 (JSON)
├── computed_at (TIMESTAMP, NULL) — 계산 날짜
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
### 6-2. cross_sections 테이블 (횡단면)
```
cross_sections
├── id (INT, PK) — 횡단면 고유 번호
├── project_id (CHAR(36), FK → projects.id) — 프로젝트 ID
├── route_id (INT, FK → routes.id) — 경로 ID
├── chainage_m (FLOAT, NULL) — 측점 (예: 0, 20, 40, ... m)
├── sequence_num (INT, NULL) — 횡단면 순번 (1, 2, ...)
├── status (VARCHAR(50), NULL) — 상태: DRAFT, CONFIRMED (기본값: DRAFT)
├── data (LONGTEXT, NULL) — 상세 데이터 (JSON)
│ ├── left_slope (좌측 사면 경사, %)
│ ├── right_slope (우측 사면 경사, %)
│ ├── width_m (노폭)
│ ├── cut_volume_m3 (절토량)
│ ├── fill_volume_m3 (성토량)
│ ├── structures (이 단면 내 구조물 ID 배열)
│ └── notes (메모)
├── cross_section_file_path (VARCHAR(500), NULL) — 저장 경로 (JSON)
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
---
## 7. 구조물 & 수량 그룹
### 7-1. structures 테이블 (구조물)
```
structures
├── id (INT, PK) — 구조물 고유 번호
├── project_id (CHAR(36), FK → projects.id) — 프로젝트 ID
├── cross_section_id (INT, FK → cross_sections.id, NULL) — 횡단면 ID
├── structure_type (VARCHAR(100), NULL) — 종류: 낙석방지책, 돌붙임, 계간수로, 낙차공 등
├── chainage_m (FLOAT, NULL) — 측점 (m)
├── location (VARCHAR(50), NULL) — 위치: LEFT, CENTER, RIGHT
├── length_m (FLOAT, NULL) — 길이
├── width_m (FLOAT, NULL) — 폭
├── height_m (FLOAT, NULL) — 높이
├── material (VARCHAR(100), NULL) — 재료: 강재, 콘크리트, 목재, 돌 등
├── quantity (INT, NULL) — 개수
├── unit_price (FLOAT, NULL) — 단가
├── design_notes (LONGTEXT, NULL) — 설계 노트 (JSON)
├── structure_data_path (VARCHAR(500), NULL) — 구조물 데이터 저장 경로
├── last_modified_by (INT, FK → users.id, NULL) — 마지막 수정자
├── created_at (TIMESTAMP) — 생성일
├── updated_at (TIMESTAMP) — 수정일
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
### 7-2. quantity_items 테이블 (수량 항목)
```
quantity_items
├── id (INT, PK) — 수량 항목 고유 번호
├── project_id (CHAR(36), FK → projects.id) — 프로젝트 ID
├── category (VARCHAR(100), NULL) — 대분류: 토공, 구조물, 포장, 배수, 녹화, 안전시설
├── item_name (VARCHAR(255), NULL) — 항목명 (예: "절토 일반", "낙석방지책 설치")
├── unit (VARCHAR(50), NULL) — 단위: m3, 개, m, m2
├── quantity_design (FLOAT, NULL) — 설계 수량
├── quantity_actual (FLOAT, NULL) — 실제 수량 (사용자 수정 가능)
├── unit_price (FLOAT, NULL) — 단가
├── total_price (FLOAT, NULL) — 소계 (quantity_actual × unit_price)
├── standard_reference (VARCHAR(255), NULL) — 기준 참고 자료
├── quantity_data_path (VARCHAR(500), NULL) — 수량 데이터 저장 경로
├── data (LONGTEXT, NULL) — 계산 과정 메모 (JSON)
│ ├── formula (계산식)
│ ├── source (데이터 출처)
│ └── ...
├── computed_at (TIMESTAMP, NULL) — 계산 날짜
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
---
## 8. 산출물 & 문서 그룹
### 8-1. outputs 테이블 (최종 산출물 세트)
```
outputs
├── id (INT, PK) — 산출물 세트 고유 번호
├── project_id (CHAR(36), FK → projects.id) — 프로젝트 ID
├── output_type (VARCHAR(100), NULL) — 종류: estimation_excel, drawing_dxf, report_pdf, all_bundle
├── status (VARCHAR(50), NULL) — 상태: GENERATING, COMPLETE, FAILED (기본값: GENERATING)
├── generated_by (INT, FK → users.id, NULL) — 생성자
├── generated_at (TIMESTAMP) — 생성 날짜
├── version (INT, NULL) — 버전 (기본값: 1)
├── outputs_directory_path (VARCHAR(500), NULL) — 산출물 저장 폴더 (예: "storage/.../outputs/v1/")
├── metadata (LONGTEXT, NULL) — 메타데이터 (JSON)
│ ├── template_used (사용한 양식)
│ ├── company_name (회사명)
│ ├── project_name (프로젝트명)
│ ├── total_cost (총 비용)
│ └── generation_time_sec (생성 소요 시간)
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
### 8-2. output_files 테이블 (최종 산출 파일)
```
output_files
├── id (INT, PK) — 파일 고유 번호
├── output_id (INT, FK → outputs.id) — 산출물 ID
├── file_type (VARCHAR(50), NULL) — 파일 형식: xlsx, pdf, dxf, dwg, json, zip
├── original_filename (VARCHAR(255), NULL) — 파일명 (예: "견적서_v1.xlsx")
├── output_file_path (VARCHAR(500), NULL) — 저장 경로 (예: "storage/.../outputs/v1/견적서.xlsx")
├── file_size_mb (FLOAT, NULL) — 파일 크기
├── download_count (INT, NULL) — 다운로드 횟수 (기본값: 0)
├── created_at (TIMESTAMP) — 생성 날짜
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
---
## 9. 변경 이력 & 감시 그룹
### 9-1. change_logs 테이블 (설계 변경 기록)
```
change_logs
├── id (INT, PK) — 변경 고유 번호
├── project_id (CHAR(36), FK → projects.id) — 프로젝트 ID
├── changed_by (INT, FK → users.id) — 변경자
├── changed_at (TIMESTAMP) — 변경 날짜
├── entity_type (VARCHAR(100)) — 대상 타입: routes, cross_sections, structures, quantity_items
├── entity_id (VARCHAR(50)) — 대상 ID
├── old_value (LONGTEXT, NULL) — 변경 전 값 (JSON)
├── new_value (LONGTEXT, NULL) — 변경 후 값 (JSON)
├── reason (TEXT, NULL) — 변경 사유
└── deleted_at (TIMESTAMP, NULL) — 삭제일 (soft delete)
```
---
## 10. 테이블 관계도
```
users (사용자)
├── id (PK)
├── email
├── password_hash
├── role (SYSTEM_ADMIN | ADMIN | USER)
├── is_master
├── status (생명주기)
├── company_id (FK) ──→ companies.id
├── last_login
└── auth_expires_at
companies (회사)
├── id (PK)
├── name
├── status (ACTIVE | INACTIVE | SUSPENDED)
├── subscription_status (FREE | TRIAL | PAID | EXPIRED)
├── created_by (FK) ──→ users.id
└── created_at
projects (프로젝트)
├── id (UUID, PK)
├── user_id (FK) ──→ users.id
├── company_id (FK) ──→ companies.id
├── name
├── status
├── crs_epsg
├── storage_path
└── created_at
├─→ input_files (입력 파일)
│ └─→ processed_point_cloud (변환된 포인트클라우드)
│ └─→ surface_models (지표면 모델)
│ └─→ terrain_layers (지형 레이어)
├─→ routes (경로)
│ ├─→ route_points (경로 포인트)
│ ├─→ route_statistics (경로 통계)
│ ├─→ longitudinal_sections (종단면)
│ └─→ cross_sections (횡단면)
│ └─→ structures (구조물)
├─→ quantity_items (수량 항목)
├─→ outputs (산출물)
│ └─→ output_files (산출물 파일)
├─→ project_versions (프로젝트 버전)
├─→ project_automations (프로젝트 자동화)
├─→ audit_logs (감시 로그)
└─→ change_logs (변경 로그)
```
---
## 11. 파일시스템 경로와 DB 링크 (워크플로우 기반 구조)
### 파일시스템 구조 (6단계 워크플로우)
```
storage/
├── {company_slug}/
│ └── {user_slug}/
│ └── {project_id}/ ← projects.storage_path
│ │
│ ├── B03_FileInput/ ← WF0: 파일 입력
│ │ └── input/ (원본 입력 파일들)
│ │ ├── las/
│ │ │ └── cloud_merged.las ← input_files.raw_file_path
│ │ ├── tif/
│ │ ├── tfw/
│ │ ├── prj/
│ │ └── dxf/
│ │
│ ├── B04_wf1_Surface/ ← WF1: 지표면 분석
│ │ ├── processed/ (변환된 포인트클라우드)
│ │ │ ├── cloud_filtered.las ← processed_point_cloud.processed_file_path
│ │ │ └── cloud_sampled.ply ← processed_point_cloud.converted_file_path
│ │ └── models/ (지표면 모델 & 레이어)
│ │ ├── dem_2m.tif ← surface_models.model_file_path
│ │ ├── tin_mesh.obj
│ │ ├── layer_ground.geojson ← terrain_layers.layer_file_path
│ │ └── layer_1.geojson
│ │
│ ├── B05_wf2_Route/ ← WF2: 경로 설계
│ │ └── route/
│ │ ├── route_main.geojson ← routes.route_data_path
│ │ ├── route_points.json
│ │ └── route_statistics.json
│ │
│ ├── B06_wf3_ProfileCross/ ← WF3: 종횡단 생성
│ │ ├── longitudinal/
│ │ │ └── longitudinal.json ← longitudinal_sections.longitudinal_file_path
│ │ └── cross_sections/
│ │ ├── cross_0000m.json ← cross_sections.cross_section_file_path
│ │ ├── cross_0020m.json
│ │ └── ...
│ │
│ ├── B07_wf4_DesignDetail/ ← WF4: 상세 설계
│ │ └── structures/
│ │ ├── struct_0020m_001.json ← structures.structure_data_path
│ │ └── ...
│ │
│ ├── B08_wf5_Quantity/ ← WF5: 수량 산출
│ │ ├── quantities/
│ │ │ └── items.json ← quantity_items.quantity_data_path
│ │ └── ...
│ │
│ └── B09_wf6_Estimation/ ← WF6: 견적·문서
│ ├── v1/ ← outputs.outputs_directory_path
│ │ ├── 견적서.xlsx ← output_files.output_file_path
│ │ ├── 설계도.dxf
│ │ └── ...
│ └── v2/
│ └── ...
```
**핵심 개념:**
- **B03~B09**: 워크플로우 6단계와 1:1 대응
- **각 단계의 입력/출력**: 명확히 분리
- **버전 관리**: B09 단계에서만 (v1, v2, ...)
---
## 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.status
FROM routes r
WHERE r.project_id = :project_id AND r.deleted_at IS NULL
ORDER BY r.computed_at DESC
LIMIT 1;
```
### 예제 3: 경로의 모든 횡단면 조회
```sql
SELECT cs.id, cs.chainage_m, cs.data
FROM cross_sections cs
WHERE cs.route_id = :route_id AND cs.deleted_at IS NULL
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 AND deleted_at IS NULL
GROUP BY category;
```
### 예제 5: 사용자의 감시 로그 (최근 30일)
```sql
SELECT action, entity_type, timestamp
FROM audit_logs
WHERE user_id = :user_id AND timestamp > DATE_SUB(NOW(), INTERVAL 30 DAY)
ORDER BY timestamp DESC;
```
---
## 13. 데이터 타입 가이드
| 타입 | 설명 | 예제 |
|------|------|------|
| INT | 정수 | 1, 2, 100 |
| FLOAT | 실수 | 1.5, 2.25, 100.75 |
| VARCHAR(n) | 문자열 (고정 길이) | "project_name" |
| TEXT / LONGTEXT | 문자열 (가변, 대용량) | 설명문, JSON 데이터 |
| TIMESTAMP | 날짜 + 시간 (자동 업데이트) | 2025-07-05 15:30:00 |
| DATETIME | 날짜 + 시간 (수동) | 2025-07-05 15:30:00 |
| TINYINT(1) | 참/거짓 (0 또는 1) | 0, 1 |
| ENUM | 열거형 | ACTIVE, INACTIVE |
| CHAR(36) | UUID (고정 36자) | 550e8400-e29b-41d4-a716-446655440000 |
---
## 14. 총 테이블 수 및 분류
### 34개 테이블 분류
**1. 사용자/회사/조직 (14개)**
- users, companies, join_requests (향후), email_otps, sessions, trusted_devices, user_consents
- login_logs, activity_logs, audit_logs, system_audit_logs, system_resources, support_requests
**2. 프로젝트 관리 (3개)**
- projects, project_versions, project_automations
**3. 파일/업로드 (3개)**
- upload_sessions, upload_chunks, input_files
**4. 지표면 분석 (3개)**
- processed_point_cloud, surface_models, terrain_layers
**5. 경로 설계 (3개)**
- routes, route_points, route_statistics
**6. 종횡단 설계 (2개)**
- longitudinal_sections, cross_sections
**7. 구조물/수량 (2개)**
- structures, quantity_items
**8. 산출물 (2개)**
- outputs, output_files
**9. 변경/이력 (1개)**
- change_logs
---
## 15. DB vs 파일시스템 역할 분리
| 저장 위치 | 내용 | 예시 |
|----------|------|------|
| **DB (MariaDB)** | 메타데이터 + 경로정보 + 상태 | 파일명, 크기, 좌표계, 상태, 저장경로, 통계 |
| **파일시스템** | 실제 파일 데이터 | LAS, PLY, TIF, DXF, Excel, PDF, JSON 파일들 |
**설계 원칙:**
- DB에는 "어디에 무엇이 있는가"의 메타정보만 저장
- 실제 대용량 파일은 파일시스템에 저장
- 각 워크플로우 단계의 입력/출력 경로를 DB에 기록
- JSON 형식으로 좌표 및 복잡한 데이터 구조를 저장
---
## 16. 마이그레이션 상태
### ✅ 완료됨
- [x] 33개 테이블 생성 (MariaDB)
- [x] 관계(FK) 설정
- [x] Soft delete (deleted_at) 지원
- [x] 인덱스 추가 (주요 조회 경로)
### 📋 진행 중
- [ ] API 라우터에서 파일 경로 저장 로직 추가
- B03 파일 업로드 → `input_files.raw_file_path` 저장
- B04 WF1 실행 → `surface_models.model_file_path`, `terrain_layers.layer_file_path` 저장
- B05 WF2 실행 → `routes.route_data_path` 저장
- B06 WF3 실행 → `longitudinal_sections.longitudinal_file_path`, `cross_sections.cross_section_file_path` 저장
- B07 WF4 실행 → `structures.structure_data_path` 저장
- B08 WF5 실행 → `quantity_items.quantity_data_path` 저장
- B09 WF6 실행 → `outputs.outputs_directory_path`, `output_files.output_file_path` 저장
### 🔮 향후 계획
- [ ] Python Pydantic 모델 정의 (ORM 대신)
- [ ] API 엔드포인트 보안 감시 강화
- [ ] 실시간 알림 시스템 (WebSocket)
+445
View File
@@ -0,0 +1,445 @@
# Wiza — Style Reference
> Twilight prospecting observatory bathed in lavender mist
**Theme:** light
Wiza operates in a light-mode universe where deep violet ink commands attention against a soft lavender atmosphere. The brand lives between a confident enterprise tool and an approachable consumer product—white surfaces, thin borders, and compact Inter type for dense data interfaces, paired with Britti Sans display headlines that feel geometric and assured. The signature move is the gradient wash: pale lavender backgrounds bleeding white-to-violet-to-peach, making even a sprawling data table feel sunlit. Purple is reserved for authority—headlines, logo, filled actions—while the rest of the UI stays in quiet neutrals, letting contact data breathe.
## Tokens — Colors
| Name | Value | Token | Role |
|------|-------|-------|------|
| Deep Iris | `#26114a` | `--color-deep-iris` | Violet supporting accent for decorative details and low-frequency emphasis. Do not promote it to the primary CTA color |
| Plum Velvet | `#312749` | `--color-plum-velvet` | Navigation text, secondary headings, card titles, footer copy — slightly lighter companion to Deep Iris |
| Royal Amethyst | `#3e0079` | `--color-royal-amethyst` | Links, input focus rings, accent strokes, icon highlights — the most saturated violet |
| Mist Violet | `#edecff` | `--color-mist-violet` | Soft highlight washes, tinted section backgrounds, decorative blurs — the lavender atmosphere |
| Lavender Wash | `linear-gradient(90deg, rgb(185, 154, 255), rgb(125, 67, 255) 10%, rgba(125, 67, 255, 0))` | `--color-lavender-wash` | Decorative gradient stops and hero atmosphere |
| Twilight Beam | `linear-gradient(to right, rgb(207, 138, 255), rgb(255, 102, 193), rgb(255, 173, 116), rgb(170, 129, 255))` | `--color-twilight-beam` | Multi-stop decorative gradient for hero glow effects |
| Canvas | `#ffffff` | `--color-canvas` | Page background, card surfaces, input fields, button text on dark fills |
| Paper | `#f6f7fa` | `--color-paper` | Subtle surface lift, table row alternation, secondary card backgrounds |
| Mist | `#e6e2e3` | `--color-mist` | Hairline borders, dividers, subtle separators |
| Smoke | `#c1c7cf` | `--color-smoke` | Disabled states, skeleton placeholders, muted background blocks |
| Ash | `#9491a1` | `--color-ash` | Muted helper text, placeholder copy, secondary metadata |
| Slate | `#615e6e` | `--color-slate` | Secondary body text, descriptions, table metadata |
| Charcoal | `#333333` | `--color-charcoal` | Body text, link text, icon strokes in neutral contexts |
| Carbon | `#222222` | `--color-carbon` | Dark UI elements, near-black accents |
| Ink | `#000000` | `--color-ink` | Maximum contrast text and borders in specific dark UI elements |
## Tokens — Typography
### Britti Sans — Display and heading typeface used exclusively at 2464px, always weight 500. The 1.0 line-height is distinctive — headings sit tight with no leading slack, creating geometric, almost architectural presence. Substitute with Plus Jakarta Sans (500) or General Sans (500) for closest geometric match. · `--font-britti-sans`
- **Substitute:** Plus Jakarta Sans
- **Weights:** 500
- **Sizes:** 24px, 32px, 40px, 56px, 64px
- **Line height:** 1.00
- **Role:** Display and heading typeface used exclusively at 2464px, always weight 500. The 1.0 line-height is distinctive — headings sit tight with no leading slack, creating geometric, almost architectural presence. Substitute with Plus Jakarta Sans (500) or General Sans (500) for closest geometric match.
### Inter — Workhorse UI and body typeface. Weight 400 for body, 500 for buttons/nav/table headers, 700 for emphasized labels. The tight size range (1216px) and compact line-heights signal a data-dense product, not a marketing site. Substitute with Inter directly or IBM Plex Sans. · `--font-inter`
- **Substitute:** Inter
- **Weights:** 400, 500, 700
- **Sizes:** 12px, 14px, 16px
- **Line height:** 1.301.50
- **Role:** Workhorse UI and body typeface. Weight 400 for body, 500 for buttons/nav/table headers, 700 for emphasized labels. The tight size range (1216px) and compact line-heights signal a data-dense product, not a marketing site. Substitute with Inter directly or IBM Plex Sans.
### Type Scale
| Role | Size | Line Height | Letter Spacing | Token |
|------|------|-------------|----------------|-------|
| caption | 12px | 1.3 | — | `--text-caption` |
| body-sm | 14px | 1.4 | — | `--text-body-sm` |
| body | 16px | 1.5 | — | `--text-body` |
| subheading | 24px | 1 | — | `--text-subheading` |
| heading-sm | 32px | 1 | — | `--text-heading-sm` |
| heading | 40px | 1 | — | `--text-heading` |
| heading-lg | 56px | 1 | — | `--text-heading-lg` |
| display | 64px | 1 | — | `--text-display` |
## Tokens — Spacing & Shapes
**Base unit:** 8px
**Density:** compact
### Spacing Scale
| Name | Value | Token |
|------|-------|-------|
| 8 | 8px | `--spacing-8` |
| 16 | 16px | `--spacing-16` |
| 24 | 24px | `--spacing-24` |
| 32 | 32px | `--spacing-32` |
| 40 | 40px | `--spacing-40` |
| 48 | 48px | `--spacing-48` |
| 56 | 56px | `--spacing-56` |
| 64 | 64px | `--spacing-64` |
| 96 | 96px | `--spacing-96` |
| 104 | 104px | `--spacing-104` |
| 216 | 216px | `--spacing-216` |
### Border Radius
| Element | Value |
|---------|-------|
| cards | 8px |
| icons | 8px |
| large | 24px |
| pills | 1440px |
| inputs | 8px |
| buttons | 8px |
### Shadows
| Name | Value | Token |
|------|-------|-------|
| sm | `rgba(18, 55, 105, 0.08) 0px 2px 4px 0px, rgba(18, 55, 105...` | `--shadow-sm` |
| lg | `rgba(14, 59, 101, 0.06) 0px 32px 24px -12px, rgba(14, 59,...` | `--shadow-lg` |
| lg-2 | `rgba(114, 49, 255, 0.32) 0px -6px 20px 0px inset, rgba(47...` | `--shadow-lg-2` |
| sm-2 | `rgba(18, 55, 105, 0.08) 0px 2px 4px 0px, rgba(18, 55, 105...` | `--shadow-sm-2` |
| sm-3 | `rgba(47, 1, 151, 0.01) 0px 11px 4px 0px, rgba(47, 1, 151,...` | `--shadow-sm-3` |
### Layout
- **Page max-width:** 1200px
- **Section gap:** 64-80px
- **Card padding:** 16-24px
- **Element gap:** 8px
## Components
### Filled Brand Button
**Role:** Primary action trigger in hero and CTA blocks
Background #26114a, text #ffffff, 8px radius, 16px vertical / 24px horizontal padding, Inter 500 at 16px. Carries the brand weight — only used for the most important conversion per screen.
### Ghost Outlined Button
**Role:** Secondary action paired beside a filled button
Background transparent, border 1px #26114a, text #26114a, 8px radius, 16px/24px padding, Inter 500 at 16px. Matches the filled button's exact dimensions for optical pairing.
### Pill Navigation Button
**Role:** Top-level nav items in the floating header
1440px radius, 8px/16px padding, Inter 500 at 14px, text #312749. Active state may use #edecff background. The full-pill shape is distinctive — not rounded rectangles, but true capsules.
### Logo Lockup
**Role:** Brand identity in header and footer
Purple triangular icon mark paired with 'wiza' wordmark in Britti Sans 500 or Inter 700. Icon uses a gradient from #b99aff to #7d43ff. Appears in a white pill container with 8px radius and subtle shadow.
### Product Screenshot Frame
**Role:** Hero product demonstration and feature illustrations
White surface with 8px radius, surrounded by the deep layered shadow stack (rgba(14,59,101,0.06) large blur). Floats over the lavender gradient background, creating depth without borders.
### Data Table Row
**Role:** Contact/prospect list display in product mockups
Alternating #ffffff and #f6f7fa backgrounds, 1px #e6e2e3 bottom border, 12px/16px cell padding, Inter 400 at 14px for data, Inter 500 for headers. Dense, compact rows with colored status dots.
### Logo Cloud Bar
**Role:** Social proof — client brand logos
Monochrome (Charcoal #333333) logo strip on white background, centered, with consistent 4060px height per logo. No colorful logos — all desaturated for visual calm.
### Rating Badge Pair
**Role:** Third-party validation scores (G2, Chrome Web Store)
White pill cards (1440px radius), Inter 500 at 14px for score, 12px for platform name. Star icon in #3e0079 or #f5b800. Displayed in a centered pair with 24px gap.
### Section Heading Block
**Role:** Intro text for content sections
Optional pill tag above (Mist Violet #edecff background, Royal Amethyst #3e0079 text, 1440px radius, Inter 500 12px, 4px/12px padding). Main heading in Britti Sans 500 at 4056px, Deep Iris #26114a. Subtext in Inter 400 at 1618px, Slate #615e6e.
### Profile Card (Magic Wand Feature)
**Role:** Contact preview cards in feature illustrations
White surface, 8px radius, subtle border, 12px/16px padding. Contains circular avatar (32px), name in Inter 500 14px #26114a, title in Inter 400 12px #615e6, company in Inter 400 12px #9491a1. Arranged in 3-column grid with 16px gaps.
### Floating Navigation Bar
**Role:** Sticky header container
White background with 8px radius, floats over hero with the layered shadow. Contains logo, nav links (Pill Navigation Button style), and login/signup actions. Max-width contained with side padding.
### Tag/Badge (Pill)
**Role:** Feature labels, job titles, status indicators
1440px radius, 4px/12px padding, Inter 500 at 12px. Variants: #edecff bg with #3e0079 text (accent tags), #f6f7fa bg with #615e6 text (neutral tags), or status-specific colors.
### Input Field
**Role:** Search and form inputs in product UI
1px #e6e2e3 border, 8px radius, 10px/14px padding, Inter 400 at 14px, placeholder in Ash #9491a1. Focus state uses 2px Royal Amethyst #3e0079 border ring.
### Feature Icon Tile
**Role:** Small decorative icon containers in section intros
16px or 24px square tiles with 8px radius, Mist Violet #edecff background, purple stroke icon inside. Used sparingly to mark section types.
## Do's and Don'ts
### Do
- Use 8px radius for all rectangular UI surfaces — cards, buttons, inputs, icon containers. The 8px is the signature, not 4px or 12px.
- Use 1440px radius exclusively for pills, tags, and capsule-shaped nav items — never for cards or panels.
- Set Britti Sans headings to line-height 1.0 exactly. The tight leading is what makes the type feel architectural.
- Use Deep Iris #26114a for headlines and filled button backgrounds. Never use it for body text larger than 24px — it overwhelms at small sizes.
- Pair every filled button with an adjacent ghost button of identical dimensions for the hero CTA pattern.
- Anchor section intros with a small pill tag above the heading in Mist Violet + Royal Amethyst.
- Use the Lavender Wash gradient on hero and feature section backgrounds — it defines the atmosphere.
- Keep Inter at 1216px for all UI text. The compact size range signals a data-dense product.
### Don't
- Do not use Royal Amethyst #3e0079 for body text — it vibrates uncomfortably at small sizes; reserve it for links, icons, and accent strokes.
- Do not apply 4px or 12px radius to cards — the 8px is non-negotiable for visual consistency.
- Do not use line-height above 1.0 for Britti Sans headings — the tightness is the point.
- Do not introduce new chromatic colors outside the violet family — the palette is deliberately monochromatic-violet.
- Do not use heavy drop shadows. The blue-tinted shadows are subtle (max 32px blur at 6% opacity).
- Do not center body text in multi-line paragraphs — only headlines and short intros get center alignment.
- Do not place dark text directly on the lavender gradient — use a white surface card between them for legibility.
- Do not use rounded images or photos in feature illustrations — the design is iconographic, not photographic.
## Surfaces
| Level | Name | Value | Purpose |
|-------|------|-------|---------|
| 0 | Canvas | `#ffffff` | Page background, outermost layer |
| 1 | Paper | `#f6f7fa` | Card surfaces, table containers, subtle lifts |
| 2 | Mist Violet | `#edecff` | Tinted highlight sections, gradient start point |
| 3 | Lavender Glow | `#b99aff` | Hero gradient stops, atmospheric overlays |
## Elevation
- **Cards and elevated panels:** `rgba(14, 59, 101, 0.06) 0px 32px 24px -12px, rgba(14, 59, 101, 0.01) 0px 11px 4px 0px, rgba(14, 59, 101, 0.02) 0px 6px 4px 0px, rgba(14, 59, 101, 0.03) 0px 3px 3px 0px, rgba(14, 59, 101, 0.04) 0px 1px 1px 0px`
- **Buttons and nav items:** `rgba(18, 55, 105, 0.08) 0px 2px 4px 0px, rgba(18, 55, 105, 0.04) 0px 1px 1px 0px, rgba(18, 55, 105, 0.08) 0px 0px 0px 1px`
- **Product feature highlights:** `rgba(114, 49, 255, 0.32) 0px -6px 20px 0px inset, rgba(47, 1, 151, 0.12) 0px 11px 12px 0px, rgba(47, 1, 151, 0.12) 0px 3px 3px 0px, rgba(47, 1, 151, 0.12) 0px 1px 1px 0px, rgba(47, 1, 151, 0.08) 0px 0px 0px 1px`
## Imagery
The visual language is almost entirely product-screenshot and iconographic — no lifestyle photography, no stock imagery, no people in context. Product mockups show a contact-prospecting data table with purple status indicators, person avatars, and company logos. Decorative visuals use soft purple gradient washes with floating profile cards arranged in grid patterns. Icons are simple, single-stroke, geometric shapes (a triangle logo, outlined feature icons). Logos in the trust bar are desaturated to monochrome. The overall feel is that of a polished SaaS dashboard screenshot rather than a marketing site with imagery.
## Layout
Centered, max-width 1200px container with generous vertical breathing room (6480px section gaps). The hero is a full-bleed lavender-to-white gradient with a centered headline stack and dual CTA buttons, followed by a floating product screenshot that breaks the container edge slightly. Below the fold, content alternates between white surface sections and light lavender tinted sections. The logo cloud is a single centered row. Feature sections use centered heading blocks followed by 2-column or 3-column card grids. A sticky floating navigation bar (pill-shaped, centered) sits over the hero.
## Agent Prompt Guide
primary action: no distinct CTA color
**Quick Color Reference**
- text (primary): #26114a
- text (secondary): #615e6e
- text (muted): #9491a1
- background: #ffffff
- surface: #f6f7fa
- border: #e6e2e3
- brand/accent: #3e0079
- filled button background: #26114a
**Example Component Prompts**
1. **Hero Section**: White-to-lavender gradient background (linear-gradient from #ffffff to #edecff). Centered headline 'Find verified emails and phone numbers' in Britti Sans weight 500 at 56px, color #26114a, line-height 1.0. Subtext in Inter 400 at 18px, color #615e6e. Two buttons side by side: filled button (background #26114a, text #ffffff, 8px radius, 16px/24px padding, Inter 500 16px) and ghost button (border 1px #26114a, text #26114a, same dimensions). Below: a product screenshot framed in a white card with 8px radius and the deep blue-tinted shadow.
2. **Data Table Card**: White surface card, 8px radius, 1px #e6e2e3 border, 16px/24px padding. Table header row in Inter 500 at 12px, color #615e6e, uppercase, with 1px #e6e2e3 bottom border. Data rows alternate #ffffff and #f6f7fa backgrounds, Inter 400 at 14px, color #26114a. Action buttons in each row: small pill tags with 1440px radius, #edecff background, #3e0079 text, Inter 500 at 12px.
3. **Section Heading with Pill Tag**: Optional pill tag above heading: 1440px radius, #edecff background, #3e0079 text, Inter 500 at 12px, 4px/12px padding. Main heading in Britti Sans 500 at 40px, #26114a, line-height 1.0. Subtext in Inter 400 at 16px, #615e6e. All centered.
4. **Profile Preview Card**: White surface, 8px radius, 1px #e6e2e3 border, 12px/16px padding. 32px circular avatar at top-left. Name in Inter 500 at 14px, #26114a. Title in Inter 400 at 12px, #615e6e. Company in Inter 400 at 12px, #9491a1. Arranged in 3-column grid with 16px gaps, centered in section.
5. **Trust Logo Bar**: Single horizontal row of 6 client logos, all rendered in monochrome #333333, centered, each logo 4050px height. White background, 64px vertical padding above and below. Below: two rating badge pills (1440px radius, white bg, subtle border) with star icon and score in Inter 500 14px, platform name in Inter 400 12px #615e6e.
## Gradient System
The lavender atmosphere is built from three gradient layers:
- **Hero wash**: linear-gradient(90deg, #b99aff, #7d43ff 10%, transparent) — fades right, creating a directional glow
- **Decorative multi-stop**: linear-gradient(to right, #cf8aff, #ff66c1, #ffad74, #ff63d1, #aa81ff) — used sparingly for special highlights
- **Section tint**: subtle white-to-#edecff vertical fade for alternating content sections
Gradients should always sit behind white content surfaces — never behind text directly.
## Similar Brands
- **Apollo.io** — Same deep-violet brand color, similar data-prospecting product, and comparable lavender gradient hero treatment
- **Lusha** — Shared violet/indigo palette, compact data table UI, and light-mode product-first layout
- **Clearbit** — Similar light-mode SaaS with subtle gradient backgrounds and confident sans-serif display type
- **ZoomInfo** — Enterprise data product with dense tabular UI, compact Inter type, and restrained brand color usage
- **Cognism** — Purple-tinted brand identity with soft gradient hero sections and data-table product showcases
## Quick Start
### CSS Custom Properties
```css
:root {
/* Colors */
--color-deep-iris: #26114a;
--color-plum-velvet: #312749;
--color-royal-amethyst: #3e0079;
--color-mist-violet: #edecff;
--color-lavender-wash: #b99aff;
--gradient-lavender-wash: linear-gradient(90deg, rgb(185, 154, 255), rgb(125, 67, 255) 10%, rgba(125, 67, 255, 0));
--color-twilight-beam: #cf8aff;
--gradient-twilight-beam: linear-gradient(to right, rgb(207, 138, 255), rgb(255, 102, 193), rgb(255, 173, 116), rgb(170, 129, 255));
--color-canvas: #ffffff;
--color-paper: #f6f7fa;
--color-mist: #e6e2e3;
--color-smoke: #c1c7cf;
--color-ash: #9491a1;
--color-slate: #615e6e;
--color-charcoal: #333333;
--color-carbon: #222222;
--color-ink: #000000;
/* Typography — Font Families */
--font-britti-sans: 'Britti Sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-inter: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
/* Typography — Scale */
--text-caption: 12px;
--leading-caption: 1.3;
--text-body-sm: 14px;
--leading-body-sm: 1.4;
--text-body: 16px;
--leading-body: 1.5;
--text-subheading: 24px;
--leading-subheading: 1;
--text-heading-sm: 32px;
--leading-heading-sm: 1;
--text-heading: 40px;
--leading-heading: 1;
--text-heading-lg: 56px;
--leading-heading-lg: 1;
--text-display: 64px;
--leading-display: 1;
/* Typography — Weights */
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-bold: 700;
/* Spacing */
--spacing-unit: 8px;
--spacing-8: 8px;
--spacing-16: 16px;
--spacing-24: 24px;
--spacing-32: 32px;
--spacing-40: 40px;
--spacing-48: 48px;
--spacing-56: 56px;
--spacing-64: 64px;
--spacing-96: 96px;
--spacing-104: 104px;
--spacing-216: 216px;
/* Layout */
--page-max-width: 1200px;
--section-gap: 64-80px;
--card-padding: 16-24px;
--element-gap: 8px;
/* Border Radius */
--radius-sm: 2px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-2xl: 16px;
--radius-3xl: 24px;
--radius-3xl-2: 28px;
--radius-3xl-3: 40px;
--radius-full: 56px;
--radius-full-2: 1440px;
/* Named Radii */
--radius-cards: 8px;
--radius-icons: 8px;
--radius-large: 24px;
--radius-pills: 1440px;
--radius-inputs: 8px;
--radius-buttons: 8px;
/* Shadows */
--shadow-sm: rgba(18, 55, 105, 0.08) 0px 2px 4px 0px, rgba(18, 55, 105, 0.04) 0px 1px 1px 0px, rgba(18, 55, 105, 0.08) 0px 0px 0px 1px;
--shadow-lg: rgba(14, 59, 101, 0.06) 0px 32px 24px -12px, rgba(14, 59, 101, 0.01) 0px 11px 4px 0px, rgba(14, 59, 101, 0.02) 0px 6px 4px 0px, rgba(14, 59, 101, 0.03) 0px 3px 3px 0px, rgba(14, 59, 101, 0.04) 0px 1px 1px 0px;
--shadow-lg-2: rgba(114, 49, 255, 0.32) 0px -6px 20px 0px inset, rgba(47, 1, 151, 0.12) 0px 11px 12px 0px, rgba(47, 1, 151, 0.12) 0px 3px 3px 0px, rgba(47, 1, 151, 0.12) 0px 1px 1px 0px, rgba(47, 1, 151, 0.08) 0px 0px 0px 1px;
--shadow-sm-2: rgba(18, 55, 105, 0.08) 0px 2px 4px 0px, rgba(18, 55, 105, 0.04) 0px 1px 1px 0px;
--shadow-sm-3: rgba(47, 1, 151, 0.01) 0px 11px 4px 0px, rgba(47, 1, 151, 0.04) 0px 6px 4px 0px, rgba(47, 1, 151, 0.06) 0px 3px 3px 0px, rgba(47, 1, 151, 0.08) 0px 1px 1px 0px, rgba(47, 1, 151, 0.08) 0px 0px 0px 1px;
/* Surfaces */
--surface-canvas: #ffffff;
--surface-paper: #f6f7fa;
--surface-mist-violet: #edecff;
--surface-lavender-glow: #b99aff;
}
```
### Tailwind v4
```css
@theme {
/* Colors */
--color-deep-iris: #26114a;
--color-plum-velvet: #312749;
--color-royal-amethyst: #3e0079;
--color-mist-violet: #edecff;
--color-lavender-wash: #b99aff;
--color-twilight-beam: #cf8aff;
--color-canvas: #ffffff;
--color-paper: #f6f7fa;
--color-mist: #e6e2e3;
--color-smoke: #c1c7cf;
--color-ash: #9491a1;
--color-slate: #615e6e;
--color-charcoal: #333333;
--color-carbon: #222222;
--color-ink: #000000;
/* Typography */
--font-britti-sans: 'Britti Sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-inter: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
/* Typography — Scale */
--text-caption: 12px;
--leading-caption: 1.3;
--text-body-sm: 14px;
--leading-body-sm: 1.4;
--text-body: 16px;
--leading-body: 1.5;
--text-subheading: 24px;
--leading-subheading: 1;
--text-heading-sm: 32px;
--leading-heading-sm: 1;
--text-heading: 40px;
--leading-heading: 1;
--text-heading-lg: 56px;
--leading-heading-lg: 1;
--text-display: 64px;
--leading-display: 1;
/* Spacing */
--spacing-8: 8px;
--spacing-16: 16px;
--spacing-24: 24px;
--spacing-32: 32px;
--spacing-40: 40px;
--spacing-48: 48px;
--spacing-56: 56px;
--spacing-64: 64px;
--spacing-96: 96px;
--spacing-104: 104px;
--spacing-216: 216px;
/* Border Radius */
--radius-sm: 2px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-2xl: 16px;
--radius-3xl: 24px;
--radius-3xl-2: 28px;
--radius-3xl-3: 40px;
--radius-full: 56px;
--radius-full-2: 1440px;
/* Shadows */
--shadow-sm: rgba(18, 55, 105, 0.08) 0px 2px 4px 0px, rgba(18, 55, 105, 0.04) 0px 1px 1px 0px, rgba(18, 55, 105, 0.08) 0px 0px 0px 1px;
--shadow-lg: rgba(14, 59, 101, 0.06) 0px 32px 24px -12px, rgba(14, 59, 101, 0.01) 0px 11px 4px 0px, rgba(14, 59, 101, 0.02) 0px 6px 4px 0px, rgba(14, 59, 101, 0.03) 0px 3px 3px 0px, rgba(14, 59, 101, 0.04) 0px 1px 1px 0px;
--shadow-lg-2: rgba(114, 49, 255, 0.32) 0px -6px 20px 0px inset, rgba(47, 1, 151, 0.12) 0px 11px 12px 0px, rgba(47, 1, 151, 0.12) 0px 3px 3px 0px, rgba(47, 1, 151, 0.12) 0px 1px 1px 0px, rgba(47, 1, 151, 0.08) 0px 0px 0px 1px;
--shadow-sm-2: rgba(18, 55, 105, 0.08) 0px 2px 4px 0px, rgba(18, 55, 105, 0.04) 0px 1px 1px 0px;
--shadow-sm-3: rgba(47, 1, 151, 0.01) 0px 11px 4px 0px, rgba(47, 1, 151, 0.04) 0px 6px 4px 0px, rgba(47, 1, 151, 0.06) 0px 3px 3px 0px, rgba(47, 1, 151, 0.08) 0px 1px 1px 0px, rgba(47, 1, 151, 0.08) 0px 0px 0px 1px;
}
```
+120
View File
@@ -0,0 +1,120 @@
# 인터페이스 및 UI 제어 명세서 (frontend.md)
## 1. 글로벌 테마 변수 강제 (Theme Constraints)
* **임의 색상 지정 금지:** 하드코딩된 Hex 코드 직접 입력 금지.
* **CSS 변수 필수 참조:** 테마 전환(라이트/다크) 및 일관성을 위해 `ui_template_theme.css` 선언 변수(`var(--color-bg)`, `var(--color-text)` 등)만 사용.
* **신규 컴포넌트/템플릿:** 신규 컴포넌트나 템플릿이 필요한 경우`.agent\design.md` 파일을 참고하여 양식을 `ui_template\ui_template_elements.ts`, `ui_template\ui_template_locale.ts`, `ui_template\ui_template_theme.css` 양식화하여 사용할 것
## 2. UI 요소 및 레이아웃 제약 (Layout Constraints)
* **공통 컴포넌트 최우선:** 임의 스타일링 금지. `ui_template_elements.ts` 규격 및 공통 컴포넌트 최우선 상속/참조.
* **3단 기본 레이아웃 준수:** 워크플로우 페이지 기본 배치 강제 (예외 요청 제외)
1. 상단 영역: 페이지 타이틀 및 현재 진행 단계 상태
2. 좌측 패널: 데이터 입력 폼 및 설정 제어 영역 (너비 고정)
3. 우측 영역: WebCAD 도면 뷰어 또는 결과 데이터 그리드 (나머지 전체 너비)
### 2.1 공통 컴포넌트 목록 (`ui_template_elements.ts`)
* **기본:** `createButton`, `createInputField`, `createCard`, `createTag`, `createWorkflowShell`
* **오버레이/알림:** `showLoadingOverlay`, `hideLoadingOverlay`, `showToast`
* **스플라인 차트:** `createLineChart(opts)` — 외부 라이브러리 없이 인라인 SVG로 시계열 렌더링
- **곡선:** Catmull-Rom → 3차 베지어 변환으로 스플라인(부드러운 곡선) 렌더
- **색상:** `--color-chart-0~3` 테마 변수 사용 (하드코딩 금지)
- **범례:** 그래프 영역 우상단에 반투명 오버레이 배치 (`opts.series[].name`)
- **X축:** `opts.xLabels` 전달 시 양끝 포함 5개 라벨 균등 표기
- **결측:** `series[].values``null` 포함 시 해당 구간 선 끊김 처리
- 데이터 2점 미만이면 빈 상태(`—`) 반환
- 사용 예: B01_Dashboard 리소스 현황(CPU/메모리/디스크 30일 추이, X축=시간)
---
## 3. 인덱스 기반 다국어 제어 (i18n Constraints)
* **텍스트 하드코딩 금지:** 라벨, 버튼 문구, 안내 메시지 등 모든 UI 문자열의 컴포넌트 내 직접 입력 절대 금지.
* **🚨 동시 수정 프로토콜 (선언 후 구현):**
1. **선(先) 등록:** 신규 문구 필요 시, `ui_template_locale.ts` 최하단에 키와 언어 배열 `[한국어, 영어]` 우선 추가.
2. **후(後) 참조:** 컴포넌트 내 `ui_locales.키값[현재언어인덱스]` 형태로만 호출.
* *제약 조건:* UI 수정 요청 시 다국어 파일 업데이트 코드 미제시 경우 가이드 위반(에러) 처리.
---
## 4. JS/TS 코드 제어 및 상태 (Logic Constraints)
* **이벤트 핸들러 명명법:** `on[페이지명]_[기능명]_[액션]` 패턴 필수 준수. (예: `onB04_Surface_Calculate_Click`)
* **비동기 상태 제어:** API 호출 시 즉시 로딩 스피너(Overlay) 활성화, 통신 완료(성공/실패) 후 해제 로직 필수 포함.
* **1차 유효성 검사:** 백엔드 전송 전 빈 값 및 데이터 타입(숫자 범위 등) 프론트엔드 선 검증, 에러 시 입력창 하단 표기.
```typescript
export const ui_locales = {
A01_Home_Title: ["반갑습니다", "Welcome"],
B04_wf1_Surface_Btn: ["지표면 분석 실행", "Run Surface Analysis"]
};
```
---
## 5. 인증, 인가, 역할 및 라우팅 (Authentication, Authorization, Roles & Routing)
### 5.1 세션 쿠키 기반 인증
* **방식:** HttpOnly Secure 세션 쿠키 (localStorage 토큰 미사용)
* **API 통신:** 모든 API 요청에 `credentials: "include"` 설정으로 쿠키 자동 전송
* **로그아웃:** `/auth/logout` API 호출 → 서버 세션 삭제 → 쿠키 자동 제거
* **인증 갱신:** 로그인 시 `users.auth_expires_at = NOW() + 3개월` 자동 설정
- 유효한 인증: `auth_expires_at > NOW()` → 바로 대시보드 이동
- 만료된 인증: `auth_expires_at <= NOW()` → 재인증 모달 표시
### 5.1.1 역할 기반 대시보드 렌더링
* **USER 역할:**
- 프로젝트 리스트 섹션
- 회사 정보 섹션 (회사 생성/연결 팝업)
- 기본정보 & 보안 섹션
* **ADMIN 역할:**
- 프로젝트 리스트 섹션 (회사 전체 프로젝트)
- 회사 정보 섹션
- 팀원 관리 섹션
- 가입 요청 섹션
- 기본정보 & 보안 섹션
* **SYSTEM_ADMIN 역할:**
- 리소스 현황 섹션 (CPU, MEM, DISK, 활성 사용자/프로젝트)
- 회사 관리 섹션 (전체 회사 리스트 + 추가/수정/삭제)
- 사용자 관리 섹션 (전체 사용자 + 역할 변경 + 회사 할당)
- 가입 요청 관리 섹션 (전체 가입 신청 + 승인/거절)
- 시스템 로그 섹션 (필터링 지원)
- 리소스 모니터링 그래프 (시계열 데이터)
- 기본정보 & 보안 섹션
### 5.2 라우팅 가드
* **인증 확인:** `isAuthenticated()` 함수로 세션 유효성 검사 (PROTECTED_ROUTES 진입 전)
* **회사 연결 확인:** B02~B11 워크플로우 진입 시 `fetchDashboardMe()` 호출 → `company_id` + `status` 확인
- `status` 확인:
- NO_COMPANY: B01_Dashboard로 강제 리다이렉트 (회사 연결 필요)
- PENDING: B01_Dashboard로 강제 리다이렉트 (승인 대기 중)
- ACTIVE: 워크플로우 진입 허용
- `company_id` 없으면 B01_Dashboard로 리다이렉트
* **역할 기반 라우팅:**
- SYSTEM_ADMIN: 모든 페이지 접근 가능
- ADMIN: B01_Dashboard (관리 섹션) + B02~B11 워크플로우 접근 가능
- USER: B01_Dashboard (사용자 섹션) + B02~B11 워크플로우 접근 가능
* **상태 전이:**
- 회원가입 후 이메일 인증 완료: `users.status = 'NO_COMPANY'` (로그인 가능, 회사 미연결)
- 회사 생성: `users.status = 'ACTIVE'`, `company_id` 세팅, `role = 'ADMIN'`, `is_master = TRUE`
- 회사 참여 신청: `users.status = 'PENDING'` (관리자 승인 대기)
- 관리자 승인: `users.status = 'ACTIVE'`, `company_id` 세팅
### 5.3 대시보드 권한 기반 UI 제어 (B01_Dashboard)
* **권한 검증 헬퍼 필수:** `.agent/plan_dashboard_management.md` 섹션 9의 헬퍼 함수 참고
- `canEditProject(user, project)` - 프로젝트 수정 권한
- `canDeleteProject(user)` - 프로젝트 삭제 권한 (SYSTEM_ADMIN만)
- `canAddUser(user)` - 사용자 추가 권한
- `canChangeRole(user, targetUser)` - 역할 변경 권한 (ADMIN: USER↔ADMIN만, SYSTEM_ADMIN 제외)
- `canDeleteUser(user, targetUser)` - 사용자 삭제 권한 (마지막 ADMIN 보호)
- `canManageAutomation(user, automation)` - 자동화 로직 관리 권한
- `isLastAdmin(company, excludeUserId)` - 회사의 마지막 ADMIN 확인
* **UI 컴포넌트 구현:** 권한에 따라 버튼/필드 표시/비활성화
- 프로젝트: 수정 모달, 삭제 확인 모달 (삭제는 SYSTEM_ADMIN만)
- 사용자: 추가 모달 (가입 계정 목록), 수정 모달, 역할변경 모달, 삭제 확인 모달
- 자동화 로직: 생성/수정/삭제 모달 (ADMIN/SYSTEM_ADMIN만)
- 모든 삭제 모달에 경고 메시지 포함 (되돌릴 수 없음)
* **다국어 반영:** 계획서 8장의 모든 키를 `ui_template_locale.ts`에 추가 후 컴포넌트에서 `L(키)` 사용
- 모달 제목, 버튼 레이블, 확인 메시지, 오류 메시지 모두 다국어 처리
- 하드코딩된 문구 절대 금지
+278
View File
@@ -0,0 +1,278 @@
# B03~B06 기능 마이그레이션 계획서
**갱신일:** 2026-07-05
**상태:** 백엔드 마이그레이션 완료 — Stage 0~4 (B03~B06) 백엔드 이전 완료. 남은 항목은 프론트엔드(Vanilla TS) 재작성과 실제 DB 연동 검증.
**범위:** `B03_FileInput` ~ `B06_wf3_ProfileCross`
**DB:** MariaDB v10.6+ (aiomysql 드라이버, Raw SQL)
**진척 요약:**
- Stage 0 (공통 기반): ✅ 완료
- Stage 1 B03 (파일 입력): ✅ 백엔드 완료
- Stage 2 B04 (지표면 분석): ✅ 백엔드 완료 (필터 4종 + 모델 5종 + 스무딩 + 등고선 + 파이프라인)
- Stage 3 B05 (경로 설계): ✅ 백엔드 완료 (Dijkstra + ridge-valley + skeleton)
- Stage 4 B06 (종횡단 생성): ✅ 백엔드 완료 (sampler + 종횡단 + Repository/Router)
- 공통: 전 페이지 DB 접근을 aiomysql Raw SQL로 통일, 순수 계산부는 실데이터로 검증
- Stage 2~4 프론트엔드 폼/결과 UI: ✅ 완료 (B04~B06 UI_Page + Api_Fetch + CSS + locale, tsc 타입체크 통과)
- 남은 작업: B04~B06 **WebCAD 3D/차트 뷰어**, 실제 aislo_db 연동·구형 수치 비교
---
## 1. 확정 기준
- 기능 동작의 기준 원본은 `0_old/main.py``0_old/utils/`이다.
- `0_old/backend/app/`은 이전 프로토타입 참고 자료이며 최종 동작 기준으로 사용하지 않는다.
- 신규 배치 구조는 `.agent/structure.md`를 따른다. 구형 파일 구조는 승계하지 않는다.
- 백엔드는 `.agent/backend.md`, 프론트엔드는 `.agent/frontend.md`를 따른다.
- 저장 구조 및 DB 경로 열은 `.agent/db_schema_simple.md`의 단계별 파일시스템 구조를 따른다.
- DB 스키마 작성·수정은 별도 담당 작업이다. 이 마이그레이션은 확정된 DB 계약만 `aiomysql`와 Raw SQL로 사용한다.
- 루트 `main.py`에는 페이지 라우터 등록만 추가한다.
- 구형 React/TSX UI는 복사하지 않고 현재 Vanilla TypeScript 구조와 공통 UI 템플릿에 맞게 재작성한다.
---
## 2. 작업 원칙
### 2.1 함수 하나씩 이전
각 작업 단위는 다음 순서를 지킨다.
1. 대상 구형 함수와 직접 의존성 확인
2. 신규 페이지 폴더에 책임에 맞는 파일 하나 생성
3. 함수 하나만 이전·수정
4. 해당 함수 단위 테스트 또는 최소 실행 검증
5. 포맷터·린터 실행
6. 검증 완료 후 다음 함수 진행
한 번에 구형 모듈 전체를 복사하지 않는다. 구형 전역 경로, 동기식 파일 처리, 하드코딩 설정은 그대로 가져오지 않는다.
### 2.2 파일 분리
- `*_Router.py`: FastAPI 엔드포인트와 응답 변환
- `*_Schema.py`: Pydantic 요청·응답 검증
- `*_Engine_*.py`: 페이지 고유 연산
- `*_Repository.py`: `asyncpg` Raw SQL과 DB 경로 메타데이터 처리
- `common_util/`: 둘 이상의 페이지가 실제로 공유하는 경로·원자적 파일 쓰기·기하 유틸만 배치
- 단일 파일이 700줄에 도달하기 전에 기능별 파일 분할을 제안하고 승인 후 `structure.md`를 갱신한다.
### 2.3 저장소 경계
```text
storage/{company_slug}/{user_slug}/{project_id}/
├── B03_FileInput/
├── B04_wf1_Surface/
├── B05_wf2_Route/
└── B06_wf3_ProfileCross/
```
각 페이지는 자기 폴더만 직접 생성·수정한다. 상위 단계 변경 시 하위 결과를 임의 삭제하지 않고 `stale_from`을 갱신한다.
---
## 3. 구형 기능 분석 결과
### 3.1 B03_FileInput
구형 원본:
- `0_old/main.py`: `check_sample`, `upload_files`
- 보조 참고: `0_old/backend/app/analyzer.py`의 파일 분석 함수
책임:
- LAS/TIF/TFW/PRJ/DXF 업로드 검증
- `B03_FileInput/input/{file_type}/` 저장
- 파일 메타데이터 추출 및 `input_files` 기록
- 업로드 완료 상태 반환
B04로 넘길 기능:
- `process_pipeline`
- `run_ground_analysis`
- 포인트 필터링 및 지표면 모델 생성
### 3.2 B04_wf1_Surface
구형 원본:
- `0_old/main.py`: `process_pipeline`, `ensure_terrain_models`, 지표면·포인트 API
- `0_old/utils/structurizer.py`
- `filter_grid_min_z.py`, `filter_csf.py`, `filter_pmf.py`, `filter_ransac.py`
- `terrain_model_converter.py`, `surface_smoother.py`, `contour_extractor.py`
책임:
- LAS 구조화와 지면 필터 생성
- 변환 포인트클라우드 저장
- TIN/DTM/NURBS/implicit/meshfree 모델 생성
- 스무딩, 미리보기, 등고선 생성
- 지표면 모델 선택 확정 및 하위 단계 stale 전파
### 3.3 B05_wf2_Route
구형 원본:
- `0_old/main.py`: 경로점 CRUD, solve/result/confirm, stale 서명 처리
- `0_old/utils/route_solver.py`
- `0_old/utils/route_solver_ridgevalley.py`
- `0_old/utils/terrain_skeleton.py`
책임:
- BP/EP/CP/AP/FP 입력 검증과 저장
- Dijkstra 및 ridge-valley 경로 계산
- 경사·곡선반경·회피/금지 구역 검증
- 경로 결과 조회·확정 및 B06 stale 전파
### 3.4 B06_wf3_ProfileCross
구형 원본:
- `0_old/main.py`: sections generate/get/confirm 및 입력 서명 처리
- `0_old/utils/surface_elevation_sampler.py`
- `0_old/utils/section_generator.py`
책임:
- 확정 경로와 확정 지표면 모델 검증
- 종단 표고 프로필 생성
- 지정 측점 간격의 횡단면 생성
- 결과 조회·확정 및 재현성 서명 관리
---
## 4. 함수 단위 실행 순서
### Stage 0 — 공통 기반
- [x] `config_system.py`의 저장 경로에서 불필요한 `projects` 경로 제거
- [x] 프로젝트 루트와 B03~B06 단계별 경로를 안전하게 만드는 공통 경로 함수 추가
- [x] JSON 원자적 쓰기 함수 이전
- [x] `workflow.json` 읽기 및 stale 필드 원자적 갱신 함수 이전
- [x] 공통 함수별 테스트 작성
### Stage 1 — B03_FileInput
- [x] `B03_FileInput_Schema.py`: 파일 종류·크기·확장자 검증 모델
- [x] `B03_FileInput_Engine.py`: 안전한 파일명과 목적 경로 결정 함수
- [x] `B03_FileInput_Engine.py`: 업로드 스트림 저장 함수
- [x] `B03_FileInput_Engine_Analyze.py`: LAS 메타데이터 분석 함수
- [x] `B03_FileInput_Engine_Analyze.py`: PRJ/TIF/TFW 메타데이터 분석 함수
- [x] `B03_FileInput_Repository.py`: `input_files` 저장 함수
- [x] `B03_FileInput_Router.py`: 업로드 엔드포인트
- [x] B03 프론트 드롭존·목록·검증·로딩 UI를 공통 컴포넌트 기반으로 작성
- [x] 업로드 통합 테스트
### Stage 2 — B04_wf1_Surface
- [x] LAS 구조화 함수 이전
- [x] grid-min-z 필터 함수 이전 및 검증
- [x] CSF 필터 함수 이전 및 검증
- [x] PMF 필터 함수 이전 및 검증
- [x] RANSAC 필터 함수 이전 및 검증
- [x] 지표면 모델 공통 컨텍스트 생성 함수 이전 (ModelContext + 메시 유틸)
- [x] TIN 생성 함수 이전
- [x] DTM 생성 함수 이전
- [x] NURBS 생성 함수 이전
- [x] implicit 생성 함수 이전
- [x] meshfree 생성 함수 이전
- [x] DTM/TIN 스무딩 함수 이전
- [x] 등고선 생성 함수 이전
- [x] 미리보기·메타데이터·모델 확정 라우트 추가 (analyze/models 라우트 + 파이프라인 manifest)
- [x] `processed_point_cloud`, `surface_models`, `terrain_layers` Raw SQL 저장 함수 추가 (aiomysql)
- [x] B04 폼·모델 목록 UI를 Vanilla TypeScript로 재작성 (UI_Page + Api_Fetch + CSS + locale)
- [ ] B04 WebCAD 3D 뷰어(포인트클라우드/메시 렌더) 추가
- [ ] 샘플 LAS 결과를 구형 출력과 비교 검증
### Stage 3 — B05_wf2_Route
- [x] 경로점 Pydantic 모델 및 유효성 검사 이전 (Schema)
- [x] 경로점 저장·조회·초기화 함수 이전 (Repository)
- [x] 비용면 생성 및 캐시 함수 이전 (Solver)
- [x] Dijkstra 단일 구간 탐색 함수 이전 (Geometry)
- [x] 최적 경로 조립 함수 이전 (Solver solve_optimal_route)
- [x] 지형 skeleton 생성 함수 이전 (Skeleton, D8 흐름누적)
- [x] ridge-valley 탐색 함수 이전 (RidgeValley, 정속경사+fillet)
- [ ] 입력 서명·stale 판정 함수 이전
- [x] 경로 확정 함수 이전 (Repository confirm_route + confirm 라우트)
- [x] `routes`, `route_points`, `route_statistics` Raw SQL 저장 함수 추가 (aiomysql)
- [x] B05 경로점 입력·제약조건·결과 메트릭 UI를 Vanilla TypeScript로 재작성 (UI_Page + Api_Fetch + CSS + locale)
- [ ] B05 경로 WebCAD 뷰어(노선 폴리라인 렌더/편집) 추가
- [x] 기존 route solver 테스트를 신규 구조로 이관·통과
### Stage 4 — B06_wf3_ProfileCross
- [x] 지표면 sampler 인터페이스 이전 (Engine_Sampler: SurfaceElevationSampler Protocol)
- [x] DTM grid sampler 함수 이전 (DtmGridSampler, 4-꼭짓점 valid 검증)
- [x] 보간 sampler 함수 이전 (InterpolatedSurfaceSampler + build_surface_sampler 5종)
- [x] 측점 배열 생성 함수 이전 (Engine_Section: _chainages)
- [x] 경로 보간 및 접선 계산 함수 이전 (_interpolate_xy, _tangent_at)
- [x] 종단·횡단 생성 함수 이전 (generate_sections)
- [ ] 입력 서명·중복 실행 방지 함수 이전 (delete_sections_for_route로 멱등 재실행만 구현)
- [x] 결과 조회·확정 라우트 추가 (sections/generate·get·confirm 라우트)
- [x] `longitudinal_sections`, `cross_sections` Raw SQL 저장 함수 추가 (aiomysql Repository)
- [x] B06 측점·횡단 옵션·결과 메트릭 UI를 Vanilla TypeScript로 재작성 (UI_Page + Api_Fetch + CSS + locale)
- [ ] B06 종단도·횡단 카드 WebCAD 뷰어(프로필 차트 렌더) 추가
- [ ] 구형 section 결과와 수치 비교 검증
**B06 백엔드 구현 파일:**
- `B06_wf3_ProfileCross_Engine_Sampler.py` — 표고 sampler (Protocol + DTM/보간 5종)
- `B06_wf3_ProfileCross_Engine_Section.py` — 종횡단 생성 (측점/접선/횡단 샘플)
- `B06_wf3_ProfileCross_Engine.py` — 오케스트레이터 (경로 GeoJSON→종횡단→파일 저장)
- `B06_wf3_ProfileCross_Repository.py` — aiomysql Raw SQL (2 테이블)
- `B06_wf3_ProfileCross_Schema.py` / `_Router.py` — Pydantic + FastAPI 라우트
**검증 상태:** B04(DTM)→B05(경로 70.71m)→B06(종단면+횡단면 5건) end-to-end 파일 생성 확인.
DB 저장 함수는 FakeCursor 기반 문법 검증만 수행 (실제 aislo_db 연동은 나스 서버 준비 후).
---
## 5. API 및 상태 원칙
- API 경로는 `/api/projects/{project_id}/...` 형태를 유지하되 페이지별 Router에서 선언한다.
- 모든 JSON 요청은 Pydantic 검증 후 Engine으로 전달한다.
- 긴 지형 연산은 이벤트 루프를 막지 않도록 실행 방식을 별도 검토한다.
- 오류 응답은 `{"status": "error", "message": "원인"}` 형식을 유지한다.
- 프론트 API 호출은 로딩 Overlay를 항상 시작·해제한다.
- UI 문자열은 `ui_template_locale.ts`에 먼저 등록한 뒤 참조한다.
- 상위 단계 재계산 시 하위 결과는 stale 처리하며 다른 페이지 폴더를 직접 삭제하지 않는다.
---
## 6. 검증 체크리스트
각 함수 이전 후:
- [ ] 구형 함수의 입력·출력 계약 비교
- [ ] 하드코딩 설정 제거 및 `config_system.py` 연결
- [ ] 신규 단계별 저장 경로 사용 확인
- [ ] 경로 이탈 및 파일명 공격 방지 확인
- [ ] 예외 처리와 표준 오류 응답 확인
- [ ] Python: `ruff format`, `ruff check --fix`
- [ ] TypeScript/CSS: `npx prettier --write`
- [ ] 단위 테스트 실행
- [ ] 관련 통합 테스트 실행
- [ ] 단일 파일 700줄 미만 확인
단계 완료 후:
- [ ] B03 업로드 파일과 DB 메타데이터 일치
- [ ] B04 지표면 모델의 범위·점 수·표고 통계 비교
- [ ] B05 경로 길이·경사·곡선반경·제약 위반 비교
- [ ] B06 측점 수·종단 표고·횡단 좌표 비교
- [ ] 다중 브라우저 polling 및 stale 전파 확인
---
## 7. 작업 경계와 보류 항목
- DB 테이블·FK·인덱스 정의 변경은 본 작업에서 수행하지 않는다.
- `.agent/db_schema_simple.md`는 DB 담당자의 작업 영역이므로 직접 수정하지 않는다.
- DB 스키마가 확정되지 않은 Repository 함수는 인터페이스만 계획하고 임의 열을 만들지 않는다.
- B07 이후 기능은 이번 마이그레이션 범위에서 제외한다.
- 구형 샘플 데이터는 검증 입력으로만 사용하고 신규 영구저장소로 자동 복사하지 않는다.
---
## 8. 다음 실행 단위
첫 구현은 Stage 0의 저장 경로 함수 하나로 시작한다. 함수와 테스트를 검증한 뒤 다음 공통 함수로 이동한다.
+189
View File
@@ -0,0 +1,189 @@
# 프로젝트 구조 명세서 (structure.md)
## 1. 폴더 및 파일 명명 규칙 (Naming Convention)
- **기본 원칙:** 프론트엔드(TypeScript)와 백엔드(Python) 파일을 수직 통합 관리
- **페이지 폴더 명명:** 로그인 전은 `A01_`, 로그인 후는 `B01_` 형태 적용, 워크플로우는 `wf[번호]` 접두사를 사용
- **시스템 폴더 명명:** 공통 유틸, DB, 리소스, 영구저장소 등 시스템 및 데이터 인프라 폴더는 접두사 ID 없이 명확한 영문명 분리
- **파일 명명:** 반드시 `폴더명(페이지명)_기능_기타사양.확장자` 형태 작성
- **공통 모듈:** 공통 유틸 코드는 `common_util` 폴더 내에 `common_util_기능.확장자` 형태 보관
---
## 2. 전체 디렉토리 트리 구조
```text
my-project/
├── .agent/
│ ├── agent.md
│ └── structure.md # 본 파일
├── A00_Common/ # 프론트엔드 공통 인프라 (부트스트랩, 라우팅, 셸)
│ ├── main.ts # 앱 진입점 (스타일 주입 → 테마 복원 → 셸 → 라우터)
│ ├── 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차 유효성 검사 (이메일/빈값/최소길이)
│ ├── common_util_auth.py # 비밀번호·OTP·세션 쿠키·권한 검증
│ ├── common_util_auth_repository.py # 인증·조직·관리 Raw SQL 저장소
│ ├── common_util_email.py # 비동기 SMTP 발송 및 재시도
│ ├── common_util_email_templates.py # 인증·조직 알림 이메일 템플릿
│ ├── common_util_string.ts
│ ├── common_util_storage.py # 프로젝트 및 워크플로우 단계별 저장 경로
│ ├── common_util_json.py # JSON 원자적 저장
│ ├── common_util_workflow.py # 공유 workflow.json 상태 처리
│ ├── common_util_resource_monitor.py # 시스템 리소스 계측(2분 주기) + log/ 로그 + DB 기록
│ └── common_util_format.py
├── db_management/ # DB 관리 폴더 (PostgreSQL 마이그레이션 및 스키마 관리)
│ ├── schema.sql
│ ├── migration_v1.py
│ └── 002_auth_security.sql # 기존 DB 로그인·보안 증분 마이그레이션
├── resources/ # 웹앱 UI 구성 리소스 (사진, 회사 로고 등 에셋)
│ ├── logo.png
│ ├── banner.jpg
│ └── legal/ # 약관 및 정책 텍스트 (회원가입 UI에서 사용)
│ ├── terms_of_service.txt # 이용약관
│ ├── privacy_policy.txt # 개인정보처리방침
│ └── marketing_consent.txt # 마케팅 정보 수신 동의
├── storage/ # 영구 저장소 (데이터 커서 및 대용량 파일 별도 저장 로직용)
│ └── [고객사명]/
│ └── [사용자명]/
│ └── [프로젝트ID]/ # 프로젝트별 내부 폴더/파일 자동 생성 (별도 폴더 ID 미사용)
├── log/ # 런타임 리소스 로그 (.gitignore, 1개월 롤링)
│ └── system_resources.log # CPU/메모리/디스크 계측 단일 파일 (2분 주기)
├── ui_template/ # 공통 UI 템플릿 및 테마
│ ├── ui_template_theme.css # 글로벌 디자인 테마 및 CSS 변수 (라이트/다크)
│ ├── ui_template_locale.ts # 다국어 텍스트 배열 방식 관리 파일
│ └── ui_template_elements.ts # 공통 컴포넌트 및 그룹 템플릿 정의
├── templates/ # 보고서 출력용 마스터 양식 (DWG, Excel, PPT 등 기본 원본 템플릿 파일)
│ ├── template_report_form.xlsx
│ └── template_drawing_base.dwg
├── config/ # 프로그램 내부 제어 및 시스템 환경 설정 폴더
│ ├── config_system.py # 백엔드 작동 제어 및 알고리즘 파라미터
│ ├── config_db.py # 데이터베이스 연결 제어 설정
│ ├── config_frontend.ts # 프론트엔드 제어 및 웹 환경 설정
│ ├── package.json # Node.js 의존성 (npm/vite)
│ ├── tsconfig.json # TypeScript 설정
│ └── vite.config.ts # Vite 빌드 설정
├── A01_Home/ # 로그인 전 01: 홈 (소개, 소식, 사이트맵 등)
│ ├── A01_Home_UI_Page.ts
│ ├── A01_Home_UI_Style.css
│ └── A01_Home_Api_Fetch.py
├── A02_ProgDetail/ # 로그인 전 02: 프로그램 상세 (6단계 워크플로우 + 핵심 기술)
│ ├── A02_ProgDetail_UI_Page.ts
│ └── A02_ProgDetail_UI_Style.css
├── A03_CompDetail/ # 로그인 전 03: 회사 상세 (미션 + 핵심 가치 + 연락처)
│ ├── A03_CompDetail_UI_Page.ts
│ └── A03_CompDetail_UI_Style.css
├── A04_NewsHistory/ # 로그인 전 04: 최신소식 및 개선 이력 (타임라인)
│ ├── A04_NewsHistory_UI_Page.ts
│ └── A04_NewsHistory_UI_Style.css
├── A05_EduDetail/ # 로그인 전 05: 교육 상세 (과정 소개 + 문의 CTA)
│ ├── A05_EduDetail_UI_Page.ts
│ └── A05_EduDetail_UI_Style.css
├── A06_Login/ # 로그인 전 06: 로그인 (이메일 + 비밀번호 폼)
│ ├── A06_Login_UI_Auth_Page.ts # 세션 쿠키·OTP 로그인 UI (현재 사용 중)
│ ├── A06_Login_UI_Style.css
│ ├── A06_Login_Api_Fetch.ts
│ ├── A06_Login_Schema.py
│ └── A06_Login_Router.py
├── A07_Register/ # 로그인 전 07: 회원가입 (회사/이름/이메일/비밀번호 폼)
│ ├── A07_Register_UI_Page.ts
│ ├── A07_Register_UI_Auth_Page.ts # 조직 유형·약관·OTP 회원가입 UI
│ ├── A07_Register_UI_Style.css
│ ├── A07_Register_Api_Fetch.ts
│ ├── A07_Register_Schema.py
│ └── A07_Register_Router.py
├── A08_Support/ # 로그인 전 08: 기술지원 요청 (문의 폼 + textarea)
│ ├── A08_Support_UI_Page.ts
│ └── A08_Support_UI_Style.css
├── A09_Security/ # 로그인 전 09: 보안 및 약관 동의 (필수/선택 약관 확인)
│ ├── A09_Security_UI_Page.ts # 약관 동의 페이지 (이용약관/개인정보/마케팅/보안정책)
│ ├── A09_Security_UI_Style.css # A09 고유 약관 박스/체크박스 스타일
│ ├── A09_Security_Api_Fetch.ts # 동의 내용 제출 API 클라이언트
│ ├── A09_Security_Terms.ts # 약관 텍스트 데이터 (한/영 다국어)
│ ├── A09_Security_Schema.py # 조직·관리자 보안 요청 검증
│ └── A09_Security_Router.py # 마스터·시스템 관리자·활동 로그 API
│ # ※ B01·B02 본문 완성 / B03~B11 헤더·셸만 (본문은 0_old 참고 후 구체화)
├── B01_Dashboard/ # 로그인 후 01: 대시보드 (역할별 정보 표시)
│ ├── B01_Dashboard_UI_Page.ts # 역할별 섹션 조건부 렌더링
│ ├── B01_Dashboard_UI_Modals.ts # 프로젝트, 사용자, 자동화 제어 모달 모음
│ ├── B01_Dashboard_UI_Helper.ts # 역할별 접근 권한 확인 헬퍼
│ ├── B01_Dashboard_UI_Resources.ts # CPU/Memory/Disk 리소스 렌더링 컴포넌트
│ ├── B01_Dashboard_UI_Style.css # 프로젝트 테이블, 워크플로우, 팝업 스타일
│ ├── B01_Dashboard_Api_Fetch.ts # dashboard API 클라이언트
│ ├── B01_Dashboard_Schema.py # 요청/응답 Pydantic 검증
│ ├── B01_Dashboard_Repository.py # Raw SQL 저장소 (사용자, 회사, 팀원, 가입 신청)
│ └── B01_Dashboard_Router.py # 역할별 API 엔드포인트
├── 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
│ ├── B03_FileInput_Api_Fetch.ts # 다중 파일 업로드 API 클라이언트
│ ├── B03_FileInput_Schema.py # 업로드 파일 Pydantic 검증
│ ├── B03_FileInput_Engine.py # 업로드 저장 경로·스트림 처리
│ ├── B03_FileInput_Engine_Analyze.py # 원본 파일 메타데이터 분석
│ ├── B03_FileInput_Repository.py # input_files asyncpg Raw SQL
│ └── B03_FileInput_Router.py # 다중 파일 업로드 API
├── B04_wf1_Surface/ # 로그인 후 04: 1차 workflow (지표면 모델 분석)
│ ├── B04_wf1_Surface_UI_Page.ts # 3단 레이아웃 (필터/표현 선택 폼 + 모델 목록)
│ ├── B04_wf1_Surface_UI_Style.css # B04 고유 폼 그룹/모델 카드 스타일
│ ├── B04_wf1_Surface_Api_Fetch.ts # surface/analyze·models API 클라이언트
│ ├── B04_wf1_Surface_Schema.py # 분석 요청/응답 Pydantic 검증
│ ├── B04_wf1_Surface_Engine.py # 지표면 분석 오케스트레이터
│ ├── B04_wf1_Surface_Engine_Structurize.py # LAS/LAZ 구조화
│ ├── B04_wf1_Surface_Engine_Ground.py # 지면 분석 진입
│ ├── B04_wf1_Surface_Engine_Filter_Grid.py # grid minimum-Z 필터
│ ├── B04_wf1_Surface_Engine_Filter_CSF.py # CSF 필터
│ ├── B04_wf1_Surface_Engine_Filter_PMF.py # PMF 필터
│ ├── B04_wf1_Surface_Engine_Filter_RANSAC.py # RANSAC 필터
│ ├── B04_wf1_Surface_Engine_ModelContext.py # 모델 공통 컨텍스트/메시 유틸
│ ├── B04_wf1_Surface_Engine_ModelBuild.py # TIN/DTM/NURBS/implicit/meshfree 생성
│ ├── B04_wf1_Surface_Engine_Smooth.py # DTM/TIN 스무딩
│ ├── B04_wf1_Surface_Engine_Contour.py # 등고선 생성
│ ├── B04_wf1_Surface_Engine_Pipeline.py # 파이프라인 manifest
│ ├── B04_wf1_Surface_Repository.py # processed_point_cloud/surface_models aiomysql Raw SQL
│ └── B04_wf1_Surface_Router.py # surface/analyze·models API
├── B05_wf2_Route/ # 로그인 후 05: 2차 workflow (경로설계)
│ ├── B05_wf2_Route_UI_Page.ts # 3단 레이아웃 (제어점/제약 폼 + 결과 메트릭)
│ ├── B05_wf2_Route_UI_Style.css # B05 고유 제어점 행/제약 그룹 스타일
│ ├── B05_wf2_Route_Api_Fetch.ts # route/solve·confirm API 클라이언트
│ ├── B05_wf2_Route_Schema.py # 경로 탐색 요청/응답 Pydantic 검증
│ ├── B05_wf2_Route_Engine.py # 경로 설계 오케스트레이터
│ ├── B05_wf2_Route_Engine_Geometry.py # Dijkstra 단일 구간 탐색
│ ├── B05_wf2_Route_Engine_Solver.py # 비용면 생성 + 최적 경로 조립
│ ├── B05_wf2_Route_Engine_Skeleton.py # 지형 skeleton (D8 흐름누적)
│ ├── B05_wf2_Route_Engine_RidgeValley.py # ridge-valley 탐색
│ ├── B05_wf2_Route_Repository.py # routes/route_points/route_statistics aiomysql Raw SQL
│ └── B05_wf2_Route_Router.py # route/solve·confirm API
├── B06_wf3_ProfileCross/ # 로그인 후 06: 3차 workflow (종횡단 생성)
│ ├── B06_wf3_ProfileCross_UI_Page.ts # 3단 레이아웃 (측점/횡단 옵션 폼 + 결과 메트릭)
│ ├── B06_wf3_ProfileCross_UI_Style.css # B06 고유 옵션 그룹/결과 메트릭 스타일
│ ├── B06_wf3_ProfileCross_Api_Fetch.ts # sections/generate·get·confirm API 클라이언트
│ ├── B06_wf3_ProfileCross_Schema.py # 종횡단 생성 요청/응답 Pydantic 검증
│ ├── B06_wf3_ProfileCross_Engine.py # 종횡단 생성 오케스트레이터
│ ├── B06_wf3_ProfileCross_Engine_Sampler.py # 표고 sampler (Protocol + DTM/보간 5종)
│ ├── B06_wf3_ProfileCross_Engine_Section.py # 종횡단 생성 (측점/접선/횡단 샘플)
│ ├── B06_wf3_ProfileCross_Repository.py # longitudinal/cross_sections aiomysql Raw SQL
│ └── B06_wf3_ProfileCross_Router.py # sections/generate·get·confirm API
├── 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)
├── .env.example # 환경 변수 템플릿
└── node_modules/ # Node.js 패키지 (.gitignore)
└── .build/ # Vite 빌드 결과 (.gitignore)
```