Files
safe-project/src/components/risk-assessment/ai-hazard-panel.tsx
Hyungi Ahn 2a9968fa7f feat: TK 안전관리 플랫폼 초기 구현
위험성평가, 안전 RAG Q&A, 안전점검 체크리스트를 통합한
안전관리자 전용 웹 플랫폼 전체 구현.

- Next.js 15 (App Router) + TypeScript + Tailwind + shadcn/ui
- Drizzle ORM + PostgreSQL 16 (12개 테이블)
- 위험성평가 CRUD + 5x5 위험성 매트릭스 + 인쇄 내보내기
- 체크리스트 템플릿/점검/NCR 추적
- RAG 문서 파이프라인 (Tika + bge-m3 + Qdrant)
- SSE 스트리밍 RAG 채팅 (qwen3.5:35b-a3b)
- AI 어시스트 (위험요인 추천, 감소대책, 점검항목 생성)
- 대시보드 통계/차트 (recharts)
- 단일 사용자 인증 (HMAC 쿠키 세션)
- 다크모드 지원
- Docker 멀티스테이지 빌드 (standalone)
- 프로젝트 가이드 문서 (docs/)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 12:33:55 +09:00

135 lines
4.7 KiB
TypeScript

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Sparkles, Plus, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
import { getRiskGrade } from "@/lib/constants";
import { addHazard } from "@/actions/risk-assessment";
interface SuggestedHazard {
category: string;
hazard: string;
consequence: string;
severity: number;
likelihood: number;
existingControls: string;
}
export function AiHazardPanel({ assessmentId }: { assessmentId: string }) {
const router = useRouter();
const [activity, setActivity] = useState("");
const [loading, setLoading] = useState(false);
const [suggestions, setSuggestions] = useState<SuggestedHazard[]>([]);
const [error, setError] = useState("");
async function handleSuggest() {
if (!activity.trim()) return;
setLoading(true);
setError("");
setSuggestions([]);
try {
const res = await fetch("/api/ai/suggest-hazards", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ activity }),
});
const data = await res.json();
if (data.error) throw new Error(data.error);
setSuggestions(data.hazards || []);
} catch (err) {
setError(err instanceof Error ? err.message : "AI 오류");
}
setLoading(false);
}
async function handleAdd(hazard: SuggestedHazard) {
await addHazard(assessmentId, {
category: hazard.category,
activity: activity,
hazard: hazard.hazard,
consequence: hazard.consequence,
severity: hazard.severity,
likelihood: hazard.likelihood,
existingControls: hazard.existingControls,
});
router.refresh();
}
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="outline" size="sm">
<Sparkles className="h-4 w-4 mr-2" />
AI
</Button>
</SheetTrigger>
<SheetContent className="w-[500px] sm:max-w-[500px] overflow-auto">
<SheetHeader>
<SheetTitle>AI </SheetTitle>
</SheetHeader>
<div className="space-y-4 mt-4">
<div className="space-y-2">
<Textarea
value={activity}
onChange={(e) => setActivity(e.target.value)}
placeholder="작업/공정을 설명하세요. 예: 5층 건물 외벽 페인트 작업, 비계 사용"
rows={3}
/>
<Button onClick={handleSuggest} disabled={loading || !activity.trim()}>
{loading ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Sparkles className="h-4 w-4 mr-2" />
)}
{loading ? "분석 중..." : "위험요인 분석"}
</Button>
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
{suggestions.map((h, i) => {
const risk = h.severity * h.likelihood;
const grade = getRiskGrade(risk);
return (
<Card key={i}>
<CardContent className="py-3 space-y-2">
<div className="flex items-center justify-between">
<Badge variant="outline">{h.category}</Badge>
<Badge className={`${grade.color} ${grade.textColor} border`} variant="outline">
{risk} ({grade.label})
</Badge>
</div>
<p className="text-sm font-medium">{h.hazard}</p>
{h.consequence && (
<p className="text-xs text-muted-foreground">
: {h.consequence}
</p>
)}
{h.existingControls && (
<p className="text-xs text-muted-foreground">
: {h.existingControls}
</p>
)}
<div className="flex justify-between items-center text-xs text-muted-foreground">
<span>S={h.severity} x L={h.likelihood}</span>
<Button size="sm" variant="outline" onClick={() => handleAdd(h)}>
<Plus className="h-3.5 w-3.5 mr-1" />
</Button>
</div>
</CardContent>
</Card>
);
})}
</div>
</SheetContent>
</Sheet>
);
}