1
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
# 대시보드 검증 후속 조치 계획서 (FOLLOWUP Dashboard Fixes)
|
||||||
|
|
||||||
|
본 문서는 대시보드 관리 기능 검증 과정에서 확인된 계획서 불일치 항목 및 오차를 해결하기 위한 백엔드 및 프론트엔드 후속 수정 내역을 기술합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 백엔드 권한 제어 수정 사항
|
||||||
|
|
||||||
|
### 1.1 회사 멤버(사용자) 조회 권한 완화
|
||||||
|
- **이슈:** `GET /api/dashboard/admin/members` API가 `require_company_admin` 데코레이터로 보호되어 있어, 일반 `USER`가 자기 회사의 팀원 목록을 조회하지 못하고 403 에러가 발생함.
|
||||||
|
- **수정 계획:**
|
||||||
|
- `admin_members` 엔드포인트의 의존성을 `Depends(verify_session)`으로 완화.
|
||||||
|
- 함수 내부에서 호출자의 `company_id`가 존재하는지 검증한 후, 해당 회사의 멤버 목록만 조회하도록 변경.
|
||||||
|
|
||||||
|
### 1.2 자동화 설계 로직 조회 권한 제한 (기획 확인에 따른 변경)
|
||||||
|
- **이슈:** `GET /projects/{project_id}/automations` API가 `_can_edit_project`를 사용하고 있어 `USER`에게 노출됨. 계획서(USER ❌ 조회 불가) 기준에 맞게 통제가 필요함.
|
||||||
|
- **수정 계획:**
|
||||||
|
- `dashboard_project_automations`에서 `USER` 권한인 경우 조회를 차단하고 403 에러를 반환하도록 로직 적용.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 프론트엔드 UI/UX 및 모달 구현 사항
|
||||||
|
|
||||||
|
### 2.1 다국어(i18n) 키 추가
|
||||||
|
- `ui_template_locale.ts` 파일 하단에 계획서 8번의 다국어 문구 키들(`B01_Dashboard_EditProject` 등)을 추가 반영.
|
||||||
|
|
||||||
|
### 2.2 누락된 관리자 모달 및 기능 구현
|
||||||
|
- **프로젝트 수정/삭제 모달:** `openEditProjectModal`, `openDeleteProjectModal` 구현 및 조건부 노출.
|
||||||
|
- **사용자 관리 모달:** `openEditUserModal`, `openChangeRoleModal`, `openDeleteUserModal` 구현.
|
||||||
|
- **자동화 설계 로직 관리:** 프로젝트 상세 화면 내에서 자동화 목록 조회 및 제어 모달 연동.
|
||||||
@@ -199,6 +199,36 @@
|
|||||||
- `ADMIN`: 회사 관리자 (회사 내 관리권 한정)
|
- `ADMIN`: 회사 관리자 (회사 내 관리권 한정)
|
||||||
- `USER`: 일반 사용자
|
- `USER`: 일반 사용자
|
||||||
|
|
||||||
|
### 6.9 대시보드 권한 검증 및 감시 로깅 (B01_Dashboard Authorization & Audit)
|
||||||
|
* **권한 검증 헬퍼 필수:** `.agent/plan_dashboard_management.md` 섹션 9의 Python 헬퍼 함수 구현
|
||||||
|
- `can_edit_project(user, project)` - 프로젝트 수정 권한
|
||||||
|
- `can_delete_project(user)` - 프로젝트 삭제 권한 (SYSTEM_ADMIN만)
|
||||||
|
- `can_change_role(user, target_user, new_role)` - 역할 변경 권한
|
||||||
|
- ADMIN이 SYSTEM_ADMIN으로 변경 시도 시 즉시 False 반환
|
||||||
|
- ADMIN은 USER ↔ ADMIN만 변경 가능 (같은 회사만)
|
||||||
|
- `can_manage_automation(user, automation)` - 자동화 로직 관리 권한 (USER 차단)
|
||||||
|
- `is_last_admin(company_id, exclude_user_id)` - 회사의 마지막 ADMIN 확인
|
||||||
|
|
||||||
|
* **백엔드 권한 재검증 필수:** 모든 수정/삭제/역할변경 API에서 권한 재검증
|
||||||
|
- 클라이언트 UI 신뢰 금지 (프론트엔드는 UI만 제어)
|
||||||
|
- 권한 없는 사용자가 API 직접 호출 시도 → 403 Forbidden
|
||||||
|
- 권한 오류 메시지: `{"status": "error", "message": "Access denied"}`
|
||||||
|
|
||||||
|
* **감시 로깅 (audit_logs):** 다음 행동들을 기록
|
||||||
|
- 프로젝트 삭제: `action="DELETE_PROJECT"`, `resource_type="project"`
|
||||||
|
- 사용자 역할 변경: `action="CHANGE_ROLE_TO_{NEW_ROLE}"`, `resource_type="user"`
|
||||||
|
- 사용자 삭제: `action="DELETE_USER"`, `resource_type="user"`
|
||||||
|
- 자동화 로직 삭제: `action="DELETE_AUTOMATION"`, `resource_type="automation"`
|
||||||
|
|
||||||
|
* **USER 자동화 조회 권한 차단:** `/api/projects/{pid}/automations` GET
|
||||||
|
- 요청 사용자의 role이 USER인 경우 403 Forbidden 반환
|
||||||
|
- 메시지: `"Automation logic viewing is not allowed for users"`
|
||||||
|
|
||||||
|
* **마지막 ADMIN 보호:** 사용자 삭제 API에서 체크
|
||||||
|
- `is_last_admin(company_id, exclude_user_id=user_id)` 호출
|
||||||
|
- True 반환 시 400 Bad Request: `"Cannot delete the last admin of the company"`
|
||||||
|
- 역할 변경 API에서도 동일 로직 적용
|
||||||
|
|
||||||
**마스터 플래그 (users.is_master)**
|
**마스터 플래그 (users.is_master)**
|
||||||
- `TRUE`: 회사 생성자 또는 ADMIN 역할 (팀원 승인/거부 권한)
|
- `TRUE`: 회사 생성자 또는 ADMIN 역할 (팀원 승인/거부 권한)
|
||||||
- `FALSE`: 일반 팀원
|
- `FALSE`: 일반 팀원
|
||||||
|
|||||||
+21
-1
@@ -97,4 +97,24 @@ export const ui_locales = {
|
|||||||
- 회원가입 후 이메일 인증 완료: `users.status = 'NO_COMPANY'` (로그인 가능, 회사 미연결)
|
- 회원가입 후 이메일 인증 완료: `users.status = 'NO_COMPANY'` (로그인 가능, 회사 미연결)
|
||||||
- 회사 생성: `users.status = 'ACTIVE'`, `company_id` 세팅, `role = 'ADMIN'`, `is_master = TRUE`
|
- 회사 생성: `users.status = 'ACTIVE'`, `company_id` 세팅, `role = 'ADMIN'`, `is_master = TRUE`
|
||||||
- 회사 참여 신청: `users.status = 'PENDING'` (관리자 승인 대기)
|
- 회사 참여 신청: `users.status = 'PENDING'` (관리자 승인 대기)
|
||||||
- 관리자 승인: `users.status = 'ACTIVE'`, `company_id` 세팅
|
- 관리자 승인: `users.status = 'ACTIVE'`, `company_id` 세팅
|
||||||
|
|
||||||
|
### 5.3 대시보드 권한 기반 UI 제어 (B01_Dashboard)
|
||||||
|
* **권한 검증 헬퍼 필수:** `.agent/plan_dashboard_management.md` 섹션 9의 헬퍼 함수 참고
|
||||||
|
- `canEditProject(user, project)` - 프로젝트 수정 권한
|
||||||
|
- `canDeleteProject(user)` - 프로젝트 삭제 권한 (SYSTEM_ADMIN만)
|
||||||
|
- `canAddUser(user)` - 사용자 추가 권한
|
||||||
|
- `canChangeRole(user, targetUser)` - 역할 변경 권한 (ADMIN: USER↔ADMIN만, SYSTEM_ADMIN 제외)
|
||||||
|
- `canDeleteUser(user, targetUser)` - 사용자 삭제 권한 (마지막 ADMIN 보호)
|
||||||
|
- `canManageAutomation(user, automation)` - 자동화 로직 관리 권한
|
||||||
|
- `isLastAdmin(company, excludeUserId)` - 회사의 마지막 ADMIN 확인
|
||||||
|
|
||||||
|
* **UI 컴포넌트 구현:** 권한에 따라 버튼/필드 표시/비활성화
|
||||||
|
- 프로젝트: 수정 모달, 삭제 확인 모달 (삭제는 SYSTEM_ADMIN만)
|
||||||
|
- 사용자: 추가 모달 (가입 계정 목록), 수정 모달, 역할변경 모달, 삭제 확인 모달
|
||||||
|
- 자동화 로직: 생성/수정/삭제 모달 (ADMIN/SYSTEM_ADMIN만)
|
||||||
|
- 모든 삭제 모달에 경고 메시지 포함 (되돌릴 수 없음)
|
||||||
|
|
||||||
|
* **다국어 반영:** 계획서 8장의 모든 키를 `ui_template_locale.ts`에 추가 후 컴포넌트에서 `L(키)` 사용
|
||||||
|
- 모달 제목, 버튼 레이블, 확인 메시지, 오류 메시지 모두 다국어 처리
|
||||||
|
- 하드코딩된 문구 절대 금지
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
# 대시보드 프로젝트/사용자/회사 관리 기능 계획서
|
||||||
|
|
||||||
|
**버전:** 2.1
|
||||||
|
**작성일:** 2026-07-08
|
||||||
|
**상태:** 승인 대기
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 역할 정의
|
||||||
|
|
||||||
|
### 1.1 SYSTEM_ADMIN (시스템 관리자)
|
||||||
|
**대상:** 서비스 개발사 (당신, 우리 회사 사람들)
|
||||||
|
|
||||||
|
**책임:**
|
||||||
|
- 서비스 배포 및 운영
|
||||||
|
- 자동화 설계 로직 제어
|
||||||
|
- 타사(고객사) 트러블슈팅
|
||||||
|
- 전체 시스템 모니터링
|
||||||
|
|
||||||
|
**권한:**
|
||||||
|
- ✅ 모든 회사/프로젝트 관리 (조회, 추가, 수정, 삭제)
|
||||||
|
- ✅ 모든 사용자 관리 (조회, 추가, 수정, 삭제)
|
||||||
|
- ✅ 모든 사용자 역할 변경
|
||||||
|
- ✅ 자동화 설계 로직 제어
|
||||||
|
- ✅ 시스템 리소스 모니터링
|
||||||
|
- ⚠️ 역할 변경은 DB 직접 수정만 가능 (UI 제공 안함)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.2 ADMIN (고객사 관리자)
|
||||||
|
**대상:** 계약된 고객사의 관리자
|
||||||
|
|
||||||
|
**책임:**
|
||||||
|
- 고객사 내부 사용자 관리
|
||||||
|
- 자동화 설계 로직 제어 (계약된 범위)
|
||||||
|
- 프로젝트 진행 관리
|
||||||
|
|
||||||
|
**권한:**
|
||||||
|
- ✅ 자신 회사의 프로젝트 조회/수정
|
||||||
|
- ✅ 자신 회사의 사용자 추가/수정/삭제
|
||||||
|
- ✅ 사용자 역할 변경 (USER ↔ ADMIN만, SYSTEM_ADMIN 제외)
|
||||||
|
- ✅ **자동화 설계 로직 제어** (계약된 범위)
|
||||||
|
- ❌ 프로젝트 삭제 불가 (SYSTEM_ADMIN만)
|
||||||
|
- ❌ 타사 데이터 접근 불가
|
||||||
|
- ❌ 시스템 리소스 모니터링 불가
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.3 USER (일반 사용자)
|
||||||
|
**대상:** 고객사에 소속된 프로젝트 진행자
|
||||||
|
|
||||||
|
**책임:**
|
||||||
|
- 할당된 프로젝트 작업 수행
|
||||||
|
|
||||||
|
**권한:**
|
||||||
|
- ✅ 자신 회사의 프로젝트 조회/수정 (진행 데이터 입력)
|
||||||
|
- ✅ 자신 회사의 멤버 조회
|
||||||
|
- ✅ 자신의 프로필 정보 수정
|
||||||
|
- ❌ 프로젝트 추가/삭제 불가
|
||||||
|
- ❌ 사용자 관리 불가
|
||||||
|
- ❌ 자동화 설계 로직 제어 불가
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 권한 기반 기능 매트릭스
|
||||||
|
|
||||||
|
### 2.1 프로젝트 관리
|
||||||
|
|
||||||
|
| 기능 | SYSTEM_ADMIN | ADMIN | USER | 비고 |
|
||||||
|
|------|:---:|:---:|:---:|------|
|
||||||
|
| 조회 | ✅ 전체 | ✅ 회사 | ✅ 회사 | 기본 기능 |
|
||||||
|
| 추가 | ✅ | ✅ | ❌ | — |
|
||||||
|
| 수정 | ✅ 전체 | ✅ 회사 | ✅ 회사 | USER: 진행 데이터만 |
|
||||||
|
| 삭제 | ✅ | ❌ | ❌ | SYSTEM_ADMIN만 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 사용자 관리
|
||||||
|
|
||||||
|
| 기능 | SYSTEM_ADMIN | ADMIN | USER | 비고 |
|
||||||
|
|------|:---:|:---:|:---:|------|
|
||||||
|
| 조회 | ✅ 전체 | ✅ 회사 | ✅ 회사 | 회사 멤버 리스트 |
|
||||||
|
| 추가 | ✅ | ✅ | ❌ | 가입 계정 기반 추가 |
|
||||||
|
| 수정 정보 | ✅ 전체 | ✅ 전체 | ✅ 자신만 | 프로필 정보 수정 |
|
||||||
|
| 역할 변경 | ✅ DB만 | ✅ USER↔ADMIN | ❌ | SYSTEM_ADMIN 변경 차단 |
|
||||||
|
| 삭제 | ✅ | ✅ 회사원 | ❌ | 마지막 ADMIN 보호 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 회사 관리
|
||||||
|
|
||||||
|
| 기능 | SYSTEM_ADMIN | 비고 |
|
||||||
|
|------|:---:|------|
|
||||||
|
| 조회 | ✅ 전체 | — |
|
||||||
|
| 추가 | ✅ | — |
|
||||||
|
| 수정 | ✅ | — |
|
||||||
|
| 삭제 | ✅ | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 자동화 설계 로직 제어
|
||||||
|
|
||||||
|
| 기능 | SYSTEM_ADMIN | ADMIN | USER | 비고 |
|
||||||
|
|------|:---:|:---:|:---:|------|
|
||||||
|
| 조회 | ✅ 전체 | ✅ 회사 | ❌ | 프로젝트 기반 조회 |
|
||||||
|
| 생성/수정 | ✅ | ✅ 회사 | ❌ | 계약된 범위만 |
|
||||||
|
| 실행 | ✅ | ✅ | ❌ | — |
|
||||||
|
| 삭제 | ✅ | ✅ | ❌ | 계약된 범위만 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 상세 기능 명세
|
||||||
|
|
||||||
|
### 3.1 프로젝트 관리
|
||||||
|
|
||||||
|
#### 📝 프로젝트 수정
|
||||||
|
- **SYSTEM_ADMIN:** 모든 필드 수정 가능
|
||||||
|
- **ADMIN:** 자신 회사의 프로젝트만 수정
|
||||||
|
- **USER:** 자신 회사의 프로젝트만 수정 (진행 관련 데이터: 좌표, 설정값 등)
|
||||||
|
|
||||||
|
#### ❌ 프로젝트 삭제
|
||||||
|
- **SYSTEM_ADMIN:** 모든 프로젝트 삭제 가능
|
||||||
|
- **ADMIN/USER:** 삭제 불가
|
||||||
|
- 경고 모달: "되돌릴 수 없습니다"
|
||||||
|
- 감시 로깅: `audit_logs` 기록
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 사용자 관리
|
||||||
|
|
||||||
|
#### ➕ 사용자 추가 (가입 계정 기반)
|
||||||
|
- **Flow:** 가입된 사용자 리스트 표시 → 선택 → 회사에 추가
|
||||||
|
- **권한:** SYSTEM_ADMIN, ADMIN만 가능
|
||||||
|
- 추가 후 상태: `ACTIVE`
|
||||||
|
|
||||||
|
#### ✏️ 사용자 정보 수정
|
||||||
|
- **SYSTEM_ADMIN:** 모든 필드 수정
|
||||||
|
- **ADMIN:** 자신 회사 사용자의 모든 필드 수정
|
||||||
|
- **USER:** 자신의 프로필만 수정 (이름, 직책, 부서, 연락처)
|
||||||
|
|
||||||
|
#### 🔄 역할 변경
|
||||||
|
- **SYSTEM_ADMIN:** 모든 역할 변경 가능 (DB 직접, UI 미제공)
|
||||||
|
- **ADMIN:** USER ↔ ADMIN만 변경 가능
|
||||||
|
- **USER:** 역할 변경 불가
|
||||||
|
- **🚫 제약:** SYSTEM_ADMIN으로 변경 시도 시 백엔드에서 차단
|
||||||
|
|
||||||
|
#### ❌ 사용자 삭제
|
||||||
|
- **SYSTEM_ADMIN:** 모든 사용자 삭제 가능
|
||||||
|
- **ADMIN:** 자신 회사 사용자만 삭제 가능 (마지막 ADMIN 보호)
|
||||||
|
- **USER:** 사용자 삭제 불가
|
||||||
|
- 경고 모달: 마지막 ADMIN 삭제 방지
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 자동화 설계 로직 제어
|
||||||
|
|
||||||
|
#### 📋 조회
|
||||||
|
- **SYSTEM_ADMIN:** 모든 자동화 설계 로직 조회
|
||||||
|
- **ADMIN:** 자신 회사의 프로젝트에 할당된 자동화 로직만 조회
|
||||||
|
- **USER:** 조회 불가
|
||||||
|
|
||||||
|
#### ➕ 생성/수정
|
||||||
|
- **SYSTEM_ADMIN:** 모든 자동화 로직 생성/수정 가능
|
||||||
|
- **ADMIN:** 자신 회사의 프로젝트에 할당 가능한 범위 내에서만 생성/수정
|
||||||
|
- **USER:** 불가
|
||||||
|
|
||||||
|
#### ▶️ 실행
|
||||||
|
- **SYSTEM_ADMIN:** 모든 자동화 로직 실행 가능
|
||||||
|
- **ADMIN:** 자신 회사의 프로젝트에 할당된 자동화 로직만 실행
|
||||||
|
- **USER:** 불가
|
||||||
|
|
||||||
|
#### ❌ 삭제
|
||||||
|
- **SYSTEM_ADMIN:** 모든 자동화 로직 삭제 가능
|
||||||
|
- **ADMIN:** 자신 회사에서 생성한 자동화 로직만 삭제
|
||||||
|
- **USER:** 불가
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 테이블 및 UI 개선
|
||||||
|
|
||||||
|
#### 프로젝트 테이블
|
||||||
|
- 프로젝트명, 지역, 진행률, 워크플로우 단계, 수정일
|
||||||
|
- 소유자, 상태 배지 (활성/비활성/완료)
|
||||||
|
- 액션 버튼:
|
||||||
|
- [수정] - SYSTEM_ADMIN, ADMIN, USER (권한별)
|
||||||
|
- [삭제] - SYSTEM_ADMIN만
|
||||||
|
|
||||||
|
#### 사용자 테이블
|
||||||
|
- 이메일, 이름, 직책, 부서, 역할, 상태
|
||||||
|
- 가입일, 마지막 로그인
|
||||||
|
- 액션 버튼:
|
||||||
|
- [수정] - 권한별 표시
|
||||||
|
- [역할변경] - ADMIN (USER↔ADMIN만)
|
||||||
|
- [삭제] - SYSTEM_ADMIN, ADMIN (회사 내만)
|
||||||
|
|
||||||
|
#### 검색/필터
|
||||||
|
- **프로젝트:** 이름, 상태로 검색/필터
|
||||||
|
- **사용자:** 이메일, 이름으로 검색, 역할/상태로 필터
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. UI 제어 로직 (권한 기반)
|
||||||
|
|
||||||
|
### 4.1 버튼 표시 규칙
|
||||||
|
|
||||||
|
| 버튼 | 표시 조건 |
|
||||||
|
|------|---------|
|
||||||
|
| [프로젝트 수정] | `SYSTEM_ADMIN` \|\| (`ADMIN` && 회사 일치) \|\| (`USER` && 회사 일치) |
|
||||||
|
| [프로젝트 삭제] | `SYSTEM_ADMIN`만 |
|
||||||
|
| [사용자 추가] | `SYSTEM_ADMIN` \|\| `ADMIN` |
|
||||||
|
| [사용자 수정] | `SYSTEM_ADMIN` \|\| (`ADMIN` && 회사 일치) \|\| (`USER` && 자신) |
|
||||||
|
| [역할 변경] | (`ADMIN` && 회사 일치 && 대상 ≠ `SYSTEM_ADMIN`) |
|
||||||
|
| [사용자 삭제] | `SYSTEM_ADMIN` \|\| (`ADMIN` && 회사 일치 && 마지막 ADMIN 아님) |
|
||||||
|
| [자동화 로직 생성/수정] | `SYSTEM_ADMIN` \|\| `ADMIN` |
|
||||||
|
| [자동화 로직 실행] | `SYSTEM_ADMIN` \|\| `ADMIN` |
|
||||||
|
| [자동화 로직 삭제] | `SYSTEM_ADMIN` \|\| (`ADMIN` && 소유자) |
|
||||||
|
|
||||||
|
### 4.2 입력 필드 비활성화 규칙
|
||||||
|
|
||||||
|
**프로젝트 수정 폼:**
|
||||||
|
- `SYSTEM_ADMIN`: 모든 필드 활성화
|
||||||
|
- `ADMIN`: 모든 필드 활성화
|
||||||
|
- `USER`: 진행 관련 필드만 활성화 (예: 좌표, 설정값, 진행도)
|
||||||
|
|
||||||
|
**사용자 수정 폼:**
|
||||||
|
- `SYSTEM_ADMIN`: 모든 필드 활성화
|
||||||
|
- `ADMIN`: 모든 필드 활성화
|
||||||
|
- `USER` (자신): 이름, 직책, 부서, 연락처만 활성화
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 필요한 API (백엔드)
|
||||||
|
|
||||||
|
### 5.1 프로젝트 API
|
||||||
|
|
||||||
|
| 메서드 | 엔드포인트 | 권한 검증 |
|
||||||
|
|--------|----------|----------|
|
||||||
|
| `PUT` | `/api/projects/{id}` | `SYSTEM_ADMIN` \|\| (`ADMIN` && 회사) \|\| (`USER` && 회사) |
|
||||||
|
| `DELETE` | `/api/projects/{id}` | `SYSTEM_ADMIN`만 |
|
||||||
|
|
||||||
|
### 5.2 사용자 API
|
||||||
|
|
||||||
|
| 메서드 | 엔드포인트 | 권한 검증 |
|
||||||
|
|--------|----------|----------|
|
||||||
|
| `POST` | `/api/companies/{cid}/users` | 가입 계정을 회사에 추가 (`SYSTEM_ADMIN` \|\| `ADMIN`) |
|
||||||
|
| `PUT` | `/api/users/{id}` | `SYSTEM_ADMIN` \|\| (`ADMIN` && 회사) \|\| (`USER` && 자신) |
|
||||||
|
| `PATCH` | `/api/users/{id}/role` | `ADMIN`: `USER`↔`ADMIN`만, `SYSTEM_ADMIN` 변경 차단 |
|
||||||
|
| `DELETE` | `/api/companies/{cid}/users/{uid}` | `SYSTEM_ADMIN` \|\| (`ADMIN` && 회사 && 마지막 ADMIN 아님) |
|
||||||
|
| `GET` | `/api/companies/{cid}/users` | `SYSTEM_ADMIN` \|\| (`ADMIN` && 회사) \|\| (`USER` && 회사) |
|
||||||
|
| `GET` | `/api/users?role=unused` | 가입했으나 회사 미배정 사용자 목록 (`ADMIN` 참조용) |
|
||||||
|
|
||||||
|
### 5.3 자동화 설계 로직 API
|
||||||
|
|
||||||
|
| 메서드 | 엔드포인트 | 권한 검증 |
|
||||||
|
|--------|----------|----------|
|
||||||
|
| `GET` | `/api/projects/{pid}/automations` | `SYSTEM_ADMIN` \|\| (`ADMIN` && 회사) \|\| (`USER` && 회사) |
|
||||||
|
| `POST` | `/api/projects/{pid}/automations` | `SYSTEM_ADMIN` \|\| (`ADMIN` && 회사) |
|
||||||
|
| `PUT` | `/api/automations/{aid}` | `SYSTEM_ADMIN` \|\| (`ADMIN` && 소유 회사) |
|
||||||
|
| `DELETE` | `/api/automations/{aid}` | `SYSTEM_ADMIN` \|\| (`ADMIN` && 소유 회사) |
|
||||||
|
| `POST` | `/api/automations/{aid}/execute` | `SYSTEM_ADMIN` \|\| (`ADMIN` && 소유 회사) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 구현 순서 (우선순위)
|
||||||
|
|
||||||
|
| 우선순위 | 항목 | 복잡도 | 중요도 | 설명 |
|
||||||
|
|:---:|------|:---:|:---:|------|
|
||||||
|
| 1 | 프로젝트 수정 (`USER` 포함) | 중간 | 높음 | 프로젝트 진행 필수 |
|
||||||
|
| 1 | 사용자 추가 (가입 계정 기반) | 높음 | 높음 | 회사 관리 필수 |
|
||||||
|
| 1 | 역할 변경 (`ADMIN`: `USER`↔`ADMIN`, `SYSTEM_ADMIN` 차단) | 높음 | 높음 | 권한 제어 핵심 |
|
||||||
|
| 2 | 사용자 삭제 | 중간 | 높음 | 기본 CRUD |
|
||||||
|
| 2 | 프로젝트 삭제 (`SYSTEM_ADMIN`만) | 중간 | 중간 | 시스템 관리 기능 |
|
||||||
|
| 2 | 자동화 설계 로직 제어 (조회/생성/삭제) | 높음 | 높음 | ADMIN 핵심 기능 |
|
||||||
|
| 3 | 검색/필터 | 낮음 | 중간 | 사용성 개선 |
|
||||||
|
| 3 | 정렬 기능 | 낮음 | 낮음 | 사용성 개선 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 보안 및 제약사항
|
||||||
|
|
||||||
|
### 7.1 백엔드 권한 검증 필수
|
||||||
|
- 모든 수정/삭제/역할변경 API에서 백엔드 권한 재검증 필수
|
||||||
|
- 클라이언트 UI는 참고만 하고, 서버에서 최종 판단
|
||||||
|
- `ADMIN`이 `SYSTEM_ADMIN`으로 변경 시도 시 차단
|
||||||
|
|
||||||
|
### 7.2 감시 로깅
|
||||||
|
- 프로젝트 삭제: `audit_logs` 기록
|
||||||
|
- 사용자 역할 변경: `audit_logs` 기록
|
||||||
|
- 사용자 삭제: `audit_logs` 기록
|
||||||
|
- 자동화 로직 삭제: `audit_logs` 기록
|
||||||
|
|
||||||
|
### 7.3 보호 조건
|
||||||
|
- **마지막 ADMIN 삭제 방지:** 회사당 최소 1명 ADMIN 유지
|
||||||
|
- **SYSTEM_ADMIN 역할 변경:** DB 직접 수정만 허용 (UI 제공 안함)
|
||||||
|
- **ADMIN의 자동화 로직 접근:** 자신 회사의 프로젝트에만 제한
|
||||||
|
- **회사 간 데이터 격리:** ADMIN이 타사 데이터 접근 불가
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 다국어 문구 추가 (ui_template_locale.ts)
|
||||||
|
|
||||||
|
다음 키를 `ui_template_locale.ts`에 추가:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 프로젝트 관리
|
||||||
|
B01_Dashboard_EditProject: ["프로젝트 수정", "Edit Project"],
|
||||||
|
B01_Dashboard_DeleteProject: ["프로젝트 삭제", "Delete Project"],
|
||||||
|
|
||||||
|
// 사용자 관리
|
||||||
|
B01_Dashboard_AddUser: ["사용자 추가", "Add User"],
|
||||||
|
B01_Dashboard_EditUser: ["사용자 수정", "Edit User"],
|
||||||
|
B01_Dashboard_DeleteUser: ["사용자 삭제", "Delete User"],
|
||||||
|
B01_Dashboard_ChangeRole: ["역할 변경", "Change Role"],
|
||||||
|
B01_Dashboard_SelectAvailableUsers: ["사용 가능한 사용자 선택", "Select Available Users"],
|
||||||
|
|
||||||
|
// 자동화 로직
|
||||||
|
B01_Dashboard_AutomationLogic: ["자동화 설계 로직", "Automation Logic"],
|
||||||
|
B01_Dashboard_CreateAutomation: ["자동화 생성", "Create Automation"],
|
||||||
|
B01_Dashboard_EditAutomation: ["자동화 수정", "Edit Automation"],
|
||||||
|
B01_Dashboard_DeleteAutomation: ["자동화 삭제", "Delete Automation"],
|
||||||
|
B01_Dashboard_ExecuteAutomation: ["자동화 실행", "Execute Automation"],
|
||||||
|
|
||||||
|
// 확인 메시지
|
||||||
|
B01_Dashboard_Confirm_DeleteProject: ["프로젝트를 삭제하시겠습니까? 되돌릴 수 없습니다.", "Delete this project? This cannot be undone."],
|
||||||
|
B01_Dashboard_Confirm_DeleteUser: ["사용자를 삭제하시겠습니까?", "Delete this user?"],
|
||||||
|
B01_Dashboard_Confirm_LastAdmin: ["회사의 유일한 관리자는 삭제할 수 없습니다.", "Cannot delete the last admin of the company."],
|
||||||
|
B01_Dashboard_CannotChangeToSystemAdmin: ["시스템 관리자로 변경할 수 없습니다.", "Cannot change role to System Admin."],
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 구현 체크리스트
|
||||||
|
|
||||||
|
### UI 컴포넌트
|
||||||
|
- [ ] 프로젝트 수정 모달 (`openEditProjectModal`)
|
||||||
|
- [ ] 프로젝트 삭제 확인 모달 (`openDeleteProjectModal`)
|
||||||
|
- [ ] 사용자 추가 모달 (`openAddUserModal` - 가입 계정 목록)
|
||||||
|
- [ ] 사용자 수정 모달 (`openEditUserModal`)
|
||||||
|
- [ ] 사용자 역할 변경 모달 (`openChangeRoleModal`)
|
||||||
|
- [ ] 사용자 삭제 확인 모달 (`openDeleteUserModal`)
|
||||||
|
- [ ] 자동화 로직 생성/수정 모달 (`openAutomationModal`)
|
||||||
|
- [ ] 자동화 로직 삭제 확인 모달 (`openDeleteAutomationModal`)
|
||||||
|
|
||||||
|
### 권한 검증 헬퍼
|
||||||
|
- [ ] `canEditProject(user, project)` - 프로젝트 수정 권한 확인
|
||||||
|
- [ ] `canDeleteProject(user)` - 프로젝트 삭제 권한 확인 (SYSTEM_ADMIN만)
|
||||||
|
- [ ] `canAddUser(user)` - 사용자 추가 권한 확인
|
||||||
|
- [ ] `canChangeRole(user, targetUser)` - 역할 변경 권한 확인
|
||||||
|
- [ ] `canDeleteUser(user, targetUser)` - 사용자 삭제 권한 확인
|
||||||
|
- [ ] `canManageAutomation(user, automation)` - 자동화 로직 제어 권한 확인
|
||||||
|
- [ ] `isLastAdmin(user, company)` - 회사의 마지막 ADMIN 확인
|
||||||
|
|
||||||
|
### API 구현 (백엔드)
|
||||||
|
- [ ] 프로젝트 수정 API - 권한 검증
|
||||||
|
- [ ] 프로젝트 삭제 API - SYSTEM_ADMIN만 허용
|
||||||
|
- [ ] 사용자 추가 API - 가입 계정 기반
|
||||||
|
- [ ] 사용자 정보 수정 API - 권한별 필드 제한
|
||||||
|
- [ ] 사용자 역할 변경 API - SYSTEM_ADMIN 변경 차단
|
||||||
|
- [ ] 사용자 삭제 API - 마지막 ADMIN 보호
|
||||||
|
- [ ] 자동화 로직 조회/생성/수정/삭제 API - 권한 검증
|
||||||
|
|
||||||
|
### 테스트 시나리오
|
||||||
|
- [ ] SYSTEM_ADMIN: 모든 기능 가능 확인
|
||||||
|
- [ ] ADMIN: 회사 내 기능만 가능 확인
|
||||||
|
- [ ] ADMIN: SYSTEM_ADMIN으로 변경 시도 → 차단 확인
|
||||||
|
- [ ] USER: 프로젝트 수정만 가능 확인
|
||||||
|
- [ ] USER: 사용자 관리 불가 확인
|
||||||
|
- [ ] 마지막 ADMIN 삭제 시도 → 차단 확인
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 비고
|
||||||
|
|
||||||
|
- **코드 작업:** 다른 AI 담당
|
||||||
|
- **계획서 위치:** `.agent/plan_dashboard_management.md`
|
||||||
|
- **참고 자료:**
|
||||||
|
- `.agent/structure.md` - 폴더 구조
|
||||||
|
- `.agent/frontend.md` - UI 가이드라인
|
||||||
|
- `.agent/backend.md` - 백엔드 명세
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# 대시보드 관리 기능 검증 보고서 (Validation Report)
|
||||||
|
|
||||||
|
본 문서는 `.agent/plan_dashboard_management.md` 계획서를 기준으로 `B01_Dashboard` 관련 프론트엔드 및 백엔드 코드를 검증한 결과를 담고 있습니다.
|
||||||
|
|
||||||
|
## 1. 종합 평가
|
||||||
|
- **백엔드 (FastAPI/Python):** 계획서상 정의된 핵심 비즈니스 로직(역할 변경 및 제약조건, 마지막 ADMIN 보호 등)은 일정 부분 반영되었으나, **자동화 로직의 USER 조회 권한 누수** 등 계획서 명세와 다른 부분이 발견되었습니다.
|
||||||
|
- **프론트엔드 (TypeScript):** 프로젝트 수정/삭제, 사용자 수정/삭제/역할변경, 자동화 설계 로직 모달 등 계획서 9번에 정의된 **주요 UI 컴포넌트(모달) 및 권한 검증 헬퍼가 대부분 미구현(누락)** 상태입니다.
|
||||||
|
- **다국어 (i18n):** 계획서 8번에서 명시한 다국어 문구 키들이 `ui_template_locale.ts`에 전혀 반영되지 않았습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 세부 검증 항목 및 대조 결과
|
||||||
|
|
||||||
|
### 2.1 UI 컴포넌트 및 제어 (프론트엔드)
|
||||||
|
|
||||||
|
| 요구 기능 (계획서 9. UI 컴포넌트) | 구현 상태 | 확인 코드 및 위치 | 특이사항 / 개선 필요 사항 |
|
||||||
|
| :--- | :---: | :--- | :--- |
|
||||||
|
| 프로젝트 수정 모달 (`openEditProjectModal`) | **❌ 미구현** | `B01_Dashboard_UI_Page.ts` | 프로젝트 목록에 수정 버튼 및 모달이 존재하지 않음 |
|
||||||
|
| 프로젝트 삭제 확인 모달 (`openDeleteProjectModal`) | **❌ 미구현** | `B01_Dashboard_UI_Page.ts` | 프로젝트 삭제 버튼 및 경고 모달 없음 |
|
||||||
|
| 사용자 추가 모달 (`openAddUserModal`) | **⚠️ 부분구현** | [openAddMemberModal](file:///D:/02_Software_Prog/%EC%9E%84%EB%8F%84%EC%84%A4%EA%B3%84%20%EB%B0%8F%20%EA%B2%AC%EC%A0%81%EC%9E%90%EB%8F%99%ED%99%94%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8%20%EA%B0%9C%EB%B0%9C/B01_Dashboard/B01_Dashboard_UI_Page.ts#L753) | 가입 계정 목록 조회 형식이 아니라 단순 이메일 입력 방식임 |
|
||||||
|
| 사용자 수정 모달 (`openEditUserModal`) | **❌ 미구현** | `B01_Dashboard_UI_Page.ts` | 사용자 수정 액션 및 모달 없음 |
|
||||||
|
| 사용자 역할 변경 모달 (`openChangeRoleModal`) | **❌ 미구현** | `B01_Dashboard_UI_Page.ts` | 역할 변경 모달 없음 |
|
||||||
|
| 사용자 삭제 확인 모달 (`openDeleteUserModal`) | **❌ 미구현** | `B01_Dashboard_UI_Page.ts` | 삭제 버튼은 있으나 단순 API 호출 처리 (경고 모달 없음) |
|
||||||
|
| 자동화 로직 생성/수정 모달 (`openAutomationModal`) | **❌ 미구현** | `B01_Dashboard_UI_Page.ts` | 대시보드 UI에 자동화 설계 로직 섹션이 완전 누락됨 |
|
||||||
|
| 자동화 로직 삭제 확인 모달 (`openDeleteAutomationModal`) | **❌ 미구현** | `B01_Dashboard_UI_Page.ts` | 위와 동일 |
|
||||||
|
|
||||||
|
### 2.2 권한 검증 헬퍼 (프론트엔드)
|
||||||
|
|
||||||
|
| 요구 헬퍼 함수 (계획서 9. 권한 검증 헬퍼) | 구현 상태 | 개선 필요 사항 |
|
||||||
|
| :--- | :---: | :--- |
|
||||||
|
| `canEditProject(user, project)` | **❌ 미구현** | UI 단의 권한 체크 분기 부재 |
|
||||||
|
| `canDeleteProject(user)` | **❌ 미구현** | SYSTEM_ADMIN 분기 처리 미흡 |
|
||||||
|
| `canAddUser(user)` | **❌ 미구현** | - |
|
||||||
|
| `canChangeRole(user, targetUser)` | **❌ 미구현** | - |
|
||||||
|
| `canDeleteUser(user, targetUser)` | **❌ 미구현** | - |
|
||||||
|
| `canManageAutomation(user, automation)` | **❌ 미구현** | - |
|
||||||
|
| `isLastAdmin(user, company)` | **❌ 미구현** | 프론트엔드 예외 처리 없음 (백엔드만 동작) |
|
||||||
|
|
||||||
|
### 2.3 API 및 백엔드 권한 검증 (백엔드)
|
||||||
|
|
||||||
|
| API 엔드포인트 / 기능 | 계획서 스펙 | 실제 구현 위치 | 검증 결과 |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| 프로젝트 수정 API | `/api/projects/{id}` | [dashboard_update_project](file:///D:/02_Software_Prog/%EC%9E%84%EB%8F%84%EC%84%A4%EA%B3%84%20%EB%B0%8F%20%EA%B2%AC%EC%A0%81%EC%9E%90%EB%8F%99%ED%99%94%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8%20%EA%B0%9C%EB%B0%9C/B01_Dashboard/B01_Dashboard_Router.py#L211) | **통과 (경로명 상이)**: 실 주소는 `/api/dashboard/projects/{project_id}` 임 |
|
||||||
|
| 프로젝트 삭제 API | `/api/projects/{id}` (SYSTEM_ADMIN만) | [dashboard_delete_project](file:///D:/02_Software_Prog/%EC%9E%84%EB%8F%84%EC%84%A4%EA%B3%84%20%EB%B0%8F%20%EA%B2%AC%EC%A0%81%EC%9E%90%EB%8F%99%ED%99%94%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8%20%EA%B0%9C%EB%B0%9C/B01_Dashboard/B01_Dashboard_Router.py#L227) | **통과 (경로명 상이)**: `require_system_admin` 데코레이터 적용 완료 |
|
||||||
|
| 사용자 추가 API | `/api/companies/{cid}/users` | [admin_add_member](file:///D:/02_Software_Prog/%EC%9E%84%EB%8F%84%EC%84%A4%EA%B3%84%20%EB%B0%8F%20%EA%B2%AC%EC%A0%81%EC%9E%90%EB%8F%99%ED%99%94%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8%20%EA%B0%9C%EB%B0%9C/B01_Dashboard/B01_Dashboard_Router.py#L160) | **통과**: `require_company_admin`으로 가입 계정 추가 제어 |
|
||||||
|
| 사용자 정보 수정 API | `/api/users/{id}` (필드 제한) | [admin_update_user](file:///D:/02_Software_Prog/%EC%9E%84%EB%8F%84%EC%84%A4%EA%B3%84%20%EB%B0%8F%20%EA%B2%AC%EC%A0%81%EC%9E%90%EB%8F%99%ED%99%94%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8%20%EA%B0%9C%EB%B0%9C/B01_Dashboard/B01_Dashboard_Router.py#L278) | **부분 통과**: USER일 때 status를 강제 비우는 세이프가드는 있으나, 다른 관리자 필드 차단 확인 필요 |
|
||||||
|
| 사용자 역할 변경 API | `/api/users/{id}/role` | [system_change_role](file:///D:/02_Software_Prog/%EC%9E%84%EB%8F%84%EC%84%A4%EA%B3%84%20%EB%B0%8F%20%EA%B2%AC%EC%A0%81%EC%9E%90%EB%8F%99%ED%99%94%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8%20%EA%B0%9C%EB%B0%9C/B01_Dashboard/B01_Dashboard_Router.py#L249) | **통과**: SYSTEM_ADMIN 변경 방지 및 마지막 ADMIN 변경 불가 보장 |
|
||||||
|
| 사용자 삭제 API | `/api/companies/{cid}/users/{uid}` | [admin_remove_member](file:///D:/02_Software_Prog/%EC%9E%84%EB%8F%84%EC%84%A4%EA%B3%84%20%EB%B0%8F%20%EA%B2%AC%EC%A0%81%EC%9E%90%EB%8F%99%ED%99%94%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8%20%EA%B0%9C%EB%B0%9C/B01_Dashboard/B01_Dashboard_Router.py#L171) | **통과**: 맴버 삭제 처리 로직 작동 |
|
||||||
|
| 자동화 로직 조회 API | `/api/projects/{pid}/automations` | [dashboard_project_automations](file:///D:/02_Software_Prog/%EC%9E%84%EB%8F%84%EC%84%A4%EA%B3%84%20%EB%B0%8F%20%EA%B2%AC%EC%A0%81%EC%9E%90%EB%8F%99%ED%99%94%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8%20%EA%B0%9C%EB%B0%9C/B01_Dashboard/B01_Dashboard_Router.py#L350) | **❌ 위반**: 일반 USER 권한도 조회 가능하게 설계되어 계획서의 USER 조회 불가(매트릭스 2.4) 위반 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 핵심 수정 필요 사항
|
||||||
|
|
||||||
|
1. **다국어 키 반영:**
|
||||||
|
`ui_template_locale.ts` 파일 하단에 계획서 8번의 다국어 문구 객체를 온전히 삽입해야 합니다.
|
||||||
|
2. **백엔드 권한 통제 강화:**
|
||||||
|
`B01_Dashboard_Router.py`의 `dashboard_project_automations` 등에서 `USER` 권한을 가진 사용자의 자동화 로직 조회를 차단해야 합니다.
|
||||||
|
3. **프론트엔드 UI/UX 완성:**
|
||||||
|
계획서에 제시된 수정/삭제 모달 창을 `B01_Dashboard_UI_Page.ts`에 추가 구현하고 관련 버튼들을 조건부 활성화 및 노출 처리해야 합니다.
|
||||||
@@ -18,6 +18,10 @@ export interface ProjectItem {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
region?: string | null;
|
region?: string | null;
|
||||||
|
road_type?: string | null;
|
||||||
|
project_year?: number | null;
|
||||||
|
estimated_length_m?: number | null;
|
||||||
|
memo?: string | null;
|
||||||
status?: string | null;
|
status?: string | null;
|
||||||
owner_name?: string | null;
|
owner_name?: string | null;
|
||||||
workflow_stage: number;
|
workflow_stage: number;
|
||||||
@@ -92,6 +96,18 @@ export interface ResourceData {
|
|||||||
stats: ResourceSnapshot;
|
stats: ResourceSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AutomationItem {
|
||||||
|
id: number;
|
||||||
|
project_id: string;
|
||||||
|
name: string;
|
||||||
|
logic_type: string;
|
||||||
|
config_json: Record<string, unknown>;
|
||||||
|
status: "DRAFT" | "ACTIVE" | "INACTIVE";
|
||||||
|
last_executed_at?: string | null;
|
||||||
|
created_at?: string | null;
|
||||||
|
updated_at?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UpdateUserRequest {
|
export interface UpdateUserRequest {
|
||||||
name: string;
|
name: string;
|
||||||
position?: string | null;
|
position?: string | null;
|
||||||
@@ -99,6 +115,27 @@ export interface UpdateUserRequest {
|
|||||||
phone?: string | null;
|
phone?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateProjectRequest {
|
||||||
|
name: string;
|
||||||
|
region?: string | null;
|
||||||
|
road_type?: string | null;
|
||||||
|
project_year?: number | null;
|
||||||
|
estimated_length_m?: number | null;
|
||||||
|
memo?: string | null;
|
||||||
|
status?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminUpdateUserRequest extends UpdateUserRequest {
|
||||||
|
status?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomationRequest {
|
||||||
|
name: string;
|
||||||
|
logic_type: string;
|
||||||
|
config_json: Record<string, unknown>;
|
||||||
|
status: "DRAFT" | "ACTIVE" | "INACTIVE";
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateCompanyRequest {
|
export interface CreateCompanyRequest {
|
||||||
name: string;
|
name: string;
|
||||||
business_registration_number: string;
|
business_registration_number: string;
|
||||||
@@ -212,7 +249,6 @@ export async function fetchAllProjects(): Promise<ProjectItem[]> {
|
|||||||
return data.projects;
|
return data.projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function fetchAllCompanies(): Promise<CompanyInfo[]> {
|
export async function fetchAllCompanies(): Promise<CompanyInfo[]> {
|
||||||
const data = await request<{ companies: CompanyInfo[] }>("/dashboard/admin/companies");
|
const data = await request<{ companies: CompanyInfo[] }>("/dashboard/admin/companies");
|
||||||
return data.companies;
|
return data.companies;
|
||||||
@@ -230,6 +266,27 @@ export function changeUserRole(userId: number, role: string): Promise<unknown> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateProject(projectId: string, payload: UpdateProjectRequest): Promise<unknown> {
|
||||||
|
return request(`/dashboard/projects/${encodeURIComponent(projectId)}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: body(payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteProject(projectId: string): Promise<unknown> {
|
||||||
|
return request(`/dashboard/projects/${encodeURIComponent(projectId)}`, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateDashboardUser(
|
||||||
|
userId: number,
|
||||||
|
payload: AdminUpdateUserRequest,
|
||||||
|
): Promise<unknown> {
|
||||||
|
return request(`/dashboard/admin/users/${userId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: body(payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function assignUserToCompany(userId: number, companyId: number | null): Promise<unknown> {
|
export function assignUserToCompany(userId: number, companyId: number | null): Promise<unknown> {
|
||||||
return request(`/dashboard/admin/users/${userId}/company`, {
|
return request(`/dashboard/admin/users/${userId}/company`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
@@ -254,3 +311,35 @@ export async function fetchAuditLogs(): Promise<AuditLog[]> {
|
|||||||
export async function fetchSystemResources(days = 30): Promise<ResourceData> {
|
export async function fetchSystemResources(days = 30): Promise<ResourceData> {
|
||||||
return request<ResourceData>(`/dashboard/admin/resources?days=${encodeURIComponent(days)}`);
|
return request<ResourceData>(`/dashboard/admin/resources?days=${encodeURIComponent(days)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchProjectAutomations(projectId: string): Promise<AutomationItem[]> {
|
||||||
|
const data = await request<{ automations: AutomationItem[] }>(
|
||||||
|
`/dashboard/projects/${encodeURIComponent(projectId)}/automations`,
|
||||||
|
);
|
||||||
|
return data.automations;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProjectAutomation(
|
||||||
|
projectId: string,
|
||||||
|
payload: AutomationRequest,
|
||||||
|
): Promise<unknown> {
|
||||||
|
return request(`/dashboard/projects/${encodeURIComponent(projectId)}/automations`, {
|
||||||
|
method: "POST",
|
||||||
|
body: body(payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateProjectAutomation(
|
||||||
|
automationId: number,
|
||||||
|
payload: AutomationRequest,
|
||||||
|
): Promise<unknown> {
|
||||||
|
return request(`/dashboard/automations/${automationId}`, { method: "PUT", body: body(payload) });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteProjectAutomation(automationId: number): Promise<unknown> {
|
||||||
|
return request(`/dashboard/automations/${automationId}`, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function executeProjectAutomation(automationId: number): Promise<unknown> {
|
||||||
|
return request(`/dashboard/automations/${automationId}/execute`, { method: "POST" });
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -116,6 +117,69 @@ async def list_all_projects() -> list[dict[str, Any]]:
|
|||||||
return [_project_row(row) for row in await cursor.fetchall()]
|
return [_project_row(row) for row in await cursor.fetchall()]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_project(project_id: str) -> dict[str, Any] | None:
|
||||||
|
pool = get_db_pool()
|
||||||
|
async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor:
|
||||||
|
await cursor.execute(
|
||||||
|
"""SELECT id, user_id, company_id, name, region, road_type, project_year,
|
||||||
|
estimated_length_m, memo, status
|
||||||
|
FROM projects WHERE id = %s AND deleted_at IS NULL""",
|
||||||
|
(project_id,),
|
||||||
|
)
|
||||||
|
return await cursor.fetchone()
|
||||||
|
|
||||||
|
|
||||||
|
async def update_project(project_id: str, data: dict[str, Any], actor_id: int) -> bool:
|
||||||
|
pool = get_db_pool()
|
||||||
|
async with pool.acquire() as connection, connection.cursor() as cursor:
|
||||||
|
await connection.begin()
|
||||||
|
await cursor.execute(
|
||||||
|
"""UPDATE projects
|
||||||
|
SET name = %s, region = %s, road_type = %s, project_year = %s,
|
||||||
|
estimated_length_m = %s, memo = %s, status = COALESCE(%s, status)
|
||||||
|
WHERE id = %s AND deleted_at IS NULL""",
|
||||||
|
(
|
||||||
|
data["name"],
|
||||||
|
data.get("region"),
|
||||||
|
data.get("road_type"),
|
||||||
|
data.get("project_year"),
|
||||||
|
data.get("estimated_length_m"),
|
||||||
|
data.get("memo"),
|
||||||
|
data.get("status"),
|
||||||
|
project_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
changed = cursor.rowcount > 0
|
||||||
|
if changed:
|
||||||
|
await cursor.execute(
|
||||||
|
"""INSERT INTO system_audit_logs (user_id, action, resource_type, resource_id)
|
||||||
|
VALUES (%s, 'PROJECT_UPDATE', 'project', NULL)""",
|
||||||
|
(actor_id,),
|
||||||
|
)
|
||||||
|
await connection.commit()
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
async def soft_delete_project(project_id: str, actor_id: int) -> bool:
|
||||||
|
pool = get_db_pool()
|
||||||
|
async with pool.acquire() as connection, connection.cursor() as cursor:
|
||||||
|
await connection.begin()
|
||||||
|
await cursor.execute(
|
||||||
|
"""UPDATE projects SET deleted_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = %s AND deleted_at IS NULL""",
|
||||||
|
(project_id,),
|
||||||
|
)
|
||||||
|
changed = cursor.rowcount > 0
|
||||||
|
if changed:
|
||||||
|
await cursor.execute(
|
||||||
|
"""INSERT INTO system_audit_logs (user_id, action, resource_type, resource_id)
|
||||||
|
VALUES (%s, 'PROJECT_DELETE', 'project', NULL)""",
|
||||||
|
(actor_id,),
|
||||||
|
)
|
||||||
|
await connection.commit()
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
async def get_user_company(company_id: int | None) -> dict[str, Any] | None:
|
async def get_user_company(company_id: int | None) -> dict[str, Any] | None:
|
||||||
if company_id is None:
|
if company_id is None:
|
||||||
return None
|
return None
|
||||||
@@ -358,6 +422,55 @@ async def change_user_role(user_id: int, role: str) -> bool:
|
|||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_admin_target(user_id: int) -> dict[str, Any] | None:
|
||||||
|
pool = get_db_pool()
|
||||||
|
async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor:
|
||||||
|
await cursor.execute(
|
||||||
|
"""SELECT id, company_id, role, is_master, status
|
||||||
|
FROM users WHERE id = %s AND deleted_at IS NULL""",
|
||||||
|
(user_id,),
|
||||||
|
)
|
||||||
|
row = await cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
row["role"] = _role(row.get("role"))
|
||||||
|
row["is_master"] = bool(row.get("is_master"))
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
async def update_admin_user(user_id: int, data: dict[str, Any]) -> bool:
|
||||||
|
pool = get_db_pool()
|
||||||
|
async with pool.acquire() as connection, connection.cursor() as cursor:
|
||||||
|
await cursor.execute(
|
||||||
|
"""UPDATE users
|
||||||
|
SET name = %s, position = %s, department = %s, phone = %s,
|
||||||
|
status = COALESCE(%s, status)
|
||||||
|
WHERE id = %s AND deleted_at IS NULL""",
|
||||||
|
(
|
||||||
|
data["name"],
|
||||||
|
data.get("position"),
|
||||||
|
data.get("department"),
|
||||||
|
data.get("phone"),
|
||||||
|
data.get("status"),
|
||||||
|
user_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
changed = cursor.rowcount > 0
|
||||||
|
await connection.commit()
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
async def count_company_admins(company_id: int) -> int:
|
||||||
|
pool = get_db_pool()
|
||||||
|
async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor:
|
||||||
|
await cursor.execute(
|
||||||
|
"""SELECT COUNT(*) AS cnt FROM users
|
||||||
|
WHERE company_id = %s AND role = 'ADMIN' AND deleted_at IS NULL""",
|
||||||
|
(company_id,),
|
||||||
|
)
|
||||||
|
row = await cursor.fetchone()
|
||||||
|
return int(row["cnt"] if row else 0)
|
||||||
|
|
||||||
|
|
||||||
async def assign_user_company(user_id: int, company_id: int | None) -> bool:
|
async def assign_user_company(user_id: int, company_id: int | None) -> bool:
|
||||||
status = "ACTIVE" if company_id else "NO_COMPANY"
|
status = "ACTIVE" if company_id else "NO_COMPANY"
|
||||||
pool = get_db_pool()
|
pool = get_db_pool()
|
||||||
@@ -442,3 +555,114 @@ async def get_system_resources(days: int = 30) -> dict[str, Any]:
|
|||||||
(bucket_seconds, bucket_seconds, since, bucket_seconds),
|
(bucket_seconds, bucket_seconds, since, bucket_seconds),
|
||||||
)
|
)
|
||||||
return {"current": current, "history": list(await cursor.fetchall()), "stats": current}
|
return {"current": current, "history": list(await cursor.fetchall()), "stats": current}
|
||||||
|
|
||||||
|
|
||||||
|
async def list_project_automations(project_id: str) -> list[dict[str, Any]]:
|
||||||
|
pool = get_db_pool()
|
||||||
|
async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor:
|
||||||
|
await cursor.execute(
|
||||||
|
"""SELECT id, project_id, name, logic_type, config_json, status,
|
||||||
|
last_executed_at, created_at, updated_at
|
||||||
|
FROM project_automations
|
||||||
|
WHERE project_id = %s AND deleted_at IS NULL
|
||||||
|
ORDER BY updated_at DESC, created_at DESC""",
|
||||||
|
(project_id,),
|
||||||
|
)
|
||||||
|
return list(await cursor.fetchall())
|
||||||
|
|
||||||
|
|
||||||
|
async def create_project_automation(
|
||||||
|
project_id: str, actor_id: int, data: dict[str, Any]
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
pool = get_db_pool()
|
||||||
|
async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor:
|
||||||
|
await connection.begin()
|
||||||
|
await cursor.execute(
|
||||||
|
"""INSERT INTO project_automations
|
||||||
|
(project_id, name, logic_type, config_json, status, created_by, updated_by)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s)""",
|
||||||
|
(
|
||||||
|
project_id,
|
||||||
|
data["name"],
|
||||||
|
data["logic_type"],
|
||||||
|
json.dumps(data["config_json"], ensure_ascii=False),
|
||||||
|
data["status"],
|
||||||
|
actor_id,
|
||||||
|
actor_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
automation_id = cursor.lastrowid
|
||||||
|
await connection.commit()
|
||||||
|
await cursor.execute("SELECT * FROM project_automations WHERE id = %s", (automation_id,))
|
||||||
|
return await cursor.fetchone()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_project_automation(automation_id: int) -> dict[str, Any] | None:
|
||||||
|
pool = get_db_pool()
|
||||||
|
async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor:
|
||||||
|
await cursor.execute(
|
||||||
|
"""SELECT a.*, p.company_id
|
||||||
|
FROM project_automations a
|
||||||
|
JOIN projects p ON p.id = a.project_id
|
||||||
|
WHERE a.id = %s AND a.deleted_at IS NULL AND p.deleted_at IS NULL""",
|
||||||
|
(automation_id,),
|
||||||
|
)
|
||||||
|
return await cursor.fetchone()
|
||||||
|
|
||||||
|
|
||||||
|
async def update_project_automation(
|
||||||
|
automation_id: int, actor_id: int, data: dict[str, Any]
|
||||||
|
) -> bool:
|
||||||
|
pool = get_db_pool()
|
||||||
|
async with pool.acquire() as connection, connection.cursor() as cursor:
|
||||||
|
await cursor.execute(
|
||||||
|
"""UPDATE project_automations
|
||||||
|
SET name = %s, logic_type = %s, config_json = %s, status = %s, updated_by = %s
|
||||||
|
WHERE id = %s AND deleted_at IS NULL""",
|
||||||
|
(
|
||||||
|
data["name"],
|
||||||
|
data["logic_type"],
|
||||||
|
json.dumps(data["config_json"], ensure_ascii=False),
|
||||||
|
data["status"],
|
||||||
|
actor_id,
|
||||||
|
automation_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
changed = cursor.rowcount > 0
|
||||||
|
await connection.commit()
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_project_automation(automation_id: int, actor_id: int) -> bool:
|
||||||
|
pool = get_db_pool()
|
||||||
|
async with pool.acquire() as connection, connection.cursor() as cursor:
|
||||||
|
await connection.begin()
|
||||||
|
await cursor.execute(
|
||||||
|
"""UPDATE project_automations
|
||||||
|
SET deleted_at = CURRENT_TIMESTAMP, updated_by = %s
|
||||||
|
WHERE id = %s AND deleted_at IS NULL""",
|
||||||
|
(actor_id, automation_id),
|
||||||
|
)
|
||||||
|
changed = cursor.rowcount > 0
|
||||||
|
if changed:
|
||||||
|
await cursor.execute(
|
||||||
|
"""INSERT INTO system_audit_logs (user_id, action, resource_type, resource_id)
|
||||||
|
VALUES (%s, 'AUTOMATION_DELETE', 'automation', %s)""",
|
||||||
|
(actor_id, automation_id),
|
||||||
|
)
|
||||||
|
await connection.commit()
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
async def mark_project_automation_executed(automation_id: int, actor_id: int) -> bool:
|
||||||
|
pool = get_db_pool()
|
||||||
|
async with pool.acquire() as connection, connection.cursor() as cursor:
|
||||||
|
await cursor.execute(
|
||||||
|
"""UPDATE project_automations
|
||||||
|
SET last_executed_at = CURRENT_TIMESTAMP, updated_by = %s
|
||||||
|
WHERE id = %s AND deleted_at IS NULL""",
|
||||||
|
(actor_id, automation_id),
|
||||||
|
)
|
||||||
|
changed = cursor.rowcount > 0
|
||||||
|
await connection.commit()
|
||||||
|
return changed
|
||||||
|
|||||||
@@ -10,9 +10,15 @@ from .B01_Dashboard_Repository import (
|
|||||||
add_company_member,
|
add_company_member,
|
||||||
assign_user_company,
|
assign_user_company,
|
||||||
change_user_role,
|
change_user_role,
|
||||||
|
count_company_admins,
|
||||||
create_company,
|
create_company,
|
||||||
|
create_project_automation,
|
||||||
|
delete_project_automation,
|
||||||
get_dashboard_me,
|
get_dashboard_me,
|
||||||
|
get_project,
|
||||||
|
get_project_automation,
|
||||||
get_system_resources,
|
get_system_resources,
|
||||||
|
get_user_admin_target,
|
||||||
get_user_company,
|
get_user_company,
|
||||||
join_company,
|
join_company,
|
||||||
list_all_companies,
|
list_all_companies,
|
||||||
@@ -22,19 +28,28 @@ from .B01_Dashboard_Repository import (
|
|||||||
list_company_members,
|
list_company_members,
|
||||||
list_company_projects,
|
list_company_projects,
|
||||||
list_join_requests,
|
list_join_requests,
|
||||||
|
list_project_automations,
|
||||||
list_user_projects,
|
list_user_projects,
|
||||||
|
mark_project_automation_executed,
|
||||||
process_join_request,
|
process_join_request,
|
||||||
remove_company_member,
|
remove_company_member,
|
||||||
search_companies,
|
search_companies,
|
||||||
|
soft_delete_project,
|
||||||
|
update_admin_user,
|
||||||
|
update_project,
|
||||||
|
update_project_automation,
|
||||||
update_user_profile,
|
update_user_profile,
|
||||||
)
|
)
|
||||||
from .B01_Dashboard_Schema import (
|
from .B01_Dashboard_Schema import (
|
||||||
AddMemberRequest,
|
AddMemberRequest,
|
||||||
|
AdminUpdateUserRequest,
|
||||||
AssignCompanyRequest,
|
AssignCompanyRequest,
|
||||||
|
AutomationRequest,
|
||||||
ChangeUserRoleRequest,
|
ChangeUserRoleRequest,
|
||||||
CreateCompanyRequest,
|
CreateCompanyRequest,
|
||||||
JoinCompanyRequest,
|
JoinCompanyRequest,
|
||||||
ProcessJoinRequest,
|
ProcessJoinRequest,
|
||||||
|
UpdateProjectRequest,
|
||||||
UpdateUserRequest,
|
UpdateUserRequest,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,6 +71,30 @@ def _require_company_id(session: dict[str, Any]) -> int:
|
|||||||
return int(company_id)
|
return int(company_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _same_company(session: dict[str, Any], company_id: int | None) -> bool:
|
||||||
|
return company_id is not None and int(session.get("company_id") or 0) == int(company_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _can_edit_project(session: dict[str, Any], project: dict[str, Any]) -> bool:
|
||||||
|
if session["role"] == "SYSTEM_ADMIN":
|
||||||
|
return True
|
||||||
|
return _same_company(session, project.get("company_id"))
|
||||||
|
|
||||||
|
|
||||||
|
def _can_manage_automation(session: dict[str, Any], project: dict[str, Any]) -> bool:
|
||||||
|
if session["role"] == "SYSTEM_ADMIN":
|
||||||
|
return True
|
||||||
|
return session["role"] == "ADMIN" and _same_company(session, project.get("company_id"))
|
||||||
|
|
||||||
|
|
||||||
|
def _can_edit_user(session: dict[str, Any], target: dict[str, Any]) -> bool:
|
||||||
|
if session["role"] == "SYSTEM_ADMIN":
|
||||||
|
return True
|
||||||
|
if session["role"] == "ADMIN":
|
||||||
|
return _same_company(session, target.get("company_id"))
|
||||||
|
return int(session["user_id"]) == int(target["id"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/me")
|
@router.get("/me")
|
||||||
async def dashboard_me(session: dict[str, Any] = Depends(verify_session)):
|
async def dashboard_me(session: dict[str, Any] = Depends(verify_session)):
|
||||||
user = await get_dashboard_me(int(session["user_id"]))
|
user = await get_dashboard_me(int(session["user_id"]))
|
||||||
@@ -149,11 +188,12 @@ async def admin_process_join_request(
|
|||||||
payload: ProcessJoinRequest,
|
payload: ProcessJoinRequest,
|
||||||
session: dict[str, Any] = Depends(require_company_admin),
|
session: dict[str, Any] = Depends(require_company_admin),
|
||||||
):
|
):
|
||||||
|
company_id = None if session["role"] == "SYSTEM_ADMIN" else _require_company_id(session)
|
||||||
changed = await process_join_request(
|
changed = await process_join_request(
|
||||||
request_id,
|
request_id,
|
||||||
int(session["user_id"]),
|
int(session["user_id"]),
|
||||||
payload.action == "APPROVE",
|
payload.action == "APPROVE",
|
||||||
_require_company_id(session),
|
company_id,
|
||||||
)
|
)
|
||||||
if not changed:
|
if not changed:
|
||||||
raise HTTPException(status_code=404, detail="처리할 가입 신청을 찾을 수 없습니다.")
|
raise HTTPException(status_code=404, detail="처리할 가입 신청을 찾을 수 없습니다.")
|
||||||
@@ -168,6 +208,32 @@ async def admin_projects(session: dict[str, Any] = Depends(require_company_admin
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/projects/{project_id}")
|
||||||
|
async def dashboard_update_project(
|
||||||
|
project_id: str,
|
||||||
|
payload: UpdateProjectRequest,
|
||||||
|
session: dict[str, Any] = Depends(verify_session),
|
||||||
|
):
|
||||||
|
project = await get_project(project_id)
|
||||||
|
if not project:
|
||||||
|
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다.")
|
||||||
|
if not _can_edit_project(session, project):
|
||||||
|
raise HTTPException(status_code=403, detail="프로젝트 수정 권한이 없습니다.")
|
||||||
|
if not await update_project(project_id, payload.model_dump(), int(session["user_id"])):
|
||||||
|
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다.")
|
||||||
|
return {"status": "success"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/projects/{project_id}")
|
||||||
|
async def dashboard_delete_project(
|
||||||
|
project_id: str,
|
||||||
|
session: dict[str, Any] = Depends(require_system_admin),
|
||||||
|
):
|
||||||
|
if not await soft_delete_project(project_id, int(session["user_id"])):
|
||||||
|
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다.")
|
||||||
|
return {"status": "success"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/admin/companies")
|
@router.get("/admin/companies")
|
||||||
async def system_companies(session: dict[str, Any] = Depends(require_system_admin)):
|
async def system_companies(session: dict[str, Any] = Depends(require_system_admin)):
|
||||||
_ = session
|
_ = session
|
||||||
@@ -184,14 +250,50 @@ async def system_users(session: dict[str, Any] = Depends(require_system_admin)):
|
|||||||
async def system_change_role(
|
async def system_change_role(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
payload: ChangeUserRoleRequest,
|
payload: ChangeUserRoleRequest,
|
||||||
session: dict[str, Any] = Depends(require_system_admin),
|
session: dict[str, Any] = Depends(verify_session),
|
||||||
):
|
):
|
||||||
_ = session
|
target = await get_user_admin_target(user_id)
|
||||||
|
if not target:
|
||||||
|
raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다.")
|
||||||
|
if payload.role == "SYSTEM_ADMIN" or target["role"] == "SYSTEM_ADMIN":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403, detail="시스템 관리자 역할은 API에서 변경할 수 없습니다."
|
||||||
|
)
|
||||||
|
if session["role"] == "ADMIN":
|
||||||
|
if not _same_company(session, target.get("company_id")):
|
||||||
|
raise HTTPException(status_code=403, detail="같은 회사 사용자만 변경할 수 있습니다.")
|
||||||
|
if target["role"] == "ADMIN" and payload.role == "USER":
|
||||||
|
admins = await count_company_admins(int(target["company_id"]))
|
||||||
|
if admins <= 1:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=409, detail="회사의 마지막 관리자는 변경할 수 없습니다."
|
||||||
|
)
|
||||||
|
elif session["role"] != "SYSTEM_ADMIN":
|
||||||
|
raise HTTPException(status_code=403, detail="역할 변경 권한이 없습니다.")
|
||||||
if not await change_user_role(user_id, payload.role):
|
if not await change_user_role(user_id, payload.role):
|
||||||
raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다.")
|
raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다.")
|
||||||
return {"status": "success"}
|
return {"status": "success"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/admin/users/{user_id}")
|
||||||
|
async def admin_update_user(
|
||||||
|
user_id: int,
|
||||||
|
payload: AdminUpdateUserRequest,
|
||||||
|
session: dict[str, Any] = Depends(verify_session),
|
||||||
|
):
|
||||||
|
target = await get_user_admin_target(user_id)
|
||||||
|
if not target:
|
||||||
|
raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다.")
|
||||||
|
if not _can_edit_user(session, target):
|
||||||
|
raise HTTPException(status_code=403, detail="사용자 수정 권한이 없습니다.")
|
||||||
|
data = payload.model_dump()
|
||||||
|
if session["role"] == "USER":
|
||||||
|
data["status"] = None
|
||||||
|
if not await update_admin_user(user_id, data):
|
||||||
|
raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다.")
|
||||||
|
return {"status": "success"}
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/admin/users/{user_id}/company")
|
@router.patch("/admin/users/{user_id}/company")
|
||||||
async def system_assign_company(
|
async def system_assign_company(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
@@ -244,3 +346,80 @@ async def system_projects(session: dict[str, Any] = Depends(require_system_admin
|
|||||||
_ = session
|
_ = session
|
||||||
return {"status": "success", "projects": await list_all_projects()}
|
return {"status": "success", "projects": await list_all_projects()}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/projects/{project_id}/automations")
|
||||||
|
async def dashboard_project_automations(
|
||||||
|
project_id: str,
|
||||||
|
session: dict[str, Any] = Depends(verify_session),
|
||||||
|
):
|
||||||
|
project = await get_project(project_id)
|
||||||
|
if not project:
|
||||||
|
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다.")
|
||||||
|
if not _can_edit_project(session, project):
|
||||||
|
raise HTTPException(status_code=403, detail="자동화 로직 조회 권한이 없습니다.")
|
||||||
|
return {"status": "success", "automations": await list_project_automations(project_id)}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/projects/{project_id}/automations")
|
||||||
|
async def dashboard_create_automation(
|
||||||
|
project_id: str,
|
||||||
|
payload: AutomationRequest,
|
||||||
|
session: dict[str, Any] = Depends(verify_session),
|
||||||
|
):
|
||||||
|
project = await get_project(project_id)
|
||||||
|
if not project:
|
||||||
|
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다.")
|
||||||
|
if not _can_manage_automation(session, project):
|
||||||
|
raise HTTPException(status_code=403, detail="자동화 로직 생성 권한이 없습니다.")
|
||||||
|
automation = await create_project_automation(
|
||||||
|
project_id, int(session["user_id"]), payload.model_dump()
|
||||||
|
)
|
||||||
|
return {"status": "success", "automation": automation}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/automations/{automation_id}")
|
||||||
|
async def dashboard_update_automation(
|
||||||
|
automation_id: int,
|
||||||
|
payload: AutomationRequest,
|
||||||
|
session: dict[str, Any] = Depends(verify_session),
|
||||||
|
):
|
||||||
|
automation = await get_project_automation(automation_id)
|
||||||
|
if not automation:
|
||||||
|
raise HTTPException(status_code=404, detail="자동화 로직을 찾을 수 없습니다.")
|
||||||
|
if not _can_manage_automation(session, automation):
|
||||||
|
raise HTTPException(status_code=403, detail="자동화 로직 수정 권한이 없습니다.")
|
||||||
|
if not await update_project_automation(
|
||||||
|
automation_id, int(session["user_id"]), payload.model_dump()
|
||||||
|
):
|
||||||
|
raise HTTPException(status_code=404, detail="자동화 로직을 찾을 수 없습니다.")
|
||||||
|
return {"status": "success"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/automations/{automation_id}")
|
||||||
|
async def dashboard_delete_automation(
|
||||||
|
automation_id: int,
|
||||||
|
session: dict[str, Any] = Depends(verify_session),
|
||||||
|
):
|
||||||
|
automation = await get_project_automation(automation_id)
|
||||||
|
if not automation:
|
||||||
|
raise HTTPException(status_code=404, detail="자동화 로직을 찾을 수 없습니다.")
|
||||||
|
if not _can_manage_automation(session, automation):
|
||||||
|
raise HTTPException(status_code=403, detail="자동화 로직 삭제 권한이 없습니다.")
|
||||||
|
if not await delete_project_automation(automation_id, int(session["user_id"])):
|
||||||
|
raise HTTPException(status_code=404, detail="자동화 로직을 찾을 수 없습니다.")
|
||||||
|
return {"status": "success"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/automations/{automation_id}/execute")
|
||||||
|
async def dashboard_execute_automation(
|
||||||
|
automation_id: int,
|
||||||
|
session: dict[str, Any] = Depends(verify_session),
|
||||||
|
):
|
||||||
|
automation = await get_project_automation(automation_id)
|
||||||
|
if not automation:
|
||||||
|
raise HTTPException(status_code=404, detail="자동화 로직을 찾을 수 없습니다.")
|
||||||
|
if not _can_manage_automation(session, automation):
|
||||||
|
raise HTTPException(status_code=403, detail="자동화 로직 실행 권한이 없습니다.")
|
||||||
|
if not await mark_project_automation_executed(automation_id, int(session["user_id"])):
|
||||||
|
raise HTTPException(status_code=404, detail="자동화 로직을 찾을 수 없습니다.")
|
||||||
|
return {"status": "success"}
|
||||||
|
|||||||
@@ -36,3 +36,26 @@ class ChangeUserRoleRequest(BaseModel):
|
|||||||
|
|
||||||
class AssignCompanyRequest(BaseModel):
|
class AssignCompanyRequest(BaseModel):
|
||||||
company_id: int | None = Field(default=None, gt=0)
|
company_id: int | None = Field(default=None, gt=0)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateProjectRequest(BaseModel):
|
||||||
|
name: str = Field(min_length=1, max_length=255)
|
||||||
|
region: str | None = Field(default=None, max_length=100)
|
||||||
|
road_type: str | None = Field(default=None, max_length=100)
|
||||||
|
project_year: int | None = Field(default=None, ge=1900, le=2100)
|
||||||
|
estimated_length_m: float | None = Field(default=None, ge=0)
|
||||||
|
memo: str | None = Field(default=None, max_length=5000)
|
||||||
|
status: str | None = Field(default=None, max_length=50)
|
||||||
|
|
||||||
|
|
||||||
|
class AdminUpdateUserRequest(UpdateUserRequest):
|
||||||
|
status: str | None = Field(
|
||||||
|
default=None, pattern="^(NO_COMPANY|PENDING|ACTIVE|INACTIVE|REJECTED)$"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AutomationRequest(BaseModel):
|
||||||
|
name: str = Field(min_length=1, max_length=100)
|
||||||
|
logic_type: str = Field(min_length=1, max_length=50)
|
||||||
|
config_json: dict = Field(default_factory=dict)
|
||||||
|
status: str = Field(default="DRAFT", pattern="^(DRAFT|ACTIVE|INACTIVE)$")
|
||||||
|
|||||||
@@ -157,8 +157,8 @@ WF6: B09_wf6_Estimation (견적·문서)
|
|||||||
## 6. 주요 문서 참고 순서
|
## 6. 주요 문서 참고 순서
|
||||||
|
|
||||||
1. **구조 설계:** `.agent/structure.md` 읽기
|
1. **구조 설계:** `.agent/structure.md` 읽기
|
||||||
2. **프론트 구현:** `.agent/frontend.md` 읽기
|
2. **프론트 구현:** `.agent/frontend.md` 읽기 (대시보드 관리 UI: `.agent/plan_dashboard_management.md` 병행)
|
||||||
3. **백엔드 구현:** `.agent/backend.md` 읽기
|
3. **백엔드 구현:** `.agent/backend.md` 읽기 (대시보드 관리 API: `.agent/plan_dashboard_management.md` 병행)
|
||||||
4. **마이그레이션:** `.agent/migration_plan.md` 읽기
|
4. **마이그레이션:** `.agent/migration_plan.md` 읽기
|
||||||
5. **DB 스키마:** `.agent/db_schema_simple.md` 읽기
|
5. **DB 스키마:** `.agent/db_schema_simple.md` 읽기
|
||||||
6. **프로젝트 정보:** `.agent/project_info.md` 읽기
|
6. **프로젝트 정보:** `.agent/project_info.md` 읽기
|
||||||
|
|||||||
@@ -50,6 +50,29 @@ CREATE TABLE IF NOT EXISTS system_resources (
|
|||||||
INDEX idx_system_resources_timestamp (timestamp)
|
INDEX idx_system_resources_timestamp (timestamp)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS project_automations (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
project_id CHAR(36) NOT NULL,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
logic_type VARCHAR(50) NOT NULL,
|
||||||
|
config_json JSON NOT NULL,
|
||||||
|
status ENUM('DRAFT', 'ACTIVE', 'INACTIVE') NOT NULL DEFAULT 'DRAFT',
|
||||||
|
created_by INT NOT NULL,
|
||||||
|
updated_by INT NULL,
|
||||||
|
last_executed_at DATETIME NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP NULL DEFAULT NULL,
|
||||||
|
INDEX idx_project_automations_project_id (project_id),
|
||||||
|
INDEX idx_project_automations_status (status),
|
||||||
|
CONSTRAINT fk_project_automations_project_id FOREIGN KEY (project_id) REFERENCES projects(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_project_automations_created_by FOREIGN KEY (created_by) REFERENCES users(id)
|
||||||
|
ON DELETE RESTRICT,
|
||||||
|
CONSTRAINT fk_project_automations_updated_by FOREIGN KEY (updated_by) REFERENCES users(id)
|
||||||
|
ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
ALTER TABLE join_requests
|
ALTER TABLE join_requests
|
||||||
ADD COLUMN IF NOT EXISTS review_comment TEXT NULL AFTER reviewed_at;
|
ADD COLUMN IF NOT EXISTS review_comment TEXT NULL AFTER reviewed_at;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user