diff --git a/src/app/[locale]/(protected)/quality/qms/components/AuditProgressBar.tsx b/src/app/[locale]/(protected)/quality/qms/components/AuditProgressBar.tsx new file mode 100644 index 00000000..04d78aca --- /dev/null +++ b/src/app/[locale]/(protected)/quality/qms/components/AuditProgressBar.tsx @@ -0,0 +1,97 @@ +'use client'; + +import React from 'react'; +import { cn } from '@/lib/utils'; + +interface AuditProgressBarProps { + day1Progress: { completed: number; total: number }; + day2Progress: { completed: number; total: number }; + activeDay: 1 | 2; +} + +export function AuditProgressBar({ + day1Progress, + day2Progress, + activeDay, +}: AuditProgressBarProps) { + const totalCompleted = day1Progress.completed + day2Progress.completed; + const totalItems = day1Progress.total + day2Progress.total; + const overallPercentage = totalItems > 0 ? Math.round((totalCompleted / totalItems) * 100) : 0; + + const day1Percentage = day1Progress.total > 0 + ? Math.round((day1Progress.completed / day1Progress.total) * 100) + : 0; + const day2Percentage = day2Progress.total > 0 + ? Math.round((day2Progress.completed / day2Progress.total) * 100) + : 0; + + return ( +
+
+

전체 심사 진행률

+ {overallPercentage}% +
+ + {/* 전체 진행률 바 */} +
+
+
+ + {/* 1일차/2일차 상세 진행률 */} +
+ {/* 1일차 */} +
+
+ 1일차: 기준/매뉴얼 + + {day1Progress.completed}/{day1Progress.total} + +
+
+
+
+
+ + {/* 2일차 */} +
+
+ 2일차: 로트추적 + + {day2Progress.completed}/{day2Progress.total} + +
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/quality/qms/components/AuditSettingsPanel.tsx b/src/app/[locale]/(protected)/quality/qms/components/AuditSettingsPanel.tsx new file mode 100644 index 00000000..55dd8cb4 --- /dev/null +++ b/src/app/[locale]/(protected)/quality/qms/components/AuditSettingsPanel.tsx @@ -0,0 +1,206 @@ +'use client'; + +import React from 'react'; +import { Settings, X, Eye, EyeOff } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { Switch } from '@/components/ui/switch'; + +export interface AuditDisplaySettings { + showProgressBar: boolean; + showDocumentViewer: boolean; + showDocumentSection: boolean; + showCompletedItems: boolean; + expandAllCategories: boolean; +} + +interface AuditSettingsPanelProps { + isOpen: boolean; + onClose: () => void; + settings: AuditDisplaySettings; + onSettingsChange: (settings: AuditDisplaySettings) => void; +} + +export function AuditSettingsPanel({ + isOpen, + onClose, + settings, + onSettingsChange, +}: AuditSettingsPanelProps) { + const handleToggle = (key: keyof AuditDisplaySettings) => { + onSettingsChange({ + ...settings, + [key]: !settings[key], + }); + }; + + if (!isOpen) return null; + + return ( +
+ {/* 배경 오버레이 */} +
+ + {/* 설정 패널 */} +
+ {/* 헤더 */} +
+
+ +

화면 설정

+
+ +
+ + {/* 설정 항목 */} +
+ {/* 레이아웃 섹션 */} +
+

레이아웃

+
+ handleToggle('showProgressBar')} + /> + handleToggle('showDocumentViewer')} + /> + handleToggle('showDocumentSection')} + /> +
+
+ + {/* 구분선 */} +
+ + {/* 점검표 섹션 */} +
+

점검표 옵션

+
+ handleToggle('showCompletedItems')} + /> + handleToggle('expandAllCategories')} + /> +
+
+ + {/* 구분선 */} +
+ + {/* 빠른 설정 */} +
+

빠른 설정

+
+ + +
+
+
+ + {/* 하단 안내 */} +
+

+ 설정은 자동으로 저장됩니다 +

+
+
+
+ ); +} + +interface SettingRowProps { + label: string; + description: string; + checked: boolean; + onChange: () => void; +} + +function SettingRow({ label, description, checked, onChange }: SettingRowProps) { + return ( +
+
+
+ {checked ? ( + + ) : ( + + )} + {label} +
+

{description}

+
+ +
+ ); +} + +// 설정 버튼 컴포넌트 (헤더에 배치용) +interface SettingsButtonProps { + onClick: () => void; +} + +export function SettingsButton({ onClick }: SettingsButtonProps) { + return ( + + ); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/quality/qms/components/Day1ChecklistPanel.tsx b/src/app/[locale]/(protected)/quality/qms/components/Day1ChecklistPanel.tsx new file mode 100644 index 00000000..4d554c82 --- /dev/null +++ b/src/app/[locale]/(protected)/quality/qms/components/Day1ChecklistPanel.tsx @@ -0,0 +1,270 @@ +'use client'; + +import React, { useState, useMemo } from 'react'; +import { ChevronDown, ChevronRight, Check, Search, X } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { ChecklistCategory, ChecklistSubItem } from '../types'; + +interface Day1ChecklistPanelProps { + categories: ChecklistCategory[]; + selectedSubItemId: string | null; + onSubItemSelect: (categoryId: string, subItemId: string) => void; + onSubItemToggle: (categoryId: string, subItemId: string, isCompleted: boolean) => void; +} + +export function Day1ChecklistPanel({ + categories, + selectedSubItemId, + onSubItemSelect, + onSubItemToggle, +}: Day1ChecklistPanelProps) { + const [expandedCategories, setExpandedCategories] = useState>( + new Set(categories.map(c => c.id)) // 기본적으로 모두 펼침 + ); + const [searchTerm, setSearchTerm] = useState(''); + + // 검색 필터링된 카테고리 + const filteredCategories = useMemo(() => { + if (!searchTerm.trim()) return categories; + + const term = searchTerm.toLowerCase(); + return categories.map(category => { + // 카테고리 제목 매칭 + const categoryMatches = category.title.toLowerCase().includes(term); + + // 하위 항목 필터링 + const filteredSubItems = category.subItems.filter(item => + item.name.toLowerCase().includes(term) + ); + + // 카테고리가 매칭되면 모든 하위 항목 포함, 아니면 필터링된 항목만 + if (categoryMatches) { + return category; + } else if (filteredSubItems.length > 0) { + return { ...category, subItems: filteredSubItems }; + } + return null; + }).filter((cat): cat is ChecklistCategory => cat !== null); + }, [categories, searchTerm]); + + // 검색 시 모든 카테고리 펼치기 + React.useEffect(() => { + if (searchTerm.trim()) { + setExpandedCategories(new Set(filteredCategories.map(c => c.id))); + } + }, [searchTerm, filteredCategories]); + + const toggleCategory = (categoryId: string) => { + setExpandedCategories(prev => { + const newSet = new Set(prev); + if (newSet.has(categoryId)) { + newSet.delete(categoryId); + } else { + newSet.add(categoryId); + } + return newSet; + }); + }; + + const getCategoryProgress = (category: ChecklistCategory) => { + // 원본 카테고리에서 진행률 계산 + const originalCategory = categories.find(c => c.id === category.id); + if (!originalCategory) return { completed: 0, total: 0 }; + const completed = originalCategory.subItems.filter(item => item.isCompleted).length; + return { completed, total: originalCategory.subItems.length }; + }; + + const clearSearch = () => { + setSearchTerm(''); + }; + + // 검색 결과 하이라이트 + const highlightText = (text: string, term: string) => { + if (!term.trim()) return text; + const regex = new RegExp(`(${term})`, 'gi'); + const parts = text.split(regex); + return parts.map((part, index) => + regex.test(part) ? ( + + {part} + + ) : ( + part + ) + ); + }; + + return ( +
+ {/* 헤더 + 검색 */} +
+

점검표 항목

+ {/* 검색 입력 */} +
+ + setSearchTerm(e.target.value)} + className="w-full pl-9 pr-8 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> + {searchTerm && ( + + )} +
+ {/* 검색 결과 카운트 */} + {searchTerm && ( +
+ {filteredCategories.length > 0 + ? `${filteredCategories.reduce((sum, cat) => sum + cat.subItems.length, 0)}개 항목 검색됨` + : '검색 결과가 없습니다' + } +
+ )} +
+ + {/* 카테고리 목록 */} +
+ {filteredCategories.length === 0 ? ( +
+ 검색 결과가 없습니다 +
+ ) : ( + filteredCategories.map((category, categoryIndex) => { + const isExpanded = expandedCategories.has(category.id); + const progress = getCategoryProgress(category); + const allCompleted = progress.completed === progress.total; + // 원본 인덱스 찾기 + const originalIndex = categories.findIndex(c => c.id === category.id); + + return ( +
+ {/* 카테고리 헤더 */} + + + {/* 하위 항목 */} + {isExpanded && ( +
+ {category.subItems.map((subItem, subIndex) => ( + onSubItemSelect(category.id, subItem.id)} + onToggle={(isCompleted) => onSubItemToggle(category.id, subItem.id, isCompleted)} + searchTerm={searchTerm} + highlightText={highlightText} + /> + ))} +
+ )} +
+ ); + }) + )} +
+
+ ); +} + +interface SubItemRowProps { + subItem: ChecklistSubItem; + index: number; + categoryId: string; + isSelected: boolean; + onSelect: () => void; + onToggle: (isCompleted: boolean) => void; + searchTerm: string; + highlightText: (text: string, term: string) => React.ReactNode; +} + +function SubItemRow({ + subItem, + index, + isSelected, + onSelect, + onToggle, + searchTerm, + highlightText, +}: SubItemRowProps) { + const handleCheckboxClick = (e: React.MouseEvent) => { + e.stopPropagation(); + onToggle(!subItem.isCompleted); + }; + + return ( +
+ {/* 체크박스 */} + + + {/* 항목 이름 */} + + {index + 1}. {highlightText(subItem.name, searchTerm)} + + + {/* 완료 표시 */} + {subItem.isCompleted && ( + 완료 + )} +
+ ); +} diff --git a/src/app/[locale]/(protected)/quality/qms/components/Day1DocumentSection.tsx b/src/app/[locale]/(protected)/quality/qms/components/Day1DocumentSection.tsx new file mode 100644 index 00000000..d35d922b --- /dev/null +++ b/src/app/[locale]/(protected)/quality/qms/components/Day1DocumentSection.tsx @@ -0,0 +1,157 @@ +'use client'; + +import React from 'react'; +import { FileText, Download, Eye, CheckCircle2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import type { Day1CheckItem, StandardDocument } from '../types'; + +interface Day1DocumentSectionProps { + checkItem: Day1CheckItem | null; + selectedDocumentId: string | null; + onDocumentSelect: (documentId: string) => void; + onConfirmComplete: () => void; + isCompleted: boolean; +} + +export function Day1DocumentSection({ + checkItem, + selectedDocumentId, + onDocumentSelect, + onConfirmComplete, + isCompleted, +}: Day1DocumentSectionProps) { + if (!checkItem) { + return ( +
+
+ +

점검표 항목을 선택하세요

+
+
+ ); + } + + return ( +
+ {/* 헤더 */} +
+

기준 문서화

+
+ + {/* 콘텐츠 */} +
+ {/* 항목 정보 */} +
+

{checkItem.title}

+

{checkItem.description}

+
+ + {/* 기준 문서 목록 */} +
+
관련 기준 문서
+
+ {checkItem.standardDocuments.map((doc) => ( + onDocumentSelect(doc.id)} + /> + ))} +
+
+ + {/* 확인 버튼 */} +
+ +
+
+
+ ); +} + +interface DocumentRowProps { + document: StandardDocument; + isSelected: boolean; + onSelect: () => void; +} + +function DocumentRow({ document, isSelected, onSelect }: DocumentRowProps) { + return ( +
+ {/* 아이콘 */} +
+ +
+ + {/* 문서 정보 */} +
+

{document.title}

+

+ {document.version !== '-' && {document.version}} + {document.date} +

+
+ + {/* 액션 버튼 */} +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/quality/qms/components/Day1DocumentViewer.tsx b/src/app/[locale]/(protected)/quality/qms/components/Day1DocumentViewer.tsx new file mode 100644 index 00000000..1a25de5a --- /dev/null +++ b/src/app/[locale]/(protected)/quality/qms/components/Day1DocumentViewer.tsx @@ -0,0 +1,186 @@ +'use client'; + +import React from 'react'; +import { FileText, Download, Printer, ZoomIn, ZoomOut, Maximize2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { StandardDocument } from '../types'; + +interface Day1DocumentViewerProps { + document: StandardDocument | null; +} + +export function Day1DocumentViewer({ document }: Day1DocumentViewerProps) { + if (!document) { + return ( +
+
+ +

문서를 선택하면 미리보기가 표시됩니다

+
+
+ ); + } + + const isPdf = document.fileName?.endsWith('.pdf'); + const isExcel = document.fileName?.endsWith('.xlsx') || document.fileName?.endsWith('.xls'); + + return ( +
+ {/* 헤더 */} +
+
+
+ +
+
+

{document.title}

+

+ {document.version !== '-' && {document.version}} + {document.date} +

+
+
+ + {/* 툴바 */} +
+ + + +
+ + +
+
+ + {/* 문서 미리보기 영역 */} +
+
+ {/* Mock 문서 내용 */} + +
+
+ + {/* 푸터 */} +
+ + 파일명: {document.fileName || '-'} + + + 1 / 1 페이지 + +
+
+ ); +} + +// Mock 문서 미리보기 내용 +function DocumentPreviewContent({ document }: { document: StandardDocument }) { + return ( +
+ {/* 문서 헤더 */} +
+

{document.title}

+
+ 문서번호: QM-{document.id} + 개정: {document.version} + 시행일: {document.date} +
+
+ + {/* Mock 문서 내용 */} +
+
+

1. 목적

+

+ 본 문서는 {document.title}의 업무 절차 및 기준을 규정함으로써 + 품질관리 업무의 효율성과 일관성을 확보하는 것을 목적으로 한다. +

+
+ +
+

2. 적용범위

+

+ 본 기준서는 당사에서 생산하는 모든 제품의 품질관리 업무에 적용한다. +

+
+ +
+

3. 용어의 정의

+
+

3.1 검사: 품질특성을 측정, 시험하여 규정된 기준과 비교하는 활동

+

3.2 적합: 규정된 요구사항이 충족된 상태

+

3.3 부적합: 규정된 요구사항이 충족되지 않은 상태

+
+
+ +
+

4. 업무 절차

+
+

4.1 담당자는 본 기준서에 따라 업무를 수행한다.

+

4.2 검사 결과는 해당 기록 양식에 기록하고 보관한다.

+

4.3 부적합 발생 시 부적합품 관리 절차에 따라 처리한다.

+
+
+ + {/* 문서 서명란 */} +
+
+
+
작성
+
+ (서명) +
+
+
+
검토
+
+ (서명) +
+
+
+
승인
+
+ (서명) +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/quality/qms/components/DayTabs.tsx b/src/app/[locale]/(protected)/quality/qms/components/DayTabs.tsx new file mode 100644 index 00000000..d7945c1d --- /dev/null +++ b/src/app/[locale]/(protected)/quality/qms/components/DayTabs.tsx @@ -0,0 +1,134 @@ +'use client'; + +import React from 'react'; +import { Calendar } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface DayTabsProps { + activeDay: 1 | 2; + onDayChange: (day: 1 | 2) => void; + day1Progress: { completed: number; total: number }; + day2Progress: { completed: number; total: number }; +} + +export function DayTabs({ activeDay, onDayChange, day1Progress, day2Progress }: DayTabsProps) { + // 전체 진행률 계산 + const totalCompleted = day1Progress.completed + day2Progress.completed; + const totalItems = day1Progress.total + day2Progress.total; + const overallPercentage = totalItems > 0 ? Math.round((totalCompleted / totalItems) * 100) : 0; + + const day1Percentage = day1Progress.total > 0 + ? Math.round((day1Progress.completed / day1Progress.total) * 100) + : 0; + const day2Percentage = day2Progress.total > 0 + ? Math.round((day2Progress.completed / day2Progress.total) * 100) + : 0; + + return ( +
+ {/* 탭 버튼 */} +
+ {/* 1일차 탭 */} + + + {/* 2일차 탭 */} + +
+ + {/* 전체 진행률 - 한줄 표시 */} +
+ 전체 심사 진행률 + + {/* 전체 프로그레스 바 */} +
+
+
+
+ {overallPercentage}% +
+ + {/* 구분선 */} +
+ + {/* 1일차 진행률 */} +
+ 1일차 +
+
+
+ + {day1Percentage}% + +
+ + {/* 2일차 진행률 */} +
+ 2일차 +
+
+
+ + {day2Percentage}% + +
+
+
+ ); +} diff --git a/src/app/[locale]/(protected)/quality/qms/components/Header.tsx b/src/app/[locale]/(protected)/quality/qms/components/Header.tsx index cd2dd5f5..59c441b0 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/Header.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/Header.tsx @@ -1,10 +1,21 @@ import React from 'react'; -export const Header = () => { +interface HeaderProps { + rightContent?: React.ReactNode; +} + +export const Header = ({ rightContent }: HeaderProps) => { return ( -
-

품질인정심사 시스템

-

SAM - Smart Automation Management

+
+
+

품질인정심사 시스템

+

SAM - Smart Automation Management

+
+ {rightContent && ( +
+ {rightContent} +
+ )}
); }; diff --git a/src/app/[locale]/(protected)/quality/qms/mockData.ts b/src/app/[locale]/(protected)/quality/qms/mockData.ts index 14664a8d..c0a2b985 100644 --- a/src/app/[locale]/(protected)/quality/qms/mockData.ts +++ b/src/app/[locale]/(protected)/quality/qms/mockData.ts @@ -1,4 +1,4 @@ -import { InspectionReport, RouteItem, Document } from './types'; +import { InspectionReport, RouteItem, Document, ChecklistCategory, StandardDocument, Day1CheckItem } from './types'; import type { WorkOrder } from '@/components/production/ProductionDashboard/types'; import type { ShipmentDetail } from '@/components/outbound/ShipmentManagement/types'; @@ -336,4 +336,255 @@ export const DEFAULT_DOCUMENTS: Document[] = [ { id: 'def-6', type: 'shipping', title: '출고증', count: 0, items: [] }, { id: 'def-7', type: 'product', title: '제품검사 성적서', count: 0, items: [] }, { id: 'def-8', type: 'quality', title: '품질관리서', count: 0, items: [] }, +]; + +// ===== 1일차: 기준/매뉴얼 심사 Mock 데이터 ===== + +// 1일차 점검표 카테고리 +export const MOCK_DAY1_CATEGORIES: ChecklistCategory[] = [ + { + id: 'cat-1', + title: '원재료 품질관리 기준', + subItems: [ + { id: 'cat-1-1', name: '수입검사 기준 확인', isCompleted: true }, + { id: 'cat-1-2', name: '불합격품 처리 기준 확인', isCompleted: true }, + { id: 'cat-1-3', name: '자재 보관 기준 확인', isCompleted: false }, + ], + }, + { + id: 'cat-2', + title: '제조공정 관리 기준', + subItems: [ + { id: 'cat-2-1', name: '작업표준서 확인', isCompleted: false }, + { id: 'cat-2-2', name: '공정검사 기준 확인', isCompleted: false }, + { id: 'cat-2-3', name: '부적합품 처리 기준 확인', isCompleted: false }, + ], + }, + { + id: 'cat-3', + title: '제품 품질관리 기준', + subItems: [ + { id: 'cat-3-1', name: '제품검사 기준 확인', isCompleted: false }, + { id: 'cat-3-2', name: '출하검사 기준 확인', isCompleted: false }, + { id: 'cat-3-3', name: '클레임 처리 기준 확인', isCompleted: false }, + ], + }, + { + id: 'cat-4', + title: '제조설비 관리', + subItems: [ + { id: 'cat-4-1', name: '설비관리 기준 확인', isCompleted: false }, + { id: 'cat-4-2', name: '설비점검 이력 확인', isCompleted: false }, + ], + }, + { + id: 'cat-5', + title: '검사설비 관리', + subItems: [ + { id: 'cat-5-1', name: '검사설비 관리 기준 확인', isCompleted: false }, + { id: 'cat-5-2', name: '교정 이력 확인', isCompleted: false }, + ], + }, + { + id: 'cat-6', + title: '문서 및 인증 관리', + subItems: [ + { id: 'cat-6-1', name: '문서관리 기준 확인', isCompleted: false }, + { id: 'cat-6-2', name: 'KS/인증 관리 현황 확인', isCompleted: false }, + ], + }, +]; + +// 1일차 기준 문서 목록 +export const MOCK_DAY1_STANDARD_DOCUMENTS: Record = { + 'cat-1-1': [ + { id: 'std-1-1-1', title: '수입검사기준서', version: 'REV12', date: '2024-10-20', fileName: '수입검사기준서_REV12.pdf' }, + { id: 'std-1-1-2', title: '검사지침서', version: 'REV11', date: '2024-08-15', fileName: '검사지침서_REV11.pdf' }, + ], + 'cat-1-2': [ + { id: 'std-1-2-1', title: '불합격품 처리절차서', version: 'REV08', date: '2024-06-10', fileName: '불합격품처리절차서_REV08.pdf' }, + ], + 'cat-1-3': [ + { id: 'std-1-3-1', title: '자재보관 관리기준서', version: 'REV05', date: '2024-03-20', fileName: '자재보관관리기준서_REV05.pdf' }, + { id: 'std-1-3-2', title: '창고관리 지침서', version: 'REV03', date: '2024-02-15', fileName: '창고관리지침서_REV03.pdf' }, + ], + 'cat-2-1': [ + { id: 'std-2-1-1', title: '스크린 작업표준서', version: 'REV15', date: '2024-09-01', fileName: '스크린작업표준서_REV15.pdf' }, + { id: 'std-2-1-2', title: '슬랫 작업표준서', version: 'REV10', date: '2024-07-20', fileName: '슬랫작업표준서_REV10.pdf' }, + { id: 'std-2-1-3', title: '절곡 작업표준서', version: 'REV12', date: '2024-08-10', fileName: '절곡작업표준서_REV12.pdf' }, + ], + 'cat-2-2': [ + { id: 'std-2-2-1', title: '공정검사 기준서', version: 'REV09', date: '2024-05-15', fileName: '공정검사기준서_REV09.pdf' }, + ], + 'cat-2-3': [ + { id: 'std-2-3-1', title: '부적합품 관리절차서', version: 'REV06', date: '2024-04-01', fileName: '부적합품관리절차서_REV06.pdf' }, + ], + 'cat-3-1': [ + { id: 'std-3-1-1', title: '제품검사 기준서', version: 'REV14', date: '2024-09-15', fileName: '제품검사기준서_REV14.pdf' }, + ], + 'cat-3-2': [ + { id: 'std-3-2-1', title: '출하검사 기준서', version: 'REV11', date: '2024-08-01', fileName: '출하검사기준서_REV11.pdf' }, + ], + 'cat-3-3': [ + { id: 'std-3-3-1', title: '클레임 처리절차서', version: 'REV07', date: '2024-05-20', fileName: '클레임처리절차서_REV07.pdf' }, + ], + 'cat-4-1': [ + { id: 'std-4-1-1', title: '설비관리 기준서', version: 'REV08', date: '2024-06-01', fileName: '설비관리기준서_REV08.pdf' }, + ], + 'cat-4-2': [ + { id: 'std-4-2-1', title: '설비점검 대장', version: 'REV04', date: '2024-10-01', fileName: '설비점검대장_REV04.xlsx' }, + ], + 'cat-5-1': [ + { id: 'std-5-1-1', title: '검사설비 관리기준서', version: 'REV06', date: '2024-04-15', fileName: '검사설비관리기준서_REV06.pdf' }, + ], + 'cat-5-2': [ + { id: 'std-5-2-1', title: '교정 관리대장', version: 'REV03', date: '2024-09-01', fileName: '교정관리대장_REV03.xlsx' }, + { id: 'std-5-2-2', title: '교정성적서', version: '-', date: '2024-09-10', fileName: '교정성적서_2024.pdf' }, + ], + 'cat-6-1': [ + { id: 'std-6-1-1', title: '문서관리 절차서', version: 'REV10', date: '2024-07-01', fileName: '문서관리절차서_REV10.pdf' }, + ], + 'cat-6-2': [ + { id: 'std-6-2-1', title: 'KS인증서', version: '-', date: '2024-01-15', fileName: 'KS인증서_2024.pdf' }, + { id: 'std-6-2-2', title: 'ISO 9001 인증서', version: '-', date: '2024-02-20', fileName: 'ISO9001인증서_2024.pdf' }, + ], +}; + +// 1일차 점검 항목 상세 정보 (버튼 클릭 시 표시) +export const MOCK_DAY1_CHECK_ITEMS: Day1CheckItem[] = [ + { + id: 'check-1-1', + categoryId: 'cat-1', + subItemId: 'cat-1-1', + title: '수입검사 기준 확인', + description: '원재료 수입검사 기준서 및 검사지침서의 내용을 확인하고, 실제 운영 현황과의 일치 여부를 점검합니다.', + buttonLabel: '기준/매뉴얼 확인', + standardDocuments: MOCK_DAY1_STANDARD_DOCUMENTS['cat-1-1'] || [], + }, + { + id: 'check-1-2', + categoryId: 'cat-1', + subItemId: 'cat-1-2', + title: '불합격품 처리 기준 확인', + description: '불합격품 처리 절차서의 내용을 확인하고, 처리 프로세스가 적절하게 수립되어 있는지 점검합니다.', + buttonLabel: '기준/매뉴얼 확인', + standardDocuments: MOCK_DAY1_STANDARD_DOCUMENTS['cat-1-2'] || [], + }, + { + id: 'check-1-3', + categoryId: 'cat-1', + subItemId: 'cat-1-3', + title: '자재 보관 기준 확인', + description: '자재 보관 관리기준서 및 창고관리 지침서를 확인하고, 보관 환경 및 관리 방법이 적절한지 점검합니다.', + buttonLabel: '기준/매뉴얼 확인', + standardDocuments: MOCK_DAY1_STANDARD_DOCUMENTS['cat-1-3'] || [], + }, + { + id: 'check-2-1', + categoryId: 'cat-2', + subItemId: 'cat-2-1', + title: '작업표준서 확인', + description: '각 공정별 작업표준서의 내용을 확인하고, 작업 방법 및 품질 기준이 명확하게 정의되어 있는지 점검합니다.', + buttonLabel: '기준/매뉴얼 확인', + standardDocuments: MOCK_DAY1_STANDARD_DOCUMENTS['cat-2-1'] || [], + }, + { + id: 'check-2-2', + categoryId: 'cat-2', + subItemId: 'cat-2-2', + title: '공정검사 기준 확인', + description: '공정검사 기준서를 확인하고, 검사 항목 및 판정 기준이 적절하게 수립되어 있는지 점검합니다.', + buttonLabel: '기준/매뉴얼 확인', + standardDocuments: MOCK_DAY1_STANDARD_DOCUMENTS['cat-2-2'] || [], + }, + { + id: 'check-2-3', + categoryId: 'cat-2', + subItemId: 'cat-2-3', + title: '부적합품 처리 기준 확인', + description: '부적합품 관리절차서를 확인하고, 부적합품 발생 시 처리 절차가 명확한지 점검합니다.', + buttonLabel: '기준/매뉴얼 확인', + standardDocuments: MOCK_DAY1_STANDARD_DOCUMENTS['cat-2-3'] || [], + }, + { + id: 'check-3-1', + categoryId: 'cat-3', + subItemId: 'cat-3-1', + title: '제품검사 기준 확인', + description: '제품검사 기준서를 확인하고, 검사 항목 및 합격 기준이 적절하게 수립되어 있는지 점검합니다.', + buttonLabel: '기준/매뉴얼 확인', + standardDocuments: MOCK_DAY1_STANDARD_DOCUMENTS['cat-3-1'] || [], + }, + { + id: 'check-3-2', + categoryId: 'cat-3', + subItemId: 'cat-3-2', + title: '출하검사 기준 확인', + description: '출하검사 기준서를 확인하고, 출하 전 최종 검사 항목 및 기준이 명확한지 점검합니다.', + buttonLabel: '기준/매뉴얼 확인', + standardDocuments: MOCK_DAY1_STANDARD_DOCUMENTS['cat-3-2'] || [], + }, + { + id: 'check-3-3', + categoryId: 'cat-3', + subItemId: 'cat-3-3', + title: '클레임 처리 기준 확인', + description: '클레임 처리절차서를 확인하고, 고객 불만 처리 프로세스가 적절하게 수립되어 있는지 점검합니다.', + buttonLabel: '기준/매뉴얼 확인', + standardDocuments: MOCK_DAY1_STANDARD_DOCUMENTS['cat-3-3'] || [], + }, + { + id: 'check-4-1', + categoryId: 'cat-4', + subItemId: 'cat-4-1', + title: '설비관리 기준 확인', + description: '설비관리 기준서를 확인하고, 설비 유지보수 및 관리 방법이 적절한지 점검합니다.', + buttonLabel: '기준/매뉴얼 확인', + standardDocuments: MOCK_DAY1_STANDARD_DOCUMENTS['cat-4-1'] || [], + }, + { + id: 'check-4-2', + categoryId: 'cat-4', + subItemId: 'cat-4-2', + title: '설비점검 이력 확인', + description: '설비점검 대장을 확인하고, 정기 점검이 계획대로 수행되고 있는지 점검합니다.', + buttonLabel: '기준/매뉴얼 확인', + standardDocuments: MOCK_DAY1_STANDARD_DOCUMENTS['cat-4-2'] || [], + }, + { + id: 'check-5-1', + categoryId: 'cat-5', + subItemId: 'cat-5-1', + title: '검사설비 관리 기준 확인', + description: '검사설비 관리기준서를 확인하고, 검사 장비의 관리 방법이 적절한지 점검합니다.', + buttonLabel: '기준/매뉴얼 확인', + standardDocuments: MOCK_DAY1_STANDARD_DOCUMENTS['cat-5-1'] || [], + }, + { + id: 'check-5-2', + categoryId: 'cat-5', + subItemId: 'cat-5-2', + title: '교정 이력 확인', + description: '교정 관리대장 및 교정성적서를 확인하고, 검사설비 교정이 적기에 수행되고 있는지 점검합니다.', + buttonLabel: '기준/매뉴얼 확인', + standardDocuments: MOCK_DAY1_STANDARD_DOCUMENTS['cat-5-2'] || [], + }, + { + id: 'check-6-1', + categoryId: 'cat-6', + subItemId: 'cat-6-1', + title: '문서관리 기준 확인', + description: '문서관리 절차서를 확인하고, 문서의 작성, 검토, 승인, 배포 프로세스가 적절한지 점검합니다.', + buttonLabel: '기준/매뉴얼 확인', + standardDocuments: MOCK_DAY1_STANDARD_DOCUMENTS['cat-6-1'] || [], + }, + { + id: 'check-6-2', + categoryId: 'cat-6', + subItemId: 'cat-6-2', + title: 'KS/인증 관리 현황 확인', + description: 'KS인증서 및 ISO 인증서를 확인하고, 인증 유효기간 및 관리 현황을 점검합니다.', + buttonLabel: '기준/매뉴얼 확인', + standardDocuments: MOCK_DAY1_STANDARD_DOCUMENTS['cat-6-2'] || [], + }, ]; \ No newline at end of file diff --git a/src/app/[locale]/(protected)/quality/qms/page.tsx b/src/app/[locale]/(protected)/quality/qms/page.tsx index c6581dd5..ad1bda51 100644 --- a/src/app/[locale]/(protected)/quality/qms/page.tsx +++ b/src/app/[locale]/(protected)/quality/qms/page.tsx @@ -1,22 +1,57 @@ "use client"; -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useCallback } from 'react'; import { Header } from './components/Header'; import { Filters } from './components/Filters'; import { ReportList } from './components/ReportList'; import { RouteList } from './components/RouteList'; import { DocumentList } from './components/DocumentList'; import { InspectionModal } from './components/InspectionModal'; -import { InspectionReport, RouteItem, Document, DocumentItem } from './types'; -import { MOCK_REPORTS, MOCK_ROUTES, MOCK_DOCUMENTS, DEFAULT_DOCUMENTS } from './mockData'; +import { DayTabs } from './components/DayTabs'; +import { Day1ChecklistPanel } from './components/Day1ChecklistPanel'; +import { Day1DocumentSection } from './components/Day1DocumentSection'; +import { Day1DocumentViewer } from './components/Day1DocumentViewer'; +import { AuditSettingsPanel, SettingsButton, type AuditDisplaySettings } from './components/AuditSettingsPanel'; +import { InspectionReport, RouteItem, Document, DocumentItem, ChecklistCategory } from './types'; +import { + MOCK_REPORTS, + MOCK_ROUTES, + MOCK_DOCUMENTS, + DEFAULT_DOCUMENTS, + MOCK_DAY1_CATEGORIES, + MOCK_DAY1_CHECK_ITEMS, + MOCK_DAY1_STANDARD_DOCUMENTS, +} from './mockData'; + +// 기본 설정값 +const DEFAULT_SETTINGS: AuditDisplaySettings = { + showProgressBar: true, + showDocumentViewer: true, + showDocumentSection: true, + showCompletedItems: true, + expandAllCategories: true, +}; export default function QualityInspectionPage() { - // 필터 상태 + // 탭 상태 + const [activeDay, setActiveDay] = useState<1 | 2>(1); + + // 설정 상태 + const [settingsOpen, setSettingsOpen] = useState(false); + const [displaySettings, setDisplaySettings] = useState(DEFAULT_SETTINGS); + + // 1일차 상태 + const [day1Categories, setDay1Categories] = useState(MOCK_DAY1_CATEGORIES); + const [selectedSubItemId, setSelectedSubItemId] = useState(null); + const [selectedCategoryId, setSelectedCategoryId] = useState(null); + const [selectedStandardDocId, setSelectedStandardDocId] = useState(null); + + // 2일차(로트추적) 필터 상태 const [selectedYear, setSelectedYear] = useState(2025); const [selectedQuarter, setSelectedQuarter] = useState('전체'); const [searchTerm, setSearchTerm] = useState(''); - // 선택 상태 + // 2일차 선택 상태 const [selectedReport, setSelectedReport] = useState(null); const [selectedRoute, setSelectedRoute] = useState(null); @@ -25,19 +60,98 @@ export default function QualityInspectionPage() { const [selectedDoc, setSelectedDoc] = useState(null); const [selectedDocItem, setSelectedDocItem] = useState(null); - // 필터링된 리포트 목록 + // ===== 1일차 진행률 계산 ===== + const day1Progress = useMemo(() => { + const total = day1Categories.reduce((sum, cat) => sum + cat.subItems.length, 0); + const completed = day1Categories.reduce( + (sum, cat) => sum + cat.subItems.filter(item => item.isCompleted).length, + 0 + ); + return { completed, total }; + }, [day1Categories]); + + // ===== 2일차 진행률 계산 (로트 추적 완료 기준) ===== + const day2Progress = useMemo(() => { + let completed = 0; + let total = 0; + Object.values(MOCK_ROUTES).forEach(routes => { + routes.forEach(route => { + total++; + const allPassed = route.subItems.length > 0 && + route.subItems.every(item => item.status === '합격'); + if (allPassed) completed++; + }); + }); + return { completed, total }; + }, []); + + // ===== 1일차 필터링된 카테고리 (완료 항목 숨기기 옵션) ===== + const filteredDay1Categories = useMemo(() => { + if (displaySettings.showCompletedItems) return day1Categories; + + return day1Categories.map(category => ({ + ...category, + subItems: category.subItems.filter(item => !item.isCompleted), + })).filter(category => category.subItems.length > 0); + }, [day1Categories, displaySettings.showCompletedItems]); + + // ===== 1일차 핸들러 ===== + const handleSubItemSelect = useCallback((categoryId: string, subItemId: string) => { + setSelectedCategoryId(categoryId); + setSelectedSubItemId(subItemId); + setSelectedStandardDocId(null); + }, []); + + const handleSubItemToggle = useCallback((categoryId: string, subItemId: string, isCompleted: boolean) => { + setDay1Categories(prev => prev.map(cat => { + if (cat.id !== categoryId) return cat; + return { + ...cat, + subItems: cat.subItems.map(item => { + if (item.id !== subItemId) return item; + return { ...item, isCompleted }; + }), + }; + })); + }, []); + + const handleConfirmComplete = useCallback(() => { + if (selectedCategoryId && selectedSubItemId) { + handleSubItemToggle(selectedCategoryId, selectedSubItemId, true); + } + }, [selectedCategoryId, selectedSubItemId, handleSubItemToggle]); + + // 선택된 1일차 점검 항목 + const selectedCheckItem = useMemo(() => { + if (!selectedSubItemId) return null; + return MOCK_DAY1_CHECK_ITEMS.find(item => item.subItemId === selectedSubItemId) || null; + }, [selectedSubItemId]); + + // 선택된 표준 문서 + const selectedStandardDoc = useMemo(() => { + if (!selectedStandardDocId || !selectedSubItemId) return null; + const docs = MOCK_DAY1_STANDARD_DOCUMENTS[selectedSubItemId] || []; + return docs.find(doc => doc.id === selectedStandardDocId) || null; + }, [selectedStandardDocId, selectedSubItemId]); + + // 선택된 항목의 완료 여부 + const isSelectedItemCompleted = useMemo(() => { + if (!selectedSubItemId) return false; + for (const cat of day1Categories) { + const item = cat.subItems.find(item => item.id === selectedSubItemId); + if (item) return item.isCompleted; + } + return false; + }, [day1Categories, selectedSubItemId]); + + // ===== 2일차(로트추적) 로직 ===== const filteredReports = useMemo(() => { return MOCK_REPORTS.filter((report) => { - // 년도 필터 if (report.year !== selectedYear) return false; - - // 분기 필터 if (selectedQuarter !== '전체') { const quarterNum = parseInt(selectedQuarter.replace('분기', '')); if (report.quarterNum !== quarterNum) return false; } - - // 검색어 필터 if (searchTerm) { const term = searchTerm.toLowerCase(); const matchesCode = report.code.toLowerCase().includes(term); @@ -45,42 +159,35 @@ export default function QualityInspectionPage() { const matchesItem = report.item.toLowerCase().includes(term); if (!matchesCode && !matchesSite && !matchesItem) return false; } - return true; }); }, [selectedYear, selectedQuarter, searchTerm]); - // 선택된 리포트의 루트 목록 const currentRoutes = useMemo(() => { if (!selectedReport) return []; return MOCK_ROUTES[selectedReport.id] || []; }, [selectedReport]); - // 선택된 루트의 문서 목록 const currentDocuments = useMemo(() => { if (!selectedRoute) return DEFAULT_DOCUMENTS; return MOCK_DOCUMENTS[selectedRoute.id] || DEFAULT_DOCUMENTS; }, [selectedRoute]); - // 리포트 선택 핸들러 const handleReportSelect = (report: InspectionReport) => { setSelectedReport(report); - setSelectedRoute(null); // 루트 선택 초기화 + setSelectedRoute(null); }; - // 루트 선택 핸들러 const handleRouteSelect = (route: RouteItem) => { setSelectedRoute(route); }; - // 문서 보기 핸들러 const handleViewDocument = (doc: Document, item?: DocumentItem) => { setSelectedDoc(doc); setSelectedDocItem(item || null); setModalOpen(true); }; - // 필터 핸들러 const handleYearChange = (year: number) => { setSelectedYear(year); setSelectedReport(null); @@ -99,46 +206,138 @@ export default function QualityInspectionPage() { return (
-
- - setSettingsOpen(true)} />} /> -
- {/* Left Panel: Report List */} -
- + {/* 1일차/2일차 탭 + 진행률 */} + {displaySettings.showProgressBar ? ( + + ) : ( + // 진행률 숨김 시 탭만 표시 +
+ +
+ )} - {/* Middle Panel: Route List */} -
- -
+ {activeDay === 1 ? ( + // ===== 1일차: 기준/매뉴얼 심사 ===== +
+ {/* 좌측: 점검표 항목 */} +
+ +
- {/* Right Panel: Documents */} -
- + {/* 중앙: 기준 문서화 */} + {displaySettings.showDocumentSection && ( +
+ +
+ )} + + {/* 우측: 문서 뷰어 */} + {displaySettings.showDocumentViewer && ( +
+ +
+ )}
-
+ ) : ( + // ===== 2일차: 로트추적 심사 ===== + <> + + +
+
+ +
+ +
+ +
+ +
+ +
+
+ + )} + + {/* 설정 패널 */} + setSettingsOpen(false)} + settings={displaySettings} + onSettingsChange={setDisplaySettings} + />
); -} \ No newline at end of file +} diff --git a/src/app/[locale]/(protected)/quality/qms/types.ts b/src/app/[locale]/(protected)/quality/qms/types.ts index f0b07a48..a8fabc3f 100644 --- a/src/app/[locale]/(protected)/quality/qms/types.ts +++ b/src/app/[locale]/(protected)/quality/qms/types.ts @@ -43,3 +43,52 @@ export interface DocumentItem { // 중간검사 성적서 서브타입 (report 타입일 때만 사용) subType?: 'screen' | 'bending' | 'slat' | 'jointbar'; } + +// ===== 1일차: 기준/매뉴얼 심사 타입 ===== + +// 점검표 하위 항목 +export interface ChecklistSubItem { + id: string; + name: string; + isCompleted: boolean; +} + +// 점검표 카테고리 +export interface ChecklistCategory { + id: string; + title: string; + subItems: ChecklistSubItem[]; +} + +// 기준 문서 +export interface StandardDocument { + id: string; + title: string; + version: string; + date: string; + fileName?: string; + fileUrl?: string; +} + +// 1일차 점검 항목 상세 정보 +export interface Day1CheckItem { + id: string; + categoryId: string; + subItemId: string; + title: string; + description: string; + buttonLabel: string; + standardDocuments: StandardDocument[]; +} + +// 1일차 진행 상태 +export interface Day1Progress { + completed: number; + total: number; +} + +// 2일차 진행 상태 (기존 로트추적) +export interface Day2Progress { + completed: number; + total: number; +}