260706_1_계정, 로그인, 보안 초안 작성

This commit is contained in:
2026-07-07 19:17:13 +09:00
parent 404941c47a
commit f8633bb1fe
4696 changed files with 6110 additions and 642171 deletions
+78
View File
@@ -101,3 +101,81 @@
}
```
* **마이그레이션 시기:** 사용자 수 증가 또는 실시간성 요구 시 적용
---
## 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만 전송)
* **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)
### 6.4 로그인 이력 기록 (Audit Log)
* **기록 항목:**
- `user_id`, `email`, `login_timestamp`, `user_agent` (브라우저/OS), `status` (SUCCESS/FAILURE)
* **보관 기간:** 1년 (자동 삭제)
* **무차별 대입 방지:** 5회 이상 실패 시 계정 15분 잠금
* **IP 주소:** 법적 이슈로 수집하지 않음 (권장)
### 6.5 권한 체크 (Authorization)
* **마스터 권한:**
- 팀원 목록 조회, 승인/거부, 내보내기
- 회사 정보 수정, 구독 관리
- 마스터 대시보드 접근
* **팀원 권한:**
- 프로젝트 생성/수정/조회
- 워크플로우 실행 (B03~B09)
- 개인 프로필 수정
* **시스템관리자 권한:**
- 모든 회사/사용자 데이터 접근 (슈퍼유저)
- 회사/사용자 생성/수정/삭제
- 관리자 대시보드 (모니터링, Q&A 관리)
* **검증 방식:** 라우터 진입 전 JWT/토큰 검증, 필요시 데이터베이스에서 권한 확인
### 6.6 3개월 주기 재인증 (Periodic Re-authentication)
* **목적:** 재직 여부 확인 (퇴사자 자동 차단)
* **방식:** 3개월마다 이메일 OTP 재인증 강제
* **구현:** `users.last_email_verified_at` 컬럼으로 추적
* **예외:** 새로운 기기에서 로그인 시 즉시 재인증 요구
### 6.7 이메일 발송 (Email Infrastructure)
* **SMTP 설정:**
- **개발:** Google Gmail SMTP (umsangdon@gmail.com)
- **상용화:** Google Workspace 또는 AWS SES로 전환
* **발송 유형:**
- OTP 인증 코드
- 가입 확인 메일
- 팀원 가입 승인 요청 (마스터 수신)
- 팀원 승인/거부 결과
- 3개월 재인증 알림
* **구현:** `common_util/common_util_email.py` 모듈
- 비동기 발송 (asyncio.create_task)
- 재시도 로직 (최대 3회)
- 템플릿 렌더링 (common_util_email_templates.py)
* **자세한 내용:** `.agent/email_infrastructure_plan.md` 참고
+191
View File
@@ -0,0 +1,191 @@
# 배포 및 실행 가이드 (Deployment & Execution Guide)
## 🎯 목표
**한 명령어로 전체 웹앱 실행** — Python의 `main.py` 하나만 실행하면 백엔드 + 프론트엔드 모두 자동 구동.
---
## 🚀 실행 방법 (How to Run)
### 1단계: 환경 설정 (One-time Setup)
```bash
# 프로젝트 루트로 이동
cd d:\02_Software_Prog\임도설계\ \ 견적자동화\ 프로그램\ 개발
# Python 가상환경 활성화
.\venv\Scripts\activate
# Node.js 패키지 설치 (처음 한 번만)
npm install
# Python 패키지 설치 (이미 설치됨, 필요시)
pip install -r requirements.txt
# MariaDB 데이터베이스 초기화 (처음 한 번만)
# MariaDB CLI에서:
# > USE aislo_db;
# > SOURCE db_management/001_create_schema.sql;
# > SOURCE db_management/002_auth_security.sql;
```
### 2단계: 웹앱 실행 (Startup)
```bash
# 프로젝트 루트에서 단일 명령:
python main.py
# 출력 예시:
# INFO:__main__:[development] 앱 시작 중...
# INFO:__main__:[Frontend] npm run build 시작...
# ✓ built in 256ms
# INFO:__main__:✓ 프론트엔드 개발 서버 백그라운드 실행
# INFO:__main__:✓ DB 풀 초기화 완료
# INFO: Application startup complete.
# INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
```
### 3단계: 브라우저 접속
```
http://localhost:8000
```
---
## 📁 프로젝트 구조 (Project Layout)
### 루트 레벨 (Root-level — Build Configuration)
```
.
├── package.json ← 프론트엔드 패키지 (루트에만 있음)
├── vite.config.ts ← Vite 빌드 설정 (루트에만 있음)
├── tsconfig.json ← TypeScript 설정 (루트에만 있음)
├── node_modules/ ← npm 의존성
└── main.py ← FastAPI 진입점 (백엔드 + 프론트엔드 자동 실행)
```
### 프론트엔드 (Frontend)
```
A00_Common/ ← 페이지 진입점 (index.html)
├── main.ts
├── router.ts ← SPA 라우터
├── app_shell.ts ← 헤더/네비/푸터
└── ...
A01_Home/ ← 로그인 전 페이지
A02_ProgDetail/
A03_CompDetail/
...
B01_AccountDetail/ ← 로그인 후 페이지
B02_ProjRegister/
B03_FileInput/
...
ui_template/ ← 공유 컴포넌트 & 스타일
├── ui_template_elements.ts
├── ui_template_locale.ts
└── ui_template_theme.css
config/config_frontend.ts ← 프론트엔드 설정 (API 주소, 경로, 상수 등)
```
### 백엔드 (Backend)
```
main.py ← 앱 진입점
config/
├── config_system.py ← 환경 변수 로드
├── config_db.py ← DB 풀 설정
└── config_frontend.ts ← 프론트엔드 설정 (TypeScript)
common_util/
├── common_util_auth.py
├── common_util_auth_repository.py
├── common_util_email.py
└── ...
A06_Login/
├── A06_Login_Router.py ← 로그인 API
├── A06_Login_Schema.py
└── A06_Login_Api_Fetch.ts ← 프론트엔드 API 호출
A07_Register/
A09_Security/
B03_FileInput/
...
```
### 데이터베이스 (Database)
```
db_management/
├── 001_create_schema.sql ← 기본 스키마 (users, companies 등)
└── 002_auth_security.sql ← 인증 테이블 (sessions, email_otps 등)
```
---
## ✅ 주의사항 (Important Notes)
### 빌드 설정
| 파일 | 위치 | 설명 |
|------|------|------|
| `package.json` | **루트만** | npm 의존성 관리 |
| `vite.config.ts` | **루트만** | 프론트엔드 번들 설정 |
| `tsconfig.json` | **루트만** | TypeScript 컴파일 설정 |
| ❌ config/ 폴더 | **금지** | 빌드 설정 이중화 방지 |
### 실행 명령어
| 명령 | 위치 | 용도 |
|------|------|------|
| `python main.py` | 루트 | 웹앱 전체 실행 (추천) |
| `npm run build` | 루트 | 프론트엔드 빌드만 (dev용) |
| `npm run dev` | 루트 | Vite 개발 서버만 (필요시) |
### 코드 작성 시
- ❌ 루트의 `package.json`, `vite.config.ts` 두지 말 것
-`config/` 폴더에 `node_modules` 두지 말 것
- ✅ 프론트엔드 코드는 `A00_Common/`, `A0*/`, `B0*/` 에만 작성
- ✅ 백엔드 라우터는 모듈별 폴더에 `*_Router.py` 로 작성
---
## 🔧 문제 해결 (Troubleshooting)
### 문제 1: 포트 이미 사용 중
```bash
# 기존 프로세스 찾기
netstat -ano | findstr :8000
# 강제 종료 (Windows)
taskkill /PID <PID> /F
```
### 문제 2: npm 빌드 오류
```bash
# 의존성 재설치
rm -rf node_modules package-lock.json
npm install
npm run build
```
### 문제 3: DB 연결 오류
```bash
# .env 파일 확인
# DB_HOST, DB_USER, DB_PASSWORD 정확한지 확인
# MariaDB 상태 확인
mysql -u root -p -h localhost
```
---
## 📚 추가 읽기
- **로그인/보안**: `.agent/login_security_plan.md`
- **DB 스키마**: `.agent/db_schema_simple.md`
- **코더 핸드오프**: `.agent/CODER_HANDOFF.md`
- **프로젝트 규칙**: `CLAUDE.md` (프로젝트 루트)
---
**최종 정리**: `python main.py` → 브라우저 `http://localhost:8000` → 완료! 🎉
@@ -0,0 +1,210 @@
# 코드 개발자용 핸드오프 문서 (Coder Handoff)
## 📌 시작하기
다른 AI 개발자분, 안녕하세요!
아래 순서대로 문서를 읽고 진행하면 됩니다.
---
## 📚 읽어야 할 문서 (순서대로)
### 1단계: 환경 설정
**파일**: `.agent/SETUP_GUIDE.md`
**해야 할 일**:
- Google Gmail 앱 비밀번호 생성
- `.env` 파일 작성
- Python 라이브러리 설치
- MariaDB 데이터베이스 및 테이블 생성
- FastAPI 서버 실행 확인
- 이메일 발송 테스트
**소요 시간**: 30분
---
### 2단계: 전체 계획 이해
**파일**: `.agent/PLANS_INDEX.md`
**목적**:
- 전체 계획서 구조 파악
- 어느 파일을 언제 봐야 하는지 알기
**소요 시간**: 10분
---
### 3단계: 로그인/보안 요구사항
**파일**: `.agent/login_security_plan.md`
**핵심 내용**:
- ✅ 계정 유형: 마스터, 팀원, 시스템관리자
- ✅ 회원가입/로그인 흐름 (OTP 기반)
- ✅ 세션 관리: 12시간 기본, 4시간 활동감지
- ✅ DB 스키마 (7개 테이블)
- ✅ 무차별 대입 방지 (5회 실패 후 15분 잠금)
- ✅ 3개월 주기 재인증
**소요 시간**: 40분
---
### 4단계: 이메일 인프라
**파일**: `.agent/email_infrastructure_plan.md`
**핵심 내용**:
- ✅ Google Gmail SMTP (개발용)
- ✅ 비동기 이메일 발송 (asyncio)
- ✅ common_util_email.py 모듈 구현
- ✅ 이메일 템플릿 관리
- ✅ AWS SES 마이그레이션 계획
**소요 시간**: 30분
---
### 5단계: 세션 쿠키 구현
**파일**: `.agent/SESSION_COOKIE_SPEC.md`
**핵심 내용**:
-**최종 확정**: Secure HttpOnly 세션 쿠키
- ✅ 쿠키 설정 코드 (set_cookie, delete_cookie)
- ✅ 세션 검증 미들웨어
- ✅ 로그아웃 및 강제 로그아웃
- ✅ 테스트 방법
**소요 시간**: 40분
---
### 6단계: 백엔드 & 프론트엔드 명세 (필요시)
**파일**: `.agent/backend.md`, `.agent/structure.md`, `.agent/frontend.md`
**목적**: 코드 작성 시 참고
**소요 시간**: 필요시
---
## 🎯 개발 순서 (Phase 1)
```
총 소요 시간: ~1-2주 (풀타임)
✅ Step 1: 환경 설정 (SETUP_GUIDE.md)
└─ .env, 데이터베이스, 라이브러리 설치
✅ Step 2: 이메일 모듈 (email_infrastructure_plan.md)
├─ common_util_email.py 구현
├─ common_util_email_templates.py 구현
└─ 이메일 발송 테스트
✅ Step 3: 세션/쿠키 인프라 (SESSION_COOKIE_SPEC.md)
├─ sessions 테이블 생성
├─ verify_session 미들웨어 구현
└─ 로그인/로그아웃 API
✅ Step 4: 회원가입 API (login_security_plan.md §3.1)
├─ A07_Register_Router.py
├─ A07_Register_Schema.py
├─ 이메일 OTP 발송
└─ 회사 정보 입력
✅ Step 5: OTP 인증 API (login_security_plan.md §3.2)
├─ OTP 검증 엔드포인트
└─ 사용자 상태 업데이트
✅ Step 6: 로그인 API (login_security_plan.md §3.1)
├─ A06_Login_Router.py
├─ 이메일 + 비밀번호 검증
├─ OTP 재발송
└─ 세션 쿠키 생성
✅ Step 7: 가입 승인 API (login_security_plan.md §3.2)
├─ 팀원 가입 신청
├─ 마스터 이메일 알림
├─ 마스터 승인/거부
└─ 팀원 최종 가입
✅ Step 8: 프론트엔드 연동
├─ A06_Login UI + API
├─ A07_Register UI + API
├─ A09_Security UI (약관 동의)
└─ 테스트 및 검증
```
---
## 🚨 주의사항
### 보안 관련
- ❌ 비밀번호를 평문으로 저장하지 마세요 → bcrypt 사용
- ❌ OTP를 평문으로 저장하지 마세요 → bcrypt 해시
- ✅ .env 파일은 .gitignore에 추가하세요
- ✅ HTTPS 환경에서만 Secure 쿠키 사용 (개발: localhost 예외)
### DB 관련
- ❌ Raw SQL에서 f-string 사용 금지 → `%s` 플레이스홀더만
- ✅ aiomysql 비동기 사용 필수
- ✅ 트랜잭션 처리 필수 (여러 테이블 수정 시)
### 이메일 관련
- ✅ 이메일 발송은 백그라운드 작업으로 (asyncio.create_task)
- ✅ 재시도 로직 필수 (최대 3회)
- ✅ 로깅 필수 (성공/실패 기록)
---
## 💬 질문이 있을 때
### 제시된 문서에서 찾기
1. **"세션은 어떻게 구현하나?"** → `SESSION_COOKIE_SPEC.md` 참고
2. **"OTP는 어디에 저장하나?"** → `login_security_plan.md` §4.1, §11.2 참고
3. **"DB 스키마는?"** → `login_security_plan.md` §11 참고
4. **"폴더 구조는?"** → `structure.md` 참고
5. **"백엔드 API 규칙은?"** → `backend.md` 참고
### 코드 예시 필요할 때
- 이메일 발송 → `email_infrastructure_plan.md` §6
- 세션 검증 → `SESSION_COOKIE_SPEC.md` §2-3
- 로그인 흐름 → `login_security_plan.md` §3
- 라우터 구조 → `backend.md` §1
---
## 📦 최종 확인 체크리스트
### Phase 1 완료 기준
- [ ] SETUP_GUIDE.md 의 모든 단계 완료
- [ ] `.env` 파일 작성 (umsangdon@gmail.com 설정)
- [ ] 데이터베이스 및 모든 테이블 생성
- [ ] common_util_email.py 구현 및 테스트
- [ ] A07_Register_Router.py 구현 (회사 정보 입력)
- [ ] A06_Login_Router.py 구현 (로그인 + OTP)
- [ ] 팀원 가입 승인 흐름 구현
- [ ] 세션 검증 미들웨어 구현
- [ ] 로그아웃 엔드포인트 구현
- [ ] 프론트엔드 (A06, A07, A09) 기본 구현
- [ ] 통합 테스트 (회원가입 → 로그인 → 조회)
### Phase 2 대기 항목 (나중에)
- 3개월 주기 재인증
- 마스터 대시보드 (팀원 관리)
- activity_logs 기록
- AWS SES 마이그레이션
---
## 📞 연락 사항
**문서 담당자**: 아이슬로 AI 설계팀
**최종 업데이트**: 2026-07-07
**버전**: 1.0
---
**모든 준비가 되었습니다. 행운을 빕니다! 🚀**
@@ -0,0 +1,781 @@
# 이메일 인프라 구현 계획서 (v1.0)
## 1. 개요
### 1.1 목표
- B2B SaaS 환경에서 **자동 이메일 발송 시스템** 구축
- Google Workspace (개인용 Gmail 대체) 기반 SMTP 연동
- 비동기 태스크 기반 논블로킹 발송
- Phase 1: Google SMTP → Phase 2: AWS SES 마이그레이션 준비
### 1.2 현재 상황
- **개발 단계**: 개인 Gmail 계정 (umsangdon@gmail.com) 사용
- **상용화 단계**: Google Workspace Business 또는 AWS SES로 전환 예정
- **타겟**: 회원가입, 로그인, 팀원 승인/거부 등 자동 메일
---
## 2. 이메일 발송 유형 및 우선순위
### 2.1 필수 (🔴) - Phase 1에서 구현
| 메일 유형 | 수신자 | 발송 타이밍 | 용도 |
|----------|--------|-----------|------|
| **OTP 인증** | 가입자/로그인자 | 가입/로그인 직후 | 이메일 소유권 확인 |
| **가입 확인** | 신규 가입자 | 가입 완료 직후 | 가입 성공 안내 |
| **팀원 승인 요청** | 마스터 | 팀원 가입 신청 시 | 승인/거부 요청 |
| **팀원 승인/거부 결과** | 팀원 | 마스터 승인/거부 직후 | 결과 안내 |
### 2.2 중요 (🟠) - Phase 2에서 구현
| 메일 유형 | 수신자 | 발송 타이밍 | 용도 |
|----------|--------|-----------|------|
| **3개월 재인증 알림** | 모든 사용자 | 주기적 (크론) | 보안 점검 |
| **비밀번호 변경 알림** | 해당 사용자 | 변경 직후 | 보안 알림 |
| **Q&A 답변 회신** | 문의자 | 관리자 처리 후 | 고객 지원 |
### 2.3 선택사항 (🟡) - Phase 3 이후
| 메일 유형 | 수신자 | 발송 타이밍 | 용도 |
|----------|--------|-----------|------|
| **세션 만료 알림** | 활동 중인 사용자 | 만료 5분 전 | UX 개선 |
| **주간 활동 리포트** | 마스터 | 매주 월요일 | 분석 정보 |
| **결제/구독 안내** | 마스터 | 결제 시기 | 구독 관리 |
---
## 3. 아키텍처 설계
### 3.1 전체 흐름도
```
┌─────────────────────────────────────────────────────┐
│ FastAPI 웹 서버 (Uvicorn) │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ API 라우터 │ │
│ │ - POST /api/auth/register │ │
│ │ - POST /api/auth/login │ │
│ │ - POST /api/auth/join-request │ │
│ │ - POST /api/admin/approve-join │ │
│ └──────────────────────────────────────────────┘ │
│ │ │
│ ↓ (즉시 응답) │
│ ┌──────────────────────────────────────────────┐ │
│ │ 백그라운드 비동기 작업 │ │
│ │ (asyncio.create_task) │ │
│ │ │ │
│ │ ├─ OTP 생성 + 암호화 저장 │ │
│ │ ├─ 이메일 템플릿 렌더링 │ │
│ │ └─ SMTP 발송 시작 │ │
│ └──────────────────────────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 이메일 발송 모듈 │ │
│ │ (common_util/common_util_email.py) │ │
│ │ │ │
│ │ ├─ SMTP 클라이언트 (Google) │ │
│ │ ├─ 재시도 로직 (최대 3회) │ │
│ │ ├─ 로깅 및 모니터링 │ │
│ │ └─ 발송 이력 DB 저장 │ │
│ └──────────────────────────────────────────────┘ │
│ │ │
└────────────────────┼────────────────────────────────┘
┌──────────────────────────────┐
│ Google Gmail SMTP │
│ (smtp.gmail.com:587) │
│ + TLS 암호화 │
└──────────────────────────────┘
┌──────────────────────────────┐
│ 수신자 이메일 서버 │
│ (Gmail, Naver, etc) │
└──────────────────────────────┘
```
### 3.2 개발 단계 상세 흐름
```
1단계: 사용자가 회원가입 버튼 클릭
2단계: FastAPI 라우터 수신
- 이메일/비밀번호 검증 (Pydantic)
- 중복 확인
- DB에 임시 사용자 생성 (status = PENDING)
3단계: 백그라운드 비동기 작업 시작 (즉시 응답)
- OTP 코드 생성 (6자리 난수)
- OTP 해시값 DB 저장 + 만료시간 기록 (5분)
- 이메일 HTML 템플릿 렌더링
4단계: 이메일 발송 (재시도 로직 포함)
- 시도 1: SMTP 연결 및 발송
- 실패 시 2초 대기 후 재시도
- 3회 모두 실패 → 로그 기록 + 관리자 알림
5단계: 클라이언트는 즉시 응답 받음
- "인증 코드를 이메일로 발송했습니다"
- 사용자는 이메일 확인하여 OTP 입력
```
---
## 4. 개발 단계 설정 (Phase 1)
### 4.1 Google Gmail SMTP 설정
#### 단계 1: 앱 비밀번호 생성
```
Google 계정 (umsangdon@gmail.com) 접속
Google 계정 관리 페이지 (myaccount.google.com)
왼쪽 메뉴: "보안" (Security)
2단계 인증 활성화 (이미 되어있으면 스킵)
앱 비밀번호 (App passwords)
├─ 앱: "메일(Mail)"
├─ 기기: "Windows"
└─ [생성] 버튼
16자리 비밀번호 생성 (예: abcd efgh ijkl mnop)
.env 파일에 저장
```
#### 단계 2: .env 파일 작성
```bash
# config/.env (개발용, .gitignore에 포함)
# ===== Email Infrastructure (Phase 1) =====
SMTP_PROVIDER=google
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=umsangdon@gmail.com
SMTP_PASSWORD=abcd efgh ijkl mnop # 위에서 생성한 앱 비밀번호
SMTP_USE_TLS=true
# 발송 이메일 주소
EMAIL_FROM_ADDRESS=umsangdon@gmail.com
EMAIL_FROM_NAME=Aislo Support
EMAIL_NOREPLY_ADDRESS=umsangdon+noreply@gmail.com # 답장 불가 계정
# 이메일 발송 설정
EMAIL_RETRY_COUNT=3
EMAIL_RETRY_DELAY_SECONDS=2
EMAIL_OTP_VALID_MINUTES=5
EMAIL_NOTIFICATION_ENABLED=true
# 관리자 이메일 (에러 발생 시 수신)
ADMIN_EMAIL=umsangdon@gmail.com
```
#### 단계 3: config_system.py 수정
```python
# config/config_system.py
import os
from dotenv import load_dotenv
load_dotenv()
# ===== Email Configuration =====
SMTP_PROVIDER = os.getenv("SMTP_PROVIDER", "google")
SMTP_HOST = os.getenv("SMTP_HOST", "smtp.gmail.com")
SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
SMTP_USERNAME = os.getenv("SMTP_USERNAME", "umsangdon@gmail.com")
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "")
SMTP_USE_TLS = os.getenv("SMTP_USE_TLS", "true").lower() == "true"
EMAIL_FROM_ADDRESS = os.getenv("EMAIL_FROM_ADDRESS", "umsangdon@gmail.com")
EMAIL_FROM_NAME = os.getenv("EMAIL_FROM_NAME", "Aislo Support")
EMAIL_NOREPLY_ADDRESS = os.getenv("EMAIL_NOREPLY_ADDRESS", "umsangdon+noreply@gmail.com")
EMAIL_RETRY_COUNT = int(os.getenv("EMAIL_RETRY_COUNT", "3"))
EMAIL_RETRY_DELAY_SECONDS = int(os.getenv("EMAIL_RETRY_DELAY_SECONDS", "2"))
EMAIL_OTP_VALID_MINUTES = int(os.getenv("EMAIL_OTP_VALID_MINUTES", "5"))
EMAIL_NOTIFICATION_ENABLED = os.getenv("EMAIL_NOTIFICATION_ENABLED", "true").lower() == "true"
ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "umsangdon@gmail.com")
```
---
## 5. 이메일 발송 모듈 (common_util_email.py)
### 5.1 기본 구조
```python
# common_util/common_util_email.py
import smtplib
import asyncio
import logging
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from datetime import datetime
from config.config_system import (
SMTP_HOST,
SMTP_PORT,
SMTP_USERNAME,
SMTP_PASSWORD,
SMTP_USE_TLS,
EMAIL_FROM_ADDRESS,
EMAIL_FROM_NAME,
EMAIL_RETRY_COUNT,
EMAIL_RETRY_DELAY_SECONDS,
)
logger = logging.getLogger(__name__)
class EmailService:
"""Google SMTP를 통한 비동기 이메일 발송"""
@staticmethod
async def send_email(
to_email: str,
subject: str,
body_html: str,
body_text: str = None,
retry_count: int = None
) -> bool:
"""
비동기 이메일 발송 (재시도 로직 포함)
Args:
to_email: 수신자 이메일
subject: 제목
body_html: HTML 본문
body_text: 텍스트 본문 (선택)
retry_count: 재시도 횟수 (기본값: 환경변수)
Returns:
발송 성공 여부 (True/False)
"""
if retry_count is None:
retry_count = EMAIL_RETRY_COUNT
for attempt in range(retry_count):
try:
# 메시지 구성
msg = MIMEMultipart("alternative")
msg["From"] = f"{EMAIL_FROM_NAME} <{EMAIL_FROM_ADDRESS}>"
msg["To"] = to_email
msg["Subject"] = subject
msg["Date"] = datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
# 텍스트 본문 추가 (메일 클라이언트가 HTML 미지원 시)
if body_text:
msg.attach(MIMEText(body_text, "plain", "utf-8"))
# HTML 본문 추가
msg.attach(MIMEText(body_html, "html", "utf-8"))
# 별도 스레드에서 SMTP 발송 (논블로킹)
await asyncio.to_thread(
EmailService._send_smtp_blocking,
msg,
to_email
)
logger.info(f"✅ 이메일 발송 성공: {to_email} (제목: {subject})")
return True
except Exception as e:
logger.warning(
f"⚠️ 이메일 발송 실패 (시도 {attempt+1}/{retry_count}): "
f"{to_email} - {str(e)}"
)
# 마지막 시도가 아니면 대기 후 재시도 (지수 백오프)
if attempt < retry_count - 1:
delay = EMAIL_RETRY_DELAY_SECONDS * (2 ** attempt)
await asyncio.sleep(delay)
continue
logger.error(f"❌ 이메일 발송 최종 실패: {to_email} (제목: {subject})")
return False
@staticmethod
def _send_smtp_blocking(msg, to_email):
"""
동기 SMTP 발송 (asyncio.to_thread에서 실행)
Args:
msg: MIMEMultipart 메시지 객체
to_email: 수신자 이메일
"""
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
if SMTP_USE_TLS:
server.starttls()
server.login(SMTP_USERNAME, SMTP_PASSWORD)
server.send_message(msg)
```
### 5.2 이메일 템플릿 모듈 (common_util_email_templates.py)
```python
# common_util/common_util_email_templates.py
def get_otp_email_html(otp_code: str, valid_minutes: int = 5) -> tuple[str, str]:
"""
OTP 인증 이메일 반환 (HTML + Text)
Returns:
(html_body, text_body)
"""
html_body = f"""
<html>
<head>
<meta charset="utf-8">
<style>
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #333;
background-color: #f5f5f5;
}}
.container {{
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
.header {{
background: linear-gradient(135deg, #2d5016 0%, #4a7c2c 100%);
color: white;
padding: 30px;
text-align: center;
border-radius: 8px 8px 0 0;
}}
.header h1 {{
margin: 0;
font-size: 28px;
}}
.content {{
padding: 30px;
}}
.otp-box {{
background: #f9f9f9;
border: 2px solid #2d5016;
border-radius: 8px;
padding: 20px;
text-align: center;
margin: 20px 0;
}}
.otp-code {{
font-size: 42px;
font-weight: bold;
color: #2d5016;
letter-spacing: 8px;
font-family: 'Courier New', monospace;
}}
.warning {{
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 12px;
margin: 15px 0;
border-radius: 4px;
font-size: 14px;
}}
.footer {{
font-size: 12px;
color: #999;
text-align: center;
margin-top: 30px;
border-top: 1px solid #eee;
padding-top: 15px;
}}
.link {{
color: #2d5016;
text-decoration: none;
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🌲 Aislo</h1>
<p style="margin: 10px 0 0 0;">이메일 인증</p>
</div>
<div class="content">
<p>안녕하세요!</p>
<p>아래 인증 코드를 입력하여 계정을 확인해주세요:</p>
<div class="otp-box">
<div class="otp-code">{otp_code}</div>
</div>
<p style="text-align: center; color: #666;">
<strong>이 코드는 {valid_minutes}분간 유효합니다.</strong>
</p>
<div class="warning">
<strong>⚠️ 주의:</strong> 본인이 요청하지 않았다면 이 이메일을 무시해주세요.
</div>
<p style="font-size: 14px; color: #666;">
Aislo는 절대로 귀하의 인증 코드를 요청하지 않습니다.
</p>
</div>
<div class="footer">
<p>&copy; 2026 Aislo. All rights reserved.</p>
<p><a href="https://aislo.company" class="link">aislo.company</a></p>
</div>
</div>
</body>
</html>
"""
text_body = f"""
Aislo - 이메일 인증
안녕하세요!
아래 인증 코드를 입력하여 계정을 확인해주세요:
{otp_code}
이 코드는 {valid_minutes}분간 유효합니다.
주의: 본인이 요청하지 않았다면 이 이메일을 무시해주세요.
---
Aislo Support
https://aislo.company
"""
return html_body, text_body
def get_join_approval_request_email_html(
user_email: str,
user_name: str,
company_name: str,
approval_url: str
) -> tuple[str, str]:
"""
팀원 가입 승인 요청 이메일 (마스터 수신)
"""
html_body = f"""
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: 'Segoe UI', sans-serif; color: #333; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background: #2d5016; color: white; padding: 20px; border-radius: 8px 8px 0 0; text-align: center; }}
.content {{ background: white; padding: 20px; border: 1px solid #ddd; border-radius: 0 0 8px 8px; }}
.info-box {{ background: #f0f8f0; border-left: 4px solid #2d5016; padding: 15px; margin: 20px 0; }}
.button {{ background: #2d5016; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block; margin: 15px 5px 15px 0; }}
.footer {{ font-size: 12px; color: #999; margin-top: 20px; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>새로운 팀원 가입 신청</h2>
</div>
<div class="content">
<p>안녕하세요 관리자님,</p>
<p><strong>{user_email}</strong>이 <strong>{company_name}</strong>에 가입을 신청했습니다.</p>
<div class="info-box">
<strong>신청자 정보:</strong><br>
이메일: {user_email}<br>
이름: {user_name}<br>
회사: {company_name}
</div>
<p>아래 버튼을 클릭하여 승인 또는 거부해주세요:</p>
<a href="{approval_url}" class="button">승인/거부 하기</a>
<p style="font-size: 12px; color: #999;">
버튼이 작동하지 않으면 아래 링크를 직접 열어주세요:<br>
{approval_url}
</p>
</div>
<div class="footer">
<p>&copy; 2026 Aislo</p>
</div>
</div>
</body>
</html>
"""
text_body = f"""
새로운 팀원 가입 신청
안녕하세요 관리자님,
{user_email}{company_name}에 가입을 신청했습니다.
신청자 정보:
- 이메일: {user_email}
- 이름: {user_name}
- 회사: {company_name}
아래 링크에서 승인 또는 거부해주세요:
{approval_url}
---
Aislo Support
"""
return html_body, text_body
```
---
## 6. 라우터 통합 예시
### 6.1 회원가입 라우터 (A07_Register_Router.py)
```python
# A07_Register/A07_Register_Router.py
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, EmailStr
from common_util.common_util_email import EmailService
from common_util.common_util_email_templates import get_otp_email_html
import asyncio
import secrets
router = APIRouter()
class RegisterRequest(BaseModel):
email: EmailStr
password: str
full_name: str
@router.post("/api/auth/register")
async def register(req: RegisterRequest):
"""회원가입 엔드포인트"""
# 1. 검증: 이미 가입된 사용자인지 확인
existing_user = await db.get_user_by_email(req.email)
if existing_user:
raise HTTPException(status_code=400, detail="이미 가입된 이메일입니다.")
# 2. OTP 코드 생성 (6자리 난수)
otp_code = str(secrets.randbelow(1000000)).zfill(6)
# 3. DB에 임시 사용자 생성 (status='PENDING', otp_hash 저장)
otp_hash = hash_password(otp_code) # bcrypt 사용
user_id = await db.create_user(
email=req.email,
password_hash=hash_password(req.password),
full_name=req.full_name,
status="PENDING",
otp_hash=otp_hash,
otp_expires_at=datetime.utcnow() + timedelta(minutes=5)
)
# 4. 백그라운드 태스크로 이메일 발송 (논블로킹)
html_body, text_body = get_otp_email_html(otp_code)
asyncio.create_task(
EmailService.send_email(
to_email=req.email,
subject="[Aislo] 이메일 인증 코드",
body_html=html_body,
body_text=text_body
)
)
# 5. 즉시 응답 (이메일 발송은 백그라운드에서 계속)
return {
"status": "success",
"message": "인증 코드를 이메일로 발송했습니다. 5분 이내에 입력해주세요.",
"user_id": user_id
}
@router.post("/api/auth/verify-otp")
async def verify_otp(user_id: int, otp_code: str):
"""OTP 검증"""
user = await db.get_user_by_id(user_id)
# OTP 만료 확인
if datetime.utcnow() > user.otp_expires_at:
raise HTTPException(status_code=400, detail="인증 코드가 만료되었습니다.")
# OTP 일치 확인 (hash 비교)
if not verify_password(otp_code, user.otp_hash):
raise HTTPException(status_code=400, detail="인증 코드가 일치하지 않습니다.")
# 사용자 상태 업데이트
await db.update_user(user_id, {
"status": "ACTIVE",
"otp_hash": None,
"otp_expires_at": None
})
return {
"status": "success",
"message": "이메일 인증 완료되었습니다."
}
```
---
## 7. 모니터링 및 로깅
### 7.1 이메일 발송 이력 저장 (선택)
향후 추가할 선택사항:
```python
# DB 테이블
CREATE TABLE email_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
to_email VARCHAR(255),
subject VARCHAR(255),
email_type ENUM('OTP', 'JOIN_APPROVAL', 'RESULT', 'OTHER'),
status ENUM('SUCCESS', 'FAILED'),
sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
retry_count INT DEFAULT 0,
error_message TEXT,
FOREIGN KEY (user_id) REFERENCES users(id),
INDEX (sent_at)
);
```
### 7.2 로깅 설정 (logging)
```python
# config/config_system.py에 추가
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('logs/email.log'),
logging.StreamHandler()
]
)
```
---
## 8. Phase 2 마이그레이션 계획 (AWS SES)
### 8.1 AWS SES로 전환 시 변경 사항
```python
# 미래: AWS SES로 변경 (최소 코드 수정)
SMTP_PROVIDER = "aws_ses" # config에서만 변경
SMTP_HOST = "email-smtp.ap-northeast-2.amazonaws.com"
SMTP_USERNAME = "AKIA..." # AWS Access Key
SMTP_PASSWORD = "..." # AWS Secret Key
```
**장점**:
- 높은 배송 성공률 (99%)
- 빠른 발송 속도
- 대량 발송 지원
- 스팸 필터 회피 능력
---
## 9. 구현 체크리스트 (Phase 1)
### 필수 항목
- [ ] .env 파일 작성 (Google 앱 비밀번호)
- [ ] config_system.py 이메일 설정 추가
- [ ] common_util_email.py 구현
- [ ] common_util_email_templates.py 구현
- [ ] A07_Register_Router.py 수정 (이메일 발송 통합)
- [ ] 테스트: 실제 Gmail 발송 확인
- [ ] logging 설정 추가
- [ ] .gitignore에 .env 추가
### 선택 항목 (Phase 2)
- [ ] email_logs DB 테이블 생성
- [ ] 발송 이력 저장 로직
- [ ] AWS SES 마이그레이션 준비
- [ ] 이메일 템플릿 관리 시스템
---
## 10. 테스트 방법
### 10.1 개발 단계 테스트
```bash
# 1. .env 파일 작성 확인
cat config/.env | grep SMTP
# 2. Python 테스트 스크립트 실행
python -m pytest tests/test_email_service.py
# 3. Gmail 계정에서 발송 이력 확인
# Gmail 웹 → 설정 → 계정 및 가져오기 → 다른 이메일 주소에서 메일 보내기
```
### 10.2 이메일 도착 확인
```
발송된 이메일:
- 받는사람: [테스트 이메일]
- 제목: "[Aislo] 이메일 인증 코드"
- 본문: HTML 템플릿 포함, 6자리 OTP 코드
- 발신: "Aislo Support <umsangdon@gmail.com>"
```
---
## 11. 보안 주의사항
### 11.1 .env 파일 보안
```bash
# .gitignore에 추가 (필수)
config/.env
config/.env.local
.env.*
# Git에서 실수로 푸시된 경우
git rm --cached config/.env
git commit -m "Remove .env from git tracking"
```
### 11.2 Gmail 앱 비밀번호 회전
- **매년 1회**: 앱 비밀번호 재생성 권장
- **의심 활동**: 즉시 재생성
- **개발 → 상용화**: 새로운 비밀번호로 변경
### 11.3 SMTP 연결 암호화
- ✅ TLS 사용 (포트 587)
- ❌ SSL 포트 465는 피할 것 (deprecated)
---
## 12. 다음 단계
1. ✅ 이 계획서 검토 및 피드백
2. 📝 .env 파일 작성 + Google 앱 비밀번호 생성
3. 💻 common_util_email.py 코드 작성
4. 🧪 실제 Gmail 발송 테스트
5. 📋 backend.md 업데이트 (이메일 API 섹션 추가)
---
**작성일**: 2026-07-07
**담당자**: 시스템 설계 팀
**상태**: 초안 (v1.0)
@@ -0,0 +1,623 @@
# 로그인 & 보안 시스템 구현 계획서 (v1.0)
## 1. 프로젝트 개요
### 1.1 목표
- B2B SaaS 환경에서 **마스터-사용자** 기반 조직 관리
- **이메일 인증** 기반 강화된 보안
- **시스템관리자** 전체 감시/관리 권한
- 개인정보보호법 준수
### 1.2 적용 대상
- 30인 미만 소규모 기업 (공용 이메일 허용)
- 개인별 계정 운영 (마스터 승인제)
- 한국 개인정보보호법 준수 필수
---
## 2. 계정 유형 및 역할 (3가지)
### 2.1 마스터 (Master / Company Admin)
**정의**: 회사 최초 가입자 (회사당 1명 권장, 복수 가능)
**권한**:
- 회사 정보 입력/수정
- 초대코드 발급 및 관리
- 신규 팀원 가입 승인/거부
- 팀원 목록 조회
- 팀원 강제 제거 (내보내기)
- 회사 구독/결제 관리
- 회사 대시보드 (기본 통계)
**DB 구분**: `users.is_master = true`
---
### 2.2 팀원 (Member / Regular User)
**정의**: 마스터가 승인한 회사 소속 사용자
**권한**:
- 서비스 기능 일반 이용 (B04~B09 워크플로우)
- 개인 프로필 수정
- 프로젝트 생성/수정/조회
**DB 구분**: `users.is_master = false`
**상태 관리**:
- `PENDING` (승인 대기중)
- `ACTIVE` (승인됨, 이용 가능)
- `INACTIVE` (마스터가 내보냄, 접근 불가)
- `REJECTED` (마스터가 거부함)
---
### 2.3 시스템관리자 (System Administrator)
**정의**: 개발사 (Aislo) 측 운영 담당자 = **당신**
**권한**:
- 모든 회사 데이터 조회 (슈퍼유저)
- 회사/사용자 계정 생성/삭제/수정
- 회사별 구독 상태 관리
- 전체 시스템 Q&A/요청사항 처리 (회사 메일로 수신 → 검토/수정/삭제)
- 관리자 대시보드
- 활성 사용자 수
- 하드웨어 리소스 모니터링 (CPU, 메모리, 디스크)
- 소프트웨어 사용 현황 (업로드/다운로드/API 호출 통계)
- 에러/경고 로그 모니터링
- 회사별 파일 저장소 사용량
- 마스터 계정 대체 관리 (마스터 부재 시)
**DB 구분**: `users.role = 'SYSTEM_ADMIN'` (별도 테이블로 관리)
**접근**: 별도 관리 포털 (프론트엔드에서 숨김)
---
## 3. 회원가입 및 조직 연결 흐름
### 3.1 마스터 가입 흐름
```
1단계: 회원가입 시작
└─ 이메일 입력 (예: founder@gmail.com)
└─ 비밀번호 입력
2단계: 이메일 인증
└─ 시스템이 해당 이메일로 6자리 OTP 코드 발송
└─ 사용자가 OTP 입력하여 인증
3단계: 회사 정보 입력
└─ 회사명
└─ 사업자번호 (선택)
└─ 회사 전화번호
└─ 주소
└─ 대표자명
4단계: 가입 완료
└─ DB에 Company 레코드 생성
└─ 고유 초대코드 자동 생성 (예: ABC-789-XYZ)
└─ users.is_master = true로 설정
└─ 마스터 대시보드로 이동
```
---
### 3.2 팀원 가입 흐름 (요청 기반)
```
1단계: 팀원이 가입 신청
└─ 이메일 입력 (예: member@gmail.com)
└─ 비밀번호 입력
2단계: 이메일 인증
└─ OTP 입력하여 인증
3단계: 회사 정보 입력 (선택지)
├─ [새 회사 등록하기] → 마스터 역할 시작
└─ [기존 회사에 참여하기] → 4단계로 진행
4단계: 회사 선택/검색
└─ "우리 회사가 이미 가입되어 있나요?"
└─ 회사명으로 검색
└─ 검색 결과에서 해당 회사 선택
5단계: 승인 요청
└─ [가입 신청 보내기] 버튼 클릭
└─ 백엔드: JoinRequest 레코드 생성
└─ 백엔드: 마스터의 등록된 이메일로 승인 메일 발송
6단계: 마스터의 승인 메일 확인
└─ 마스터가 받은 승인 메일:
"홍길동(member@gmail.com)님이 우리 회사에 가입을 신청했습니다.
[승인] [거부] 버튼"
└─ 마스터가 [승인] 클릭
└─ 팀원 users.status = 'ACTIVE'로 변경
└─ 팀원에게 "승인되었습니다" 메일 발송
7단계: 팀원 로그인 가능
└─ 팀원이 이제 서비스에 로그인 가능
```
**💡 핵심 차이점**
- ❌ 초대코드 쪽지 방식 아님
- ✅ 사용자가 스스로 요청 → 마스터가 메일로 승인
---
## 4. 이메일 인증 (MFA) 정책
### 4.1 로그인 프로세스
```
1단계: 아이디(이메일)/비밀번호 입력
2단계: 백엔드 검증 (계정 존재, 비밀번호 일치, 상태 = ACTIVE)
3단계: OTP 발송 및 입력
└─ 등록된 이메일로 6자리 OTP 코드 발송
└─ 사용자 입력 (유효시간: 5분)
4단계: 로그인 완료
└─ JWT/세션 토큰 발급
```
### 4.2 이메일 인증 주기
| 상황 | 인증 여부 |
|------|---------|
| **최초 가입 시** | ✅ 필수 |
| **로그인할 때마다** | ❌ 아니오 (UX 개선) |
| **3개월 주기 재인증** | ✅ 필수 |
| **새로운 브라우저/기기** | ✅ 필수 |
| **비밀번호 변경 후** | ✅ 필수 |
| **관리자가 강제 로그아웃** | ✅ 필수 (재로그인 시) |
**구현 방식**:
- DB: `users.last_email_verified_at` 컬럼 추가
- 로그인 시점에 "마지막 인증이 3개월 이상 지났는가?" 체크
- 맞으면 이메일 인증 강제
---
## 5. 세션 관리 정책
### 5.1 세션 유지 시간
| 시나리오 | 유지 시간 | 이유 |
|---------|---------|------|
| **기본 세션 유지** | 12시간 | 대용량 파일 업로드 고려 (최대 20GB) |
| **활동 중인 경우** | 연장 | 파일 전송/처리 중일 때는 타임아웃 제외 (마지막 요청으로부터 4시간) |
| **로그인 유지 옵션** | 최대 30일 | (선택사항) 차후 구현 검토 |
### 5.2 활동 감지 로직
```
활동으로 간주하는 API 엔드포인트:
- 파일 업로드/다운로드
- 프로젝트 저장
- 데이터 처리 요청
- 대시보드/페이지 조회
세션 연장 방식:
- 각 API 호출 시 last_activity_at 컬럼 업데이트
- 만료 판정:
if (current_time - last_activity_at > 4시간)
→ 세션 자동 만료
else if (current_time - created_at > 12시간)
→ 세션 자동 만료
```
### 5.3 세션 만료 처리
- ✅ 프론트엔드: 만료 5분 전 알림 ("5분 후 로그아웃됩니다")
- ✅ 백엔드: 만료 후 모든 요청 거부 (401 Unauthorized)
- ✅ 재로그인 필요
---
## 6. 비밀번호 정책 (Google 기준 참고)
### 6.1 비밀번호 입력 요구사항
| 요구사항 | 기준 |
|---------|------|
| **최소 길이** | 8자 이상 |
| **대문자** | A-Z 최소 1개 (필수 아님, 권장) |
| **소문자** | a-z 최소 1개 (필수 아님, 권장) |
| **숫자** | 0-9 최소 1개 (필수 아님, 권장) |
| **특수문자** | !@#$%^&* 등 (필수 아님, 권장) |
**정책**: Google과 유사하게 "권장이지만 강제하지 않음"
- 이유: 강제 정책은 사용자 이탈 증가
### 6.2 비밀번호 저장 (DB)
- ❌ 평문 저장 절대 금지
- ✅ bcrypt 또는 PBKDF2 해시 저장
- ✅ Salt 포함 (bcrypt 내장)
- 해시 라운드: 12라운드 이상
### 6.3 비밀번호 변경
- 이전 비밀번호와 동일하면 거부
- 변경 시 "모든 기기 로그아웃" 옵션 제공 (선택사항)
- 마스터의 경우: 보안상 권장
---
## 7. 로그인 이력 및 감시 (Audit Log)
### 7.1 기록할 정보
**로그인 성공**:
- `user_id`
- `email`
- `login_timestamp`
- `user_agent` (브라우저명, OS 정보만 - 기기 ID 아님)
- `last_activity_at` (업데이트 시작 시점)
**로그인 실패**:
- `email` (계정 존재 여부 관계없이)
- `failed_timestamp`
- `failure_reason` (계정없음 / 비밀번호틀림 / 이메일인증실패 / 계정비활성 등)
- `user_agent`
**프로그램 사용 이력**:
- `user_id`
- `action_type` (파일업로드 / 프로젝트저장 / 데이터처리 등)
- `action_timestamp`
- `resource_id` (프로젝트ID, 파일ID 등)
- `status` (성공/실패)
### 7.2 IP 주소 수집 (법적 고려)
**❌ 수집하지 않음 (권장)**
- 이유: 국내 개인정보보호법상 개인정보이며, 별도 동의 + 암호화 필수
- 법적 리스크 회피
**만약 수집한다면**:
- ✅ 회원가입 약관에 명시 ("보안 목적으로 로그인 IP 기록")
- ✅ 개인정보 처리방침에 명시 (보관기간: 3개월)
- ✅ DB에 암호화하여 저장 (평문 금지)
- ✅ 마스터/시스템관리자만 조회 가능
**현재 권장**: IP 수집 제외, User-Agent(브라우저/OS)만 저장
### 7.3 데이터 보관 기간
| 로그 타입 | 보관 기간 | 자동 삭제 |
|----------|---------|---------|
| 로그인 성공 | 1년 | ✅ 자동 삭제 |
| 로그인 실패 | 6개월 | ✅ 자동 삭제 |
| 프로그램 사용 | 2년 | ✅ 자동 삭제 |
| 삭제된 사용자 로그 | 법적 보유 기간 후 삭제 | ✅ 자동 삭제 |
---
## 8. 시스템관리자 대시보드
### 8.1 기능
#### 📊 시스템 모니터링
- 활성 사용자 수 (실시간)
- 활성 회사 수
- 일일/주간/월간 활동 통계 그래프
#### 💾 리소스 모니터링
- **하드웨어**
- CPU 사용률 (%)
- 메모리 사용률 (%)
- 디스크 사용량 (GB)
- 활성 프로세스 수
- **소프트웨어**
- 일일 API 호출 수
- 업로드된 총 파일 크기 (GB)
- 다운로드 횟수
- 동시 접속자 수
#### 👥 회사 관리
- 회사 목록 조회 (가입일, 상태, 사용자 수)
- 회사 구독 상태 확인
- 마스터 정보 확인 및 변경
- 회사 삭제 (강제)
#### 👤 사용자 관리
- 전체 사용자 목록
- 사용자 상태 변경 (ACTIVE / INACTIVE / REJECTED)
- 비밀번호 초기화 (관리자용)
- 사용자 로그인 이력 조회
#### 📧 Q&A / 요청사항 관리
- 회사 메일로 수신된 Q&A/요청 목록
- 상태 관리 (NEW / REVIEWED / RESOLVED / DELETED)
- 요청 내용 검토 및 수정
- 요청 삭제 및 히스토리 관리
#### ⚠️ 에러/경고 로그
- 시스템 에러 로그 조회
- 비정상 로그인 시도 (같은 계정 5회 이상 실패)
- 대역폭 초과 경고
- 데이터베이스 에러
---
## 9. 보안 방어 로직
### 9.1 무차별 대입(Brute Force) 공격 방지
```
로그인 실패 5회 이상 → 계정 15분 잠금
- 로그인 실패 횟수 저장: login_failures (컬럼)
- 마지막 실패 시간: last_failed_at (컬럼)
- 15분 후 자동 해제
시스템관리자에게 알림 (캡파 or SMS)
- 5회 실패 시 시스템 관리자 메일 알림
- 10회 이상 실패 시 경고 레벨 상향
```
### 9.2 동시 로그인 처리 (개인별 계정 기준)
- ✅ 동시 로그인 허용
- 이유: 개인별 계정이므로 가정용 + 업무 기기 동시 사용 고려
- 다만, 의심 로그인 시 알림 (예: 다른 국가/지역에서 동시 접속)
### 9.3 세션 토큰 보안 (확정: Secure HttpOnly 세션 쿠키)
**선택**: Secure HttpOnly 세션 쿠키 (JWT 아님)
- **이유**: 모놀리식 구조 + 중소규모 + 보안 최우선 + 세션 추적 필요
**쿠키 설정**:
-**쿠키 이름**: `session_id`
-**유효 시간**: 12시간
-**Secure 플래그**: true (HTTPS만 전송)
-**HttpOnly 플래그**: true (JS 접근 불가, XSS 방지)
-**SameSite**: Lax (CSRF 방지)
**세션 저장소**: MariaDB `sessions` 테이블
```sql
CREATE TABLE sessions (
id VARCHAR(255) PRIMARY KEY,
user_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
last_activity_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
```
---
## 10. 이메일 발송 인프라
### 10.1 발송해야 할 메일 유형
| 메일 타입 | 수신자 | 발송 타이밍 | 중요도 |
|----------|--------|-----------|------|
| **가입 확인 메일** | 신규 가입자 | 가입 완료 직후 | 🔴 필수 |
| **OTP 인증 코드** | 로그인/재인증 자 | 로그인 시도 직후 | 🔴 필수 |
| **팀원 승인 요청** | 마스터 | 팀원이 가입 신청 시 | 🔴 필수 |
| **가입 승인/거부 결과** | 팀원 | 마스터 승인/거부 직후 | 🟠 중요 |
| **3개월 재인증 알림** | 모든 사용자 | 3개월마다 정기적 | 🟠 중요 |
| **세션 만료 알림** | 활동 중인 사용자 | 만료 5분 전 | 🟡 알림 |
| **비밀번호 변경 알림** | 해당 사용자 | 변경 직후 | 🟡 알림 |
| **Q&A 답변 회신** | 문의자 | 시스템 관리자 처리 후 | 🟡 알림 |
### 10.2 메일 발송 서비스
**권장 선택**: AWS SES, SendGrid, 또는 한국 국내 서비스 (Solapi, Stibee)
**이유**:
- 높은 배송 성공률 (99% 이상)
- 빠른 발송 속도 (즉시~1초)
- 스팸 필터 회피 (신뢰도 높음)
- API 기반 통합 용이
**구현 방식**:
- FastAPI 백엔드에서 비동기 작업 큐 (Celery + Redis)
- 실패한 메일은 재시도 로직 (최대 3회)
---
## 11. DB 스키마 (최소 필수 테이블)
### 11.1 companies 테이블
```sql
CREATE TABLE companies (
id INT AUTO_INCREMENT PRIMARY KEY,
company_name VARCHAR(255) NOT NULL,
company_code VARCHAR(20) UNIQUE NOT NULL, -- 초대코드 역할
business_number VARCHAR(20),
phone_number VARCHAR(20),
address VARCHAR(500),
representative_name VARCHAR(100),
master_user_id INT NOT NULL,
status ENUM('ACTIVE', 'INACTIVE', 'SUSPENDED') DEFAULT 'ACTIVE',
subscription_status ENUM('FREE', 'TRIAL', 'PAID', 'EXPIRED') DEFAULT 'FREE',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (master_user_id) REFERENCES users(id)
);
```
### 11.2 users 테이블
```sql
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
company_id INT,
is_master BOOLEAN DEFAULT FALSE,
status ENUM('PENDING', 'ACTIVE', 'INACTIVE', 'REJECTED') DEFAULT 'PENDING',
last_email_verified_at TIMESTAMP,
login_failures INT DEFAULT 0, -- 실패 횟수
last_failed_at TIMESTAMP, -- 마지막 실패 시간
account_locked_until TIMESTAMP, -- 계정 잠금 해제 시간
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (company_id) REFERENCES companies(id)
);
```
### 11.3 join_requests 테이블
```sql
CREATE TABLE join_requests (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
company_id INT NOT NULL,
status ENUM('PENDING', 'APPROVED', 'REJECTED') DEFAULT 'PENDING',
requested_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
reviewed_at TIMESTAMP,
reviewed_by INT, -- 마스터 user_id
review_comment TEXT,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (company_id) REFERENCES companies(id),
FOREIGN KEY (reviewed_by) REFERENCES users(id)
);
```
### 11.4 login_logs 테이블
```sql
CREATE TABLE login_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
email VARCHAR(255), -- 실패 시 user_id가 NULL일 수 있음
status ENUM('SUCCESS', 'FAILURE') NOT NULL,
failure_reason VARCHAR(100),
user_agent TEXT, -- 브라우저, OS 정보만
login_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
INDEX (user_id),
INDEX (login_at)
);
```
### 11.5 activity_logs 테이블
```sql
CREATE TABLE activity_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
action_type ENUM('FILE_UPLOAD', 'FILE_DOWNLOAD', 'PROJECT_SAVE', 'DATA_PROCESS', 'PAGE_VIEW'),
action_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
resource_id VARCHAR(255), -- 프로젝트/파일 ID
status ENUM('SUCCESS', 'FAILURE') DEFAULT 'SUCCESS',
details JSON, -- 추가 정보
FOREIGN KEY (user_id) REFERENCES users(id),
INDEX (user_id),
INDEX (action_at)
);
```
### 11.6 system_admin_logs 테이블
```sql
CREATE TABLE system_admin_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
admin_user_id INT NOT NULL,
action_type ENUM('USER_DELETE', 'COMPANY_DELETE', 'QA_MODIFY', 'QA_DELETE', 'RESET_PASSWORD'),
target_id INT, -- 대상 사용자/회사 ID
action_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
details JSON,
FOREIGN KEY (admin_user_id) REFERENCES users(id),
INDEX (admin_user_id),
INDEX (action_at)
);
```
### 11.7 support_requests 테이블 (Q&A)
```sql
CREATE TABLE support_requests (
id INT AUTO_INCREMENT PRIMARY KEY,
company_id INT,
user_email VARCHAR(255),
subject VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
status ENUM('NEW', 'REVIEWED', 'RESOLVED', 'DELETED') DEFAULT 'NEW',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
reviewed_at TIMESTAMP,
reviewed_by INT, -- 시스템 관리자 user_id
review_notes TEXT,
FOREIGN KEY (company_id) REFERENCES companies(id),
FOREIGN KEY (reviewed_by) REFERENCES users(id),
INDEX (status),
INDEX (created_at)
);
```
---
## 12. 구현 우선순위 (Phase)
### Phase 1 (필수): 기본 인증 및 회원가입
- [ ] 회원가입 로직 (이메일 OTP 포함)
- [ ] 마스터 회사 정보 입력
- [ ] 로그인 로직 (비밀번호 + 이메일 OTP)
- [ ] 세션/토큰 관리
- [ ] 사용자 상태 관리 (ACTIVE/INACTIVE)
### Phase 2 (필수): 팀원 가입 승인제
- [ ] 팀원 가입 신청 기능
- [ ] 가입 요청 알림 메일 (마스터 수신)
- [ ] 마스터 승인/거부 대시보드
- [ ] 승인/거부 결과 메일 (팀원 수신)
### Phase 3 (중요): 보안 강화
- [ ] 3개월 주기 재인증 로직
- [ ] 로그인 이력 기록 (login_logs)
- [ ] 무차별 대입 공격 방지 (5회 실패 잠금)
- [ ] 활동 로그 기록 (activity_logs)
### Phase 4 (중요): 마스터 관리 기능
- [ ] 팀원 목록 조회
- [ ] 팀원 강제 내보내기 (status = INACTIVE)
- [ ] 마스터 대시보드 (회사 통계)
### Phase 5 (고급): 시스템관리자 포털
- [ ] 관리자 대시보드
- [ ] 회사/사용자 관리
- [ ] Q&A 관리
- [ ] 리소스 모니터링
- [ ] 에러 로그 모니터링
---
## 13. 주요 검토 사항
### 13.1 이메일 발송 인프라
- [ ] SMTP 서버 선정 (AWS SES / SendGrid / Solapi)
- [ ] 테스트: 가입 메일 발송 성공률 확인
- [ ] 스팸 필터 회피 전략 (SPF, DKIM, DMARC 설정)
### 13.2 비밀번호 해싱 라이브러리
- [ ] Python bcrypt 또는 argon2 설정
- [ ] 라운드 수 결정 (권장: 12 이상)
### 13.3 JWT vs 세션 쿠키
- [ ] 선택: Secure HttpOnly 쿠키 (권장, CSRF/XSS 방어)
- [ ] 토큰 만료 시간 설정
### 13.4 개인정보 처리방침 작성
- [ ] 로그인 이력 수집 목적 명시
- [ ] 보관 기간 명시
- [ ] 사용자 동의 형식 결정
---
## 14. 다음 단계
1.**이 계획서 검토** (당신의 피드백)
2. 📝 **세부 기술 사양 작성** (.agent/backend.md 업데이트)
3. 💻 **Phase 1 코드 구현 시작** (회원가입 + 로그인)
4. 🧪 **단위 테스트 작성**
5. 🎨 **프론트엔드 UI/UX 설계** (.agent/frontend.md)
6. 📊 **통합 테스트 및 검증**
---
**작성일**: 2026-07-07
**담당자**: 시스템 설계 팀
**상태**: 초안 (v1.0)
+131
View File
@@ -0,0 +1,131 @@
# .agent 폴더 계획 문서 인덱스
## 📋 핵심 계획 문서 (*_plan.md)
### 1. 로그인 & 보안 계획서
- **파일**: `login_security_plan.md`
- **최신 버전**: v1.0 (2026-07-07)
- **내용**:
- 계정 유형 정의 (마스터, 팀원, 시스템관리자)
- 회원가입/로그인 흐름
- 이메일 인증(MFA) 정책
- 세션 관리 (12시간 기본, 4시간 활동감지)
- DB 스키마 (users, companies, join_requests, login_logs, activity_logs 등)
- ISMS-P 보안 기초 구축
- **상태**: 완결, 코드 작성 대기
### 2. 이메일 인프라 계획서
- **파일**: `email_infrastructure_plan.md`
- **최신 버전**: v1.0 (2026-07-07)
- **내용**:
- 발송 유형 (OTP, 가입 확인, 팀원 승인 요청 등)
- Google Gmail SMTP 설정 (개발용: umsangdon@gmail.com)
- 비동기 이메일 발송 아키텍처
- common_util_email.py 모듈 예시
- 이메일 템플릿 (OTP, 팀원 승인 요청)
- Phase 2 마이그레이션 계획 (AWS SES)
- **상태**: 완결, 코드 작성 대기
### 3. 세션 쿠키 구현 명세
- **파일**: `SESSION_COOKIE_SPEC.md`
- **최신 버전**: v1.0 (2026-07-07)
- **내용**:
- **최종 확정**: Secure HttpOnly 세션 쿠키 (JWT 아님)
- 쿠키 설정 상세 (name, max_age, secure, httponly, samesite)
- sessions 테이블 스키마
- 세션 검증 미들웨어 코드
- 로그아웃 및 강제 로그아웃 구현
- 만료된 세션 자동 정리
- 활성 세션 모니터링
- 테스트 방법 (수동/자동)
- 프론트엔드 연동 (credentials: "include")
- **상태**: 완결, 코드 작성 대기
### 4. 마이그레이션 계획서
- **파일**: `migration_plan.md`
- **내용**: 프로젝트 초기 구현 단계
- **상태**: 코드 구현 완료, 검증 진행 중
---
## 📚 기술 명세 문서
### 1. 프로젝트 구조
- **파일**: `structure.md`
- **최신 업데이트**: 2026-07-07 (A09_Security 추가)
- **내용**: 전체 폴더/파일 구조, 명명 규칙
- **변경사항**:
- ✅ A09_Security 폴더 추가 (보안/약관 동의 페이지)
### 2. 백엔드 명세
- **파일**: `backend.md`
- **최신 업데이트**: 2026-07-07 (섹션 6 추가)
- **내용**: FastAPI 아키텍처, DB 관리, 워크플로우 동기화
- **변경사항**:
- ✅ 섹션 6 추가: 로그인/인증 시스템 (OTP, 비밀번호, 세션, 권한, 이메일)
### 3. 프론트엔드 명세
- **파일**: `frontend.md`
- **내용**: UI 테마, 컴포넌트, 레이아웃 제약
- **상태**: 검토 대기 (A09 페이지 가이드 추가 필요)
### 4. DB 스키마
- **파일**: `db_schema.md`
- **내용**: 데이터베이스 테이블 정의
- **상태**: 로그인 테이블 추가 필요
---
## 💬 참고 자료
### 1. 로그인 대화 기록
- **파일**: `login_chat_info.md`
- **내용**: 제미나와의 로그인/보안 요구사항 논의
- **방향성**:
- ✅ 개인별 계정 (공용 이메일 허용)
- ✅ 초대코드 → 사용자 요청 기반 (마스터 승인제)
- ✅ 이메일 OTP 인증 (3개월 주기)
- ✅ 세션 12시간 (4시간 활동감지)
- ✅ IP 수집 제외, User-Agent만 저장
---
## 🔄 다음 액션 아이템
### Phase 1 (필수, 즉시)
- [ ] **.env 파일 작성** (Google 앱 비밀번호 설정)
- [ ] **common_util_email.py** 코드 구현
- [ ] **A07_Register_Router.py** 이메일 발송 통합
- [ ] **로그인 API** 구현 및 테스트
- [ ] **DB 테이블** 생성 (schema.sql)
- [ ] **로그인 페이지** (A06, A07, A09) 프론트엔드 구현
### Phase 2 (중요)
- [ ] **3개월 재인증** 로직 구현
- [ ] **로그인 이력** 기록 (activity_logs)
- [ ] **마스터 대시보드** (팀원 관리)
- [ ] **frontend.md** 업데이트 (A06~A09 가이드)
- [ ] **db_schema.md** 업데이트 (모든 로그인 테이블)
### Phase 3 (고급)
- [ ] **시스템관리자 포털** 개발
- [ ] **AWS SES** 마이그레이션 준비
- [ ] **ISMS-P** 인증 준비 문서
---
## 📝 문서 관리 정책
### 규칙
1. **_plan.md** 파일은 수정하지 않음 (새 버전 생성)
2. **기술 명세 문서** (structure, backend, frontend, db_schema)는 필요시 즉시 업데이트
3. **참고 자료** (chat_info 등)는 아카이브 용도
### 버전 관리
- 계획서 버전 변경: `v1.0``v1.1` (CHANGELOG 추가)
- 기술 명세는 날짜만 기록
---
**마지막 업데이트**: 2026-07-07
**담당자**: AI 문서 관리팀
@@ -0,0 +1,426 @@
# 세션 쿠키 구현 명세 (Session Cookie Specification)
## 🔐 최종 확정: Secure HttpOnly 세션 쿠키
### 선택 이유
-**모놀리식 구조** (FastAPI + MariaDB)
-**중소규모 프로젝트** (30인 미만 회사)
-**보안 최우선** (CSRF/XSS 방어)
-**세션 추적 필요** (로그인 이력, 마스터 강제 로그아웃)
-**JWT의 단점 회피** (상태 비저장, 로그아웃 어려움)
---
## 📋 쿠키 설정 상세
### 쿠키 생성 (로그인 성공 시)
```python
# A06_Login/A06_Login_Router.py (예시)
from fastapi import APIRouter
from fastapi.responses import JSONResponse
import secrets
from datetime import datetime, timedelta
router = APIRouter()
@router.post("/api/auth/login")
async def login(email: str, password: str, otp_code: str):
"""
로그인 엔드포인트 (OTP 인증 완료 후)
"""
# 1. 사용자 검증 및 OTP 확인
user = await verify_user_and_otp(email, password, otp_code)
# 2. 세션 ID 생성 (32바이트 난수)
session_id = secrets.token_urlsafe(32)
# 3. DB에 세션 저장
await db.create_session(
session_id=session_id,
user_id=user.id,
expires_at=datetime.utcnow() + timedelta(hours=12)
)
# 4. 응답 생성 + 쿠키 설정
response = JSONResponse({
"status": "success",
"message": "로그인 성공",
"user_id": user.id,
"user_email": user.email
})
response.set_cookie(
key="session_id",
value=session_id,
max_age=43200, # 12시간 (초 단위: 12 * 60 * 60)
secure=True, # HTTPS만 전송 (개발: False 가능)
httponly=True, # JavaScript 접근 불가
samesite="Lax", # CSRF 방지
domain=None, # 현재 도메인만
path="/" # 모든 경로에서 전송
)
return response
```
### 쿠키 값 요구사항
| 항목 | 값 | 설명 |
|------|-----|------|
| **쿠키 이름** | `session_id` | 표준 이름 |
| **값** | 32바이트 난수 | `secrets.token_urlsafe(32)` |
| **유효시간** | 43200초 (12시간) | 브라우저 종료 후 삭제 |
| **Secure** | true | HTTPS만 (개발 환경: localhost는 예외) |
| **HttpOnly** | true | XSS 방지 필수 |
| **SameSite** | Lax | CSRF 방지 (기본값) |
| **Domain** | None | 현재 도메인만 |
| **Path** | / | 모든 경로에서 유효 |
---
## 💾 DB 스키마
### sessions 테이블
```sql
CREATE TABLE sessions (
id VARCHAR(255) PRIMARY KEY, -- session_id (쿠키 값)
user_id INT NOT NULL, -- 사용자 ID
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 생성 시간
expires_at TIMESTAMP NOT NULL, -- 만료 시간 (12시간 후)
last_activity_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 마지막 활동
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX (user_id),
INDEX (expires_at) -- 만료된 세션 정리용
);
```
### 예제 데이터
```
id: "xY9z_kL2m-nO3pQr4sT5uV6w7xYz_aB1cD2eF3gH"
user_id: 1
created_at: 2026-07-07 10:00:00
expires_at: 2026-07-07 22:00:00
last_activity_at: 2026-07-07 10:15:30
```
---
## 🔍 요청 검증 (미들웨어)
### 세션 검증 로직
```python
# common_util/common_util_auth.py
from fastapi import Request, HTTPException
from datetime import datetime
async def verify_session(request: Request):
"""
요청의 세션 쿠키를 검증하고 사용자 정보 반환
Returns:
dict: {"user_id": int, "user_email": str, "company_id": int}
Raises:
HTTPException: 401 (세션 없음, 만료, 잘못됨)
"""
# 1. 쿠키에서 세션 ID 추출
session_id = request.cookies.get("session_id")
if not session_id:
raise HTTPException(
status_code=401,
detail="로그인 필요"
)
# 2. DB에서 세션 조회
session = await db.get_session(session_id)
if not session:
raise HTTPException(
status_code=401,
detail="유효하지 않은 세션"
)
# 3. 세션 만료 확인
if datetime.utcnow() > session.expires_at:
# 만료된 세션 삭제
await db.delete_session(session_id)
raise HTTPException(
status_code=401,
detail="세션 만료 (다시 로그인하세요)"
)
# 4. 활동 타임아웃 확인 (마지막 활동으로부터 4시간)
time_since_activity = datetime.utcnow() - session.last_activity_at
if time_since_activity.total_seconds() > 14400: # 4시간
await db.delete_session(session_id)
raise HTTPException(
status_code=401,
detail="비활동으로 인한 세션 만료"
)
# 5. 활동 시간 업데이트
await db.update_session_activity(session_id)
# 6. 사용자 정보 조회 및 반환
user = await db.get_user(session.user_id)
return {
"user_id": user.id,
"user_email": user.email,
"company_id": user.company_id,
"is_master": user.is_master
}
```
### FastAPI 라우터에서 사용
```python
# 모든 보호된 엔드포인트에서
from fastapi import Depends
@router.get("/api/projects")
async def list_projects(session: dict = Depends(verify_session)):
"""
프로젝트 목록 조회 (로그인 필요)
"""
user_id = session["user_id"]
company_id = session["company_id"]
projects = await db.get_company_projects(company_id)
return {"status": "success", "projects": projects}
```
---
## 🔐 로그아웃 처리
### 로그아웃 엔드포인트
```python
@router.post("/api/auth/logout")
async def logout(request: Request):
"""
로그아웃 (세션 삭제)
"""
session_id = request.cookies.get("session_id")
if session_id:
# DB에서 세션 삭제
await db.delete_session(session_id)
# 응답 생성 + 쿠키 삭제
response = JSONResponse({
"status": "success",
"message": "로그아웃 성공"
})
# 쿠키 삭제 (max_age=0)
response.delete_cookie(
key="session_id",
path="/",
secure=True,
httponly=True,
samesite="Lax"
)
return response
```
---
## 🛡️ 마스터 강제 로그아웃
### 특정 사용자의 모든 세션 삭제
```python
async def force_logout_user(user_id: int):
"""
사용자의 모든 세션 삭제 (마스터가 팀원을 내보낼 때)
"""
await db.delete_all_user_sessions(user_id)
# DB 쿼리: DELETE FROM sessions WHERE user_id = ?
# 마스터가 팀원을 내보낼 때
@router.post("/api/admin/member/remove")
async def remove_member(
member_user_id: int,
session: dict = Depends(verify_session)
):
"""마스터가 팀원 내보내기"""
# 권한 확인: 현재 사용자가 마스터인지
requester = await db.get_user(session["user_id"])
if not requester.is_master:
raise HTTPException(status_code=403, detail="권한 없음")
# 팀원 상태 변경
await db.update_user_status(member_user_id, "INACTIVE")
# 해당 팀원의 모든 세션 삭제 (즉시 로그아웃)
await force_logout_user(member_user_id)
return {"status": "success", "message": "팀원이 제거되었습니다"}
```
---
## 🧹 자동 정리 (크론 작업)
### 만료된 세션 자동 삭제
```python
# 매시간 실행
async def cleanup_expired_sessions():
"""만료된 세션 자동 삭제"""
deleted_count = await db.delete_expired_sessions(
before_date=datetime.utcnow()
)
logger.info(f"{deleted_count}개의 만료된 세션 삭제")
# SQL
# DELETE FROM sessions WHERE expires_at < NOW()
```
---
## 📊 모니터링
### 활성 세션 통계
```python
async def get_active_sessions_stats():
"""활성 세션 통계 (관리자 대시보드용)"""
stats = {
"total_active_sessions": await db.count_active_sessions(),
"total_active_users": await db.count_active_users(),
"expired_sessions_today": await db.count_expired_sessions_today(),
"by_company": await db.get_active_sessions_by_company()
}
return stats
```
---
## 🧪 테스트
### 수동 테스트
```bash
# 1. 로그인 (쿠키 받기)
curl -i -X POST http://localhost:8000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "pass123", "otp_code": "123456"}'
# 응답:
# Set-Cookie: session_id=...; Max-Age=43200; Path=/; Secure; HttpOnly; SameSite=Lax
# 2. 쿠키와 함께 요청 (자동으로 포함됨)
curl -i http://localhost:8000/api/projects \
-b "session_id=..." \
-H "Accept: application/json"
# 3. 로그아웃
curl -i -X POST http://localhost:8000/api/auth/logout \
-b "session_id=..."
# 응답:
# Set-Cookie: session_id=; Max-Age=0; Path=/; Secure; HttpOnly; SameSite=Lax
```
### 자동 테스트
```python
# test_session_cookie.py
import pytest
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_login_creates_session_cookie():
"""로그인 시 세션 쿠키 생성"""
response = client.post("/api/auth/login", json={
"email": "test@example.com",
"password": "password123",
"otp_code": "123456"
})
assert response.status_code == 200
assert "session_id" in response.cookies
assert response.cookies["session_id"].get("secure")
assert response.cookies["session_id"].get("httponly")
def test_protected_endpoint_requires_session():
"""보호된 엔드포인트는 세션 필요"""
response = client.get("/api/projects")
assert response.status_code == 401
def test_logout_deletes_session():
"""로그아웃 시 세션 쿠키 삭제"""
# 먼저 로그인
login_response = client.post("/api/auth/login", json={...})
# 그 후 로그아웃
logout_response = client.post("/api/auth/logout")
assert logout_response.status_code == 200
assert logout_response.cookies["session_id"].get("max_age") == 0
```
---
## 🔄 프론트엔드 연동
### 쿠키는 자동으로 전송됨
```typescript
// A06_Login_Api_Fetch.ts
export async function login(email: string, password: string, otpCode: string) {
const response = await fetch("http://localhost:8000/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include", // 쿠키 자동 포함
body: JSON.stringify({ email, password, otp_code: otpCode })
});
return response.json();
}
// 이후 요청에서도 credentials: "include"를 사용하면 자동으로 쿠키 전송됨
export async function getProjects() {
const response = await fetch("http://localhost:8000/api/projects", {
credentials: "include" // 쿠키 자동 포함
});
return response.json();
}
```
---
## ✅ 최종 체크리스트
- [ ] sessions 테이블 생성
- [ ] 로그인 API에서 쿠키 생성 (set_cookie)
- [ ] 세션 검증 미들웨어 구현
- [ ] 모든 보호된 엔드포인트에 Depends(verify_session) 추가
- [ ] 로그아웃 API 구현 (쿠키 삭제)
- [ ] 마스터 강제 로그아웃 구현
- [ ] 자동 정리 크론 작업 추가
- [ ] 프론트엔드 credentials: "include" 설정
- [ ] 수동/자동 테스트 통과
---
**확정일**: 2026-07-07
**상태**: 최종 확정 (v1.0)
+471
View File
@@ -0,0 +1,471 @@
# 개발 환경 설정 가이드 (Setup Guide)
## 🚀 개발 시작 전 필수 설정
이 가이드는 **로그인/보안 시스템 개발**을 시작하기 전에 필요한 모든 설정을 다룹니다.
---
## 1️⃣ Google Gmail SMTP 설정 (개발용)
### Step 1: Google 계정 2단계 인증 활성화
```
1. Google 계정으로 로그인
→ https://myaccount.google.com
2. 왼쪽 메뉴: "보안" (Security)
3. "2단계 인증" → 활성화
(이미 활성화되어 있으면 스킵)
```
### Step 2: 앱 비밀번호 생성
```
1. Google 계정 관리 페이지
→ https://myaccount.google.com/apppasswords
2. 앱 선택: "메일(Mail)"
3. 기기 선택: "Windows" (또는 개발 환경)
4. [생성] 버튼 클릭
5. 16자리 비밀번호 생성됨
예: "abcd efgh ijkl mnop"
6. 이 비밀번호를 안전히 보관
```
⚠️ **주의**: 이 비밀번호는 .env 파일에만 저장하고, Git에 절대 커밋하지 마세요!
### Step 3: .env 파일 작성
프로젝트 루트에 `config/.env` 파일 생성:
```bash
# config/.env
# ===== Email Infrastructure (Phase 1) =====
SMTP_PROVIDER=google
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=umsangdon@gmail.com
SMTP_PASSWORD=abcd efgh ijkl mnop # 위에서 생성한 앱 비밀번호
SMTP_USE_TLS=true
# 발송 이메일 주소
EMAIL_FROM_ADDRESS=umsangdon@gmail.com
EMAIL_FROM_NAME=Aislo Support
EMAIL_NOREPLY_ADDRESS=umsangdon+noreply@gmail.com
# 이메일 발송 설정
EMAIL_RETRY_COUNT=3
EMAIL_RETRY_DELAY_SECONDS=2
EMAIL_OTP_VALID_MINUTES=5
EMAIL_NOTIFICATION_ENABLED=true
# 관리자 이메일
ADMIN_EMAIL=umsangdon@gmail.com
# ===== Database =====
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=your_password
DB_NAME=aislo_db
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
```
### Step 4: .gitignore 확인
프로젝트 루트의 `.gitignore`에 다음 추가:
```bash
# 환경 변수 (절대 Git에 커밋하지 않음)
config/.env
config/.env.local
.env
.env.local
.env.*.local
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
```
---
## 2️⃣ Python 환경 설정
### 필요한 라이브러리 설치
```bash
# 프로젝트 루트에서 실행
# FastAPI + 비동기 ASGI
pip install fastapi uvicorn
# 데이터베이스
pip install aiomysql
# 유효성 검사
pip install pydantic email-validator
# 비밀번호 암호화
pip install bcrypt
# 환경 변수 관리
pip install python-dotenv
# 이메일 발송 (기본 내장, 추가 설치 불필요)
# smtplib, email (Python 표준 라이브러리)
```
### requirements.txt 생성 (권장)
```bash
pip freeze > requirements.txt
```
그 다음, 팀원이 환경 구축할 때:
```bash
pip install -r requirements.txt
```
---
## 3️⃣ 데이터베이스 초기화
### MariaDB 서버 확인
```bash
# MariaDB 실행 중 확인
mysql -u root -p -e "SELECT VERSION();"
```
### DB 및 테이블 생성
```bash
# MySQL 클라이언트 실행
mysql -u root -p
# 다음 SQL 실행:
```
```sql
-- 데이터베이스 생성
CREATE DATABASE IF NOT EXISTS aislo_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE aislo_db;
-- 회사 테이블
CREATE TABLE IF NOT EXISTS companies (
id INT AUTO_INCREMENT PRIMARY KEY,
company_name VARCHAR(255) NOT NULL,
company_code VARCHAR(20) UNIQUE NOT NULL,
business_number VARCHAR(20),
phone_number VARCHAR(20),
address VARCHAR(500),
representative_name VARCHAR(100),
master_user_id INT NOT NULL,
status ENUM('ACTIVE', 'INACTIVE', 'SUSPENDED') DEFAULT 'ACTIVE',
subscription_status ENUM('FREE', 'TRIAL', 'PAID', 'EXPIRED') DEFAULT 'FREE',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (master_user_id) REFERENCES users(id)
);
-- 사용자 테이블
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
company_id INT,
is_master BOOLEAN DEFAULT FALSE,
status ENUM('PENDING', 'ACTIVE', 'INACTIVE', 'REJECTED') DEFAULT 'PENDING',
last_email_verified_at TIMESTAMP,
login_failures INT DEFAULT 0,
last_failed_at TIMESTAMP,
account_locked_until TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (company_id) REFERENCES companies(id),
INDEX (email),
INDEX (company_id)
);
-- OTP 저장 (선택사항, users 테이블에 컬럼 추가 가능)
ALTER TABLE users ADD COLUMN IF NOT EXISTS otp_hash VARCHAR(255);
ALTER TABLE users ADD COLUMN IF NOT EXISTS otp_expires_at TIMESTAMP;
-- 가입 요청 테이블
CREATE TABLE IF NOT EXISTS join_requests (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
company_id INT NOT NULL,
status ENUM('PENDING', 'APPROVED', 'REJECTED') DEFAULT 'PENDING',
requested_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
reviewed_at TIMESTAMP,
reviewed_by INT,
review_comment TEXT,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (company_id) REFERENCES companies(id),
FOREIGN KEY (reviewed_by) REFERENCES users(id),
INDEX (status),
INDEX (company_id)
);
-- 로그인 이력 테이블
CREATE TABLE IF NOT EXISTS login_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
email VARCHAR(255),
status ENUM('SUCCESS', 'FAILURE') NOT NULL,
failure_reason VARCHAR(100),
user_agent TEXT,
login_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
INDEX (user_id),
INDEX (login_at)
);
-- 활동 로그 테이블
CREATE TABLE IF NOT EXISTS activity_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
action_type ENUM('FILE_UPLOAD', 'FILE_DOWNLOAD', 'PROJECT_SAVE', 'DATA_PROCESS', 'PAGE_VIEW'),
action_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
resource_id VARCHAR(255),
status ENUM('SUCCESS', 'FAILURE') DEFAULT 'SUCCESS',
details JSON,
FOREIGN KEY (user_id) REFERENCES users(id),
INDEX (user_id),
INDEX (action_at)
);
-- 시스템 관리자 로그
CREATE TABLE IF NOT EXISTS system_admin_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
admin_user_id INT NOT NULL,
action_type ENUM('USER_DELETE', 'COMPANY_DELETE', 'QA_MODIFY', 'QA_DELETE', 'RESET_PASSWORD'),
target_id INT,
action_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
details JSON,
FOREIGN KEY (admin_user_id) REFERENCES users(id),
INDEX (admin_user_id),
INDEX (action_at)
);
-- Q&A 요청 테이블
CREATE TABLE IF NOT EXISTS support_requests (
id INT AUTO_INCREMENT PRIMARY KEY,
company_id INT,
user_email VARCHAR(255),
subject VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
status ENUM('NEW', 'REVIEWED', 'RESOLVED', 'DELETED') DEFAULT 'NEW',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
reviewed_at TIMESTAMP,
reviewed_by INT,
review_notes TEXT,
FOREIGN KEY (company_id) REFERENCES companies(id),
FOREIGN KEY (reviewed_by) REFERENCES users(id),
INDEX (status),
INDEX (created_at)
);
-- 세션 테이블 (Secure HttpOnly 쿠키 저장소)
CREATE TABLE IF NOT EXISTS sessions (
id VARCHAR(255) PRIMARY KEY,
user_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
last_activity_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
INDEX (user_id),
INDEX (expires_at)
);
```
확인:
```sql
-- 테이블 목록 확인
SHOW TABLES;
-- 사용자 테이블 구조 확인
DESCRIBE users;
```
---
## 4️⃣ 프로젝트 폴더 구조 확인
```bash
# 필요한 폴더 생성
mkdir -p common_util
mkdir -p config
mkdir -p logs
mkdir -p storage
# .env 파일 위치 확인
ls -la config/.env
```
---
## 5️⃣ 개발 서버 실행
### FastAPI 서버 시작
```bash
# 프로젝트 루트에서
cd your-project
# Uvicorn 서버 실행
uvicorn main:app --reload --host 127.0.0.1 --port 8000
```
확인:
```
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started server process [12345]
```
### 테스트 엔드포인트 확인
```bash
# 다른 터미널에서
curl http://127.0.0.1:8000/api/auth/health
```
---
## 6️⃣ 이메일 발송 테스트
### Python 테스트 스크립트 작성
```python
# test_email.py
import asyncio
from common_util.common_util_email import EmailService
from common_util.common_util_email_templates import get_otp_email_html
async def test_send_otp():
"""OTP 이메일 발송 테스트"""
html_body, text_body = get_otp_email_html("123456")
result = await EmailService.send_email(
to_email="umsangdon@gmail.com", # 수신 테스트 이메일
subject="[Aislo] 테스트 - 이메일 인증 코드",
body_html=html_body,
body_text=text_body
)
if result:
print("✅ 이메일 발송 성공!")
else:
print("❌ 이메일 발송 실패")
if __name__ == "__main__":
asyncio.run(test_send_otp())
```
실행:
```bash
python test_email.py
```
결과:
```
✅ 이메일 발송 성공!
```
Google 계정에서 수신 확인:
```
- 받는사람: umsangdon@gmail.com
- 발신: "Aislo Support <umsangdon@gmail.com>"
- 제목: "[Aislo] 테스트 - 이메일 인증 코드"
- 본문: HTML 템플릿 + OTP 코드 "123456"
```
---
## 7️⃣ 문제 해결 (Troubleshooting)
### 문제 1: "SMTP 연결 실패"
```
Error: [Errno 11001] getaddrinfo failed
```
**해결**:
1. 인터넷 연결 확인
2. .env 파일의 SMTP_HOST, SMTP_PORT 확인
3. 방화벽에서 포트 587 허용 확인
### 문제 2: "앱 비밀번호 인증 실패"
```
Error: b'5.7.8 Username and password not accepted'
```
**해결**:
1. Google 계정 2단계 인증 활성화 확인
2. 앱 비밀번호 정확히 입력 (띄어쓰기 포함)
3. .env 파일에서 SMTP_PASSWORD 재확인
### 문제 3: "메일이 스팸 폴더로 들어감"
**해결**:
1. 발신자 이름 명확히 하기: `EMAIL_FROM_NAME=Aislo Support`
2. 이메일 템플릿에 스팸 키워드 제거
3. Phase 2에서 AWS SES로 마이그레이션
---
## ✅ 체크리스트
- [ ] Google 계정 2단계 인증 활성화
- [ ] 앱 비밀번호 생성 (16자리)
- [ ] `config/.env` 파일 작성
- [ ] `.gitignore``.env` 추가
- [ ] Python 라이브러리 설치 (pip install -r requirements.txt)
- [ ] MariaDB 데이터베이스 및 테이블 생성
- [ ] 필요한 폴더 생성 (common_util, config, logs, storage)
- [ ] FastAPI 서버 실행 확인
- [ ] 이메일 발송 테스트 성공
---
## 📚 다음 단계
1. **로그인 페이지 구현** (A06_Login, A07_Register, A09_Security)
2. **회원가입 API 작성** (A07_Register_Router.py)
3. **로그인 API 작성** (A06_Login_Router.py)
4. **프론트엔드 연결** (A06_Login_Api_Fetch.ts, A07_Register_Api_Fetch.ts)
5. **테스트 및 검증**
자세한 내용은 다음 문서를 참고하세요:
- `.agent/login_security_plan.md` — 전체 보안 계획
- `.agent/email_infrastructure_plan.md` — 이메일 인프라 상세
- `.agent/backend.md` — 백엔드 API 명세
---
**작성일**: 2026-07-07
**상태**: 초안 (v1.0)
@@ -0,0 +1,78 @@
# Aislo 로그인 및 보안 시스템 종합 검증 결과 보고서 (Validation Report)
본 보고서는 Aislo(아이슬로) 프로그램의 **로그인, 보안 및 세션 관리 시스템**의 설계 사양서(`.agent/` 폴더 내 문서군)와 실제 구현된 백엔드 코드(`common_util/`, `A06_Login/`, `A07_Register/`, `main.py`, `db_management/`) 간의 일관성 및 보안성 검증 결과를 담고 있습니다.
---
## 📊 종합 검증 요약
| 검증 부문 | 요구사항 (사양서) | 실제 구현 상태 | 일치 여부 | 비고 |
| :--- | :--- | :--- | :---: | :--- |
| **1. DB 스키마 및 마이그레이션** | `sessions`, `email_otps` 포함 9개 보안 테이블 | `002_auth_security.sql` 반영 완료 | **일치 (Pass)** | MariaDB 10.6+ 최적화 |
| **2. 비밀번호 및 OTP 보안** | bcrypt 암호화, 6자리 난수 OTP | `common_util_auth.py` 내 구현 완료 | **일치 (Pass)** | bcrypt 복잡도/라운드 설정 준수 |
| **3. 세션 쿠키 제어** | Secure, HttpOnly, SameSite=Lax 쿠키 | `set_session_cookie` 구현 완료 | **일치 (Pass)** | 브라우저 쿠키 탈취 원천 방지 |
| **4. 세션 타임아웃** | 12시간 절대 만료 & 4시간 비활동 만료 | `verify_session` 미들웨어 검증 | **일치 (Pass)** | DB 세션 타임존 UTC 강제 설정 완료 |
| **5. 무차별 대입 방지** | 5회 실패 시 15분 잠금 & 관리자 알림 | `record_failed_login` 연동 완료 | **일치 (Pass)** | OTP 검증 실패 시 계정 잠금 연동 완료 |
| **6. 비동기 이메일 인프라** | SMTP 비동기 스레드 위임 및 3회 재시도 | `send_email_background` 구현 완료 | **일치 (Pass)** | 지수 백오프(Exponential Backoff) 적용 |
| **7. 강제 로그아웃 흐름** | 팀원 추방/비밀번호 변경 시 세션 일괄 삭제 | `remove_member`, `change_password` | **일치 (Pass)** | DB 세션 레코드 즉시 삭제 |
| **8. 주기적 세션 정리** | 만료 세션 및 구형 데이터 자동 클리닝 | `cleanup_expired_sessions` 데몬 | **일치 (Pass)** | FastAPI `lifespan` 내 매시간 수행 |
---
## 🔍 세부 검증 결과 분석
### 1. 데이터베이스 및 스키마 검증
* **검증 대상**: `db_management/002_auth_security.sql`
* **분석**:
* 기존 `users``companies` 테이블을 안전하게 확장하기 위한 `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` 구문을 사용하여 무중단 증분 마이그레이션이 가능하게 설계되었습니다.
* 신규 보안 필수 테이블인 `sessions`, `email_otps`, `trusted_devices`, `join_requests`, `login_logs`, `activity_logs`, `system_admin_logs`, `support_requests`, `user_consents`가 MariaDB 규격에 맞추어 인덱스 및 외래키 제약조건(`ON DELETE CASCADE` 등)과 함께 깔끔하게 생성되었습니다.
### 2. 세션 및 쿠키 라이프사이클 검증
* **검증 대상**: `common_util/common_util_auth.py`, `A06_Login/A06_Login_Router.py`
* **분석**:
* **보안 쿠키 설정**: `set_session_cookie`에서 `HttpOnly=True`, `SameSite="lax"`, `Path="/"`, `Secure=True` (설정에 따라 분기)를 완벽하게 지정하여 클라이언트 측 자바스크립트의 접근을 막아 XSS 공격을 무력화합니다.
* **이중 만료 메커니즘**: `verify_session` 미들웨어에서 절대 시간(12시간) 및 비활동 시간(4시간)을 정확하게 체크하고 있습니다.
* 만료된 세션 혹은 비활성(ACTIVE가 아닌) 상태의 사용자는 DB 세션 테이블에서 즉시 삭제 후 `401 Unauthorized`를 발생시킵니다.
* 활성 세션에 대해서는 요청 시마다 `last_activity_at = CURRENT_TIMESTAMP`로 업데이트를 수행하여 지속적인 세션을 유지합니다.
### 3. 무차별 대입 공격 및 계정 잠금 검증
* **검증 대상**: `A06_Login/A06_Login_Router.py`
* **분석**:
* 로그인 시 비밀번호 불일치 시 `users.login_failures`를 증가시키며, 5회 이상 실패 시 `account_locked_until`에 현재 시점 기준 15분 후의 타임스탬프를 부여합니다.
* 계정이 잠긴 동안에는 올바른 비밀번호를 입력하더라도 로그인 요청이 거부됩니다.
* 5회 실패 임계치 도달 시, 시스템 관리자 메일(`ADMIN_EMAIL`)로 경고 이메일(`security_alert_email`)이 즉각 발송됩니다.
### 4. 비동기 이메일 발송 및 재시도 메커니즘 검증
* **검증 대상**: `common_util/common_util_email.py`
* **분석**:
* 동기형 `smtplib` 모듈의 블로킹 현상을 방지하기 위해 `asyncio.to_thread`를 사용하여 별도의 워커 스레드 풀에서 이메일 발송 작업을 실행시킵니다.
* API 응답 시간을 단축하기 위해 `send_email_background` 함수를 활용해 백그라운드 태스크(`asyncio.create_task`)로 예약을 잡고 컨트롤을 바로 라우터로 반환합니다.
* 네트워크 순시 장애에 대비하여 지수 백오프(`EMAIL_RETRY_DELAY_SECONDS * (2 ** (attempt - 1))`)를 적용하여 최대 3회 재시도를 구현했습니다.
### 5. 세션 정리 및 리소스 관리 검증
* **검증 대상**: `main.py`
* **분석**:
* FastAPI의 `lifespan` 컨텍스트 매니저 시작부에서 `cleanup_expired_sessions` 백그라운드 루프가 가동됩니다.
* 1시간 간격으로 만료된 세션 레코드, 만료/하루가 지난 OTP 정보, 보존 기한이 초과된 로그인/활동 로그들을 일괄 정리(`DELETE`)하여 DB 인덱스 비대화 및 스토리지 낭비를 사전에 방지합니다.
---
## 🛠️ 조치 완료 사항 (Fixed Items)
식별되었던 2건의 잠재적 리스크 및 취약점이 성공적으로 보완되었습니다.
### 1. DB 세션 타임존 UTC 강제 바인딩 완료 (`config/config_db.py`)
`config/config_db.py` 파일 내 `init_db_pool` 함수에서 `aiomysql.create_pool`을 생성할 때 `init_command="SET time_zone='+00:00'"` 옵션을 적용하여 DB 서버와 백엔드 간의 시간 비교 로직이 UTC 기준으로 완벽히 동기화되었습니다. (세션 강제 만료 이슈 해결)
### 2. OTP 실패 시에도 계정 잠금 및 경고 알림 연동 완료 (`A06_Login/A06_Login_Router.py`)
`A06_Login_Router.py``verify_login` 함수에서 OTP 입력 실패 시에도 `record_failed_login`을 호출하도록 반영되었습니다. 이제 OTP 브루트포스(무차별 대입) 시도 시에도 5회 실패 시 계정이 자동 잠금되며 관리자에게 보안 경고 이메일이 전송됩니다. (2차 인증 무력화 방어 완료)
---
## 💡 최종 결론
이전 검증에서 식별된 **서버-DB 타임존 불일치 문제**와 **OTP 무차별 대입 취약점**에 대한 코드 수정 패치가 완벽하게 적용되었음을 확인하였습니다.
이로써 로그인, 조직 및 세션 보안 관리용 백엔드 코드의 모든 핵심 요구사항이 보안성과 신뢰성을 완전히 갖춘 상태로 검증을 마쳤습니다.
* **작성자**: Antigravity 코딩 검증 에이전트
* **검증 완료일**: 2026-07-07
* **상태**: **PASSED (검증 통과 - 모든 보안 이슈 해결 완료)**
+263
View File
@@ -0,0 +1,263 @@
기업대상으로 saas프로그램을 개발중인데 로그인 /회원가입 기능을 구현하려고해. 여기서 법적이나 기능적으로 신경써야 하는게뭐야?
기업 대상(B2B) SaaS 프로그램은 일반 개인 대상(B2C) 서비스보다 보안, 권한 관리, 규제 준수(Compliance) 기준이 훨씬 엄격합니다. 로그인 및 회원가입 기능 구현 시 반드시 반영해야 할 법적·기능적 핵심 체크리스트를 정리해 드립니다.
1. 법적 고려사항 (Legal & Compliance)
국내법(개인정보 보호법, 정보통신망법) 및 글로벌 표준을 기준으로 아래 사항을 반드시 준수해야 합니다.
필수 약관 및 동의 항목 분리
이용약관과 개인정보 처리방침은 반드시 사용자가 동의를 체크해야 가입이 되도록 설정해야 합니다.
마케팅 목적의 수신 동의(이메일/SMS)는 반드시 '선택' 항목으로 분리해야 하며, 기본 체크 해제 상태여야 합니다. (위반 시 과태료 대상)
개인정보 암호화 및 전송 보안
비밀번호 일방향 암호화: DB에 저장할 때 bcrypt, scrypt, PBKDF2 같은 강력한 해시 알고리즘을 사용해야 합니다. (평문 저장 절대 금지)
전송 구간 암호화: 로그인/회원가입이 일어나는 모든 페이지는 반드시 HTTPS(TLS)가 적용되어야 합니다.
개인정보 최소 수집 원칙
B2B 서비스 특성상 가입 시점에 주민등록번호 등 민감한 식별정보를 수집하면 안 됩니다. 이메일, 이름, 회사명, 부서 등 서비스 제공에 필요한 최소한의 정보만 수집하세요.
기업 대상 보안 인증 (장기적 관점)
향후 대기업이나 공공기관을 고객사로 유치할 경우, ISMS-P(국내)나 ISO 27001(글로벌) 인증을 요구받을 수 있습니다. 초기 설계 때부터 로그인 이력 저장 등 보안 표준을 염두에 두는 것이 좋습니다.
2. 기능적 고려사항 (Functional Requirements)
B2B SaaS의 로그인/회원가입은 '개인'이 아닌 '기업(조직) 단위'의 워크스페이스 구조를 기반으로 설계해야 합니다.
① B2B 특화 회원가입/인증 흐름
워크스페이스(Tenant) 생성 및 초대 구조:
A 회사의 관리자가 먼저 가입하여 '회사 워크스페이스'를 생성하고, 이후 팀원들은 관리자의 이메일 초대 링크를 통해 가입하는 흐름이 기본적입니다.
도메인 기반 자동 가입 (Domain Auto-Join):
@company.com 같은 특정 기업 도메인으로 가입 시, 해당 기업의 워크스페이스로 자동 소속되게 하는 기능입니다. (엔터프라이즈 고객 선호)
② 엔터프라이즈급 보안 기능
기업용 SSO (Single Sign-On) 연동:
대기업 고객은 자신들이 쓰는 사내 계정(Google Workspace, Microsoft Entra ID/Azure AD, Okta 등)으로 로그인하길 원합니다. SAML 2.0이나 OIDC(OpenID Connect) 기반의 SSO 연동 확장이 가능하도록 인증 구조를 설계해야 합니다.
다요소 인증 (MFA / 2FA):
관리자 권한을 가진 계정이나 보안이 중요한 기업을 위해 이메일, SMS, 또는 OTP 앱(Google Authenticator)을 통한 2차 인증 기능을 선택 또는 필수 옵션으로 제공해야 합니다.
동시 로그인 제한 및 세션 만료:
보안을 위해 일정 시간 동안 활동이 없으면 자동으로 로그아웃되는 Session Timeout 기능과, 하나의 계정으로 여러 기기에서 동시 로그인하는 것을 제어하는 기능이 필요합니다.
③ 계정 관리 및 감사 (Audit)
RBAC (Role-Based Access Control):
가입 후 조직 내에서 권한을 분리해야 합니다. (예: 소유자/Owner, 관리자/Admin, 일반 멤버/Member, 청구 관리자/Billing)
감사 로그 (Audit Log):
기업의 보안 담당자는 사용자의 로그인 일시, IP 주소, 기기 정보 등의 로그를 요구합니다. 로그인 성공/실패 이력을 DB나 로그 시스템에 엄격히 기록해두어야 합니다.
무차별 대입 공격(Brute Force) 방지:
로그인 실패가 5회 이상 반복될 경우 계정을 잠그거나, 캡차(reCAPTCHA)를 노출하고 일정 시간 로그인을 차단하는 방어 로직이 필수적입니다.
현재 타겟으로 삼고 계신 고객사의 규모(스타트업/중소기업 vs 대기업·공공기관)나, 다루시는 서비스의 도메인(금융, 의료, 일반 협업툴 등)은 구체적으로 어떻게 되시나요?
법적요소는 제외하고 주 고객은 30인 미만의 기업들이 대상이야. 그래서 조직베이스로가되 먼저 가입자를 대상으로 회사정보를 받고 다음부터는 가져와 사용하는거지. 이때 가입아이디는 회사계정을 사용하여 아이디에활용하고 재직여부를 체크하는거야. 도메인, sso 불필요 다인증 방식은 이메일로, rbac는 불필ㅇ요
30인 미만의 소규모 기업을 타깃으로 하며 SSO, RBAC, 복잡한 도메인 관리 없이 ‘이메일 인증’과 ‘회사 단위(Organization)의 간결함’에 집중한 구조군요.
이 조건에 맞춰 대기업용 스펙을 걷어내고, 가장 빠르고 효율적으로 구현할 수 있는 기능적 핵심 설계 포인트를 정리해 드립니다.
1. 회원가입 및 조직 연동 흐름 (기능 설계)
"첫 가입자가 회사 정보를 입력하고, 다음 사람부터는 이를 재사용한다"는 요구사항을 구현할 때, 복잡한 도메인 파싱 로직 없이 처리하는 가장 확실한 방법은 [초대 번호/회사 코드] 또는 [도메인 텍스트 매칭]입니다.
최초 가입자 (마스터 유저):
이메일(예: gildong@hong.com)로 가입 신청.
이메일로 발송된 6자리 인증코드 입력 (이메일 MFA 겸용).
인증 성공 후, 회사 정보 입력 폼 작성 (회사명, 사업자번호 등).
완료 시 DB에 Company 레코드가 생성되고, 해당 회사의 고유 '회사 코드(예: HONG-1234)'가 발급됨.
이후 가입자 (팀원):
이메일(예: chulsoo@hong.com)로 가입 신청 및 이메일 인증.
"기존 회사에 참여하기" 단계에서 먼저 가입한 동료에게 받은 '회사 코드'를 입력.
코드가 일치하면 해당 회사 정보(회사명 등)를 화면에 보여주며 확인을 받고, Company ID를 유저 계정에 바인딩.
💡 도메인 로직을 최소한으로 쓴다면?
별도의 엔터프라이즈 도메인 기능은 없더라도, 이후 가입자가 코드를 입력했을 때 "최초 가입자의 이메일 도메인(@hong.com)과 현재 가입자의 도메인이 일치하는지" 백엔드에서 체크하는 방어 로직 하나만 넣어두면 잘못된 코드로 남의 회사에 들어가는 것을 쉽게 막을 수 있습니다.
2. '재직 여부 체크' 및 '이메일 다인증(MFA)' 구현 전략
30인 미만 기업에서 인사 시스템(HRIS)이나 Active Directory 연동 없이 재직 여부를 확인할 수 있는 가장 완벽하고 비용이 적게 드는 방법은 "회사 이메일 계정의 소유권 지속 확인"입니다. 퇴사자는 회사 이메일이 가장 먼저 차단되기 때문입니다.
이메일 OTP 기반의 로그인 (MFA 겸용):
아이디/비밀번호 로그인 외에, 로그인할 때마다 또는 주기적(예: 30일마다)으로 등록된 회사 이메일로 6자리 인증코드를 보내 확인하게 합니다.
효과: 사용자가 퇴사하여 회사 이메일 계정이 정지되거나 삭제되면, 이 인증코드를 받을 수 없으므로 자연스럽게 서비스 접근이 차단(재직 체크 완료)됩니다.
세션 만료 기간 단축:
B2B 서비스이므로 "로그인 유지하기"를 선택하더라도 최대 세션 유지 기간을 2주~1달 수준으로 제한하는 것이 좋습니다. 세션이 만료되어 재로그인할 때 이메일 인증을 다시 거치게 하면 재직 여부가 자동으로 걸러집니다.
3. RBAC가 없는 단순화된 데이터베이스(DB) 구조
RBAC(역할 기반 권한 제어)가 필요 없으므로 DB 구조를 극도로 단순화할 수 있습니다. 1:N(회사:유저) 관계만 정의하면 됩니다.
[Company 테이블] [User 테이블]
----------------- ---------------
- id (PK) - id (PK)
- company_name - email (ID로 사용)
- company_code (고유 코드) - password_hash
- created_at - company_id (FK -> Company.id)
- is_master (최초 가입자 여부, 정산/수정용)
- created_at
권한 제어의 단순화:
모든 유저는 기본적으로 동일한 기능 권한을 가집니다.
다만, 회사 정보 수정이나 결제(구독) 관리 기능을 위해 is_master (또는 is_admin) 라는 Boolean 플래그 하나만 유저 테이블에 둡니다. 최초 가입자만 이 값을 true로 가집니다.
4. 실무 개발 시 놓치기 쉬운 디테일 (체크리스트)
동일 도메인 프리랜서/아웃소싱 계정 예외 처리:
간혹 @gmail.com이나 @naver.com 같은 공용 이메일로 가입하려는 사용자가 있습니다. 소기업일수록 공용 메일을 회사 계정처럼 쓰는 경우가 많으므로, 공용 도메인으로 가입 시에는 "회사 코드가 일치해도 도메인 일치성 체크를 건너뛰는" 예외 예외 로직을 고려해야 합니다.
이메일 발송 인프라(SMTP) 신뢰도:
로그인 및 재직 확인이 '이메일 인증코드'에 의존하기 때문에, 인증 메일이 스팸함으로 가거나 발송이 지연되면 서비스 전체 사용성이 마비됩니다.
AWS SES, Stibee, Solapi 등 인증 메일 발송 성공률이 높고 속도가 빠른 전문 SaaS 인프라를 초기부터 연동해 사용하는 것을 강력히 권장합니다.
동시 로그인 차단 (선택사항):
30인 미만 기업 환경에서는 계정 하나를 생성해 온 직원이 돌려쓰는 편법을 쓰기 쉽습니다. 이를 방지하려면 새로운 기기에서 로그인 시 기존 기기의 세션을 끊어버리는(만료시키는) 로직을 넣어두는 것이 비즈니스 모델(ID당 과금 등) 유지에 유리합니다.
소규모 업장이라 도메인이 아닐수도 있어
아, 아주 중요한 지적입니다! 맞습니다. 30인 미만 소규모 업장이나 초기 스타트업, 자영업 형태의 기업들은 전용 도메인(@company.com) 없이 @gmail.com, @naver.com 같은 공용 이메일을 회사 업무용으로 그대로 쓰는 경우가 허다합니다.
공용 이메일을 쓴다면 기존에 도메인 기반으로 필터링하려던 로직을 완전히 뒤바꿔야 보안 사고(예: 남의 회사 데이터에 접근하는 일)를 막을 수 있습니다. 이 환경에 맞춘 현실적인 로그인/회원가입 기능 수정안을 제안해 드립니다.
1. 회사 연결 방식: '초대 코드' + '마스터 승인제'
도메인이 일치하는지 확인할 수 없기 때문에, 두 번째 가입자부터는 100% 철저하게 사람이 확인하는 구조로 가야 합니다.
1단계: 초대 코드(또는 링크) 입력
먼저 가입한 마스터가 발급받은 '회사 고유 코드'를 입력하고 가입을 진행합니다.
2단계: '승인 대기' 상태 진입 (핵심)
코드가 맞다고 해서 바로 회사 워크스페이스에 접속시키면 안 됩니다. (코드가 유출되거나 무작위 대입으로 뚫릴 위험 존재)
팀원이 가입 버튼을 누르면 화면에 "관리자의 가입 승인을 기다리고 있습니다"라는 안내를 띄우고 진입을 차단합니다.
3단계: 마스터가 최종 승인
최초 가입자(마스터)에게 "홍길동(gildong@gmail.com)님이 가입을 요청했습니다"라는 알림을 주고, 마스터가 화면에서 [승인]을 눌러야만 최종적으로 해당 회사 멤버로 묶이게 합니다.
2. 재직 여부 체크 방식의 변화: '멤버 내보내기' 기능 필수
전용 도메인을 쓸 때는 퇴사자의 메일 계정 자체가 회사에서 삭제되므로 이메일 OTP로 재직 확인이 자동화되었지만, 개인용 @gmail.com을 업무용으로 쓰던 직원이 퇴사하면 퇴사 후에도 그 메일 주소는 살아있습니다.
따라서 이메일 인증만으로는 퇴사자 차단이 불가능하므로, 시스템적으로 아래 두 가지 장치가 꼭 필요합니다.
마스터 전용 '멤버 관리(강제 퇴장)' 기능
RBAC(복잡한 권한)는 필요 없지만, 마스터 유저 화면에 [우리 회사 멤버 목록]을 볼 수 있는 심플한 페이지가 하나 있어야 합니다.
직원이 퇴사하면 사장님(마스터)이 목록에서 [내보내기] 또는 [비활성화] 버튼을 누르는 방식입니다. 이 버튼을 누르면 해당 유저의 company_id를 끊어버리거나 계정 상태를 inactive로 변경하여 즉시 로그아웃 및 접근 차단을 시킵니다.
로그인 세션 만료 알림
퇴사자가 한 달 전에 로그인해 둔 세션으로 계속 접속하는 것을 막기 위해, 로그인 유지 기간이 지나 세션이 만료될 때 "보안을 위해 다시 로그인해 주세요"라는 안내와 함께 튕겨내야 합니다. (이때 이미 내보내기 처리된 유저라면 로그인이 막힙니다.)
3. 회원가입 화면(UI/UX)에서의 팁
공용 이메일을 쓰는 소규모 업장 타깃이라면, 가입 단계에서 유저가 헷갈리지 않게 명확한 가이드를 주는 것이 가입 이탈률을 줄이는 데 좋습니다.
"회사에서 업무용으로 사용하는 이메일을 입력해 주세요." 라는 문구를 인풋창 위에 배치합니다.
가입 화면 처음에 두 가지 버튼을 명확히 찢어놓는 것이 좋습니다.
[우리 회사 최초 가입자 (방 만들기)] → 회사 정보 입력 폼으로 이동
[동료에게 초대를 받으셨나요? (참여하기)] → 초대 코드 입력 창으로 이동
이렇게 설계하면 공용 도메인을 쓰더라도 보안을 챙기면서, 30인 미만 업장 사장님과 직원들이 아주 직관적으로 서비스를 시작할 수 있습니다.
회사마나 공용계정을 1-5개를 운영한다면
소규모 업장(예: 자영업, 프랜차이즈 매장, 소형 대리점 등)에서는 비용 절감이나 인수인계의 편의를 위해 shop1@gmail.com, sales_team@naver.com 같은 공용 계정 하나를 여러 직원이 공유해서 쓰는 경우가 실제로 아주 많습니다.
이 조건이 추가되면, 앞서 이야기한 '1인 1계정' 기준의 설계(이메일 OTP를 통한 자동 재직 확인 등)를 그대로 쓸 수 없습니다. 공용 계정 환경에 맞춰 인증 및 보안 로직을 어떻게 수정해야 하는지 핵심 포인트를 정리해 드립니다.
1. 로그인 정책의 대전환: '동시 로그인 허용'
이전 답변에서는 계정 공유를 막기 위해 동시 로그인을 차단하라고 말씀드렸지만, 공용 계정이 전제라면 동시 로그인을 무조건 허용해야 합니다.
문제 상황: 매장 PC에서 '공용 계정'으로 로그인해서 쓰고 있는데, 외근 나간 직원이 모바일로 같은 계정에 로그인하는 순간 매장 PC가 로그아웃되면 업무가 마비됩니다.
해결책: 동일 계정으로 여러 기기(PC, 태블릿, 모바일 등)에서 세션이 동시에 유지되도록 백엔드 설계를 개방해야 합니다.
2. 이메일 인증(MFA)의 딜레마 해결: '기기 신뢰(Trust Device)'
공용 계정은 비밀번호를 공유하지만, 해당 이메일의 실제 수신 권한은 사장님이나 매니저 한 명만 가지고 있는 경우가 많습니다. 따라서 로그인할 때마다 이메일 인증코드를 요구하면 교대 근무자나 아르바이트생은 로그인을 못 하는 대참사가 일어납니다.
해결책 (브라우저/기기 기억하기):
최초 가입 및 새로운 기기에서 처음 로그인할 때만 이메일 인증(MFA)을 거치게 합니다.
인증 성공 시 "이 브라우저/기기를 30일간 기억하기" 옵션을 제공합니다.
쿠키나 로컬 스토리지를 활용해 해당 기기에서는 한 달 동안 이메일 2차 인증 없이 ID/비밀번호만으로 바로 로그인할 수 있게 장벽을 낮춰주어야 합니다.
3. 공용 계정 환경에서 '퇴사자 차단(재직 체크)' 방법
공용 계정의 가장 큰 취약점은 퇴사한 직원이 집에서 매장 비밀번호로 접속해 데이터를 보거나 조작할 수 있다는 점입니다. 이메일이 개인 소유가 아니므로 이메일 차단만으로는 해결되지 않습니다. 이를 막기 위해 다음 2가지 기능이 필수적입니다.
방법 A: 사무실/매장 IP 제한 (가장 추천)
소규모 업장은 대개 정해진 공간(사무실, 매장)에서만 업무를 봅니다.
마스터 설정 페이지에 "우리 매장 IP(또는 와이파이 환경)에서만 로그인 허용" 옵션을 넣는 것입니다. 이렇게 하면 퇴사자가 비밀번호를 알아도 집에서는 로그인이 불가능합니다.
방법 B: 비밀번호 변경 시 '모든 기기 강제 로그아웃'
직원이 퇴사하면 사장님은 공용 계정의 비밀번호를 바꿀 것입니다.
이때 비밀번호만 바뀌고 기존 기기들의 세션이 살아있으면 퇴사자 PC에서 계속 접속이 유지됩니다.
따라서 마스터가 비밀번호를 변경할 때 "기존에 로그인된 모든 기기 로그아웃(세션 만료)" 처리가 백엔드에서 동시에 일어나도록 구현해야 합니다. 그래야 출근한 직원들이 새 비밀번호로 다시 로그인하면서 퇴사자만 자연스럽게 튕겨 나갑니다.
4. 데이터 관리를 위한 팁: '심플 프로필' (선택 사항)
RBAC(권한 분리)는 필요 없더라도, "누가 이 주문을 처리했지?", "누가 이 데이터를 지웠지?" 같은 최소한의 추적은 필요할 수 있습니다. 넷플릭스나 당근마켓 비즈니스 계정 같은 방식을 차용하면 소규모 업장에서 매우 유용하게 씁니다.
공용 계정(shop1@gmail.com)으로 로그인하면 바로 서비스 화면이 나오는 것이 아니라, "현재 사용하는 직원을 선택해 주세요"라는 단순 닉네임 선택 창을 띄우는 것입니다. (예: 매니저, 오전알바, 오후알바)
비밀번호는 공유하지만, 액션 로그(Audit Log)에는 선택된 닉네임이 남기 때문에 30인 미만 기업 내에서 책임 소지를 명확히 할 수 있어 사장님들이 매우 선호하는 구조입니다.
개인별로 계정을 만드는게 맞겠다..
현명한 결정이십니다! 아무리 소규모 업장이라도 공용 계정을 허용하는 순간, 나중에 데이터가 꼬이거나 보안 사고가 터졌을 때 책임 소재를 가리기가 불가능해집니다. 개발자 입장에서도 개인별 계정으로 가는 것이 예외 로직을 줄이고 시스템을 안정적으로 가져가는 가장 좋은 길입니다.
개인별 계정(공용 이메일 허용)으로 방향을 굳히셨으니, 가장 담백하고 직관적인 가입/인증 흐름을 최종 정리해 드립니다.
🛠️ 개인별 계정 기반의 최종 인증 프로세스
1. 회원가입 및 조직 연결 (초대 코드 + 마스터 승인)
도메인(@company.com)이 없어도 안심하고 유저를 묶을 수 있는 가장 확실한 방법입니다.
마스터(첫 가입자): 가입 후 회사 정보를 입력하고, 우리 회사 고유의 '초대 코드(예: ABC-789)'를 발급받아 직원들에게 공유합니다.
팀원(이후 가입자): 각자 개인 이메일(@gmail.com 등)로 가입한 뒤, 초대 코드를 입력합니다.
승인 대기: 코드가 맞더라도 즉시 접속되지 않고 [승인 대기] 상태가 됩니다. 마스터가 관리자 화면에서 "어, 이 메일 주소 우리 직원 맞네" 하고 [승인]을 누르면 가입이 최종 완료됩니다.
2. 이메일 인증 (MFA) 및 재직 체크
개인 계정이므로 이메일 인증의 효율이 극대화됩니다.
로그인할 때마다, 혹은 보안상 안전하게 2~3주에 한 번씩 등록된 개인 이메일로 6자리 OTP 인증코드를 발송해 입력하게 합니다.
직원이 퇴사하면 마스터(사장님)가 멤버 목록에서 [내보내기] 버튼을 누릅니다. 백엔드에서는 해당 유저의 세션을 즉시 만료시키고 회사 ID(company_id) 연동을 끊어버립니다. 이제 그 직원은 자기 이메일로 로그인해도 우리 회사 데이터에 접근할 수 없습니다.
+23 -3
View File
@@ -25,6 +25,10 @@ my-project/
│ └── 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 원자적 저장
@@ -32,7 +36,8 @@ my-project/
│ └── common_util_format.py
├── db_management/ # DB 관리 폴더 (PostgreSQL 마이그레이션 및 스키마 관리)
│ ├── schema.sql
── migration_v1.py
── migration_v1.py
│ └── 002_auth_security.sql # 기존 DB 로그인·보안 증분 마이그레이션
├── resources/ # 웹앱 UI 구성 리소스 (사진, 회사 로고 등 에셋)
│ ├── logo.png
│ └── banner.jpg
@@ -72,13 +77,28 @@ my-project/
│ └── A05_EduDetail_UI_Style.css
├── A06_Login/ # 로그인 전 06: 로그인 (이메일 + 비밀번호 폼)
│ ├── A06_Login_UI_Page.ts
── A06_Login_UI_Style.css
── 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_Style.css
── 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_AccountDetail/ # 로그인 후 01: 계정 상세 (기본정보 수정 + 비밀번호 변경)
-35
View File
@@ -1,35 +0,0 @@
---
document_type: entrypoint
priority: CRITICAL
version: 2.0
---
# Aislo (아이슬로) 프로젝트 지식 베이스 진입점
이 문서는 AI 코딩 에이전트(Codex, Claude, Antigravity 등)가 프로젝트의 방대한 정보 중 **현재 필요한 컨텍스트를 정확하게 로드**하기 위한 인덱스입니다. 임의로 구조를 추론하지 말고, 작업할 영역에 맞춰 아래 연결된 상세 명세서를 반드시 읽고 코딩을 진행하십시오.
## 🧭 컨텍스트 라우팅 (필요한 문서를 로드하는 방법)
작업 성격에 따라 다음 번호의 문서를 확인하십시오.
모든 문서는 이 폴더(`.agent/test/`) 하위에 존재합니다.
1. **[구조/아키텍처 파악]** `01_Architecture_and_Structure.md`
- **언제 읽는가:** 새로운 페이지를 추가하거나, 파일(프론트/백엔드)을 생성/수정해야 할 때.
- **내용:** A00~B11까지의 전체 디렉토리 트리, 파일 명명 규칙(수직 통합).
2. **[DB 연동 및 쿼리 작성]** `02_Database_Schema.md`
- **언제 읽는가:** DB 테이블 조회, 쿼리문 작성, 관계(FK) 및 데이터 저장이 필요할 때.
- **내용:** 18개 전체 테이블 스키마, 핵심 쿼리 예제, 공간 데이터(JSON) 설계 원칙.
3. **[백엔드 로직/저장소 제어]** `03_Backend_Logic_and_Storage.md`
- **언제 읽는가:** FastAPI 라우터, 비즈니스 엔진 로직, 영구저장소(물리 파일) 입출력을 다룰 때.
- **내용:** Raw SQL 제약, 워크플로우 6단계 폴더 매핑, `workflow.json` 기반의 Stale 상태 동기화 기법.
4. **[프론트엔드 UI/디자인 구현]** `04_Frontend_and_Design.md`
- **언제 읽는가:** TypeScript 이벤트 핸들러, 화면 레이아웃, 다국어 처리, CSS 스타일링을 수행할 때.
- **내용:** 3단 레이아웃 제약, 다국어(`ui_locales`) 제약, Wiza 테마(Britti Sans, 색상 토큰) 및 프롬프트 가이드.
---
## 🛑 범용 제약 사항 (절대 준수)
1. **700줄 제한:** 단일 파일 코드 작성/수정 시 700줄이 넘으면 기능별 분할 필수.
2. **코드 포맷팅:** 작성 후 반드시 포맷터 적용 (Python: `ruff`, TS/CSS: `prettier`).
3. **인코딩:** 모든 파일은 UTF-8.
4. **DB 원칙:** `aiomysql` 기반 비동기 통신. **ORM 사용은 절대 금지**하며 무조건 Raw SQL 사용.
5. **다국어 규칙:** 프론트엔드 내 UI 텍스트 하드코딩 금지. 반드시 다국어 인덱스 배열 방식 준수.
@@ -1,65 +0,0 @@
---
document_type: architecture
priority: HIGH
---
# 프로젝트 구조 및 아키텍처 명세
## 1. 아키텍처 기본 원칙
* **수직 통합:** 백엔드 로직과 프론트엔드 코드는 페이지 폴더(`A01_`, `B01_` 등)별로 격리. 거대 단일 모듈을 금지하고 기능별로 파일을 분할한다.
* **루트 main.py:** FastAPI 진입점으로, 라우터 등록(`include_router`)만 수행. 비즈니스 로직 작성 금지.
* **명명 규칙:**
- 백엔드: `[폴더명]_Router.py`, `[폴더명]_Engine.py`, `[폴더명]_Schema.py`, `[폴더명]_Repository.py`
- 프론트엔드: `[폴더명]_UI_Page.ts`, `[폴더명]_UI_Style.css`, `[폴더명]_Api_Fetch.ts`
- 로그인 전은 `A01_`, 로그인 후는 `B01_`, 워크플로우는 `B04_wf1_` 형태 적용.
- 공통 유틸 코드는 `common_util_[기능].확장자`
## 2. 전체 디렉토리 트리 상세
```text
my-project/
├── A00_Common/ # 프론트엔드 공통 인프라
│ ├── main.ts # 앱 진입점
│ ├── router.ts # 해시 기반 SPA 라우터
│ └── app_shell.ts # 공통 헤더/푸터
├── common_util/ # 공통 유틸리티
│ ├── common_util_validate.ts # 프론트 유효성 검사
│ ├── common_util_storage.py # 저장 경로 로직
│ └── common_util_workflow.py # 상태 동기화 로직
├── config/ # 환경 설정
│ ├── config_system.py # 시스템 파라미터 (제어 변수 중앙화)
│ └── config_db.py # DB 설정
├── db_management/ # 스키마 및 마이그레이션
├── resources/ # 정적 리소스 (에셋)
├── storage/ # 물리 파일 영구 저장소 (DB와 매핑)
├── templates/ # 보고서/도면 양식
├── ui_template/ # 공통 UI 컴포넌트, 테마, 다국어
│ ├── ui_template_theme.css
│ ├── ui_template_locale.ts
│ └── ui_template_elements.ts
# --- 로그인 전 페이지 (A 그룹) ---
├── A01_Home/ # 홈 (소개, 소식)
├── A02_ProgDetail/ # 프로그램 상세 (6단계 워크플로우 설명)
├── A03_CompDetail/ # 회사 상세 (미션 등)
├── A04_NewsHistory/ # 최신소식 및 개선 이력
├── A05_EduDetail/ # 교육 상세
├── A06_Login/ # 로그인 (이메일/비밀번호)
├── A07_Register/ # 회원가입
├── A08_Support/ # 기술지원 요청
# --- 로그인 후 페이지 (B 그룹) ---
├── B01_AccountDetail/ # 계정 관리
├── B02_ProjRegister/ # 신규 프로젝트 등록
├── B03_FileInput/ # [WF0] 원본 파일 입력 (다중 파일 업로드)
├── B04_wf1_Surface/ # [WF1] 지표면 모델 분석 (LAS 처리)
├── B05_wf2_Route/ # [WF2] 경로 설계 (최적 경로 계산)
├── B06_wf3_ProfileCross/ # [WF3] 종횡단 생성 (단면 추출)
├── B07_wf4_DesignDetail/ # [WF4] 상세 설계 (구조물 배치)
├── B08_wf5_Quantity/ # [WF5] 수량 산출 (물량 집계)
├── B09_wf6_Estimation/ # [WF6] 견적/최종 산출물 생성
├── B10_Payment/ # 결제 관리
└── B11_Status/ # 상태 출력 페이지
├── main.py # FastAPI 루트
└── package.json, requirements.txt 등
```
-20
View File
@@ -1,20 +0,0 @@
---
document_type: rules
priority: CRITICAL
---
# 프로젝트 기본 규칙 및 제약 (Rules & Constraints)
## 🚨 필수 행동 지침 (Do's)
- **[MUST]** 코드 작성 전 반드시 `02_Architecture_and_Structure.md`의 구조를 확인하고 적절한 위치에 작성한다.
- **[MUST]** 700줄 제한: 단일 파일이 700줄을 초과할 경우 사전 공지하고 기능별로 파일을 분할한다.
- **[MUST]** 인코딩은 반드시 **UTF-8** 표준으로 통일한다.
- **[MUST]** 코드 작성이 완료되면 다음 포맷터를 실행한다.
- Python: `ruff format [파일명]`, `ruff check --fix [파일명]`
- TypeScript/CSS: `npx prettier --write [파일명]`
- **[MUST]** 외부 라이브러리 추가 시 프로젝트 호환성을 고려하고 무분별한 사용을 지양한다.
## 🛑 금지 행동 지침 (Don'ts)
- **[DON'T]** 하드코딩 금지: 제어 변수/파라미터는 `config/config_system.py`에서, UI 문자열은 `ui_template/ui_template_locale.ts`에서 가져와야 한다.
- **[DON'T]** 임의 파일 경로 생성 금지: 모든 파일 입출력 및 저장은 `05_Workflow_and_Storage.md`에 정의된 6단계 워크플로우 폴더 경로를 준수해야 한다.
- **[DON'T]** 백엔드에서 중간 산출물이나 임시 파일을 MariaDB에 직접 바이너리로 저장하지 않는다. 물리 파일로 저장 후 DB에는 경로(프로젝트 상대 경로)만 기록한다.
- **[DON'T]** 삭제 및 덮어쓰기 금지: 다른 단계의 산출물을 직접 삭제하거나 덮어쓰지 않고 stale 상태를 통해 재계산 필요성을 전파한다.
@@ -1,27 +0,0 @@
---
document_type: architecture
priority: HIGH
---
# 파일 구조 및 명명 규칙
## 📂 아키텍처 원칙
- **수직 통합:** 프론트엔드(TypeScript)와 백엔드(Python) 파일을 기능/페이지별로 같은 폴더(`A01_`, `B01_` 등)에 격리하여 관리한다. (단일 거대 모듈 금지)
- **루트 `main.py`:** FastAPI의 진입점. 라우터 등록(`include_router`)만 수행하고 비즈니스 로직은 절대 작성하지 않는다.
## 📝 명명 규칙
- **폴더명:**
- 로그인 전 페이지: `A01_`, `A02_` ...
- 로그인 후 페이지: `B01_`, `B02_` ...
- 워크플로우 페이지: `B04_wf1_`, `B05_wf2_` ...
- 공통/시스템 폴더: `A00_Common`, `common_util`, `config` 등 접두사 없이 영문명 사용.
- **파일명:** `[폴더명(페이지명)]_[기능]_[기타사양].[확장자]` 형태 필수.
- 예시: `B04_wf1_Surface_UI_Page.ts`, `B04_wf1_Surface_Router.py`
- **공통 모듈:** `common_util` 폴더 내에 `common_util_[기능].[확장자]` 형태로 보관.
## 🏗️ 주요 폴더 구조 요약
- `A00_Common/`: 프론트엔드 공통 인프라 (SPA 라우터, 앱 셸 등)
- `common_util/`: 공통 유틸리티 코드 (검증, 스토리지 제어 등)
- `config/`: 시스템 설정, DB 설정, TS/Vite 환경 설정
- `ui_template/`: 공통 UI 테마, 컴포넌트, 다국어 파일 배열
- `storage/`: 파일 영구 저장소 (DB 경로와 연동됨)
- `B03_FileInput/` ~ `B09_wf6_Estimation/`: 핵심 6단계 워크플로우 페이지 및 API
-60
View File
@@ -1,60 +0,0 @@
---
document_type: database
priority: CRITICAL
---
# 데이터베이스 스키마 명세 (MariaDB)
MariaDB v10.6+를 사용하며, 총 18개의 테이블로 구성됩니다. ORM 없이 Raw SQL과 `aiomysql` 드라이버를 사용합니다. **공간 데이터(GEOMETRY)는 지원 한계로 인해 JSONB 또는 TEXT(GeoJSON)로 저장합니다.**
## 테이블 목록 및 컬럼 구조
### 1. 사용자 & 프로젝트 (Core)
* **`users`**: `id`(PK), `email`, `password_hash`, `name`, `position`, `department`, `phone`, `company_id`(FK)
* **`companies`**: `id`(PK), `name`, `business_registration_number`, `business_address`, `business_owner`, `business_status`
* **`projects`**: `id`(UUID, PK), `user_id`(FK), `company_id`(FK), `name`, `region`, `road_type`, `status`, `crs_epsg`, `storage_path`
* **`project_versions`**: `id`, `project_id`, `version_num`, `data`(JSONB)
### 2. 파일 & 지표면 데이터 (WF0, WF1)
* **`input_files`**: `id`(PK), `project_id`(FK), `file_type`(las, tif 등), `original_filename`, `raw_file_path`, `crs_epsg`, `metadata`(JSONB)
* **`processed_point_cloud`**: `id`, `input_file_id`, `project_id`, `processed_file_path`, `converted_file_path`, `min_z`, `max_z` 등 통계치
* **`surface_models`**: `id`, `project_id`, `model_type`(dem_grid, tin 등), `model_file_path`, `generation_params`(JSONB)
* **`terrain_layers`**: `id`, `surface_model_id`, `layer_name`, `layer_file_path`(geojson 등)
### 3. 경로 설계 (WF2)
* **`routes`**: `id`(PK), `project_id`, `surface_model_id`, `status`, `geometry`(LineString JSON), `route_data_path`, `constraints`(JSONB)
* **`route_points`**: `id`, `route_id`, `chainage_m`, `geometry`(Point JSON), `elevation_m`
* **`route_statistics`**: `id`, `route_id`, `mean_slope`, `cut_volume_m3`, `fill_volume_m3`
### 4. 종횡단 및 구조물 설계 (WF3, WF4)
* **`longitudinal_sections`**: `id`, `project_id`, `route_id`, `longitudinal_file_path`, `data`(JSONB - chainages, elevations 등 배열)
* **`cross_sections`**: `id`, `route_id`, `chainage_m`, `cross_section_file_path`, `data`(JSONB - left_slope, cut_volume_m3 등)
* **`structures`**: `id`, `cross_section_id`, `structure_type`, `location`(LEFT/RIGHT/CENTER), `length_m`, `geometry`(Polygon JSON), `structure_data_path`
### 5. 수량 산출 및 산출물 (WF5, WF6)
* **`quantity_items`**: `id`, `project_id`, `category`, `item_name`, `quantity_design`, `quantity_actual`, `unit_price`, `quantity_data_path`
* **`outputs`**: `id`, `project_id`, `output_type`, `outputs_directory_path`
* **`output_files`**: `id`, `output_id`, `file_type`, `output_file_path`
### 6. 로그 (Audit)
* **`audit_logs`**, **`change_logs`**
---
## 💡 주요 원칙 (DB vs File System)
- **DB에는 메타데이터와 파일 시스템의 "상대 경로"만 기록합니다.** (예: `input_files.raw_file_path = "B03_FileInput/input/las/cloud.las"`)
- 대용량 파일(LAS, TIF, GeoJSON)은 MariaDB에 Blob으로 저장하지 않습니다.
## 📝 쿼리 작성 예제 (Raw SQL & aiomysql)
```python
# 쿼리에는 ORM이 아닌 %s 플레이스홀더를 사용해야 함
async def get_project_route(pool, project_id: str):
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute("""
SELECT r.id, r.total_length_m, r.route_data_path
FROM routes r
WHERE r.project_id = %s
ORDER BY r.computed_at DESC LIMIT 1
""", (project_id,))
return await cur.fetchone()
```
@@ -1,44 +0,0 @@
---
document_type: backend
priority: HIGH
---
# 백엔드 제어 및 저장소/워크플로우 명세
## 1. 물리 파일 및 저장소 구조 (Workflow Mapping)
프로젝트 저장소 내부는 워크플로우 단계와 1:1로 매핑됩니다. 백엔드에서 파일을 다룰 때는 DB에 직접 Blob을 저장하지 않고 이 구조를 따라 저장 후 DB에 상대 경로만 기록합니다.
```text
storage/{company_slug}/{user_slug}/{project_id}/
├── B03_FileInput/
│ └── input/ (원본 업로드 파일들: .las, .tif)
├── B04_wf1_Surface/
│ ├── processed/ (변환된 데이터: cloud_filtered.las 등)
│ └── models/ (DEM, TIN 결과물: dem_2m.tif, layer.geojson)
├── B05_wf2_Route/
│ └── route/ (최적 경로: route_main.geojson)
├── B06_wf3_ProfileCross/
│ ├── longitudinal/ (종단면: longitudinal.json)
│ └── cross_sections/ (횡단면 배열: cross_0020m.json 등)
├── B07_wf4_DesignDetail/
│ └── structures/ (구조물 객체: struct_0020m_001.json)
├── B08_wf5_Quantity/
│ └── quantities/ (수량 집계: items.json)
└── B09_wf6_Estimation/
└── v1/, v2/ (버전별 최종 엑셀, PDF, DXF)
```
**규칙:** 페이지별 백엔드 모듈은 자기 단계의 폴더만 제어합니다. 하위 단계의 파일을 강제로 지우거나 덮어쓰지 않고 `stale_from` 상태를 통해 제어합니다.
## 2. 워크플로우 상태 동기화 (workflow.json)
다중 브라우저 동시 작업을 지원하기 위해 서버 측 상태 파일(`workflow.json`)을 원자적(Atomic)으로 관리합니다.
* **Stale (상태 무효화) 발생:**
상위 단계(예: 지표면 분석 B04)가 재계산되면, 그에 의존하는 하위 단계(경로 B05, 횡단 B06 등) 결과는 신뢰할 수 없으므로 `workflow.json``stale_from: "route"` 형태로 마킹됩니다.
* **상태 업데이트 함수 (예시):**
```python
def _patch_workflow_stale(project_id: str, stale_from: str | None) -> None:
# workflow.json의 stale_from 필드만 원자적 업데이트
```
## 3. 클라이언트 폴링 (Polling)
- 프론트는 3초 주기로 `GET /api/projects/{project_id}/workflow` 를 폴링합니다.
- 서버가 반환하는 `stale_from` 값을 확인하여, 만약 현재 단계와 다르다면 UI에서 사용자에게 "결과가 변경되었습니다. 재계산하시겠습니까?"를 경고합니다.
-23
View File
@@ -1,23 +0,0 @@
---
document_type: backend
priority: HIGH
---
# 백엔드 & 데이터베이스 제어 명세
## 🐍 백엔드 (FastAPI / Python)
- **파일 분할:** 라우터(`_Router.py`), 비즈니스 로직(`_Engine.py`), 스키마(`_Schema.py`), DB 접근(`_Repository.py`) 형태로 세분화.
- **데이터 검증:** 모든 JSON 요청은 Pydantic 모델을 통해 라우터 진입 전 타입/범위 필수 검증.
- **예외 처리:** 지형/메쉬 연산 등 복잡한 로직 수행 시 `try-except` 필수 처리. 에러 발생 시 `{"status": "error", "message": "원인"}` 반환 표준 포맷 유지.
## 🗄️ 데이터베이스 (MariaDB)
- **[MUST]** 비동기 통신: `aiomysql` 드라이버 사용 강제. (`asyncmy` 사용 불가)
- **[DON'T]** ORM 사용 금지: 오직 **Raw SQL**만 작성한다.
- **[MUST]** 파라미터 바인딩: `%s` 플레이스홀더를 사용하여 SQL Injection 방지.
- **트랜잭션:** `pool.acquire()`로 커넥션 확보 후 다건 쓰기는 `connection.begin()``commit()` / 예외 시 `rollback()` 필수.
- **자동 증가 ID:** `INSERT``cursor.lastrowid`로 조회.
- **공간 데이터 처리:** MariaDB의 GEOMETRY 타입 한계로 인해, 공간 기하 데이터(좌표, 다각형, 경로 등)는 JSON (GeoJSON 형식)으로 저장 후 애플리케이션(Python Shapely/Geopandas)에서 파싱하여 기하 연산을 처리한다.
## 🔄 워크플로우 상태 동기화 (Sync)
- **공유 상태:** 다중 브라우저 대응을 위해 각 프로젝트는 `workflow.json`으로 현재 진행 단계와 'stale(무효화)' 상태를 관리한다.
- **Stale 전파:** 상위 단계(예: 지표면 분석)가 재실행되면 하위 단계들은 자동으로 `stale_from`으로 표기되며, 클라이언트는 3초 주기의 폴링(Polling)을 통해 상태를 확인하고 경고/재계산을 유도한다.
- **원자적 쓰기:** `workflow.json` 상태 업데이트는 `common_util_workflow.py`를 통해 원자적 업데이트로 보호되어야 한다.
-47
View File
@@ -1,47 +0,0 @@
---
document_type: frontend
priority: HIGH
---
# 프론트엔드 제어 및 Wiza 디자인 시스템
## 1. Wiza 디자인 시스템 (Theme Constraints)
프론트엔드 UI는 Wiza 브랜드 가이드라인에 따라 구현해야 합니다. 하드코딩된 색상이나 수치를 금지하며, 제공되는 `ui_template_theme.css` 토큰을 사용합니다.
* **색상 토큰 (Monochromatic Violet):**
- `--color-deep-iris` (#26114a): 헤드라인, 메인 버튼 배경 (강조)
- `--color-plum-velvet` (#312749): 네비게이션, 2차 헤딩
- `--color-royal-amethyst` (#3e0079): 링크, 포커스 링, 강조 아이콘
- `--color-lavender-wash`: 영웅 영역 및 섹션의 부드러운 그라데이션 배경
- `--color-canvas` (#ffffff): 카드, 입력 폼 등 캔버스 표면
* **타이포그래피:**
- **Display/Heading:** `Britti Sans` (대체제: Plus Jakarta Sans), 500 Weight, **Line-height 1.0 (절대 준수)**.
- **Body/UI:** `Inter`, 400/500/700 Weight, 크기는 컴팩트하게(12~16px) 유지.
* **형태 및 간격 (Spacing & Radius):**
- 카드, 버튼, 인풋 폼 등 모든 사각형 컨테이너는 **8px Radius** 강제.
- 뱃지(Badge), 필(Pill), 네비게이션 아이템 등은 **1440px Radius** 강제.
- 요소 간 기본 간격 유닛은 **8px**.
## 2. 레이아웃 제약
워크플로우 페이지(B04~B09)는 반드시 **3단 기본 레이아웃**을 따릅니다.
1. 상단: 페이지 타이틀 및 현재 진행 단계
2. 좌측: 데이터 입력 폼 및 설정 제어 패널 (너비 고정)
3. 우측: WebCAD 도면 뷰어 (Canvas/WebGL) 또는 결과 데이터 그리드 (나머지 화면 너비 전체)
## 3. 다국어 제어 원칙 (i18n)
모든 텍스트는 컴포넌트 내에 직접 작성할 수 없습니다.
```typescript
// 선언부: ui_template/ui_template_locale.ts
export const ui_locales = {
A01_Home_Title: ["반갑습니다", "Welcome"],
B04_wf1_Surface_Btn: ["지표면 분석 실행", "Run Surface Analysis"]
};
// 구현부 호출 예시
<button>{ui_locales.B04_wf1_Surface_Btn[currentLanguageIndex]}</button>
```
## 4. 로직 컨벤션 (TypeScript)
* **이벤트 핸들러:** `on[페이지명]_[기능명]_[액션]` 형태 강제. (예: `onB04_Surface_Calculate_Click`)
* **프론트 유효성 검사:** 백엔드 API 전송 전, 1차적으로 빈 값 및 타입 유효성 검사를 수행하여 피드백합니다.
* **로딩 스피너:** API 요청 시작 전 Overlay 활성화, 통신 성공/실패 시 즉시 해제 로직 필수.
-26
View File
@@ -1,26 +0,0 @@
---
document_type: frontend
priority: HIGH
---
# 프론트엔드 및 UI 제어 명세
## 🎨 테마 및 디자인 규약
- **[DON'T]** 하드코딩된 Hex 색상 코드나 임의의 스타일 직접 입력 금지.
- **[MUST]** 테마(라이트/다크) 전환 호환을 위해 `ui_template/ui_template_theme.css`에 선언된 CSS 변수(`var(--color-...)`)만 참조.
- **공통 레이아웃 (3단 분리 준수):**
1. 상단 영역: 페이지 타이틀 및 진행 상태 (헤더)
2. 좌측 패널: 데이터 입력 폼 및 제어 설정 창 (고정 너비)
3. 우측 영역: WebCAD 도면 뷰어 (Canvas/WebGL) 또는 데이터 그리드 (가변 너비)
## 🌐 다국어 지원 (i18n)
- **[DON'T]** UI 문자열(라벨, 버튼 명, 툴팁, 안내 메시지 등)의 코드 내 하드코딩 절대 금지.
- **[MUST]** 동시 수정 프로토콜:
1. 신규 텍스트 발생 시 `ui_template/ui_template_locale.ts` 최하단에 키(Key)와 번역 배열(`[한국어, 영어]`) 선 등록.
2. 컴포넌트 내에서는 `ui_locales.키값[현재언어인덱스]` 형태로만 호출한다.
3. UI 수정 PR시 다국어 업데이트 내역 누락은 심각한 에러로 취급한다.
## ⚙️ TypeScript 및 로직 제어
- **이벤트 핸들러 명명법:** `on[페이지명]_[기능명]_[액션]` 규칙 준수.
- 예: `onB04_Surface_Calculate_Click`
- **상태 제어 피드백:** API 비동기 호출 시작 시 전체 로딩 스피너(Overlay) 활성화, 통신 완료(성공/에러) 시 반드시 해제 보장.
- **1차 검증:** 백엔드 전송 전 빈 값, 최대 길이, 숫자 범위 검증은 프론트엔드(`common_util_validate.ts`)에서 우선 처리하며, 에러 시 입력창 주변에 명시한다.
-31
View File
@@ -1,31 +0,0 @@
---
document_type: workflow
priority: HIGH
---
# 워크플로우 및 데이터 저장소 명세
## 🔄 6단계 워크플로우 (6-Stage Workflow)
설계의 순차적 흐름을 정의하며, DB 및 폴더 구조와 1:1 맵핑됩니다.
- **WF0 (B03): 파일 입력** - 사용자 원본 파일 (LAS, TIF, DXF 등) 업로드
- **WF1 (B04): 지표면 분석** - 포인트클라우드 포맷 변환 및 지표면 모델(DEM/TIN) 생성
- **WF2 (B05): 경로 설계** - 제약조건 반영 최적 선형 도출 (GeoJSON)
- **WF3 (B06): 종횡단 생성** - 종단면도, 횡단면도 지형/설계 데이터 생성
- **WF4 (B07): 상세 설계** - 횡단면별 구조물(옹벽, 수로 등) 배치
- **WF5 (B08): 수량 산출** - 구조물 수량 및 토공량 집계
- **WF6 (B09): 견적·문서** - Excel, PDF, DXF 등 최종 산출물 패키지 생성
## 📁 영구 저장소 구조 (파일시스템)
중간 산출물(LAS, TIF, GeoJSON 등) 및 대용량 파일은 DB가 아닌 파일시스템에 저장되며, DB(MariaDB)에는 저장된 파일의 **상대 경로**만 기록합니다. 임의의 위치에 파일을 쓰거나 사용자 입력을 경로에 그대로 결합하지 않습니다.
**저장 규칙 기반 디렉토리 구조:**
`storage/{company_slug}/{user_slug}/{project_id}/` 내부
- `B03_FileInput/input/` : 원본 업로드 파일
- `B04_wf1_Surface/processed/` : 변환 완료된 파일 (ply, 필터링 las 등)
- `B04_wf1_Surface/models/` : 생성된 DEM, TIN 모델
- `B05_wf2_Route/route/` : 주요 경로선 (GeoJSON) 및 제약 정보
- `B06_wf3_ProfileCross/longitudinal/` : 종단면 데이터셋
- `B06_wf3_ProfileCross/cross_sections/` : 구간별 횡단면 데이터셋
- `B07_wf4_DesignDetail/structures/` : 배치된 구조물 메타데이터
- `B08_wf5_Quantity/quantities/` : 산출 물량표 내역
- `B09_wf6_Estimation/v1/` : 완료 버전별 패키징 결과물 (엑셀, 도면)