139 lines
5.2 KiB
Python
139 lines
5.2 KiB
Python
"""
|
|
통합 검색 API (모바일 기능 #50).
|
|
|
|
GET /api/search/?q={query}&types=sr,server,kb,institution
|
|
|
|
SR, 서버(CMDB), KB 문서, 기관을 동시에 검색하여 타입별 결과를 반환.
|
|
보안: 서버 결과는 ServerOut 안전 필드만 반환(ip_addr/ssh_user/os_pw_enc 제외).
|
|
CUSTOMER 역할은 자신의 기관 SR/서버만 조회.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy import select, or_
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from core.auth import get_current_user
|
|
from database import get_db
|
|
from models import (
|
|
Institution, KBDocument, Server, SRRequest, User, UserRole,
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/search", tags=["Search"])
|
|
|
|
_PER_TYPE_LIMIT = 5
|
|
|
|
|
|
async def _customer_inst_id(user: User, db: AsyncSession) -> Optional[int]:
|
|
"""CUSTOMER 역할이면 소속 기관 id 반환, 아니면 None."""
|
|
if user.role == UserRole.CUSTOMER and user.inst_code:
|
|
inst = (await db.execute(
|
|
select(Institution).where(Institution.inst_code == user.inst_code)
|
|
)).scalars().first()
|
|
return inst.id if inst else -1
|
|
return None
|
|
|
|
|
|
@router.get("/")
|
|
async def global_search(
|
|
q: str = Query(..., min_length=1, description="검색어"),
|
|
types: str = Query("sr,server,kb,institution", description="콤마 구분 검색 대상"),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""SR + 서버 + KB + 기관 통합 검색."""
|
|
wanted = {t.strip() for t in types.split(",") if t.strip()}
|
|
if not wanted:
|
|
raise HTTPException(422, "types에 최소 하나의 검색 대상을 지정하세요.")
|
|
|
|
like = f"%{q}%"
|
|
results: dict = {}
|
|
cust_inst_id = await _customer_inst_id(current_user, db)
|
|
|
|
# ── SR 검색 ──────────────────────────────────────────────────────────
|
|
if "sr" in wanted:
|
|
sr_q = select(SRRequest).where(
|
|
or_(SRRequest.title.ilike(like),
|
|
SRRequest.description.ilike(like),
|
|
SRRequest.sr_id.ilike(like))
|
|
)
|
|
if cust_inst_id is not None:
|
|
sr_q = sr_q.where(SRRequest.inst_id == cust_inst_id)
|
|
sr_q = sr_q.order_by(SRRequest.created_at.desc()).limit(_PER_TYPE_LIMIT)
|
|
srs = (await db.execute(sr_q)).scalars().all()
|
|
results["sr"] = [
|
|
{
|
|
"sr_id": s.sr_id,
|
|
"title": s.title,
|
|
"status": s.status,
|
|
"priority": s.priority,
|
|
"sr_type": s.sr_type,
|
|
}
|
|
for s in srs
|
|
]
|
|
|
|
# ── 서버(CMDB) 검색 — 자격증명 필드 절대 제외 ──────────────────────────
|
|
if "server" in wanted:
|
|
srv_q = select(Server).where(
|
|
or_(Server.server_name.ilike(like),
|
|
Server.server_role.ilike(like),
|
|
Server.os_type.ilike(like))
|
|
)
|
|
if cust_inst_id is not None:
|
|
srv_q = srv_q.where(Server.inst_id == cust_inst_id)
|
|
srv_q = srv_q.limit(_PER_TYPE_LIMIT)
|
|
servers = (await db.execute(srv_q)).scalars().all()
|
|
results["server"] = [
|
|
{
|
|
"id": s.id,
|
|
"server_name": s.server_name,
|
|
"server_role": s.server_role,
|
|
"os_type": s.os_type,
|
|
"inst_id": s.inst_id,
|
|
# ip_addr / ssh_user / os_pw_enc 절대 미포함
|
|
}
|
|
for s in servers
|
|
]
|
|
|
|
# ── KB 검색 ──────────────────────────────────────────────────────────
|
|
if "kb" in wanted:
|
|
kb_q = select(KBDocument).where(
|
|
or_(KBDocument.title.ilike(like),
|
|
KBDocument.symptoms.ilike(like),
|
|
KBDocument.tags.ilike(like))
|
|
).limit(_PER_TYPE_LIMIT)
|
|
kbs = (await db.execute(kb_q)).scalars().all()
|
|
results["kb"] = [
|
|
{
|
|
"doc_id": k.doc_id,
|
|
"title": k.title,
|
|
"category": k.category,
|
|
"tags": k.tags,
|
|
}
|
|
for k in kbs
|
|
]
|
|
|
|
# ── 기관 검색 ────────────────────────────────────────────────────────
|
|
if "institution" in wanted:
|
|
inst_q = select(Institution).where(
|
|
or_(Institution.inst_name.ilike(like),
|
|
Institution.inst_code.ilike(like))
|
|
)
|
|
if cust_inst_id is not None and cust_inst_id != -1:
|
|
inst_q = inst_q.where(Institution.id == cust_inst_id)
|
|
inst_q = inst_q.limit(_PER_TYPE_LIMIT)
|
|
insts = (await db.execute(inst_q)).scalars().all()
|
|
results["institution"] = [
|
|
{
|
|
"id": i.id,
|
|
"inst_code": i.inst_code,
|
|
"inst_name": i.inst_name,
|
|
}
|
|
for i in insts
|
|
]
|
|
|
|
total = sum(len(v) for v in results.values())
|
|
return {"query": q, "total": total, "results": results}
|