"""회사 구성원과 회사 대표 로고 저장소 (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) } # 참여자도 같은 회사 사람이어야 한다 (2026-09-06 사용자 확정). user_ids |= {int(uid) for uid in (data.get("member_user_ids") or [])} 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="로고·서명은 같은 회사 자산이어야 합니다.") async def list_project_member_ids(cursor: Any, project_ids: list[str]) -> dict[str, list[int]]: """프로젝트별 참여자 id 목록 (2026-09-06 사용자 확정).""" if not project_ids: return {} marks = ", ".join(["%s"] * len(project_ids)) await cursor.execute( f"""SELECT project_id, user_id FROM project_members WHERE project_id IN ({marks}) ORDER BY user_id""", tuple(project_ids), ) result: dict[str, list[int]] = {} for row in await cursor.fetchall(): key = row["project_id"] if isinstance(row, dict) else row[0] value = row["user_id"] if isinstance(row, dict) else row[1] result.setdefault(str(key), []).append(int(value)) return result async def set_project_members(project_id: str, user_ids: list[int]) -> None: """참여자 목록을 통째로 맞춘다. 만든 사람은 화면에서 늘 포함해 보낸다.""" pool = get_db_pool() async with pool.acquire() as connection, connection.cursor() as cursor: await connection.begin() await cursor.execute("DELETE FROM project_members WHERE project_id = %s", (project_id,)) for user_id in dict.fromkeys(user_ids): await cursor.execute( "INSERT IGNORE INTO project_members (project_id, user_id) VALUES (%s, %s)", (project_id, int(user_id)), ) await connection.commit() async def is_project_member(project_id: str, user_id: int) -> bool: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor() as cursor: await cursor.execute( "SELECT 1 FROM project_members WHERE project_id = %s AND user_id = %s", (project_id, user_id), ) return await cursor.fetchone() is not None