Files
sam-react-prod/src/components/process-management/StepForm.tsx
유병철 efcc645e24 feat(WEB): 생산/검사 기능 대폭 확장 및 작업자화면 검사입력 추가
생산관리:
- 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>
2026-02-05 21:43:28 +09:00

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}
/>
</>
);
}