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>
This commit is contained in:
282
src/components/process-management/InspectionPreviewModal.tsx
Normal file
282
src/components/process-management/InspectionPreviewModal.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 중간검사 미리보기 모달
|
||||
*
|
||||
* 설정된 검사 항목들로 실제 성적서가 어떻게 보일지 미리보기
|
||||
*/
|
||||
|
||||
import { Fragment } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import type { InspectionSetting } from '@/types/process';
|
||||
|
||||
interface InspectionPreviewModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
inspectionSetting?: InspectionSetting;
|
||||
}
|
||||
|
||||
export function InspectionPreviewModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
inspectionSetting,
|
||||
}: InspectionPreviewModalProps) {
|
||||
if (!inspectionSetting) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>중간검사 미리보기</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-12 text-center text-muted-foreground">
|
||||
검사 설정이 없습니다. 먼저 검사 설정을 완료해주세요.
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
닫기
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// 활성화된 겉모양 항목들
|
||||
const activeAppearanceItems = [
|
||||
{ key: 'bendingStatus', label: '절곡상태', enabled: inspectionSetting.appearance.bendingStatus.enabled },
|
||||
{ key: 'processingStatus', label: '가공상태', enabled: inspectionSetting.appearance.processingStatus.enabled },
|
||||
{ key: 'sewingStatus', label: '재봉상태', enabled: inspectionSetting.appearance.sewingStatus.enabled },
|
||||
{ key: 'assemblyStatus', label: '조립상태', enabled: inspectionSetting.appearance.assemblyStatus.enabled },
|
||||
].filter((item) => item.enabled);
|
||||
|
||||
// 활성화된 치수 항목들
|
||||
const activeDimensionItems = [
|
||||
{ key: 'length', label: '길이', ...inspectionSetting.dimension.length },
|
||||
{ key: 'width', label: '너비', ...inspectionSetting.dimension.width },
|
||||
{ key: 'height1', label: '1 높이', ...inspectionSetting.dimension.height1 },
|
||||
{ key: 'height2', label: '2 높이', ...inspectionSetting.dimension.height2 },
|
||||
{ key: 'gap', label: '간격', ...inspectionSetting.dimension.gap },
|
||||
].filter((item) => item.enabled);
|
||||
|
||||
// 샘플 데이터 (미리보기용)
|
||||
const sampleRows = [1, 2, 3, 4, 5];
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="w-[95vw] max-w-[1400px] sm:max-w-[1400px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>중간검사 미리보기</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 mt-4">
|
||||
{/* 헤더 정보 */}
|
||||
<div className="flex items-center gap-4 p-4 bg-muted/50 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">기준서명:</span>
|
||||
<Badge variant="outline">{inspectionSetting.standardName || '미설정'}</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">활성 항목:</span>
|
||||
<Badge>{activeAppearanceItems.length + activeDimensionItems.length}개</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 중간검사 기준서 */}
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="bg-muted/50 px-4 py-2 font-semibold text-sm border-b">
|
||||
중간검사 기준서
|
||||
</div>
|
||||
<div className="p-4 grid grid-cols-2 gap-4">
|
||||
{/* 도해 이미지 영역 */}
|
||||
<div className="border rounded-lg p-4 min-h-[200px] flex items-center justify-center bg-muted/30">
|
||||
{inspectionSetting.schematicImage ? (
|
||||
<img
|
||||
src={inspectionSetting.schematicImage}
|
||||
alt="도해 이미지"
|
||||
className="max-w-full max-h-[180px] object-contain"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-sm">도해 이미지 없음</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 검사기준 이미지 또는 검사 항목 테이블 */}
|
||||
<div className="border rounded-lg overflow-hidden min-h-[200px] flex items-center justify-center bg-muted/30">
|
||||
{inspectionSetting.inspectionStandardImage ? (
|
||||
<img
|
||||
src={inspectionSetting.inspectionStandardImage}
|
||||
alt="검사기준 이미지"
|
||||
className="max-w-full max-h-[180px] object-contain"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/50">
|
||||
<tr>
|
||||
<th className="border-b px-3 py-2 text-left">검사항목</th>
|
||||
<th className="border-b px-3 py-2 text-left">검사방법</th>
|
||||
<th className="border-b px-3 py-2 text-left">포인트</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{activeAppearanceItems.map((item) => (
|
||||
<tr key={item.key} className="border-b last:border-b-0">
|
||||
<td className="px-3 py-2">{item.label}</td>
|
||||
<td className="px-3 py-2">양자택일</td>
|
||||
<td className="px-3 py-2">-</td>
|
||||
</tr>
|
||||
))}
|
||||
{activeDimensionItems.map((item) => (
|
||||
<tr key={item.key} className="border-b last:border-b-0">
|
||||
<td className="px-3 py-2">{item.label}</td>
|
||||
<td className="px-3 py-2">{item.method}</td>
|
||||
<td className="px-3 py-2">{item.point}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 중간검사 DATA */}
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="bg-muted/50 px-4 py-2 font-semibold text-sm border-b">
|
||||
중간검사 DATA
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/50">
|
||||
<tr>
|
||||
<th className="border-b border-r px-3 py-2 text-center w-12">No.</th>
|
||||
{/* 겉모양 항목들 */}
|
||||
{activeAppearanceItems.map((item) => (
|
||||
<th
|
||||
key={item.key}
|
||||
className="border-b border-r px-3 py-2 text-center min-w-[80px]"
|
||||
>
|
||||
{item.label}
|
||||
</th>
|
||||
))}
|
||||
{/* 치수 항목들 */}
|
||||
{activeDimensionItems.map((item) => (
|
||||
<th
|
||||
key={item.key}
|
||||
className="border-b border-r px-3 py-2 text-center"
|
||||
colSpan={2}
|
||||
>
|
||||
{item.label} (mm)
|
||||
</th>
|
||||
))}
|
||||
{/* 판정 */}
|
||||
{inspectionSetting.judgment && (
|
||||
<th className="border-b px-3 py-2 text-center w-20">
|
||||
판정
|
||||
<br />
|
||||
<span className="text-xs">(적/부)</span>
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
{/* 치수 서브헤더 */}
|
||||
{activeDimensionItems.length > 0 && (
|
||||
<tr className="bg-muted/30">
|
||||
<th className="border-b border-r px-3 py-1"></th>
|
||||
{activeAppearanceItems.map((item) => (
|
||||
<th key={item.key} className="border-b border-r px-3 py-1 text-xs">
|
||||
양호/불량
|
||||
</th>
|
||||
))}
|
||||
{activeDimensionItems.map((item) => (
|
||||
<Fragment key={`${item.key}-header`}>
|
||||
<th className="border-b border-r px-3 py-1 text-xs">
|
||||
도면치수
|
||||
</th>
|
||||
<th className="border-b border-r px-3 py-1 text-xs">
|
||||
측정값
|
||||
</th>
|
||||
</Fragment>
|
||||
))}
|
||||
{inspectionSetting.judgment && (
|
||||
<th className="border-b px-3 py-1"></th>
|
||||
)}
|
||||
</tr>
|
||||
)}
|
||||
</thead>
|
||||
<tbody>
|
||||
{sampleRows.map((row) => (
|
||||
<tr key={row} className="border-b last:border-b-0 hover:bg-muted/20">
|
||||
<td className="border-r px-3 py-2 text-center">{row}</td>
|
||||
{/* 겉모양 샘플 데이터 */}
|
||||
{activeAppearanceItems.map((item) => (
|
||||
<td key={item.key} className="border-r px-3 py-2 text-center">
|
||||
<span className="text-muted-foreground">☐ 양호</span>
|
||||
<br />
|
||||
<span className="text-muted-foreground">☐ 불량</span>
|
||||
</td>
|
||||
))}
|
||||
{/* 치수 샘플 데이터 */}
|
||||
{activeDimensionItems.map((item) => (
|
||||
<Fragment key={`${item.key}-data-${row}`}>
|
||||
<td className="border-r px-3 py-2 text-center text-muted-foreground">
|
||||
-
|
||||
</td>
|
||||
<td className="border-r px-3 py-2 text-center text-muted-foreground">
|
||||
-
|
||||
</td>
|
||||
</Fragment>
|
||||
))}
|
||||
{/* 판정 샘플 */}
|
||||
{inspectionSetting.judgment && (
|
||||
<td className="px-3 py-2 text-center">
|
||||
<span className="text-muted-foreground">-</span>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 부적합 내용 */}
|
||||
{inspectionSetting.nonConformingContent && (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="bg-muted/50 px-4 py-2 font-semibold text-sm border-r">
|
||||
부적합 내용
|
||||
</div>
|
||||
<div className="bg-muted/50 px-4 py-2 font-semibold text-sm">
|
||||
종합판정
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="px-4 py-3 border-r min-h-[60px] text-muted-foreground text-sm">
|
||||
(부적합 사항 입력 영역)
|
||||
</div>
|
||||
<div className="px-4 py-3 text-center text-muted-foreground text-sm">
|
||||
합격 / 불합격
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 버튼 영역 */}
|
||||
<div className="flex justify-end mt-6">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
닫기
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
293
src/components/process-management/InspectionSettingModal.tsx
Normal file
293
src/components/process-management/InspectionSettingModal.tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 중간검사 설정 모달
|
||||
*
|
||||
* 기획서 Page 9 기준:
|
||||
* - 왼쪽: 기준서명, 도해 이미지, 검사기준 이미지, 겉모양 항목들 ON/OFF
|
||||
* - 오른쪽: 치수 항목들 (포인트, 방법, ON/OFF), 판정, 부적합 내용
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { ImageUpload } from '@/components/ui/image-upload';
|
||||
import type {
|
||||
InspectionSetting,
|
||||
InspectionPointType,
|
||||
InspectionMethodType,
|
||||
} from '@/types/process';
|
||||
import {
|
||||
INSPECTION_POINT_OPTIONS,
|
||||
INSPECTION_METHOD_OPTIONS,
|
||||
DEFAULT_INSPECTION_SETTING,
|
||||
} from '@/types/process';
|
||||
|
||||
interface InspectionSettingModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
initialData?: InspectionSetting;
|
||||
onSave: (data: InspectionSetting) => void;
|
||||
}
|
||||
|
||||
export function InspectionSettingModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
initialData,
|
||||
onSave,
|
||||
}: InspectionSettingModalProps) {
|
||||
const [formData, setFormData] = useState<InspectionSetting>(
|
||||
initialData || DEFAULT_INSPECTION_SETTING
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setFormData(initialData || DEFAULT_INSPECTION_SETTING);
|
||||
}
|
||||
}, [open, initialData]);
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(formData);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
// 겉모양 항목 토글
|
||||
const toggleAppearance = (key: keyof typeof formData.appearance) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
appearance: {
|
||||
...prev.appearance,
|
||||
[key]: { ...prev.appearance[key], enabled: !prev.appearance[key].enabled },
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
// 치수 항목 토글
|
||||
const toggleDimension = (key: keyof typeof formData.dimension) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
dimension: {
|
||||
...prev.dimension,
|
||||
[key]: { ...prev.dimension[key], enabled: !prev.dimension[key].enabled },
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
// 치수 포인트 변경
|
||||
const setDimensionPoint = (
|
||||
key: keyof typeof formData.dimension,
|
||||
point: InspectionPointType
|
||||
) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
dimension: {
|
||||
...prev.dimension,
|
||||
[key]: { ...prev.dimension[key], point },
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
// 치수 방법 변경
|
||||
const setDimensionMethod = (
|
||||
key: keyof typeof formData.dimension,
|
||||
method: InspectionMethodType
|
||||
) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
dimension: {
|
||||
...prev.dimension,
|
||||
[key]: { ...prev.dimension[key], method },
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="w-[95vw] max-w-[1200px] sm:max-w-[1200px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>중간검사 설정</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-4">
|
||||
{/* 왼쪽 패널 - 기본 정보 및 겉모양 */}
|
||||
<div className="space-y-6 border rounded-lg p-4 bg-muted/30">
|
||||
<h3 className="font-semibold text-sm border-b pb-2">중간검사 설정</h3>
|
||||
|
||||
{/* 기준서명 */}
|
||||
<div className="space-y-2">
|
||||
<Label>기준서명</Label>
|
||||
<Input
|
||||
value={formData.standardName}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({ ...prev, standardName: e.target.value }))
|
||||
}
|
||||
placeholder="예: KDPS-20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 중간검사 기준서 이미지 (통합) */}
|
||||
<div className="space-y-2">
|
||||
<Label>중간검사 기준서 이미지</Label>
|
||||
<ImageUpload
|
||||
value={formData.schematicImage}
|
||||
onChange={(file) => {
|
||||
const url = URL.createObjectURL(file);
|
||||
// 두 필드 모두 동일 이미지로 설정 (호환성)
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
schematicImage: url,
|
||||
inspectionStandardImage: url,
|
||||
}));
|
||||
}}
|
||||
onRemove={() => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
schematicImage: undefined,
|
||||
inspectionStandardImage: undefined,
|
||||
}));
|
||||
}}
|
||||
aspectRatio="wide"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
hint="도해 및 검사기준이 포함된 통합 이미지를 업로드하세요"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 겉모양 항목들 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>겉모양 절곡상태</Label>
|
||||
<Switch
|
||||
checked={formData.appearance.bendingStatus.enabled}
|
||||
onCheckedChange={() => toggleAppearance('bendingStatus')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>겉모양 가공상태</Label>
|
||||
<Switch
|
||||
checked={formData.appearance.processingStatus.enabled}
|
||||
onCheckedChange={() => toggleAppearance('processingStatus')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>겉모양 재봉상태</Label>
|
||||
<Switch
|
||||
checked={formData.appearance.sewingStatus.enabled}
|
||||
onCheckedChange={() => toggleAppearance('sewingStatus')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>겉모양 조립상태</Label>
|
||||
<Switch
|
||||
checked={formData.appearance.assemblyStatus.enabled}
|
||||
onCheckedChange={() => toggleAppearance('assemblyStatus')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 오른쪽 패널 - 치수 및 기타 */}
|
||||
<div className="space-y-6 border rounded-lg p-4 bg-muted/30">
|
||||
<h3 className="font-semibold text-sm border-b pb-2">중간검사</h3>
|
||||
|
||||
{/* 치수 항목들 */}
|
||||
{[
|
||||
{ key: 'length' as const, label: '길이' },
|
||||
{ key: 'width' as const, label: '너비' },
|
||||
{ key: 'height1' as const, label: '1 높이' },
|
||||
{ key: 'height2' as const, label: '2 높이' },
|
||||
{ key: 'gap' as const, label: '간격' },
|
||||
].map(({ key, label }) => (
|
||||
<div key={key} className="grid grid-cols-[60px_1fr_100px_50px] items-center gap-3">
|
||||
<Label className="shrink-0">{label}</Label>
|
||||
<Select
|
||||
value={formData.dimension[key].point}
|
||||
onValueChange={(v) => setDimensionPoint(key, v as InspectionPointType)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{INSPECTION_POINT_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={formData.dimension[key].method}
|
||||
onValueChange={(v) => setDimensionMethod(key, v as InspectionMethodType)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{INSPECTION_METHOD_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Switch
|
||||
checked={formData.dimension[key].enabled}
|
||||
onCheckedChange={() => toggleDimension(key)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 판정 */}
|
||||
<div className="flex items-center justify-between pt-4 border-t">
|
||||
<Label>판정</Label>
|
||||
<Switch
|
||||
checked={formData.judgment}
|
||||
onCheckedChange={(checked) =>
|
||||
setFormData((prev) => ({ ...prev, judgment: checked }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 부적합 내용 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>부적합 내용</Label>
|
||||
<Switch
|
||||
checked={formData.nonConformingContent}
|
||||
onCheckedChange={(checked) =>
|
||||
setFormData((prev) => ({ ...prev, nonConformingContent: checked }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 버튼 영역 */}
|
||||
<div className="flex justify-end gap-2 mt-6">
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSave}>저장</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ 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,
|
||||
@@ -23,14 +24,23 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { toast } from 'sonner';
|
||||
import type { ProcessStep, StepConnectionType, StepCompletionType } from '@/types/process';
|
||||
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: '단계',
|
||||
@@ -94,8 +104,20 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) {
|
||||
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()) {
|
||||
@@ -114,6 +136,7 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) {
|
||||
connectionType,
|
||||
connectionTarget: connectionType === '팝업' ? connectionTarget : undefined,
|
||||
completionType,
|
||||
inspectionSetting: isInspectionEnabled ? inspectionSetting : undefined,
|
||||
};
|
||||
|
||||
setIsLoading(true);
|
||||
@@ -236,7 +259,7 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) {
|
||||
<CardTitle className="text-base">연결 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-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
|
||||
@@ -270,6 +293,28 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) {
|
||||
</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>
|
||||
@@ -314,19 +359,37 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) {
|
||||
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}
|
||||
/>
|
||||
<>
|
||||
<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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user