From b68bf78e40cc174446d753d6ec425015f08ecb67 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Sat, 25 Oct 2025 08:59:52 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=EB=B3=84=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A0=91=EA=B7=BC=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 4단계 권한을 admin/user 2단계로 단순화 - 페이지별 세부 접근 권한 관리 시스템 추가 - 부적합 조회 시 일반 사용자는 본인 등록 건만 조회 가능하도록 제한 - 관리자 전용 전체 부적합 조회 API 추가 (/api/issues/admin/all) Backend Changes: - models.py: UserPagePermission 모델 추가, UserRole 단순화 - page_permissions.py: 페이지 권한 관리 API 라우터 추가 - auth.py: 사용자 목록 조회 및 비밀번호 초기화 API 추가 - issues.py: 권한별 부적합 조회 제한 로직 구현 - 마이그레이션: 010~012 권한 시스템 관련 DB 스키마 변경 --- backend/database/models.py | 28 +- backend/main.py | 3 +- backend/migrations/010_add_etc_category.sql | 15 + .../migrations/011_add_permission_system.sql | 137 ++++++++ .../migrations/012_simplify_permissions.sql | 111 ++++++ backend/routers/auth.py | 31 ++ backend/routers/issues.py | 35 +- backend/routers/page_permissions.py | 324 ++++++++++++++++++ 8 files changed, 676 insertions(+), 8 deletions(-) create mode 100644 backend/migrations/010_add_etc_category.sql create mode 100644 backend/migrations/011_add_permission_system.sql create mode 100644 backend/migrations/012_simplify_permissions.sql create mode 100644 backend/routers/page_permissions.py diff --git a/backend/database/models.py b/backend/database/models.py index 8ea8700..1127d6b 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, BigInteger, String, DateTime, Float, Boolean, Text, ForeignKey, Enum +from sqlalchemy import Column, Integer, BigInteger, String, DateTime, Float, Boolean, Text, ForeignKey, Enum, Index from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from datetime import datetime, timezone, timedelta @@ -14,8 +14,8 @@ def get_kst_now(): Base = declarative_base() class UserRole(str, enum.Enum): - admin = "admin" - user = "user" + admin = "admin" # 관리자 + user = "user" # 일반 사용자 class IssueStatus(str, enum.Enum): new = "new" @@ -44,6 +44,28 @@ class User(Base): issues = relationship("Issue", back_populates="reporter") daily_works = relationship("DailyWork", back_populates="created_by") projects = relationship("Project", back_populates="created_by") + page_permissions = relationship("UserPagePermission", back_populates="user", foreign_keys="UserPagePermission.user_id") + +class UserPagePermission(Base): + __tablename__ = "user_page_permissions" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) + page_name = Column(String(50), nullable=False) + can_access = Column(Boolean, default=False) + granted_by_id = Column(Integer, ForeignKey("users.id")) + granted_at = Column(DateTime, default=get_kst_now) + notes = Column(Text) + + # Relationships + user = relationship("User", back_populates="page_permissions", foreign_keys=[user_id]) + granted_by = relationship("User", foreign_keys=[granted_by_id], post_update=True) + + # Unique constraint + __table_args__ = ( + Index('idx_user_page_permissions_user_id', 'user_id'), + Index('idx_user_page_permissions_page_name', 'page_name'), + ) class Issue(Base): __tablename__ = "issues" diff --git a/backend/main.py b/backend/main.py index 157d19e..d85fc1e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -5,7 +5,7 @@ import uvicorn from database.database import engine, get_db from database.models import Base -from routers import auth, issues, daily_work, reports, projects +from routers import auth, issues, daily_work, reports, projects, page_permissions from services.auth_service import create_admin_user # 데이터베이스 테이블 생성 @@ -36,6 +36,7 @@ app.include_router(issues.router) app.include_router(daily_work.router) app.include_router(reports.router) app.include_router(projects.router) +app.include_router(page_permissions.router) # 시작 시 관리자 계정 생성 @app.on_event("startup") diff --git a/backend/migrations/010_add_etc_category.sql b/backend/migrations/010_add_etc_category.sql new file mode 100644 index 0000000..1e8dda7 --- /dev/null +++ b/backend/migrations/010_add_etc_category.sql @@ -0,0 +1,15 @@ +-- 부적합 카테고리에 'etc' (기타) 값 추가 +-- 백엔드 코드와 데이터베이스 enum 타입 불일치 해결 + +-- issuecategory enum 타입에 'etc' 값 추가 +ALTER TYPE issuecategory ADD VALUE 'etc'; + +-- 확인 쿼리 (주석) +-- SELECT enumlabel FROM pg_enum WHERE enumtypid = (SELECT oid FROM pg_type WHERE typname = 'issuecategory') ORDER BY enumsortorder; + +-- 이제 사용 가능한 카테고리: +-- 1. material_missing (자재누락) +-- 2. design_error (설계미스) +-- 3. incoming_defect (입고자재 불량) +-- 4. inspection_miss (검사미스) +-- 5. etc (기타) ✅ 새로 추가됨 diff --git a/backend/migrations/011_add_permission_system.sql b/backend/migrations/011_add_permission_system.sql new file mode 100644 index 0000000..c3d3872 --- /dev/null +++ b/backend/migrations/011_add_permission_system.sql @@ -0,0 +1,137 @@ +-- 권한 시스템 개선 마이그레이션 +-- 새로운 사용자 역할 추가 및 개별 권한 테이블 생성 + +-- 1. 새로운 사용자 역할 추가 +ALTER TYPE userrole ADD VALUE 'super_admin'; +ALTER TYPE userrole ADD VALUE 'manager'; + +-- 2. 사용자별 개별 권한 테이블 생성 +CREATE TABLE IF NOT EXISTS user_permissions ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + permission VARCHAR(50) NOT NULL, + granted BOOLEAN DEFAULT TRUE, + granted_by_id INTEGER REFERENCES users(id), + granted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + revoked_at TIMESTAMP WITH TIME ZONE, + notes TEXT, + UNIQUE(user_id, permission) +); + +-- 3. 인덱스 생성 +CREATE INDEX IF NOT EXISTS idx_user_permissions_user_id ON user_permissions(user_id); +CREATE INDEX IF NOT EXISTS idx_user_permissions_permission ON user_permissions(permission); +CREATE INDEX IF NOT EXISTS idx_user_permissions_granted ON user_permissions(granted); + +-- 4. 기본 권한 설정 (기존 관리자에게 super_admin 권한 부여) +UPDATE users SET role = 'super_admin' WHERE username = 'hyungi'; + +-- 5. 권한 확인 함수 생성 +CREATE OR REPLACE FUNCTION check_user_permission(p_user_id INTEGER, p_permission VARCHAR) +RETURNS BOOLEAN AS $$ +DECLARE + user_role userrole; + has_permission BOOLEAN := FALSE; +BEGIN + -- 사용자 역할 가져오기 + SELECT role INTO user_role FROM users WHERE id = p_user_id AND is_active = TRUE; + + IF user_role IS NULL THEN + RETURN FALSE; + END IF; + + -- super_admin은 모든 권한 보유 + IF user_role = 'super_admin' THEN + RETURN TRUE; + END IF; + + -- 개별 권한 확인 + SELECT granted INTO has_permission + FROM user_permissions + WHERE user_id = p_user_id + AND permission = p_permission + AND granted = TRUE + AND revoked_at IS NULL; + + -- 개별 권한이 없으면 역할 기반 기본 권한 확인 + IF has_permission IS NULL THEN + -- 기본 권한 매트릭스 + CASE + WHEN p_permission IN ('issues.create', 'issues.view') THEN + has_permission := TRUE; -- 모든 사용자 + WHEN p_permission IN ('issues.edit', 'issues.review', 'daily_work.create', 'daily_work.view', 'daily_work.edit') THEN + has_permission := user_role IN ('admin', 'manager'); -- 관리자, 매니저 + WHEN p_permission IN ('projects.create', 'projects.edit', 'issues.delete', 'daily_work.delete') THEN + has_permission := user_role = 'admin'; -- 관리자만 + WHEN p_permission IN ('projects.delete', 'users.create', 'users.edit', 'users.delete', 'users.change_role') THEN + has_permission := user_role = 'super_admin'; -- 최고 관리자만 + ELSE + has_permission := FALSE; + END CASE; + END IF; + + RETURN COALESCE(has_permission, FALSE); +END; +$$ LANGUAGE plpgsql; + +-- 6. 권한 부여 함수 생성 +CREATE OR REPLACE FUNCTION grant_user_permission( + p_user_id INTEGER, + p_permission VARCHAR, + p_granted_by_id INTEGER, + p_notes TEXT DEFAULT NULL +) +RETURNS BOOLEAN AS $$ +BEGIN + INSERT INTO user_permissions (user_id, permission, granted, granted_by_id, notes) + VALUES (p_user_id, p_permission, TRUE, p_granted_by_id, p_notes) + ON CONFLICT (user_id, permission) + DO UPDATE SET + granted = TRUE, + granted_by_id = p_granted_by_id, + granted_at = NOW(), + revoked_at = NULL, + notes = p_notes; + + RETURN TRUE; +END; +$$ LANGUAGE plpgsql; + +-- 7. 권한 취소 함수 생성 +CREATE OR REPLACE FUNCTION revoke_user_permission( + p_user_id INTEGER, + p_permission VARCHAR, + p_revoked_by_id INTEGER, + p_notes TEXT DEFAULT NULL +) +RETURNS BOOLEAN AS $$ +BEGIN + UPDATE user_permissions + SET granted = FALSE, + revoked_at = NOW(), + notes = p_notes + WHERE user_id = p_user_id + AND permission = p_permission; + + RETURN TRUE; +END; +$$ LANGUAGE plpgsql; + +-- 8. 사용자 권한 목록 조회 뷰 생성 +CREATE OR REPLACE VIEW user_permissions_view AS +SELECT + u.id as user_id, + u.username, + u.full_name, + u.role, + up.permission, + up.granted, + up.granted_at, + up.revoked_at, + granted_by.username as granted_by_username, + up.notes +FROM users u +LEFT JOIN user_permissions up ON u.id = up.user_id +LEFT JOIN users granted_by ON up.granted_by_id = granted_by.id +WHERE u.is_active = TRUE +ORDER BY u.username, up.permission; diff --git a/backend/migrations/012_simplify_permissions.sql b/backend/migrations/012_simplify_permissions.sql new file mode 100644 index 0000000..8e3c784 --- /dev/null +++ b/backend/migrations/012_simplify_permissions.sql @@ -0,0 +1,111 @@ +-- 권한 시스템 단순화 +-- admin/user 구조로 변경하고 페이지별 접근 권한으로 변경 + +-- 1. 기존 복잡한 권한 테이블 삭제하고 단순한 페이지 권한 테이블로 변경 +DROP TABLE IF EXISTS user_permissions CASCADE; + +-- 2. 페이지별 접근 권한 테이블 생성 +CREATE TABLE user_page_permissions ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + page_name VARCHAR(50) NOT NULL, + can_access BOOLEAN DEFAULT FALSE, + granted_by_id INTEGER REFERENCES users(id), + granted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + notes TEXT, + UNIQUE(user_id, page_name) +); + +-- 3. 인덱스 생성 +CREATE INDEX IF NOT EXISTS idx_user_page_permissions_user_id ON user_page_permissions(user_id); +CREATE INDEX IF NOT EXISTS idx_user_page_permissions_page_name ON user_page_permissions(page_name); + +-- 4. 기존 복잡한 함수들 삭제 +DROP FUNCTION IF EXISTS check_user_permission(INTEGER, VARCHAR); +DROP FUNCTION IF EXISTS grant_user_permission(INTEGER, VARCHAR, INTEGER, TEXT); +DROP FUNCTION IF EXISTS revoke_user_permission(INTEGER, VARCHAR, INTEGER, TEXT); + +-- 5. 단순한 페이지 접근 권한 체크 함수 +CREATE OR REPLACE FUNCTION check_page_access(p_user_id INTEGER, p_page_name VARCHAR) +RETURNS BOOLEAN AS $$ +DECLARE + user_role userrole; + has_access BOOLEAN := FALSE; +BEGIN + -- 사용자 역할 가져오기 + SELECT role INTO user_role FROM users WHERE id = p_user_id AND is_active = TRUE; + + IF user_role IS NULL THEN + RETURN FALSE; + END IF; + + -- admin은 모든 페이지 접근 가능 + IF user_role = 'admin' THEN + RETURN TRUE; + END IF; + + -- 일반 사용자는 개별 페이지 권한 확인 + SELECT can_access INTO has_access + FROM user_page_permissions + WHERE user_id = p_user_id + AND page_name = p_page_name; + + -- 권한이 설정되지 않은 경우 기본값 (부적합 등록/조회만 허용) + IF has_access IS NULL THEN + CASE p_page_name + WHEN 'issues_create' THEN has_access := TRUE; + WHEN 'issues_view' THEN has_access := TRUE; + ELSE has_access := FALSE; + END CASE; + END IF; + + RETURN COALESCE(has_access, FALSE); +END; +$$ LANGUAGE plpgsql; + +-- 6. 페이지 권한 부여 함수 +CREATE OR REPLACE FUNCTION grant_page_access( + p_user_id INTEGER, + p_page_name VARCHAR, + p_can_access BOOLEAN, + p_granted_by_id INTEGER, + p_notes TEXT DEFAULT NULL +) +RETURNS BOOLEAN AS $$ +BEGIN + INSERT INTO user_page_permissions (user_id, page_name, can_access, granted_by_id, notes) + VALUES (p_user_id, p_page_name, p_can_access, p_granted_by_id, p_notes) + ON CONFLICT (user_id, page_name) + DO UPDATE SET + can_access = p_can_access, + granted_by_id = p_granted_by_id, + granted_at = NOW(), + notes = p_notes; + + RETURN TRUE; +END; +$$ LANGUAGE plpgsql; + +-- 7. 사용자 페이지 권한 조회 뷰 +CREATE OR REPLACE VIEW user_page_access_view AS +SELECT + u.id as user_id, + u.username, + u.full_name, + u.role, + upp.page_name, + upp.can_access, + upp.granted_at, + granted_by.username as granted_by_username, + upp.notes +FROM users u +LEFT JOIN user_page_permissions upp ON u.id = upp.user_id +LEFT JOIN users granted_by ON upp.granted_by_id = granted_by.id +WHERE u.is_active = TRUE +ORDER BY u.username, upp.page_name; + +-- 8. 기존 super_admin, manager 역할을 admin으로 변경 +UPDATE users SET role = 'admin' WHERE role IN ('super_admin', 'manager'); + +-- 9. 기존 뷰 삭제 +DROP VIEW IF EXISTS user_permissions_view; diff --git a/backend/routers/auth.py b/backend/routers/auth.py index 67951bc..1523190 100644 --- a/backend/routers/auth.py +++ b/backend/routers/auth.py @@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from sqlalchemy.orm import Session from typing import List +from pydantic import BaseModel from database.database import get_db from database.models import User, UserRole @@ -61,6 +62,15 @@ async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = async def read_users_me(current_user: User = Depends(get_current_user)): return current_user +@router.get("/users", response_model=List[schemas.User]) +async def get_all_users( + current_admin: User = Depends(get_current_admin), + db: Session = Depends(get_db) +): + """모든 사용자 목록 조회 (관리자 전용)""" + users = db.query(User).filter(User.is_active == True).all() + return users + @router.post("/users", response_model=schemas.User) async def create_user( user: schemas.UserCreate, @@ -153,3 +163,24 @@ async def change_password( db.commit() return {"detail": "Password changed successfully"} + +class PasswordReset(BaseModel): + new_password: str + +@router.post("/users/{user_id}/reset-password") +async def reset_user_password( + user_id: int, + password_reset: PasswordReset, + current_admin: User = Depends(get_current_admin), + db: Session = Depends(get_db) +): + """사용자 비밀번호 초기화 (관리자 전용)""" + db_user = db.query(User).filter(User.id == user_id).first() + if not db_user: + raise HTTPException(status_code=404, detail="User not found") + + # 새 비밀번호로 업데이트 + db_user.hashed_password = get_password_hash(password_reset.new_password) + db.commit() + + return {"detail": f"Password reset successfully for user {db_user.username}"} diff --git a/backend/routers/issues.py b/backend/routers/issues.py index c07e4b8..2cc61f1 100644 --- a/backend/routers/issues.py +++ b/backend/routers/issues.py @@ -6,7 +6,7 @@ from datetime import datetime from database.database import get_db from database.models import Issue, IssueStatus, User, UserRole from database import schemas -from routers.auth import get_current_user +from routers.auth import get_current_user, get_current_admin from services.file_service import save_base64_image, delete_file router = APIRouter(prefix="/api/issues", tags=["issues"]) @@ -54,8 +54,30 @@ async def read_issues( ): query = db.query(Issue) - # 모든 사용자가 모든 이슈를 조회 가능 - # (필터링 제거 - 협업을 위해 모두가 볼 수 있어야 함) + # 권한별 조회 제한 + if current_user.role == UserRole.admin: + # 관리자는 모든 이슈 조회 가능 + pass + else: + # 일반 사용자는 본인이 등록한 이슈만 조회 가능 + query = query.filter(Issue.reporter_id == current_user.id) + + if status: + query = query.filter(Issue.status == status) + + issues = query.offset(skip).limit(limit).all() + return issues + +@router.get("/admin/all", response_model=List[schemas.Issue]) +async def read_all_issues_admin( + skip: int = 0, + limit: int = 100, + status: Optional[IssueStatus] = None, + current_admin: User = Depends(get_current_admin), + db: Session = Depends(get_db) +): + """관리자 전용: 모든 부적합 조회""" + query = db.query(Issue) if status: query = query.filter(Issue.status == status) @@ -73,7 +95,12 @@ async def read_issue( if not issue: raise HTTPException(status_code=404, detail="Issue not found") - # 모든 사용자가 모든 이슈를 조회 가능 (협업을 위해) + # 권한별 조회 제한 + if current_user.role != UserRole.admin and issue.reporter_id != current_user.id: + raise HTTPException( + status_code=403, + detail="본인이 등록한 부적합만 조회할 수 있습니다." + ) return issue diff --git a/backend/routers/page_permissions.py b/backend/routers/page_permissions.py new file mode 100644 index 0000000..9337be1 --- /dev/null +++ b/backend/routers/page_permissions.py @@ -0,0 +1,324 @@ +""" +페이지 권한 관리 API 라우터 +사용자별 페이지 접근 권한을 관리하는 엔드포인트들 +""" + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List, Optional +from pydantic import BaseModel + +from database.database import get_db +from database.models import User, UserPagePermission, UserRole +from routers.auth import get_current_user + +router = APIRouter(prefix="/api", tags=["page-permissions"]) + +# Pydantic 모델들 +class PagePermissionRequest(BaseModel): + user_id: int + page_name: str + can_access: bool + notes: Optional[str] = None + +class PagePermissionResponse(BaseModel): + id: int + user_id: int + page_name: str + can_access: bool + granted_by_id: Optional[int] + granted_at: str + notes: Optional[str] + + class Config: + from_attributes = True + +class UserPagePermissionSummary(BaseModel): + user_id: int + username: str + full_name: Optional[str] + role: str + permissions: List[PagePermissionResponse] + +# 기본 페이지 목록 +DEFAULT_PAGES = { + 'issues_create': {'title': '부적합 등록', 'default_access': True}, + 'issues_view': {'title': '부적합 조회', 'default_access': True}, + 'issues_manage': {'title': '부적합 관리', 'default_access': False}, + 'projects_manage': {'title': '프로젝트 관리', 'default_access': False}, + 'daily_work': {'title': '일일 공수', 'default_access': False}, + 'reports': {'title': '보고서', 'default_access': False} +} + +@router.post("/page-permissions/grant") +async def grant_page_permission( + request: PagePermissionRequest, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """페이지 권한 부여/취소""" + + # 관리자만 권한 설정 가능 + if current_user.role != UserRole.admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="관리자만 권한을 설정할 수 있습니다." + ) + + # 대상 사용자 확인 + target_user = db.query(User).filter(User.id == request.user_id).first() + if not target_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="사용자를 찾을 수 없습니다." + ) + + # 유효한 페이지명 확인 + if request.page_name not in DEFAULT_PAGES: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="유효하지 않은 페이지명입니다." + ) + + # 기존 권한 확인 + existing_permission = db.query(UserPagePermission).filter( + UserPagePermission.user_id == request.user_id, + UserPagePermission.page_name == request.page_name + ).first() + + if existing_permission: + # 기존 권한 업데이트 + existing_permission.can_access = request.can_access + existing_permission.granted_by_id = current_user.id + existing_permission.notes = request.notes + db.commit() + db.refresh(existing_permission) + return {"message": "권한이 업데이트되었습니다.", "permission_id": existing_permission.id} + else: + # 새 권한 생성 + new_permission = UserPagePermission( + user_id=request.user_id, + page_name=request.page_name, + can_access=request.can_access, + granted_by_id=current_user.id, + notes=request.notes + ) + db.add(new_permission) + db.commit() + db.refresh(new_permission) + return {"message": "권한이 설정되었습니다.", "permission_id": new_permission.id} + +@router.get("/users/{user_id}/page-permissions", response_model=List[PagePermissionResponse]) +async def get_user_page_permissions( + user_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """특정 사용자의 페이지 권한 목록 조회""" + + # 관리자이거나 본인의 권한만 조회 가능 + if current_user.role != UserRole.admin and current_user.id != user_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="권한이 없습니다." + ) + + # 사용자 존재 확인 + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="사용자를 찾을 수 없습니다." + ) + + # 사용자의 페이지 권한 조회 + permissions = db.query(UserPagePermission).filter( + UserPagePermission.user_id == user_id + ).all() + + return permissions + +@router.get("/page-permissions/check/{user_id}/{page_name}") +async def check_page_access( + user_id: int, + page_name: str, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """특정 사용자의 특정 페이지 접근 권한 확인""" + + # 사용자 존재 확인 + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="사용자를 찾을 수 없습니다." + ) + + # admin은 모든 페이지 접근 가능 + if user.role == UserRole.admin: + return {"can_access": True, "reason": "admin_role"} + + # 유효한 페이지명 확인 + if page_name not in DEFAULT_PAGES: + return {"can_access": False, "reason": "invalid_page"} + + # 개별 권한 확인 + permission = db.query(UserPagePermission).filter( + UserPagePermission.user_id == user_id, + UserPagePermission.page_name == page_name + ).first() + + if permission: + return { + "can_access": permission.can_access, + "reason": "explicit_permission", + "granted_at": permission.granted_at.isoformat() if permission.granted_at else None + } + + # 기본 권한 확인 + default_access = DEFAULT_PAGES[page_name]['default_access'] + return { + "can_access": default_access, + "reason": "default_permission" + } + +@router.get("/page-permissions/all-users", response_model=List[UserPagePermissionSummary]) +async def get_all_users_permissions( + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """모든 사용자의 페이지 권한 요약 조회 (관리자용)""" + + if current_user.role != UserRole.admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="관리자만 접근할 수 있습니다." + ) + + # 모든 사용자 조회 + users = db.query(User).filter(User.is_active == True).all() + + result = [] + for user in users: + # 각 사용자의 권한 조회 + permissions = db.query(UserPagePermission).filter( + UserPagePermission.user_id == user.id + ).all() + + result.append(UserPagePermissionSummary( + user_id=user.id, + username=user.username, + full_name=user.full_name, + role=user.role.value, + permissions=permissions + )) + + return result + +@router.get("/page-permissions/available-pages") +async def get_available_pages( + current_user: User = Depends(get_current_user) +): + """사용 가능한 페이지 목록 조회""" + + return { + "pages": DEFAULT_PAGES, + "total_count": len(DEFAULT_PAGES) + } + +@router.delete("/page-permissions/{permission_id}") +async def delete_page_permission( + permission_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """페이지 권한 삭제 (기본값으로 되돌림)""" + + if current_user.role != UserRole.admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="관리자만 권한을 삭제할 수 있습니다." + ) + + # 권한 조회 + permission = db.query(UserPagePermission).filter( + UserPagePermission.id == permission_id + ).first() + + if not permission: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="권한을 찾을 수 없습니다." + ) + + # 권한 삭제 + db.delete(permission) + db.commit() + + return {"message": "권한이 삭제되었습니다. 기본값이 적용됩니다."} + +class BulkPermissionRequest(BaseModel): + user_id: int + permissions: List[dict] # [{"page_name": "issues_manage", "can_access": true}, ...] + +@router.post("/page-permissions/bulk-grant") +async def bulk_grant_permissions( + request: BulkPermissionRequest, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """사용자의 여러 페이지 권한을 일괄 설정""" + + if current_user.role != UserRole.admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="관리자만 권한을 설정할 수 있습니다." + ) + + # 대상 사용자 확인 + target_user = db.query(User).filter(User.id == request.user_id).first() + if not target_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="사용자를 찾을 수 없습니다." + ) + + updated_permissions = [] + + for perm_data in request.permissions: + page_name = perm_data.get('page_name') + can_access = perm_data.get('can_access', False) + + # 유효한 페이지명 확인 + if page_name not in DEFAULT_PAGES: + continue + + # 기존 권한 확인 + existing_permission = db.query(UserPagePermission).filter( + UserPagePermission.user_id == request.user_id, + UserPagePermission.page_name == page_name + ).first() + + if existing_permission: + # 기존 권한 업데이트 + existing_permission.can_access = can_access + existing_permission.granted_by_id = current_user.id + updated_permissions.append(existing_permission) + else: + # 새 권한 생성 + new_permission = UserPagePermission( + user_id=request.user_id, + page_name=page_name, + can_access=can_access, + granted_by_id=current_user.id + ) + db.add(new_permission) + updated_permissions.append(new_permission) + + db.commit() + + return { + "message": f"{len(updated_permissions)}개의 권한이 설정되었습니다.", + "updated_count": len(updated_permissions) + }