사요자 대시보드 추가

This commit is contained in:
2026-07-08 18:12:34 +09:00
parent 5da0260be3
commit 3485eeb096
24 changed files with 3461 additions and 229 deletions
+664 -65
View File
@@ -1,94 +1,693 @@
# 회원가입 플로우 개편: 계정 생성과 회사 연결 분리
# B01_Dashboard 설계 및 구현 계획
## Context
**상태:** 기본 설계 완료. 역할별 대시보드 구축 및 회사 연결 기능 미구현
**참고:** 완료된 항목은 `.agent/complete_and_old/plan_register_split_COMPLETED.md` 참조
현재 `A07_Register` 회원가입 폼은 계정 생성과 동시에 회사 생성/연결(신규 회사명 또는 기존 company_id)을 강제로 요구한다. 사용자는 이 결합이 부적절하다고 판단했다 — 회사 관련 정보(사업자등록번호, 전화번호, 주소 등)는 계정을 만든 사람이 로그인한 이후에 "회사 생성 또는 검색·연결" 단계로 넘겨야 자연스럽다. 또한:
---
- 회원가입 폼 필드를 이름/직급/이메일/전화번호/비밀번호/비밀번호 확인으로 재정의 (department는 여기서 제외, 회사 연결 이후 설정)
- 약관/개인정보처리방침/마케팅 동의는 필수 항목이지만 실제 내용이 어디에도 없었다 — 체크박스 옆에 이름을 두고 펼치면 내용이 열리는 UI로 바꾸고, 실제 텍스트는 나중에 쉽게 고칠 수 있도록 리소스 텍스트 파일로 분리
- 이메일 인증은 회원가입 제출 즉시 폼 하단에 OTP 입력 칸이 나타나는 현재 방식을 유지
## 1. B01_Dashboard 개편 사항
목표: 계정 생성(A07) → 이메일 인증 → 로그인 가능(company 없이) → 로그인 후 B01_AccountDetail에서 회사 생성/연결 → 연결 완료 후에만 B02~B11 워크플로우 접근 가능.
### 1.1 페이지명 변경
- **이전:** B01_AccountDetail (계정 상세 페이지)
- **현재:** B01_Dashboard (역할별 대시보드)
- **목적:** 로그인 후 메인 랜딩 페이지로, 역할에 따라 다양한 정보 표시
## 현재 구조 파악 결과
### 1.2 역할별 대시보드 구조
- `users` 테이블: `company_id` nullable, `role` ENUM('SYSTEM_ADMIN','MASTER','MEMBER') 기본 MEMBER, `is_master` BOOLEAN, `status` ENUM('PENDING_EMAIL','PENDING','ACTIVE','INACTIVE','REJECTED')
- `create_registration()` (`common_util/common_util_auth_repository.py:30`): 현재 회원가입 시 계정+회사(or join_request)+user_consents를 한 트랜잭션에서 같이 생성
- `complete_registration()` (`common_util/common_util_auth_repository.py:134`): 이메일 인증 완료 시 MASTER→ACTIVE, MEMBER→PENDING(마스터 승인 대기)로 status 변경
- `verify_session()` (`common_util/common_util_auth.py:74`): 세션 조회 시 `company_id`, `role`, `is_master`, `status`를 함께 반환 — 이미 dependency에서 활용 가능한 형태
- `main.py:236-240`: B03~B06 라우터에 `protected = [Depends(verify_session)]`만 걸려 있음 (company_id 미검사, B07~B11은 라우터 자체가 아직 없음)
- `A00_Common/router.ts`: `PROTECTED_ROUTES` 목록으로 인증 가드만 수행, company 연결 여부는 검사하지 않음
- `A09_Security_Terms.ts` + `A09_Security_UI_Page.ts`: 이미 "보안정책 안내 페이지" 용도로 존재하지만 세션/보안 정책 설명이지 회원가입 약관 동의가 아님 — 이 구조(약관 텍스트 분리 + 언어별 배열)를 참고해 새 리소스를 만든다
- `B01_AccountDetail`: 현재 `UI_Page.ts`, `UI_Style.css`만 있고 백엔드/API 없음 — 여기에 계정 상세 정보 표시 + 회사 연결 섹션을 새로 구현해야 함
- `resources/` 폴더(프로젝트 루트)는 로고/배너 등 에셋용으로 이미 정의돼 있음 (`structure.md:41`) — 약관 텍스트는 이 폴더 하위에 신설 서브폴더로 정리
#### 🟡 **일반 사용자 (USER) 대시보드**
```
┌─────────────────────────────────────────────────────┐
│ AISLO 대시보드 │
├─────────────────────────────────────────────────────┤
│ │
│ 📊 [나의 프로젝트] │
│ • 프로젝트 리스트 (테이블) │
│ • 프로젝트명 | 지역 | 진행도 [워크플로우] | 수정일 │
│ • 클릭 시 해당 단계로 이동 │
│ • [+ 신규 프로젝트] 버튼 │
│ │
│ 🏢 [회사 정보] │
│ • 현재 소속 회사명 │
│ • 팀원 수, 진행 중인 프로젝트 수 │
│ │
│ 📞 [기본정보 & 보안] (섹션 병합) │
│ • 사용자명, 직급, 부서, 전화번호 수정 │
│ • 비밀번호 변경 │
│ │
└─────────────────────────────────────────────────────┘
```
## DB 변경
#### 🟢 **회사 관리자 (ADMIN) 대시보드**
```
┌─────────────────────────────────────────────────────┐
│ AISLO 회사 관리자 대시보드 │
├─────────────────────────────────────────────────────┤
│ │
│ 🏢 [회사 정보] │
│ 회사명: 테스트회사 | 사업자: 123-45-67890 │
│ 주소: 서울시 강남구 | 상태: 활동중 │
│ │
│ 👥 [팀원 관리] │
│ • 소속 사용자 리스트 (테이블) │
│ • 사용자명 | 직급 | 부서 | 역할 | 상태 │
│ • [+ 팀원 추가] (기가입 계정 검색/추가) │
│ • [제거] 버튼 │
│ │
│ 📋 [가입 요청] │
│ • 이 회사에 가입을 신청한 사용자 리스트 │
│ • [승인] [거절] 버튼 │
│ │
│ 📊 [회사 프로젝트] │
│ • 회사의 모든 프로젝트 리스트 (테이블) │
│ • 프로젝트명 | 소유자 | 진행도 | 상태 │
│ │
│ 📞 [기본정보 & 보안] │
│ • 사용자 이름, 직급, 전화번호 수정 │
│ • 비밀번호 변경 │
│ │
└─────────────────────────────────────────────────────┘
```
`db_management/003_register_split.sql` 신규 마이그레이션:
#### 🔴 **시스템 관리자 (SYSTEM_ADMIN) 대시보드**
```
┌────────────────────────────────────────────────────┐
│ AISLO 시스템 관리자 대시보드 │
├────────────────────────────────────────────────────┤
│ │
│ 📊 [리소스 현황] │
│ CPU: 45% | MEM: 62% | DISK: 78% │
│ 활성 사용자: 23 | 활성 프로젝트: 15 │
│ │
│ 🏢 [회사 관리] │
│ • 전체 회사 리스트 (테이블) │
│ • 회사명 | 사업자번호 | 상태 | 사용자 수 │
│ • [+ 회사 추가] 버튼 │
│ │
│ 👤 [사용자 관리] │
│ • 전체 사용자 리스트 (테이블) │
│ • 이메일 | 이름 | 역할 | 회사 | 상태 │
│ • [역할 변경] [회사 할당] [비활성화] 버튼 │
│ │
│ 📝 [가입 요청 관리] │
│ • 회사 참여 대기 중인 요청 (테이블) │
│ • 신청자 | 회사명 | 신청일 | 상태 │
│ • [승인] [거절] 버튼 │
│ │
│ 📊 [시스템 로그] │
│ • 사용자 로그인/로그아웃 이력 (테이블) │
│ • 계정 생성/수정/삭제 이력 │
│ • 필터링: 기간, 사용자, 행동 유형 │
│ │
│ 🖥️ [하드웨어 리소스 모니터링] │
│ • CPU 사용량 그래프 (시간대별) │
│ • 메모리 사용량 그래프 │
│ • 디스크 사용량 그래프 │
│ • 동시 접속자 수 추이 │
│ │
└────────────────────────────────────────────────────┘
```
1. `users.status` ENUM에 `'NO_COMPANY'` 추가 (이메일 인증 완료, 회사 미연결 상태)
2. `users.company_id`, `users.role`, `users.is_master` 는 구조 그대로 유지하되 회원가입 시점에는 전부 NULL/기본값으로 시작
3. 기존 `join_requests` 테이블 그대로 재사용 (MEMBER가 기존 회사 연결 신청 시)
4. `department` 컬럼은 그대로 두되 회원가입 INSERT에서 제외 (NULL 허용이라 문제 없음)
---
## 백엔드 변경
## 2. 핵심 기능 상세 설계
### A07_Register (회원가입 축소)
### 2.1 프로젝트 리스트 섹션 (USER, ADMIN 공통)
- `A07_Register_Schema.py`: `RegisterRequest`에서 `account_type`, `company_id`, `company_name`, `business_number`, `phone_number`(→ 개인 전화번호로 의미 변경), `address`, `representative_name` 제거. 필드: `email`, `password`, `password_confirm`, `name`, `position`, `phone`, `terms_agreed`, `privacy_agreed`, `marketing_agreed`, `terms_version`. `password_confirm == password` 검증을 `model_validator`에 추가.
- `A07_Register_Router.py``request_registration()`: `create_registration()` 호출을 단순화된 데이터로 변경.
- `common_util_auth_repository.py``create_registration()`: 회사/join_request 생성 로직 제거. `users` INSERT만 수행 (`role` 기본값 MEMBER, `company_id` NULL, `is_master` FALSE). `user_consents` INSERT는 유지.
- `complete_registration()`: 이메일 인증 완료 시 무조건 `status = 'NO_COMPANY'`로 변경 (MASTER/MEMBER 분기 제거).
- `A06_Login_Router.py``request_login()`: `user["status"] != "ACTIVE"` 체크를 `status not in ("ACTIVE", "NO_COMPANY")`로 완화 — company 미연결 상태에서도 로그인은 허용.
**테이블 구조:**
| 프로젝트명 | 지역 | 진행도 | 워크플로우 | 수정일 |
|----------|------|--------|----------|--------|
| 2025년 산불진화임도 | 울진군 | 60% | [B03●●●○○○] | 2025-07-08 |
### 회사 생성/연결 API (신규, B01_AccountDetail 백엔드)
**진행도 워크플로우:**
```
[B03 완료] → [B04 진행중 ▶] → [B05 대기] → [B06 대기] → ...
↑ (클릭하면 이동) (비활성화)
```
`B01_AccountDetail/B01_AccountDetail_Router.py`, `_Schema.py`, `_Repository.py` 신규 생성 (기존 패턴 준수, `common_util_auth_repository.py``search_companies`/`_company_code` 재사용):
**동작:**
- **완료/진행 중 단계:** 클릭 가능 → 해당 페이지로 이동
- **미진행 단계:** 비활성화 (회색), 클릭 불가
- **[+ 신규 프로젝트]:** 새 프로젝트 생성 페이지로 이동 (B02_ProjRegister)
- `GET /api/account/me` — 현재 계정 상세 (session 기반)
- `PATCH /api/account/me` — 이름/직급/전화번호 수정
- `GET /api/account/companies?q=` — 기존 `search_companies()` 재사용
- `POST /api/account/company/create` — 신규 회사 생성 + 본인을 MASTER/is_master=TRUE로 연결, `role='MASTER'`, `status='ACTIVE'`로 전환. 회사 필드(사업자등록번호/주소/전화번호/대표자명)를 여기서 받음.
- `POST /api/account/company/join` — 기존 회사에 `join_requests` 생성 + `users.company_id` 세팅(단, status는 `PENDING`으로, 마스터 승인 전까지 유지)
### 2.2 회사 상태 섹션 (USER만 표시)
### 접근 제어 (company 연결 강제)
**상태 표시:**
```
회사 상태: 테스트회사 (활동중)
[회사 변경] ← 다른 회사 검색 및 신청
```
- `common_util/common_util_auth.py``require_company()` dependency 추가: `session["company_id"] is None` 이면 403.
- `main.py`: B03~B06 라우터의 `protected` 리스트에 `Depends(require_company)` 추가.
**동작:**
- `status = 'NO_COMPANY'`: "회사 미연결" 표시 + [회사 생성] [회사 찾기] 버튼
- `status = 'PENDING'`: "가입 승인 대기 중 (OOO회사)" 표시
- `status = 'ACTIVE'`: "OOO회사 (ACTIVE)" 표시 + [회사 변경] 버튼
## 프론트엔드 변경
### 2.3 팀원 관리 섹션 (ADMIN만 표시)
### 리소스 텍스트 분리
**팀원 추가 동작:**
1. [+ 팀원 추가] 클릭 → 모달 팝업 노출
2. 기가입 사용자 검색 (이메일 입력)
3. 검색 결과 선택 → 회사에 추가
4. 중복 확인 (이미 팀원인 경우 "이미 팀원입니다" 메시지)
`resources/legal/` 신규 폴더:
- `terms_of_service.txt` — 이용약관 (Aislo/임도설계 자동화 서비스에 맞는 내용 초안 작성)
- `privacy_policy.txt` — 개인정보처리방침
- `marketing_consent.txt` — 마케팅 정보 수신 동의
**팀원 제거:**
- [제거] 버튼 클릭 → 확인 팝업 → 회사에서 제거
이 텍스트는 빌드 시 정적 자산으로 fetch하거나, 간단히 `A07_Register`에서 TS 상수 모듈로 다시 export하는 두 방식 중 — Vite가 `?raw` import를 지원하므로 `import termsText from "../../resources/legal/terms_of_service.txt?raw"` 형태로 로드 (외부 자산 폴더를 벗어나지 않고 사용자가 텍스트 파일만 고치면 되도록).
### 2.4 가입 요청 섹션 (ADMIN은 회사별, SYSTEM_ADMIN은 전체)
### A07_Register (폼 축소 + 약관 아코디언 + OTP 유지)
**ADMIN 대시보드:**
```
신청자 | 신청일 | 상태
김테스트 | 2025-07-08 | [승인] [거절]
```
- `A07_Register_UI_Auth_Page.ts`: `accountType`, `company`, `companyId` 필드 및 관련 로직 제거. `position`(직급), `phone`(전화번호), `passwordConfirm` 필드 추가.
- 약관 체크박스 영역: 현재 `check()` 헬퍼(단순 체크박스+라벨)를 확장한 `checkWithAccordion()`로 교체 — 라벨 클릭 시 하단에 `resources/legal/*.txt` 내용이 펼쳐지는 `<details>/<summary>` 또는 토글 버튼 구조.
- OTP 입력 칸: 기존처럼 제출 후 폼 하단에 노출되는 현재 로직 그대로 유지 (`verifyStep` 플래그 방식 재사용).
- `A07_Register_Api_Fetch.ts``RegisterPayload`: 축소된 필드로 갱신, `searchCompanies` export는 B01로 이동(또는 공용 유지 — B01에서 재사용하도록 export 유지).
**SYSTEM_ADMIN 대시보드:**
```
신청자 | 회사명 | 신청일 | 상태
김테스트 | 테스트회사 | 2025-07-08 | [승인] [거절]
```
### B01_AccountDetail (계정 상세 + 회사 연결)
**동작:**
- [승인]: `join_requests.status = 'APPROVED'``users.status = 'ACTIVE'`
- [거절]: `join_requests.status = 'REJECTED'` → 이메일 발송
- `B01_AccountDetail_UI_Page.ts`: 계정 기본정보 표시/수정 폼 + "회사 생성" / "회사 검색·연결" 두 개의 섹션(회사 미연결 시에만 노출, `A07_Register_Api_Fetch.searchCompanies` 재사용 가능).
- `B01_AccountDetail_Api_Fetch.ts` 신규: 위 백엔드 API 클라이언트.
### 2.5 인증 갱신 로직
### 라우팅 가드 (company 없으면 B02+ 차단)
**회원가입 시:**
```
1. 회원가입 폼 입력
2. [가입하기] 클릭
3. 인증번호 입력 필드 노출 ← 가입 후에만 표시
4. 인증 완료 → users.auth_expires_at = NOW() + 3개월 설정
```
- `A00_Common/router.ts``renderCurrentRoute()`: `PROTECTED_ROUTES` 검사 이후, B02~B11 라우트에 한해 세션의 `company_id` 확인 로직 추가 (session 조회 API에 `company_id` 이미 포함돼 있음 — `A06_Login_Router.py``/session` 응답에 `company_id` 필드만 추가하면 됨). 없으면 `ROUTES.B01_ACCOUNT`로 리다이렉트.
- `A06_Login_Router.py``current_session()`: 응답에 `company_id`, `status` 추가.
**로그인 시:**
```
1. 이메일/비밀번호 입력
2. DB 조회 후 auth_expires_at 확인
- auth_expires_at > NOW() → 바로 대시보드 이동 ✅
- auth_expires_at <= NOW() → 재인증 모달 표시 ⚠️
3. 로그인 성공 → last_login = NOW() 업데이트
4. 다음 로그인 시 auth_expires_at = NOW() + 3개월로 연장
```
## 검증 방법
**인증 만료 기준:**
- **고정 기간 방식 (이전):** 가입일로부터 3개월 후 만료 (자주 사용하면 3개월 후 갑자기 재인증)
- **로그인 기반 갱신 (현재):** 매 로그인 시 `auth_expires_at` 연장 (지속 사용 시 계속 유효)
1. `python main.py` 실행 후 브라우저에서 `/#/a07-register` 접속 → 축소된 필드로 가입, 약관 아코디언 펼쳐서 텍스트 노출 확인, 제출 후 OTP 칸 노출 확인
2. OTP 인증 완료 → DB에서 `users.status = 'NO_COMPANY'`, `company_id IS NULL` 확인
3. 해당 계정으로 로그인 성공 확인 (기존엔 `status != ACTIVE`면 403이었음)
4. 로그인 상태에서 `/#/b02-proj-register` 접속 시도 → `/#/b01-account`로 리다이렉트되는지 확인
5. B01에서 "회사 생성" 실행 → `role='MASTER'`, `status='ACTIVE'`, `company_id` 세팅 확인 후 B02 접근 가능한지 재확인
6. 두 번째 테스트 계정으로 "회사 검색·연결"(join) 플로우 → `join_requests` 생성 확인, 기존 마스터 승인 API(`A09_Security_Router.py``/master/join-requests`)로 승인 후 status ACTIVE 전환 확인
---
## 3. DB 스키마 확장
### 3.1 users 테이블 추가 컬럼
```sql
ALTER TABLE users ADD COLUMN (
role ENUM('SYSTEM_ADMIN', 'ADMIN', 'USER') DEFAULT 'USER',
is_master BOOLEAN DEFAULT FALSE,
status ENUM('PENDING_EMAIL', 'NO_COMPANY', 'PENDING', 'ACTIVE', 'INACTIVE', 'REJECTED') DEFAULT 'PENDING_EMAIL',
last_login DATETIME DEFAULT NULL,
auth_expires_at DATETIME DEFAULT NULL
);
```
### 3.2 신규 테이블: join_requests
```sql
CREATE TABLE join_requests (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
company_id INT NOT NULL,
requested_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status ENUM('PENDING', 'APPROVED', 'REJECTED') DEFAULT 'PENDING',
reviewed_by INT DEFAULT NULL,
reviewed_at TIMESTAMP DEFAULT NULL,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (company_id) REFERENCES companies(id),
FOREIGN KEY (reviewed_by) REFERENCES users(id),
UNIQUE KEY (user_id, company_id) -- 중복 신청 방지
);
```
### 3.3 신규 테이블: system_audit_logs
```sql
CREATE TABLE system_audit_logs (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
action VARCHAR(50), -- LOGIN, LOGOUT, USER_CREATE, USER_UPDATE, COMPANY_CREATE, etc
resource_type VARCHAR(50), -- user, company, project
resource_id INT,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ip_address VARCHAR(50),
user_agent TEXT,
FOREIGN KEY (user_id) REFERENCES users(id),
INDEX (timestamp),
INDEX (action)
);
```
### 3.4 신규 테이블: system_resources
```sql
CREATE TABLE system_resources (
id INT PRIMARY KEY AUTO_INCREMENT,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
cpu_usage_percent FLOAT,
memory_usage_percent FLOAT,
disk_usage_percent FLOAT,
active_user_count INT,
active_project_count INT,
total_storage_mb FLOAT,
INDEX (timestamp)
);
```
---
## 4. 백엔드 API 설계
### 4.1 B01_Dashboard 라우터 (`B01_Dashboard/B01_Dashboard_Router.py`)
#### 공통 API
```python
# GET /api/dashboard/me — 현재 사용자 정보 조회
@router.get("/api/dashboard/me")
async def get_dashboard_me(session: dict = Depends(verify_session)):
"""
현재 로그인한 사용자의 정보 반환
응답: {
id, email, name, position, department, phone, company_id, role, is_master, status,
last_login, auth_expires_at
}
"""
# PATCH /api/dashboard/me — 사용자 정보 수정
@router.patch("/api/dashboard/me")
async def patch_dashboard_me(
request: UpdateUserRequest,
session: dict = Depends(verify_session)
):
"""이름/직급/부서/전화번호 수정"""
# 응답: 수정된 사용자 정보
```
#### USER 역할 API
```python
# GET /api/dashboard/user/projects — 사용자의 프로젝트 리스트
@router.get("/api/dashboard/user/projects")
async def get_user_projects(session: dict = Depends(verify_session)):
"""
사용자가 생성한 프로젝트 리스트
응답: [{id, name, region, status, progress_percent, created_at, updated_at}, ...]
"""
# GET /api/dashboard/user/company — 사용자의 회사 정보
@router.get("/api/dashboard/user/company")
async def get_user_company(session: dict = Depends(verify_session)):
"""현재 소속 회사 정보"""
# 응답: {id, name, status, user_count, project_count}
# GET /api/dashboard/user/companies?q= — 회사 검색
@router.get("/api/dashboard/user/companies")
async def search_companies_for_join(
q: str = Query(...),
session: dict = Depends(verify_session)
):
"""회사명 또는 사업자번호로 검색"""
# 응답: [{id, name, business_number, status}, ...]
# POST /api/dashboard/user/company/create — 신규 회사 생성
@router.post("/api/dashboard/user/company/create")
async def create_company(
request: CreateCompanyRequest,
session: dict = Depends(verify_session)
):
"""
신규 회사 생성 + 사용자를 ADMIN(is_master=TRUE)으로 자동 연결
- companies 테이블에 INSERT
- users.company_id = new_company_id
- users.role = 'ADMIN'
- users.is_master = TRUE
- users.status = 'ACTIVE'
"""
# 응답: {company_id, status='ACTIVE'}
# POST /api/dashboard/user/company/join — 회사 참여 신청
@router.post("/api/dashboard/user/company/join")
async def join_company(
request: JoinCompanyRequest,
session: dict = Depends(verify_session)
):
"""
기존 회사에 참여 신청
- join_requests 테이블에 INSERT (status='PENDING')
- users.status = 'PENDING' (관리자 승인 대기)
- 관리자에게 이메일 발송
"""
# 응답: {join_request_id, status='PENDING'}
```
#### ADMIN 역할 API
```python
# GET /api/dashboard/admin/members — 회사 팀원 리스트
@router.get("/api/dashboard/admin/members")
async def get_company_members(session: dict = Depends(verify_session)):
"""회사에 소속된 모든 팀원 조회"""
# 응답: [{id, email, name, position, department, role, status}, ...]
# POST /api/dashboard/admin/members — 팀원 추가
@router.post("/api/dashboard/admin/members")
async def add_company_member(
request: AddMemberRequest,
session: dict = Depends(verify_session)
):
"""
기가입 사용자를 회사에 추가
- users.company_id = session.company_id
- users.status = 'ACTIVE'
- 중복 확인 필수
"""
# 응답: {user_id, status='ACTIVE'}
# DELETE /api/dashboard/admin/members/{user_id} — 팀원 제거
@router.delete("/api/dashboard/admin/members/{user_id}")
async def remove_company_member(
user_id: int,
session: dict = Depends(verify_session)
):
"""팀원을 회사에서 제거 (soft delete)"""
# GET /api/dashboard/admin/join-requests — 가입 신청 리스트
@router.get("/api/dashboard/admin/join-requests")
async def get_company_join_requests(session: dict = Depends(verify_session)):
"""이 회사에 가입을 신청한 사용자 리스트"""
# 응답: [{id, user_id, user_email, user_name, requested_at, status}, ...]
# PATCH /api/dashboard/admin/join-requests/{request_id} — 가입 신청 처리
@router.patch("/api/dashboard/admin/join-requests/{request_id}")
async def process_join_request(
request_id: int,
request: ProcessJoinRequest, # {action: 'APPROVE' | 'REJECT'}
session: dict = Depends(verify_session)
):
"""
가입 신청 승인/거절
- APPROVE: join_requests.status='APPROVED', users.status='ACTIVE'
- REJECT: join_requests.status='REJECTED'
- 사용자에게 이메일 발송
"""
# GET /api/dashboard/admin/projects — 회사 프로젝트 리스트
@router.get("/api/dashboard/admin/projects")
async def get_company_projects(session: dict = Depends(verify_session)):
"""회사에 속한 모든 프로젝트 조회"""
# 응답: [{id, name, owner, status, progress_percent}, ...]
```
#### SYSTEM_ADMIN 역할 API
```python
# GET /api/dashboard/admin/companies — 전체 회사 리스트
@router.get("/api/dashboard/admin/companies")
async def get_all_companies(session: dict = Depends(verify_session)):
"""시스템 관리자용: 전체 회사 조회"""
# 응답: [{id, name, status, user_count, project_count, created_at}, ...]
# GET /api/dashboard/admin/users — 전체 사용자 리스트
@router.get("/api/dashboard/admin/users")
async def get_all_users(session: dict = Depends(verify_session)):
"""시스템 관리자용: 전체 사용자 조회"""
# 응답: [{id, email, name, role, company_id, status, last_login}, ...]
# PATCH /api/dashboard/admin/users/{user_id}/role — 사용자 역할 변경
@router.patch("/api/dashboard/admin/users/{user_id}/role")
async def change_user_role(
user_id: int,
request: ChangeUserRoleRequest,
session: dict = Depends(verify_session)
):
"""사용자 역할 변경 (USER ↔ ADMIN ↔ SYSTEM_ADMIN)"""
# PATCH /api/dashboard/admin/users/{user_id}/company — 사용자 회사 할당
@router.patch("/api/dashboard/admin/users/{user_id}/company")
async def assign_user_to_company(
user_id: int,
request: AssignCompanyRequest,
session: dict = Depends(verify_session)
):
"""사용자를 다른 회사에 할당"""
# GET /api/dashboard/admin/join-requests-all — 전체 가입 신청
@router.get("/api/dashboard/admin/join-requests-all")
async def get_all_join_requests(session: dict = Depends(verify_session)):
"""시스템 관리자용: 전체 가입 신청 조회"""
# PATCH /api/dashboard/admin/join-requests/{request_id}/approve — 시스템 레벨 승인
@router.patch("/api/dashboard/admin/join-requests/{request_id}/approve")
async def system_approve_join_request(
request_id: int,
session: dict = Depends(verify_session)
):
"""시스템 관리자가 가입 신청 승인"""
# GET /api/dashboard/admin/audit-logs — 시스템 로그
@router.get("/api/dashboard/admin/audit-logs")
async def get_system_audit_logs(
page: int = Query(1),
limit: int = Query(50),
start_date: str = Query(None),
end_date: str = Query(None),
user_id: int = Query(None),
action: str = Query(None),
session: dict = Depends(verify_session)
):
"""
시스템 감시 로그 조회 (필터링 지원)
응답: {total, items: [{id, user_id, action, resource_type, timestamp}]}
"""
# GET /api/dashboard/admin/resources — 하드웨어 리소스 현황
@router.get("/api/dashboard/admin/resources")
async def get_system_resources(
period: str = Query('1h'), # 1h, 6h, 24h, 7d
session: dict = Depends(verify_session)
):
"""
시스템 리소스 사용량 조회 (그래프용)
응답: {
current: {cpu_usage_percent, memory_usage_percent, disk_usage_percent},
history: [{timestamp, cpu_usage_percent, memory_usage_percent, ...}],
stats: {active_user_count, active_project_count, total_storage_mb}
}
"""
```
### 4.2 Schema 정의 (`B01_Dashboard/B01_Dashboard_Schema.py`)
```python
from pydantic import BaseModel
# 공통
class UpdateUserRequest(BaseModel):
name: str
position: str | None
department: str | None
phone: str | None
# USER 역할
class CreateCompanyRequest(BaseModel):
name: str
business_registration_number: str
business_address: str
business_owner: str
class JoinCompanyRequest(BaseModel):
company_id: int
class AddMemberRequest(BaseModel):
email: str # 기가입 사용자의 이메일
class ProcessJoinRequest(BaseModel):
action: str # 'APPROVE' | 'REJECT'
reason: str | None = None
# SYSTEM_ADMIN 역할
class ChangeUserRoleRequest(BaseModel):
role: str # 'USER' | 'ADMIN' | 'SYSTEM_ADMIN'
class AssignCompanyRequest(BaseModel):
company_id: int | None = None # None이면 회사 미연결
```
### 4.3 Repository (`B01_Dashboard/B01_Dashboard_Repository.py`)
Raw SQL을 이용한 데이터 조회/수정 함수들을 구현
---
## 5. 프론트엔드 구현 필요
### 5.1 B01_Dashboard_UI_Page.ts 전체 재설계
**구조:**
```typescript
function DashboardPage() {
const user = useAuth(); // role, status, company_id 포함
return (
<div class="dashboard">
{/* USER/ADMIN 공통: 프로젝트 리스트 */}
{(user.role === 'USER' || user.role === 'ADMIN') && (
<ProjectListSection />
)}
{/* USER만: 회사 정보 */}
{user.role === 'USER' && (
<CompanyInfoSection />
)}
{/* ADMIN만: 팀원 관리 */}
{user.role === 'ADMIN' && (
<>
<CompanyInfoSection />
<TeamMembersSection />
<JoinRequestsSection />
</>
)}
{/* SYSTEM_ADMIN만: 시스템 관리 */}
{user.role === 'SYSTEM_ADMIN' && (
<>
<ResourceStatusSection />
<CompanyManagementSection />
<UserManagementSection />
<JoinRequestsManagementSection />
<SystemAuditLogsSection />
<SystemResourcesGraphSection />
</>
)}
{/* 모든 역할: 기본정보 & 보안 */}
<ProfileSection />
</div>
);
}
```
### 5.2 B01_Dashboard_Api_Fetch.ts 신규
```typescript
// 공통
export async function fetchDashboardMe(): Promise<DashboardUser>;
export async function updateUserProfile(request: UpdateUserRequest): Promise<void>;
// USER 역할
export async function fetchUserProjects(): Promise<ProjectItem[]>;
export async function fetchUserCompany(): Promise<CompanyInfo>;
export async function searchCompanies(query: string): Promise<Company[]>;
export async function createCompany(request: CreateCompanyRequest): Promise<CompanyCreateResponse>;
export async function joinCompany(companyId: number): Promise<JoinCompanyResponse>;
// ADMIN 역할
export async function fetchCompanyMembers(): Promise<Member[]>;
export async function addCompanyMember(email: string): Promise<Member>;
export async function removeCompanyMember(userId: number): Promise<void>;
export async function fetchCompanyJoinRequests(): Promise<JoinRequest[]>;
export async function processJoinRequest(requestId: number, action: 'APPROVE' | 'REJECT'): Promise<void>;
export async function fetchCompanyProjects(): Promise<ProjectItem[]>;
// SYSTEM_ADMIN 역할
export async function fetchAllCompanies(): Promise<Company[]>;
export async function fetchAllUsers(filters?: UserFilters): Promise<User[]>;
export async function changeUserRole(userId: number, role: string): Promise<void>;
export async function assignUserToCompany(userId: number, companyId: number | null): Promise<void>;
export async function fetchAllJoinRequests(): Promise<JoinRequest[]>;
export async function systemApproveJoinRequest(requestId: number): Promise<void>;
export async function fetchAuditLogs(filters?: AuditLogFilters): Promise<AuditLog[]>;
export async function fetchSystemResources(period: string): Promise<ResourceData>;
```
### 5.3 B01_Dashboard_UI_Style.css 확장
- 역할별 섹션 레이아웃
- 프로젝트 리스트 테이블 스타일
- 워크플로우 진행도 시각화
- 팝업 모달 스타일
- 그래프/차트 영역 스타일
### 5.4 섹션별 컴포넌트
#### ProjectListSection
- 프로젝트 테이블 (프로젝트명, 지역, 진행도, 워크플로우, 수정일)
- [+ 신규 프로젝트] 버튼
- 워크플로우 단계 클릭 이벤트 처리
#### CompanyInfoSection (USER용)
```
회사 상태: 테스트회사 (ACTIVE) | [회사 변경]
상태별 표시:
- NO_COMPANY: "회사 미연결" + [회사 생성] [회사 찾기]
- PENDING: "가입 승인 대기 중 (OOO회사)"
- ACTIVE: "OOO회사 (ACTIVE)" + [회사 변경]
```
#### TeamMembersSection (ADMIN용)
- 팀원 리스트 테이블 (이메일, 이름, 직급, 부서, 역할, 상태)
- [+ 팀원 추가] 버튼 → 모달 팝업
- [제거] 버튼
#### JoinRequestsSection (ADMIN용)
- 가입 신청 리스트 테이블 (신청자, 신청일, 상태)
- [승인] [거절] 버튼
#### ResourceStatusSection (SYSTEM_ADMIN용)
```
CPU: 45% | MEM: 62% | DISK: 78%
활성 사용자: 23 | 활성 프로젝트: 15
```
#### SystemAuditLogsSection (SYSTEM_ADMIN용)
- 시스템 로그 테이블 (사용자, 행동, 대상, 타임스탬프)
- 필터링: 기간, 사용자, 행동 유형
#### SystemResourcesGraphSection (SYSTEM_ADMIN용)
- CPU 사용량 그래프 (시간대별)
- 메모리 사용량 그래프
- 디스크 사용량 그래프
- 동시 접속자 수 추이
---
## 6. 검증 방법 (다음 단계)
### 6.1 USER 역할 검증
1. 새 계정으로 로그인 → B01_Dashboard 진입
2. 프로젝트 리스트 표시 확인
3. 회사 상태: "회사 미연결" 표시 + [회사 생성] [회사 찾기] 버튼 확인
4. [회사 생성] → 모달 팝업 → 회사 정보 입력 → [생성]
5. DB 확인:
- `users.company_id` 세팅
- `users.role = 'ADMIN'` (관리자 전환)
- `users.is_master = TRUE`
- `users.status = 'ACTIVE'`
6. 페이지 새로고침 → 회사명 표시 확인
### 6.2 ADMIN 역할 검증
1. 회사 생성자(관리자)로 로그인 → B01_Dashboard 진입
2. 팀원 관리, 가입 요청, 회사 프로젝트 섹션 표시 확인
3. [+ 팀원 추가] → 모달 → 기가입 사용자 이메일 입력 → 검색 → 선택 → [추가]
4. DB 확인:
- `users.company_id` 업데이트
- 중복 확인 (이미 팀원인 경우 오류 메시지)
5. 팀원 리스트에 새로운 팀원 표시 확인
### 6.3 SYSTEM_ADMIN 역할 검증
1. 시스템 관리자 계정으로 로그인 → B01_Dashboard 진입
2. 전체 회사 리스트, 사용자 리스트, 시스템 로그, 리소스 현황 섹션 표시 확인
3. 리소스 현황 (CPU, MEM, DISK, 활성 사용자/프로젝트) 표시 확인
4. 사용자 역할 변경: USER → ADMIN 전환 → DB 확인
5. 시스템 로그 필터링 (기간, 사용자, 행동 유형) 동작 확인