diff --git a/.gitignore b/.gitignore index d061a18d..5e9d859b 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,7 @@ playwright.config.ts playwright-report/ test-results/ .playwright/ + +# 로컬 테스트/개발용 폴더 +src/app/\[locale\]/(protected)/dev/ +src/components/common/EditableTable/ diff --git a/src/app/[locale]/(protected)/master-data/process-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/master-data/process-management/[id]/edit/page.tsx new file mode 100644 index 00000000..fc77f758 --- /dev/null +++ b/src/app/[locale]/(protected)/master-data/process-management/[id]/edit/page.tsx @@ -0,0 +1,86 @@ +/** + * 공정 수정 페이지 + */ + +'use client'; + +import { use } from 'react'; +import { notFound } from 'next/navigation'; +import { ProcessForm } from '@/components/process-management'; +import type { Process } from '@/types/process'; + +// Mock 데이터 +const mockProcesses: Process[] = [ + { + id: '1', + processCode: 'P-004', + processName: '재고(포밍)', + description: '철판을 포밍하여 절곡 부품(반제품) 생산 후 재고 입고', + processType: '생산', + department: '포밍생산부서', + workLogTemplate: '재고생산 작업일지', + classificationRules: [], + requiredWorkers: 2, + workSteps: ['포밍', '검사', '포장'], + status: '사용중', + createdAt: '2025-01-01', + updatedAt: '2025-01-15', + }, + { + id: '2', + processCode: 'P-003', + processName: '슬랫', + description: '슬랫 코일 절단 및 성형', + processType: '생산', + department: '슬랫생산부서', + classificationRules: [], + requiredWorkers: 3, + workSteps: ['절단', '성형', '검사'], + status: '사용중', + createdAt: '2025-01-01', + updatedAt: '2025-01-10', + }, + { + id: '3', + processCode: 'P-002', + processName: '절곡', + description: '가이드레일, 케이스, 하단마감재 제작', + processType: '생산', + department: '절곡생산부서', + classificationRules: [], + requiredWorkers: 4, + workSteps: ['절단', '절곡', '용접', '검사'], + status: '사용중', + createdAt: '2025-01-01', + updatedAt: '2025-01-08', + }, + { + id: '4', + processCode: 'P-001', + processName: '스크린', + description: '방화스크린 원단 가공 및 조립', + processType: '생산', + department: '스크린생산부서', + classificationRules: [], + requiredWorkers: 3, + workSteps: ['원단가공', '조립', '검사', '포장'], + status: '사용중', + createdAt: '2025-01-01', + updatedAt: '2025-01-05', + }, +]; + +export default function EditProcessPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = use(params); + const process = mockProcesses.find((p) => p.id === id); + + if (!process) { + notFound(); + } + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/master-data/process-management/[id]/page.tsx b/src/app/[locale]/(protected)/master-data/process-management/[id]/page.tsx new file mode 100644 index 00000000..b7ed3b5c --- /dev/null +++ b/src/app/[locale]/(protected)/master-data/process-management/[id]/page.tsx @@ -0,0 +1,114 @@ +/** + * 공정 상세 페이지 + */ + +import { Suspense } from 'react'; +import { notFound } from 'next/navigation'; +import { ProcessDetail } from '@/components/process-management'; +import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; +import type { Process } from '@/types/process'; + +// Mock 데이터 +const mockProcesses: Process[] = [ + { + id: '1', + processCode: 'P-004', + processName: '재고(포밍)', + description: '철판을 포밍하여 절곡 부품(반제품) 생산 후 재고 입고', + processType: '생산', + department: '포밍생산부서', + workLogTemplate: '재고생산 작업일지', + classificationRules: [], + requiredWorkers: 2, + workSteps: ['포밍', '검사', '포장'], + status: '사용중', + createdAt: '2025-01-01', + updatedAt: '2025-01-15', + }, + { + id: '2', + processCode: 'P-003', + processName: '슬랫', + description: '슬랫 코일 절단 및 성형', + processType: '생산', + department: '슬랫생산부서', + classificationRules: [], + requiredWorkers: 3, + workSteps: ['절단', '성형', '검사'], + status: '사용중', + createdAt: '2025-01-01', + updatedAt: '2025-01-10', + }, + { + id: '3', + processCode: 'P-002', + processName: '절곡', + description: '가이드레일, 케이스, 하단마감재 제작', + processType: '생산', + department: '절곡생산부서', + classificationRules: [], + requiredWorkers: 4, + workSteps: ['절단', '절곡', '용접', '검사'], + status: '사용중', + createdAt: '2025-01-01', + updatedAt: '2025-01-08', + }, + { + id: '4', + processCode: 'P-001', + processName: '스크린', + description: '방화스크린 원단 가공 및 조립', + processType: '생산', + department: '스크린생산부서', + classificationRules: [], + requiredWorkers: 3, + workSteps: ['원단가공', '조립', '검사', '포장'], + status: '사용중', + createdAt: '2025-01-01', + updatedAt: '2025-01-05', + }, +]; + +async function getProcessById(id: string): Promise { + const process = mockProcesses.find((p) => p.id === id); + return process || null; +} + +export default async function ProcessDetailPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + const process = await getProcessById(id); + + if (!process) { + notFound(); + } + + return ( + }> + + + ); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + const process = await getProcessById(id); + + if (!process) { + return { + title: '공정을 찾을 수 없습니다', + }; + } + + return { + title: `${process.processName} - 공정 상세`, + description: `${process.processCode} 공정 정보`, + }; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/master-data/process-management/new/page.tsx b/src/app/[locale]/(protected)/master-data/process-management/new/page.tsx new file mode 100644 index 00000000..f6984856 --- /dev/null +++ b/src/app/[locale]/(protected)/master-data/process-management/new/page.tsx @@ -0,0 +1,11 @@ +/** + * 공정 등록 페이지 + */ + +'use client'; + +import { ProcessForm } from '@/components/process-management'; + +export default function CreateProcessPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/master-data/process-management/page.tsx b/src/app/[locale]/(protected)/master-data/process-management/page.tsx new file mode 100644 index 00000000..a4df5a46 --- /dev/null +++ b/src/app/[locale]/(protected)/master-data/process-management/page.tsx @@ -0,0 +1,21 @@ +/** + * 공정 목록 페이지 + */ + +import { Suspense } from 'react'; +import ProcessListClient from '@/components/process-management/ProcessListClient'; +import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; +import type { Metadata } from 'next'; + +export const metadata: Metadata = { + title: '공정 목록', + description: '공정 관리 - 생산 공정 목록 조회 및 관리', +}; + +export default function ProcessManagementPage() { + return ( + }> + + + ); +} \ No newline at end of file diff --git a/src/components/molecules/MobileCard.tsx b/src/components/molecules/MobileCard.tsx new file mode 100644 index 00000000..922a4af9 --- /dev/null +++ b/src/components/molecules/MobileCard.tsx @@ -0,0 +1,99 @@ +'use client'; + +import { ReactNode } from 'react'; +import { Card, CardContent } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Checkbox } from '@/components/ui/checkbox'; +import { cn } from '@/lib/utils'; + +interface MobileCardDetail { + label: string; + value: string | ReactNode; +} + +interface MobileCardProps { + title: string; + subtitle?: string; + description?: string; + badge?: string; + badgeVariant?: 'default' | 'secondary' | 'destructive' | 'outline'; + isSelected?: boolean; + onToggle?: () => void; + onClick?: () => void; + details?: MobileCardDetail[]; + actions?: ReactNode; + className?: string; +} + +export function MobileCard({ + title, + subtitle, + description, + badge, + badgeVariant = 'default', + isSelected = false, + onToggle, + onClick, + details = [], + actions, + className, +}: MobileCardProps) { + return ( + + +
+ {onToggle && ( +
e.stopPropagation()}> + +
+ )} +
+ {/* 헤더 */} +
+
+
{title}
+ {subtitle && ( +
{subtitle}
+ )} +
+ {badge && {badge}} +
+ + {/* 설명 */} + {description && ( +

+ {description} +

+ )} + + {/* 상세 정보 */} + {details.length > 0 && ( +
+ {details.map((detail, index) => ( +
+ {detail.label}: + {detail.value} +
+ ))} +
+ )} + + {/* 액션 */} + {actions && ( +
e.stopPropagation()}> + {actions} +
+ )} +
+
+
+
+ ); +} diff --git a/src/components/process-management/ProcessDetail.tsx b/src/components/process-management/ProcessDetail.tsx new file mode 100644 index 00000000..0bc3e340 --- /dev/null +++ b/src/components/process-management/ProcessDetail.tsx @@ -0,0 +1,217 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { List, Edit, Wrench } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { PageLayout } from '@/components/organisms/PageLayout'; +import { ProcessWorkLogPreviewModal } from './ProcessWorkLogPreviewModal'; +import type { Process } from '@/types/process'; + +interface ProcessDetailProps { + process: Process; +} + +export function ProcessDetail({ process }: ProcessDetailProps) { + const router = useRouter(); + const [workLogModalOpen, setWorkLogModalOpen] = useState(false); + + const handleEdit = () => { + router.push(`/ko/master-data/process-management/${process.id}/edit`); + }; + + const handleList = () => { + router.push('/ko/master-data/process-management'); + }; + + const handleViewWorkLog = () => { + setWorkLogModalOpen(true); + }; + + return ( + + {/* 헤더 */} +
+
+ +

공정 상세

+
+
+ + +
+
+ +
+ {/* 기본 정보 */} + + + 기본 정보 + + +
+
+
공정코드
+
{process.processCode}
+
+
+
공정명
+
{process.processName}
+
+
+
공정구분
+ {process.processType} +
+
+
담당부서
+
{process.department}
+
+
+
작업일지 양식
+
+ + {process.workLogTemplate || '-'} + + {process.workLogTemplate && ( + + )} +
+
+
+
+
+ + {/* 등록 정보 */} + + + 등록 정보 + + +
+
+
등록일
+
{process.createdAt}
+
+
+
최종수정일
+
{process.updatedAt}
+
+
+
+
+ + {/* 자동 분류 규칙 */} + + + 자동 분류 규칙 + + + {process.classificationRules.length === 0 ? ( +
+ +

등록된 자동 분류 규칙이 없습니다

+
+ ) : ( +
+ {process.classificationRules.map((rule) => ( +
+
+ + {rule.isActive ? '활성' : '비활성'} + +
+
+ {rule.ruleType} - "{rule.conditionValue}" +
+ {rule.description && ( +
+ {rule.description} +
+ )} +
+
+ 우선순위: {rule.priority} +
+ ))} +
+ )} +
+
+ + {/* 세부 작업단계 */} + + + 세부 작업단계 + + + {process.workSteps.length > 0 ? ( +
+ {process.workSteps.map((step, index) => ( +
+ + + {index + 1} + + {step} + + {index < process.workSteps.length - 1 && ( + {'>'} + )} +
+ ))} +
+ ) : ( +
-
+ )} +
+
+ + {/* 작업 정보 */} + + + 작업 정보 + + +
+
필요인원
+
{process.requiredWorkers}명
+
+ {process.equipmentInfo && ( +
+
설비정보
+
{process.equipmentInfo}
+
+ )} +
+
설명
+
{process.description || '-'}
+
+
+
+
+ + {/* 작업일지 양식 미리보기 모달 */} + +
+ ); +} \ No newline at end of file diff --git a/src/components/process-management/ProcessForm.tsx b/src/components/process-management/ProcessForm.tsx new file mode 100644 index 00000000..5b63f671 --- /dev/null +++ b/src/components/process-management/ProcessForm.tsx @@ -0,0 +1,359 @@ +'use client'; + +import { useState, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; +import { X, Save, Plus, Wrench, Trash2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { PageLayout } from '@/components/organisms/PageLayout'; +import { RuleModal } from './RuleModal'; +import type { Process, ClassificationRule, ProcessType } from '@/types/process'; +import { PROCESS_TYPE_OPTIONS, MATCHING_TYPE_OPTIONS } from '@/types/process'; + +// Mock 담당부서 옵션 +const DEPARTMENT_OPTIONS = [ + { value: '스크린생산부서', label: '스크린생산부서' }, + { value: '절곡생산부서', label: '절곡생산부서' }, + { value: '슬랫생산부서', label: '슬랫생산부서' }, + { value: '품질관리부서', label: '품질관리부서' }, + { value: '포장/출하부서', label: '포장/출하부서' }, +]; + +// Mock 작업일지 양식 옵션 +const WORK_LOG_OPTIONS = [ + { value: '스크린 작업일지', label: '스크린 작업일지' }, + { value: '절곡 작업일지', label: '절곡 작업일지' }, + { value: '슬랫 작업일지', label: '슬랫 작업일지' }, + { value: '재고생산 작업일지', label: '재고생산 작업일지' }, + { value: '포장 작업일지', label: '포장 작업일지' }, +]; + +interface ProcessFormProps { + mode: 'create' | 'edit'; + initialData?: Process; +} + +export function ProcessForm({ mode, initialData }: ProcessFormProps) { + const router = useRouter(); + const isEdit = mode === 'edit'; + + // 폼 상태 + const [processName, setProcessName] = useState(initialData?.processName || ''); + const [processType, setProcessType] = useState( + initialData?.processType || '생산' + ); + const [department, setDepartment] = useState(initialData?.department || ''); + const [workLogTemplate, setWorkLogTemplate] = useState( + initialData?.workLogTemplate || '' + ); + const [classificationRules, setClassificationRules] = useState( + initialData?.classificationRules || [] + ); + const [requiredWorkers, setRequiredWorkers] = useState( + initialData?.requiredWorkers || 1 + ); + const [equipmentInfo, setEquipmentInfo] = useState(initialData?.equipmentInfo || ''); + const [workSteps, setWorkSteps] = useState(initialData?.workSteps?.join(', ') || ''); + const [note, setNote] = useState(initialData?.note || ''); + const [isActive, setIsActive] = useState(initialData?.status === '사용중' ?? true); + + // 규칙 모달 상태 + const [ruleModalOpen, setRuleModalOpen] = useState(false); + + // 규칙 추가 + const handleAddRule = useCallback( + (ruleData: Omit) => { + const newRule: ClassificationRule = { + ...ruleData, + id: `rule-${Date.now()}`, + createdAt: new Date().toISOString(), + }; + setClassificationRules((prev) => [...prev, newRule]); + }, + [] + ); + + // 규칙 삭제 + const handleDeleteRule = useCallback((ruleId: string) => { + setClassificationRules((prev) => prev.filter((r) => r.id !== ruleId)); + }, []); + + // 제출 + const handleSubmit = () => { + if (!processName.trim()) { + alert('공정명을 입력해주세요.'); + return; + } + if (!department) { + alert('담당부서를 선택해주세요.'); + return; + } + + const formData = { + processName: processName.trim(), + processType, + department, + workLogTemplate: workLogTemplate || undefined, + classificationRules, + requiredWorkers, + equipmentInfo: equipmentInfo.trim() || undefined, + workSteps: workSteps + .split(',') + .map((s) => s.trim()) + .filter(Boolean), + note: note.trim() || undefined, + isActive, + }; + + console.log(isEdit ? '공정 수정 데이터:' : '공정 등록 데이터:', formData); + alert(`공정이 ${isEdit ? '수정' : '등록'}되었습니다.`); + router.push('/ko/master-data/process-management'); + }; + + // 취소 + const handleCancel = () => { + router.back(); + }; + + return ( + + {/* 헤더 */} +
+

공정 {isEdit ? '수정' : '등록'}

+
+ + +
+
+ +
+ {/* 기본 정보 */} + + + 기본 정보 + + +
+
+ + setProcessName(e.target.value)} + placeholder="예: 스크린" + /> +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + {/* 자동 분류 규칙 */} + + +
+
+ 자동 분류 규칙 +

+ 품목이 이 공정에 이동으로 분류되는 규칙을 생성합니다. +

+
+ +
+
+ + {classificationRules.length === 0 ? ( +
+ +

품목별 규칙이 없습니다

+

+ 규칙을 추가하면 해당 패턴의 품목이 이 공정으로 분류됩니다 +

+
+ ) : ( +
+ {classificationRules.map((rule) => ( +
+
+ + {rule.isActive ? '활성' : '비활성'} + +
+
+ {rule.ruleType}{' '} + { + MATCHING_TYPE_OPTIONS.find( + (o) => o.value === rule.matchingType + )?.label + }{' '} + "{rule.conditionValue}" +
+ {rule.description && ( +
+ {rule.description} +
+ )} +
+
+
+ 우선순위: {rule.priority} + +
+
+ ))} +
+ )} +
+
+ + {/* 작업 정보 */} + + + 작업 정보 + + +
+ + setRequiredWorkers(Number(e.target.value))} + min={1} + className="w-32" + /> +
+
+ + setEquipmentInfo(e.target.value)} + placeholder="예: 미싱기 3대, 절단기 1대" + /> +
+
+ + setWorkSteps(e.target.value)} + placeholder="예: 원단절단, 미싱, 핸드작업, 중간검사, 포장" + /> +
+
+
+ + {/* 설명 */} + + + 설명 + + +
+ +