"""기관(사이트) + 담당자 등록·관리 엔드포인트.""" from datetime import datetime from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user from database import get_db from middleware.license_guard import check_institution_limit from models import ( Institution, InstitutionCreate, InstitutionOut, InstitutionUpdate, InstContact, InstContactCreate, InstContactOut, InstContactUpdate, Server, User, UserRole, ) router = APIRouter(prefix="/api/institutions", tags=["institutions"]) def _require_admin_or_pm(user: User): if user.role not in (UserRole.ADMIN, UserRole.PM): raise HTTPException(403, "ADMIN 또는 PM 권한이 필요합니다.") # ── 기관 CRUD ────────────────────────────────────────────────────────────────── @router.get("", response_model=List[InstitutionOut]) async def list_institutions( keyword: Optional[str] = Query(None), region: Optional[str] = Query(None), is_active: Optional[bool] = Query(None), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): q = select(Institution).order_by(Institution.inst_name) if keyword: q = q.where(Institution.inst_name.contains(keyword)) if region: q = q.where(Institution.region == region) if is_active is not None: q = q.where(Institution.is_active == is_active) result = await db.execute(q) return result.scalars().all() @router.get("/{inst_code}", response_model=InstitutionOut) async def get_institution( inst_code: str, db: AsyncSession = Depends(get_db), _u: User = Depends(get_current_user), ): r = await db.execute(select(Institution).where(Institution.inst_code == inst_code)) inst = r.scalars().first() if not inst: raise HTTPException(404, "기관을 찾을 수 없습니다.") return inst @router.post("", response_model=InstitutionOut, status_code=201) async def create_institution( payload: InstitutionCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), _: None = Depends(check_institution_limit), ): _require_admin_or_pm(current_user) # 중복 코드 확인 dup = await db.execute( select(Institution).where(Institution.inst_code == payload.inst_code) ) if dup.scalars().first(): raise HTTPException(409, f"기관 코드 '{payload.inst_code}'가 이미 존재합니다.") inst = Institution(**payload.model_dump()) db.add(inst) await db.commit() await db.refresh(inst) return inst @router.patch("/{inst_code}", response_model=InstitutionOut) async def update_institution( inst_code: str, payload: InstitutionUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): _require_admin_or_pm(current_user) r = await db.execute(select(Institution).where(Institution.inst_code == inst_code)) inst = r.scalars().first() if not inst: raise HTTPException(404, "기관을 찾을 수 없습니다.") for k, v in payload.model_dump(exclude_unset=True).items(): setattr(inst, k, v) inst.updated_at = datetime.now() await db.commit() await db.refresh(inst) return inst @router.delete("/{inst_code}", status_code=204) async def deactivate_institution( inst_code: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): if current_user.role != UserRole.ADMIN: raise HTTPException(403, "ADMIN 권한이 필요합니다.") r = await db.execute(select(Institution).where(Institution.inst_code == inst_code)) inst = r.scalars().first() if not inst: raise HTTPException(404, "기관을 찾을 수 없습니다.") inst.is_active = False inst.updated_at = datetime.now() await db.commit() # ── 담당자 CRUD ──────────────────────────────────────────────────────────────── async def _get_inst(db: AsyncSession, inst_code: str) -> Institution: r = await db.execute(select(Institution).where(Institution.inst_code == inst_code)) inst = r.scalars().first() if not inst: raise HTTPException(404, "기관을 찾을 수 없습니다.") return inst @router.get("/{inst_code}/contacts", response_model=List[InstContactOut]) async def list_contacts( inst_code: str, db: AsyncSession = Depends(get_db), _u: User = Depends(get_current_user), ): inst = await _get_inst(db, inst_code) result = await db.execute( select(InstContact) .where(InstContact.inst_id == inst.id) .order_by(InstContact.is_primary.desc(), InstContact.contact_name) ) return result.scalars().all() @router.post("/{inst_code}/contacts", response_model=InstContactOut, status_code=201) async def add_contact( inst_code: str, payload: InstContactCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): _require_admin_or_pm(current_user) inst = await _get_inst(db, inst_code) # 주 담당자로 설정 시 기존 주 담당자 해제 if payload.is_primary: await db.execute( select(InstContact).where( InstContact.inst_id == inst.id, InstContact.is_primary == True ) ) prev_primary = (await db.execute( select(InstContact).where( InstContact.inst_id == inst.id, InstContact.is_primary == True ) )).scalars().first() if prev_primary: prev_primary.is_primary = False contact = InstContact(inst_id=inst.id, **payload.model_dump()) db.add(contact) await db.commit() await db.refresh(contact) return contact @router.patch("/{inst_code}/contacts/{contact_id}", response_model=InstContactOut) async def update_contact( inst_code: str, contact_id: int, payload: InstContactUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): _require_admin_or_pm(current_user) inst = await _get_inst(db, inst_code) r = await db.execute( select(InstContact).where( InstContact.id == contact_id, InstContact.inst_id == inst.id ) ) contact = r.scalars().first() if not contact: raise HTTPException(404, "담당자를 찾을 수 없습니다.") # 주 담당자로 변경 시 기존 주 담당자 해제 if payload.is_primary: prev = (await db.execute( select(InstContact).where( InstContact.inst_id == inst.id, InstContact.is_primary == True, InstContact.id != contact_id ) )).scalars().first() if prev: prev.is_primary = False for k, v in payload.model_dump(exclude_unset=True).items(): setattr(contact, k, v) await db.commit() await db.refresh(contact) return contact @router.delete("/{inst_code}/contacts/{contact_id}", status_code=204) async def delete_contact( inst_code: str, contact_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): if current_user.role != UserRole.ADMIN: raise HTTPException(403, "ADMIN 권한이 필요합니다.") inst = await _get_inst(db, inst_code) r = await db.execute( select(InstContact).where( InstContact.id == contact_id, InstContact.inst_id == inst.id ) ) contact = r.scalars().first() if not contact: raise HTTPException(404, "담당자를 찾을 수 없습니다.") contact.is_active = False await db.commit() # ── 기관 서버 목록 (기존 cmdb 대체) ─────────────────────────────────────────── @router.get("/{inst_code}/servers") async def list_inst_servers( inst_code: str, db: AsyncSession = Depends(get_db), _u: User = Depends(get_current_user), ): from models import ServerOut inst = await _get_inst(db, inst_code) result = await db.execute( select(Server) .where(Server.inst_id == inst.id) .order_by(Server.server_role, Server.server_name) ) servers = result.scalars().all() return [ServerOut.model_validate(s) for s in servers]