o2o-negosium-original/negodata/backend/services/company_user_service.py
Mina Choi cfee6e89d0 [feat] negodata: 권한 3단계(개발자 신설)·개발자 메뉴 그룹·디자인 시스템 페이지
- UserRole 1=일반 2=최고관리자 3=개발자(내부 운영). 개발자 계정은 회원 목록·총계에서 제외해 고객사에 노출하지 않음
- 계정 생성 시 권한 선택(일반/최고관리자) 추가, 개발자는 앱에서 부여 불가(DB 시드 전용)
- 사이드바 '개발자' 그룹 신설 — 회사 설정·디자인 시스템을 개발자에게만 노출
- /dev/design 디자인 시스템 페이지: 색 토큰·타이포·버튼·배지·입력·반경, 목록 화면 구조, 반응형 기준, URL 상태 규칙, 회사 커스터마이징 훅
- 공급사 재협상 요청(#15) 플로우 문서 — sessions.custom 기반(DDL 0)
2026-07-23 15:41:36 +09:00

159 lines
6.4 KiB
Python

import uuid
from fastapi import Depends
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import users
from common.enums import DBWRType, ErrorType, UserRole, UserStatus
from common.utils.gtime import GTime
from crud.user_crud import IUserCRUD, UserCRUD
from router.v1.company.protocol import (
CompanyUserData,
Req_CreateCompanyUser,
Req_UpdateCompanyUser,
Res_CompanyUser,
Res_CompanyUserList,
Res_DeleteCompanyUser,
)
from router.v1.validator.dependencies import GetHashedPW
class CompanyUserService:
"""최고관리자(OWNER)의 자기 회사 유저 관리 로직.
- 라우터에서 RequireOwner 로 1차 권한을 거른 뒤 호출된다.
- company_id 는 토큰값만 쓴다(요청 body 무시) → 남의 회사 데이터 불가.
- 변경 대상이 OWNER 면 거부한다(최고관리자는 앱에서 수정·삭제 불가).
"""
def __init__(self, user_crud: IUserCRUD = Depends(UserCRUD)):
self.user_crud = user_crud
async def _fetch_managed(self, company_uuid: uuid.UUID, user_id: uuid.UUID):
"""대상 유저 조회 + 같은 회사 + 비-OWNER 확인. (ErrorType, user|None) 반환."""
err_type, user = await DB_SESSION_MNG.execute_lambda(
users.DBType(),
DBWRType.DB_READ.value,
lambda s: self.user_crud.get_by_user_id(s, user_id),
)
if err_type != ErrorType.SUCCESS or user is None:
return ErrorType.ACCOUNT_NOT_FOUND, None
if user.company_id != company_uuid:
return ErrorType.ACCOUNT_NOT_FOUND, None
if user.role == UserRole.OWNER.value:
return ErrorType.ACCOUNT_FORBIDDEN, None
return ErrorType.SUCCESS, user
async def list_users(self, company_id: str, search, pg, hide_dev: bool = False) -> Res_CompanyUserList:
res = Res_CompanyUserList(page=pg.page, size=pg.size)
company_uuid = uuid.UUID(company_id)
err_type, rows, total = await DB_SESSION_MNG.execute_lambda(
users.DBType(),
DBWRType.DB_READ.value,
lambda s: self.user_crud.list_by_company(s, company_uuid, search, pg.skip, pg.size, hide_dev),
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
res.users = [CompanyUserData.model_validate(r) for r in rows]
res.total = total
return res
async def get_user(self, company_id: str, user_id: str) -> Res_CompanyUser:
res = Res_CompanyUser()
err_type, user = await self._fetch_managed(uuid.UUID(company_id), uuid.UUID(user_id))
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
res.user = CompanyUserData.model_validate(user)
return res
async def create_user(self, company_id: str, req: Req_CreateCompanyUser) -> Res_CompanyUser:
res = Res_CompanyUser()
company_uuid = uuid.UUID(company_id)
# 1) 로그인 ID 중복 확인 (id 는 전역 unique)
err_type = await DB_SESSION_MNG.execute_lambda(
users.DBType(),
DBWRType.DB_READ.value,
lambda s: self.user_crud.is_user(s, req.id),
)
if err_type == ErrorType.DB_ALREADY_SAME_KEY:
res.result.SetResult(ErrorType.ACCOUNT_ALREADY_EXIST)
return res
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
# 2) 생성 — 회사는 토큰값. 권한은 일반·관리자만 허용(최고관리자 승격은 앱에서 불가).
user = users(
company_id=company_uuid,
id=req.id,
password=await GetHashedPW(req.password),
name=req.name or None,
email=req.email or None,
contact_number=req.contact_number or None,
role=req.role if req.role in (UserRole.USER.value, UserRole.OWNER.value) else UserRole.USER.value,
)
err_type = await DB_SESSION_MNG.execute_lambda_run(
[users.DBType()],
[lambda s: self.user_crud.add_user(s, user)],
)
if err_type != ErrorType.SUCCESS:
if err_type == ErrorType.DB_ALREADY_SAME_KEY:
res.result.SetResult(ErrorType.ACCOUNT_ALREADY_EXIST)
else:
res.result.SetResult(err_type)
return res
# 서버 기본값(created_at 등)은 insert 후 객체에 안 실리므로 재조회.
return await self.get_user(company_id, str(user.user_id))
async def update_user(self, company_id: str, user_id: str, req: Req_UpdateCompanyUser) -> Res_CompanyUser:
res = Res_CompanyUser()
company_uuid = uuid.UUID(company_id)
user_uuid = uuid.UUID(user_id)
err_type, _ = await self._fetch_managed(company_uuid, user_uuid)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
data = req.model_dump(exclude_unset=True)
if data.get("status") is not None:
s = data["status"] # pydantic 은 enum 멤버로 돌려준다 → SMALLINT 값으로 환원
data["status"] = s.value if isinstance(s, UserStatus) else int(s)
if data.get("password"):
data["password"] = await GetHashedPW(data["password"])
else:
data.pop("password", None) # 빈 비밀번호는 변경하지 않음
err_type = await DB_SESSION_MNG.execute_lambda_run(
[users.DBType()],
[lambda s: self.user_crud.update_user(s, user_uuid, data)],
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
return await self.get_user(company_id, user_id)
async def delete_user(self, company_id: str, user_id: str) -> Res_DeleteCompanyUser:
res = Res_DeleteCompanyUser()
company_uuid = uuid.UUID(company_id)
user_uuid = uuid.UUID(user_id)
err_type, _ = await self._fetch_managed(company_uuid, user_uuid)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
err_type = await DB_SESSION_MNG.execute_lambda_run(
[users.DBType()],
[lambda s: self.user_crud.update_user(s, user_uuid, {"deleted": True, "updated_at": GTime.UTC()})],
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res