- BOM 항목 추가/수정/삭제 시 섹션탭 즉시 반영 - 섹션 복제 시 UI 즉시 업데이트 (null vs undefined 이슈 해결) - 항목 수정 기능 추가 (useTemplateManagement) - 실시간 동기화 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
368 lines
14 KiB
TypeScript
368 lines
14 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Check, X } from 'lucide-react';
|
|
import type { ItemMasterField } from '@/contexts/ItemMasterContext';
|
|
|
|
const INPUT_TYPE_OPTIONS = [
|
|
{ value: 'textbox', label: '텍스트 입력' },
|
|
{ value: 'number', label: '숫자 입력' },
|
|
{ value: 'dropdown', label: '드롭다운' },
|
|
{ value: 'checkbox', label: '체크박스' },
|
|
{ value: 'date', label: '날짜' },
|
|
{ value: 'textarea', label: '긴 텍스트' },
|
|
];
|
|
|
|
interface TemplateFieldDialogProps {
|
|
isTemplateFieldDialogOpen: boolean;
|
|
setIsTemplateFieldDialogOpen: (open: boolean) => void;
|
|
editingTemplateFieldId: number | null;
|
|
setEditingTemplateFieldId: (id: number | null) => void;
|
|
templateFieldName: string;
|
|
setTemplateFieldName: (name: string) => void;
|
|
templateFieldKey: string;
|
|
setTemplateFieldKey: (key: string) => void;
|
|
templateFieldInputType: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
|
|
setTemplateFieldInputType: (type: any) => void;
|
|
templateFieldRequired: boolean;
|
|
setTemplateFieldRequired: (required: boolean) => void;
|
|
templateFieldOptions: string;
|
|
setTemplateFieldOptions: (options: string) => void;
|
|
templateFieldDescription: string;
|
|
setTemplateFieldDescription: (description: string) => void;
|
|
templateFieldMultiColumn: boolean;
|
|
setTemplateFieldMultiColumn: (multi: boolean) => void;
|
|
templateFieldColumnCount: number;
|
|
setTemplateFieldColumnCount: (count: number) => void;
|
|
templateFieldColumnNames: string[];
|
|
setTemplateFieldColumnNames: (names: string[]) => void;
|
|
handleAddTemplateField: () => void | Promise<void>;
|
|
// 항목 관련 props
|
|
itemMasterFields?: ItemMasterField[];
|
|
templateFieldInputMode?: 'custom' | 'master';
|
|
setTemplateFieldInputMode?: (mode: 'custom' | 'master') => void;
|
|
showMasterFieldList?: boolean;
|
|
setShowMasterFieldList?: (show: boolean) => void;
|
|
selectedMasterFieldId?: string;
|
|
setSelectedMasterFieldId?: (id: string) => void;
|
|
}
|
|
|
|
export function TemplateFieldDialog({
|
|
isTemplateFieldDialogOpen,
|
|
setIsTemplateFieldDialogOpen,
|
|
editingTemplateFieldId,
|
|
setEditingTemplateFieldId,
|
|
templateFieldName,
|
|
setTemplateFieldName,
|
|
templateFieldKey,
|
|
setTemplateFieldKey,
|
|
templateFieldInputType,
|
|
setTemplateFieldInputType,
|
|
templateFieldRequired,
|
|
setTemplateFieldRequired,
|
|
templateFieldOptions,
|
|
setTemplateFieldOptions,
|
|
templateFieldDescription,
|
|
setTemplateFieldDescription,
|
|
templateFieldMultiColumn,
|
|
setTemplateFieldMultiColumn,
|
|
templateFieldColumnCount,
|
|
setTemplateFieldColumnCount,
|
|
templateFieldColumnNames,
|
|
setTemplateFieldColumnNames,
|
|
handleAddTemplateField,
|
|
// 항목 관련 props (optional)
|
|
itemMasterFields = [],
|
|
templateFieldInputMode = 'custom',
|
|
setTemplateFieldInputMode,
|
|
showMasterFieldList = false,
|
|
setShowMasterFieldList,
|
|
selectedMasterFieldId = '',
|
|
setSelectedMasterFieldId,
|
|
}: TemplateFieldDialogProps) {
|
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
|
|
// 유효성 검사
|
|
const isNameEmpty = !templateFieldName.trim();
|
|
const isKeyEmpty = !templateFieldKey.trim();
|
|
|
|
const handleClose = () => {
|
|
setIsSubmitted(false);
|
|
setIsTemplateFieldDialogOpen(false);
|
|
setEditingTemplateFieldId(null);
|
|
setTemplateFieldName('');
|
|
setTemplateFieldKey('');
|
|
setTemplateFieldInputType('textbox');
|
|
setTemplateFieldRequired(false);
|
|
setTemplateFieldOptions('');
|
|
setTemplateFieldDescription('');
|
|
setTemplateFieldMultiColumn(false);
|
|
setTemplateFieldColumnCount(2);
|
|
setTemplateFieldColumnNames(['컬럼1', '컬럼2']);
|
|
// 항목 관련 상태 초기화
|
|
setTemplateFieldInputMode?.('custom');
|
|
setShowMasterFieldList?.(false);
|
|
setSelectedMasterFieldId?.('');
|
|
};
|
|
|
|
const handleSelectMasterField = (field: ItemMasterField) => {
|
|
setSelectedMasterFieldId?.(String(field.id));
|
|
setTemplateFieldName(field.field_name);
|
|
setTemplateFieldKey(field.id.toString());
|
|
setTemplateFieldInputType(field.field_type);
|
|
setTemplateFieldRequired(field.properties?.required || false);
|
|
setTemplateFieldDescription(field.description || '');
|
|
// options는 {label, value}[] 배열이므로 label만 추출
|
|
setTemplateFieldOptions(field.options?.map(opt => opt.label).join(', ') || '');
|
|
if (field.properties?.multiColumn && field.properties?.columnNames) {
|
|
setTemplateFieldMultiColumn(true);
|
|
setTemplateFieldColumnCount(field.properties.columnNames.length);
|
|
setTemplateFieldColumnNames(field.properties.columnNames);
|
|
} else {
|
|
setTemplateFieldMultiColumn(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={isTemplateFieldDialogOpen} onOpenChange={handleClose}>
|
|
<DialogContent className="max-w-[95vw] sm:max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>{editingTemplateFieldId ? '항목 수정' : '항목 추가'}</DialogTitle>
|
|
<DialogDescription>
|
|
재사용 가능한 항목을 선택하거나 직접 입력하세요
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
{/* 입력 모드 선택 (편집 시에는 표시 안 함) */}
|
|
{!editingTemplateFieldId && setTemplateFieldInputMode && (
|
|
<div className="flex gap-2 p-1 bg-gray-100 rounded">
|
|
<Button
|
|
variant={templateFieldInputMode === 'custom' ? 'default' : 'ghost'}
|
|
size="sm"
|
|
onClick={() => setTemplateFieldInputMode('custom')}
|
|
className="flex-1"
|
|
>
|
|
직접 입력
|
|
</Button>
|
|
<Button
|
|
variant={templateFieldInputMode === 'master' ? 'default' : 'ghost'}
|
|
size="sm"
|
|
onClick={() => {
|
|
setTemplateFieldInputMode('master');
|
|
setShowMasterFieldList?.(true);
|
|
}}
|
|
className="flex-1"
|
|
>
|
|
항목 선택
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* 항목 목록 */}
|
|
{templateFieldInputMode === 'master' && !editingTemplateFieldId && showMasterFieldList && (
|
|
<div className="border rounded p-3 space-y-2 max-h-[400px] overflow-y-auto">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<Label>항목 목록</Label>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setShowMasterFieldList?.(false)}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
{itemMasterFields.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground text-center py-4">
|
|
등록된 항목이 없습니다
|
|
</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{itemMasterFields.map(field => (
|
|
<div
|
|
key={field.id}
|
|
className={`p-3 border rounded cursor-pointer transition-colors ${
|
|
selectedMasterFieldId === String(field.id)
|
|
? 'bg-blue-50 border-blue-300'
|
|
: 'hover:bg-gray-50'
|
|
}`}
|
|
onClick={() => handleSelectMasterField(field)}
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium">{field.field_name}</span>
|
|
<Badge variant="outline" className="text-xs">
|
|
{INPUT_TYPE_OPTIONS.find(o => o.value === field.field_type)?.label || field.field_type}
|
|
</Badge>
|
|
{field.properties?.required && (
|
|
<Badge variant="destructive" className="text-xs">필수</Badge>
|
|
)}
|
|
</div>
|
|
{field.description && (
|
|
<p className="text-xs text-muted-foreground mt-1">{field.description}</p>
|
|
)}
|
|
{field.category && (
|
|
<div className="flex gap-1 mt-1">
|
|
<Badge variant="secondary" className="text-xs">
|
|
{field.category}
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{selectedMasterFieldId === String(field.id) && (
|
|
<Check className="h-5 w-5 text-blue-600" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 직접 입력 폼 */}
|
|
{(templateFieldInputMode === 'custom' || editingTemplateFieldId || !setTemplateFieldInputMode) && (
|
|
<>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label>항목명 *</Label>
|
|
<Input
|
|
value={templateFieldName}
|
|
onChange={(e) => setTemplateFieldName(e.target.value)}
|
|
placeholder="예: 품목명"
|
|
className={isSubmitted && isNameEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
|
/>
|
|
{isSubmitted && isNameEmpty && (
|
|
<p className="text-xs text-red-500 mt-1">항목명을 입력해주세요</p>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<Label>필드 키 *</Label>
|
|
<Input
|
|
value={templateFieldKey}
|
|
onChange={(e) => setTemplateFieldKey(e.target.value)}
|
|
placeholder="예: itemName"
|
|
className={isSubmitted && isKeyEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
|
/>
|
|
{isSubmitted && isKeyEmpty && (
|
|
<p className="text-xs text-red-500 mt-1">필드 키를 입력해주세요</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>입력방식 *</Label>
|
|
<Select value={templateFieldInputType} onValueChange={(v: any) => setTemplateFieldInputType(v)}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{INPUT_TYPE_OPTIONS.map(opt => (
|
|
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{templateFieldInputType === 'dropdown' && (
|
|
<div>
|
|
<Label>드롭다운 옵션</Label>
|
|
<Input
|
|
value={templateFieldOptions}
|
|
onChange={(e) => setTemplateFieldOptions(e.target.value)}
|
|
placeholder="옵션1, 옵션2, 옵션3 (쉼표로 구분)"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{(templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && (
|
|
<div className="space-y-3 border rounded-lg p-4 bg-muted/30">
|
|
<div className="flex items-center gap-2">
|
|
<Switch
|
|
checked={templateFieldMultiColumn}
|
|
onCheckedChange={setTemplateFieldMultiColumn}
|
|
/>
|
|
<Label>다중 컬럼 사용</Label>
|
|
</div>
|
|
|
|
{templateFieldMultiColumn && (
|
|
<>
|
|
<div>
|
|
<Label>컬럼 개수</Label>
|
|
<Input
|
|
type="number"
|
|
min={2}
|
|
max={10}
|
|
value={templateFieldColumnCount}
|
|
onChange={(e) => {
|
|
const count = parseInt(e.target.value) || 2;
|
|
setTemplateFieldColumnCount(count);
|
|
const newNames = Array.from({ length: count }, (_, i) =>
|
|
templateFieldColumnNames[i] || `컬럼${i + 1}`
|
|
);
|
|
setTemplateFieldColumnNames(newNames);
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>컬럼명</Label>
|
|
{Array.from({ length: templateFieldColumnCount }).map((_, idx) => (
|
|
<Input
|
|
key={idx}
|
|
placeholder={`컬럼 ${idx + 1}`}
|
|
value={templateFieldColumnNames[idx] || ''}
|
|
onChange={(e) => {
|
|
const newNames = [...templateFieldColumnNames];
|
|
newNames[idx] = e.target.value;
|
|
setTemplateFieldColumnNames(newNames);
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<Label>설명 (선택)</Label>
|
|
<Textarea
|
|
value={templateFieldDescription}
|
|
onChange={(e) => setTemplateFieldDescription(e.target.value)}
|
|
placeholder="항목에 대한 설명"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Switch checked={templateFieldRequired} onCheckedChange={setTemplateFieldRequired} />
|
|
<Label>필수 항목</Label>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={handleClose}>취소</Button>
|
|
<Button onClick={() => {
|
|
setIsSubmitted(true);
|
|
const shouldValidate = templateFieldInputMode === 'custom' || editingTemplateFieldId || !setTemplateFieldInputMode;
|
|
if (shouldValidate && (isNameEmpty || isKeyEmpty)) return;
|
|
handleAddTemplateField();
|
|
setIsSubmitted(false);
|
|
}}>
|
|
{editingTemplateFieldId ? '수정' : '추가'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|