생산관리: - WipProductionModal 기능 개선 - WorkOrderDetail/Edit 확장 (+265줄) - 검사성적서 콘텐츠 5종 대폭 확장 (벤딩/벤딩WIP/스크린/슬랫/슬랫조인트바) - InspectionReportModal 기능 강화 작업자화면: - WorkerScreen 기능 대폭 확장 (+211줄) - WorkItemCard 개선 - InspectionInputModal 신규 추가 (작업자 검사입력) 공정관리: - StepForm 검사항목 설정 기능 추가 - InspectionSettingModal 신규 추가 - InspectionPreviewModal 신규 추가 - process.ts 타입 확장 (+102줄) 자재관리: - StockStatus 상세/목록/타입/목데이터 개선 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
396 lines
13 KiB
TypeScript
396 lines
13 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* 단계 등록/수정 폼 컴포넌트
|
|
*
|
|
* 기획서 스크린샷 2 기준:
|
|
* - 기본 정보: 단계코드(자동), 단계명, 필수여부, 승인여부, 검사여부, 상태
|
|
* - 연결 정보: 유형(팝업/없음), 도달(Select)
|
|
* - 완료 정보: 유형(선택 완료 시 완료/클릭 시 완료)
|
|
*/
|
|
|
|
import { useState, useCallback } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
|
import { toast } from 'sonner';
|
|
import { Settings, Eye } from 'lucide-react';
|
|
import type {
|
|
ProcessStep,
|
|
StepConnectionType,
|
|
StepCompletionType,
|
|
InspectionSetting,
|
|
} from '@/types/process';
|
|
import {
|
|
STEP_CONNECTION_TYPE_OPTIONS,
|
|
STEP_COMPLETION_TYPE_OPTIONS,
|
|
STEP_CONNECTION_TARGET_OPTIONS,
|
|
DEFAULT_INSPECTION_SETTING,
|
|
} from '@/types/process';
|
|
import { createProcessStep, updateProcessStep } from './actions';
|
|
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
|
import { InspectionSettingModal } from './InspectionSettingModal';
|
|
import { InspectionPreviewModal } from './InspectionPreviewModal';
|
|
|
|
const stepCreateConfig: DetailConfig = {
|
|
title: '단계',
|
|
description: '새로운 단계를 등록합니다',
|
|
basePath: '',
|
|
fields: [],
|
|
actions: {
|
|
showBack: true,
|
|
showEdit: false,
|
|
showDelete: false,
|
|
showSave: true,
|
|
submitLabel: '등록',
|
|
},
|
|
};
|
|
|
|
const stepEditConfig: DetailConfig = {
|
|
...stepCreateConfig,
|
|
title: '단계',
|
|
description: '단계 정보를 수정합니다',
|
|
actions: {
|
|
...stepCreateConfig.actions,
|
|
submitLabel: '저장',
|
|
},
|
|
};
|
|
|
|
interface StepFormProps {
|
|
mode: 'create' | 'edit';
|
|
processId: string;
|
|
initialData?: ProcessStep;
|
|
}
|
|
|
|
export function StepForm({ mode, processId, initialData }: StepFormProps) {
|
|
const router = useRouter();
|
|
const isEdit = mode === 'edit';
|
|
|
|
// 기본 정보
|
|
const [stepName, setStepName] = useState(initialData?.stepName || '');
|
|
const [isRequired, setIsRequired] = useState(
|
|
initialData?.isRequired ? '필수' : '선택'
|
|
);
|
|
const [needsApproval, setNeedsApproval] = useState(
|
|
initialData?.needsApproval ? '필요' : '불필요'
|
|
);
|
|
const [needsInspection, setNeedsInspection] = useState(
|
|
initialData?.needsInspection ? '필요' : '불필요'
|
|
);
|
|
const [isActive, setIsActive] = useState(
|
|
initialData?.isActive !== false ? '사용' : '미사용'
|
|
);
|
|
|
|
// 연결 정보
|
|
const [connectionType, setConnectionType] = useState<StepConnectionType>(
|
|
initialData?.connectionType || '없음'
|
|
);
|
|
const [connectionTarget, setConnectionTarget] = useState(
|
|
initialData?.connectionTarget || ''
|
|
);
|
|
|
|
// 완료 정보
|
|
const [completionType, setCompletionType] = useState<StepCompletionType>(
|
|
initialData?.completionType || '클릭 시 완료'
|
|
);
|
|
|
|
// 검사 설정
|
|
const [inspectionSetting, setInspectionSetting] = useState<InspectionSetting>(
|
|
initialData?.inspectionSetting || DEFAULT_INSPECTION_SETTING
|
|
);
|
|
|
|
// 모달 상태
|
|
const [isInspectionSettingOpen, setIsInspectionSettingOpen] = useState(false);
|
|
const [isInspectionPreviewOpen, setIsInspectionPreviewOpen] = useState(false);
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
// 검사여부가 "필요"인지 확인
|
|
const isInspectionEnabled = needsInspection === '필요';
|
|
|
|
// 제출
|
|
const handleSubmit = async (): Promise<{ success: boolean; error?: string }> => {
|
|
if (!stepName.trim()) {
|
|
toast.error('단계명을 입력해주세요.');
|
|
return { success: false, error: '단계명을 입력해주세요.' };
|
|
}
|
|
|
|
const stepData: Omit<ProcessStep, 'id'> = {
|
|
stepCode: initialData?.stepCode || `STP-${Date.now().toString().slice(-6)}`,
|
|
stepName: stepName.trim(),
|
|
isRequired: isRequired === '필수',
|
|
needsApproval: needsApproval === '필요',
|
|
needsInspection: needsInspection === '필요',
|
|
isActive: isActive === '사용',
|
|
order: initialData?.order || 0,
|
|
connectionType,
|
|
connectionTarget: connectionType === '팝업' ? connectionTarget : undefined,
|
|
completionType,
|
|
inspectionSetting: isInspectionEnabled ? inspectionSetting : undefined,
|
|
};
|
|
|
|
setIsLoading(true);
|
|
try {
|
|
if (isEdit && initialData?.id) {
|
|
const result = await updateProcessStep(processId, initialData.id, stepData);
|
|
if (result.success) {
|
|
toast.success('단계가 수정되었습니다.');
|
|
router.push(`/ko/master-data/process-management/${processId}`);
|
|
return { success: true };
|
|
} else {
|
|
toast.error(result.error || '수정에 실패했습니다.');
|
|
return { success: false, error: result.error };
|
|
}
|
|
} else {
|
|
const result = await createProcessStep(processId, stepData);
|
|
if (result.success) {
|
|
toast.success('단계가 등록되었습니다.');
|
|
router.push(`/ko/master-data/process-management/${processId}`);
|
|
return { success: true };
|
|
} else {
|
|
toast.error(result.error || '등록에 실패했습니다.');
|
|
return { success: false, error: result.error };
|
|
}
|
|
}
|
|
} catch {
|
|
toast.error('처리 중 오류가 발생했습니다.');
|
|
return { success: false, error: '처리 중 오류가 발생했습니다.' };
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
router.push(`/ko/master-data/process-management/${processId}`);
|
|
};
|
|
|
|
const renderFormContent = useCallback(
|
|
() => (
|
|
<div className="space-y-6">
|
|
{/* 기본 정보 */}
|
|
<Card>
|
|
<CardHeader className="bg-muted/50">
|
|
<CardTitle className="text-base">기본 정보</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="pt-6">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-6">
|
|
<div className="space-y-2">
|
|
<Label>단계코드</Label>
|
|
<Input
|
|
value={initialData?.stepCode || '자동생성'}
|
|
disabled
|
|
className="bg-muted"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="stepName">단계명 *</Label>
|
|
<Input
|
|
id="stepName"
|
|
value={stepName}
|
|
onChange={(e) => setStepName(e.target.value)}
|
|
placeholder="예: 자재투입"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>필수여부</Label>
|
|
<Select value={isRequired} onValueChange={setIsRequired}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="필수">필수</SelectItem>
|
|
<SelectItem value="선택">선택</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>승인여부</Label>
|
|
<Select value={needsApproval} onValueChange={setNeedsApproval}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="필요">필요</SelectItem>
|
|
<SelectItem value="불필요">불필요</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>검사여부</Label>
|
|
<Select value={needsInspection} onValueChange={setNeedsInspection}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="필요">필요</SelectItem>
|
|
<SelectItem value="불필요">불필요</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>상태</Label>
|
|
<Select value={isActive} onValueChange={setIsActive}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="사용">사용</SelectItem>
|
|
<SelectItem value="미사용">미사용</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 연결 정보 */}
|
|
<Card>
|
|
<CardHeader className="bg-muted/50">
|
|
<CardTitle className="text-base">연결 정보</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="pt-6">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 items-end">
|
|
<div className="space-y-2">
|
|
<Label>유형</Label>
|
|
<Select
|
|
value={connectionType}
|
|
onValueChange={(v) => setConnectionType(v as StepConnectionType)}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{STEP_CONNECTION_TYPE_OPTIONS.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>도달</Label>
|
|
<Select value={connectionTarget} onValueChange={setConnectionTarget}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="선택하세요" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{STEP_CONNECTION_TARGET_OPTIONS.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
{/* 검사여부가 "필요"일 때 버튼 표시 */}
|
|
{isInspectionEnabled && (
|
|
<>
|
|
<Button
|
|
type="button"
|
|
variant="default"
|
|
className="bg-amber-500 hover:bg-amber-600 text-white"
|
|
onClick={() => setIsInspectionSettingOpen(true)}
|
|
>
|
|
<Settings className="h-4 w-4 mr-2" />
|
|
검사 설정
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setIsInspectionPreviewOpen(true)}
|
|
>
|
|
<Eye className="h-4 w-4 mr-2" />
|
|
검사 미리보기
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 완료 정보 */}
|
|
<Card>
|
|
<CardHeader className="bg-muted/50">
|
|
<CardTitle className="text-base">완료 정보</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="pt-6">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
|
<div className="space-y-2">
|
|
<Label>유형</Label>
|
|
<Select
|
|
value={completionType}
|
|
onValueChange={(v) => setCompletionType(v as StepCompletionType)}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{STEP_COMPLETION_TYPE_OPTIONS.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
),
|
|
[
|
|
stepName,
|
|
isRequired,
|
|
needsApproval,
|
|
needsInspection,
|
|
isActive,
|
|
connectionType,
|
|
connectionTarget,
|
|
completionType,
|
|
initialData?.stepCode,
|
|
isInspectionEnabled,
|
|
]
|
|
);
|
|
|
|
const config = isEdit ? stepEditConfig : stepCreateConfig;
|
|
|
|
return (
|
|
<>
|
|
<IntegratedDetailTemplate
|
|
config={config}
|
|
mode={isEdit ? 'edit' : 'create'}
|
|
isLoading={false}
|
|
onCancel={handleCancel}
|
|
onSubmit={handleSubmit}
|
|
renderForm={renderFormContent}
|
|
/>
|
|
|
|
{/* 검사 설정 모달 */}
|
|
<InspectionSettingModal
|
|
open={isInspectionSettingOpen}
|
|
onOpenChange={setIsInspectionSettingOpen}
|
|
initialData={inspectionSetting}
|
|
onSave={setInspectionSetting}
|
|
/>
|
|
|
|
{/* 검사 미리보기 모달 */}
|
|
<InspectionPreviewModal
|
|
open={isInspectionPreviewOpen}
|
|
onOpenChange={setIsInspectionPreviewOpen}
|
|
inspectionSetting={isInspectionEnabled ? inspectionSetting : undefined}
|
|
/>
|
|
</>
|
|
);
|
|
}
|