chore(docs): docs 폴더 git 추적 전환 · PLAN·OWNERS 최상위 이관
- `docs/` 를 .gitignore 에서 빼 git 추적 대상으로 전환 (위키·완료 이력·검증 기록) - `docs/raw/PLAN.md` · `docs/raw/OWNERS.md` 를 저장소 최상위로 이동 후 .gitignore 에 등록 — 창끼리 공유하되 저장소에는 안 올리는 장부 - 살아 있는 경로 참조 7개 파일 정정 (아카이브 107건은 그때 사실이라 그대로 둠) - graphify 날짜별 산출물(`docs/wiki/graphify-out/20*/`) 제외 — `graphify update` 가 다시 만듦 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 (역할별 동적 렌더링) |
|
||||
@@ -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 수준
|
||||
@@ -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)
|
||||
@@ -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 24–64px, 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 24–64px, 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 (12–16px) 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.30–1.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 (12–16px) 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 40–60px 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 40–56px, Deep Iris #26114a. Subtext in Inter 400 at 16–18px, 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 12–16px 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 (64–80px 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 40–50px 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;
|
||||
}
|
||||
```
|
||||
@@ -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(키)` 사용
|
||||
- 모달 제목, 버튼 레이블, 확인 메시지, 오류 메시지 모두 다국어 처리
|
||||
- 하드코딩된 문구 절대 금지
|
||||
@@ -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의 저장 경로 함수 하나로 시작한다. 함수와 테스트를 검증한 뒤 다음 공통 함수로 이동한다.
|
||||
@@ -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)
|
||||
```
|
||||
@@ -0,0 +1,697 @@
|
||||
# 일하는 법 · 검증 함정 — 겪은 것에서 (2026-09-07 ~ 09)
|
||||
|
||||
> **상시계획서 `PLAN.md` 「참고」에서 옮긴 원문임. 줄이지 않았음.**
|
||||
> 옮긴 까닭 — 계획서의 「참고」는 **코드 작업 중 필요한 기준표·확정사항만** 두기로 되어 있는데
|
||||
> 여기 든 것은 기준표가 아니라 **일하는 법**임(2026-09-09 사용자 지시 「반영」).
|
||||
> **다음 세션이 계획서보다 먼저 읽을 것.** 계획서 「참고」에는 이 파일 링크만 남겼음.
|
||||
>
|
||||
> 든 것 — 일하는 법 열둘 · 좁은 폭 화면 깨짐 · vite 캐시 · 공용 이름 바꾸기 ·
|
||||
> [저장]과 횡단 재계산 · 공용 브라우저 조작 주의 셋 · 검증 함정 모음.
|
||||
|
||||
### ⚠⚠ 일하는 법 넷 — 2026-09-07 두 창이 겪은 것에서 (다음 세션이 먼저 읽을 것)
|
||||
|
||||
이 절의 값어치는 **같은 사고를 다시 겪지 않는 것**이다. 넷 다 하루에 여러 번 나왔다.
|
||||
|
||||
#### ㉠ 「숫자가 늘어난 것」은 좋아진 신호가 아니다
|
||||
|
||||
배분율 표 25건이 살아났으나 그게 **10 % 짜리**였고, 2단 표 165줄이 늘었으나 상당수가
|
||||
오독이었다. **늘어난 뒤에는 반드시 최대·중앙값과 손대조를 볼 것.**
|
||||
(그래서 산출 요약에 최소·중앙·최대를 붙였다 — 8-22 ③.)
|
||||
|
||||
#### ㉡ 넓은 규칙이 정상 값을 지운다 — 하루에 **여덟 번** (두 창에서 넷씩)
|
||||
|
||||
① 배합 검사가 `막자갈`을 오탐 ② 자원 필터가 정상 자원 70/745 를 지움 ③ 비율 지시를
|
||||
먼저 봐 강관동바리 소요량표가 참조로 넘어감 ④ 머리글 필터가 등급 글자(상·중·하)를 지움
|
||||
⑤ 「당」을 느슨히 잡아 `(무한궤도,0.7㎥)` 를 밑수로 읽음 ⑥ 식 칸 집계가 비고 설명문까지
|
||||
걸어 93 → 31 로 좁힘 ⑦ 「값 못 읽은 줄」 판정이 **값이 없는 게 당연한 머리 줄**까지 세어
|
||||
28 → 12 ⑧ 표 안 긁기가 비고의 「10㎡」를 그 표의 밑수로 물어 옴.
|
||||
|
||||
⑨ **`REFERENCE_MARKS` 의 「단 위」 하나가 공종 41건을 참조로 버림** — 그 안에
|
||||
**돌쌓기(장비) 13-4-5·13-4-2**(우리 매핑이 매일 쓰는 코드)가 있었다. 규모가 가장 컸다.
|
||||
⑩ 자간 공백 이름 세기가 「단 위」·「모 래」까지 걸어 101 → 20 으로 좁힘.
|
||||
|
||||
**⚠ ⑦ 은 이 병을 다섯 번 잡아 놓고 새로 하나 만든 것**이다. 원인은 **짝 시험을 그 자리에
|
||||
같이 안 둔 것**. 세기 전에 **「이건 세면 안 된다」 쪽 표본을 먼저 정해 둘 것.**
|
||||
|
||||
**⚠ ⑨ 의 진단이 이 절의 처방이다** — 그 표지의 원래 겨냥(품셈 1-2-2 「단위 표준」)은
|
||||
**chapter 규칙이 이미 거르고 있었다.** 즉 **넓기만 하고 얻는 것이 없었다.**
|
||||
**⇒ 넓은 규칙을 만나면 그것이 실제로 무엇을 **더** 잡는지 세어 볼 것 — 다른 규칙과 겹치면
|
||||
얻는 것 없이 잃기만 한다.** 규칙을 지울 근거는 「안 잡히는 것」이 아니라 **「이미 다른 규칙이
|
||||
잡고 있는 것」**이다.
|
||||
|
||||
**⚠ 정규화는 값을 살리지만 잘못된 줄도 함께 살린다** (2026-09-07 서브 창 실례).
|
||||
기종 이름 공백을 지워 빠졌던 장비 몫을 살렸더니, **같은 표의 버킷계수 `K` 를 소요량으로 읽어**
|
||||
시간당 사용료가 이중이 될 뻔했다. **살리는 규칙에도 짝 시험을 둘 것.**
|
||||
|
||||
**⇒ 규칙을 새로 둘 때마다 「걸려야 한다」와 「걸리면 안 된다」 짝 시험을 함께 둘 것.**
|
||||
**⇒ 값을 지우는 규칙은 좁게, 표시만 하는 깃발은 넓게.** 깃발은 넓어도 값이 안 사라진다
|
||||
(`partial_ratio` 를 몫 유무로 넓게 잡은 판단이 그것).
|
||||
|
||||
#### ㉢ 검사가 도는 줄 알았는데 안 돌고 있었다 — 하루에 네 번
|
||||
|
||||
① 가드를 만들어 두고 안 부름 ② 지문 장치가 원판 지문을 봐서 안 움직임
|
||||
③ `npx tsc` 가 **엉뚱한 패키지를 실행**(이 저장소는
|
||||
`node ./config/node_modules/typescript/bin/tsc --noEmit` 로 부를 것)
|
||||
④ 사방공에 **없는 `type_id`** 를 적어 영영 안 걸릴 뻔함.
|
||||
|
||||
**⇒ 새 검사·장치는 일부러 깨뜨려 보고 실제로 잡히는지 확인할 것.**
|
||||
**⇒ 이름으로 무언가를 알아보는 코드는 정본(레지스트리·마스터)과 대조하는 시험을 둘 것.**
|
||||
|
||||
#### ㉤ ⚠ **먼저 원문부터 뒤진다** — 「관측값인 줄 알았는데 법이었다」가 **여덟 번**
|
||||
|
||||
여덟 다 「실무 관측값을 쓰자」·「사용자에게 물어야 한다」로 시작했다가 **품셈 원문에 이미
|
||||
정해져 있는 것**을 찾았다. 뒤 여섯은 **2026-09-08 재조사에서 네 창이 동시에** 건졌다.
|
||||
|
||||
| 물음 | 처음 생각 | 원문 |
|
||||
|---|---|---|
|
||||
| 거푸집을 몇 번 쓰나 | 관측값 병용 | **품셈 1-7-1** — 「3회 … 옹벽, 파라펫트, 날개벽」 |
|
||||
| 철근 갈래가 무엇인가 | 사용자에게 물음 | **품셈 12-3 [주]①** — 「보통: 수문, 반중력식 옹벽 및 교대」 |
|
||||
| 덤프 t↔㎥ 환산 γt | 지식DB 에 없음 | **품셈 10-12-1 [주]②** — 토사 1.9 · 암 2.4 t/㎥ |
|
||||
| 공구손료 밑수·제잡비와의 관계 | 두 문서가 어긋남 | **건설품셈 1-2-6** — 「명시된 것은 그것을 계상, 명시 안 된 것만 별도」·「인력품의 3%까지」 |
|
||||
| 야면석 개수·중량 원단위 | 실무값밖에 없음 | **품셈 13-4** 「돌쌓기의 개수 및 중량의 표준」 — 0.575 / 0.88 / 1.10 |
|
||||
| 물빼기 구멍 간격·지름 | 사용자에게 물음 | **품셈 13-4-4 [주]④⑤⑦** — 2~3㎡ 마다 Ø3~6㎝ · 모르터 0.009 |
|
||||
| 돌쌓기 **전면 기울기** | 사용자에게 물음 | **품셈 13-4-4 「표준경사」 표** — 직고×메찰×성절토 20칸 |
|
||||
| 돌쌓기 **뒷길이** 규격 | 우리 표 네 칸이 전부 | **품셈 13-4-4 「뒷길이 표준」 표** — 일곱 규격, 높이별 |
|
||||
|
||||
**⇒ 「이건 관측값밖에 없다」·「이건 사용자가 정할 값이다」로 넘어가기 전에 품셈 원문
|
||||
본문(표가 아니라 **표 앞뒤 [주]·안내 줄**)을 먼저 볼 것.** 밑수도 같은 자리였다 —
|
||||
표 안이 아니라 표 바로 위 본문에 있었다.
|
||||
|
||||
**⇒ 「지식DB 전수 확인: 없음」은 원문까지 안 보면 절반만 본 것이다** (2026-09-08 넷으로 굳음).
|
||||
지식DB 는 **요약·후보 가이드**라 원문의 [주]·부속 표가 옮겨지지 않은 자리가 많다.
|
||||
**조사 보고에 「원문 어디까지 봤나」를 함께 적을 것** — 파일명·줄번호가 없으면 「안 본 것」으로 친다.
|
||||
|
||||
**⇒ 원문에 표가 있으면 물음이 「값」에서 「축」으로 바뀐다.** 전면 기울기가 그랬다 —
|
||||
높이·메찰·성절토를 우리가 이미 아니까 **값은 자동으로 정해지고**, 남은 물음은
|
||||
「품셈 축(높이·성절토)과 교본 축(시설 종류) 중 어느 쪽이냐」 하나였다.
|
||||
|
||||
**⇒ 판정할 수 있는 것을 사람에게 물으면 그것이 곧 미결이 된다.** 물음 목록이 길어질수록
|
||||
사용자가 답할 수 있는 물음이 묻힌다. 실제로 「철근 갈래 전체」를 물으려던 것이 원문 확인 뒤
|
||||
**「캔틸레버식 옹벽 하나」로 좁아졌다.**
|
||||
|
||||
#### ㉪ ⚠ 계약이 바뀌면 시험부터 의심할 것 — 이틀에 **열두 번**
|
||||
|
||||
**2026-09-08 네 번** (덧곱 · 구조물 한 줄 · 사방공 이름 · 뒷길이 접기)
|
||||
**2026-09-09 여덟 번** — 사용자 확정을 붙일 때마다 **옛 시험이 옛 계약을 지키고 있었음**:
|
||||
```
|
||||
버림 콘크리트 「없음」을 못 박은 둘 (구조물 한 줄 · 큰돌쌓기 성분 집합)
|
||||
층따기 「막힌다」를 못 박은 둘
|
||||
기울기 「늘 1:0.3」을 못 박은 셋 (실무 시트 대조 시험 포함)
|
||||
stone_supply 기본값을 「근거 없음」으로 잡은 둘 ← 근거를 적어 예외로 뺌
|
||||
```
|
||||
⇒ **사용자 확정이 오면 「어느 시험이 옛 계약을 지키고 있나」를 먼저 찾을 것.**
|
||||
⚠ **검사를 눅이지 말 것** — 근거를 문장으로 적어 **예외로 빼는 것**과, 검사 자체를 약하게 만드는 것은 다름.
|
||||
|
||||
**옛 시험이 틀린 계약을 못 박고 지키고 있던 자리.** 실물:
|
||||
```python
|
||||
def test_표에_없는_뒷길이는_덮는_칸으로_접을것():
|
||||
assert _back_length({"back_len_cm": "25"}) == 35
|
||||
assert _back_length({"back_len_cm": "75"}) == 60
|
||||
```
|
||||
**「접는 것」이 옳다고 시험이 지키고 있었음.** 그런데 **접을 까닭이 아예 없었음** — 품셈이
|
||||
일곱 규격(25·30·35·45·55·60·75)을 다 주는데 **우리 표가 네 칸만 들고 있었을 뿐**임
|
||||
(실무 라이브러리가 네 칸만 옮겨 적은 것을 그대로 받았음).
|
||||
|
||||
⇒ **시험이 있다는 것이 「맞다」는 뜻이 아님.** 값이 나오면 아무 시험도 안 잡음(㉣ 과 한 벌).
|
||||
|
||||
**곁들여 같은 자리에서 나온 것 둘**
|
||||
- ⚠ **원문이 「-」인 칸에 0 줄을 만들지 말 것** — 25㎝ 고임돌이 **0.0 으로 서고 있었음**.
|
||||
**0 은 「없음」과 구별이 안 되고** 받는 쪽이 「값이 0 인 자재」로 읽음. 줄을 빼고 사유를 낼 것.
|
||||
- ⚠ **고른 갈래의 빈 칸은 빈 칸으로 덮을 것** — `None` 이라고 안 덮었더니
|
||||
**「야면석 75㎝」에 종전 값(깬돌 0.25)이 조용히 섰음.**
|
||||
|
||||
#### ㉦ ⚠ 저장 제원의 키 이름과 엔진이 읽는 키 이름을 대조할 것
|
||||
|
||||
레지스트리는 `back_len_cm` 인데 엔진이 `stone_back_length_cm` 을 읽고 있어 **저장값이
|
||||
영영 안 닿고 늘 기본 45㎝ 로 돌았다.** 사용자가 뒷길이 75 를 골라도 45 계수가 붙었다.
|
||||
|
||||
**⇒ 오늘 잡은 「조용히 틀린 값」 가운데 사용자가 고른 값이 직접 버려진 첫 사례다.**
|
||||
앞의 것들은 우리가 원문을 잘못 읽은 것이었는데 이건 **사용자 입력이 무시된** 것이라
|
||||
성격이 더 나쁘다.
|
||||
|
||||
**⇒ 「이름으로 알아보는 코드는 정본과 대조하라」(㉢)의 데이터 키 판이다.** 대조를 시험으로
|
||||
둔다(`test_b08_option_keys.py`). **「칸이 아예 없는 것」과 「이름이 어긋난 것」은 다른 것**이라
|
||||
가려 적는다 — 앞은 「칸이 생기면 받는다」이고 뒤는 **버그**다.
|
||||
|
||||
#### ㉥ 폴더 튐은 「내 것이 없어진 것처럼 보이는」 착시까지 만든다
|
||||
|
||||
2026-09-07 두 창이 한 저장소에서 일하며 **폴더 튐이 네 번** 났다. 그중 하나는 상대 창이
|
||||
**남의 폴더 파일을 읽으며 「내 코드가 사라졌다」**고 판단한 것이었고, 자기 폴더에서는 멀쩡했다.
|
||||
|
||||
**⇒ 「내 변경이 사라졌다」고 느끼는 순간 먼저 현재 폴더부터 확인할 것.** 그 상태에서
|
||||
「복구」에 손대면 **없어지지 않은 것을 되살리려다 멀쩡한 것을 덮는** 진짜 사고가 된다.
|
||||
**⇒ push 전에 `git status` 로 남의 파일이 섞였는지 볼 것**(OWNERS 규칙 2). 남의 파일이
|
||||
보이면 **경로를 지정해 커밋**하고 자동 스크립트를 쓰지 말 것 — 폴백이 주워 담는다.
|
||||
|
||||
#### ㉣ 「값이 있기는 하니 안 보이는」 것이 가장 위험하다
|
||||
|
||||
씨앗뿜어붙이기 합계 68.8원 · 측구터파기가 인력 10 % 몫만 · 운반 표가 [확정] 뒤 0줄 ·
|
||||
부분 성공한 표가 엉뚱한 직종에 붙음. **부분 성공이 완전 실패보다 위험하다** —
|
||||
실패는 눈에 띄고 부분 성공은 안 띈다.
|
||||
|
||||
**⇒ 「0 이 아님」이 아니라 「크기가 말이 되나」로 잴 것.**
|
||||
|
||||
**⚠ 그 반대 짝도 있다 — 「값이 맞는데 이상해 보이는」 경우.** 반영률이 전부 100 % 라
|
||||
성토면다짐과 층따기가 같은 수로 나온 것을 「곱하기가 빠졌다」로 오진한 일이 있었다
|
||||
(두 계열이 같은 사면 면적에서 나오므로 원래 같다).
|
||||
**⇒ 「두 값이 같다」를 만나면 코드보다 설정·입력을 먼저 읽을 것.**
|
||||
|
||||
#### ㉧ 양쪽 근거가 다 있으면 **한쪽으로 몰아 보고하지 말 것** (2026-09-08)
|
||||
|
||||
교차검토에서 「제잡비 3% 의 밑수에 굴착기 조종원 노임이 들어가는가」가 나왔음.
|
||||
산림품셈 13-6-2 [주]③ 은 「**노무비의 합계액**」이라고만 하고 그게 무엇인지 정의를 안 함.
|
||||
근거가 양쪽에 다 있었음 —
|
||||
|
||||
| 쪽 | 근거 |
|
||||
|---|---|
|
||||
| 사람 품만 | 건설품셈 제8장이 같은 「제잡비율」을 **「직접노무비」로 못 박음**. 기계를 밑수에 넣을 때는 **「노무비, 기계손료 및 운전경비의 합」이라 따로 적음** |
|
||||
| 노무비 계정 전체 | 우리 원가 체계에서 「노무비」와 「직접노무비」는 **다른 말**이고, 중기 운전사 노임도 같은 노임표에서 옴 |
|
||||
|
||||
한쪽만 실어 보냈으면 받는 창이 그대로 받았을 것임. **양쪽을 나란히 놓자 조율 창이
|
||||
자기 앞 판정을 뒤집었음**(「사람 품만」으로 확정, 미결 ㉮ 로도 올림).
|
||||
|
||||
**⇒ 판단은 받는 쪽이 함. 반대 근거가 없으면 판단 자체가 안 됨.**
|
||||
**⇒ 문구가 갈리는 자리는 「같은 규정이 다른 데서는 어떻게 쓰였나」를 볼 것** — 이 건은
|
||||
**「넣을 땐 넣는다고 쓴다」**가 갈랐음.
|
||||
**⇒ 그리고 그 조항이 무엇을 사는 값인지 읽을 것** — 제잡비는 「콘크리트 버켓 손료·다짐기계
|
||||
손료」, 즉 **본 자원에 안 선 잔 기계**임. 그 표에 굴착기는 이미 본 자원으로 서 있으므로
|
||||
잔 기계를 **그 굴착기 조종원 노임에 비례**시키는 것은 뜻이 어긋남.
|
||||
|
||||
**⚠ 이 자리는 어떤 시험도 안 잡았음** — 값이 나오고 자원 넷이 원문과 정확히 맞았음.
|
||||
**손대조로 차액을 역산**해서야 밑수가 드러났음(㉣ 「크기가 말이 되나」의 한 단 아래).
|
||||
|
||||
**⇒ 그리고 「없다」를 확정하기 전에 「어디에 없나」를 볼 것** (2026-09-08 ㉗).
|
||||
품셈에 없다는 것이 **답이 없다는 뜻은 아님.** 같은 날 둘이 그랬음 —
|
||||
**지장목제거**는 품셈에 공종이 없을 뿐 **수량은 법령이 요구하고 실무가 같은 산식으로 세고 있었음**,
|
||||
**입목 본수**는 조사값이 아니라 **우리 저장소가 재료(원본 LAS·다중 리턴)를 이미 갖고 있었음**.
|
||||
**⇒ 찾을 자리는 셋임 — 품셈 · 다른 문서(법령·교본·실무) · 우리가 이미 가진 것.**
|
||||
|
||||
**⇒ 값이 움직였는데 코드 diff 에 없으면 **남의 창을 먼저 볼 것** (2026-09-08).
|
||||
**DB 는 원격 공용 서버 한 대**(`dsm.chemifactory.com`)라 네 환경이 같은 자료를 쓴다.
|
||||
어느 창이든 [확정]·[저장]을 부르면 나머지 셋의 값이 그 자리에서 바뀐다 — 코드를 하나도
|
||||
안 건드려도 바뀐다. 실제로 성토가 +1.14 % 움직였고 원인은 랩탑의 실화면 검증이었다.
|
||||
⚠ `cross_sections` 표에 **시각 칸이 없어** 언제·누가 바꿨는지 DB 로는 못 짚는다.
|
||||
짚는 순서는 ① 커밋 diff 로 코드 배제 ② 서버 로그로 내 창 배제 ③ DB 재계산으로 「후」 값 확인
|
||||
④ 파일 mtime 으로 시각 잡기. **되받기 전후로 `tmp/snapshot_b08.py` 를 떠 두면 대조가 된다.**
|
||||
|
||||
#### ㉩ ⚠ md 에 없으면 원본(pdf·xlsx)을 열어 볼 것 (2026-09-08)
|
||||
|
||||
**변환본이 원본을 못 따라가는 자리가 있음.** 「돌쌓기 표준도 그림이 저장소에 없다」고 판단해
|
||||
원본 확보를 요청하려 했는데, **PDF 원본(173쪽)이 이미 저장소에 있었음.** md 로 옮길 때
|
||||
**그림 폴더를 안 받아 둔 것**이었고, PDF 에서 그대로 뽑아 넣으니 끝났음.
|
||||
|
||||
- 오늘 같은 계열이 셋임 — **표가 코드를 못 따라감**(8-25 화면 칸이 낡음) ·
|
||||
**내 출력이 실물을 못 따라감**(옵션 넷만 골라 찍고 「빠졌다」로 오해) ·
|
||||
**변환본이 원본을 못 따라감**(이번).
|
||||
- ⇒ **「없다」를 말하기 전에 원본 형식(pdf·xlsx·las)을 한 번 볼 것.** 특히 **그림·서식·병합셀**은
|
||||
텍스트 변환에서 빠지기 쉬움.
|
||||
|
||||
#### ㉨ ⚠ 「사유」 칸에는 왜 막혔나만 — 설명은 비고로 (2026-09-08, 하루에 **세 번**)
|
||||
|
||||
**받는 쪽은 사유 칸을 「상태」로 읽는다.** 설명을 거기 적으면 그 줄이 통째로 막힌 것이 된다.
|
||||
|
||||
- **겹침 설명을 `blocked_reason` 에 넣었더니** B09 가 「막힌 줄」로 읽어 금액을 안 붙였음.
|
||||
- **`blocked_kind: None`** — 「다른 표에서 이미 섬」은 막힌 것이 아니라 **여기서 세면 안 되는 줄**임.
|
||||
막힘으로 보내면 받는 쪽이 「만들어야 할 것」에 얹어 **결국 이중계상으로 감.**
|
||||
- **사유를 뭉뚱그리면 할 일이 갈리지 않음** — 「일위대가 없음」 하나로는
|
||||
「아직 안 만든 것」과 「성분이 빠져 못 세운 것」이 구분되지 않음. 앞엣것은 우리가 만들면 되고
|
||||
뒤엣것은 **참조 대상을 이어야** 풀림. 사유를 갈라 적으니 다음 일감이 바로 보였음.
|
||||
|
||||
**⇒ 사유 칸은 「왜 막혔나」만. 설명·계산 경위는 비고로. 그리고 사유를 갈라 적을 것.**
|
||||
|
||||
#### ㉫ ⚠⚠ **법이 두 동작을 묶은 자리 — 앞 동작만 세고 있지 않은지** (2026-09-09, 하루에 **두 번**)
|
||||
|
||||
**막힘 목록에도 없고 어떤 검사에도 안 걸리는 자리임.** 줄이 서 있고 값도 맞으니
|
||||
**「없는 줄」을 아무도 못 봄.** 오늘 둘 다 별표2 원문을 읽다가 나왔음.
|
||||
|
||||
```
|
||||
표토 별표2 「표토는 전량 **제거한 후** … 최고 홍수위보다 높은 장소로 **운반하고 쌓아두어야**」
|
||||
우리 — 표토제거(9-15) 한 줄만. **운반·적치가 없었음**
|
||||
|
||||
뿌리 품셈 9-20 가. 「벌개·제근 → 뿌리다듬기 → **적재** → **운반**」 (시공 과정 표준)
|
||||
별표2 「입목…과 **그 뿌리**, 표토는 전량 제거한 후 … 운반하고 쌓아두어야」
|
||||
우리 — 「제근·뿌리다듬기」 한 줄뿐. **적재·운반이 없었음**
|
||||
```
|
||||
|
||||
**⇒ 어떻게 찾나** — 원문에서 **동사가 둘 이상 이어진 문장**을 볼 것.
|
||||
`제거한 **후** … 운반하고 쌓아두어야` · `~하고 ~하여야` · `→ 로 이어진 과정 표준`.
|
||||
**앞 동작에 우리 줄이 있으면, 뒤 동작에도 줄이 있어야 함.**
|
||||
|
||||
**⇒ 찾은 뒤 지킬 것 셋**
|
||||
- **물량을 다시 세지 말 것** — 뒤 동작의 밑수는 **앞 동작의 물량 그대로**임(표토 운반 = 표토 제거량).
|
||||
다시 세면 이중계상.
|
||||
- **앞이 안 서면 뒤도 안 섬** — 밑수가 그 줄이므로 함께 막히게 할 것.
|
||||
- **법 문구를 사유에 실을 것** — 화면에서 **왜 이 줄을 세는지**가 보여야 함.
|
||||
|
||||
⚠ **품셈에 그 공종이 없을 수 있음**(뿌리 운반이 그랬음 — 10장 어디에 붙는지 원문이 안 말함).
|
||||
그때도 **줄은 세우고 코드만 비울 것.** 지어내지 말고 사유로. → 아래 ㉬.
|
||||
|
||||
#### ㉬ ⚠ **「수량은 서는데 품셈에 공종이 없는 줄」은 한 물음임 — 모아서 한 번에 물을 것** (2026-09-09)
|
||||
|
||||
따로 만나면 매번 새 물음처럼 보이나 **답은 하나임.**
|
||||
|
||||
```
|
||||
뿌리 운반 9-20 가. 가 과정으로 두었는데 붙을 공종이 10장에 없음
|
||||
도수로·절토사면 배수로 「수로」 전수 검색에 셋뿐 — 그 셋에 없음
|
||||
지장목제거 수량은 서는데 단가 공종이 없음
|
||||
부대시설 넷 국가지점번호판·안내판·차단기·수방자재 — 개소를 넣어도 단가 없음
|
||||
```
|
||||
|
||||
**⇒ 선례가 답을 정해 둠 — 확정 8-4** 「편책은 바자얽기(5-15)를 **빌려 쓰지 않음. 일위대가를 따로
|
||||
만들어 연결**」. 즉 **품셈에 없으면 우리가 만들어 잇는다**가 사용자 방침임.
|
||||
⚠ **빌려 쓸 후보가 있어도 답이 「만들라」일 수 있음** — 편책이 그랬음. **후보 유무로 미리 가르지 말 것.**
|
||||
|
||||
**⇒ 물을 때는 한 표로** — `공종명 | 수량은 서나 | 빌려 쓸 후보 | 없으면 새로 만들 성분`.
|
||||
넷을 따로 물으면 사용자가 네 번 답해야 하고, 그 사이 창마다 다르게 처리함.
|
||||
|
||||
#### ㉭ ⚠⚠ **대조표를 인용하기 전에 「그것이 언제 것인지」부터 볼 것** (2026-09-09, 한 건이 **세 번** 뒤집힘)
|
||||
|
||||
**오늘 가장 비싼 헛걸음임.** 구조물 터파기 한 줄을 두고 조율 창·데스크탑 메인·랩탑 보조가
|
||||
**세 바퀴**를 돌았고, **뿌리는 「오전에 뜬 대조표를 오후에 그대로 인용한 것」** 하나였음.
|
||||
|
||||
```
|
||||
대조표(오전) 「우리 2.30」 = 2.0 × (0.95 + 0.2) ← 옛 두께식 · 기초 몫 없던 때
|
||||
그 사이 바뀐 것 확정 ② 두께 → 뒷길이 기반 (`8141eb34`)
|
||||
확정 3차 → 기초유/기초버림 축, 기초 몫 0.45·0.07 이 붙음 (`b4ae32d1`·`759343e3`)
|
||||
실제(오후) H=2.0 · 두께 0.83 · 기초유 → **2.510** = 실무 정본 2.51 과 자릿수까지 같음
|
||||
```
|
||||
|
||||
**⇒ 그 낡은 2.30 하나가 만든 잘못된 결론 셋**
|
||||
```
|
||||
1. 「우리가 6.6배 과다 — 955만원을 줄일까요」 ← 사용자에게 갈 뻔한 틀린 물음
|
||||
2. 「기초 몫 0.45 를 빠뜨렸다」 ← 이미 들어 있었음
|
||||
3. 「화면 0.5 와 수량 H 가 다른 말을 한다」 ← 셋이 같은 말을 하고 있었음
|
||||
```
|
||||
|
||||
**⇒ 지킬 것**
|
||||
- **대조표·실측표를 인용할 때 날짜와 커밋을 함께 적을 것.** 「오늘」은 날짜가 아님 — 하루에 두 번 바뀜.
|
||||
- **금액이 걸린 인용은 그 자리에서 다시 재 볼 것.** 재는 데 몇 분, 헛걸음은 세 창 몇 시간.
|
||||
- **표를 만들 때 머리에 「잰 시각 + 그때 커밋」을 박아 둘 것.** 표가 스스로 낡았다고 말하게.
|
||||
- ⚠ **이 병은 「값이 틀린 것」이 아니라 「값이 옛것인 것」이라 어떤 검사에도 안 걸림.**
|
||||
시험도 통과하고 숫자도 맞음 — **그때는** 맞았기 때문.
|
||||
|
||||
**⇒ 곁들여 나온 것** — 그럴듯하게 맞아떨어지는 설명이 **같은 건에서 두 번** 틀렸음
|
||||
(`0.7 × 0.5 = 0.35` · 「H × 평균두께」 모양 일치). **딱 맞아떨어질수록 곱셈을 해 볼 것.** → ㉧
|
||||
|
||||
**⇒ 같은 집안 하나 더 — 「잘라서 인용한 자리」를 「없는 자리」로 읽지 말 것** (2026-09-09).
|
||||
규준틀 사유를 **44자로 잘라** 보고하다 **「아무 표시가 없다」**로 냈음. 실제로는 그 뒤에
|
||||
「재료량은 품셈 11-2 [주]④ 「설계수량에 따른다」라 미확보」가 **적혀 있었음.**
|
||||
⇒ **길이를 줄여 인용할 때는 「…(잘림)」을 붙일 것.** 안 붙이면 **자기가 자른 자리를 사실로 읽음.**
|
||||
⇒ 그리고 **「아무도 말 안 함」과 「이쪽만 말 안 함」은 다름** — 보내는 쪽이 이미 말하고 있을 수 있음.
|
||||
|
||||
#### ㉮ ⚠⚠ **시험이 「지금 동작」을 베껴 적으면 버그를 잠근다** (2026-09-09, 실물 한 건)
|
||||
|
||||
**제근 단가가 두 배로 서 있던 것을 시험이 「맞다」고 지키고 있었음.**
|
||||
```
|
||||
있던 시험 「제근은 보통인부 줄이 **둘**이다」
|
||||
실제 그 둘이 곧 이중계상 — 품셈은 「0.2㎥를 쓰면 이 값 **또는** 0.7㎥를 쓰면 저 값」
|
||||
⇒ 시험이 버그를 계약으로 못 박아, 고치려 하면 **시험이 먼저 막아섬**
|
||||
```
|
||||
|
||||
**⇒ 어떻게 생기나** — 시험을 쓸 때 **원문을 안 보고 지금 출력을 베껴** 적으면 이렇게 됨.
|
||||
「돌려 보니 둘이 나오네」 → 「둘이어야 한다」. 그 순간 **틀린 값이 정본이 됨.**
|
||||
|
||||
**⇒ 지킬 것**
|
||||
- **시험의 기대값은 「돌려 본 결과」가 아니라 「원문」에서 올 것.** 기대값 옆에 **근거를 한 줄** 적을 것
|
||||
(품셈 절 번호·실무 시트 셀 좌표). 근거를 못 적겠으면 **그 시험은 아직 쓰면 안 되는 것**임.
|
||||
- **개수를 세는 시험**(「줄이 둘이다」·「성분이 N개다」)이 가장 위험함 — 값이 아니라 **모양**을 잠그는데
|
||||
모양이 틀렸을 때 그것을 알려 줄 것이 없음.
|
||||
- **뒤집을 때는 「왜 뒤집혔는지」를 그 시험 설명에 남길 것.** 안 남기면 다음 사람이 되돌림.
|
||||
|
||||
⚠ **㉪(계약이 바뀌면 시험부터 의심할 것)과 다른 병임.** ㉪는 **시험이 낡은 것**이고,
|
||||
이것은 **시험이 처음부터 틀린 것**임. 낡은 시험은 계약이 바뀔 때 깨져서 드러나는데,
|
||||
**처음부터 틀린 시험은 영영 안 깨짐** — 지키는 대상이 실제로 그렇게 돌고 있으므로.
|
||||
|
||||
#### ㉯ ⚠ **검산은 「어디까지 쟀는지」를 함께 적을 것** (2026-09-09)
|
||||
|
||||
뒤채움 두께가 품셈 범위 안에 드는지 재면서 **H 4.0m 까지만 재고 「세 자리 다 범위 안」**으로 냈음.
|
||||
그물을 시험으로 옮겨 **H 6.0 까지** 늘리자 **4.0~4.5 사이에서 범위 밖으로 나감**이 바로 걸렸음.
|
||||
```
|
||||
우리 두께 직고에 0.30/m 로 **계속** 붙음 (확정 ② 실무 정본 식)
|
||||
품셈 표 하부 두께가 **1.40m 에서 멈춤** ([주]⑨ 직고 7m 칸 상한)
|
||||
⇒ 높이 축의 기울기가 다름. 어느 쪽도 상대를 틀렸다고 못 함 — **갈리는 자리를 값으로 남길 것**
|
||||
```
|
||||
**⇒ 「범위 안이다」는 잰 범위 안에서만 참임.** 시험 이름·주석에 **잰 구간**을 박을 것.
|
||||
**⇒ 그리고 범위 검산은 등호로 바꾸지 말 것** — 밖으로 나가면 「틀림」이 아니라 **「봐야 함」**임.
|
||||
우리 식이 맞을 수도 있음.
|
||||
|
||||
#### ㉰ ⚠⚠ **표를 읽을 때 칸을 이어 붙이면 「원문에 없는 값」이 만들어진다** (2026-09-09)
|
||||
|
||||
**앞 칸의 숫자 + 뒤 칸의 단위가 붙어, 원문 어디에도 없는 밑수가 섰음.**
|
||||
```
|
||||
품셈 13-2-4 … 0.28 | **0.36** / **㎥당** | 0.60 … ⇒ 「0.36㎥당」 (0.36 은 뒷길이 60 의 ㎡당 값)
|
||||
품셈 5-19-2 15 | **30**(표토두께 ㎝) / **㎥ 당** | … ⇒ 「30㎥당」 (30 은 두께)
|
||||
```
|
||||
⚠ **잘못된 값이 아니라 「없는 값」임.** 그리고 **두 숫자 다 그 표 안에 실재**해서
|
||||
**그럴듯함 — 어떤 검사에도 안 걸림.**
|
||||
|
||||
**⇒ 지킬 것**
|
||||
- **표를 훑을 때 「칸 하나 안에서만」 볼 것.** 셀을 죽 이어 붙여 정규식을 걸면 이 병이 남.
|
||||
「100㎥당」처럼 **한 칸에 다 든 것**만 인정.
|
||||
- **밑수가 늘어난 것은 좋아진 신호가 아님** — 이 건에서 밑수 확보가 **182 → 180 으로 줄어든 것이
|
||||
개선**이었음(없는 값 둘이 빠지고 넷이 「미확보」로 정직해짐). → ㉠
|
||||
- **잡은 실례를 그대로 재는 시험을 남길 것.** 그래야 다음에 정규식을 넓힐 때 걸림.
|
||||
|
||||
⚠ **같은 날 잡은 「인」 오독과 다른 병임** — 「인」은 **잘못된 단위**(원문에 있는 글자를 잘못 읽음),
|
||||
이것은 **없는 숫자**(원문에 없는 조합을 만들어 냄). **고치는 자리도 다름.**
|
||||
|
||||
#### ㉱ ⚠⚠ **코드 추적으로만 낸 「원인」은 자료를 재 보기 전까지 가설임** (2026-09-09, 하루를 돌아감)
|
||||
|
||||
계획서 3-14 에 **「원인 찾음(코드 추적)」**이라 적어 두었는데 **자료를 안 재 봤고, 재 보니 틀렸음.**
|
||||
```
|
||||
적어 둔 원인 「관을 나중에 놓으면 그 측점이 안 생긴다」 — 측점 생성 호출부를 따라가 낸 결론
|
||||
실제 측점 440.0 · 620.0 · 720.0 · 900.0 이 **이미 있었고 구조물 이름표까지 달고 있었음**
|
||||
진짜 원인 관 자리와 측점 자리가 **최대 0.5m 어긋나는 것이 설계**(정수 미터 격자로 스냅)인데
|
||||
**붙이는 쪽이 0.02m 로만 봐서** 안 붙었음
|
||||
```
|
||||
|
||||
**⇒ 그 틀린 원인이 하루 동안 판단 셋을 끌고 다녔음**
|
||||
```
|
||||
1. 「B06 자리」로 넘겨 B08·B09 가 손을 못 댐
|
||||
2. 조율 창이 **「0.05 와 0.5 를 맞추지 말라」**고 지시 — **좁은 쪽이 스냅을 모르는 것**이 진짜 문제였음
|
||||
3. [측점 만들기] 단추를 **주 해법**으로 잡음 (실제로는 0개짜리 예외 처리였음)
|
||||
```
|
||||
|
||||
**⇒ 지킬 것**
|
||||
- **「원인」이라 적을 때 어떻게 알았는지를 함께 적을 것** — `코드 추적` / `실측` / `원문`.
|
||||
**추적만으로 낸 것은 「가설」이라 쓸 것.**
|
||||
- **가설을 남에게 넘기기 전에 한 번 잴 것.** 이 건은 **「그 측점이 정말 없는가」를 한 번 조회**하면
|
||||
끝날 일이었음.
|
||||
- **남이 넘겨준 진단을 그대로 이어받아 지시하지 말 것** — 조율 창이 그 위에 지시 둘을 얹었고
|
||||
**둘 다 틀렸음.**
|
||||
|
||||
⚠ **㉭(대조표가 낡음)과 다른 병임.** 그건 **한때 맞았던 값**이 낡은 것이고,
|
||||
이것은 **처음부터 안 재 본 것**임. 낡은 값은 날짜로 걸러지지만 **안 잰 것은 걸러지지 않음.**
|
||||
|
||||
#### ㉲ ⚠⚠ **「막혔다」고 말하기 전에 차단 표시를 볼 것 — 화면 문구는 근거가 아님** (2026-09-09, 하루에 **두 번**)
|
||||
|
||||
**주의 문구를 차단으로 읽어 두 번 헛돌았음.**
|
||||
```
|
||||
아침 겹침 설명을 `blocked_reason` 에 넣었더니 **B09 가 「막힌 줄」로 읽어 금액을 안 붙였음**
|
||||
⇒ 비고 칸으로 옮기고 막힘은 **진짜 막힐 때만** 넣게 고침
|
||||
밤 「관종을 안 정해 기본값(파형강관)으로 섰습니다」를 **조율 창과 B09 창이 「막는 사유」로 읽음**
|
||||
실제 `blocked_kind` 는 **공종코드를 못 찾을 때만** 섬(`if code is None`).
|
||||
기본값 파형강관은 코드가 있어 **줄도 서고 금액도 섬** — 그것은 `kind_note`(주의)였음
|
||||
⇒ 「칸을 만들라」는 지시가 두 창에 나갔고 **칸은 이미 있었음**
|
||||
```
|
||||
|
||||
**⇒ 지킬 것**
|
||||
- **차단 여부의 근거는 `blocked_kind` 하나임.** 사유 글자·주의 문구·「미정」이라는 말은 근거가 아님.
|
||||
- **막힘을 보고할 때 `blocked_kind` 값을 함께 적을 것.** 「막혔음」만 적으면 받는 쪽이 문구를 읽음.
|
||||
- **화면에서도 「주의」와 「차단」을 갈라 보일 것** — 같은 자리에 뜨면 사람도 코드도 헷갈림.
|
||||
- ⚠ **남이 옮겨 준 사유 문구를 그대로 판단 근거로 쓰지 말 것.** 두 번 다 **문구를 옮긴 창과 받은 창이
|
||||
함께 틀렸음.** → ㉱
|
||||
|
||||
⚠ **㉨(「사유」 칸에는 왜 막혔나만)의 반대쪽 얼굴임.** 그건 **쓰는 쪽** 규칙이고 이것은 **읽는 쪽** 규칙임.
|
||||
사유 칸에 설명을 섞으면 읽는 쪽이 막힘으로 읽고, 읽는 쪽이 표시를 안 보면 안 막힌 것을 막혔다고 함.
|
||||
|
||||
#### ㉳ ⭐⭐ **예외는 「git 밖 시험 목록」이 아니라 「정본 스키마」에 적을 것** (2026-09-09)
|
||||
|
||||
**칸은 건너가는데 까닭은 안 건너감** — 그래서 **같은 판인데 창마다 시험 결과가 다름.**
|
||||
```
|
||||
정본 등록부 B05_Profile_Structure_Types.json git **안** ⇒ 다른 창에 감
|
||||
정책 시험 tmp/tests/…_registry_policy.py git **밖**(CLAUDE.md 4장) ⇒ **안 감**
|
||||
⇒ 칸을 열고 시험의 예외 목록에 까닭을 적으면 **칸만 건너가고 까닭은 안 건너감**
|
||||
⇒ 연 창은 통과, 받은 창은 실패. **둘 다 맞는 상태인데 서로 틀렸다고 함**
|
||||
```
|
||||
|
||||
**⇒ 고침 — 「예외」를 목록에서 스키마로 옮길 것.** 실제로 세운 두 갈래:
|
||||
```
|
||||
empty_means 「비워 두는 것이 뜻인 칸」 — 비면 계산 쪽이 기준값으로 돌고 그 사실이 화면에 뜸.
|
||||
값을 넣으면 그 값이 이김. **까닭 한 줄을 값 옆에** 적음
|
||||
default_basis 기본값이 **도메인 확정값이 아닐 때** 그 뜻(「다단 없음」·「안 더함」).
|
||||
법정·확정 수치면 **비워 둠** — 비어 있는 것이 「확정값」이라는 뜻
|
||||
```
|
||||
⇒ 시험은 **목록을 읽지 않고 그 칸을 읽음.** 까닭이 한 낱말이면 잡는 시험도 함께 둘 것
|
||||
(빈 문구로 통과하는 것을 막음).
|
||||
|
||||
**⇒ 규칙 한 줄 — 등록부에 칸을 열 때 예외 목록을 고치지 말고 까닭을 값 옆에 적을 것.**
|
||||
|
||||
⚠ **다만 구조를 고쳐도 「옮기는 일」은 남음** (2026-09-09 그날 저녁 실측).
|
||||
스키마를 고치면 **그 스키마를 읽는 시험 파일도 바뀌는데**, 그 파일이 `tmp/tests`(git 밖)라
|
||||
**여전히 안 건너감.** 실제로 한 창이 스키마를 고친 뒤 다른 창이 **옛 시험으로 2건 실패**했음.
|
||||
```
|
||||
⇒ 스키마를 고쳤으면 **그 시험 파일 내용을 메시지로 보내** 각 창이 자기 폴더에 놓을 것
|
||||
⚠ 남의 폴더에 직접 쓰지 말 것 — 보내고, 받는 쪽이 놓음
|
||||
⇒ 「내 창은 통과인데 저쪽은 실패」가 보이면 먼저 **시험 파일 판을 맞출 것**
|
||||
```
|
||||
|
||||
흙깎기 장비 이름이 표에 없고 **[주]① 이 지정**
|
||||
기초잡석 운반 표에 「덤프트럭(15ton)」 줄만 있고 시간이 빔 — **10-12 가 거리로 냄**(같은 15ton)
|
||||
커플링밴드 표 「EA | (빈칸) | 필요시적용」 — **[주]① 「1EA/6m 이며 필요시 별도 산정」**
|
||||
```
|
||||
**⇒ 빈 칸을 만나면 그 표의 [주]·비고·앞뒤 절 참조를 먼저 훑을 것.** 셋 다 **표만 보면 「없음」**이었고
|
||||
**[주]를 보면 있었음.**
|
||||
|
||||
**⇒ 사유 문구를 갈라 쓸 것 — 다음 사람이 볼 때 뜻이 아주 다름**
|
||||
```
|
||||
「원문에 값이 없음」 ⇒ 영영 막힌 것으로 읽힘. **지어내지 말라는 뜻**
|
||||
「[주]에 있음 — 조건이 붙음」 ⇒ 조건만 정하면 서는 것
|
||||
「다른 절이 냄 — 거리/규격 필요」 ⇒ 그 값이 오면 저절로 섬
|
||||
```
|
||||
⚠ 셋을 뭉쳐 「값 없음」으로 적으면 **사용자가 없는 것을 정하러 감.** → ㉲
|
||||
|
||||
⚠ **㉢(밑수 단위가 표 밖)과 같은 집안이나 다른 것임** — 그건 **단위**가 표 밖에 있는 것이고
|
||||
이것은 **값 자체**가 표 밖에 있는 것임. 둘 다 「표만 읽는 코드」가 못 잡음.
|
||||
|
||||
#### ㉵ ⚠⚠ **`0` 과 「없음」을 같은 것으로 두지 말 것** (2026-09-09, 하루에 **세 번**)
|
||||
|
||||
```
|
||||
표토 대상 면적이 **0 ㎡** 인데 **0 ㎥ 를 「값 있음」으로** 냄
|
||||
⇒ 받는 쪽이 「표토가 없는 노선」으로 읽음. 두께 미입력은 막고 있었는데 면적 0 만 통과
|
||||
사토장 구조물을 지웠는데 **설계 결과가 0.0 을 늘 실어** 횡단도·면적표에 계속 남아 있었음
|
||||
⇒ 「칸이 있는 것」과 「값이 있는 것」이 달랐음
|
||||
말풍선 폭을 이분법으로 찾아 남은 **0.0063㎥** 에 「⚠ 못 담음」 경고가 늘 떴음
|
||||
⇒ 표시 자릿수에서 안 보이는 몫을 「남았다」고 말했음
|
||||
```
|
||||
|
||||
**⇒ 세 가지 다 「0 이라는 값」이 거짓말을 한 것임.** 값이 없어서가 아니라 **있어서** 안 걸림.
|
||||
|
||||
**⇒ 지킬 것**
|
||||
- **「없음」은 `None`(빈 값)으로 낼 것.** `0` 은 **「재 보니 0 이었다」**는 뜻으로만 쓸 것.
|
||||
- **얹는 코드를 쓰면 지우는 길도 함께 쓸 것.** 사토장이 그 자리였음 — 얹기만 하고 안 지워
|
||||
**구조물을 없앤 뒤에도 값이 남았음.** 시험은 「놓았을 때」만 보고 「지웠을 때」를 안 봤음.
|
||||
- **경고는 사람이 보는 자릿수로 판정할 것.** 0.0063 처럼 화면에 안 보이는 몫을 경고하면
|
||||
**경고 전체가 믿을 것이 못 됨.**
|
||||
|
||||
⚠ **셋 다 시험은 통과했고 화면에서 잡혔음.** 「있다」와 「돈다」의 그 자리임 — **지우는 길·빈 자료·
|
||||
표시 자릿수는 시험이 잘 안 훑는 세 곳**임.
|
||||
|
||||
### ⚠ 좁은 폭에서 화면이 깨지면 볼 자리 셋 (2026-09-09 B03 업로드 현황에서 셋이 한꺼번에)
|
||||
|
||||
셋 다 **넓은 화면에서는 안 보이다가 창을 줄이면 터짐.** 다른 페이지도 같은 짜임이면 같이 깨짐.
|
||||
|
||||
- **`minmax(260px, 1fr)` 는 「260보다 좁아질 수 없다」는 뜻** — 컨테이너가 그보다 좁아지면
|
||||
**격자가 컨테이너를 뚫음.** ⇒ `minmax(min(260px, 100%), 1fr)` 로 적을 것.
|
||||
- **접는 분기가 아예 없는 2열** — `repeat(2, minmax(0,1fr))` 는 좁아져도 2열을 고집해
|
||||
열마다 카드 하나도 못 담음. ⇒ `repeat(auto-fit, minmax(min(360px,100%), 1fr))`.
|
||||
- **flex 자식의 `min-width: 0` 누락** — 기본값이 `auto` 라 **내용보다 안 줄어듦.**
|
||||
안쪽 격자가 넘치면 **페이지 몸통이 통째로 가로로 밀림.**
|
||||
|
||||
**판정은 눈이 아니라 숫자로** — 폭을 여러 개(1400·1000·760·620·480) 돌며
|
||||
`scrollWidth − clientWidth` 가 0 인지 볼 것. 창이 눈에 안 따라와도 **뷰포트 폭만 바꾸면
|
||||
레이아웃은 다시 계산되므로 측정은 됨.**
|
||||
|
||||
**⚠ 같은 짜임이 다른 화면에도 있음 — 훑은 목록** (2026-09-09, 아직 안 고침)
|
||||
|
||||
```
|
||||
㉡ 접는 분기 없는 고정 2열 — 아홉 곳 (B03 에서 깨진 것과 같은 짜임)
|
||||
B01_Dashboard_UI_Style.css:22 · :134 · :166
|
||||
B02_ProjRegister_UI_Style.css:18 ← 등록 폼, 사용자가 자주 봄
|
||||
B05_Profile_UI_Style.css:303 · :338 · :436
|
||||
B05_Profile_UI_Style_Structures.css:157
|
||||
B06_Section_UI_Style.css:286
|
||||
B06_Section_UI_Style_Cross_Controls.css:314
|
||||
B07_DesignDetail_UI_Style.css:55 (1fr 1fr)
|
||||
⚠ B05·B06 은 좌측 패널 안이라 폭이 원래 좁음 — B03 보다 먼저 깨질 수 있음
|
||||
㉢ min-width:0 누락 — 62 곳. 전부가 문제는 아니고 ㉡ 와 겹치는 자리부터 볼 것
|
||||
㉠ minmax(고정px) — B01_Dashboard_UI_Style.css:231 한 곳뿐(150px 라 위험 낮음)
|
||||
㉣ @media 가 아예 없는 화면 — B05_* · B06_* · B08_* · B09_*
|
||||
⇒ 좁은 폭 대응을 안 한 화면이 넷임
|
||||
```
|
||||
**고칠 순서 제안** — ① B02 등록 폼 ② B05·B06 좌측 패널 2열 ③ 나머지.
|
||||
|
||||
### ⚠ **`reload()` 만으로는 vite 가 옛 모듈을 물고 있을 수 있음 — 캐시부터 비울 것** (2026-09-09)
|
||||
|
||||
지침이 「페이지·공용 코드는 `page.reload()` 한 번(vite dev)」이라 적고 있으나 **그것으로 안 걷힌 자리**가 나왔음. **한 증상을 두고 세 창이 차례로 헛짚었음.**
|
||||
```
|
||||
증상 벽 터파기가 화면에 0건 (관 터파기는 뜸)
|
||||
헛짚음 ① 「그리기 가드가 먹는다」 ② 「초안 경로에서만 그려진다」 ③ 「백엔드 재시작을 안 했다」
|
||||
실측 백엔드 stale:false · API 상세에 revetment.foundation="기초유" **이미 옴** · 화면만 옛것
|
||||
답 CDP Network.clearBrowserCache + sessionStorage.clear + reload → **바로 뜸**
|
||||
```
|
||||
⇒ **TS 를 고쳤는데 화면이 옛것 같으면 「캐시부터」.** `page.goto`·`reload` 만으로는 부족할 수 있음.
|
||||
⚠ **브라우저 창은 재시작하지 말 것** — 캐시 비우기로 끝남(지침 4장 그대로).
|
||||
|
||||
### ⚠ 공용 이름을 **갈거나 지울 때는 창들에 먼저 알릴 것** (2026-09-09, 하루에 세 번)
|
||||
|
||||
**값을 한 벌로 두어도 이름이 바뀌면 깨진다.** 「공용 상수를 직접 읽으니 저절로 따라온다」가
|
||||
**반만 맞는 말**임 — 값은 따라오고 **이름은 안 따라옴.**
|
||||
```
|
||||
① BLANK_DRAWINGS·BLANK_LABELS 를 걷어냄 → 남의 tmp/tests 가 수집 단계에서 터져 **267건이 통째로 멈춤**
|
||||
② tmp/tests 는 git 밖 → 병합에 안 실림. **지운 쪽이 알리는 것 말고 길이 없음**
|
||||
③ 두께 상수 이름 셋을 갈이 → 남의 표준도 그림이 KeyError 로 **죽음**
|
||||
thickness_base_m·_top_coeff·_bottom_coeff → _top_add_m·_slope_per_m·_height_base_m
|
||||
```
|
||||
⇒ **새로 넣는 것은 안전함. 갈거나 지우는 것만 알리면 됨.**
|
||||
⇒ 셋 다 **실화면·실행에서만** 드러났음 — 코드만 봐서는 안 잡힘.
|
||||
|
||||
### ⚠ **[저장]은 횡단 설계를 다시 계산하지 않음 — 새 키를 넣었으면 「재생성」을 돌릴 것** (2026-09-09)
|
||||
|
||||
**설계 엔진에 키를 새로 넣었는데 저장분에 안 들어오는 자리.** 하루를 「값이 0 이다」로 보냄.
|
||||
```
|
||||
[저장] 편집을 적용하고 Node 로 **구조물 면적·유토곡선만** 다시 냄(_recompute_stored_designs)
|
||||
⇒ 저장분이 **옛 34키 그대로** — 새 키(bench_cut_length_m 등)가 안 들어감
|
||||
재생성 파이썬 설계 엔진이 **전 측점을 새로 냄** ⇒ design 키가 **38개**로 늘어남
|
||||
```
|
||||
**세 창에 돌린 절차 (순서를 지킬 것)**
|
||||
```
|
||||
① git sync
|
||||
② npm run build:server-calc ⚠ 번들은 git 밖 — 창마다 각자 해야 함
|
||||
③ 백엔드 재시작 ⚠ @lru_cache 가 옛 등록부·config 를 물고 있음
|
||||
④ B06 열고 카드가 다 뜰 때까지 기다림
|
||||
⑤ POST /api/projects/{pid}/sections/{routeId}/regenerate
|
||||
body {"cross_half_width_m": <지금 쓰는 반폭>}
|
||||
(routeId·반폭은 GET /api/projects/{pid}/sections/context 의 route_id·defaults)
|
||||
⑥ 확인: GET …/sections/{routeId}/detail → **design 키 38개**
|
||||
```
|
||||
⚠ **[확정]은 필요 없음** — 저장·재생성만으로 정본이 섬.
|
||||
⚠ **추정치를 그대로 쓰지 말 것** — 같은 노선에서 추정 12,931㎡ → 실측 **13,699.7㎡ (+6%)** 로 갈렸음.
|
||||
|
||||
### ⚠ 창 크기를 바꿀 때 `page.set_viewport_size` 를 부르지 말 것 — `resize(w, h)` (2026-09-09)
|
||||
|
||||
**그 한 줄이 화면을 그 크기에 박아** 사용자가 창을 끌어도 안 따라옴. `Emulation.
|
||||
clearDeviceMetricsOverride` 로도 안 풀림(Playwright 가 다시 걺) — **드라이버 재시작 말고는
|
||||
푸는 길이 없음.** 데스크탑이 그렇게 하루를 그 상태로 보냈고, **랩탑이 멀쩡했던 것은
|
||||
그 호출을 안 썼기 때문**이지 배율·인자 차이가 아니었음.
|
||||
|
||||
- 드라이버에 **`resize(w, h)`** 를 넣었음 — CDP `Browser.setWindowBounds` 로 **창 자체**를 바꿔
|
||||
화면이 따라옴. `page.set_viewport_size` 는 이제 **막힘 문구와 함께 거부**됨.
|
||||
- ⚠ **`.claude/` 는 git 밖이라 커밋으로 안 건너감** — 시놀로지 동기화에 기대거나 창마다 붙여야 함.
|
||||
붙었는지는 `grep -n "def resize" .claude/browser_driver.py` 로 확인.
|
||||
- **화면이 안 따라와도 폭 측정은 됨** — 뷰포트 폭만 바꾸면 레이아웃은 다시 계산되므로
|
||||
`scrollWidth − clientWidth` 로 재는 검증은 그대로 유효함. 눈으로 보는 확인만 뒤로 미룰 것.
|
||||
|
||||
### ⚠ 브라우저 명령 파일은 **다른 이름으로 쓴 뒤 `mv` 로 넣을 것** (2026-09-09, 하루에 두 번)
|
||||
|
||||
`tmp/browser/cmd/` 에 파일을 **직접 쓰면 드라이버(1초 폴링)가 쓰는 도중에 집어감.**
|
||||
**실행은 되고 `OK` 도 뜨는데 내용이 비어 아무 일도 안 일어남** — driver.log 에도 안 남음.
|
||||
⇒ **딴 이름으로 다 쓴 다음 `mv` 로 옮길 것.** 「도는 척만 하는 검사」와 같은 계열이라 특히 나쁨(㉢).
|
||||
|
||||
### 공용 브라우저가 로그인 화면에서 멈췄을 때 (2026-09-08 — 두 창이 같은 자리에서 막힘)
|
||||
|
||||
**튕기면 서버가 아니라 쿠키를 의심할 것.** 세션은 DB 표(`sessions`)라 **서버 재시작으로 안 지워진다.**
|
||||
날아가는 것은 브라우저 쿠키 쪽이다(새 프로필·만료·캐시 비우기에 함께).
|
||||
쿠키는 포트를 안 가려 **8000·5173 중 한 번만 로그인하면 둘 다 붙는다.**
|
||||
|
||||
**되살리는 법** — 자격증명은 `tmp/aislo_cred.json`(git 밖, 사용자가 유지 — **지우지 말 것**).
|
||||
없으면 **짐작하지 말고 사용자에게 요청**할 것. 명령 파일에서:
|
||||
|
||||
```python
|
||||
import json
|
||||
cred = json.load(open("tmp/aislo_cred.json", encoding="utf-8"))
|
||||
api = page.context.request # ⚠ 브라우저 컨텍스트의 요청 — 쿠키를 공유한다
|
||||
if api.get("http://localhost:8000/api/auth/session").status != 200:
|
||||
api.post("http://localhost:8000/api/auth/login/request", data=cred)
|
||||
log(api.get("http://localhost:8000/api/auth/session").status) # 200 이어야 함
|
||||
```
|
||||
|
||||
⚠ **`page.evaluate` 안에서 `fetch` 로 하지 말 것** — 로그인 화면이 그 사이 이동해
|
||||
`Execution context was destroyed` 로 죽는다(2026-09-08 두 번 겪음). `page.context.request` 는
|
||||
페이지 이동과 무관하다.
|
||||
|
||||
⚠ `dev_up.py`·`browser_driver.py` 에 **로그인 처리가 없는 것이 맞다** — 드라이버는 창만 띄우고
|
||||
로그인은 **명령 파일 몫**이다. grep 해도 안 나온다.
|
||||
|
||||
⚠ **`shot()` 은 확장자를 붙일 것** — `shot("이름")` 은 `unsupported mime type ""` 로 죽는다.
|
||||
`page.screenshot(path="tmp/browser/shots/이름.png")` 로 쓰는 편이 안전하다.
|
||||
|
||||
⚠ **값을 만지기 전에 「어느 프로젝트가 열려 있나」부터 볼 것** —
|
||||
`localStorage['frd_current_project_id']`. 사용자 프로젝트(`5cff3920`)면 **읽기만** 한다.
|
||||
|
||||
### 검증 함정 모음 — 재기 전에 볼 것 (2026-09-07 정리)
|
||||
|
||||
끝난 절을 `plans/` 로 옮기며, **다시 걸리면 또 오진할 것들만** 여기로 건져 왔음.
|
||||
괄호는 원래 절 번호(`docs/raw/plans/2026-09-07_plan_completed_items.md` 에 원문 있음).
|
||||
|
||||
- ⚠ **시험을 저장소 실물에 매지 말 것 — 만든 창에서만 통과함** (2026-09-09).
|
||||
`test_prj_identify.py::test_compound_vertical_prj` 가 특정 프로젝트의
|
||||
`…/input/prj/result.prj` 를 **실경로로 열어** epsg 를 확인하는데, **그 프로젝트가 지워지자**
|
||||
두 PC 에서 `FileNotFoundError` 로 깨졌음. `tmp/tests` 는 git 밖이지만 시놀로지로 **파일은
|
||||
건너오고 저장소 자료는 창마다 다름** — 그래서 **남의 PC 에서만 깨지는 시험**이 됨.
|
||||
**⇒ 실물 파일이 필요하면 없을 때 `skip` 하게 가드를 붙일 것.** 같은 파일의 다른 넷은 이미
|
||||
그렇게 돼 있었고 하나만 빠져 있었음. **깨진 것을 「병합이 망가뜨렸다」로 오진하기 쉬운 자리임.**
|
||||
|
||||
- **옛 코드로 도는 서버에 속지 말 것** — 속도·화면을 재기 전에 ① 백엔드를 확실히 재시작하고
|
||||
② **그 수정의 로그나 새 응답 항목이 실제로 있는지** 먼저 볼 것. 하루에 두 번 속았고, 한 번은
|
||||
응답에 새 항목이 아예 없는데 화면은 정상처럼 보였음(다른 값으로 세는 폴백). (0-12)
|
||||
- **캐드(B07) 화면은 `npm run build` + 캐시 비우기** 를 해야 바뀜 — 안 하면 옛 화면을 봄. (4-4)
|
||||
- **화면이 백지가 되면 옛 vite 프로세스를 먼저 볼 것** (2026-09-07 보조 창, 하루 세 번 겪음) —
|
||||
증상: 해시 이동은 되는데 `document.body` 가 빈 문자열, 콘솔에 `ERR_CONNECTION_RESET`.
|
||||
원인: 백엔드를 다시 띄우면 `main.py` 가 vite 를 새로 띄우는데 **옛 vite 가 남아** 탭이 죽은
|
||||
소켓을 물고 있음(실측: 5174 를 쥔 node 하나 + 유령 둘). 캐시만 비워서는 안 살아남.
|
||||
되살리는 순서 — ① `Get-NetTCPConnection -LocalPort <포트>` 로 주인 확인, 유령 node 정리
|
||||
② 백엔드 재시작 ③ **`about:blank` → `http://localhost:<포트>/`(wait "load") → 그다음 해시**.
|
||||
해시로 곧장 가면 계속 백지였고, 루트를 먼저 거치면 살아남.
|
||||
- **조정창은 두 벌이고 조작이 갈려 있음** (2026-09-07 두 창이 각각 헛짚음) — 좌측 [횡단 조정]
|
||||
dock 에는 **이동 십자(▲▼◀▶)·집수정 9키가 CSS 로 숨겨져** 있고(`_Style_Cross_Controls.css:244~`),
|
||||
그 십자는 **도면 위에 뜨는 오버레이 창에만** 있음. 반대로 오버레이에는 값·형식 행이 숨겨져
|
||||
있음(2026-08-29 「값은 좌측, 위치제어는 도면 위」). dock 에서 ◀ 를 찾으면 DOM 에는 있는데
|
||||
`vis:false` 인 것이 **정상**임 — 결함으로 오해하지 말 것.
|
||||
- **요소가 보이는지는 `offsetParent` 로 재지 말 것** (2026-09-07, 두 창이 같이 걸림) —
|
||||
**고정 위치(`position: fixed`) 요소는 보이든 안 보이든 `offsetParent` 가 늘 `null`** 이라
|
||||
「안 보인다」로 오판함. 크기로 잴 것 — `getBoundingClientRect()` 의 width/height, 또는
|
||||
`offsetWidth || offsetHeight`. 둘 다 0 이면 실제로 안 그려진 것임.
|
||||
- **UI 를 붙였으면 크기도 같이 잴 것** (2026-09-07) — 알약 레인을 종단 패널에 넣고 「알약이 뜬다」만
|
||||
보고 넘겼다가, 잰 높이에서 레인 몫을 안 빼 되먹임이 생겨 **패널이 16,664px 로 부풀었음**.
|
||||
그 상태에서는 카드·알약을 아예 못 눌러 다음 검증이 통째로 막혔음.
|
||||
- **3D 클릭을 잴 때** — `__corridorScene.project()` 좌표는 DOM 이 덮고 있는지를 안 봄.
|
||||
클릭 직전에 `document.elementFromPoint(x, y) === document.querySelector('canvas')` 를 확인하거나
|
||||
종단 오버레이 핸들(`.ui-workflow-overlay__handle`)을 먼저 접을 것. 이걸 몰라 두 번 오진. (2-2)
|
||||
- **조정창은 셋으로 쪼개져 있음 — 「단 수」는 어느 조정창에도 없음** (2026-09-07, 3-7 에서
|
||||
한참 헤맴). ① **카드 위 조정창** = 이동 십자·집수정 9키 **전용** ② 좌측 **[횡단 조정] dock**
|
||||
= 높이·길이·기준측점 전/후 ③ **단 수(추가 기슭막이)와 옵션(연동·경사)** 은 둘 다 아니고
|
||||
**좌측 구조물 폼의 유입구/유출구 칸**(`.b05-structure__adjust-slot`)으로 옮겨 붙음.
|
||||
카드 조정창 안에도 그 행의 DOM 은 있지만 CSS 로 `display:none` 이라 눌리지 않음 —
|
||||
`innerText` 로만 보면 「있는데 왜 안 눌리지」로 헤매게 됨. **크기(rect)로 볼 것.**
|
||||
- **B06 조정창을 볼 때 — 두 번 걸린 함정임(3-4 · 3-7).** `.b06-structure-panel` 은 **카드마다
|
||||
하나씩** 있음(66~67개). 목록에서 `find` 로 아무거나 집으면 **다른 카드의 빈 패널**이 먼저 잡혀
|
||||
크기 0 · `is-hidden` 으로 나옴. 2026-09-07 에는 이것 때문에 두 창이 「십자 단추가 안 그려진다 =
|
||||
사용자도 벽을 못 옮긴다」로 잘못 결론냈다가 되물렀음(고른 벽의 카드에서 재니 조정창 124×185,
|
||||
이동 행 107×86 으로 멀쩡했음). **반드시 그 카드 안의 것**(`card.querySelector`)을 볼 것.
|
||||
또 **벽을 안 그리는 카드**를 고르면 굳히기 루프가 0회 도니
|
||||
`card.querySelector('.b06-chart__culvert-revet-hit')` 로 먼저 거를 것.
|
||||
- **화면 서버(vite)는 백엔드(`main.py`)의 자식임** (`main.py → npm → vite`, `.claude/dev_up.py:40`).
|
||||
백엔드를 트리째 끄면 **화면 서버도 같이 죽고** 열려 있던 탭이 옛 모듈을 붙들어 **백지**가 됨.
|
||||
재시작이 비싼 진짜 까닭은 서버가 아니라 이쪽임. 조작 전에 8000·5173(또는 8001·5174) **둘 다**
|
||||
200 인지 볼 것. (옛 「7. 개발 환경」에서 건져 온 것)
|
||||
- **로그인 세션은 DB 표**(`sessions`, `common_util/common_util_auth.py:115`)라 재시작으로 안 지워짐.
|
||||
⚠ **그래도 브라우저 쪽 세션은 끊김** — 2026-09-08 데스크탑 두 창이 같은 자리에서 막혔음.
|
||||
화면 검증 전에 `#/a06-login` 으로 튀는지 먼저 볼 것. (로그인하는 절차는 랩탑 창 확인 중)
|
||||
- ⚠ **화면이 막혀도 엔진은 파이썬에서 직접 부를 수 있음** — 라우터가 인계를 함수로
|
||||
부르므로(`from B08_Quantity… import get_handoff`) HTTP·로그인을 안 탐.
|
||||
⚠ 다만 **`await init_db_pool()` 을 먼저 부를 것** — 안 부르면 예외가 응답 본문으로 나가
|
||||
**빈 응답처럼 보이고 「자료가 없다」로 잘못 읽힘**(2026-09-08 실측). 그리고 **한 이벤트
|
||||
루프 안에서** 다 끝낼 것(따로 돌리면 `Event loop is closed` 가 쏟아짐).
|
||||
- ⚠ **되받기 전후로 인계 응답을 파일로 떠 둘 것** — 「합계가 221원 줄었다」를 짚을 때
|
||||
**어느 줄이 얼마나 바뀌었는지**를 대조할 자료가 없으면 짐작이 됨(2026-09-08).
|
||||
마침 화면 로그에 옛 수량이 남아 있어 짚었으나 그건 운이었음.
|
||||
⚠ 다만 「재시작하면 다시 로그인해야 했다」는 기록이 있으므로 **다른 까닭이 있을 수 있음** —
|
||||
실측은 안 해 봤음.
|
||||
- 🔴 **횡단 카드를 확대·이동하면 그 측점의 면적이 달라질 수 있음** (2026-09-07 3-9 뒷정리에서
|
||||
발견). 확대·이동은 그 측점의 **「표시 반폭」**(`display_half_width_m`)을 바꾸고(실측 12 → 27m),
|
||||
**사면이 반폭 안에서 지반을 못 만나는 측점**은 잘린 만큼 면적이 달라짐(성토 **57.45 → 62.79㎡**).
|
||||
세션 `crossw` 에 남으므로 **검증하러 확대하는 것만으로 수치가 흔들림.**
|
||||
· 설계상 그런 자리는 `slope_unclosed` 경고가 이미 뜨므로 **결함이라기보다 「반폭이 좁으면 면적이
|
||||
잘린다」는 성질**임. 다만 검증할 때는 **확대 전 반폭을 적어 두고 끝나면 되돌릴 것.**
|
||||
- **화면 요소가 보이는지 `offsetParent` 로 재지 말 것** — 고정 위치 요소는 늘 `null` 이라 늘
|
||||
안 보이는 것으로 나옴. `getBoundingClientRect()` 크기로 잴 것. (3-7)
|
||||
- **단계바 `.ui-workflow-layout__step` 은 0번이 대시보드** — 종단설계는 3번. (2-7)
|
||||
- **살아 있는 프로젝트에 검증용 `solve_route` 를 돌리지 말 것** — `route_main.geojson` 은 프로젝트당
|
||||
한 벌이라 노선 행이 늘어도 파일은 덮임(실제로 덮였고 되돌렸음). (0-10)
|
||||
- **이미 만든 프로젝트는 그대로임** — 노선이 폴리라인 기준이 되려면 새로 업로드해야 함. (0-10)
|
||||
- **곡선 생략(내각 155°) 규칙을 쓰지 않기로 해 곡선 개수가 늘었음**(용화 13 → 26곳) —
|
||||
평면 R 을 읽는 자리(3-1 확폭 · 2-6 구조물)는 그것을 감안할 것. (0-12)
|
||||
- **3-5 물량 파급을 되돌릴 자리** — `Cross_Culvert_Geom.ts` 의 `extendTrimSlope` 위 주석.
|
||||
사용자가 「그대로 둠」으로 확정했으므로 근거로만 남긴 것. (3-5)
|
||||
- **확폭이 수량에 실리는 것은 B08 재작업 때** — `carriageway_width_m`(= 3.0 + 확폭)을 쓰면 됨. (3-1)
|
||||
@@ -0,0 +1,109 @@
|
||||
# 보조 워크트리 만들기 (PC마다 1개)
|
||||
|
||||
작성 2026-08-31. 대상 = 각 PC에서 일하는 AI. 이 문서대로 하면 한 PC에서 창 두 개가
|
||||
서로 밟지 않고 동시에 일한다.
|
||||
|
||||
## 왜 이렇게 하나
|
||||
|
||||
- **동기화 밖에 둔다.** `C:\Program_coding` 은 Synology Drive 동기화 루트다. 워크트리를
|
||||
그 안에 두면 NAS 사본이 편집을 되돌린다(2026-08-31에 `.claude/dev_up.py` 가 두 번
|
||||
옛 사본으로 덮였다). 그래서 워크트리는 **동기화 루트 밖**에 만든다.
|
||||
- **포트를 나눈다.** 두 폴더가 같은 포트를 쓰면 나중에 뜬 쪽이 앞의 것을 죽인다.
|
||||
- **브랜치를 나눈다.** git 은 같은 브랜치를 두 워크트리가 동시에 체크아웃하지 못한다.
|
||||
|
||||
## 이름 규칙
|
||||
|
||||
| 대상 | 규칙 | 이 PC(노트북)의 실제 값 |
|
||||
|---|---|---|
|
||||
| 폴더 | 동기화 루트 밖, `C:\Aislo_wt\<이름>` | `C:\Aislo_wt\aislo-b0506` |
|
||||
| 메인 폴더 브랜치 | `main` 또는 `main_<pc>_<번호>` | `main` |
|
||||
| 보조 트리 브랜치 | `sub_<pc>_<번호>` | `sub_laptop_1` |
|
||||
|
||||
PC 이름을 브랜치에 박아 두면 원격 목록만 봐도 어느 기계 것인지 갈린다
|
||||
(현재 원격: `main`, `main_desktop_1`, `main_laptop_1`, `sub_laptop_1`).
|
||||
|
||||
## 만드는 절차
|
||||
|
||||
먼저 그 PC의 동기화 루트를 확인한다 — `.SynologyWorkingDirectory` 파일이 있는 폴더가
|
||||
루트다. 새 워크트리 경로가 그 아래로 들어가면 안 된다.
|
||||
|
||||
```powershell
|
||||
# 1) 워크트리 생성 (메인 폴더에서 실행)
|
||||
New-Item -ItemType Directory C:\Aislo_wt
|
||||
git -C "<메인 폴더>" worktree add C:\Aislo_wt\<이름> -b sub_<pc>_<번호> origin/main
|
||||
|
||||
# 2) 메인 폴더와 한 벌로 쓸 것들을 링크로 연결 (전부 gitignore 대상이라 안 딸려온다)
|
||||
$M = "<메인 폴더>"; $W = "C:\Aislo_wt\<이름>"
|
||||
New-Item -ItemType Junction $W\venv -Target $M\venv
|
||||
New-Item -ItemType Junction $W\.claude -Target $M\.claude
|
||||
New-Item -ItemType Junction $W\docs -Target $M\docs
|
||||
New-Item -ItemType Junction $W\storage -Target $M\storage
|
||||
New-Item -ItemType SymbolicLink $W\CLAUDE.md -Target $M\CLAUDE.md
|
||||
New-Item -ItemType SymbolicLink $W\AGENTS.md -Target $M\AGENTS.md
|
||||
|
||||
# 3) 프론트 의존성 (워크트리마다 따로 필요)
|
||||
cd $W\config; npm install
|
||||
```
|
||||
|
||||
`graphify-out` 링크가 있다면 옛 경로를 가리키므로 새로 건다 —
|
||||
대상은 `<메인 폴더>\docs\wiki\graphify-out\<날짜>`.
|
||||
|
||||
## 서버 띄우기 — 포트가 갈린다
|
||||
|
||||
| 폴더 | 백엔드 | vite |
|
||||
|---|---|---|
|
||||
| 메인 | 8000 | 5173 |
|
||||
| 보조 | 8001 | 5174 |
|
||||
|
||||
메인 폴더는 그대로다.
|
||||
|
||||
```powershell
|
||||
./venv/Scripts/python.exe .claude/dev_up.py
|
||||
```
|
||||
|
||||
보조 트리는 `dev_up.py` 가 8000·5173 고정이라 **환경변수를 주고 직접 띄운다**.
|
||||
|
||||
```powershell
|
||||
$env:SERVER_PORT='8001'; $env:FRONTEND_DEV_PORT='5174'; $env:AISLO_API_PORT='8001'
|
||||
Start-Process "$W\venv\Scripts\python.exe" -ArgumentList 'main.py' -WorkingDirectory $W `
|
||||
-RedirectStandardOutput "$W\tmp\server.log" -RedirectStandardError "$W\tmp\server.err.log"
|
||||
|
||||
$env:AISLO_VITE_PORT='5174'
|
||||
Start-Process "$W\venv\Scripts\python.exe" -ArgumentList '.claude\browser_driver.py' `
|
||||
-WorkingDirectory $W -RedirectStandardOutput "$W\tmp\browser_driver.out.log" `
|
||||
-RedirectStandardError "$W\tmp\browser_driver.err.log"
|
||||
```
|
||||
|
||||
각 변수가 하는 일 — `SERVER_PORT` 백엔드, `FRONTEND_DEV_PORT` vite, `AISLO_API_PORT`
|
||||
vite 프록시가 볼 백엔드(`config/vite.config.ts`), `AISLO_VITE_PORT` 브라우저 드라이버가
|
||||
열 주소. 넷을 한 벌로 맞춰야 프록시가 옆 포트로 새지 않는다.
|
||||
|
||||
확인: `5174/api/health` 가 200이면 프록시가 8001을 보고 있는 것이다.
|
||||
|
||||
**백엔드·DB 는 원래 한 벌을 공유해도 된다.** 백엔드를 고치는 작업일 때만 8001을 따로 띄운다.
|
||||
프론트만 만질 때는 vite 만 5174로 올리고 `AISLO_API_PORT=8000` 을 주면 된다.
|
||||
|
||||
## 지켜야 할 것
|
||||
|
||||
- **`git worktree remove` 를 쓰지 않는다.** Windows 긴 경로(`node_modules`)에서 실패하면
|
||||
공유 `.git` 과 `.claude` 까지 지운다(2026-08-30 실제 사고 2건). 폴더 정리가 필요하면
|
||||
사용자에게 요청한다. 옮기는 것은 `git worktree move` 로 안전하다 — 폴더 이동과 포인터
|
||||
두 개를 git 이 같이 고친다.
|
||||
- **워크트리를 지울 때는 링크부터 끊는다.** `.claude`·`docs`·`storage`·`venv` 는 메인
|
||||
폴더를 가리키는 링크라, 재귀 삭제가 링크를 따라가면 원본이 날아간다. `cmd /c rmdir <링크>`
|
||||
로 링크만 먼저 떼고 폴더를 지운다.
|
||||
- **커밋했으면 그 자리에서 push.** PC 이동이 곧 인수인계다. 로컬에만 있으면 다음 PC가 못 받는다.
|
||||
- **같은 브랜치를 두 트리가 못 쓴다.** 메인이 `main` 을 물고 있으면 보조는 `sub_*` 여야 한다.
|
||||
- **공유되는 것을 기억한다** — `venv` `.claude` `docs`(PLAN.md) `storage`(프로젝트 자료), DB.
|
||||
코드 파일만 갈릴 뿐 이것들은 한 벌이다. `PLAN.md` 는 자기 섹션만 고친다.
|
||||
- **`.claude/` 는 git 밖이고 동기화 안이다.** 두 PC의 AI가 같은 파일을 고치면 나중 동기화가
|
||||
앞의 것을 덮는다. 고치기 전에 사용자에게 알린다.
|
||||
|
||||
## 이 PC(노트북) 현재 상태
|
||||
|
||||
```
|
||||
C:\Program_coding\임도설계 및 견적자동화 프로그램 개발 [main] 8000 · 5173
|
||||
C:\Aislo_wt\aislo-b0506 [sub_laptop_1] 8001 · 5174
|
||||
```
|
||||
|
||||
원격에 `sub_laptop_1` 을 미리 만들어 두었다(`origin/sub_laptop_1`, main 과 같은 지점).
|
||||
Reference in New Issue
Block a user