feat: [QMS] 점검표 템플릿 Mock→API 연동 + 버전 UI 제거
- actions.ts: 5개 Server Actions 추가 (조회/저장/문서CRUD) - useChecklistTemplate: Mock→API 전환, loading/error 상태 추가 - ChecklistTemplateEditor: VersionSelectBox 제거, loading/error UI - AuditSettingsPanel/page.tsx: 버전 관련 props 정리 - types.ts: ChecklistTemplateVersion 제거, ChecklistTemplate 수정
This commit is contained in:
@@ -304,3 +304,137 @@ export async function detachStandardDocument(itemId: string, docId: string) {
|
||||
errorMessage: '기준 문서 연결 해제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 점검표 템플릿 관리 (설정 모달) =====
|
||||
|
||||
interface ChecklistTemplateApi {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
categories: {
|
||||
id: string;
|
||||
title: string;
|
||||
subItems: { id: string; name: string }[];
|
||||
}[];
|
||||
options: Record<string, unknown> | null;
|
||||
file_counts: Record<string, number>;
|
||||
updated_at: string | null;
|
||||
updated_by: string | null;
|
||||
}
|
||||
|
||||
interface TemplateDocumentApi {
|
||||
id: number;
|
||||
field_key: string;
|
||||
display_name: string;
|
||||
file_size: number;
|
||||
mime_type: string;
|
||||
uploaded_by: string | null;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
export async function getChecklistTemplate(type: string = 'day1_audit') {
|
||||
return executeServerAction({
|
||||
url: buildApiUrl('/api/v1/quality/checklist-templates', { type }),
|
||||
transform: (data: ChecklistTemplateApi) => ({
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
type: data.type,
|
||||
categories: data.categories.map((cat) => ({
|
||||
id: cat.id,
|
||||
title: cat.title,
|
||||
subItems: cat.subItems.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
isCompleted: false,
|
||||
})),
|
||||
})),
|
||||
options: data.options,
|
||||
fileCounts: data.file_counts,
|
||||
updatedAt: data.updated_at,
|
||||
updatedBy: data.updated_by,
|
||||
}),
|
||||
errorMessage: '점검표 템플릿 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveChecklistTemplate(
|
||||
id: number,
|
||||
data: { name?: string; categories: { id: string; title: string; subItems: { id: string; name: string }[] }[]; options?: Record<string, unknown> },
|
||||
) {
|
||||
return executeServerAction({
|
||||
url: buildApiUrl(`/api/v1/quality/checklist-templates/${id}`),
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
transform: (result: ChecklistTemplateApi) => ({
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
type: result.type,
|
||||
categories: result.categories.map((cat) => ({
|
||||
id: cat.id,
|
||||
title: cat.title,
|
||||
subItems: cat.subItems.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
isCompleted: false,
|
||||
})),
|
||||
})),
|
||||
options: result.options,
|
||||
fileCounts: result.file_counts,
|
||||
updatedAt: result.updated_at,
|
||||
updatedBy: result.updated_by,
|
||||
}),
|
||||
errorMessage: '점검표 템플릿 저장에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
export async function getTemplateDocuments(templateId: number, subItemId?: string) {
|
||||
return executeServerAction({
|
||||
url: buildApiUrl('/api/v1/quality/qms-documents', {
|
||||
template_id: templateId,
|
||||
sub_item_id: subItemId,
|
||||
}),
|
||||
transform: (data: TemplateDocumentApi[]) =>
|
||||
data.map((d) => ({
|
||||
id: d.id,
|
||||
fieldKey: d.field_key,
|
||||
displayName: d.display_name,
|
||||
fileSize: d.file_size,
|
||||
mimeType: d.mime_type,
|
||||
uploadedBy: d.uploaded_by,
|
||||
createdAt: d.created_at,
|
||||
})),
|
||||
errorMessage: '템플릿 문서 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
export async function uploadTemplateDocument(templateId: number, subItemId: string, file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('template_id', String(templateId));
|
||||
formData.append('sub_item_id', subItemId);
|
||||
formData.append('file', file);
|
||||
|
||||
return executeServerAction({
|
||||
url: buildApiUrl('/api/v1/quality/qms-documents'),
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
transform: (d: TemplateDocumentApi) => ({
|
||||
id: d.id,
|
||||
fieldKey: d.field_key,
|
||||
displayName: d.display_name,
|
||||
fileSize: d.file_size,
|
||||
mimeType: d.mime_type,
|
||||
createdAt: d.created_at,
|
||||
}),
|
||||
errorMessage: '파일 업로드에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteTemplateDocument(fileId: number, replace: boolean = false) {
|
||||
return executeServerAction({
|
||||
url: buildApiUrl(`/api/v1/quality/qms-documents/${fileId}`, {
|
||||
replace: replace ? 'true' : undefined,
|
||||
}),
|
||||
method: 'DELETE',
|
||||
errorMessage: '파일 삭제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Settings, X, Eye, EyeOff, ListChecks } from 'lucide-react';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ChecklistTemplateEditor } from './ChecklistTemplateEditor';
|
||||
import type { ChecklistCategory, ChecklistTemplateVersion } from '../types';
|
||||
import type { ChecklistCategory } from '../types';
|
||||
|
||||
export interface AuditDisplaySettings {
|
||||
showProgressBar: boolean;
|
||||
@@ -18,10 +18,10 @@ export interface AuditDisplaySettings {
|
||||
// 점검표 관리 props
|
||||
export interface ChecklistManagementProps {
|
||||
categories: ChecklistCategory[];
|
||||
versions: ChecklistTemplateVersion[];
|
||||
currentVersion: number;
|
||||
hasChanges: boolean;
|
||||
saving: boolean;
|
||||
loading?: boolean;
|
||||
error?: string | null;
|
||||
onAddCategory: () => void;
|
||||
onUpdateCategoryTitle: (categoryId: string, title: string) => void;
|
||||
onDeleteCategory: (categoryId: string) => void;
|
||||
@@ -32,9 +32,8 @@ export interface ChecklistManagementProps {
|
||||
onDeleteSubItem: (categoryId: string, subItemId: string) => void;
|
||||
onMoveSubItemUp: (categoryId: string, index: number) => void;
|
||||
onMoveSubItemDown: (categoryId: string, index: number) => void;
|
||||
onSave: (description?: string) => void;
|
||||
onSave: () => void;
|
||||
onReset: () => void;
|
||||
onRestoreVersion: (versionId: string) => void;
|
||||
}
|
||||
|
||||
interface AuditSettingsPanelProps {
|
||||
@@ -131,10 +130,10 @@ export function AuditSettingsPanel({
|
||||
) : checklistManagement ? (
|
||||
<ChecklistTemplateEditor
|
||||
categories={checklistManagement.categories}
|
||||
versions={checklistManagement.versions}
|
||||
currentVersion={checklistManagement.currentVersion}
|
||||
hasChanges={checklistManagement.hasChanges}
|
||||
saving={checklistManagement.saving}
|
||||
loading={checklistManagement.loading}
|
||||
error={checklistManagement.error}
|
||||
onAddCategory={checklistManagement.onAddCategory}
|
||||
onUpdateCategoryTitle={checklistManagement.onUpdateCategoryTitle}
|
||||
onDeleteCategory={checklistManagement.onDeleteCategory}
|
||||
@@ -147,7 +146,6 @@ export function AuditSettingsPanel({
|
||||
onMoveSubItemDown={checklistManagement.onMoveSubItemDown}
|
||||
onSave={checklistManagement.onSave}
|
||||
onReset={checklistManagement.onReset}
|
||||
onRestoreVersion={checklistManagement.onRestoreVersion}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-32 text-gray-400 text-sm">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
ChevronUp,
|
||||
ChevronDown,
|
||||
@@ -12,19 +12,17 @@ import {
|
||||
ChevronRight,
|
||||
Save,
|
||||
RotateCcw,
|
||||
History,
|
||||
Search,
|
||||
ChevronsUpDown,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ChecklistCategory, ChecklistSubItem, ChecklistTemplateVersion } from '../types';
|
||||
import type { ChecklistCategory, ChecklistSubItem } from '../types';
|
||||
|
||||
interface ChecklistTemplateEditorProps {
|
||||
categories: ChecklistCategory[];
|
||||
versions: ChecklistTemplateVersion[];
|
||||
currentVersion: number;
|
||||
hasChanges: boolean;
|
||||
saving: boolean;
|
||||
loading?: boolean;
|
||||
error?: string | null;
|
||||
// 카테고리
|
||||
onAddCategory: () => void;
|
||||
onUpdateCategoryTitle: (categoryId: string, title: string) => void;
|
||||
@@ -37,18 +35,17 @@ interface ChecklistTemplateEditorProps {
|
||||
onDeleteSubItem: (categoryId: string, subItemId: string) => void;
|
||||
onMoveSubItemUp: (categoryId: string, index: number) => void;
|
||||
onMoveSubItemDown: (categoryId: string, index: number) => void;
|
||||
// 저장/초기화/복원
|
||||
onSave: (description?: string) => void;
|
||||
// 저장/초기화
|
||||
onSave: () => void;
|
||||
onReset: () => void;
|
||||
onRestoreVersion: (versionId: string) => void;
|
||||
}
|
||||
|
||||
export function ChecklistTemplateEditor({
|
||||
categories,
|
||||
versions,
|
||||
currentVersion,
|
||||
hasChanges,
|
||||
saving,
|
||||
loading,
|
||||
error,
|
||||
onAddCategory,
|
||||
onUpdateCategoryTitle,
|
||||
onDeleteCategory,
|
||||
@@ -61,7 +58,6 @@ export function ChecklistTemplateEditor({
|
||||
onMoveSubItemDown,
|
||||
onSave,
|
||||
onReset,
|
||||
onRestoreVersion,
|
||||
}: ChecklistTemplateEditorProps) {
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
|
||||
new Set(categories.map(c => c.id))
|
||||
@@ -76,15 +72,25 @@ export function ChecklistTemplateEditor({
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-32 text-gray-400 text-sm gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
점검표 로딩 중...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-32 text-red-500 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{/* 버전 셀렉트박스 */}
|
||||
<VersionSelectBox
|
||||
versions={versions}
|
||||
currentVersion={currentVersion}
|
||||
onRestoreVersion={onRestoreVersion}
|
||||
/>
|
||||
|
||||
{/* 카테고리 목록 */}
|
||||
<div className="space-y-1.5">
|
||||
{categories.map((category, catIdx) => (
|
||||
@@ -137,7 +143,7 @@ export function ChecklistTemplateEditor({
|
||||
className="flex-1 flex items-center justify-center gap-1.5 py-2 text-xs text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Save className="h-3.5 w-3.5" />
|
||||
{saving ? '저장 중...' : '저장 (새 버전)'}
|
||||
{saving ? '저장 중...' : '저장'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -441,165 +447,3 @@ function SubItemEditor({
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 버전 검색 셀렉트박스 =====
|
||||
|
||||
interface VersionSelectBoxProps {
|
||||
versions: ChecklistTemplateVersion[];
|
||||
currentVersion: number;
|
||||
onRestoreVersion: (versionId: string) => void;
|
||||
}
|
||||
|
||||
function VersionSelectBox({ versions, currentVersion, onRestoreVersion }: VersionSelectBoxProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// 외부 클릭 시 닫기
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
setSearch('');
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [open]);
|
||||
|
||||
// 열릴 때 검색 input 포커스
|
||||
useEffect(() => {
|
||||
if (open) searchInputRef.current?.focus();
|
||||
}, [open]);
|
||||
|
||||
const currentV = versions.find(v => v.version === currentVersion);
|
||||
|
||||
const filteredVersions = useMemo(() => {
|
||||
if (!search.trim()) return versions;
|
||||
const term = search.toLowerCase();
|
||||
return versions.filter(v =>
|
||||
`v${v.version}`.includes(term) ||
|
||||
v.createdAt.includes(term) ||
|
||||
v.createdBy.toLowerCase().includes(term) ||
|
||||
(v.description?.toLowerCase().includes(term))
|
||||
);
|
||||
}, [versions, search]);
|
||||
|
||||
const handleRestore = (versionId: string) => {
|
||||
onRestoreVersion(versionId);
|
||||
setOpen(false);
|
||||
setSearch('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative">
|
||||
{/* 트리거 버튼 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(!open)}
|
||||
className={cn(
|
||||
'w-full flex items-center justify-between px-3 py-2 text-sm bg-white border rounded-lg transition-colors',
|
||||
open ? 'border-blue-400 ring-1 ring-blue-200' : 'border-gray-200 hover:border-gray-300'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<History className="h-4 w-4 text-gray-500" />
|
||||
<span className="font-medium text-gray-800">v{currentVersion}</span>
|
||||
{currentV && (
|
||||
<span className="text-xs text-gray-400">{currentV.createdAt}</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronsUpDown className="h-3.5 w-3.5 text-gray-400" />
|
||||
</button>
|
||||
|
||||
{/* 드롭다운 */}
|
||||
{open && (
|
||||
<div className="absolute z-10 top-full left-0 right-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg overflow-hidden">
|
||||
{/* 검색 */}
|
||||
<div className="p-2 border-b border-gray-100">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-gray-400" />
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="버전, 날짜, 설명 검색..."
|
||||
className="w-full pl-7 pr-2 py-1.5 text-xs border border-gray-200 rounded-md focus:outline-none focus:border-blue-300 focus:ring-1 focus:ring-blue-200"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearch('')}
|
||||
className="absolute right-1.5 top-1/2 -translate-y-1/2 p-0.5 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 버전 목록 */}
|
||||
<div className="max-h-48 overflow-y-auto">
|
||||
{filteredVersions.length === 0 ? (
|
||||
<div className="px-3 py-4 text-center text-xs text-gray-400">
|
||||
검색 결과가 없습니다
|
||||
</div>
|
||||
) : (
|
||||
filteredVersions.map(v => {
|
||||
const isCurrent = v.version === currentVersion;
|
||||
return (
|
||||
<div
|
||||
key={v.id}
|
||||
className={cn(
|
||||
'flex items-center justify-between px-3 py-2 text-xs transition-colors cursor-pointer',
|
||||
isCurrent
|
||||
? 'bg-blue-50'
|
||||
: 'hover:bg-gray-50'
|
||||
)}
|
||||
onClick={() => !isCurrent && handleRestore(v.id)}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className={cn(
|
||||
'font-semibold',
|
||||
isCurrent ? 'text-blue-600' : 'text-gray-700'
|
||||
)}>
|
||||
v{v.version}
|
||||
</span>
|
||||
{isCurrent && (
|
||||
<span className="text-[9px] bg-blue-100 text-blue-600 px-1 py-0.5 rounded">
|
||||
현재
|
||||
</span>
|
||||
)}
|
||||
<span className="text-gray-400">{v.createdAt}</span>
|
||||
<span className="text-gray-400">({v.createdBy})</span>
|
||||
</div>
|
||||
{v.description && (
|
||||
<p className="text-gray-500 mt-0.5 truncate">{v.description}</p>
|
||||
)}
|
||||
</div>
|
||||
{!isCurrent && (
|
||||
<span className="ml-2 text-[10px] text-blue-500 flex-shrink-0">
|
||||
복원
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 하단 카운트 */}
|
||||
<div className="px-3 py-1.5 border-t border-gray-100 bg-gray-50">
|
||||
<span className="text-[10px] text-gray-400">
|
||||
전체 {versions.length}개 버전
|
||||
{search && ` / ${filteredVersions.length}개 검색됨`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,41 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import type { ChecklistCategory, ChecklistSubItem, ChecklistTemplate, ChecklistTemplateVersion } from '../types';
|
||||
import { MOCK_DAY1_CATEGORIES } from '../mockData';
|
||||
|
||||
const USE_MOCK = true;
|
||||
|
||||
// Mock 버전 데이터
|
||||
const MOCK_VERSIONS: ChecklistTemplateVersion[] = [
|
||||
{ id: 'v8', version: 8, createdAt: '2026-03-10', createdBy: '홍길동', description: '검사설비 항목 추가' },
|
||||
{ id: 'v7', version: 7, createdAt: '2026-03-05', createdBy: '김영수', description: '문서관리 기준 보완' },
|
||||
{ id: 'v6', version: 6, createdAt: '2026-02-28', createdBy: '홍길동', description: '클레임 처리 항목 신규' },
|
||||
{ id: 'v5', version: 5, createdAt: '2026-02-20', createdBy: '이민정', description: '출하검사 기준 수정' },
|
||||
{ id: 'v4', version: 4, createdAt: '2026-02-15', createdBy: '홍길동', description: '제조공정 기준 세분화' },
|
||||
{ id: 'v3', version: 3, createdAt: '2026-02-01', createdBy: '박서연', description: 'KS인증 항목 반영' },
|
||||
{ id: 'v2', version: 2, createdAt: '2026-01-25', createdBy: '김영수', description: '설비점검 이력 추가' },
|
||||
{ id: 'v1', version: 1, createdAt: '2026-01-20', createdBy: '홍길동', description: '초기 점검표 생성' },
|
||||
];
|
||||
import type { ChecklistCategory } from '../types';
|
||||
import { getChecklistTemplate, saveChecklistTemplate } from '../actions';
|
||||
|
||||
function generateId() {
|
||||
return `item-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
||||
}
|
||||
|
||||
export function useChecklistTemplate() {
|
||||
// 편집 중인 카테고리 (원본 복사본)
|
||||
const [editCategories, setEditCategories] = useState<ChecklistCategory[]>(
|
||||
() => structuredClone(MOCK_DAY1_CATEGORIES)
|
||||
);
|
||||
// 마지막 저장 상태 (초기화용)
|
||||
const savedRef = useRef<ChecklistCategory[]>(structuredClone(MOCK_DAY1_CATEGORIES));
|
||||
const [templateId, setTemplateId] = useState<number | null>(null);
|
||||
const [editCategories, setEditCategories] = useState<ChecklistCategory[]>([]);
|
||||
const savedRef = useRef<ChecklistCategory[]>([]);
|
||||
|
||||
const [versions] = useState<ChecklistTemplateVersion[]>(MOCK_VERSIONS);
|
||||
const [currentVersion, setCurrentVersion] = useState(8);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
// === 초기 로드 ===
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
getChecklistTemplate('day1_audit')
|
||||
.then((result) => {
|
||||
if (cancelled) return;
|
||||
if (result.success && result.data) {
|
||||
setTemplateId(result.data.id);
|
||||
const cats = result.data.categories;
|
||||
setEditCategories(structuredClone(cats));
|
||||
savedRef.current = structuredClone(cats);
|
||||
} else {
|
||||
setError(result.error || '템플릿 로드 실패');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setError('템플릿 로드 중 오류 발생');
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
// === 변경 추적 ===
|
||||
const markChanged = useCallback(() => setHasChanges(true), []);
|
||||
|
||||
@@ -154,23 +165,40 @@ export function useChecklistTemplate() {
|
||||
}, [markChanged]);
|
||||
|
||||
// === 저장 ===
|
||||
const saveTemplate = useCallback(async (description?: string) => {
|
||||
if (USE_MOCK) {
|
||||
setSaving(true);
|
||||
// Mock: 0.5초 딜레이
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
savedRef.current = structuredClone(editCategories);
|
||||
setCurrentVersion(prev => prev + 1);
|
||||
setHasChanges(false);
|
||||
setSaving(false);
|
||||
toast.success(`점검표 v${currentVersion + 1} 저장 완료`);
|
||||
return editCategories;
|
||||
}
|
||||
const saveTemplate = useCallback(async () => {
|
||||
if (!templateId) return;
|
||||
|
||||
// TODO: API 연동
|
||||
// const result = await saveChecklistTemplate({ categories: editCategories, description });
|
||||
return editCategories;
|
||||
}, [editCategories, currentVersion]);
|
||||
setSaving(true);
|
||||
try {
|
||||
// API용 데이터: isCompleted 제거
|
||||
const apiCategories = editCategories.map(cat => ({
|
||||
id: cat.id,
|
||||
title: cat.title,
|
||||
subItems: cat.subItems.map(item => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
})),
|
||||
}));
|
||||
|
||||
const result = await saveChecklistTemplate(templateId, {
|
||||
categories: apiCategories,
|
||||
});
|
||||
|
||||
if (result.success && result.data) {
|
||||
const cats = result.data.categories;
|
||||
setEditCategories(structuredClone(cats));
|
||||
savedRef.current = structuredClone(cats);
|
||||
setHasChanges(false);
|
||||
toast.success('점검표가 저장되었습니다.');
|
||||
} else {
|
||||
toast.error(result.error || '저장에 실패했습니다.');
|
||||
}
|
||||
} catch {
|
||||
toast.error('저장 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [editCategories, templateId]);
|
||||
|
||||
// === 초기화 ===
|
||||
const resetToSaved = useCallback(() => {
|
||||
@@ -178,23 +206,14 @@ export function useChecklistTemplate() {
|
||||
setHasChanges(false);
|
||||
}, []);
|
||||
|
||||
// === 버전 복원 ===
|
||||
const restoreVersion = useCallback(async (versionId: string) => {
|
||||
if (USE_MOCK) {
|
||||
// Mock: 버전에 상관없이 현재 데이터 유지 (실제로는 API에서 해당 버전 데이터 조회)
|
||||
toast.success('해당 버전으로 복원되었습니다. (Mock)');
|
||||
return;
|
||||
}
|
||||
// TODO: API 연동
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// 데이터
|
||||
templateId,
|
||||
editCategories,
|
||||
versions,
|
||||
currentVersion,
|
||||
hasChanges,
|
||||
saving,
|
||||
loading,
|
||||
error,
|
||||
|
||||
// 카테고리
|
||||
addCategory,
|
||||
@@ -213,6 +232,5 @@ export function useChecklistTemplate() {
|
||||
// 저장/초기화
|
||||
saveTemplate,
|
||||
resetToSaved,
|
||||
restoreVersion,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -228,10 +228,10 @@ export default function QualityInspectionPage() {
|
||||
onSettingsChange={setDisplaySettings}
|
||||
checklistManagement={{
|
||||
categories: checklistTemplate.editCategories,
|
||||
versions: checklistTemplate.versions,
|
||||
currentVersion: checklistTemplate.currentVersion,
|
||||
hasChanges: checklistTemplate.hasChanges,
|
||||
saving: checklistTemplate.saving,
|
||||
loading: checklistTemplate.loading,
|
||||
error: checklistTemplate.error,
|
||||
onAddCategory: checklistTemplate.addCategory,
|
||||
onUpdateCategoryTitle: checklistTemplate.updateCategoryTitle,
|
||||
onDeleteCategory: checklistTemplate.deleteCategory,
|
||||
@@ -244,7 +244,6 @@ export default function QualityInspectionPage() {
|
||||
onMoveSubItemDown: checklistTemplate.moveSubItemDown,
|
||||
onSave: checklistTemplate.saveTemplate,
|
||||
onReset: checklistTemplate.resetToSaved,
|
||||
onRestoreVersion: checklistTemplate.restoreVersion,
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -95,19 +95,14 @@ export interface Day2Progress {
|
||||
|
||||
// ===== 점검표 템플릿 관리 타입 =====
|
||||
|
||||
// 점검표 템플릿 버전
|
||||
export interface ChecklistTemplateVersion {
|
||||
id: string;
|
||||
version: number;
|
||||
createdAt: string;
|
||||
createdBy: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// 점검표 템플릿 (API 응답)
|
||||
export interface ChecklistTemplate {
|
||||
id: string;
|
||||
currentVersion: number;
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
categories: ChecklistCategory[];
|
||||
versions: ChecklistTemplateVersion[];
|
||||
options: Record<string, unknown> | null;
|
||||
fileCounts: Record<string, number>;
|
||||
updatedAt: string | null;
|
||||
updatedBy: string | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user