Files
Aislo/.agent/backend.md
T
2026-07-08 20:28:24 +09:00

301 lines
17 KiB
Markdown

# 백엔드 제어 명세서 (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 수준