diff --git a/src/app/[locale]/(protected)/quality/qms/actions.ts b/src/app/[locale]/(protected)/quality/qms/actions.ts index 0fbfb8bf..ca036034 100644 --- a/src/app/[locale]/(protected)/quality/qms/actions.ts +++ b/src/app/[locale]/(protected)/quality/qms/actions.ts @@ -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 | null; + file_counts: Record; + 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 }, +) { + 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: '파일 삭제에 실패했습니다.', + }); +} diff --git a/src/app/[locale]/(protected)/quality/qms/components/AuditSettingsPanel.tsx b/src/app/[locale]/(protected)/quality/qms/components/AuditSettingsPanel.tsx index 362a5978..ae8fb2ac 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/AuditSettingsPanel.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/AuditSettingsPanel.tsx @@ -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 ? ( ) : (
diff --git a/src/app/[locale]/(protected)/quality/qms/components/ChecklistTemplateEditor.tsx b/src/app/[locale]/(protected)/quality/qms/components/ChecklistTemplateEditor.tsx index 1f8d8ee3..3003c2cd 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/ChecklistTemplateEditor.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/ChecklistTemplateEditor.tsx @@ -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>( new Set(categories.map(c => c.id)) @@ -76,15 +72,25 @@ export function ChecklistTemplateEditor({ }); }; + if (loading) { + return ( +
+ + 점검표 로딩 중... +
+ ); + } + + if (error) { + return ( +
+ {error} +
+ ); + } + return (
- {/* 버전 셀렉트박스 */} - - {/* 카테고리 목록 */}
{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" > - {saving ? '저장 중...' : '저장 (새 버전)'} + {saving ? '저장 중...' : '저장'}
@@ -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(null); - const searchInputRef = useRef(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 ( -
- {/* 트리거 버튼 */} - - - {/* 드롭다운 */} - {open && ( -
- {/* 검색 */} -
-
- - 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 && ( - - )} -
-
- - {/* 버전 목록 */} -
- {filteredVersions.length === 0 ? ( -
- 검색 결과가 없습니다 -
- ) : ( - filteredVersions.map(v => { - const isCurrent = v.version === currentVersion; - return ( -
!isCurrent && handleRestore(v.id)} - > -
-
- - v{v.version} - - {isCurrent && ( - - 현재 - - )} - {v.createdAt} - ({v.createdBy}) -
- {v.description && ( -

{v.description}

- )} -
- {!isCurrent && ( - - 복원 - - )} -
- ); - }) - )} -
- - {/* 하단 카운트 */} -
- - 전체 {versions.length}개 버전 - {search && ` / ${filteredVersions.length}개 검색됨`} - -
-
- )} -
- ); -} diff --git a/src/app/[locale]/(protected)/quality/qms/hooks/useChecklistTemplate.ts b/src/app/[locale]/(protected)/quality/qms/hooks/useChecklistTemplate.ts index ba9bc9ed..bf7dd437 100644 --- a/src/app/[locale]/(protected)/quality/qms/hooks/useChecklistTemplate.ts +++ b/src/app/[locale]/(protected)/quality/qms/hooks/useChecklistTemplate.ts @@ -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( - () => structuredClone(MOCK_DAY1_CATEGORIES) - ); - // 마지막 저장 상태 (초기화용) - const savedRef = useRef(structuredClone(MOCK_DAY1_CATEGORIES)); + const [templateId, setTemplateId] = useState(null); + const [editCategories, setEditCategories] = useState([]); + const savedRef = useRef([]); - const [versions] = useState(MOCK_VERSIONS); - const [currentVersion, setCurrentVersion] = useState(8); const [saving, setSaving] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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, }; } diff --git a/src/app/[locale]/(protected)/quality/qms/page.tsx b/src/app/[locale]/(protected)/quality/qms/page.tsx index b05127a3..d67a9f3d 100644 --- a/src/app/[locale]/(protected)/quality/qms/page.tsx +++ b/src/app/[locale]/(protected)/quality/qms/page.tsx @@ -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, }} /> diff --git a/src/app/[locale]/(protected)/quality/qms/types.ts b/src/app/[locale]/(protected)/quality/qms/types.ts index 668d8107..67c1e7d2 100644 --- a/src/app/[locale]/(protected)/quality/qms/types.ts +++ b/src/app/[locale]/(protected)/quality/qms/types.ts @@ -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 | null; + fileCounts: Record; + updatedAt: string | null; + updatedBy: string | null; }