[feat]: Item Master 데이터 관리 기능 구현 및 타입 에러 수정

- ItemMasterDataManagement 컴포넌트 구조화 (tabs, dialogs, components 분리)
- HierarchyTab 타입 에러 수정 (BOMItem section_id, updated_at 추가)
- API 클라이언트 구현 (item-master.ts, 13개 엔드포인트)
- ItemMasterContext 구현 (상태 관리 및 데이터 흐름)
- 백엔드 요구사항 문서 작성 (CORS 설정, API 스펙 등)
- SSR 호환성 수정 (navigator API typeof window 체크)
- 미사용 변수 ESLint 에러 해결
- Context 리팩토링 (AuthContext, RootProvider 추가)
- API 유틸리티 추가 (error-handler, logger, transformers)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-11-23 16:10:27 +09:00
parent 63f5df7d7d
commit df3db155dd
69 changed files with 31467 additions and 4796 deletions

View File

@@ -0,0 +1,106 @@
'use client';
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 { toast } from 'sonner';
interface ColumnDialogProps {
isColumnDialogOpen: boolean;
setIsColumnDialogOpen: (open: boolean) => void;
editingColumnId: string | null;
setEditingColumnId: (id: string | null) => void;
columnName: string;
setColumnName: (name: string) => void;
columnKey: string;
setColumnKey: (key: string) => void;
textboxColumns: Array<{ id: string; name: string; key: string }>;
setTextboxColumns: React.Dispatch<React.SetStateAction<Array<{ id: string; name: string; key: string }>>>;
}
export function ColumnDialog({
isColumnDialogOpen,
setIsColumnDialogOpen,
editingColumnId,
setEditingColumnId,
columnName,
setColumnName,
columnKey,
setColumnKey,
textboxColumns,
setTextboxColumns,
}: ColumnDialogProps) {
const handleSubmit = () => {
if (!columnName.trim() || !columnKey.trim()) {
return toast.error('모든 필드를 입력해주세요');
}
if (editingColumnId) {
// 수정
setTextboxColumns(prev => prev.map(col =>
col.id === editingColumnId
? { ...col, name: columnName, key: columnKey }
: col
));
toast.success('컬럼이 수정되었습니다');
} else {
// 추가
setTextboxColumns(prev => [...prev, {
id: `col-${Date.now()}`,
name: columnName,
key: columnKey
}]);
toast.success('컬럼이 추가되었습니다');
}
setIsColumnDialogOpen(false);
setEditingColumnId(null);
setColumnName('');
setColumnKey('');
};
return (
<Dialog open={isColumnDialogOpen} onOpenChange={(open) => {
setIsColumnDialogOpen(open);
if (!open) {
setEditingColumnId(null);
setColumnName('');
setColumnKey('');
}
}}>
<DialogContent>
<DialogHeader>
<DialogTitle>{editingColumnId ? '컬럼 수정' : '컬럼 추가'}</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label> *</Label>
<Input
value={columnName}
onChange={(e) => setColumnName(e.target.value)}
placeholder="예: 가로"
/>
</div>
<div>
<Label> *</Label>
<Input
value={columnKey}
onChange={(e) => setColumnKey(e.target.value)}
placeholder="예: width"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsColumnDialogOpen(false)}></Button>
<Button onClick={handleSubmit}>
{editingColumnId ? '수정' : '추가'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,199 @@
'use client';
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { Badge } from '@/components/ui/badge';
import { Plus, Trash2 } from 'lucide-react';
import { toast } from 'sonner';
import type { OptionColumn } from '../types';
interface AttributeSubTab {
id: string;
label: string;
key: string;
order: number;
isDefault?: boolean;
}
interface ColumnManageDialogProps {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
managingColumnType: string | null;
attributeSubTabs: AttributeSubTab[];
attributeColumns: Record<string, OptionColumn[]>;
setAttributeColumns: React.Dispatch<React.SetStateAction<Record<string, OptionColumn[]>>>;
newColumnName: string;
setNewColumnName: (name: string) => void;
newColumnKey: string;
setNewColumnKey: (key: string) => void;
newColumnType: 'text' | 'number';
setNewColumnType: (type: 'text' | 'number') => void;
newColumnRequired: boolean;
setNewColumnRequired: (required: boolean) => void;
}
export function ColumnManageDialog({
isOpen,
setIsOpen,
managingColumnType,
attributeSubTabs,
attributeColumns,
setAttributeColumns,
newColumnName,
setNewColumnName,
newColumnKey,
setNewColumnKey,
newColumnType,
setNewColumnType,
newColumnRequired,
setNewColumnRequired,
}: ColumnManageDialogProps) {
return (
<Dialog open={isOpen} onOpenChange={(open) => {
setIsOpen(open);
if (!open) {
setNewColumnName('');
setNewColumnKey('');
setNewColumnType('text');
setNewColumnRequired(false);
}
}}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription>
{managingColumnType === 'units' && '단위'}
{managingColumnType === 'materials' && '재질'}
{managingColumnType === 'surface' && '표면처리'}
{managingColumnType && !['units', 'materials', 'surface'].includes(managingColumnType) &&
(attributeSubTabs.find(t => t.key === managingColumnType)?.label || '속성')}
{' '} (: 규격 // )
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 기존 칼럼 목록 */}
{managingColumnType && attributeColumns[managingColumnType]?.length > 0 && (
<div className="border rounded-lg p-4">
<h4 className="font-medium mb-3"> </h4>
<div className="space-y-2">
{attributeColumns[managingColumnType].map((column, idx) => (
<div key={column.id} className="flex items-center justify-between p-3 bg-gray-50 rounded">
<div className="flex items-center gap-3">
<Badge variant="outline">{idx + 1}</Badge>
<div>
<p className="font-medium">{column.name}</p>
<p className="text-xs text-muted-foreground">
: {column.key} | : {column.type === 'text' ? '텍스트' : '숫자'}
{column.required && ' | 필수'}
</p>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => {
if (managingColumnType) {
setAttributeColumns(prev => ({
...prev,
[managingColumnType]: prev[managingColumnType]?.filter(c => c.id !== column.id) || []
}));
}
}}
>
<Trash2 className="w-4 h-4 text-red-500" />
</Button>
</div>
))}
</div>
</div>
)}
{/* 새 칼럼 추가 폼 */}
<div className="border rounded-lg p-4 space-y-3">
<h4 className="font-medium"> </h4>
<div className="grid grid-cols-2 gap-3">
<div>
<Label> *</Label>
<Input
value={newColumnName}
onChange={(e) => setNewColumnName(e.target.value)}
placeholder="예: 속성, 값, 단위"
/>
</div>
<div>
<Label> () *</Label>
<Input
value={newColumnKey}
onChange={(e) => setNewColumnKey(e.target.value)}
placeholder="예: property, value, unit"
/>
</div>
<div>
<Label></Label>
<Select value={newColumnType} onValueChange={(value: 'text' | 'number') => setNewColumnType(value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text"></SelectItem>
<SelectItem value="number"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2 pt-6">
<Switch
checked={newColumnRequired}
onCheckedChange={setNewColumnRequired}
/>
<Label> </Label>
</div>
</div>
<Button
size="sm"
className="w-full"
onClick={() => {
if (!newColumnName.trim() || !newColumnKey.trim()) {
toast.error('칼럼명과 키를 입력해주세요');
return;
}
if (managingColumnType) {
const newColumn: OptionColumn = {
id: `col-${Date.now()}`,
name: newColumnName,
key: newColumnKey,
type: newColumnType,
required: newColumnRequired
};
setAttributeColumns(prev => ({
...prev,
[managingColumnType]: [...(prev[managingColumnType] || []), newColumn]
}));
// 입력 필드 초기화
setNewColumnName('');
setNewColumnKey('');
setNewColumnType('text');
setNewColumnRequired(false);
toast.success('칼럼이 추가되었습니다');
}
}}
>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
<DialogFooter>
<Button onClick={() => setIsOpen(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,408 @@
'use client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Plus, X, Edit, Trash2, Check } from 'lucide-react';
import { toast } from 'sonner';
import type { ItemPage, ItemSection, ItemMasterField } from '@/contexts/ItemMasterContext';
import { ConditionalDisplayUI, type ConditionalFieldConfig } from '../components/ConditionalDisplayUI';
// 텍스트박스 칼럼 타입 (단순 구조)
interface OptionColumn {
id: string;
name: string;
key: string;
}
const INPUT_TYPE_OPTIONS = [
{ value: 'textbox', label: '텍스트박스' },
{ value: 'dropdown', label: '드롭다운' },
{ value: 'checkbox', label: '체크박스' },
{ value: 'number', label: '숫자' },
{ value: 'date', label: '날짜' },
{ value: 'textarea', label: '텍스트영역' }
];
interface FieldDialogProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
editingFieldId: number | null;
setEditingFieldId: (id: number | null) => void;
fieldInputMode: 'custom' | 'master';
setFieldInputMode: (mode: 'custom' | 'master') => void;
showMasterFieldList: boolean;
setShowMasterFieldList: (show: boolean) => void;
selectedMasterFieldId: string;
setSelectedMasterFieldId: (id: string) => void;
textboxColumns: OptionColumn[];
setTextboxColumns: React.Dispatch<React.SetStateAction<OptionColumn[]>>;
newFieldConditionEnabled: boolean;
setNewFieldConditionEnabled: (enabled: boolean) => void;
newFieldConditionTargetType: 'field' | 'section';
setNewFieldConditionTargetType: (type: 'field' | 'section') => void;
newFieldConditionFields: ConditionalFieldConfig[];
setNewFieldConditionFields: React.Dispatch<React.SetStateAction<ConditionalFieldConfig[]>>;
newFieldConditionSections: string[];
setNewFieldConditionSections: React.Dispatch<React.SetStateAction<string[]>>;
tempConditionValue: string;
setTempConditionValue: (value: string) => void;
newFieldName: string;
setNewFieldName: (name: string) => void;
newFieldKey: string;
setNewFieldKey: (key: string) => void;
newFieldInputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea' | 'section';
setNewFieldInputType: (type: any) => void;
newFieldRequired: boolean;
setNewFieldRequired: (required: boolean) => void;
newFieldDescription: string;
setNewFieldDescription: (description: string) => void;
newFieldOptions: string;
setNewFieldOptions: (options: string) => void;
selectedSectionForField: ItemSection | null;
selectedPage: ItemPage | null;
itemMasterFields: ItemMasterField[];
handleAddField: () => void;
setIsColumnDialogOpen: (open: boolean) => void;
setEditingColumnId: (id: string | null) => void;
setColumnName: (name: string) => void;
setColumnKey: (key: string) => void;
}
export function FieldDialog({
isOpen,
onOpenChange,
editingFieldId,
setEditingFieldId,
fieldInputMode,
setFieldInputMode,
showMasterFieldList,
setShowMasterFieldList,
selectedMasterFieldId,
setSelectedMasterFieldId,
textboxColumns,
setTextboxColumns,
newFieldConditionEnabled,
setNewFieldConditionEnabled,
newFieldConditionTargetType,
setNewFieldConditionTargetType,
newFieldConditionFields,
setNewFieldConditionFields,
newFieldConditionSections,
setNewFieldConditionSections,
tempConditionValue,
setTempConditionValue,
newFieldName,
setNewFieldName,
newFieldKey,
setNewFieldKey,
newFieldInputType,
setNewFieldInputType,
newFieldRequired,
setNewFieldRequired,
newFieldDescription,
setNewFieldDescription,
newFieldOptions,
setNewFieldOptions,
selectedSectionForField,
selectedPage,
itemMasterFields,
handleAddField,
setIsColumnDialogOpen,
setEditingColumnId,
setColumnName,
setColumnKey,
}: FieldDialogProps) {
const handleClose = () => {
onOpenChange(false);
setEditingFieldId(null);
setFieldInputMode('custom');
setShowMasterFieldList(false);
setSelectedMasterFieldId('');
setTextboxColumns([]);
setNewFieldConditionEnabled(false);
setNewFieldConditionTargetType('field');
setNewFieldConditionFields([]);
setNewFieldConditionSections([]);
setTempConditionValue('');
// 핵심 입력 필드 초기화 (취소 시에도 이전 데이터 남지 않도록)
setNewFieldName('');
setNewFieldKey('');
setNewFieldInputType('textbox');
setNewFieldRequired(false);
setNewFieldOptions('');
setNewFieldDescription('');
};
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-2xl max-h-[90vh] flex flex-col p-0">
<DialogHeader className="sticky top-0 bg-white z-10 px-6 py-4 border-b">
<DialogTitle>{editingFieldId ? '항목 수정' : '항목 추가'}</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-4">
{/* 입력 모드 선택 (편집 시에는 표시 안 함) */}
{!editingFieldId && (
<div className="flex gap-2 p-1 bg-gray-100 rounded">
<Button
variant={fieldInputMode === 'custom' ? 'default' : 'ghost'}
size="sm"
onClick={() => setFieldInputMode('custom')}
className="flex-1"
>
</Button>
<Button
variant={fieldInputMode === 'master' ? 'default' : 'ghost'}
size="sm"
onClick={() => {
setFieldInputMode('master');
setShowMasterFieldList(true);
}}
className="flex-1"
>
</Button>
</div>
)}
{/* 마스터 항목 목록 */}
{fieldInputMode === 'master' && !editingFieldId && 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 === field.id
? 'bg-blue-50 border-blue-300'
: 'hover:bg-gray-50'
}`}
onClick={() => {
setSelectedMasterFieldId(field.id);
setNewFieldName(field.name);
setNewFieldKey(field.fieldKey);
setNewFieldInputType(field.property.inputType);
setNewFieldRequired(field.property.required);
setNewFieldDescription(field.description || '');
setNewFieldOptions(field.property.options?.join(', ') || '');
if (field.property.multiColumn && field.property.columnNames) {
setTextboxColumns(
field.property.columnNames.map((name, idx) => ({
id: `col-${idx}`,
name,
key: `column${idx + 1}`
}))
);
}
}}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium">{field.name}</span>
<Badge variant="outline" className="text-xs">
{INPUT_TYPE_OPTIONS.find(o => o.value === field.property.inputType)?.label}
</Badge>
{field.property.required && (
<Badge variant="destructive" className="text-xs"></Badge>
)}
</div>
{field.description && (
<p className="text-xs text-muted-foreground mt-1">{field.description}</p>
)}
{Array.isArray(field.category) && field.category.length > 0 && (
<div className="flex gap-1 mt-1">
{field.category.map((cat, idx) => (
<Badge key={idx} variant="secondary" className="text-xs">
{cat}
</Badge>
))}
</div>
)}
</div>
{selectedMasterFieldId === field.id && (
<Check className="h-5 w-5 text-blue-600" />
)}
</div>
</div>
))}
</div>
)}
</div>
)}
{/* 직접 입력 폼 */}
{(fieldInputMode === 'custom' || editingFieldId) && (
<>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<Label> *</Label>
<Input
value={newFieldName}
onChange={(e) => setNewFieldName(e.target.value)}
placeholder="예: 품목명"
/>
</div>
<div>
<Label> *</Label>
<Input
value={newFieldKey}
onChange={(e) => setNewFieldKey(e.target.value)}
placeholder="예: itemName"
/>
</div>
</div>
<div>
<Label> *</Label>
<Select value={newFieldInputType} onValueChange={(v: any) => setNewFieldInputType(v)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{INPUT_TYPE_OPTIONS.map(opt => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{newFieldInputType === 'dropdown' && (
<div>
<Label> </Label>
<Input
value={newFieldOptions}
onChange={(e) => setNewFieldOptions(e.target.value)}
placeholder="옵션1, 옵션2, 옵션3 (쉼표로 구분)"
/>
</div>
)}
{/* 텍스트박스 컬럼 관리 */}
{newFieldInputType === 'textbox' && (
<div className="border rounded p-3 space-y-3">
<div className="flex items-center justify-between">
<Label> </Label>
<Button
variant="outline"
size="sm"
onClick={() => {
setIsColumnDialogOpen(true);
setEditingColumnId(null);
setColumnName('');
setColumnKey('');
}}
>
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
{textboxColumns.length > 0 ? (
<div className="space-y-2">
{textboxColumns.map((col, index) => (
<div key={col.id} className="flex items-center gap-2 p-2 bg-gray-50 rounded">
<span className="text-sm flex-1">
{index + 1}. {col.name} ({col.key})
</span>
<Button
variant="ghost"
size="sm"
onClick={() => {
setEditingColumnId(col.id);
setColumnName(col.name);
setColumnKey(col.key);
setIsColumnDialogOpen(true);
}}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
setTextboxColumns(prev => prev.filter(c => c.id !== col.id));
toast.success('컬럼이 삭제되었습니다');
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground text-center py-2">
</p>
)}
</div>
)}
<div>
<Label> ()</Label>
<Textarea
value={newFieldDescription}
onChange={(e) => setNewFieldDescription(e.target.value)}
placeholder="항목에 대한 설명"
/>
</div>
<div className="flex items-center gap-2">
<Switch checked={newFieldRequired} onCheckedChange={setNewFieldRequired} />
<Label> </Label>
</div>
</>
)}
{/* 조건부 표시 설정 - 모든 입력방식에서 사용 가능 */}
{(fieldInputMode === 'custom' || editingFieldId) && (
<ConditionalDisplayUI
newFieldConditionEnabled={newFieldConditionEnabled}
setNewFieldConditionEnabled={setNewFieldConditionEnabled}
newFieldConditionTargetType={newFieldConditionTargetType}
setNewFieldConditionTargetType={setNewFieldConditionTargetType}
newFieldConditionFields={newFieldConditionFields}
setNewFieldConditionFields={setNewFieldConditionFields}
tempConditionValue={tempConditionValue}
setTempConditionValue={setTempConditionValue}
newFieldKey={newFieldKey}
newFieldInputType={newFieldInputType}
selectedPage={selectedPage}
selectedSectionForField={selectedSectionForField}
editingFieldId={editingFieldId}
INPUT_TYPE_OPTIONS={INPUT_TYPE_OPTIONS}
/>
)}
</div>
<DialogFooter className="shrink-0 bg-white z-10 px-6 py-4 border-t">
<Button variant="outline" onClick={handleClose}></Button>
<Button onClick={handleAddField}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,628 @@
'use client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import { Badge } from '@/components/ui/badge';
import { Drawer, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle } from '@/components/ui/drawer';
import { Plus, X, Edit, Trash2, Check } from 'lucide-react';
import { toast } from 'sonner';
import type { ItemPage, ItemSection, ItemMasterField } from '@/contexts/ItemMasterContext';
interface OptionColumn {
id: string;
name: string;
key: string;
}
const INPUT_TYPE_OPTIONS = [
{ value: 'textbox', label: '텍스트박스' },
{ value: 'dropdown', label: '드롭다운' },
{ value: 'checkbox', label: '체크박스' },
{ value: 'number', label: '숫자' },
{ value: 'date', label: '날짜' },
{ value: 'textarea', label: '텍스트영역' }
];
interface FieldDrawerProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
editingFieldId: number | null;
setEditingFieldId: (id: number | null) => void;
fieldInputMode: 'custom' | 'master';
setFieldInputMode: (mode: 'custom' | 'master') => void;
showMasterFieldList: boolean;
setShowMasterFieldList: (show: boolean) => void;
selectedMasterFieldId: string;
setSelectedMasterFieldId: (id: string) => void;
textboxColumns: OptionColumn[];
setTextboxColumns: React.Dispatch<React.SetStateAction<OptionColumn[]>>;
newFieldConditionEnabled: boolean;
setNewFieldConditionEnabled: (enabled: boolean) => void;
newFieldConditionTargetType: 'field' | 'section';
setNewFieldConditionTargetType: (type: 'field' | 'section') => void;
newFieldConditionFields: Array<{ fieldKey: string; expectedValue: string }>;
setNewFieldConditionFields: React.Dispatch<React.SetStateAction<Array<{ fieldKey: string; expectedValue: string }>>>;
newFieldConditionSections: string[];
setNewFieldConditionSections: React.Dispatch<React.SetStateAction<string[]>>;
tempConditionValue: string;
setTempConditionValue: (value: string) => void;
newFieldName: string;
setNewFieldName: (name: string) => void;
newFieldKey: string;
setNewFieldKey: (key: string) => void;
newFieldInputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
setNewFieldInputType: (type: any) => void;
newFieldRequired: boolean;
setNewFieldRequired: (required: boolean) => void;
newFieldDescription: string;
setNewFieldDescription: (description: string) => void;
newFieldOptions: string;
setNewFieldOptions: (options: string) => void;
selectedSectionForField: ItemSection | null;
selectedPage: ItemPage | null;
itemMasterFields: ItemMasterField[];
handleAddField: () => void;
setIsColumnDialogOpen: (open: boolean) => void;
setEditingColumnId: (id: string | null) => void;
setColumnName: (name: string) => void;
setColumnKey: (key: string) => void;
}
export function FieldDrawer({
isOpen,
onOpenChange,
editingFieldId,
setEditingFieldId,
fieldInputMode,
setFieldInputMode,
showMasterFieldList,
setShowMasterFieldList,
selectedMasterFieldId,
setSelectedMasterFieldId,
textboxColumns,
setTextboxColumns,
newFieldConditionEnabled,
setNewFieldConditionEnabled,
newFieldConditionTargetType,
setNewFieldConditionTargetType,
newFieldConditionFields,
setNewFieldConditionFields,
newFieldConditionSections,
setNewFieldConditionSections,
tempConditionValue,
setTempConditionValue,
newFieldName,
setNewFieldName,
newFieldKey,
setNewFieldKey,
newFieldInputType,
setNewFieldInputType,
newFieldRequired,
setNewFieldRequired,
newFieldDescription,
setNewFieldDescription,
newFieldOptions,
setNewFieldOptions,
selectedSectionForField,
selectedPage,
itemMasterFields,
handleAddField,
setIsColumnDialogOpen,
setEditingColumnId,
setColumnName,
setColumnKey
}: FieldDrawerProps) {
const handleClose = () => {
onOpenChange(false);
setEditingFieldId(null);
setFieldInputMode('custom');
setShowMasterFieldList(false);
setSelectedMasterFieldId('');
setTextboxColumns([]);
setNewFieldConditionEnabled(false);
setNewFieldConditionTargetType('field');
setNewFieldConditionFields([]);
setNewFieldConditionSections([]);
setTempConditionValue('');
};
return (
<Drawer open={isOpen} onOpenChange={handleClose}>
<DrawerContent className="max-h-[90vh] flex flex-col">
<DrawerHeader className="px-4 py-3 border-b">
<DrawerTitle>{editingFieldId ? '항목 수정' : '항목 추가'}</DrawerTitle>
<DrawerDescription>
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-4">
{/* 입력 모드 선택 (편집 시에는 표시 안 함) */}
{!editingFieldId && (
<div className="flex gap-2 p-1 bg-gray-100 rounded">
<Button
variant={fieldInputMode === 'custom' ? 'default' : 'ghost'}
size="sm"
onClick={() => setFieldInputMode('custom')}
className="flex-1"
>
</Button>
<Button
variant={fieldInputMode === 'master' ? 'default' : 'ghost'}
size="sm"
onClick={() => {
setFieldInputMode('master');
setShowMasterFieldList(true);
}}
className="flex-1"
>
</Button>
</div>
)}
{/* 마스터 항목 목록 */}
{fieldInputMode === 'master' && !editingFieldId && 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 === field.id
? 'bg-blue-50 border-blue-300'
: 'hover:bg-gray-50'
}`}
onClick={() => {
setSelectedMasterFieldId(field.id);
setNewFieldName(field.name);
setNewFieldKey(field.fieldKey);
setNewFieldInputType(field.property.inputType);
setNewFieldRequired(field.property.required);
setNewFieldDescription(field.description || '');
setNewFieldOptions(field.property.options?.join(', ') || '');
if (field.property.multiColumn && field.property.columnNames) {
setTextboxColumns(
field.property.columnNames.map((name, idx) => ({
id: `col-${idx}`,
name,
key: `column${idx + 1}`
}))
);
}
}}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium">{field.name}</span>
<Badge variant="outline" className="text-xs">
{INPUT_TYPE_OPTIONS.find(o => o.value === field.property.inputType)?.label}
</Badge>
{field.property.required && (
<Badge variant="destructive" className="text-xs"></Badge>
)}
</div>
{field.description && (
<p className="text-xs text-muted-foreground mt-1">{field.description}</p>
)}
{Array.isArray(field.category) && field.category.length > 0 && (
<div className="flex gap-1 mt-1">
{field.category.map((cat, idx) => (
<Badge key={idx} variant="secondary" className="text-xs">
{cat}
</Badge>
))}
</div>
)}
</div>
{selectedMasterFieldId === field.id && (
<Check className="h-5 w-5 text-blue-600" />
)}
</div>
</div>
))}
</div>
)}
</div>
)}
{/* 직접 입력 폼 */}
{(fieldInputMode === 'custom' || editingFieldId) && (
<>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<Label> *</Label>
<Input
value={newFieldName}
onChange={(e) => setNewFieldName(e.target.value)}
placeholder="예: 품목명"
/>
</div>
<div>
<Label> *</Label>
<Input
value={newFieldKey}
onChange={(e) => setNewFieldKey(e.target.value)}
placeholder="예: itemName"
/>
</div>
</div>
<div>
<Label> *</Label>
<Select value={newFieldInputType} onValueChange={(v: any) => setNewFieldInputType(v)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{INPUT_TYPE_OPTIONS.map(opt => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{newFieldInputType === 'dropdown' && (
<div>
<Label> </Label>
<Input
value={newFieldOptions}
onChange={(e) => setNewFieldOptions(e.target.value)}
placeholder="옵션1, 옵션2, 옵션3 (쉼표로 구분)"
/>
</div>
)}
{/* 텍스트박스 컬럼 관리 */}
{newFieldInputType === 'textbox' && (
<div className="border rounded p-3 space-y-3">
<div className="flex items-center justify-between">
<Label> </Label>
<Button
variant="outline"
size="sm"
onClick={() => {
setIsColumnDialogOpen(true);
setEditingColumnId(null);
setColumnName('');
setColumnKey('');
}}
>
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
{textboxColumns.length > 0 ? (
<div className="space-y-2">
{textboxColumns.map((col, index) => (
<div key={col.id} className="flex items-center gap-2 p-2 bg-gray-50 rounded">
<span className="text-sm flex-1">
{index + 1}. {col.name} ({col.key})
</span>
<Button
variant="ghost"
size="sm"
onClick={() => {
setEditingColumnId(col.id);
setColumnName(col.name);
setColumnKey(col.key);
setIsColumnDialogOpen(true);
}}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
setTextboxColumns(prev => prev.filter(c => c.id !== col.id));
toast.success('컬럼이 삭제되었습니다');
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground text-center py-2">
</p>
)}
</div>
)}
<div>
<Label> ()</Label>
<Textarea
value={newFieldDescription}
onChange={(e) => setNewFieldDescription(e.target.value)}
placeholder="항목에 대한 설명"
/>
</div>
<div className="flex items-center gap-2">
<Switch checked={newFieldRequired} onCheckedChange={setNewFieldRequired} />
<Label> </Label>
</div>
</>
)}
{/* 조건부 표시 설정 - 모든 입력방식에서 사용 가능 */}
{(fieldInputMode === 'custom' || editingFieldId) && (
<div className="border-t pt-4 space-y-3">
<div className="space-y-2">
<div className="flex items-center gap-2">
<Switch checked={newFieldConditionEnabled} onCheckedChange={setNewFieldConditionEnabled} />
<Label className="text-base"> </Label>
</div>
<p className="text-xs text-muted-foreground pl-8">
/ ( )
</p>
</div>
{newFieldConditionEnabled && selectedSectionForField && (
<div className="space-y-4 pl-6 pt-3 border-l-2 border-blue-200">
{/* 대상 타입 선택 */}
<div className="space-y-2 bg-blue-50 p-3 rounded">
<Label className="text-sm font-semibold"> ?</Label>
<div className="flex gap-4 pl-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
checked={newFieldConditionTargetType === 'field'}
onChange={() => setNewFieldConditionTargetType('field')}
className="cursor-pointer"
/>
<span className="text-sm"> ( )</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
checked={newFieldConditionTargetType === 'section'}
onChange={() => setNewFieldConditionTargetType('section')}
className="cursor-pointer"
/>
<span className="text-sm"> </span>
</label>
</div>
</div>
{/* 일반항목용 조건 설정 */}
{newFieldConditionTargetType === 'field' && (
<div className="space-y-3">
<div className="bg-yellow-50 p-3 rounded border border-yellow-200">
<p className="text-xs text-yellow-800">
<strong>💡 :</strong><br/>
1. (: "제품", "원자재") <br/>
2. , <br/>
3. "조건부 표시" <br/>
4.
</p>
</div>
<div>
<Label className="text-sm font-semibold"> ( )</Label>
<p className="text-xs text-muted-foreground mt-1">
{newFieldInputType === 'dropdown' && '드롭다운 옵션값을 입력하세요'}
{newFieldInputType === 'checkbox' && '체크박스 상태값(true/false)을 입력하세요'}
{newFieldInputType === 'textbox' && '텍스트 값을 입력하세요 (예: "제품", "부품")'}
{newFieldInputType === 'number' && '숫자 값을 입력하세요 (예: 100, 200)'}
{newFieldInputType === 'date' && '날짜 값을 입력하세요 (예: 2025-01-01)'}
{newFieldInputType === 'textarea' && '텍스트 값을 입력하세요'}
</p>
</div>
{/* 추가된 조건 목록 */}
{newFieldConditionFields.length > 0 && (
<div className="space-y-2">
{newFieldConditionFields.map((condition, index) => (
<div key={index} className="flex items-center gap-2 p-3 bg-blue-50 rounded border border-blue-200">
<div className="flex-1">
<span className="text-sm font-medium text-blue-900">
"{condition.expectedValue}"
</span>
<p className="text-xs text-blue-700 mt-1">
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => {
setNewFieldConditionFields(prev => prev.filter((_, i) => i !== index));
toast.success('조건이 제거되었습니다.');
}}
className="h-8 w-8 p-0"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
{/* 새 조건 추가 */}
<div className="flex gap-2">
<Input
value={tempConditionValue}
onChange={(e) => setTempConditionValue(e.target.value)}
placeholder={
newFieldInputType === 'dropdown' ? "예: 제품, 부품, 원자재..." :
newFieldInputType === 'checkbox' ? "예: true 또는 false" :
newFieldInputType === 'number' ? "예: 100" :
newFieldInputType === 'date' ? "예: 2025-01-01" :
"예: 값을 입력하세요"
}
className="flex-1"
/>
<Button
variant="outline"
size="sm"
onClick={() => {
if (tempConditionValue) {
setNewFieldConditionFields(prev => [...prev, {
fieldKey: newFieldKey, // 현재 항목 자신의 키를 사용
expectedValue: tempConditionValue
}]);
setTempConditionValue('');
toast.success('조건값이 추가되었습니다.');
} else {
toast.error('조건값을 입력해주세요.');
}
}}
>
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
</div>
)}
{/* 섹션용 조건 설정 */}
{newFieldConditionTargetType === 'section' && selectedPage && (
<div className="space-y-3">
<div className="bg-yellow-50 p-3 rounded border border-yellow-200">
<p className="text-xs text-yellow-800">
<strong>💡 :</strong><br/>
1. (: "제품", "부품")<br/>
2. <br/>
3. /
</p>
</div>
{/* 조건값 추가 */}
<div>
<Label className="text-sm font-semibold"> </Label>
<div className="flex gap-2 mt-2">
<Input
value={tempConditionValue}
onChange={(e) => setTempConditionValue(e.target.value)}
placeholder={
newFieldInputType === 'dropdown' ? "예: 제품, 부품, 원자재..." :
newFieldInputType === 'checkbox' ? "예: true 또는 false" :
newFieldInputType === 'number' ? "예: 100" :
newFieldInputType === 'date' ? "예: 2025-01-01" :
"예: 값을 입력하세요"
}
className="flex-1"
/>
<Button
variant="outline"
size="sm"
onClick={() => {
if (tempConditionValue) {
if (!newFieldConditionFields.find(f => f.expectedValue === tempConditionValue)) {
setNewFieldConditionFields(prev => [...prev, {
fieldKey: newFieldKey,
expectedValue: tempConditionValue
}]);
setTempConditionValue('');
toast.success('조건값이 추가되었습니다.');
} else {
toast.error('이미 추가된 조건값입니다.');
}
} else {
toast.error('조건값을 입력해주세요.');
}
}}
>
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
</div>
{/* 조건값 목록 표시 */}
{newFieldConditionFields.length > 0 && (
<div className="space-y-2">
<Label className="text-xs text-muted-foreground"> :</Label>
<div className="flex flex-wrap gap-2">
{newFieldConditionFields.map((condition, index) => (
<Badge key={index} variant="secondary" className="gap-1">
{condition.expectedValue}
<button
onClick={() => {
setNewFieldConditionFields(prev => prev.filter((_, i) => i !== index));
toast.success('조건값이 제거되었습니다.');
}}
className="ml-1 hover:text-red-500"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
</div>
)}
{/* 섹션 선택 */}
<div className="space-y-2">
<Label className="text-sm font-semibold"> :</Label>
<p className="text-xs text-muted-foreground">
</p>
<div className="space-y-2 max-h-60 overflow-y-auto">
{selectedPage.sections
.filter(section => section.type !== 'bom')
.map(section => (
<label key={section.id} className="flex items-center gap-2 p-2 bg-muted rounded cursor-pointer hover:bg-muted/80">
<input
type="checkbox"
checked={newFieldConditionSections.includes(section.id)}
onChange={(e) => {
if (e.target.checked) {
setNewFieldConditionSections(prev => [...prev, section.id]);
} else {
setNewFieldConditionSections(prev => prev.filter(id => id !== section.id));
}
}}
className="cursor-pointer"
/>
<span className="flex-1 text-sm">{section.title}</span>
</label>
))}
</div>
{newFieldConditionSections.length > 0 && (
<div className="text-sm text-blue-600 font-medium mt-2">
{newFieldConditionSections.length}
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
)}
</div>
<DrawerFooter className="px-4 py-3 border-t flex-row gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)} className="flex-1"></Button>
<Button onClick={handleAddField} className="flex-1"></Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}

View File

@@ -0,0 +1,101 @@
'use client';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Package, Folder } from 'lucide-react';
import type { SectionTemplate } from '@/contexts/ItemMasterContext';
interface LoadTemplateDialogProps {
isLoadTemplateDialogOpen: boolean;
setIsLoadTemplateDialogOpen: (open: boolean) => void;
sectionTemplates: SectionTemplate[];
selectedTemplateId: string | null;
setSelectedTemplateId: (id: string | null) => void;
handleLoadTemplate: () => void;
}
const ITEM_TYPE_OPTIONS = [
{ value: 'product', label: '제품' },
{ value: 'part', label: '부품' },
{ value: 'material', label: '자재' },
{ value: 'assembly', label: '조립품' },
];
export function LoadTemplateDialog({
isLoadTemplateDialogOpen,
setIsLoadTemplateDialogOpen,
sectionTemplates,
selectedTemplateId,
setSelectedTemplateId,
handleLoadTemplate,
}: LoadTemplateDialogProps) {
return (
<Dialog open={isLoadTemplateDialogOpen} onOpenChange={(open) => {
setIsLoadTemplateDialogOpen(open);
if (!open) {
setSelectedTemplateId(null);
}
}}>
<DialogContent className="max-w-[95vw] sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{sectionTemplates.length === 0 ? (
<div className="text-center py-8">
<p className="text-muted-foreground"> </p>
</div>
) : (
<div className="space-y-2 max-h-96 overflow-y-auto">
{sectionTemplates.map((template) => (
<div
key={template.id}
onClick={() => setSelectedTemplateId(String(template.id))}
className={`p-4 border rounded-lg cursor-pointer transition-colors ${
selectedTemplateId === String(template.id)
? 'border-blue-500 bg-blue-50 dark:bg-blue-950'
: 'hover:bg-gray-50 dark:hover:bg-gray-800'
}`}
>
<div className="flex items-start gap-3">
<div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-900">
{template.section_type === 'BOM' ? (
<Package className="w-5 h-5 text-blue-600 dark:text-blue-400" />
) : (
<Folder className="w-5 h-5 text-blue-600 dark:text-blue-400" />
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium">{template.template_name}</h4>
<Badge variant={template.section_type === 'BOM' ? 'default' : 'secondary'}>
{template.section_type}
</Badge>
</div>
{template.description && (
<p className="text-sm text-muted-foreground mb-2">{template.description}</p>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsLoadTemplateDialogOpen(false)}></Button>
<Button
onClick={handleLoadTemplate}
disabled={!selectedTemplateId}
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,262 @@
'use client';
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';
const INPUT_TYPE_OPTIONS = [
{ value: 'textbox', label: '텍스트 입력' },
{ value: 'number', label: '숫자 입력' },
{ value: 'dropdown', label: '드롭다운' },
{ value: 'checkbox', label: '체크박스' },
{ value: 'date', label: '날짜' },
{ value: 'textarea', label: '긴 텍스트' },
];
interface MasterFieldDialogProps {
isMasterFieldDialogOpen: boolean;
setIsMasterFieldDialogOpen: (open: boolean) => void;
editingMasterFieldId: number | null;
setEditingMasterFieldId: (id: number | null) => void;
newMasterFieldName: string;
setNewMasterFieldName: (name: string) => void;
newMasterFieldKey: string;
setNewMasterFieldKey: (key: string) => void;
newMasterFieldInputType: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
setNewMasterFieldInputType: (type: any) => void;
newMasterFieldRequired: boolean;
setNewMasterFieldRequired: (required: boolean) => void;
newMasterFieldCategory: string;
setNewMasterFieldCategory: (category: string) => void;
newMasterFieldDescription: string;
setNewMasterFieldDescription: (description: string) => void;
newMasterFieldOptions: string;
setNewMasterFieldOptions: (options: string) => void;
newMasterFieldAttributeType: 'custom' | 'unit' | 'material' | 'surface';
setNewMasterFieldAttributeType: (type: 'custom' | 'unit' | 'material' | 'surface') => void;
newMasterFieldMultiColumn: boolean;
setNewMasterFieldMultiColumn: (multi: boolean) => void;
newMasterFieldColumnCount: number;
setNewMasterFieldColumnCount: (count: number) => void;
newMasterFieldColumnNames: string[];
setNewMasterFieldColumnNames: (names: string[]) => void;
handleUpdateMasterField: () => void;
handleAddMasterField: () => void;
}
export function MasterFieldDialog({
isMasterFieldDialogOpen,
setIsMasterFieldDialogOpen,
editingMasterFieldId,
setEditingMasterFieldId,
newMasterFieldName,
setNewMasterFieldName,
newMasterFieldKey,
setNewMasterFieldKey,
newMasterFieldInputType,
setNewMasterFieldInputType,
newMasterFieldRequired,
setNewMasterFieldRequired,
newMasterFieldCategory,
setNewMasterFieldCategory,
newMasterFieldDescription,
setNewMasterFieldDescription,
newMasterFieldOptions,
setNewMasterFieldOptions,
newMasterFieldAttributeType,
setNewMasterFieldAttributeType,
newMasterFieldMultiColumn,
setNewMasterFieldMultiColumn,
newMasterFieldColumnCount,
setNewMasterFieldColumnCount,
newMasterFieldColumnNames,
setNewMasterFieldColumnNames,
handleUpdateMasterField,
handleAddMasterField,
}: MasterFieldDialogProps) {
return (
<Dialog open={isMasterFieldDialogOpen} onOpenChange={(open) => {
setIsMasterFieldDialogOpen(open);
if (!open) {
setEditingMasterFieldId(null);
setNewMasterFieldName('');
setNewMasterFieldKey('');
setNewMasterFieldInputType('textbox');
setNewMasterFieldRequired(false);
setNewMasterFieldCategory('공통');
setNewMasterFieldDescription('');
setNewMasterFieldOptions('');
setNewMasterFieldAttributeType('custom');
setNewMasterFieldMultiColumn(false);
setNewMasterFieldColumnCount(2);
setNewMasterFieldColumnNames(['컬럼1', '컬럼2']);
}
}}>
<DialogContent className="max-w-[95vw] sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{editingMasterFieldId ? '마스터 항목 수정' : '마스터 항목 추가'}</DialogTitle>
<DialogDescription>
릿
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<Label> *</Label>
<Input
value={newMasterFieldName}
onChange={(e) => setNewMasterFieldName(e.target.value)}
placeholder="예: 품목명"
/>
</div>
<div>
<Label> *</Label>
<Input
value={newMasterFieldKey}
onChange={(e) => setNewMasterFieldKey(e.target.value)}
placeholder="예: itemName"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label> *</Label>
<Select value={newMasterFieldInputType} onValueChange={(v: any) => setNewMasterFieldInputType(v)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{INPUT_TYPE_OPTIONS.map(opt => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="flex items-center gap-2">
<Switch checked={newMasterFieldRequired} onCheckedChange={setNewMasterFieldRequired} />
<Label> </Label>
</div>
<div>
<Label></Label>
<Textarea
value={newMasterFieldDescription}
onChange={(e) => setNewMasterFieldDescription(e.target.value)}
placeholder="항목에 대한 설명"
/>
<p className="text-xs text-gray-500 mt-1">* []</p>
</div>
{(newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') && (
<div className="space-y-4 p-4 border rounded-lg bg-gray-50">
<div className="flex items-center gap-2">
<Switch
checked={newMasterFieldMultiColumn}
onCheckedChange={(checked) => {
setNewMasterFieldMultiColumn(checked);
if (!checked) {
setNewMasterFieldColumnCount(2);
setNewMasterFieldColumnNames(['컬럼1', '컬럼2']);
}
}}
/>
<Label> </Label>
</div>
<p className="text-xs text-gray-500">
(: 규격 - , , )
</p>
{newMasterFieldMultiColumn && (
<div className="space-y-4 pt-4 border-t">
<div>
<Label> </Label>
<Input
type="number"
min="2"
max="10"
value={newMasterFieldColumnCount}
onChange={(e) => {
const count = parseInt(e.target.value) || 2;
setNewMasterFieldColumnCount(count);
// 컬럼 개수에 맞게 이름 배열 조정
const newNames = Array.from({ length: count }, (_, i) =>
newMasterFieldColumnNames[i] || `컬럼${i + 1}`
);
setNewMasterFieldColumnNames(newNames);
}}
placeholder="컬럼 개수 (2~10)"
/>
</div>
<div>
<Label> </Label>
<div className="space-y-2 mt-2">
{Array.from({ length: newMasterFieldColumnCount }, (_, i) => (
<Input
key={i}
value={newMasterFieldColumnNames[i] || ''}
onChange={(e) => {
const newNames = [...newMasterFieldColumnNames];
newNames[i] = e.target.value;
setNewMasterFieldColumnNames(newNames);
}}
placeholder={`${i + 1}번째 컬럼 이름`}
/>
))}
</div>
<p className="text-xs text-gray-500 mt-2">
예시: 가로, , / , / ,
</p>
</div>
</div>
)}
</div>
)}
{newMasterFieldInputType === 'dropdown' && (
<div className="space-y-4">
<div>
<div className="flex items-center justify-between mb-2">
<Label> </Label>
{newMasterFieldAttributeType !== 'custom' && (
<Badge variant="secondary" className="text-xs">
{newMasterFieldAttributeType === 'unit' ? '단위' :
newMasterFieldAttributeType === 'material' ? '재질' : '표면처리'}
</Badge>
)}
</div>
<Textarea
value={newMasterFieldOptions}
onChange={(e) => setNewMasterFieldOptions(e.target.value)}
placeholder="제품,부품,원자재 (쉼표로 구분)"
disabled={newMasterFieldAttributeType !== 'custom'}
className="min-h-[80px]"
/>
<p className="text-xs text-gray-500 mt-1">
{newMasterFieldAttributeType === 'custom'
? '쉼표(,)로 구분하여 입력하세요'
: '속성 탭에서 옵션을 추가/삭제하면 자동으로 반영됩니다'
}
</p>
</div>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsMasterFieldDialogOpen(false)}></Button>
<Button onClick={editingMasterFieldId ? handleUpdateMasterField : handleAddMasterField}>
{editingMasterFieldId ? '수정' : '추가'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,222 @@
'use client';
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
interface OptionColumn {
id: string;
name: string;
key: string;
type: string;
required: boolean;
}
interface AttributeSubTab {
id: string;
label: string;
key: string;
order: number;
isDefault?: boolean;
}
interface OptionDialogProps {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
newOptionValue: string;
setNewOptionValue: (value: string) => void;
newOptionLabel: string;
setNewOptionLabel: (label: string) => void;
newOptionColumnValues: Record<string, string>;
setNewOptionColumnValues: (values: Record<string, string>) => void;
newOptionInputType: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
setNewOptionInputType: (type: any) => void;
newOptionRequired: boolean;
setNewOptionRequired: (required: boolean) => void;
newOptionOptions: string;
setNewOptionOptions: (options: string) => void;
newOptionPlaceholder: string;
setNewOptionPlaceholder: (placeholder: string) => void;
newOptionDefaultValue: string;
setNewOptionDefaultValue: (defaultValue: string) => void;
editingOptionType: string | null;
attributeSubTabs: AttributeSubTab[];
attributeColumns: Record<string, OptionColumn[]>;
handleAddOption: () => void;
}
export function OptionDialog({
isOpen,
setIsOpen,
newOptionValue,
setNewOptionValue,
newOptionLabel,
setNewOptionLabel,
newOptionColumnValues,
setNewOptionColumnValues,
newOptionInputType,
setNewOptionInputType,
newOptionRequired,
setNewOptionRequired,
newOptionOptions,
setNewOptionOptions,
newOptionPlaceholder,
setNewOptionPlaceholder,
newOptionDefaultValue,
setNewOptionDefaultValue,
editingOptionType,
attributeSubTabs,
attributeColumns,
handleAddOption,
}: OptionDialogProps) {
return (
<Dialog open={isOpen} onOpenChange={(open) => {
setIsOpen(open);
if (!open) {
setNewOptionValue('');
setNewOptionLabel('');
setNewOptionColumnValues({});
setNewOptionInputType('textbox');
setNewOptionRequired(false);
setNewOptionOptions('');
setNewOptionPlaceholder('');
setNewOptionDefaultValue('');
}
}}>
<DialogContent className="max-w-[95vw] sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription>
{editingOptionType === 'unit' && '단위'}
{editingOptionType === 'material' && '재질'}
{editingOptionType === 'surface' && '표면처리'}
{editingOptionType && !['unit', 'material', 'surface'].includes(editingOptionType) &&
(attributeSubTabs.find(t => t.key === editingOptionType)?.label || '속성')}
{' '} . .
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 기본 정보 */}
<div className="border rounded-lg p-4 space-y-3 bg-blue-50">
<h4 className="font-medium text-sm"> </h4>
<div className="grid grid-cols-2 gap-3">
<div>
<Label> (Value) *</Label>
<Input
value={newOptionValue}
onChange={(e) => setNewOptionValue(e.target.value)}
placeholder="예: kg, stainless"
/>
</div>
<div>
<Label> () *</Label>
<Input
value={newOptionLabel}
onChange={(e) => setNewOptionLabel(e.target.value)}
placeholder="예: 킬로그램, 스테인리스"
/>
</div>
</div>
</div>
{/* 입력 방식 설정 */}
<div className="border rounded-lg p-4 space-y-3">
<h4 className="font-medium text-sm"> </h4>
<div>
<Label> *</Label>
<Select value={newOptionInputType} onValueChange={(v: any) => setNewOptionInputType(v)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="textbox"></SelectItem>
<SelectItem value="number"></SelectItem>
<SelectItem value="dropdown"></SelectItem>
<SelectItem value="checkbox"></SelectItem>
<SelectItem value="date"></SelectItem>
<SelectItem value="textarea"></SelectItem>
</SelectContent>
</Select>
</div>
{newOptionInputType === 'dropdown' && (
<div>
<Label className="flex items-center gap-1">
<span className="text-red-500">*</span>
</Label>
<Input
value={newOptionOptions}
onChange={(e) => setNewOptionOptions(e.target.value)}
placeholder="옵션1, 옵션2, 옵션3 (쉼표로 구분)"
/>
<p className="text-xs text-muted-foreground mt-1">
</p>
</div>
)}
<div>
<Label> ()</Label>
<Input
value={newOptionPlaceholder}
onChange={(e) => setNewOptionPlaceholder(e.target.value)}
placeholder="예: 값을 입력하세요"
/>
</div>
<div>
<Label> ()</Label>
<Input
value={newOptionDefaultValue}
onChange={(e) => setNewOptionDefaultValue(e.target.value)}
placeholder={
newOptionInputType === 'checkbox' ? 'true 또는 false' :
newOptionInputType === 'number' ? '숫자' :
'기본값'
}
/>
</div>
<div className="flex items-center gap-2">
<Switch checked={newOptionRequired} onCheckedChange={setNewOptionRequired} />
<Label> </Label>
</div>
</div>
{/* 추가 칼럼 (기존 칼럼 시스템과 호환) */}
{editingOptionType && attributeColumns[editingOptionType]?.length > 0 && (
<div className="border rounded-lg p-4 space-y-3">
<h4 className="font-medium text-sm"> </h4>
<div className="grid grid-cols-2 gap-4">
{attributeColumns[editingOptionType].map((column) => (
<div key={column.id}>
<Label className="flex items-center gap-1">
{column.name}
{column.required && <span className="text-red-500">*</span>}
</Label>
<Input
type={column.type === 'number' ? 'number' : 'text'}
value={newOptionColumnValues[column.key] || ''}
onChange={(e) => setNewOptionColumnValues({
...newOptionColumnValues,
[column.key]: e.target.value
})}
placeholder={`${column.name} 입력`}
/>
</div>
))}
</div>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsOpen(false)}></Button>
<Button onClick={handleAddOption}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,78 @@
'use client';
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
const ITEM_TYPE_OPTIONS = [
{ value: 'product', label: '제품' },
{ value: 'part', label: '부품' },
{ value: 'material', label: '자재' },
{ value: 'assembly', label: '조립품' },
];
interface PageDialogProps {
isPageDialogOpen: boolean;
setIsPageDialogOpen: (open: boolean) => void;
newPageName: string;
setNewPageName: (name: string) => void;
newPageItemType: 'FG' | 'PT' | 'SM' | 'RM' | 'CS';
setNewPageItemType: React.Dispatch<React.SetStateAction<'FG' | 'PT' | 'SM' | 'RM' | 'CS'>>;
handleAddPage: () => void;
}
export function PageDialog({
isPageDialogOpen,
setIsPageDialogOpen,
newPageName,
setNewPageName,
newPageItemType,
setNewPageItemType,
handleAddPage,
}: PageDialogProps) {
return (
<Dialog open={isPageDialogOpen} onOpenChange={(open) => {
setIsPageDialogOpen(open);
if (!open) {
setNewPageName('');
setNewPageItemType('FG');
}
}}>
<DialogContent>
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription> </DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label> *</Label>
<Input
value={newPageName}
onChange={(e) => setNewPageName(e.target.value)}
placeholder="예: 품목 등록"
/>
</div>
<div>
<Label> *</Label>
<Select value={newPageItemType} onValueChange={(v: any) => setNewPageItemType(v)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{ITEM_TYPE_OPTIONS.map(opt => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsPageDialogOpen(false)}></Button>
<Button onClick={handleAddPage}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,71 @@
'use client';
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 { toast } from 'sonner';
interface PathEditDialogProps {
editingPathPageId: number | null;
setEditingPathPageId: (id: number | null) => void;
editingAbsolutePath: string;
setEditingAbsolutePath: (path: string) => void;
updateItemPage: (id: number, updates: any) => void;
trackChange: (type: 'pages' | 'sections' | 'fields' | 'masterFields' | 'attributes' | 'sectionTemplates', id: string, action: 'add' | 'update', data: any, attributeType?: string) => void;
}
export function PathEditDialog({
editingPathPageId,
setEditingPathPageId,
editingAbsolutePath,
setEditingAbsolutePath,
updateItemPage,
trackChange,
}: PathEditDialogProps) {
return (
<Dialog open={editingPathPageId !== null} onOpenChange={(open) => {
if (!open) {
setEditingPathPageId(null);
setEditingAbsolutePath('');
}
}}>
<DialogContent>
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription> (: //)</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label> *</Label>
<Input
value={editingAbsolutePath}
onChange={(e) => setEditingAbsolutePath(e.target.value)}
placeholder="/제품관리/제품등록"
/>
<p className="text-xs text-gray-500 mt-1">(/) , </p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setEditingPathPageId(null)}></Button>
<Button onClick={() => {
if (!editingAbsolutePath.trim()) {
toast.error('절대경로를 입력해주세요');
return;
}
if (!editingAbsolutePath.startsWith('/')) {
toast.error('절대경로는 슬래시(/)로 시작해야 합니다');
return;
}
if (editingPathPageId) {
updateItemPage(editingPathPageId, { absolutePath: editingAbsolutePath });
trackChange('pages', editingPathPageId, 'update', { absolutePath: editingAbsolutePath });
setEditingPathPageId(null);
toast.success('절대경로가 수정되었습니다 (저장 필요)');
}
}}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,86 @@
'use client';
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';
interface SectionDialogProps {
isSectionDialogOpen: boolean;
setIsSectionDialogOpen: (open: boolean) => void;
newSectionType: 'fields' | 'bom';
setNewSectionType: (type: 'fields' | 'bom') => void;
newSectionTitle: string;
setNewSectionTitle: (title: string) => void;
newSectionDescription: string;
setNewSectionDescription: (description: string) => void;
handleAddSection: () => void;
}
export function SectionDialog({
isSectionDialogOpen,
setIsSectionDialogOpen,
newSectionType,
setNewSectionType,
newSectionTitle,
setNewSectionTitle,
newSectionDescription,
setNewSectionDescription,
handleAddSection,
}: SectionDialogProps) {
return (
<Dialog open={isSectionDialogOpen} onOpenChange={(open) => {
setIsSectionDialogOpen(open);
if (!open) {
setNewSectionType('fields');
setNewSectionTitle('');
setNewSectionDescription('');
}
}}>
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>{newSectionType === 'bom' ? 'BOM 섹션' : '일반 섹션'} </DialogTitle>
<DialogDescription>
{newSectionType === 'bom'
? '새로운 BOM(자재명세서) 섹션을 추가합니다'
: '새로운 일반 섹션을 추가합니다'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label> *</Label>
<Input
value={newSectionTitle}
onChange={(e) => setNewSectionTitle(e.target.value)}
placeholder={newSectionType === 'bom' ? '예: BOM 구성' : '예: 기본 정보'}
/>
</div>
<div>
<Label> ()</Label>
<Textarea
value={newSectionDescription}
onChange={(e) => setNewSectionDescription(e.target.value)}
placeholder="섹션에 대한 설명"
/>
</div>
{newSectionType === 'bom' && (
<div className="bg-blue-50 p-3 rounded-md">
<p className="text-sm text-blue-700">
<strong>BOM :</strong> (BOM) .
, , .
</p>
</div>
)}
</div>
<DialogFooter className="flex-col sm:flex-row gap-2">
<Button variant="outline" onClick={() => {
setIsSectionDialogOpen(false);
setNewSectionType('fields');
}} className="w-full sm:w-auto"></Button>
<Button onClick={handleAddSection} className="w-full sm:w-auto"></Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,147 @@
'use client';
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';
const ITEM_TYPE_OPTIONS = [
{ value: 'product', label: '제품' },
{ value: 'part', label: '부품' },
{ value: 'material', label: '자재' },
{ value: 'assembly', label: '조립품' },
];
interface SectionTemplateDialogProps {
isSectionTemplateDialogOpen: boolean;
setIsSectionTemplateDialogOpen: (open: boolean) => void;
editingSectionTemplateId: number | null;
setEditingSectionTemplateId: (id: number | null) => void;
newSectionTemplateTitle: string;
setNewSectionTemplateTitle: (title: string) => void;
newSectionTemplateDescription: string;
setNewSectionTemplateDescription: (description: string) => void;
newSectionTemplateCategory: string[];
setNewSectionTemplateCategory: (category: string[]) => void;
newSectionTemplateType: 'fields' | 'bom';
setNewSectionTemplateType: (type: 'fields' | 'bom') => void;
handleUpdateSectionTemplate: () => void;
handleAddSectionTemplate: () => void;
}
export function SectionTemplateDialog({
isSectionTemplateDialogOpen,
setIsSectionTemplateDialogOpen,
editingSectionTemplateId,
setEditingSectionTemplateId,
newSectionTemplateTitle,
setNewSectionTemplateTitle,
newSectionTemplateDescription,
setNewSectionTemplateDescription,
newSectionTemplateCategory,
setNewSectionTemplateCategory,
newSectionTemplateType,
setNewSectionTemplateType,
handleUpdateSectionTemplate,
handleAddSectionTemplate,
}: SectionTemplateDialogProps) {
return (
<Dialog open={isSectionTemplateDialogOpen} onOpenChange={(open) => {
setIsSectionTemplateDialogOpen(open);
if (!open) {
setEditingSectionTemplateId(null);
setNewSectionTemplateTitle('');
setNewSectionTemplateDescription('');
setNewSectionTemplateCategory([]);
setNewSectionTemplateType('fields');
}
}}>
<DialogContent className="max-w-[95vw] sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{editingSectionTemplateId ? '섹션 수정' : '섹션 추가'}</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label> *</Label>
<Input
value={newSectionTemplateTitle}
onChange={(e) => setNewSectionTemplateTitle(e.target.value)}
placeholder="예: 기본 정보"
/>
</div>
<div>
<Label> ()</Label>
<Textarea
value={newSectionTemplateDescription}
onChange={(e) => setNewSectionTemplateDescription(e.target.value)}
placeholder="섹션에 대한 설명"
/>
</div>
<div>
<Label> *</Label>
<Select
value={newSectionTemplateType}
onValueChange={(val) => setNewSectionTemplateType(val as 'fields' | 'bom')}
disabled={!!editingSectionTemplateId}
>
<SelectTrigger>
<SelectValue placeholder="섹션 타입을 선택하세요" />
</SelectTrigger>
<SelectContent>
<SelectItem value="fields"> </SelectItem>
<SelectItem value="bom">BOM ( )</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-gray-500 mt-1">
{editingSectionTemplateId
? '※ 템플릿 타입은 수정할 수 없습니다.'
: '일반 필드: 텍스트, 드롭다운 등의 항목 관리 | BOM: 하위 품목 구성 관리'}
</p>
</div>
<div>
<Label> ()</Label>
<div className="grid grid-cols-3 gap-2 mt-2">
{ITEM_TYPE_OPTIONS.map((type) => (
<div key={type.value} className="flex items-center gap-2">
<input
type="checkbox"
id={`cat-${type.value}`}
checked={newSectionTemplateCategory.includes(type.value)}
onChange={(e) => {
if (e.target.checked) {
setNewSectionTemplateCategory([...newSectionTemplateCategory, type.value]);
} else {
setNewSectionTemplateCategory(newSectionTemplateCategory.filter(c => c !== type.value));
}
}}
className="rounded"
/>
<label htmlFor={`cat-${type.value}`} className="text-sm cursor-pointer">
{type.label}
</label>
</div>
))}
</div>
<p className="text-xs text-muted-foreground mt-2">
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsSectionTemplateDialogOpen(false)}></Button>
<Button onClick={editingSectionTemplateId ? handleUpdateSectionTemplate : handleAddSectionTemplate}>
{editingSectionTemplateId ? '수정' : '추가'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,428 @@
'use client';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { ChevronUp, ChevronDown, Edit, Trash2, Plus, Settings } from 'lucide-react';
interface TabManagementDialogsProps {
// Manage Tabs Dialog
isManageTabsDialogOpen: boolean;
setIsManageTabsDialogOpen: (open: boolean) => void;
customTabs: Array<{ id: string; label: string; icon: string; order: number; isDefault?: boolean; key?: string }>;
moveTabUp: (id: string) => void;
moveTabDown: (id: string) => void;
handleEditTabFromManage: (tab: any) => void;
handleDeleteTab: (id: string) => void;
getTabIcon: (iconName: string) => any;
setIsAddTabDialogOpen: (open: boolean) => void;
// Delete Tab Dialog
isDeleteTabDialogOpen: boolean;
setIsDeleteTabDialogOpen: (open: boolean) => void;
deletingTabId: string | null;
setDeletingTabId: (id: string | null) => void;
confirmDeleteTab: () => void;
// Add/Edit Tab Dialog
isAddTabDialogOpen: boolean;
editingTabId: string | null;
setEditingTabId: (id: string | null) => void;
newTabLabel: string;
setNewTabLabel: (label: string) => void;
handleUpdateTab: () => void;
handleAddTab: () => void;
// Manage Attribute Tabs Dialog
isManageAttributeTabsDialogOpen: boolean;
setIsManageAttributeTabsDialogOpen: (open: boolean) => void;
attributeSubTabs: Array<{ id: string; label: string; key: string; order: number; isDefault?: boolean }>;
moveAttributeTabUp: (id: string) => void;
moveAttributeTabDown: (id: string) => void;
handleDeleteAttributeTab: (id: string) => void;
setIsAddAttributeTabDialogOpen: (open: boolean) => void;
// Delete Attribute Tab Dialog
isDeleteAttributeTabDialogOpen: boolean;
setIsDeleteAttributeTabDialogOpen: (open: boolean) => void;
deletingAttributeTabId: string | null;
setDeletingAttributeTabId: (id: string | null) => void;
confirmDeleteAttributeTab: () => void;
// Add/Edit Attribute Tab Dialog
isAddAttributeTabDialogOpen: boolean;
editingAttributeTabId: string | null;
setEditingAttributeTabId: (id: string | null) => void;
newAttributeTabLabel: string;
setNewAttributeTabLabel: (label: string) => void;
handleUpdateAttributeTab: () => void;
handleAddAttributeTab: () => void;
}
export function TabManagementDialogs({
// Manage Tabs Dialog
isManageTabsDialogOpen,
setIsManageTabsDialogOpen,
customTabs,
moveTabUp,
moveTabDown,
handleEditTabFromManage,
handleDeleteTab,
getTabIcon,
setIsAddTabDialogOpen,
// Delete Tab Dialog
isDeleteTabDialogOpen,
setIsDeleteTabDialogOpen,
deletingTabId,
setDeletingTabId,
confirmDeleteTab,
// Add/Edit Tab Dialog
isAddTabDialogOpen,
editingTabId,
setEditingTabId,
newTabLabel,
setNewTabLabel,
handleUpdateTab,
handleAddTab,
// Manage Attribute Tabs Dialog
isManageAttributeTabsDialogOpen,
setIsManageAttributeTabsDialogOpen,
attributeSubTabs,
moveAttributeTabUp,
moveAttributeTabDown,
handleDeleteAttributeTab,
setIsAddAttributeTabDialogOpen,
// Delete Attribute Tab Dialog
isDeleteAttributeTabDialogOpen,
setIsDeleteAttributeTabDialogOpen,
deletingAttributeTabId,
setDeletingAttributeTabId,
confirmDeleteAttributeTab,
// Add/Edit Attribute Tab Dialog
isAddAttributeTabDialogOpen,
editingAttributeTabId,
setEditingAttributeTabId,
newAttributeTabLabel,
setNewAttributeTabLabel,
handleUpdateAttributeTab,
handleAddAttributeTab,
}: TabManagementDialogsProps) {
return (
<>
{/* 탭 관리 다이얼로그 */}
<Dialog open={isManageTabsDialogOpen} onOpenChange={setIsManageTabsDialogOpen}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription>
,
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
{customTabs.sort((a, b) => a.order - b.order).map((tab, index) => {
const Icon = getTabIcon(tab.icon);
return (
<div
key={tab.id}
className="flex items-center gap-3 p-3 border rounded-lg hover:bg-gray-50 transition-colors"
>
<div className="flex flex-col gap-1">
<Button
size="sm"
variant="ghost"
className="h-6 w-6 p-0"
onClick={() => moveTabUp(tab.id)}
disabled={index === 0}
>
<ChevronUp className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
className="h-6 w-6 p-0"
onClick={() => moveTabDown(tab.id)}
disabled={index === customTabs.length - 1}
>
<ChevronDown className="h-4 w-4" />
</Button>
</div>
<Icon className="w-5 h-5 text-gray-500 flex-shrink-0" />
<div className="flex-1">
<div className="font-medium">{tab.label}</div>
<div className="text-xs text-gray-500">
{tab.isDefault ? '기본 탭' : '사용자 정의 탭'} : {tab.order}
</div>
</div>
<div className="flex gap-2">
{!tab.isDefault && (
<>
<Button
size="sm"
variant="outline"
onClick={() => handleEditTabFromManage(tab)}
>
<Edit className="h-4 w-4 mr-1" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleDeleteTab(tab.id)}
>
<Trash2 className="h-4 w-4 mr-1 text-red-500" />
</Button>
</>
)}
{tab.isDefault && (
<Badge variant="secondary"> </Badge>
)}
</div>
</div>
);
})}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setIsManageTabsDialogOpen(false);
setIsAddTabDialogOpen(true);
}}
>
<Plus className="h-4 w-4 mr-2" />
</Button>
<Button onClick={() => setIsManageTabsDialogOpen(false)}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 탭 삭제 확인 다이얼로그 */}
<AlertDialog open={isDeleteTabDialogOpen} onOpenChange={setIsDeleteTabDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription>
"{customTabs.find(t => t.id === deletingTabId)?.label}" ?
<br />
.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => {
setIsDeleteTabDialogOpen(false);
setDeletingTabId(null);
}}>
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDeleteTab}
className="bg-red-500 hover:bg-red-600"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* 탭 추가/수정 다이얼로그 */}
<Dialog open={isAddTabDialogOpen} onOpenChange={(open) => {
setIsAddTabDialogOpen(open);
if (!open) {
setEditingTabId(null);
setNewTabLabel('');
}
}}>
<DialogContent>
<DialogHeader>
<DialogTitle>{editingTabId ? '탭 수정' : '탭 추가'}</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label> *</Label>
<Input
value={newTabLabel}
onChange={(e) => setNewTabLabel(e.target.value)}
placeholder="예: 거래처, 창고"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddTabDialogOpen(false)}></Button>
<Button onClick={editingTabId ? handleUpdateTab : handleAddTab}>
{editingTabId ? '수정' : '추가'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 속성 하위 탭 관리 다이얼로그 */}
<Dialog open={isManageAttributeTabsDialogOpen} onOpenChange={setIsManageAttributeTabsDialogOpen}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription>
,
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
{attributeSubTabs.sort((a, b) => a.order - b.order).map((tab, index) => {
const Icon = Settings;
return (
<div key={tab.id} className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50">
<div className="flex items-center gap-3">
<Icon className="h-5 w-5 text-gray-500" />
<div>
<div className="font-medium">{tab.label}</div>
<div className="text-sm text-gray-500">ID: {tab.key}</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button
size="sm"
variant="outline"
onClick={() => moveAttributeTabUp(tab.id)}
disabled={index === 0}
>
<ChevronUp className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => moveAttributeTabDown(tab.id)}
disabled={index === attributeSubTabs.length - 1}
>
<ChevronDown className="h-4 w-4" />
</Button>
{!tab.isDefault && (
<>
<Button
size="sm"
variant="outline"
onClick={() => {
setEditingAttributeTabId(tab.id);
setNewAttributeTabLabel(tab.label);
setIsManageAttributeTabsDialogOpen(false);
setIsAddAttributeTabDialogOpen(true);
}}
>
<Edit className="h-4 w-4 mr-1" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleDeleteAttributeTab(tab.id)}
>
<Trash2 className="h-4 w-4 mr-1 text-red-500" />
</Button>
</>
)}
{tab.isDefault && (
<Badge variant="secondary"> </Badge>
)}
</div>
</div>
);
})}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setIsManageAttributeTabsDialogOpen(false);
setIsAddAttributeTabDialogOpen(true);
}}
>
<Plus className="h-4 w-4 mr-2" />
</Button>
<Button onClick={() => setIsManageAttributeTabsDialogOpen(false)}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 속성 하위 탭 삭제 확인 다이얼로그 */}
<AlertDialog open={isDeleteAttributeTabDialogOpen} onOpenChange={setIsDeleteAttributeTabDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription>
"{attributeSubTabs.find(t => t.id === deletingAttributeTabId)?.label}" ?
<br />
.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => {
setIsDeleteAttributeTabDialogOpen(false);
setDeletingAttributeTabId(null);
}}>
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDeleteAttributeTab}
className="bg-red-500 hover:bg-red-600"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* 속성 하위 탭 추가/수정 다이얼로그 */}
<Dialog open={isAddAttributeTabDialogOpen} onOpenChange={(open) => {
setIsAddAttributeTabDialogOpen(open);
if (!open) {
setEditingAttributeTabId(null);
setNewAttributeTabLabel('');
}
}}>
<DialogContent>
<DialogHeader>
<DialogTitle>{editingAttributeTabId ? '속성 탭 수정' : '속성 탭 추가'}</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label> *</Label>
<Input
value={newAttributeTabLabel}
onChange={(e) => setNewAttributeTabLabel(e.target.value)}
placeholder="예: 색상, 규격"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddAttributeTabDialogOpen(false)}></Button>
<Button onClick={editingAttributeTabId ? handleUpdateAttributeTab : handleAddAttributeTab}>
{editingAttributeTabId ? '수정' : '추가'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -0,0 +1,212 @@
'use client';
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';
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;
}
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,
}: TemplateFieldDialogProps) {
return (
<Dialog open={isTemplateFieldDialogOpen} onOpenChange={(open) => {
setIsTemplateFieldDialogOpen(open);
if (!open) {
setEditingTemplateFieldId(null);
setTemplateFieldName('');
setTemplateFieldKey('');
setTemplateFieldInputType('textbox');
setTemplateFieldRequired(false);
setTemplateFieldOptions('');
setTemplateFieldDescription('');
setTemplateFieldMultiColumn(false);
setTemplateFieldColumnCount(2);
setTemplateFieldColumnNames(['컬럼1', '컬럼2']);
}
}}>
<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">
<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="예: 품목명"
/>
</div>
<div>
<Label> *</Label>
<Input
value={templateFieldKey}
onChange={(e) => setTemplateFieldKey(e.target.value)}
placeholder="예: itemName"
/>
</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={() => setIsTemplateFieldDialogOpen(false)}></Button>
<Button onClick={handleAddTemplateField}>
{editingTemplateFieldId ? '수정' : '추가'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}