Files
conai/backend/app/api/settings.py
sinmb79 2a4950d8a0 feat: CONAI Phase 1 MVP 초기 구현
소형 건설업체(100억 미만)를 위한 AI 기반 토목공사 통합관리 플랫폼

Backend (FastAPI):
- SQLAlchemy 모델 13개 (users, projects, wbs, tasks, daily_reports, reports, inspections, quality, weather, permits, rag, settings)
- API 라우터 11개 (auth, projects, tasks, daily_reports, reports, inspections, weather, rag, kakao, permits, settings)
- Services: Claude AI 래퍼, CPM Gantt 계산, 기상청 API, RAG(pgvector), 카카오 Skill API
- Alembic 마이그레이션 (pgvector 포함)
- pytest 테스트 (CPM, 날씨 경보)

Frontend (Next.js 15):
- 11개 페이지 (대시보드, 프로젝트, Gantt, 일보, 검측, 품질, 날씨, 인허가, RAG, 설정)
- TanStack Query + Zustand + Tailwind CSS

인프라:
- Docker Compose (PostgreSQL pgvector + backend + frontend)
- 한국어 README 및 설치 가이드

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 20:06:36 +09:00

146 lines
6.0 KiB
Python

import uuid
from datetime import datetime, timezone
from fastapi import APIRouter, HTTPException, status
from sqlalchemy import select
from app.deps import CurrentUser, DB
from app.models.settings import ClientProfile, AlertRule, WorkTypeLibrary
from app.schemas.settings import (
ClientProfileCreate, ClientProfileResponse,
WorkTypeCreate, WorkTypeResponse,
AlertRuleCreate, AlertRuleResponse,
SettingsExport,
)
router = APIRouter(prefix="/settings", tags=["커스텀 설정"])
# Client Profiles
@router.get("/client-profiles", response_model=list[ClientProfileResponse])
async def list_profiles(db: DB, current_user: CurrentUser):
result = await db.execute(select(ClientProfile).order_by(ClientProfile.name))
return result.scalars().all()
@router.post("/client-profiles", response_model=ClientProfileResponse, status_code=status.HTTP_201_CREATED)
async def create_profile(data: ClientProfileCreate, db: DB, current_user: CurrentUser):
profile = ClientProfile(**data.model_dump())
db.add(profile)
await db.commit()
await db.refresh(profile)
return profile
@router.put("/client-profiles/{profile_id}", response_model=ClientProfileResponse)
async def update_profile(profile_id: uuid.UUID, data: ClientProfileCreate, db: DB, current_user: CurrentUser):
result = await db.execute(select(ClientProfile).where(ClientProfile.id == profile_id))
profile = result.scalar_one_or_none()
if not profile:
raise HTTPException(status_code=404, detail="발주처 프로파일을 찾을 수 없습니다")
for field, value in data.model_dump(exclude_none=True).items():
setattr(profile, field, value)
await db.commit()
await db.refresh(profile)
return profile
@router.delete("/client-profiles/{profile_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_profile(profile_id: uuid.UUID, db: DB, current_user: CurrentUser):
result = await db.execute(select(ClientProfile).where(ClientProfile.id == profile_id))
profile = result.scalar_one_or_none()
if not profile:
raise HTTPException(status_code=404, detail="발주처 프로파일을 찾을 수 없습니다")
await db.delete(profile)
await db.commit()
# Work Types
@router.get("/work-types", response_model=list[WorkTypeResponse])
async def list_work_types(db: DB, current_user: CurrentUser):
result = await db.execute(select(WorkTypeLibrary).order_by(WorkTypeLibrary.category, WorkTypeLibrary.name))
return result.scalars().all()
@router.post("/work-types", response_model=WorkTypeResponse, status_code=status.HTTP_201_CREATED)
async def create_work_type(data: WorkTypeCreate, db: DB, current_user: CurrentUser):
wt = WorkTypeLibrary(**data.model_dump(), is_system=False)
db.add(wt)
await db.commit()
await db.refresh(wt)
return wt
# Alert Rules
@router.get("/alert-rules", response_model=list[AlertRuleResponse])
async def list_alert_rules(db: DB, current_user: CurrentUser):
result = await db.execute(select(AlertRule).order_by(AlertRule.created_at.desc()))
return result.scalars().all()
@router.post("/alert-rules", response_model=AlertRuleResponse, status_code=status.HTTP_201_CREATED)
async def create_alert_rule(data: AlertRuleCreate, db: DB, current_user: CurrentUser):
rule = AlertRule(**data.model_dump())
db.add(rule)
await db.commit()
await db.refresh(rule)
return rule
@router.put("/alert-rules/{rule_id}", response_model=AlertRuleResponse)
async def update_alert_rule(rule_id: uuid.UUID, data: AlertRuleCreate, db: DB, current_user: CurrentUser):
result = await db.execute(select(AlertRule).where(AlertRule.id == rule_id))
rule = result.scalar_one_or_none()
if not rule:
raise HTTPException(status_code=404, detail="알림 규칙을 찾을 수 없습니다")
for field, value in data.model_dump(exclude_none=True).items():
setattr(rule, field, value)
await db.commit()
await db.refresh(rule)
return rule
# JSON Export / Import
@router.get("/export", response_model=SettingsExport)
async def export_settings(db: DB, current_user: CurrentUser):
profiles_result = await db.execute(select(ClientProfile))
work_types_result = await db.execute(select(WorkTypeLibrary))
rules_result = await db.execute(select(AlertRule))
return SettingsExport(
client_profiles=[ClientProfileResponse.model_validate(p) for p in profiles_result.scalars().all()],
work_types=[WorkTypeResponse.model_validate(wt) for wt in work_types_result.scalars().all()],
alert_rules=[AlertRuleResponse.model_validate(r) for r in rules_result.scalars().all()],
exported_at=datetime.now(timezone.utc),
)
@router.post("/import", status_code=status.HTTP_200_OK)
async def import_settings(data: SettingsExport, db: DB, current_user: CurrentUser):
"""Import settings from JSON. Does NOT overwrite existing records."""
imported = {"client_profiles": 0, "work_types": 0, "alert_rules": 0}
for profile in data.client_profiles:
existing = await db.execute(select(ClientProfile).where(ClientProfile.name == profile.name))
if not existing.scalar_one_or_none():
db.add(ClientProfile(
name=profile.name,
report_frequency=profile.report_frequency,
template_config=profile.template_config,
contact_info=profile.contact_info,
is_default=profile.is_default,
))
imported["client_profiles"] += 1
for wt in data.work_types:
existing = await db.execute(select(WorkTypeLibrary).where(WorkTypeLibrary.code == wt.code))
if not existing.scalar_one_or_none():
db.add(WorkTypeLibrary(
code=wt.code, name=wt.name, category=wt.category,
weather_constraints=wt.weather_constraints,
default_checklist=wt.default_checklist,
is_system=False,
))
imported["work_types"] += 1
await db.commit()
return {"message": "설정을 가져왔습니다", "imported": imported}