위험성평가, 안전 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>
225 lines
5.6 KiB
TypeScript
225 lines
5.6 KiB
TypeScript
"use server";
|
|
|
|
import { db } from "@/lib/db";
|
|
import {
|
|
checklistTemplates,
|
|
checklistTemplateItems,
|
|
inspections,
|
|
inspectionResults,
|
|
nonConformances,
|
|
} from "@/lib/db/schema";
|
|
import { eq, desc, asc } from "drizzle-orm";
|
|
import { revalidatePath } from "next/cache";
|
|
|
|
// ─── Templates ───
|
|
|
|
export async function getTemplates() {
|
|
return db.query.checklistTemplates.findMany({
|
|
orderBy: [desc(checklistTemplates.createdAt)],
|
|
with: { items: { orderBy: [asc(checklistTemplateItems.sortOrder)] } },
|
|
});
|
|
}
|
|
|
|
export async function getTemplate(id: string) {
|
|
return db.query.checklistTemplates.findFirst({
|
|
where: eq(checklistTemplates.id, id),
|
|
with: { items: { orderBy: [asc(checklistTemplateItems.sortOrder)] } },
|
|
});
|
|
}
|
|
|
|
export async function createTemplate(data: {
|
|
name: string;
|
|
type: "daily" | "regular" | "special" | "equipment";
|
|
description?: string;
|
|
}) {
|
|
const [result] = await db
|
|
.insert(checklistTemplates)
|
|
.values(data)
|
|
.returning();
|
|
revalidatePath("/checklists");
|
|
return result;
|
|
}
|
|
|
|
export async function updateTemplate(
|
|
id: string,
|
|
data: Partial<{
|
|
name: string;
|
|
type: "daily" | "regular" | "special" | "equipment";
|
|
description: string;
|
|
isActive: boolean;
|
|
}>
|
|
) {
|
|
const [result] = await db
|
|
.update(checklistTemplates)
|
|
.set({ ...data, updatedAt: new Date() })
|
|
.where(eq(checklistTemplates.id, id))
|
|
.returning();
|
|
revalidatePath("/checklists");
|
|
return result;
|
|
}
|
|
|
|
export async function deleteTemplate(id: string) {
|
|
await db.delete(checklistTemplates).where(eq(checklistTemplates.id, id));
|
|
revalidatePath("/checklists");
|
|
}
|
|
|
|
export async function addTemplateItem(
|
|
templateId: string,
|
|
data: { content: string; category?: string; standard?: string }
|
|
) {
|
|
const existing = await db.query.checklistTemplateItems.findMany({
|
|
where: eq(checklistTemplateItems.templateId, templateId),
|
|
columns: { sortOrder: true },
|
|
});
|
|
const maxOrder = existing.reduce((max, i) => Math.max(max, i.sortOrder), -1);
|
|
|
|
const [result] = await db
|
|
.insert(checklistTemplateItems)
|
|
.values({ templateId, sortOrder: maxOrder + 1, ...data })
|
|
.returning();
|
|
revalidatePath("/checklists");
|
|
return result;
|
|
}
|
|
|
|
export async function updateTemplateItem(
|
|
itemId: string,
|
|
data: Partial<{ content: string; category: string; standard: string; sortOrder: number }>
|
|
) {
|
|
const [result] = await db
|
|
.update(checklistTemplateItems)
|
|
.set(data)
|
|
.where(eq(checklistTemplateItems.id, itemId))
|
|
.returning();
|
|
revalidatePath("/checklists");
|
|
return result;
|
|
}
|
|
|
|
export async function deleteTemplateItem(itemId: string) {
|
|
await db
|
|
.delete(checklistTemplateItems)
|
|
.where(eq(checklistTemplateItems.id, itemId));
|
|
revalidatePath("/checklists");
|
|
}
|
|
|
|
// ─── Inspections ───
|
|
|
|
export async function getInspections() {
|
|
return db.query.inspections.findMany({
|
|
orderBy: [desc(inspections.createdAt)],
|
|
with: {
|
|
template: true,
|
|
results: true,
|
|
nonConformances: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function getInspection(id: string) {
|
|
return db.query.inspections.findFirst({
|
|
where: eq(inspections.id, id),
|
|
with: {
|
|
template: { with: { items: { orderBy: [asc(checklistTemplateItems.sortOrder)] } } },
|
|
results: true,
|
|
nonConformances: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function createInspection(data: {
|
|
templateId: string;
|
|
inspector?: string;
|
|
location?: string;
|
|
}) {
|
|
const [result] = await db.insert(inspections).values(data).returning();
|
|
revalidatePath("/checklists/inspections");
|
|
return result;
|
|
}
|
|
|
|
export async function saveInspectionResults(
|
|
inspectionId: string,
|
|
results: Array<{
|
|
templateItemId: string;
|
|
result: string;
|
|
memo?: string;
|
|
}>
|
|
) {
|
|
// Delete existing results for this inspection
|
|
await db
|
|
.delete(inspectionResults)
|
|
.where(eq(inspectionResults.inspectionId, inspectionId));
|
|
|
|
if (results.length > 0) {
|
|
await db.insert(inspectionResults).values(
|
|
results.map((r) => ({
|
|
inspectionId,
|
|
templateItemId: r.templateItemId,
|
|
result: r.result,
|
|
memo: r.memo,
|
|
}))
|
|
);
|
|
}
|
|
|
|
revalidatePath(`/checklists/inspections/${inspectionId}`);
|
|
}
|
|
|
|
export async function completeInspection(inspectionId: string) {
|
|
await db
|
|
.update(inspections)
|
|
.set({ completedAt: new Date() })
|
|
.where(eq(inspections.id, inspectionId));
|
|
revalidatePath("/checklists/inspections");
|
|
}
|
|
|
|
// ─── Non-conformances ───
|
|
|
|
export async function getNonConformances() {
|
|
return db.query.nonConformances.findMany({
|
|
orderBy: [desc(nonConformances.createdAt)],
|
|
with: { inspection: { with: { template: true } } },
|
|
});
|
|
}
|
|
|
|
export async function createNonConformance(data: {
|
|
inspectionId: string;
|
|
inspectionResultId?: string;
|
|
description: string;
|
|
severity?: string;
|
|
responsiblePerson?: string;
|
|
dueDate?: string;
|
|
}) {
|
|
const [result] = await db
|
|
.insert(nonConformances)
|
|
.values({
|
|
...data,
|
|
dueDate: data.dueDate ? new Date(data.dueDate) : undefined,
|
|
})
|
|
.returning();
|
|
revalidatePath("/checklists");
|
|
return result;
|
|
}
|
|
|
|
export async function updateNonConformance(
|
|
id: string,
|
|
data: Partial<{
|
|
status: "open" | "corrective_action" | "verification" | "closed";
|
|
correctiveAction: string;
|
|
responsiblePerson: string;
|
|
closedBy: string;
|
|
}>
|
|
) {
|
|
const updateData: Record<string, unknown> = {
|
|
...data,
|
|
updatedAt: new Date(),
|
|
};
|
|
if (data.status === "closed") {
|
|
updateData.closedAt = new Date();
|
|
}
|
|
const [result] = await db
|
|
.update(nonConformances)
|
|
.set(updateData)
|
|
.where(eq(nonConformances.id, id))
|
|
.returning();
|
|
revalidatePath("/checklists");
|
|
return result;
|
|
}
|