""" ERP / 그룹웨어 연동 커넥터 기능: - 그룹웨어 전자결재 연동 (결재 요청 → GUARDiA SR 생성) - ERP HR 데이터 동기화 (사용자 조직 정보) - 범용 REST API 커넥터 (설정 기반) 엔드포인트: POST /api/erp/config — ERP 연동 설정 GET /api/erp/config — 설정 조회 POST /api/erp/test — 연결 테스트 POST /api/erp/webhook — ERP 웹훅 수신 (결재 알림) POST /api/erp/sync-users — HR 사용자 동기화 GET /api/erp/org-chart — 조직도 조회 """ from __future__ import annotations import logging from datetime import datetime from typing import Optional import httpx from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request from pydantic import BaseModel, Field from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user, require_admin_role from database import get_db from models import User, UserRole, SRRequest, SRStatus, ERPConfig # 신규 logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/erp", tags=["ERP 연동"]) class ERPConfigCreate(BaseModel): name: str = Field(..., max_length=100, description="시스템명 (예: 나라장터, 그룹웨어)") base_url: str api_key: Optional[str] = None username: Optional[str] = None password: Optional[str] = None erp_type: str = Field("generic", description="groupware | nara | hr | generic") @router.post("/config") async def save_erp_config( req: ERPConfigCreate, db: AsyncSession = Depends(get_db), user: User = Depends(require_admin_role), ): cfg = ERPConfig( tenant_id=user.tenant_id, name=req.name, base_url=req.base_url, api_key_enc=req.api_key, username=req.username, password_enc=req.password, erp_type=req.erp_type, is_active=True, created_at=datetime.utcnow() ) db.add(cfg) await db.commit() await db.refresh(cfg) return {"ok": True, "id": cfg.id} @router.get("/config") async def list_erp_configs(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): rows = await db.execute(select(ERPConfig).where(ERPConfig.tenant_id == user.tenant_id, ERPConfig.is_active == True)) cfgs = rows.scalars().all() return [{"id": c.id, "name": c.name, "erp_type": c.erp_type, "base_url": c.base_url[:30] + "..."} for c in cfgs] @router.post("/test/{config_id}") async def test_erp(config_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): row = await db.execute(select(ERPConfig).where(ERPConfig.id == config_id, ERPConfig.tenant_id == user.tenant_id)) cfg = row.scalar_one_or_none() if not cfg: raise HTTPException(404, "설정 없음") try: headers = {"Authorization": f"Bearer {cfg.api_key_enc}"} if cfg.api_key_enc else {} async with httpx.AsyncClient(timeout=10, verify=False) as c: r = await c.get(cfg.base_url, headers=headers) return {"ok": r.status_code < 400, "status_code": r.status_code} except Exception as e: return {"ok": False, "error": str(e)} @router.post("/webhook") async def erp_webhook(request: Request, background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db)): """ERP 웹훅 수신 — 결재 요청 → SR 자동 생성.""" body = await request.json() event_type = body.get("event_type", "") title = body.get("title") or body.get("subject") or "ERP 연동 요청" description = body.get("description") or body.get("content") or json_to_str(body) if event_type in ("APPROVAL_REQUEST", "WORK_ORDER", "MAINTENANCE_REQUEST"): sr = SRRequest( title=f"[ERP] {title[:100]}", description=description[:1000], category="ERP", priority="MEDIUM", status=SRStatus.OPEN, created_at=datetime.utcnow(), ) db.add(sr) await db.commit() return {"ok": True, "sr_id": sr.id} return {"ok": True, "skipped": True} @router.post("/sync-users/{config_id}") async def sync_hr_users( config_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(require_admin_role), ): """ERP HR → GUARDiA 사용자 동기화.""" row = await db.execute(select(ERPConfig).where(ERPConfig.id == config_id, ERPConfig.tenant_id == user.tenant_id)) cfg = row.scalar_one_or_none() if not cfg: raise HTTPException(404, "설정 없음") try: headers = {"Authorization": f"Bearer {cfg.api_key_enc}"} if cfg.api_key_enc else {} async with httpx.AsyncClient(timeout=15, verify=False) as c: r = await c.get(f"{cfg.base_url}/users", headers=headers) if r.status_code != 200: raise HTTPException(400, "HR API 응답 오류") hr_users = r.json().get("users", r.json() if isinstance(r.json(), list) else []) except Exception as e: raise HTTPException(500, f"HR 연결 실패: {e}") synced = 0 for hr_user in hr_users: email = hr_user.get("email") or hr_user.get("mail") name = hr_user.get("name") or hr_user.get("displayName") if not email: continue existing = await db.execute(select(User).where(User.email == email)) u = existing.scalar_one_or_none() if u: if name: u.name = name synced += 1 await db.commit() return {"ok": True, "synced": synced, "total_hr": len(hr_users)} @router.get("/org-chart/{config_id}") async def get_org_chart( config_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): row = await db.execute(select(ERPConfig).where(ERPConfig.id == config_id, ERPConfig.tenant_id == user.tenant_id)) cfg = row.scalar_one_or_none() if not cfg: raise HTTPException(404, "설정 없음") try: headers = {"Authorization": f"Bearer {cfg.api_key_enc}"} if cfg.api_key_enc else {} async with httpx.AsyncClient(timeout=15, verify=False) as c: r = await c.get(f"{cfg.base_url}/org-chart", headers=headers) return r.json() if r.status_code == 200 else {"departments": []} except Exception: return {"departments": []} def json_to_str(data: dict) -> str: import json return json.dumps(data, ensure_ascii=False)[:500]