"""회사 구성원과 회사 대표 로고 저장소 (B01_Dashboard_Repository 에서 분리, 700줄 제한). 구성원은 도면 표제란의 사람 자리(과업책임자·분야별책임자·설계자)를 채우는 원천이라 자산·로고와 같은 결로 묶어 둔다. """ from __future__ import annotations import secrets from typing import Any import aiomysql from fastapi import HTTPException from common_util.common_util_auth import hash_password from config.config_db import get_db_pool from .B01_Dashboard_Repository import _role, get_dashboard_me 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 add_company_member( company_id: int, email: str, profile: dict[str, Any] | None = None, ) -> dict[str, Any] | None: """회사에 사람을 붙인다. `profile["name"]` 이 있으면 **계정이 없는 사람도 그 자리에서 만든다** (2026-09-02 사용자 확정 — 담당자 선택의 「신규 등록…」). 새 계정은 로그인할 수 없는 비밀번호로 서고(`status='PENDING'`), 본인이 비밀번호를 세우면 그때 쓰인다. """ 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 email = %s AND deleted_at IS NULL FOR UPDATE""", (email.lower(),), ) user = await cursor.fetchone() if not user and profile and profile.get("name"): await cursor.execute( """INSERT INTO users (email, password_hash, name, position, department, company_id, role, status) VALUES (%s, %s, %s, %s, %s, %s, 'USER', 'PENDING')""", ( email.lower(), hash_password(secrets.token_urlsafe(32)), # 아무도 못 맞히는 비밀번호 profile["name"], profile.get("position"), profile.get("department"), company_id, ), ) await connection.commit() return await get_dashboard_me(cursor.lastrowid) if not user or user["company_id"] == company_id: await connection.rollback() return None await cursor.execute( """UPDATE users SET company_id = %s, status = 'ACTIVE', role = 'USER', is_master = FALSE WHERE id = %s""", (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="로고·서명은 같은 회사 자산이어야 합니다.")