o2o-negosium-original/negodata/backend/services/company_user_service.py
Mina Choi a40257ddc1 [feat] negodata: 회원관리 / 최고관리자(OWNER)
- OWNER 권한 신설, 최고관리자가 자기 회사 소속 직원(USER) 계정 생성·수정·삭제 관리
- 회사 사용자 API(/v1/company/user/*), 회원관리 페이지·폼
- 로그인/프로필 흐름 정비, 무인증 계정생성(/auth/create) 제거

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 13:31:32 +09:00

159 lines
6.2 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) -> 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),
)
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 로 고정.
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=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