Files
Aislo/B01_Dashboard/B01_Dashboard_Repository_Members.py
T
eomsangdonandClaude Opus 5 2adb5507a4 feat(B01,B02): 팀원 등록 방식 교체·사용자 삭제/비활성화·B02 등록 화면 정비
B01
- 팀원 등록을 계정 대리 생성에서 「소속 없는 기존 가입자 검색·선택」으로 교체, 미가입자는 안내 메일만 발송
- 사용자 계정 삭제 API 신설, 삭제 모달에서 회사 제외/계정 삭제 선택, 관리자 수정 모달에 활성·비활성 상태 지정
- 기본정보 폼과 사용자 수정 폼을 공용 입력칸 한 벌로 통일 (이름 > 직급 > 이메일 > 부서 > 전화)
- 프로젝트 담당자 선택의 신규 등록 항목 제거

B02
- 프로젝트명 자동 조합 (사업연도 + 사업지역 + 임도종류 + 직접 입력값) 및 미리보기 표시
- 예상 연장 입력칸 제거, 노선 종료 누가거리 제목에 구간 연장 표기, 표지 값은 프로젝트명·연장에서 파생
- 취소 버튼 추가 (대시보드 복귀), 담당자 기본값을 생성자로 지정

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 11:24:43 +09:00

136 lines
5.7 KiB
Python

"""회사 구성원과 회사 대표 로고 저장소 (B01_Dashboard_Repository 에서 분리, 700줄 제한).
구성원은 도면 표제란의 사람 자리(과업책임자·분야별책임자·설계자)를 채우는 원천이라
자산·로고와 같은 결로 묶어 둔다.
"""
from __future__ import annotations
from typing import Any
import aiomysql
from fastapi import HTTPException
from config.config_db import get_db_pool
from .B01_Dashboard_Repository import _role, get_dashboard_me, role_for_company
from .B01_Dashboard_Repository_Assets import list_company_assets
async def list_company_members(company_id: int) -> 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, email, name, position, department, role, is_master, status
FROM users WHERE company_id = %s AND deleted_at IS NULL ORDER BY name, email""",
(company_id,),
)
rows = list(await cursor.fetchall())
for row in rows:
row["role"] = _role(row.get("role"))
row["is_master"] = bool(row.get("is_master"))
return rows
async def list_unassigned_users(query: str) -> list[dict[str, Any]]:
"""소속이 없는 가입자만 찾는다 (2026-09-06 사용자 확정).
다른 회사 소속자는 보이지 않는다 — 팀원 등록은 「이미 가입한 사람을 고르는」 일이다.
"""
like = f"%{query.strip()}%"
pool = get_db_pool()
async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor:
await cursor.execute(
"""SELECT id, name, position, email, department, status
FROM users
WHERE company_id IS NULL AND deleted_at IS NULL
AND status IN ('NO_COMPANY', 'PENDING')
AND (name LIKE %s OR email LIKE %s)
ORDER BY name, email LIMIT 20""",
(like, like),
)
return list(await cursor.fetchall())
async def attach_company_member(company_id: int, user_id: int) -> dict[str, Any] | None:
"""가입한 사람을 회사에 붙인다. 소속이 이미 있으면 붙이지 않는다."""
pool = get_db_pool()
async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor:
await connection.begin()
await cursor.execute(
"""SELECT id, company_id FROM users
WHERE id = %s AND deleted_at IS NULL FOR UPDATE""",
(user_id,),
)
user = await cursor.fetchone()
if not user or user["company_id"] is not None:
await connection.rollback()
return None
await cursor.execute(
"""UPDATE users SET company_id = %s, status = 'ACTIVE', role = %s,
is_master = FALSE
WHERE id = %s""",
(company_id, await role_for_company(company_id), user_id),
)
# 남아 있던 가입 신청은 닫는다 — 목록에 같은 사람이 두 번 보이지 않게 한다.
await cursor.execute(
"""UPDATE join_requests
SET status = CASE WHEN company_id = %s THEN 'APPROVED' ELSE 'REJECTED' END,
reviewed_at = CURRENT_TIMESTAMP
WHERE user_id = %s AND status = 'PENDING'""",
(company_id, user_id),
)
await connection.commit()
return await get_dashboard_me(user_id)
async def remove_company_member(company_id: int, user_id: int) -> bool:
pool = get_db_pool()
async with pool.acquire() as connection, connection.cursor() as cursor:
await cursor.execute(
"""UPDATE users SET company_id = NULL, status = 'NO_COMPANY', role = 'USER',
is_master = FALSE
WHERE id = %s AND company_id = %s AND is_master = FALSE""",
(user_id, company_id),
)
changed = cursor.rowcount > 0
await connection.commit()
return changed
async def set_company_logo(company_id: int, asset_id: int | None) -> bool:
"""회사 대표 로고를 지정한다 (2026-09-02 사용자 확정 — 회사 등록 단계에서 받고 변경).
`asset_id` 가 같은 회사의 `kind='LOGO'` 자산인지는 라우터가 확인한다.
"""
pool = get_db_pool()
async with pool.acquire() as connection, connection.cursor() as cursor:
await cursor.execute(
"UPDATE companies SET logo_asset_id = %s WHERE id = %s AND deleted_at IS NULL",
(asset_id, company_id),
)
changed = cursor.rowcount > 0
await connection.commit()
return changed
async def check_project_refs(company_id: int, data: dict[str, Any]) -> None:
"""담당자·로고·서명은 그 회사의 것만 물린다 (2026-09-02 사용자 확정).
프로젝트 수정(B01)과 등록(B02)이 같은 규칙을 써야 해서 저장소에 둔다.
"""
user_ids = {
data.get(k) for k in ("pm_user_id", "field_lead_user_id", "designer_user_id") if data.get(k)
}
if user_ids and not user_ids <= {m["id"] for m in await list_company_members(company_id)}:
raise HTTPException(status_code=400, detail="담당자는 같은 회사 구성원이어야 합니다.")
wanted = {
k: kind
for k, kind in (("logo_asset_id", "LOGO"), ("signature_asset_id", "SIGNATURE"))
if data.get(k)
}
if wanted:
kinds = {a["id"]: a["kind"] for a in await list_company_assets(company_id)}
if any(kinds.get(data[k]) != kind for k, kind in wanted.items()):
raise HTTPException(status_code=400, detail="로고·서명은 같은 회사 자산이어야 합니다.")