feat(WEB): QMS 품질관리 Day1 심사 기능 구현
- Day1 체크리스트 패널 및 문서 뷰어 컴포넌트 추가 - 심사 진행 상태바 및 설정 패널 구현 - Day 탭 네비게이션 컴포넌트 추가 - 목업 데이터 확장 및 타입 정의 보강 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<div className="bg-white rounded-lg border border-gray-200 px-4 py-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-sm font-medium text-gray-700">전체 심사 진행률</h4>
|
||||
<span className="text-sm font-bold text-blue-600">{overallPercentage}%</span>
|
||||
</div>
|
||||
|
||||
{/* 전체 진행률 바 */}
|
||||
<div className="h-3 bg-gray-200 rounded-full overflow-hidden mb-4">
|
||||
<div
|
||||
className="h-full bg-blue-600 rounded-full transition-all duration-500"
|
||||
style={{ width: `${overallPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 1일차/2일차 상세 진행률 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* 1일차 */}
|
||||
<div className={cn(
|
||||
'p-3 rounded-lg border transition-colors',
|
||||
activeDay === 1 ? 'bg-blue-50 border-blue-200' : 'bg-gray-50 border-gray-200'
|
||||
)}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs font-medium text-gray-600">1일차: 기준/매뉴얼</span>
|
||||
<span className={cn(
|
||||
'text-xs font-bold',
|
||||
day1Percentage === 100 ? 'text-green-600' : 'text-gray-600'
|
||||
)}>
|
||||
{day1Progress.completed}/{day1Progress.total}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full transition-all duration-500',
|
||||
day1Percentage === 100 ? 'bg-green-500' : 'bg-blue-500'
|
||||
)}
|
||||
style={{ width: `${day1Percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2일차 */}
|
||||
<div className={cn(
|
||||
'p-3 rounded-lg border transition-colors',
|
||||
activeDay === 2 ? 'bg-blue-50 border-blue-200' : 'bg-gray-50 border-gray-200'
|
||||
)}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs font-medium text-gray-600">2일차: 로트추적</span>
|
||||
<span className={cn(
|
||||
'text-xs font-bold',
|
||||
day2Percentage === 100 ? 'text-green-600' : 'text-gray-600'
|
||||
)}>
|
||||
{day2Progress.completed}/{day2Progress.total}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full transition-all duration-500',
|
||||
day2Percentage === 100 ? 'bg-green-500' : 'bg-blue-500'
|
||||
)}
|
||||
style={{ width: `${day2Percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="fixed inset-0 z-50 flex items-start justify-end">
|
||||
{/* 배경 오버레이 */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/30"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* 설정 패널 */}
|
||||
<div className="relative w-80 h-full bg-white shadow-xl flex flex-col animate-slide-in-right">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 bg-gray-50">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="h-5 w-5 text-gray-600" />
|
||||
<h3 className="font-semibold text-gray-900">화면 설정</h3>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="p-1.5 hover:bg-gray-200 rounded-lg transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 설정 항목 */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{/* 레이아웃 섹션 */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">레이아웃</h4>
|
||||
<div className="space-y-3">
|
||||
<SettingRow
|
||||
label="진행률 표시"
|
||||
description="상단 전체 심사 진행률 바를 표시합니다"
|
||||
checked={settings.showProgressBar}
|
||||
onChange={() => handleToggle('showProgressBar')}
|
||||
/>
|
||||
<SettingRow
|
||||
label="문서 뷰어"
|
||||
description="우측 문서 미리보기 패널을 표시합니다"
|
||||
checked={settings.showDocumentViewer}
|
||||
onChange={() => handleToggle('showDocumentViewer')}
|
||||
/>
|
||||
<SettingRow
|
||||
label="기준 문서화 섹션"
|
||||
description="중앙 기준 문서 목록 패널을 표시합니다"
|
||||
checked={settings.showDocumentSection}
|
||||
onChange={() => handleToggle('showDocumentSection')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 구분선 */}
|
||||
<div className="border-t border-gray-200" />
|
||||
|
||||
{/* 점검표 섹션 */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">점검표 옵션</h4>
|
||||
<div className="space-y-3">
|
||||
<SettingRow
|
||||
label="완료된 항목 표시"
|
||||
description="완료된 점검 항목을 목록에 표시합니다"
|
||||
checked={settings.showCompletedItems}
|
||||
onChange={() => handleToggle('showCompletedItems')}
|
||||
/>
|
||||
<SettingRow
|
||||
label="모든 카테고리 펼치기"
|
||||
description="점검표 카테고리를 기본으로 펼쳐서 표시합니다"
|
||||
checked={settings.expandAllCategories}
|
||||
onChange={() => handleToggle('expandAllCategories')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 구분선 */}
|
||||
<div className="border-t border-gray-200" />
|
||||
|
||||
{/* 빠른 설정 */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">빠른 설정</h4>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSettingsChange({
|
||||
showProgressBar: true,
|
||||
showDocumentViewer: true,
|
||||
showDocumentSection: true,
|
||||
showCompletedItems: true,
|
||||
expandAllCategories: true,
|
||||
})}
|
||||
className="px-3 py-2 text-sm bg-blue-50 text-blue-700 rounded-lg hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
모두 표시
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSettingsChange({
|
||||
showProgressBar: false,
|
||||
showDocumentViewer: false,
|
||||
showDocumentSection: true,
|
||||
showCompletedItems: true,
|
||||
expandAllCategories: false,
|
||||
})}
|
||||
className="px-3 py-2 text-sm bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
간소화
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 하단 안내 */}
|
||||
<div className="px-4 py-3 border-t border-gray-200 bg-gray-50">
|
||||
<p className="text-xs text-gray-500">
|
||||
설정은 자동으로 저장됩니다
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SettingRowProps {
|
||||
label: string;
|
||||
description: string;
|
||||
checked: boolean;
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
function SettingRow({ label, description, checked, onChange }: SettingRowProps) {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-3 p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
{checked ? (
|
||||
<Eye className="h-4 w-4 text-blue-500" />
|
||||
) : (
|
||||
<EyeOff className="h-4 w-4 text-gray-400" />
|
||||
)}
|
||||
<span className="text-sm font-medium text-gray-800">{label}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-0.5 ml-6">{description}</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={checked}
|
||||
onCheckedChange={onChange}
|
||||
className="data-[state=checked]:bg-blue-500"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 설정 버튼 컴포넌트 (헤더에 배치용)
|
||||
interface SettingsButtonProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function SettingsButton({ onClick }: SettingsButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="flex items-center gap-2 px-4 py-2.5 text-sm text-white bg-white/10 border border-white/20 rounded-lg hover:bg-white/20 transition-colors"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
<span>화면 설정</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -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<Set<string>>(
|
||||
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) ? (
|
||||
<mark key={index} className="bg-yellow-200 text-yellow-900 px-0.5 rounded">
|
||||
{part}
|
||||
</mark>
|
||||
) : (
|
||||
part
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden h-full flex flex-col">
|
||||
{/* 헤더 + 검색 */}
|
||||
<div className="bg-gray-100 px-4 py-3 border-b border-gray-200">
|
||||
<h3 className="font-semibold text-gray-900 mb-2">점검표 항목</h3>
|
||||
{/* 검색 입력 */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="항목 검색..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => 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 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearSearch}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-gray-200 rounded-full transition-colors"
|
||||
>
|
||||
<X className="h-4 w-4 text-gray-400" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{/* 검색 결과 카운트 */}
|
||||
{searchTerm && (
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
{filteredCategories.length > 0
|
||||
? `${filteredCategories.reduce((sum, cat) => sum + cat.subItems.length, 0)}개 항목 검색됨`
|
||||
: '검색 결과가 없습니다'
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 카테고리 목록 */}
|
||||
<div className="flex-1 overflow-y-auto divide-y divide-gray-200">
|
||||
{filteredCategories.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-32 text-gray-400 text-sm">
|
||||
검색 결과가 없습니다
|
||||
</div>
|
||||
) : (
|
||||
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 (
|
||||
<div key={category.id}>
|
||||
{/* 카테고리 헤더 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCategory(category.id)}
|
||||
className={cn(
|
||||
'w-full flex items-center justify-between px-4 py-3 text-left transition-colors',
|
||||
'hover:bg-gray-50',
|
||||
allCompleted && 'bg-green-50'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
<span className="text-sm font-medium text-gray-800">
|
||||
{originalIndex + 1}. {highlightText(category.title, searchTerm)}
|
||||
</span>
|
||||
</div>
|
||||
<span className={cn(
|
||||
'text-xs px-2 py-0.5 rounded-full',
|
||||
allCompleted
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-gray-200 text-gray-600'
|
||||
)}>
|
||||
{progress.completed}/{progress.total}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* 하위 항목 */}
|
||||
{isExpanded && (
|
||||
<div className="bg-gray-50 border-t border-gray-100">
|
||||
{category.subItems.map((subItem, subIndex) => (
|
||||
<SubItemRow
|
||||
key={subItem.id}
|
||||
subItem={subItem}
|
||||
index={subIndex}
|
||||
categoryId={category.id}
|
||||
isSelected={selectedSubItemId === subItem.id}
|
||||
onSelect={() => onSubItemSelect(category.id, subItem.id)}
|
||||
onToggle={(isCompleted) => onSubItemToggle(category.id, subItem.id, isCompleted)}
|
||||
searchTerm={searchTerm}
|
||||
highlightText={highlightText}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-4 py-2.5 cursor-pointer transition-colors',
|
||||
isSelected
|
||||
? 'bg-blue-100 border-l-4 border-blue-500'
|
||||
: 'hover:bg-gray-100 border-l-4 border-transparent',
|
||||
subItem.isCompleted && !isSelected && 'bg-green-50/50'
|
||||
)}
|
||||
onClick={onSelect}
|
||||
>
|
||||
{/* 체크박스 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCheckboxClick}
|
||||
className={cn(
|
||||
'flex-shrink-0 w-5 h-5 rounded border-2 flex items-center justify-center transition-colors',
|
||||
subItem.isCompleted
|
||||
? 'bg-green-500 border-green-500 text-white'
|
||||
: 'bg-white border-gray-300 hover:border-blue-400'
|
||||
)}
|
||||
>
|
||||
{subItem.isCompleted && <Check className="h-3 w-3" />}
|
||||
</button>
|
||||
|
||||
{/* 항목 이름 */}
|
||||
<span className={cn(
|
||||
'text-sm flex-1',
|
||||
subItem.isCompleted ? 'text-gray-500' : 'text-gray-700'
|
||||
)}>
|
||||
{index + 1}. {highlightText(subItem.name, searchTerm)}
|
||||
</span>
|
||||
|
||||
{/* 완료 표시 */}
|
||||
{subItem.isCompleted && (
|
||||
<span className="text-xs text-green-600 font-medium">완료</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="bg-white rounded-lg border border-gray-200 h-full flex items-center justify-center">
|
||||
<div className="text-center text-gray-400 py-12">
|
||||
<FileText className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-sm">점검표 항목을 선택하세요</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden h-full flex flex-col">
|
||||
{/* 헤더 */}
|
||||
<div className="bg-gray-100 px-4 py-3 border-b border-gray-200">
|
||||
<h3 className="font-semibold text-gray-900">기준 문서화</h3>
|
||||
</div>
|
||||
|
||||
{/* 콘텐츠 */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{/* 항목 정보 */}
|
||||
<div className="bg-blue-50 rounded-lg p-4">
|
||||
<h4 className="font-medium text-blue-900 mb-2">{checkItem.title}</h4>
|
||||
<p className="text-sm text-blue-700">{checkItem.description}</p>
|
||||
</div>
|
||||
|
||||
{/* 기준 문서 목록 */}
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-gray-700 mb-2">관련 기준 문서</h5>
|
||||
<div className="space-y-2">
|
||||
{checkItem.standardDocuments.map((doc) => (
|
||||
<DocumentRow
|
||||
key={doc.id}
|
||||
document={doc}
|
||||
isSelected={selectedDocumentId === doc.id}
|
||||
onSelect={() => onDocumentSelect(doc.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 확인 버튼 */}
|
||||
<div className="pt-4 border-t border-gray-200">
|
||||
<Button
|
||||
onClick={onConfirmComplete}
|
||||
disabled={isCompleted}
|
||||
className={cn(
|
||||
'w-full',
|
||||
isCompleted
|
||||
? 'bg-green-500 hover:bg-green-500 cursor-default'
|
||||
: 'bg-blue-600 hover:bg-blue-700'
|
||||
)}
|
||||
>
|
||||
{isCompleted ? (
|
||||
<>
|
||||
<CheckCircle2 className="h-4 w-4 mr-2" />
|
||||
확인 완료
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{checkItem.buttonLabel}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface DocumentRowProps {
|
||||
document: StandardDocument;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
}
|
||||
|
||||
function DocumentRow({ document, isSelected, onSelect }: DocumentRowProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors',
|
||||
isSelected
|
||||
? 'bg-blue-50 border-blue-300'
|
||||
: 'bg-gray-50 border-gray-200 hover:bg-gray-100'
|
||||
)}
|
||||
onClick={onSelect}
|
||||
>
|
||||
{/* 아이콘 */}
|
||||
<div className={cn(
|
||||
'flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center',
|
||||
document.fileName?.endsWith('.pdf')
|
||||
? 'bg-red-100 text-red-600'
|
||||
: 'bg-green-100 text-green-600'
|
||||
)}>
|
||||
<FileText className="h-5 w-5" />
|
||||
</div>
|
||||
|
||||
{/* 문서 정보 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">{document.title}</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{document.version !== '-' && <span className="mr-2">{document.version}</span>}
|
||||
<span>{document.date}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 액션 버튼 */}
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 rounded hover:bg-gray-200 text-gray-500 hover:text-blue-600 transition-colors"
|
||||
title="미리보기"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelect();
|
||||
}}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 rounded hover:bg-gray-200 text-gray-500 hover:text-blue-600 transition-colors"
|
||||
title="다운로드"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// TODO: 다운로드 기능
|
||||
}}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="bg-white rounded-lg border border-gray-200 h-full flex items-center justify-center">
|
||||
<div className="text-center text-gray-400 py-12">
|
||||
<FileText className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-sm">문서를 선택하면 미리보기가 표시됩니다</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isPdf = document.fileName?.endsWith('.pdf');
|
||||
const isExcel = document.fileName?.endsWith('.xlsx') || document.fileName?.endsWith('.xls');
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden h-full flex flex-col">
|
||||
{/* 헤더 */}
|
||||
<div className="bg-gray-100 px-4 py-3 border-b border-gray-200 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={cn(
|
||||
'w-8 h-8 rounded flex items-center justify-center',
|
||||
isPdf ? 'bg-red-100 text-red-600' :
|
||||
isExcel ? 'bg-green-100 text-green-600' :
|
||||
'bg-gray-200 text-gray-600'
|
||||
)}>
|
||||
<FileText className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900 text-sm">{document.title}</h3>
|
||||
<p className="text-xs text-gray-500">
|
||||
{document.version !== '-' && <span className="mr-2">{document.version}</span>}
|
||||
{document.date}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 툴바 */}
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 rounded hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"
|
||||
title="축소"
|
||||
>
|
||||
<ZoomOut className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 rounded hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"
|
||||
title="확대"
|
||||
>
|
||||
<ZoomIn className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 rounded hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"
|
||||
title="전체화면"
|
||||
>
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="w-px h-6 bg-gray-300 mx-1" />
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 rounded hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"
|
||||
title="인쇄"
|
||||
>
|
||||
<Printer className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 rounded hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"
|
||||
title="다운로드"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 문서 미리보기 영역 */}
|
||||
<div className="flex-1 bg-gray-200 p-4 overflow-auto">
|
||||
<div className="bg-white rounded shadow-lg max-w-3xl mx-auto min-h-[600px]">
|
||||
{/* Mock 문서 내용 */}
|
||||
<DocumentPreviewContent document={document} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="bg-gray-100 px-4 py-2 border-t border-gray-200 flex items-center justify-between">
|
||||
<span className="text-xs text-gray-500">
|
||||
파일명: {document.fileName || '-'}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
1 / 1 페이지
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Mock 문서 미리보기 내용
|
||||
function DocumentPreviewContent({ document }: { document: StandardDocument }) {
|
||||
return (
|
||||
<div className="p-8">
|
||||
{/* 문서 헤더 */}
|
||||
<div className="text-center border-b-2 border-gray-800 pb-4 mb-6">
|
||||
<h1 className="text-xl font-bold text-gray-900">{document.title}</h1>
|
||||
<div className="flex justify-center gap-6 mt-2 text-sm text-gray-600">
|
||||
<span>문서번호: QM-{document.id}</span>
|
||||
<span>개정: {document.version}</span>
|
||||
<span>시행일: {document.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mock 문서 내용 */}
|
||||
<div className="space-y-4 text-sm text-gray-700">
|
||||
<section>
|
||||
<h2 className="font-bold text-gray-900 mb-2">1. 목적</h2>
|
||||
<p className="pl-4">
|
||||
본 문서는 {document.title}의 업무 절차 및 기준을 규정함으로써
|
||||
품질관리 업무의 효율성과 일관성을 확보하는 것을 목적으로 한다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-bold text-gray-900 mb-2">2. 적용범위</h2>
|
||||
<p className="pl-4">
|
||||
본 기준서는 당사에서 생산하는 모든 제품의 품질관리 업무에 적용한다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-bold text-gray-900 mb-2">3. 용어의 정의</h2>
|
||||
<div className="pl-4 space-y-1">
|
||||
<p>3.1 검사: 품질특성을 측정, 시험하여 규정된 기준과 비교하는 활동</p>
|
||||
<p>3.2 적합: 규정된 요구사항이 충족된 상태</p>
|
||||
<p>3.3 부적합: 규정된 요구사항이 충족되지 않은 상태</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-bold text-gray-900 mb-2">4. 업무 절차</h2>
|
||||
<div className="pl-4 space-y-2">
|
||||
<p>4.1 담당자는 본 기준서에 따라 업무를 수행한다.</p>
|
||||
<p>4.2 검사 결과는 해당 기록 양식에 기록하고 보관한다.</p>
|
||||
<p>4.3 부적합 발생 시 부적합품 관리 절차에 따라 처리한다.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 문서 서명란 */}
|
||||
<div className="mt-12 pt-6 border-t border-gray-300">
|
||||
<div className="grid grid-cols-3 gap-4 text-center text-sm">
|
||||
<div className="border border-gray-300 p-3">
|
||||
<div className="text-gray-500 mb-2">작성</div>
|
||||
<div className="h-12 flex items-center justify-center text-gray-400">
|
||||
(서명)
|
||||
</div>
|
||||
</div>
|
||||
<div className="border border-gray-300 p-3">
|
||||
<div className="text-gray-500 mb-2">검토</div>
|
||||
<div className="h-12 flex items-center justify-center text-gray-400">
|
||||
(서명)
|
||||
</div>
|
||||
</div>
|
||||
<div className="border border-gray-300 p-3">
|
||||
<div className="text-gray-500 mb-2">승인</div>
|
||||
<div className="h-12 flex items-center justify-center text-gray-400">
|
||||
(서명)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
134
src/app/[locale]/(protected)/quality/qms/components/DayTabs.tsx
Normal file
134
src/app/[locale]/(protected)/quality/qms/components/DayTabs.tsx
Normal file
@@ -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 (
|
||||
<div className="mb-4 space-y-3">
|
||||
{/* 탭 버튼 */}
|
||||
<div className="flex gap-3">
|
||||
{/* 1일차 탭 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onDayChange(1)}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-2 py-3 px-4 rounded-lg border-2 transition-all',
|
||||
activeDay === 1
|
||||
? 'bg-blue-600 border-blue-600 text-white shadow-lg'
|
||||
: 'bg-white border-gray-200 text-gray-700 hover:border-blue-300 hover:bg-blue-50'
|
||||
)}
|
||||
>
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span className="font-medium">1일차: 기준/매뉴얼 심사</span>
|
||||
<span className={cn(
|
||||
'text-xs px-2 py-0.5 rounded-full ml-2',
|
||||
activeDay === 1 ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-600'
|
||||
)}>
|
||||
{day1Progress.completed}/{day1Progress.total}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* 2일차 탭 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onDayChange(2)}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-2 py-3 px-4 rounded-lg border-2 transition-all',
|
||||
activeDay === 2
|
||||
? 'bg-blue-600 border-blue-600 text-white shadow-lg'
|
||||
: 'bg-white border-gray-200 text-gray-700 hover:border-blue-300 hover:bg-blue-50'
|
||||
)}
|
||||
>
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span className="font-medium">2일차: 로트추적 심사</span>
|
||||
<span className={cn(
|
||||
'text-xs px-2 py-0.5 rounded-full ml-2',
|
||||
activeDay === 2 ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-600'
|
||||
)}>
|
||||
{day2Progress.completed}/{day2Progress.total}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 전체 진행률 - 한줄 표시 */}
|
||||
<div className="bg-white rounded-lg border border-gray-200 px-4 py-2.5 flex items-center gap-4">
|
||||
<span className="text-sm font-medium text-gray-700 whitespace-nowrap">전체 심사 진행률</span>
|
||||
|
||||
{/* 전체 프로그레스 바 */}
|
||||
<div className="flex-1 flex items-center gap-3">
|
||||
<div className="flex-1 h-2.5 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-600 rounded-full transition-all duration-500"
|
||||
style={{ width: `${overallPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-blue-600 whitespace-nowrap">{overallPercentage}%</span>
|
||||
</div>
|
||||
|
||||
{/* 구분선 */}
|
||||
<div className="w-px h-5 bg-gray-300" />
|
||||
|
||||
{/* 1일차 진행률 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 whitespace-nowrap">1일차</span>
|
||||
<div className="w-20 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full transition-all duration-500',
|
||||
day1Percentage === 100 ? 'bg-green-500' : 'bg-blue-500'
|
||||
)}
|
||||
style={{ width: `${day1Percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className={cn(
|
||||
'text-xs font-medium whitespace-nowrap',
|
||||
day1Percentage === 100 ? 'text-green-600' : 'text-gray-600'
|
||||
)}>
|
||||
{day1Percentage}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 2일차 진행률 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 whitespace-nowrap">2일차</span>
|
||||
<div className="w-20 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full transition-all duration-500',
|
||||
day2Percentage === 100 ? 'bg-green-500' : 'bg-blue-500'
|
||||
)}
|
||||
style={{ width: `${day2Percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className={cn(
|
||||
'text-xs font-medium whitespace-nowrap',
|
||||
day2Percentage === 100 ? 'text-green-600' : 'text-gray-600'
|
||||
)}>
|
||||
{day2Percentage}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
export const Header = () => {
|
||||
interface HeaderProps {
|
||||
rightContent?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Header = ({ rightContent }: HeaderProps) => {
|
||||
return (
|
||||
<div className="w-full bg-[#1e3a8a] text-white p-6 rounded-lg mb-4 shadow-md flex flex-col justify-center h-24">
|
||||
<h1 className="text-2xl font-bold mb-1">품질인정심사 시스템</h1>
|
||||
<p className="text-sm opacity-80 text-blue-100">SAM - Smart Automation Management</p>
|
||||
<div className="w-full bg-[#1e3a8a] text-white p-6 rounded-lg mb-4 shadow-md flex items-center justify-between h-24">
|
||||
<div className="flex flex-col justify-center">
|
||||
<h1 className="text-2xl font-bold mb-1">품질인정심사 시스템</h1>
|
||||
<p className="text-sm opacity-80 text-blue-100">SAM - Smart Automation Management</p>
|
||||
</div>
|
||||
{rightContent && (
|
||||
<div className="flex items-center">
|
||||
{rightContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<string, StandardDocument[]> = {
|
||||
'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'] || [],
|
||||
},
|
||||
];
|
||||
@@ -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<AuditDisplaySettings>(DEFAULT_SETTINGS);
|
||||
|
||||
// 1일차 상태
|
||||
const [day1Categories, setDay1Categories] = useState<ChecklistCategory[]>(MOCK_DAY1_CATEGORIES);
|
||||
const [selectedSubItemId, setSelectedSubItemId] = useState<string | null>(null);
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null);
|
||||
const [selectedStandardDocId, setSelectedStandardDocId] = useState<string | null>(null);
|
||||
|
||||
// 2일차(로트추적) 필터 상태
|
||||
const [selectedYear, setSelectedYear] = useState(2025);
|
||||
const [selectedQuarter, setSelectedQuarter] = useState<string>('전체');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
// 선택 상태
|
||||
// 2일차 선택 상태
|
||||
const [selectedReport, setSelectedReport] = useState<InspectionReport | null>(null);
|
||||
const [selectedRoute, setSelectedRoute] = useState<RouteItem | null>(null);
|
||||
|
||||
@@ -25,19 +60,98 @@ export default function QualityInspectionPage() {
|
||||
const [selectedDoc, setSelectedDoc] = useState<Document | null>(null);
|
||||
const [selectedDocItem, setSelectedDocItem] = useState<DocumentItem | null>(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 (
|
||||
<div className="w-full min-h-[calc(100vh-64px)] lg:h-[calc(100vh-64px)] p-6 bg-slate-100 flex flex-col lg:overflow-hidden">
|
||||
<Header />
|
||||
|
||||
<Filters
|
||||
selectedYear={selectedYear}
|
||||
selectedQuarter={selectedQuarter}
|
||||
searchTerm={searchTerm}
|
||||
onYearChange={handleYearChange}
|
||||
onQuarterChange={handleQuarterChange}
|
||||
onSearchChange={handleSearchChange}
|
||||
{/* 헤더 (설정 버튼 포함) */}
|
||||
<Header
|
||||
rightContent={<SettingsButton onClick={() => setSettingsOpen(true)} />}
|
||||
/>
|
||||
|
||||
<div className="flex-1 grid grid-cols-12 gap-6 lg:min-h-0">
|
||||
{/* Left Panel: Report List */}
|
||||
<div className="col-span-12 lg:col-span-3 min-h-[300px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
|
||||
<ReportList
|
||||
reports={filteredReports}
|
||||
selectedId={selectedReport?.id || null}
|
||||
onSelect={handleReportSelect}
|
||||
/>
|
||||
{/* 1일차/2일차 탭 + 진행률 */}
|
||||
{displaySettings.showProgressBar ? (
|
||||
<DayTabs
|
||||
activeDay={activeDay}
|
||||
onDayChange={setActiveDay}
|
||||
day1Progress={day1Progress}
|
||||
day2Progress={day2Progress}
|
||||
/>
|
||||
) : (
|
||||
// 진행률 숨김 시 탭만 표시
|
||||
<div className="flex gap-3 mb-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveDay(1)}
|
||||
className={`flex-1 py-3 px-4 rounded-lg border-2 font-medium transition-all ${
|
||||
activeDay === 1
|
||||
? 'bg-blue-600 border-blue-600 text-white'
|
||||
: 'bg-white border-gray-200 text-gray-700 hover:border-blue-300'
|
||||
}`}
|
||||
>
|
||||
1일차: 기준/매뉴얼 심사
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveDay(2)}
|
||||
className={`flex-1 py-3 px-4 rounded-lg border-2 font-medium transition-all ${
|
||||
activeDay === 2
|
||||
? 'bg-blue-600 border-blue-600 text-white'
|
||||
: 'bg-white border-gray-200 text-gray-700 hover:border-blue-300'
|
||||
}`}
|
||||
>
|
||||
2일차: 로트추적 심사
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Middle Panel: Route List */}
|
||||
<div className="col-span-12 lg:col-span-4 min-h-[250px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
|
||||
<RouteList
|
||||
routes={currentRoutes}
|
||||
selectedId={selectedRoute?.id || null}
|
||||
onSelect={handleRouteSelect}
|
||||
reportCode={selectedReport?.code || null}
|
||||
/>
|
||||
</div>
|
||||
{activeDay === 1 ? (
|
||||
// ===== 1일차: 기준/매뉴얼 심사 =====
|
||||
<div className="flex-1 grid grid-cols-12 gap-4 lg:min-h-0">
|
||||
{/* 좌측: 점검표 항목 */}
|
||||
<div className={`col-span-12 min-h-[300px] lg:min-h-0 lg:h-full overflow-auto ${
|
||||
displaySettings.showDocumentSection && displaySettings.showDocumentViewer
|
||||
? 'lg:col-span-3'
|
||||
: displaySettings.showDocumentSection || displaySettings.showDocumentViewer
|
||||
? 'lg:col-span-4'
|
||||
: 'lg:col-span-12'
|
||||
}`}>
|
||||
<Day1ChecklistPanel
|
||||
categories={filteredDay1Categories}
|
||||
selectedSubItemId={selectedSubItemId}
|
||||
onSubItemSelect={handleSubItemSelect}
|
||||
onSubItemToggle={handleSubItemToggle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Panel: Documents */}
|
||||
<div className="col-span-12 lg:col-span-5 min-h-[250px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
|
||||
<DocumentList
|
||||
documents={currentDocuments}
|
||||
routeCode={selectedRoute?.code || null}
|
||||
onViewDocument={handleViewDocument}
|
||||
/>
|
||||
{/* 중앙: 기준 문서화 */}
|
||||
{displaySettings.showDocumentSection && (
|
||||
<div className={`col-span-12 min-h-[250px] lg:min-h-0 lg:h-full overflow-auto ${
|
||||
displaySettings.showDocumentViewer ? 'lg:col-span-4' : 'lg:col-span-8'
|
||||
}`}>
|
||||
<Day1DocumentSection
|
||||
checkItem={selectedCheckItem}
|
||||
selectedDocumentId={selectedStandardDocId}
|
||||
onDocumentSelect={setSelectedStandardDocId}
|
||||
onConfirmComplete={handleConfirmComplete}
|
||||
isCompleted={isSelectedItemCompleted}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 우측: 문서 뷰어 */}
|
||||
{displaySettings.showDocumentViewer && (
|
||||
<div className={`col-span-12 min-h-[250px] lg:min-h-0 lg:h-full overflow-auto ${
|
||||
displaySettings.showDocumentSection ? 'lg:col-span-5' : 'lg:col-span-8'
|
||||
}`}>
|
||||
<Day1DocumentViewer document={selectedStandardDoc} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// ===== 2일차: 로트추적 심사 =====
|
||||
<>
|
||||
<Filters
|
||||
selectedYear={selectedYear}
|
||||
selectedQuarter={selectedQuarter}
|
||||
searchTerm={searchTerm}
|
||||
onYearChange={handleYearChange}
|
||||
onQuarterChange={handleQuarterChange}
|
||||
onSearchChange={handleSearchChange}
|
||||
/>
|
||||
|
||||
<div className="flex-1 grid grid-cols-12 gap-6 lg:min-h-0">
|
||||
<div className="col-span-12 lg:col-span-3 min-h-[300px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
|
||||
<ReportList
|
||||
reports={filteredReports}
|
||||
selectedId={selectedReport?.id || null}
|
||||
onSelect={handleReportSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 lg:col-span-4 min-h-[250px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
|
||||
<RouteList
|
||||
routes={currentRoutes}
|
||||
selectedId={selectedRoute?.id || null}
|
||||
onSelect={handleRouteSelect}
|
||||
reportCode={selectedReport?.code || null}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 lg:col-span-5 min-h-[250px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
|
||||
<DocumentList
|
||||
documents={currentDocuments}
|
||||
routeCode={selectedRoute?.code || null}
|
||||
onViewDocument={handleViewDocument}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 설정 패널 */}
|
||||
<AuditSettingsPanel
|
||||
isOpen={settingsOpen}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
settings={displaySettings}
|
||||
onSettingsChange={setDisplaySettings}
|
||||
/>
|
||||
|
||||
<InspectionModal
|
||||
isOpen={modalOpen}
|
||||
@@ -148,4 +347,4 @@ export default function QualityInspectionPage() {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user