refactor: 품목기준관리 hooks 분리 및 다이얼로그 개선

- ItemMasterDataManagement 컴포넌트에서 hooks 분리
- 다이얼로그 컴포넌트들 타입 및 구조 개선
- BOMManagementSection 개선
- HierarchyTab 업데이트

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-11-26 14:06:48 +09:00
parent 593644922a
commit b73603822b
25 changed files with 3559 additions and 1703 deletions

View File

@@ -1,5 +1,6 @@
'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';
@@ -31,9 +32,24 @@ export function ColumnDialog({
textboxColumns,
setTextboxColumns,
}: ColumnDialogProps) {
const [isSubmitted, setIsSubmitted] = useState(false);
// 유효성 검사
const isNameEmpty = !columnName.trim();
const isKeyEmpty = !columnKey.trim();
const handleClose = () => {
setIsColumnDialogOpen(false);
setEditingColumnId(null);
setColumnName('');
setColumnKey('');
setIsSubmitted(false);
};
const handleSubmit = () => {
if (!columnName.trim() || !columnKey.trim()) {
return toast.error('모든 필드를 입력해주세요');
setIsSubmitted(true);
if (isNameEmpty || isKeyEmpty) {
return;
}
if (editingColumnId) {
@@ -54,20 +70,14 @@ export function ColumnDialog({
toast.success('컬럼이 추가되었습니다');
}
setIsColumnDialogOpen(false);
setEditingColumnId(null);
setColumnName('');
setColumnKey('');
handleClose();
setIsSubmitted(false);
};
return (
<Dialog open={isColumnDialogOpen} onOpenChange={(open) => {
setIsColumnDialogOpen(open);
if (!open) {
setEditingColumnId(null);
setColumnName('');
setColumnKey('');
}
if (!open) handleClose();
else setIsColumnDialogOpen(open);
}}>
<DialogContent>
<DialogHeader>
@@ -83,7 +93,11 @@ export function ColumnDialog({
value={columnName}
onChange={(e) => setColumnName(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>
@@ -91,11 +105,15 @@ export function ColumnDialog({
value={columnKey}
onChange={(e) => setColumnKey(e.target.value)}
placeholder="예: width"
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>
<DialogFooter>
<Button variant="outline" onClick={() => setIsColumnDialogOpen(false)}></Button>
<Button variant="outline" onClick={handleClose}></Button>
<Button onClick={handleSubmit}>
{editingColumnId ? '수정' : '추가'}
</Button>

View File

@@ -1,5 +1,6 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -121,7 +122,14 @@ export function FieldDialog({
setColumnName,
setColumnKey,
}: FieldDialogProps) {
const [isSubmitted, setIsSubmitted] = useState(false);
// 유효성 검사
const isNameEmpty = !newFieldName.trim();
const isKeyEmpty = !newFieldKey.trim();
const handleClose = () => {
setIsSubmitted(false);
onOpenChange(false);
setEditingFieldId(null);
setFieldInputMode('custom');
@@ -268,7 +276,11 @@ export function FieldDialog({
value={newFieldName}
onChange={(e) => setNewFieldName(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>
@@ -276,7 +288,11 @@ export function FieldDialog({
value={newFieldKey}
onChange={(e) => setNewFieldKey(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>
@@ -402,7 +418,12 @@ export function FieldDialog({
</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>
<Button onClick={() => {
setIsSubmitted(true);
if ((fieldInputMode === 'custom' || editingFieldId) && (isNameEmpty || isKeyEmpty)) return;
handleAddField();
setIsSubmitted(false);
}}></Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -189,21 +189,22 @@ export function FieldDrawer({
<div
key={field.id}
className={`p-3 border rounded cursor-pointer transition-colors ${
selectedMasterFieldId === field.id
selectedMasterFieldId === String(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);
setSelectedMasterFieldId(String(field.id));
setNewFieldName(field.field_name);
setNewFieldKey(`field_${field.id}`);
setNewFieldInputType(field.field_type);
setNewFieldRequired((field.properties as any)?.required ?? false);
setNewFieldDescription(field.description || '');
setNewFieldOptions(field.property.options?.join(', ') || '');
if (field.property.multiColumn && field.property.columnNames) {
setNewFieldOptions(field.options?.map(o => o.value).join(', ') || '');
const props = field.properties as any;
if (props?.multiColumn && props?.columnNames) {
setTextboxColumns(
field.property.columnNames.map((name, idx) => ({
props.columnNames.map((name: string, idx: number) => ({
id: `col-${idx}`,
name,
key: `column${idx + 1}`
@@ -215,28 +216,26 @@ export function FieldDrawer({
<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>
<span className="font-medium">{field.field_name}</span>
<Badge variant="outline" className="text-xs">
{INPUT_TYPE_OPTIONS.find(o => o.value === field.property.inputType)?.label}
{INPUT_TYPE_OPTIONS.find(o => o.value === field.field_type)?.label}
</Badge>
{field.property.required && (
{(field.properties as any)?.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 && (
{field.category && (
<div className="flex gap-1 mt-1">
{field.category.map((cat, idx) => (
<Badge key={idx} variant="secondary" className="text-xs">
{cat}
</Badge>
))}
<Badge variant="secondary" className="text-xs">
{field.category}
</Badge>
</div>
)}
</div>
{selectedMasterFieldId === field.id && (
{selectedMasterFieldId === String(field.id) && (
<Check className="h-5 w-5 text-blue-600" />
)}
</div>
@@ -585,22 +584,23 @@ export function FieldDrawer({
</p>
<div className="space-y-2 max-h-60 overflow-y-auto">
{selectedPage.sections
.filter(section => section.type !== 'bom')
.filter(section => 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)}
checked={newFieldConditionSections.includes(String(section.id))}
onChange={(e) => {
const sectionIdStr = String(section.id);
if (e.target.checked) {
setNewFieldConditionSections(prev => [...prev, section.id]);
setNewFieldConditionSections(prev => [...prev, sectionIdStr]);
} else {
setNewFieldConditionSections(prev => prev.filter(id => id !== section.id));
setNewFieldConditionSections(prev => prev.filter(id => id !== sectionIdStr));
}
}}
className="cursor-pointer"
/>
<span className="flex-1 text-sm">{section.title}</span>
<span className="flex-1 text-sm">{section.section_name}</span>
</label>
))}
</div>

View File

@@ -1,5 +1,6 @@
'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';
@@ -79,23 +80,45 @@ export function MasterFieldDialog({
handleUpdateMasterField,
handleAddMasterField,
}: MasterFieldDialogProps) {
const [isSubmitted, setIsSubmitted] = useState(false);
// 유효성 검사
const isNameEmpty = !newMasterFieldName.trim();
const isKeyEmpty = !newMasterFieldKey.trim();
const handleClose = () => {
setIsMasterFieldDialogOpen(false);
setEditingMasterFieldId(null);
setNewMasterFieldName('');
setNewMasterFieldKey('');
setNewMasterFieldInputType('textbox');
setNewMasterFieldRequired(false);
setNewMasterFieldCategory('공통');
setNewMasterFieldDescription('');
setNewMasterFieldOptions('');
setNewMasterFieldAttributeType('custom');
setNewMasterFieldMultiColumn(false);
setNewMasterFieldColumnCount(2);
setNewMasterFieldColumnNames(['컬럼1', '컬럼2']);
setIsSubmitted(false);
};
const handleSubmit = () => {
setIsSubmitted(true);
if (!isNameEmpty && !isKeyEmpty) {
if (editingMasterFieldId) {
handleUpdateMasterField();
} else {
handleAddMasterField();
}
setIsSubmitted(false);
}
};
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']);
}
if (!open) handleClose();
else setIsMasterFieldDialogOpen(open);
}}>
<DialogContent className="max-w-[95vw] sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
@@ -112,7 +135,11 @@ export function MasterFieldDialog({
value={newMasterFieldName}
onChange={(e) => setNewMasterFieldName(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>
@@ -120,7 +147,11 @@ export function MasterFieldDialog({
value={newMasterFieldKey}
onChange={(e) => setNewMasterFieldKey(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>
@@ -251,8 +282,8 @@ export function MasterFieldDialog({
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsMasterFieldDialogOpen(false)}></Button>
<Button onClick={editingMasterFieldId ? handleUpdateMasterField : handleAddMasterField}>
<Button variant="outline" onClick={handleClose}></Button>
<Button onClick={handleSubmit}>
{editingMasterFieldId ? '수정' : '추가'}
</Button>
</DialogFooter>

View File

@@ -1,5 +1,6 @@
'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';
@@ -72,19 +73,38 @@ export function OptionDialog({
attributeColumns,
handleAddOption,
}: OptionDialogProps) {
const [isSubmitted, setIsSubmitted] = useState(false);
// 유효성 검사
const isValueEmpty = !newOptionValue.trim();
const isLabelEmpty = !newOptionLabel.trim();
const isDropdownOptionsEmpty = newOptionInputType === 'dropdown' && !newOptionOptions.trim();
const handleClose = () => {
setIsOpen(false);
setNewOptionValue('');
setNewOptionLabel('');
setNewOptionColumnValues({});
setNewOptionInputType('textbox');
setNewOptionRequired(false);
setNewOptionOptions('');
setNewOptionPlaceholder('');
setNewOptionDefaultValue('');
setIsSubmitted(false);
};
const handleSubmit = () => {
setIsSubmitted(true);
if (!isValueEmpty && !isLabelEmpty && !isDropdownOptionsEmpty) {
handleAddOption();
setIsSubmitted(false);
}
};
return (
<Dialog open={isOpen} onOpenChange={(open) => {
setIsOpen(open);
if (!open) {
setNewOptionValue('');
setNewOptionLabel('');
setNewOptionColumnValues({});
setNewOptionInputType('textbox');
setNewOptionRequired(false);
setNewOptionOptions('');
setNewOptionPlaceholder('');
setNewOptionDefaultValue('');
}
if (!open) handleClose();
else setIsOpen(open);
}}>
<DialogContent className="max-w-[95vw] sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
@@ -109,7 +129,11 @@ export function OptionDialog({
value={newOptionValue}
onChange={(e) => setNewOptionValue(e.target.value)}
placeholder="예: kg, stainless"
className={isSubmitted && isValueEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
/>
{isSubmitted && isValueEmpty && (
<p className="text-xs text-red-500 mt-1"> </p>
)}
</div>
<div>
<Label> () *</Label>
@@ -117,7 +141,11 @@ export function OptionDialog({
value={newOptionLabel}
onChange={(e) => setNewOptionLabel(e.target.value)}
placeholder="예: 킬로그램, 스테인리스"
className={isSubmitted && isLabelEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
/>
{isSubmitted && isLabelEmpty && (
<p className="text-xs text-red-500 mt-1"> </p>
)}
</div>
</div>
</div>
@@ -151,10 +179,15 @@ export function OptionDialog({
value={newOptionOptions}
onChange={(e) => setNewOptionOptions(e.target.value)}
placeholder="옵션1, 옵션2, 옵션3 (쉼표로 구분)"
className={isSubmitted && isDropdownOptionsEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
/>
<p className="text-xs text-muted-foreground mt-1">
</p>
{isSubmitted && isDropdownOptionsEmpty ? (
<p className="text-xs text-red-500 mt-1"> </p>
) : (
<p className="text-xs text-muted-foreground mt-1">
</p>
)}
</div>
)}
@@ -213,8 +246,8 @@ export function OptionDialog({
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsOpen(false)}></Button>
<Button onClick={handleAddOption}></Button>
<Button variant="outline" onClick={handleClose}></Button>
<Button onClick={handleSubmit}></Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -1,5 +1,6 @@
'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';
@@ -33,27 +34,53 @@ export function PageDialog({
setNewPageItemType,
handleAddPage,
}: PageDialogProps) {
const [isSubmitted, setIsSubmitted] = useState(false);
// 유효성 검사
const isPageNameEmpty = !newPageName.trim();
const handleSubmit = () => {
setIsSubmitted(true);
if (!isPageNameEmpty) {
handleAddPage();
setIsSubmitted(false);
}
};
const handleClose = () => {
setIsPageDialogOpen(false);
setNewPageName('');
setNewPageItemType('FG');
setIsSubmitted(false);
};
return (
<Dialog open={isPageDialogOpen} onOpenChange={(open) => {
setIsPageDialogOpen(open);
if (!open) {
setNewPageName('');
setNewPageItemType('FG');
handleClose();
} else {
setIsPageDialogOpen(open);
}
}}>
<DialogContent>
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription> </DialogDescription>
<DialogTitle> </DialogTitle>
<DialogDescription> </DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label> *</Label>
<Label> *</Label>
<Input
value={newPageName}
onChange={(e) => setNewPageName(e.target.value)}
placeholder="예: 품 등록"
placeholder="예: 품 등록"
className={isSubmitted && isPageNameEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
/>
{isSubmitted && isPageNameEmpty && (
<p className="text-xs text-red-500 mt-1">
</p>
)}
</div>
<div>
<Label> *</Label>
@@ -70,8 +97,8 @@ export function PageDialog({
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsPageDialogOpen(false)}></Button>
<Button onClick={handleAddPage}></Button>
<Button variant="outline" onClick={handleClose}></Button>
<Button onClick={handleSubmit}></Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -1,5 +1,6 @@
'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';
@@ -23,12 +24,21 @@ export function PathEditDialog({
updateItemPage,
trackChange,
}: PathEditDialogProps) {
const [isSubmitted, setIsSubmitted] = useState(false);
// 유효성 검사
const isPathEmpty = !editingAbsolutePath.trim();
const isPathInvalid = editingAbsolutePath.trim() && !editingAbsolutePath.startsWith('/');
const handleClose = () => {
setEditingPathPageId(null);
setEditingAbsolutePath('');
setIsSubmitted(false);
};
return (
<Dialog open={editingPathPageId !== null} onOpenChange={(open) => {
if (!open) {
setEditingPathPageId(null);
setEditingAbsolutePath('');
}
if (!open) handleClose();
}}>
<DialogContent>
<DialogHeader>
@@ -42,25 +52,30 @@ export function PathEditDialog({
value={editingAbsolutePath}
onChange={(e) => setEditingAbsolutePath(e.target.value)}
placeholder="/제품관리/제품등록"
className={isSubmitted && (isPathEmpty || isPathInvalid) ? 'border-red-500 focus-visible:ring-red-500' : ''}
/>
<p className="text-xs text-gray-500 mt-1">(/) , </p>
{isSubmitted && isPathEmpty && (
<p className="text-xs text-red-500 mt-1"> </p>
)}
{isSubmitted && isPathInvalid && (
<p className="text-xs text-red-500 mt-1"> (/) </p>
)}
{!isSubmitted || (!isPathEmpty && !isPathInvalid) ? (
<p className="text-xs text-gray-500 mt-1">(/) , </p>
) : null}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setEditingPathPageId(null)}></Button>
<Button variant="outline" onClick={handleClose}></Button>
<Button onClick={() => {
if (!editingAbsolutePath.trim()) {
toast.error('절대경로를 입력해주세요');
return;
}
if (!editingAbsolutePath.startsWith('/')) {
toast.error('절대경로는 슬래시(/)로 시작해야 합니다');
setIsSubmitted(true);
if (isPathEmpty || isPathInvalid) {
return;
}
if (editingPathPageId) {
updateItemPage(editingPathPageId, { absolutePath: editingAbsolutePath });
trackChange('pages', editingPathPageId, 'update', { absolutePath: editingAbsolutePath });
setEditingPathPageId(null);
trackChange('pages', String(editingPathPageId), 'update', { absolutePath: editingAbsolutePath });
handleClose();
toast.success('절대경로가 수정되었습니다 (저장 필요)');
}
}}></Button>

View File

@@ -1,12 +1,13 @@
'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 { Badge } from '@/components/ui/badge';
import { FileText, Package, Check, X } from 'lucide-react';
import { FileText, Package, Check } from 'lucide-react';
import type { SectionTemplate } from '@/contexts/ItemMasterContext';
interface SectionDialogProps {
@@ -45,6 +46,11 @@ export function SectionDialog({
setSelectedTemplateId,
handleLinkTemplate,
}: SectionDialogProps) {
const [isSubmitted, setIsSubmitted] = useState(false);
// 유효성 검사
const isTitleEmpty = !newSectionTitle.trim();
const handleClose = () => {
setIsSectionDialogOpen(false);
setNewSectionType('fields');
@@ -52,6 +58,15 @@ export function SectionDialog({
setNewSectionDescription('');
setSectionInputMode('custom');
setSelectedTemplateId(null);
setIsSubmitted(false);
};
const handleSubmit = () => {
setIsSubmitted(true);
if (sectionInputMode === 'custom' && !isTitleEmpty) {
handleAddSection();
setIsSubmitted(false);
}
};
// 템플릿 선택 시 폼에 값 채우기
@@ -220,7 +235,11 @@ export function SectionDialog({
onChange={(e) => setNewSectionTitle(e.target.value)}
placeholder={newSectionType === 'bom' ? '예: BOM 구성' : '예: 기본 정보'}
disabled={sectionInputMode === 'template'}
className={isSubmitted && isTitleEmpty && sectionInputMode === 'custom' ? 'border-red-500 focus-visible:ring-red-500' : ''}
/>
{isSubmitted && isTitleEmpty && sectionInputMode === 'custom' && (
<p className="text-xs text-red-500 mt-1"> </p>
)}
</div>
<div>
<Label> ()</Label>
@@ -269,7 +288,7 @@ export function SectionDialog({
</Button>
) : (
<Button
onClick={handleAddSection}
onClick={handleSubmit}
className="w-full sm:w-auto"
disabled={sectionInputMode === 'template' && !selectedTemplateId}
>

View File

@@ -1,5 +1,6 @@
'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';
@@ -47,16 +48,37 @@ export function SectionTemplateDialog({
handleUpdateSectionTemplate,
handleAddSectionTemplate,
}: SectionTemplateDialogProps) {
const [isSubmitted, setIsSubmitted] = useState(false);
// 유효성 검사
const isTitleEmpty = !newSectionTemplateTitle.trim();
const handleClose = () => {
setIsSectionTemplateDialogOpen(false);
setEditingSectionTemplateId(null);
setNewSectionTemplateTitle('');
setNewSectionTemplateDescription('');
setNewSectionTemplateCategory([]);
setNewSectionTemplateType('fields');
setIsSubmitted(false);
};
const handleSubmit = () => {
setIsSubmitted(true);
if (!isTitleEmpty) {
if (editingSectionTemplateId) {
handleUpdateSectionTemplate();
} else {
handleAddSectionTemplate();
}
setIsSubmitted(false);
}
};
return (
<Dialog open={isSectionTemplateDialogOpen} onOpenChange={(open) => {
setIsSectionTemplateDialogOpen(open);
if (!open) {
setEditingSectionTemplateId(null);
setNewSectionTemplateTitle('');
setNewSectionTemplateDescription('');
setNewSectionTemplateCategory([]);
setNewSectionTemplateType('fields');
}
if (!open) handleClose();
else setIsSectionTemplateDialogOpen(open);
}}>
<DialogContent className="max-w-[95vw] sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
@@ -72,7 +94,11 @@ export function SectionTemplateDialog({
value={newSectionTemplateTitle}
onChange={(e) => setNewSectionTemplateTitle(e.target.value)}
placeholder="예: 기본 정보"
className={isSubmitted && isTitleEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
/>
{isSubmitted && isTitleEmpty && (
<p className="text-xs text-red-500 mt-1"> </p>
)}
</div>
<div>
@@ -136,8 +162,8 @@ export function SectionTemplateDialog({
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsSectionTemplateDialogOpen(false)}></Button>
<Button onClick={editingSectionTemplateId ? handleUpdateSectionTemplate : handleAddSectionTemplate}>
<Button variant="outline" onClick={handleClose}></Button>
<Button onClick={handleSubmit}>
{editingSectionTemplateId ? '수정' : '추가'}
</Button>
</DialogFooter>

View File

@@ -1,5 +1,6 @@
'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';
@@ -87,7 +88,14 @@ export function TemplateFieldDialog({
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('');
@@ -230,7 +238,11 @@ export function TemplateFieldDialog({
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>
@@ -238,7 +250,11 @@ export function TemplateFieldDialog({
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>
@@ -334,8 +350,14 @@ export function TemplateFieldDialog({
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsTemplateFieldDialogOpen(false)}></Button>
<Button onClick={handleAddTemplateField}>
<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>

View File

@@ -0,0 +1,20 @@
export { usePageManagement } from './usePageManagement';
export type { UsePageManagementReturn } from './usePageManagement';
export { useSectionManagement } from './useSectionManagement';
export type { UseSectionManagementReturn } from './useSectionManagement';
export { useFieldManagement } from './useFieldManagement';
export type { UseFieldManagementReturn } from './useFieldManagement';
export { useMasterFieldManagement } from './useMasterFieldManagement';
export type { UseMasterFieldManagementReturn } from './useMasterFieldManagement';
export { useTemplateManagement } from './useTemplateManagement';
export type { UseTemplateManagementReturn } from './useTemplateManagement';
export { useAttributeManagement } from './useAttributeManagement';
export type { UseAttributeManagementReturn } from './useAttributeManagement';
export { useTabManagement } from './useTabManagement';
export type { UseTabManagementReturn, CustomTab, AttributeSubTab } from './useTabManagement';

View File

@@ -0,0 +1,351 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { toast } from 'sonner';
import { useItemMaster } from '@/contexts/ItemMasterContext';
import type { MasterOption, OptionColumn } from '../types';
export interface UseAttributeManagementReturn {
// 속성 옵션 상태
unitOptions: MasterOption[];
setUnitOptions: React.Dispatch<React.SetStateAction<MasterOption[]>>;
materialOptions: MasterOption[];
setMaterialOptions: React.Dispatch<React.SetStateAction<MasterOption[]>>;
surfaceTreatmentOptions: MasterOption[];
setSurfaceTreatmentOptions: React.Dispatch<React.SetStateAction<MasterOption[]>>;
customAttributeOptions: Record<string, MasterOption[]>;
setCustomAttributeOptions: React.Dispatch<React.SetStateAction<Record<string, MasterOption[]>>>;
// 옵션 다이얼로그 상태
isOptionDialogOpen: boolean;
setIsOptionDialogOpen: (open: boolean) => void;
editingOptionType: string | null;
setEditingOptionType: (type: string | null) => void;
// 옵션 폼 상태
newOptionValue: string;
setNewOptionValue: (value: string) => void;
newOptionLabel: string;
setNewOptionLabel: (label: string) => void;
newOptionColumnValues: Record<string, string>;
setNewOptionColumnValues: React.Dispatch<React.SetStateAction<Record<string, string>>>;
newOptionInputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
setNewOptionInputType: (type: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea') => void;
newOptionRequired: boolean;
setNewOptionRequired: (required: boolean) => void;
newOptionOptions: string;
setNewOptionOptions: (options: string) => void;
newOptionPlaceholder: string;
setNewOptionPlaceholder: (placeholder: string) => void;
newOptionDefaultValue: string;
setNewOptionDefaultValue: (value: string) => void;
// 칼럼 관리 상태
isColumnManageDialogOpen: boolean;
setIsColumnManageDialogOpen: (open: boolean) => void;
managingColumnType: string | null;
setManagingColumnType: (type: string | null) => void;
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;
// 핸들러
handleAddOption: () => void;
handleDeleteOption: (type: string, id: string) => void;
handleAddColumn: () => void;
handleDeleteColumn: (columnKey: string) => void;
resetOptionForm: () => void;
resetColumnForm: () => void;
}
export function useAttributeManagement(): UseAttributeManagementReturn {
const {
itemMasterFields,
updateItemMasterField
} = useItemMaster();
// 속성 옵션 상태
const [unitOptions, setUnitOptions] = useState<MasterOption[]>([]);
const [materialOptions, setMaterialOptions] = useState<MasterOption[]>([]);
const [surfaceTreatmentOptions, setSurfaceTreatmentOptions] = useState<MasterOption[]>([]);
const [customAttributeOptions, setCustomAttributeOptions] = useState<Record<string, MasterOption[]>>({});
// 옵션 다이얼로그 상태
const [isOptionDialogOpen, setIsOptionDialogOpen] = useState(false);
const [editingOptionType, setEditingOptionType] = useState<string | null>(null);
// 옵션 폼 상태
const [newOptionValue, setNewOptionValue] = useState('');
const [newOptionLabel, setNewOptionLabel] = useState('');
const [newOptionColumnValues, setNewOptionColumnValues] = useState<Record<string, string>>({});
const [newOptionInputType, setNewOptionInputType] = useState<'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'>('textbox');
const [newOptionRequired, setNewOptionRequired] = useState(false);
const [newOptionOptions, setNewOptionOptions] = useState('');
const [newOptionPlaceholder, setNewOptionPlaceholder] = useState('');
const [newOptionDefaultValue, setNewOptionDefaultValue] = useState('');
// 칼럼 관리 상태
const [isColumnManageDialogOpen, setIsColumnManageDialogOpen] = useState(false);
const [managingColumnType, setManagingColumnType] = useState<string | null>(null);
const [attributeColumns, setAttributeColumns] = useState<Record<string, OptionColumn[]>>({});
// 칼럼 폼 상태
const [newColumnName, setNewColumnName] = useState('');
const [newColumnKey, setNewColumnKey] = useState('');
const [newColumnType, setNewColumnType] = useState<'text' | 'number'>('text');
const [newColumnRequired, setNewColumnRequired] = useState(false);
// 이전 옵션 값 추적용 ref (무한 루프 방지)
const prevOptionsRef = useRef<string>('');
// 속성 변경 시 연동된 마스터 항목의 옵션 자동 업데이트
// 주의: itemMasterFields를 의존성에서 제거하여 무한 루프 방지
useEffect(() => {
// 현재 옵션 상태를 문자열로 직렬화
const currentOptionsState = JSON.stringify({
unit: unitOptions.map(o => o.label).sort(),
material: materialOptions.map(o => o.label).sort(),
surface: surfaceTreatmentOptions.map(o => o.label).sort(),
custom: Object.keys(customAttributeOptions).reduce((acc, key) => {
acc[key] = (customAttributeOptions[key] || []).map(o => o.label).sort();
return acc;
}, {} as Record<string, string[]>)
});
// 이전 상태와 동일하면 업데이트 스킵
if (prevOptionsRef.current === currentOptionsState) {
return;
}
prevOptionsRef.current = currentOptionsState;
// 실제 업데이트가 필요한 경우만 처리
itemMasterFields.forEach(field => {
// properties가 null/undefined인 경우 스킵
if (!field.properties) return;
const attributeType = (field.properties as any).attributeType;
if (attributeType && attributeType !== 'custom' && (field.properties as any)?.inputType === 'dropdown') {
let newOptions: string[] = [];
if (attributeType === 'unit') {
newOptions = unitOptions.map(opt => opt.label);
} else if (attributeType === 'material') {
newOptions = materialOptions.map(opt => opt.label);
} else if (attributeType === 'surface') {
newOptions = surfaceTreatmentOptions.map(opt => opt.label);
} else {
const customOpts = customAttributeOptions[attributeType] || [];
newOptions = customOpts.map(opt => opt.label);
}
const currentOptions = (field.properties as any)?.options || [];
const optionsChanged = JSON.stringify([...currentOptions].sort()) !== JSON.stringify([...newOptions].sort());
if (optionsChanged && newOptions.length > 0) {
updateItemMasterField(field.id, {
properties: {
...(field.properties || {}),
options: newOptions
}
});
}
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [unitOptions, materialOptions, surfaceTreatmentOptions, customAttributeOptions]);
// 옵션 추가
const handleAddOption = () => {
if (!editingOptionType || !newOptionValue.trim() || !newOptionLabel.trim()) {
toast.error('모든 항목을 입력해주세요');
return;
}
// dropdown일 경우 옵션 필수 체크
if (newOptionInputType === 'dropdown' && !newOptionOptions.trim()) {
toast.error('드롭다운 옵션을 입력해주세요');
return;
}
// 칼럼 필수 값 체크
const currentColumns = attributeColumns[editingOptionType] || [];
for (const column of currentColumns) {
if (column.required && !newOptionColumnValues[column.key]?.trim()) {
toast.error(`${column.name}은(는) 필수 입력 항목입니다`);
return;
}
}
const newOption: MasterOption = {
id: `${editingOptionType}-${Date.now()}`,
value: newOptionValue,
label: newOptionLabel,
isActive: true,
inputType: newOptionInputType,
required: newOptionRequired,
options: newOptionInputType === 'dropdown' ? newOptionOptions.split(',').map(o => o.trim()).filter(o => o) : undefined,
placeholder: newOptionPlaceholder || undefined,
defaultValue: newOptionDefaultValue || undefined,
columnValues: Object.keys(newOptionColumnValues).length > 0 ? { ...newOptionColumnValues } : undefined
};
if (editingOptionType === 'unit') {
setUnitOptions(prev => [...prev, newOption]);
} else if (editingOptionType === 'material') {
setMaterialOptions(prev => [...prev, newOption]);
} else if (editingOptionType === 'surface') {
setSurfaceTreatmentOptions(prev => [...prev, newOption]);
} else {
setCustomAttributeOptions(prev => ({
...prev,
[editingOptionType]: [...(prev[editingOptionType] || []), newOption]
}));
}
resetOptionForm();
toast.success('속성이 추가되었습니다 (저장 필요)');
};
// 옵션 삭제
const handleDeleteOption = (type: string, id: string) => {
if (type === 'unit') {
setUnitOptions(prev => prev.filter(o => o.id !== id));
} else if (type === 'material') {
setMaterialOptions(prev => prev.filter(o => o.id !== id));
} else if (type === 'surface') {
setSurfaceTreatmentOptions(prev => prev.filter(o => o.id !== id));
} else {
setCustomAttributeOptions(prev => ({
...prev,
[type]: (prev[type] || []).filter(o => o.id !== id)
}));
}
toast.success('삭제되었습니다');
};
// 칼럼 추가
const handleAddColumn = () => {
if (!managingColumnType || !newColumnName.trim() || !newColumnKey.trim()) {
toast.error('칼럼명과 키를 입력해주세요');
return;
}
const newColumn: OptionColumn = {
id: `col-${Date.now()}`,
key: newColumnKey,
name: newColumnName,
type: newColumnType,
required: newColumnRequired
};
setAttributeColumns(prev => ({
...prev,
[managingColumnType]: [...(prev[managingColumnType] || []), newColumn]
}));
resetColumnForm();
toast.success('칼럼이 추가되었습니다');
};
// 칼럼 삭제
const handleDeleteColumn = (columnKey: string) => {
if (!managingColumnType) return;
setAttributeColumns(prev => ({
...prev,
[managingColumnType]: (prev[managingColumnType] || []).filter(c => c.key !== columnKey)
}));
toast.success('칼럼이 삭제되었습니다');
};
// 옵션 폼 초기화
const resetOptionForm = () => {
setNewOptionValue('');
setNewOptionLabel('');
setNewOptionColumnValues({});
setNewOptionInputType('textbox');
setNewOptionRequired(false);
setNewOptionOptions('');
setNewOptionPlaceholder('');
setNewOptionDefaultValue('');
setIsOptionDialogOpen(false);
};
// 칼럼 폼 초기화
const resetColumnForm = () => {
setNewColumnName('');
setNewColumnKey('');
setNewColumnType('text');
setNewColumnRequired(false);
};
return {
// 속성 옵션 상태
unitOptions,
setUnitOptions,
materialOptions,
setMaterialOptions,
surfaceTreatmentOptions,
setSurfaceTreatmentOptions,
customAttributeOptions,
setCustomAttributeOptions,
// 옵션 다이얼로그 상태
isOptionDialogOpen,
setIsOptionDialogOpen,
editingOptionType,
setEditingOptionType,
// 옵션 폼 상태
newOptionValue,
setNewOptionValue,
newOptionLabel,
setNewOptionLabel,
newOptionColumnValues,
setNewOptionColumnValues,
newOptionInputType,
setNewOptionInputType,
newOptionRequired,
setNewOptionRequired,
newOptionOptions,
setNewOptionOptions,
newOptionPlaceholder,
setNewOptionPlaceholder,
newOptionDefaultValue,
setNewOptionDefaultValue,
// 칼럼 관리 상태
isColumnManageDialogOpen,
setIsColumnManageDialogOpen,
managingColumnType,
setManagingColumnType,
attributeColumns,
setAttributeColumns,
// 칼럼 폼 상태
newColumnName,
setNewColumnName,
newColumnKey,
setNewColumnKey,
newColumnType,
setNewColumnType,
newColumnRequired,
setNewColumnRequired,
// 핸들러
handleAddOption,
handleDeleteOption,
handleAddColumn,
handleDeleteColumn,
resetOptionForm,
resetColumnForm,
};
}

View File

@@ -0,0 +1,360 @@
'use client';
import { useState, useEffect } from 'react';
import { toast } from 'sonner';
import { useItemMaster } from '@/contexts/ItemMasterContext';
import type { ItemPage, ItemField, ItemMasterField, FieldDisplayCondition } from '@/contexts/ItemMasterContext';
import { type ConditionalFieldConfig } from '../components/ConditionalDisplayUI';
export interface UseFieldManagementReturn {
// 다이얼로그 상태
isFieldDialogOpen: boolean;
setIsFieldDialogOpen: (open: boolean) => void;
selectedSectionForField: number | null;
setSelectedSectionForField: (id: number | null) => void;
editingFieldId: number | null;
setEditingFieldId: (id: number | null) => void;
// 입력 모드
fieldInputMode: 'master' | 'custom';
setFieldInputMode: (mode: 'master' | 'custom') => void;
showMasterFieldList: boolean;
setShowMasterFieldList: (show: boolean) => void;
selectedMasterFieldId: string;
setSelectedMasterFieldId: (id: string) => void;
// 필드 폼 상태
newFieldName: string;
setNewFieldName: (name: string) => void;
newFieldKey: string;
setNewFieldKey: (key: string) => void;
newFieldInputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
setNewFieldInputType: (type: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea') => void;
newFieldRequired: boolean;
setNewFieldRequired: (required: boolean) => void;
newFieldOptions: string;
setNewFieldOptions: (options: string) => void;
newFieldDescription: string;
setNewFieldDescription: (desc: string) => void;
// 텍스트박스 컬럼
textboxColumns: Array<{ id: string; name: string; key: string }>;
setTextboxColumns: React.Dispatch<React.SetStateAction<Array<{ id: string; name: string; key: string }>>>;
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;
// 조건부 필드
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;
// 핸들러
handleAddField: (selectedPage: ItemPage | undefined) => void;
handleEditField: (sectionId: string, field: ItemField) => void;
handleDeleteField: (pageId: string, sectionId: string, fieldId: string) => void;
resetFieldForm: () => void;
}
export function useFieldManagement(): UseFieldManagementReturn {
const {
itemMasterFields,
addFieldToSection,
updateField,
deleteField,
addItemMasterField,
updateItemMasterField,
} = useItemMaster();
// 다이얼로그 상태
const [isFieldDialogOpen, setIsFieldDialogOpen] = useState(false);
const [selectedSectionForField, setSelectedSectionForField] = useState<number | null>(null);
const [editingFieldId, setEditingFieldId] = useState<number | null>(null);
// 입력 모드
const [fieldInputMode, setFieldInputMode] = useState<'master' | 'custom'>('custom');
const [showMasterFieldList, setShowMasterFieldList] = useState(false);
const [selectedMasterFieldId, setSelectedMasterFieldId] = useState('');
// 필드 폼 상태
const [newFieldName, setNewFieldName] = useState('');
const [newFieldKey, setNewFieldKey] = useState('');
const [newFieldInputType, setNewFieldInputType] = useState<'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'>('textbox');
const [newFieldRequired, setNewFieldRequired] = useState(false);
const [newFieldOptions, setNewFieldOptions] = useState('');
const [newFieldDescription, setNewFieldDescription] = useState('');
// 텍스트박스 컬럼
const [textboxColumns, setTextboxColumns] = useState<Array<{ id: string; name: string; key: string }>>([]);
const [isColumnDialogOpen, setIsColumnDialogOpen] = useState(false);
const [editingColumnId, setEditingColumnId] = useState<string | null>(null);
const [columnName, setColumnName] = useState('');
const [columnKey, setColumnKey] = useState('');
// 조건부 필드
const [newFieldConditionEnabled, setNewFieldConditionEnabled] = useState(false);
const [newFieldConditionTargetType, setNewFieldConditionTargetType] = useState<'field' | 'section'>('field');
const [newFieldConditionFields, setNewFieldConditionFields] = useState<ConditionalFieldConfig[]>([]);
const [newFieldConditionSections, setNewFieldConditionSections] = useState<string[]>([]);
const [tempConditionValue, setTempConditionValue] = useState('');
// 마스터 필드 선택 시 폼 자동 채우기
useEffect(() => {
if (fieldInputMode === 'master' && selectedMasterFieldId) {
const masterField = itemMasterFields.find(f => f.id === Number(selectedMasterFieldId));
if (masterField) {
setNewFieldName(masterField.field_name);
setNewFieldKey(masterField.id.toString());
setNewFieldInputType(masterField.field_type || 'textbox');
// properties에서 required 확인, 또는 validation_rules에서 확인
const isRequired = (masterField.properties as any)?.required || false;
setNewFieldRequired(isRequired);
setNewFieldOptions(masterField.options?.map(o => o.label).join(', ') || '');
setNewFieldDescription(masterField.description || '');
}
} else if (fieldInputMode === 'custom') {
// 직접 입력 모드로 전환 시 폼 초기화
setNewFieldName('');
setNewFieldKey('');
setNewFieldInputType('textbox');
setNewFieldRequired(false);
setNewFieldOptions('');
setNewFieldDescription('');
}
}, [fieldInputMode, selectedMasterFieldId, itemMasterFields]);
// 필드 추가
const handleAddField = (selectedPage: ItemPage | undefined) => {
if (!selectedPage || !selectedSectionForField || !newFieldName.trim() || !newFieldKey.trim()) {
toast.error('모든 필수 항목을 입력해주세요');
return;
}
// 조건부 표시 설정
const displayCondition: FieldDisplayCondition | undefined = newFieldConditionEnabled
? {
targetType: newFieldConditionTargetType,
fieldConditions: newFieldConditionTargetType === 'field' && newFieldConditionFields.length > 0
? newFieldConditionFields
: undefined,
sectionIds: newFieldConditionTargetType === 'section' && newFieldConditionSections.length > 0
? newFieldConditionSections
: undefined
}
: undefined;
// 텍스트박스 컬럼 설정
const hasColumns = newFieldInputType === 'textbox' && textboxColumns.length > 0;
// 마스터 항목에서 가져온 경우 master_field_id 설정
const masterFieldId = fieldInputMode === 'master' && selectedMasterFieldId
? Number(selectedMasterFieldId)
: null;
const newField: ItemField = {
id: editingFieldId ? Number(editingFieldId) : Date.now(),
section_id: Number(selectedSectionForField),
master_field_id: masterFieldId,
field_name: newFieldName,
field_type: newFieldInputType,
order_no: 0,
is_required: newFieldRequired,
placeholder: newFieldDescription || null,
default_value: null,
display_condition: displayCondition as Record<string, any> | null || null,
validation_rules: null,
options: newFieldInputType === 'dropdown' && newFieldOptions.trim()
? newFieldOptions.split(',').map(o => ({ label: o.trim(), value: o.trim() }))
: null,
properties: hasColumns ? {
multiColumn: true,
columnCount: textboxColumns.length,
columnNames: textboxColumns.map(c => c.name)
} : null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
if (editingFieldId) {
console.log('Updating field:', { pageId: selectedPage.id, sectionId: selectedSectionForField, fieldId: editingFieldId, fieldName: newField.field_name });
updateField(Number(editingFieldId), newField);
// 항목관리 탭의 마스터 항목도 업데이트 (동일한 fieldKey가 있으면)
const existingMasterField = itemMasterFields.find(mf => mf.id.toString() === newField.field_name);
if (existingMasterField) {
const updatedMasterField: Partial<ItemMasterField> = {
field_name: newField.field_name,
description: newField.placeholder ?? null,
properties: newField.properties,
updated_at: new Date().toISOString()
};
updateItemMasterField(existingMasterField.id, updatedMasterField);
}
toast.success('항목이 섹션에 수정되었습니다!');
} else {
console.log('Adding field to section:', { pageId: selectedPage.id, sectionId: selectedSectionForField, fieldName: newField.field_name });
// 1. 섹션에 항목 추가
addFieldToSection(Number(selectedSectionForField), newField);
// 2. 마스터 항목 선택이 아닌 경우에만 새 마스터 항목 자동 생성
const isFromMasterField = masterFieldId !== null;
const existingMasterField = itemMasterFields.find(mf => mf.id.toString() === newField.field_name);
if (!isFromMasterField && !existingMasterField) {
// ItemMasterField 타입에 맞게 필수 필드 포함
const newMasterFieldData: Omit<ItemMasterField, 'id' | 'tenant_id' | 'created_by' | 'updated_by' | 'created_at' | 'updated_at'> = {
field_name: newField.field_name,
field_type: newField.field_type,
description: newField.placeholder ?? null,
category: selectedPage.item_type,
is_common: false,
default_value: null,
options: newField.options ?? null,
validation_rules: null,
properties: newField.properties ?? null,
};
addItemMasterField(newMasterFieldData as any);
console.log('Field added to both section and master fields:', {
fieldId: newField.id,
fieldName: newMasterFieldData.field_name
});
toast.success('항목이 섹션에 추가되고 마스터 항목으로도 등록되었습니다!');
} else {
toast.success('항목이 섹션에 추가되었습니다!');
}
}
resetFieldForm();
};
// 필드 수정
const handleEditField = (sectionId: string, field: ItemField) => {
setSelectedSectionForField(Number(sectionId));
setEditingFieldId(field.id);
setNewFieldName(field.field_name);
setNewFieldKey(field.id.toString());
setNewFieldInputType(field.field_type);
setNewFieldRequired(field.is_required);
setNewFieldOptions(field.options?.map(opt => opt.value).join(', ') || '');
setNewFieldDescription('');
// 조건부 표시 설정 로드
if (field.display_condition) {
setNewFieldConditionEnabled(true);
setNewFieldConditionTargetType(field.display_condition.targetType);
setNewFieldConditionFields(field.display_condition.fieldConditions || []);
setNewFieldConditionSections(field.display_condition.sectionIds || []);
} else {
setNewFieldConditionEnabled(false);
setNewFieldConditionTargetType('field');
setNewFieldConditionFields([]);
setNewFieldConditionSections([]);
}
setIsFieldDialogOpen(true);
};
// 필드 삭제
const handleDeleteField = (pageId: string, sectionId: string, fieldId: string) => {
deleteField(Number(fieldId));
console.log('필드 삭제 완료:', fieldId);
};
// 폼 초기화
const resetFieldForm = () => {
setNewFieldName('');
setNewFieldKey('');
setNewFieldInputType('textbox');
setNewFieldRequired(false);
setNewFieldOptions('');
setNewFieldDescription('');
setNewFieldConditionEnabled(false);
setNewFieldConditionTargetType('field');
setNewFieldConditionFields([]);
setNewFieldConditionSections([]);
setTempConditionValue('');
setEditingFieldId(null);
setSelectedSectionForField(null);
setFieldInputMode('custom');
setSelectedMasterFieldId('');
setTextboxColumns([]);
setIsFieldDialogOpen(false);
};
return {
// 다이얼로그 상태
isFieldDialogOpen,
setIsFieldDialogOpen,
selectedSectionForField,
setSelectedSectionForField,
editingFieldId,
setEditingFieldId,
// 입력 모드
fieldInputMode,
setFieldInputMode,
showMasterFieldList,
setShowMasterFieldList,
selectedMasterFieldId,
setSelectedMasterFieldId,
// 필드 폼 상태
newFieldName,
setNewFieldName,
newFieldKey,
setNewFieldKey,
newFieldInputType,
setNewFieldInputType,
newFieldRequired,
setNewFieldRequired,
newFieldOptions,
setNewFieldOptions,
newFieldDescription,
setNewFieldDescription,
// 텍스트박스 컬럼
textboxColumns,
setTextboxColumns,
isColumnDialogOpen,
setIsColumnDialogOpen,
editingColumnId,
setEditingColumnId,
columnName,
setColumnName,
columnKey,
setColumnKey,
// 조건부 필드
newFieldConditionEnabled,
setNewFieldConditionEnabled,
newFieldConditionTargetType,
setNewFieldConditionTargetType,
newFieldConditionFields,
setNewFieldConditionFields,
newFieldConditionSections,
setNewFieldConditionSections,
tempConditionValue,
setTempConditionValue,
// 핸들러
handleAddField,
handleEditField,
handleDeleteField,
resetFieldForm,
};
}

View File

@@ -0,0 +1,214 @@
'use client';
import { useState } from 'react';
import { toast } from 'sonner';
import { useItemMaster } from '@/contexts/ItemMasterContext';
import type { ItemMasterField } from '@/contexts/ItemMasterContext';
export interface UseMasterFieldManagementReturn {
// 다이얼로그 상태
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' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
setNewMasterFieldInputType: (type: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea') => void;
newMasterFieldRequired: boolean;
setNewMasterFieldRequired: (required: boolean) => void;
newMasterFieldCategory: string;
setNewMasterFieldCategory: (category: string) => void;
newMasterFieldDescription: string;
setNewMasterFieldDescription: (desc: 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: React.Dispatch<React.SetStateAction<string[]>>;
// 핸들러
handleAddMasterField: () => void;
handleEditMasterField: (field: ItemMasterField) => void;
handleUpdateMasterField: () => void;
handleDeleteMasterField: (id: number) => void;
resetMasterFieldForm: () => void;
}
export function useMasterFieldManagement(): UseMasterFieldManagementReturn {
const {
itemMasterFields,
addItemMasterField,
updateItemMasterField,
deleteItemMasterField,
} = useItemMaster();
// 다이얼로그 상태
const [isMasterFieldDialogOpen, setIsMasterFieldDialogOpen] = useState(false);
const [editingMasterFieldId, setEditingMasterFieldId] = useState<number | null>(null);
// 폼 상태
const [newMasterFieldName, setNewMasterFieldName] = useState('');
const [newMasterFieldKey, setNewMasterFieldKey] = useState('');
const [newMasterFieldInputType, setNewMasterFieldInputType] = useState<'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'>('textbox');
const [newMasterFieldRequired, setNewMasterFieldRequired] = useState(false);
const [newMasterFieldCategory, setNewMasterFieldCategory] = useState('공통');
const [newMasterFieldDescription, setNewMasterFieldDescription] = useState('');
const [newMasterFieldOptions, setNewMasterFieldOptions] = useState('');
const [newMasterFieldAttributeType, setNewMasterFieldAttributeType] = useState<'custom' | 'unit' | 'material' | 'surface'>('custom');
const [newMasterFieldMultiColumn, setNewMasterFieldMultiColumn] = useState(false);
const [newMasterFieldColumnCount, setNewMasterFieldColumnCount] = useState(2);
const [newMasterFieldColumnNames, setNewMasterFieldColumnNames] = useState<string[]>(['컬럼1', '컬럼2']);
// 마스터 항목 추가
const handleAddMasterField = () => {
if (!newMasterFieldName.trim() || !newMasterFieldKey.trim()) {
toast.error('항목명과 필드 키를 입력해주세요');
return;
}
// ItemMasterField 타입에 맞게 필수 필드 포함
const newMasterFieldData: Omit<ItemMasterField, 'id' | 'tenant_id' | 'created_by' | 'updated_by' | 'created_at' | 'updated_at'> = {
field_name: newMasterFieldName,
field_type: newMasterFieldInputType,
category: newMasterFieldCategory || null,
description: newMasterFieldDescription || null,
is_common: false,
default_value: null,
options: newMasterFieldInputType === 'dropdown' && newMasterFieldOptions.trim()
? newMasterFieldOptions.split(',').map(o => ({ label: o.trim(), value: o.trim() }))
: null,
validation_rules: null,
properties: {
required: newMasterFieldRequired,
attributeType: newMasterFieldInputType === 'dropdown' ? newMasterFieldAttributeType : undefined,
multiColumn: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') ? newMasterFieldMultiColumn : undefined,
columnCount: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') && newMasterFieldMultiColumn ? newMasterFieldColumnCount : undefined,
columnNames: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') && newMasterFieldMultiColumn ? newMasterFieldColumnNames : undefined
},
};
addItemMasterField(newMasterFieldData as any);
resetMasterFieldForm();
toast.success('마스터 항목이 추가되었습니다 (저장 필요)');
};
// 마스터 항목 수정 시작
const handleEditMasterField = (field: ItemMasterField) => {
setEditingMasterFieldId(field.id);
setNewMasterFieldName(field.field_name);
setNewMasterFieldKey(field.id.toString());
setNewMasterFieldInputType(field.field_type || 'textbox');
setNewMasterFieldRequired((field.properties as any)?.required || false);
setNewMasterFieldCategory(field.category || '공통');
setNewMasterFieldDescription(field.description || '');
setNewMasterFieldOptions(field.options?.map(o => o.label).join(', ') || '');
setNewMasterFieldAttributeType((field.properties as any)?.attributeType || 'custom');
setNewMasterFieldMultiColumn((field.properties as any)?.multiColumn || false);
setNewMasterFieldColumnCount((field.properties as any)?.columnCount || 2);
setNewMasterFieldColumnNames((field.properties as any)?.columnNames || ['컬럼1', '컬럼2']);
setIsMasterFieldDialogOpen(true);
};
// 마스터 항목 업데이트
const handleUpdateMasterField = () => {
if (!editingMasterFieldId || !newMasterFieldName.trim() || !newMasterFieldKey.trim()) {
toast.error('항목명과 필드 키를 입력해주세요');
return;
}
const updateData: Partial<ItemMasterField> = {
field_name: newMasterFieldName,
field_type: newMasterFieldInputType,
category: newMasterFieldCategory || null,
description: newMasterFieldDescription || null,
options: newMasterFieldInputType === 'dropdown' && newMasterFieldOptions.trim()
? newMasterFieldOptions.split(',').map(o => ({ label: o.trim(), value: o.trim() }))
: null,
properties: {
required: newMasterFieldRequired,
attributeType: newMasterFieldInputType === 'dropdown' ? newMasterFieldAttributeType : undefined,
multiColumn: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') ? newMasterFieldMultiColumn : undefined,
columnCount: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') && newMasterFieldMultiColumn ? newMasterFieldColumnCount : undefined,
columnNames: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') && newMasterFieldMultiColumn ? newMasterFieldColumnNames : undefined
},
};
updateItemMasterField(editingMasterFieldId, updateData);
resetMasterFieldForm();
toast.success('마스터 항목이 수정되었습니다 (저장 필요)');
};
// 마스터 항목 삭제
const handleDeleteMasterField = (id: number) => {
if (confirm('이 마스터 항목을 삭제하시겠습니까?')) {
deleteItemMasterField(id);
toast.success('마스터 항목이 삭제되었습니다');
}
};
// 폼 초기화
const resetMasterFieldForm = () => {
setEditingMasterFieldId(null);
setNewMasterFieldName('');
setNewMasterFieldKey('');
setNewMasterFieldInputType('textbox');
setNewMasterFieldRequired(false);
setNewMasterFieldCategory('공통');
setNewMasterFieldDescription('');
setNewMasterFieldOptions('');
setNewMasterFieldAttributeType('custom');
setNewMasterFieldMultiColumn(false);
setNewMasterFieldColumnCount(2);
setNewMasterFieldColumnNames(['컬럼1', '컬럼2']);
setIsMasterFieldDialogOpen(false);
};
return {
// 다이얼로그 상태
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,
// 핸들러
handleAddMasterField,
handleEditMasterField,
handleUpdateMasterField,
handleDeleteMasterField,
resetMasterFieldForm,
};
}

View File

@@ -0,0 +1,259 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { toast } from 'sonner';
import { useItemMaster } from '@/contexts/ItemMasterContext';
import type { ItemPage } from '@/contexts/ItemMasterContext';
import { ApiError, getErrorMessage } from '@/lib/api/error-handler';
import { generateAbsolutePath } from '../utils/pathUtils';
export interface UsePageManagementReturn {
// 상태
selectedPageId: number | null;
setSelectedPageId: (id: number | null) => void;
selectedPage: ItemPage | undefined;
editingPageId: number | null;
setEditingPageId: (id: number | null) => void;
editingPageName: string;
setEditingPageName: (name: string) => void;
isPageDialogOpen: boolean;
setIsPageDialogOpen: (open: boolean) => void;
newPageName: string;
setNewPageName: (name: string) => void;
newPageItemType: 'FG' | 'PT' | 'SM' | 'RM' | 'CS';
setNewPageItemType: (type: 'FG' | 'PT' | 'SM' | 'RM' | 'CS') => void;
editingPathPageId: number | null;
setEditingPathPageId: (id: number | null) => void;
editingAbsolutePath: string;
setEditingAbsolutePath: (path: string) => void;
isLoading: boolean;
// 핸들러
handleAddPage: () => Promise<void>;
handleDuplicatePage: (pageId: number) => Promise<void>;
handleDeletePage: (pageId: number) => void;
handleUpdatePageName: (pageId: number, newName: string) => Promise<void>;
handleUpdateAbsolutePath: (pageId: number, newPath: string) => Promise<void>;
}
export function usePageManagement(): UsePageManagementReturn {
const {
itemPages,
addItemPage,
updateItemPage,
deleteItemPage,
} = useItemMaster();
// 상태
const [selectedPageId, setSelectedPageId] = useState<number | null>(itemPages[0]?.id || null);
const [editingPageId, setEditingPageId] = useState<number | null>(null);
const [editingPageName, setEditingPageName] = useState('');
const [isPageDialogOpen, setIsPageDialogOpen] = useState(false);
const [newPageName, setNewPageName] = useState('');
const [newPageItemType, setNewPageItemType] = useState<'FG' | 'PT' | 'SM' | 'RM' | 'CS'>('FG');
const [editingPathPageId, setEditingPathPageId] = useState<number | null>(null);
const [editingAbsolutePath, setEditingAbsolutePath] = useState('');
const [isLoading, setIsLoading] = useState(false);
// 선택된 페이지
const selectedPage = itemPages.find(p => p.id === selectedPageId);
// 마이그레이션 완료 추적용 ref
const migrationDoneRef = useRef<Set<number>>(new Set());
// 기존 페이지들에 절대경로 자동 생성 (마이그레이션)
useEffect(() => {
// itemPages가 비어있으면 스킵
if (itemPages.length === 0) return;
const pagesToMigrate = itemPages.filter(
page => !page.absolute_path && !migrationDoneRef.current.has(page.id)
);
// 마이그레이션할 페이지가 없으면 스킵
if (pagesToMigrate.length === 0) return;
// 마이그레이션 실행 (한 번에 처리)
pagesToMigrate.forEach(page => {
const absolutePath = generateAbsolutePath(page.item_type, page.page_name);
updateItemPage(page.id, { absolute_path: absolutePath });
migrationDoneRef.current.add(page.id);
});
console.log(`절대경로가 자동으로 생성되었습니다 (${pagesToMigrate.length}개 페이지)`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [itemPages.length]); // itemPages 길이가 변경될 때만 체크
// 페이지 추가
const handleAddPage = async () => {
if (!newPageName.trim()) {
toast.error('섹션명을 입력해주세요');
return;
}
try {
setIsLoading(true);
const absolutePath = generateAbsolutePath(newPageItemType, newPageName);
const newPage = await addItemPage({
page_name: newPageName,
item_type: newPageItemType,
absolute_path: absolutePath,
is_active: true,
sections: [],
order_no: 0,
});
// 새로 생성된 페이지를 선택
setSelectedPageId(newPage.id);
// 폼 초기화
setNewPageName('');
setNewPageItemType('FG');
setIsPageDialogOpen(false);
toast.success('페이지가 추가되었습니다');
} catch (err) {
if (err instanceof ApiError && err.errors) {
const errorMessages = Object.entries(err.errors)
.map(([field, messages]) => `${field}: ${messages.join(', ')}`)
.join('\n');
toast.error(errorMessages);
} else {
const errorMessage = getErrorMessage(err);
toast.error(errorMessage);
}
console.error('❌ Failed to create page:', err);
} finally {
setIsLoading(false);
}
};
// 페이지 복제
const handleDuplicatePage = async (pageId: number) => {
const originalPage = itemPages.find(p => p.id === pageId);
if (!originalPage) {
toast.error('페이지를 찾을 수 없습니다');
return;
}
try {
setIsLoading(true);
const duplicatedPageName = `${originalPage.page_name} (복제)`;
const absolutePath = generateAbsolutePath(originalPage.item_type, duplicatedPageName);
const newPage = await addItemPage({
page_name: duplicatedPageName,
item_type: originalPage.item_type,
sections: [], // 섹션은 별도 API로 복제해야 함
is_active: true,
absolute_path: absolutePath,
order_no: 0,
});
setSelectedPageId(newPage.id);
toast.success('페이지가 복제되었습니다');
// TODO: 원본 페이지의 섹션들도 복제 필요 (별도 API 호출)
} catch (err) {
const errorMessage = getErrorMessage(err);
toast.error(errorMessage);
console.error('❌ Failed to duplicate page:', err);
} finally {
setIsLoading(false);
}
};
// 페이지 삭제
const handleDeletePage = (pageId: number) => {
const pageToDelete = itemPages.find(p => p.id === pageId);
const sectionIds = pageToDelete?.sections.map(s => s.id) || [];
const fieldIds = pageToDelete?.sections.flatMap(s => s.fields?.map(f => f.id) || []) || [];
deleteItemPage(pageId);
// 삭제된 페이지가 선택된 페이지였다면 다른 페이지 선택
if (selectedPageId === pageId) {
const remainingPages = itemPages.filter(p => p.id !== pageId);
setSelectedPageId(remainingPages[0]?.id || null);
}
console.log('페이지 삭제 완료:', {
pageId,
removedSections: sectionIds.length,
removedFields: fieldIds.length
});
};
// 페이지명 수정
const handleUpdatePageName = async (pageId: number, newName: string) => {
if (!newName.trim()) {
toast.error('페이지명을 입력해주세요');
return;
}
try {
setIsLoading(true);
await updateItemPage(pageId, { page_name: newName });
setEditingPageId(null);
setEditingPageName('');
toast.success('페이지명이 수정되었습니다');
} catch (err) {
const errorMessage = getErrorMessage(err);
toast.error(errorMessage);
} finally {
setIsLoading(false);
}
};
// 절대경로 수정
const handleUpdateAbsolutePath = async (pageId: number, newPath: string) => {
if (!newPath.trim()) {
toast.error('절대경로를 입력해주세요');
return;
}
try {
setIsLoading(true);
await updateItemPage(pageId, { absolute_path: newPath });
setEditingPathPageId(null);
setEditingAbsolutePath('');
toast.success('절대경로가 수정되었습니다');
} catch (err) {
const errorMessage = getErrorMessage(err);
toast.error(errorMessage);
} finally {
setIsLoading(false);
}
};
return {
// 상태
selectedPageId,
setSelectedPageId,
selectedPage,
editingPageId,
setEditingPageId,
editingPageName,
setEditingPageName,
isPageDialogOpen,
setIsPageDialogOpen,
newPageName,
setNewPageName,
newPageItemType,
setNewPageItemType,
editingPathPageId,
setEditingPathPageId,
editingAbsolutePath,
setEditingAbsolutePath,
isLoading,
// 핸들러
handleAddPage,
handleDuplicatePage,
handleDeletePage,
handleUpdatePageName,
handleUpdateAbsolutePath,
};
}

View File

@@ -0,0 +1,247 @@
'use client';
import { useState } from 'react';
import { toast } from 'sonner';
import { useItemMaster } from '@/contexts/ItemMasterContext';
import type { ItemPage, ItemSection, SectionTemplate } from '@/contexts/ItemMasterContext';
export interface UseSectionManagementReturn {
// 상태
editingSectionId: number | null;
setEditingSectionId: (id: number | null) => void;
editingSectionTitle: string;
setEditingSectionTitle: (title: string) => void;
isSectionDialogOpen: boolean;
setIsSectionDialogOpen: (open: boolean) => void;
newSectionTitle: string;
setNewSectionTitle: (title: string) => void;
newSectionDescription: string;
setNewSectionDescription: (desc: string) => void;
newSectionType: 'fields' | 'bom';
setNewSectionType: (type: 'fields' | 'bom') => void;
sectionInputMode: 'custom' | 'template';
setSectionInputMode: (mode: 'custom' | 'template') => void;
selectedSectionTemplateId: number | null;
setSelectedSectionTemplateId: (id: number | null) => void;
expandedSections: Record<string, boolean>;
setExpandedSections: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
// 핸들러
handleAddSection: (selectedPage: ItemPage | undefined) => void;
handleLinkTemplate: (template: SectionTemplate, selectedPage: ItemPage | undefined) => void;
handleEditSectionTitle: (sectionId: number, currentTitle: string) => void;
handleSaveSectionTitle: (selectedPage: ItemPage | undefined) => void;
handleDeleteSection: (pageId: number, sectionId: number) => void;
toggleSection: (sectionId: string) => void;
resetSectionForm: () => void;
}
export function useSectionManagement(): UseSectionManagementReturn {
const {
itemPages,
addSectionToPage,
updateSection,
deleteSection,
addSectionTemplate,
tenantId,
} = useItemMaster();
// 상태
const [editingSectionId, setEditingSectionId] = useState<number | null>(null);
const [editingSectionTitle, setEditingSectionTitle] = useState('');
const [isSectionDialogOpen, setIsSectionDialogOpen] = useState(false);
const [newSectionTitle, setNewSectionTitle] = useState('');
const [newSectionDescription, setNewSectionDescription] = useState('');
const [newSectionType, setNewSectionType] = useState<'fields' | 'bom'>('fields');
const [sectionInputMode, setSectionInputMode] = useState<'custom' | 'template'>('custom');
const [selectedSectionTemplateId, setSelectedSectionTemplateId] = useState<number | null>(null);
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({});
// 섹션 추가
const handleAddSection = (selectedPage: ItemPage | undefined) => {
if (!selectedPage || !newSectionTitle.trim()) {
toast.error('하위섹션 제목을 입력해주세요');
return;
}
const sectionType: 'BASIC' | 'BOM' | 'CUSTOM' = newSectionType === 'bom' ? 'BOM' : 'BASIC';
const newSection: ItemSection = {
id: Date.now(),
page_id: selectedPage.id,
section_name: newSectionTitle,
section_type: sectionType,
description: newSectionDescription || undefined,
order_no: selectedPage.sections.length + 1,
is_collapsible: true,
is_default_open: true,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
fields: [],
bomItems: sectionType === 'BOM' ? [] : undefined
};
console.log('Adding section to page:', {
pageId: selectedPage.id,
page_name: selectedPage.page_name,
sectionTitle: newSection.section_name,
sectionType: newSection.section_type,
currentSectionCount: selectedPage.sections.length,
newSection: newSection
});
// 1. 페이지에 섹션 추가
addSectionToPage(selectedPage.id, newSection);
// 2. 섹션관리 탭에도 템플릿으로 자동 추가
const newTemplateData = {
tenant_id: tenantId ?? 0,
template_name: newSection.section_name,
section_type: newSection.section_type as 'BASIC' | 'BOM' | 'CUSTOM',
description: newSection.description ?? null,
default_fields: null,
created_by: null,
updated_by: null,
};
addSectionTemplate(newTemplateData);
console.log('Section added to both page and template:', {
sectionId: newSection.id,
templateTitle: newTemplateData.template_name
});
resetSectionForm();
toast.success(`${newSectionType === 'bom' ? 'BOM' : '일반'} 섹션이 페이지에 추가되고 템플릿으로도 등록되었습니다!`);
};
// 섹션 템플릿을 페이지에 연결
const handleLinkTemplate = (template: SectionTemplate, selectedPage: ItemPage | undefined) => {
if (!selectedPage) {
toast.error('페이지를 먼저 선택해주세요');
return;
}
// 템플릿을 섹션으로 변환하여 페이지에 추가
const newSection: Omit<ItemSection, 'id' | 'created_at' | 'updated_at'> = {
page_id: selectedPage.id,
section_name: template.template_name,
section_type: template.section_type,
description: template.description || undefined,
order_no: selectedPage.sections.length + 1,
is_collapsible: true,
is_default_open: true,
fields: template.fields ? template.fields.map((field, idx) => ({
id: Date.now() + idx,
section_id: 0, // 추후 업데이트됨
field_name: field.name,
field_type: field.property.inputType,
order_no: idx + 1,
is_required: field.property.required,
placeholder: field.description || null,
default_value: null,
display_condition: null,
validation_rules: null,
options: field.property.options
? field.property.options.map(opt => ({ label: opt, value: opt }))
: null,
properties: field.property.multiColumn ? {
multiColumn: true,
columnCount: field.property.columnCount,
columnNames: field.property.columnNames
} : null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})) : [],
bomItems: template.section_type === 'BOM' ? (template.bomItems || []) : undefined
};
console.log('Linking template to page:', {
templateId: template.id,
templateName: template.template_name,
pageId: selectedPage.id,
newSection
});
addSectionToPage(selectedPage.id, newSection);
resetSectionForm();
toast.success(`"${template.template_name}" 템플릿이 페이지에 연결되었습니다!`);
};
// 섹션 제목 수정 시작
const handleEditSectionTitle = (sectionId: number, currentTitle: string) => {
setEditingSectionId(sectionId);
setEditingSectionTitle(currentTitle);
};
// 섹션 제목 저장
const handleSaveSectionTitle = (selectedPage: ItemPage | undefined) => {
if (!selectedPage || !editingSectionId || !editingSectionTitle.trim()) {
toast.error('하위섹션 제목을 입력해주세요');
return;
}
updateSection(editingSectionId, { section_name: editingSectionTitle });
setEditingSectionId(null);
setEditingSectionTitle('');
toast.success('하위섹션 제목이 수정되었습니다 (저장 필요)');
};
// 섹션 삭제
const handleDeleteSection = (pageId: number, sectionId: number) => {
const page = itemPages.find(p => p.id === pageId);
const sectionToDelete = page?.sections.find(s => s.id === sectionId);
const fieldIds = sectionToDelete?.fields?.map(f => f.id) || [];
deleteSection(sectionId);
console.log('섹션 삭제 완료:', {
sectionId,
removedFields: fieldIds.length
});
};
// 섹션 확장/축소 토글
const toggleSection = (sectionId: string) => {
setExpandedSections(prev => ({ ...prev, [sectionId]: !prev[sectionId] }));
};
// 폼 초기화
const resetSectionForm = () => {
setNewSectionTitle('');
setNewSectionDescription('');
setNewSectionType('fields');
setSectionInputMode('custom');
setSelectedSectionTemplateId(null);
setIsSectionDialogOpen(false);
};
return {
// 상태
editingSectionId,
setEditingSectionId,
editingSectionTitle,
setEditingSectionTitle,
isSectionDialogOpen,
setIsSectionDialogOpen,
newSectionTitle,
setNewSectionTitle,
newSectionDescription,
setNewSectionDescription,
newSectionType,
setNewSectionType,
sectionInputMode,
setSectionInputMode,
selectedSectionTemplateId,
setSelectedSectionTemplateId,
expandedSections,
setExpandedSections,
// 핸들러
handleAddSection,
handleLinkTemplate,
handleEditSectionTitle,
handleSaveSectionTitle,
handleDeleteSection,
toggleSection,
resetSectionForm,
};
}

View File

@@ -0,0 +1,441 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { toast } from 'sonner';
import { useItemMaster } from '@/contexts/ItemMasterContext';
import {
FolderTree,
ListTree,
FileText,
Settings,
Layers,
Database,
Plus,
Folder
} from 'lucide-react';
export interface CustomTab {
id: string;
label: string;
icon: string;
isDefault: boolean;
order: number;
}
export interface AttributeSubTab {
id: string;
label: string;
key: string;
isDefault: boolean;
order: number;
}
export interface UseTabManagementReturn {
// 메인 탭 상태
customTabs: CustomTab[];
setCustomTabs: React.Dispatch<React.SetStateAction<CustomTab[]>>;
activeTab: string;
setActiveTab: (tab: string) => void;
// 속성 하위 탭 상태
attributeSubTabs: AttributeSubTab[];
setAttributeSubTabs: React.Dispatch<React.SetStateAction<AttributeSubTab[]>>;
activeAttributeTab: string;
setActiveAttributeTab: (tab: string) => void;
// 메인 탭 다이얼로그 상태
isAddTabDialogOpen: boolean;
setIsAddTabDialogOpen: (open: boolean) => void;
isManageTabsDialogOpen: boolean;
setIsManageTabsDialogOpen: (open: boolean) => void;
newTabLabel: string;
setNewTabLabel: (label: string) => void;
editingTabId: string | null;
setEditingTabId: (id: string | null) => void;
deletingTabId: string | null;
setDeletingTabId: (id: string | null) => void;
isDeleteTabDialogOpen: boolean;
setIsDeleteTabDialogOpen: (open: boolean) => void;
// 속성 하위 탭 다이얼로그 상태
isManageAttributeTabsDialogOpen: boolean;
setIsManageAttributeTabsDialogOpen: (open: boolean) => void;
isAddAttributeTabDialogOpen: boolean;
setIsAddAttributeTabDialogOpen: (open: boolean) => void;
newAttributeTabLabel: string;
setNewAttributeTabLabel: (label: string) => void;
editingAttributeTabId: string | null;
setEditingAttributeTabId: (id: string | null) => void;
deletingAttributeTabId: string | null;
setDeletingAttributeTabId: (id: string | null) => void;
isDeleteAttributeTabDialogOpen: boolean;
setIsDeleteAttributeTabDialogOpen: (open: boolean) => void;
// 핸들러
handleAddTab: () => void;
handleUpdateTab: () => void;
handleDeleteTab: (tabId: string) => void;
confirmDeleteTab: () => void;
handleAddAttributeTab: () => void;
handleUpdateAttributeTab: () => void;
handleDeleteAttributeTab: (tabId: string) => void;
confirmDeleteAttributeTab: () => void;
moveTabUp: (tabId: string) => void;
moveTabDown: (tabId: string) => void;
moveAttributeTabUp: (tabId: string) => void;
moveAttributeTabDown: (tabId: string) => void;
getTabIcon: (iconName: string) => any;
handleEditTabFromManage: (tab: CustomTab) => void;
}
export function useTabManagement(): UseTabManagementReturn {
const { itemMasterFields } = useItemMaster();
// 메인 탭 상태
const [customTabs, setCustomTabs] = useState<CustomTab[]>([
{ id: 'hierarchy', label: '계층구조', icon: 'FolderTree', isDefault: true, order: 1 },
{ id: 'sections', label: '섹션', icon: 'Layers', isDefault: true, order: 2 },
{ id: 'items', label: '항목', icon: 'ListTree', isDefault: true, order: 3 },
{ id: 'attributes', label: '속성', icon: 'Settings', isDefault: true, order: 4 }
]);
const [activeTab, setActiveTab] = useState('hierarchy');
// 속성 하위 탭 상태
const [attributeSubTabs, setAttributeSubTabs] = useState<AttributeSubTab[]>([]);
const [activeAttributeTab, setActiveAttributeTab] = useState('units');
// 메인 탭 다이얼로그 상태
const [isAddTabDialogOpen, setIsAddTabDialogOpen] = useState(false);
const [isManageTabsDialogOpen, setIsManageTabsDialogOpen] = useState(false);
const [newTabLabel, setNewTabLabel] = useState('');
const [editingTabId, setEditingTabId] = useState<string | null>(null);
const [deletingTabId, setDeletingTabId] = useState<string | null>(null);
const [isDeleteTabDialogOpen, setIsDeleteTabDialogOpen] = useState(false);
// 속성 하위 탭 다이얼로그 상태
const [isManageAttributeTabsDialogOpen, setIsManageAttributeTabsDialogOpen] = useState(false);
const [isAddAttributeTabDialogOpen, setIsAddAttributeTabDialogOpen] = useState(false);
const [newAttributeTabLabel, setNewAttributeTabLabel] = useState('');
const [editingAttributeTabId, setEditingAttributeTabId] = useState<string | null>(null);
const [deletingAttributeTabId, setDeletingAttributeTabId] = useState<string | null>(null);
const [isDeleteAttributeTabDialogOpen, setIsDeleteAttributeTabDialogOpen] = useState(false);
// 이전 필드 상태 추적용 ref (무한 루프 방지)
const prevFieldsRef = useRef<string>('');
// 마스터 항목이 추가/수정될 때 속성 탭 자동 생성
useEffect(() => {
// 현재 필드 상태를 문자열로 직렬화
const currentFieldsState = JSON.stringify(
itemMasterFields.map(f => ({ id: f.id, name: f.field_name })).sort((a, b) => a.id - b.id)
);
// 이전 상태와 동일하면 업데이트 스킵
if (prevFieldsRef.current === currentFieldsState) {
return;
}
prevFieldsRef.current = currentFieldsState;
setAttributeSubTabs(prev => {
const newTabs: AttributeSubTab[] = [];
const updates: { key: string; label: string }[] = [];
itemMasterFields.forEach(field => {
const existingTab = prev.find(tab => tab.key === field.id.toString());
if (!existingTab) {
const maxOrder = Math.max(...prev.map(t => t.order), ...newTabs.map(t => t.order), -1);
newTabs.push({
id: `attr-${field.id.toString()}`,
label: field.field_name,
key: field.id.toString(),
isDefault: false,
order: maxOrder + 1
});
} else if (existingTab.label !== field.field_name) {
updates.push({ key: existingTab.key, label: field.field_name });
}
});
// 변경사항 없으면 이전 상태 그대로 반환
if (newTabs.length === 0 && updates.length === 0) {
return prev;
}
let result = prev.map(tab => {
const update = updates.find(u => u.key === tab.key);
return update ? { ...tab, label: update.label } : tab;
});
result = [...result, ...newTabs];
// 중복 제거
return result.filter((tab, index, self) =>
index === self.findIndex(t => t.key === tab.key)
);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [itemMasterFields]);
// 메인 탭 핸들러
const handleAddTab = () => {
if (!newTabLabel.trim()) {
toast.error('탭 이름을 입력해주세요');
return;
}
const newTab: CustomTab = {
id: Date.now().toString(),
label: newTabLabel,
icon: 'FileText',
isDefault: false,
order: customTabs.length + 1
};
setCustomTabs(prev => [...prev, newTab]);
setNewTabLabel('');
setIsAddTabDialogOpen(false);
toast.success('탭이 추가되었습니다');
};
const handleUpdateTab = () => {
if (!newTabLabel.trim() || !editingTabId) {
toast.error('탭 이름을 입력해주세요');
return;
}
setCustomTabs(prev => prev.map(tab =>
tab.id === editingTabId ? { ...tab, label: newTabLabel } : tab
));
setEditingTabId(null);
setNewTabLabel('');
setIsAddTabDialogOpen(false);
setIsManageTabsDialogOpen(true);
toast.success('탭이 수정되었습니다');
};
const handleDeleteTab = (tabId: string) => {
const tab = customTabs.find(t => t.id === tabId);
if (!tab || tab.isDefault) {
toast.error('기본 탭은 삭제할 수 없습니다');
return;
}
setDeletingTabId(tabId);
setIsDeleteTabDialogOpen(true);
};
const confirmDeleteTab = () => {
if (!deletingTabId) return;
setCustomTabs(prev => prev.filter(t => t.id !== deletingTabId));
if (activeTab === deletingTabId) {
setActiveTab('hierarchy');
}
setIsDeleteTabDialogOpen(false);
setDeletingTabId(null);
toast.success('탭이 삭제되었습니다');
};
// 속성 하위 탭 핸들러
const handleAddAttributeTab = () => {
if (!newAttributeTabLabel.trim()) {
toast.error('탭 이름을 입력해주세요');
return;
}
const newTab: AttributeSubTab = {
id: `attr-${Date.now()}`,
label: newAttributeTabLabel,
key: `custom-${Date.now()}`,
isDefault: false,
order: attributeSubTabs.length
};
setAttributeSubTabs(prev => [...prev, newTab]);
setNewAttributeTabLabel('');
setIsAddAttributeTabDialogOpen(false);
toast.success('속성 탭이 추가되었습니다');
};
const handleUpdateAttributeTab = () => {
if (!newAttributeTabLabel.trim() || !editingAttributeTabId) {
toast.error('탭 이름을 입력해주세요');
return;
}
setAttributeSubTabs(prev => prev.map(tab =>
tab.id === editingAttributeTabId ? { ...tab, label: newAttributeTabLabel } : tab
));
setEditingAttributeTabId(null);
setNewAttributeTabLabel('');
setIsAddAttributeTabDialogOpen(false);
setIsManageAttributeTabsDialogOpen(true);
toast.success('속성 탭이 수정되었습니다');
};
const handleDeleteAttributeTab = (tabId: string) => {
const tab = attributeSubTabs.find(t => t.id === tabId);
if (!tab || tab.isDefault) {
toast.error('기본 속성 탭은 삭제할 수 없습니다');
return;
}
setDeletingAttributeTabId(tabId);
setIsDeleteAttributeTabDialogOpen(true);
};
const confirmDeleteAttributeTab = () => {
if (!deletingAttributeTabId) return;
setAttributeSubTabs(prev => prev.filter(t => t.id !== deletingAttributeTabId));
if (activeAttributeTab === deletingAttributeTabId) {
const firstTab = attributeSubTabs.find(t => t.id !== deletingAttributeTabId);
if (firstTab) {
setActiveAttributeTab(firstTab.key);
}
}
setIsDeleteAttributeTabDialogOpen(false);
setDeletingAttributeTabId(null);
toast.success('속성 탭이 삭제되었습니다');
};
// 탭 순서 변경 핸들러
const moveTabUp = (tabId: string) => {
const tabIndex = customTabs.findIndex(t => t.id === tabId);
if (tabIndex <= 0) return;
const newTabs = [...customTabs];
const temp = newTabs[tabIndex - 1].order;
newTabs[tabIndex - 1].order = newTabs[tabIndex].order;
newTabs[tabIndex].order = temp;
setCustomTabs(newTabs.sort((a, b) => a.order - b.order));
toast.success('탭 순서가 변경되었습니다');
};
const moveTabDown = (tabId: string) => {
const tabIndex = customTabs.findIndex(t => t.id === tabId);
if (tabIndex >= customTabs.length - 1) return;
const newTabs = [...customTabs];
const temp = newTabs[tabIndex + 1].order;
newTabs[tabIndex + 1].order = newTabs[tabIndex].order;
newTabs[tabIndex].order = temp;
setCustomTabs(newTabs.sort((a, b) => a.order - b.order));
toast.success('탭 순서가 변경되었습니다');
};
const moveAttributeTabUp = (tabId: string) => {
const tabIndex = attributeSubTabs.findIndex(t => t.id === tabId);
if (tabIndex <= 0) return;
const newTabs = [...attributeSubTabs];
const temp = newTabs[tabIndex - 1].order;
newTabs[tabIndex - 1].order = newTabs[tabIndex].order;
newTabs[tabIndex].order = temp;
setAttributeSubTabs(newTabs.sort((a, b) => a.order - b.order));
};
const moveAttributeTabDown = (tabId: string) => {
const tabIndex = attributeSubTabs.findIndex(t => t.id === tabId);
if (tabIndex >= attributeSubTabs.length - 1) return;
const newTabs = [...attributeSubTabs];
const temp = newTabs[tabIndex + 1].order;
newTabs[tabIndex + 1].order = newTabs[tabIndex].order;
newTabs[tabIndex].order = temp;
setAttributeSubTabs(newTabs.sort((a, b) => a.order - b.order));
};
// 아이콘 헬퍼
const getTabIcon = (iconName: string) => {
const icons: Record<string, any> = {
FolderTree,
ListTree,
FileText,
Settings,
Layers,
Database,
Plus,
Folder
};
return icons[iconName] || FileText;
};
// 탭 관리에서 수정 시작
const handleEditTabFromManage = (tab: CustomTab) => {
if (tab.isDefault) {
toast.error('기본 탭은 수정할 수 없습니다');
return;
}
setEditingTabId(tab.id);
setNewTabLabel(tab.label);
setIsManageTabsDialogOpen(false);
setIsAddTabDialogOpen(true);
};
return {
// 메인 탭 상태
customTabs,
setCustomTabs,
activeTab,
setActiveTab,
// 속성 하위 탭 상태
attributeSubTabs,
setAttributeSubTabs,
activeAttributeTab,
setActiveAttributeTab,
// 메인 탭 다이얼로그 상태
isAddTabDialogOpen,
setIsAddTabDialogOpen,
isManageTabsDialogOpen,
setIsManageTabsDialogOpen,
newTabLabel,
setNewTabLabel,
editingTabId,
setEditingTabId,
deletingTabId,
setDeletingTabId,
isDeleteTabDialogOpen,
setIsDeleteTabDialogOpen,
// 속성 하위 탭 다이얼로그 상태
isManageAttributeTabsDialogOpen,
setIsManageAttributeTabsDialogOpen,
isAddAttributeTabDialogOpen,
setIsAddAttributeTabDialogOpen,
newAttributeTabLabel,
setNewAttributeTabLabel,
editingAttributeTabId,
setEditingAttributeTabId,
deletingAttributeTabId,
setDeletingAttributeTabId,
isDeleteAttributeTabDialogOpen,
setIsDeleteAttributeTabDialogOpen,
// 핸들러
handleAddTab,
handleUpdateTab,
handleDeleteTab,
confirmDeleteTab,
handleAddAttributeTab,
handleUpdateAttributeTab,
handleDeleteAttributeTab,
confirmDeleteAttributeTab,
moveTabUp,
moveTabDown,
moveAttributeTabUp,
moveAttributeTabDown,
getTabIcon,
handleEditTabFromManage,
};
}

View File

@@ -0,0 +1,470 @@
'use client';
import { useState } from 'react';
import { toast } from 'sonner';
import { useItemMaster } from '@/contexts/ItemMasterContext';
import type { ItemPage, SectionTemplate, TemplateField, BOMItem, ItemMasterField } from '@/contexts/ItemMasterContext';
export interface UseTemplateManagementReturn {
// 섹션 템플릿 다이얼로그 상태
isSectionTemplateDialogOpen: boolean;
setIsSectionTemplateDialogOpen: (open: boolean) => void;
editingSectionTemplateId: number | null;
setEditingSectionTemplateId: (id: number | null) => void;
// 섹션 템플릿 폼 상태
newSectionTemplateTitle: string;
setNewSectionTemplateTitle: (title: string) => void;
newSectionTemplateDescription: string;
setNewSectionTemplateDescription: (desc: string) => void;
newSectionTemplateCategory: string[];
setNewSectionTemplateCategory: React.Dispatch<React.SetStateAction<string[]>>;
newSectionTemplateType: 'fields' | 'bom';
setNewSectionTemplateType: (type: 'fields' | 'bom') => void;
// 템플릿 불러오기 다이얼로그
isLoadTemplateDialogOpen: boolean;
setIsLoadTemplateDialogOpen: (open: boolean) => void;
selectedTemplateId: string | null;
setSelectedTemplateId: (id: string | null) => void;
// 템플릿 필드 다이얼로그 상태
isTemplateFieldDialogOpen: boolean;
setIsTemplateFieldDialogOpen: (open: boolean) => void;
currentTemplateId: number | null;
setCurrentTemplateId: (id: number | null) => void;
editingTemplateFieldId: number | null;
setEditingTemplateFieldId: (id: number | null) => void;
// 템플릿 필드 폼 상태
templateFieldName: string;
setTemplateFieldName: (name: string) => void;
templateFieldKey: string;
setTemplateFieldKey: (key: string) => void;
templateFieldInputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
setTemplateFieldInputType: (type: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea') => void;
templateFieldRequired: boolean;
setTemplateFieldRequired: (required: boolean) => void;
templateFieldOptions: string;
setTemplateFieldOptions: (options: string) => void;
templateFieldDescription: string;
setTemplateFieldDescription: (desc: string) => void;
templateFieldMultiColumn: boolean;
setTemplateFieldMultiColumn: (multi: boolean) => void;
templateFieldColumnCount: number;
setTemplateFieldColumnCount: (count: number) => void;
templateFieldColumnNames: string[];
setTemplateFieldColumnNames: React.Dispatch<React.SetStateAction<string[]>>;
// 템플릿 필드 마스터 항목 관련
templateFieldInputMode: 'custom' | 'master';
setTemplateFieldInputMode: (mode: 'custom' | 'master') => void;
templateFieldShowMasterFieldList: boolean;
setTemplateFieldShowMasterFieldList: (show: boolean) => void;
templateFieldSelectedMasterFieldId: string;
setTemplateFieldSelectedMasterFieldId: (id: string) => void;
// 핸들러
handleAddSectionTemplate: () => void;
handleEditSectionTemplate: (template: SectionTemplate) => void;
handleUpdateSectionTemplate: () => void;
handleDeleteSectionTemplate: (id: number) => void;
handleLoadTemplate: (selectedPage: ItemPage | undefined) => void;
handleAddTemplateField: () => void;
handleEditTemplateField: (templateId: number, field: TemplateField) => void;
handleDeleteTemplateField: (templateId: number, fieldId: string) => void;
handleAddBOMItemToTemplate: (templateId: number, item: Omit<BOMItem, 'id' | 'created_at' | 'updated_at' | 'tenant_id' | 'section_id'>) => void;
handleUpdateBOMItemInTemplate: (templateId: number, itemId: number, item: Partial<BOMItem>) => void;
handleDeleteBOMItemFromTemplate: (templateId: number, itemId: number) => void;
resetSectionTemplateForm: () => void;
resetTemplateFieldForm: () => void;
}
export function useTemplateManagement(): UseTemplateManagementReturn {
const {
sectionTemplates,
addSectionTemplate,
updateSectionTemplate,
deleteSectionTemplate,
addSectionToPage,
addItemMasterField,
itemMasterFields,
tenantId
} = useItemMaster();
// 섹션 템플릿 다이얼로그 상태
const [isSectionTemplateDialogOpen, setIsSectionTemplateDialogOpen] = useState(false);
const [editingSectionTemplateId, setEditingSectionTemplateId] = useState<number | null>(null);
// 섹션 템플릿 폼 상태
const [newSectionTemplateTitle, setNewSectionTemplateTitle] = useState('');
const [newSectionTemplateDescription, setNewSectionTemplateDescription] = useState('');
const [newSectionTemplateCategory, setNewSectionTemplateCategory] = useState<string[]>([]);
const [newSectionTemplateType, setNewSectionTemplateType] = useState<'fields' | 'bom'>('fields');
// 템플릿 불러오기 다이얼로그
const [isLoadTemplateDialogOpen, setIsLoadTemplateDialogOpen] = useState(false);
const [selectedTemplateId, setSelectedTemplateId] = useState<string | null>(null);
// 템플릿 필드 다이얼로그 상태
const [isTemplateFieldDialogOpen, setIsTemplateFieldDialogOpen] = useState(false);
const [currentTemplateId, setCurrentTemplateId] = useState<number | null>(null);
const [editingTemplateFieldId, setEditingTemplateFieldId] = useState<number | null>(null);
// 템플릿 필드 폼 상태
const [templateFieldName, setTemplateFieldName] = useState('');
const [templateFieldKey, setTemplateFieldKey] = useState('');
const [templateFieldInputType, setTemplateFieldInputType] = useState<'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'>('textbox');
const [templateFieldRequired, setTemplateFieldRequired] = useState(false);
const [templateFieldOptions, setTemplateFieldOptions] = useState('');
const [templateFieldDescription, setTemplateFieldDescription] = useState('');
const [templateFieldMultiColumn, setTemplateFieldMultiColumn] = useState(false);
const [templateFieldColumnCount, setTemplateFieldColumnCount] = useState(2);
const [templateFieldColumnNames, setTemplateFieldColumnNames] = useState<string[]>(['컬럼1', '컬럼2']);
// 템플릿 필드 마스터 항목 관련
const [templateFieldInputMode, setTemplateFieldInputMode] = useState<'custom' | 'master'>('custom');
const [templateFieldShowMasterFieldList, setTemplateFieldShowMasterFieldList] = useState(false);
const [templateFieldSelectedMasterFieldId, setTemplateFieldSelectedMasterFieldId] = useState('');
// 섹션 템플릿 추가
const handleAddSectionTemplate = () => {
if (!newSectionTemplateTitle.trim()) {
toast.error('섹션 제목을 입력해주세요');
return;
}
const newTemplateData = {
tenant_id: tenantId ?? 0,
template_name: newSectionTemplateTitle,
section_type: (newSectionTemplateType === 'bom' ? 'BOM' : 'BASIC') as 'BASIC' | 'BOM' | 'CUSTOM',
description: newSectionTemplateDescription || null,
default_fields: null,
category: newSectionTemplateCategory,
created_by: null,
updated_by: null,
};
console.log('Adding section template:', newTemplateData);
addSectionTemplate(newTemplateData);
resetSectionTemplateForm();
toast.success('섹션 템플릿이 추가되었습니다!');
};
// 섹션 템플릿 수정 시작
const handleEditSectionTemplate = (template: SectionTemplate) => {
setEditingSectionTemplateId(template.id);
setNewSectionTemplateTitle(template.template_name);
setNewSectionTemplateDescription(template.description || '');
setNewSectionTemplateCategory(template.category || []);
setNewSectionTemplateType(template.section_type === 'BOM' ? 'bom' : 'fields');
setIsSectionTemplateDialogOpen(true);
};
// 섹션 템플릿 업데이트
const handleUpdateSectionTemplate = () => {
if (!editingSectionTemplateId || !newSectionTemplateTitle.trim()) {
toast.error('섹션 제목을 입력해주세요');
return;
}
const updateData = {
template_name: newSectionTemplateTitle,
description: newSectionTemplateDescription || undefined,
category: newSectionTemplateCategory.length > 0 ? newSectionTemplateCategory : undefined,
section_type: (newSectionTemplateType === 'bom' ? 'BOM' : 'BASIC') as 'BASIC' | 'BOM' | 'CUSTOM'
};
console.log('Updating section template:', { id: editingSectionTemplateId, updateData });
updateSectionTemplate(editingSectionTemplateId, updateData);
resetSectionTemplateForm();
toast.success('섹션이 수정되었습니다 (저장 필요)');
};
// 섹션 템플릿 삭제
const handleDeleteSectionTemplate = (id: number) => {
if (confirm('이 섹션을 삭제하시겠습니까?')) {
deleteSectionTemplate(id);
toast.success('섹션이 삭제되었습니다');
}
};
// 템플릿 불러오기
const handleLoadTemplate = (selectedPage: ItemPage | undefined) => {
if (!selectedTemplateId || !selectedPage) {
toast.error('템플릿을 선택해주세요');
return;
}
const template = sectionTemplates.find(t => t.id === Number(selectedTemplateId));
if (!template) {
toast.error('템플릿을 찾을 수 없습니다');
return;
}
const newSection = {
page_id: selectedPage.id,
section_name: template.template_name,
section_type: template.section_type === 'BOM' ? 'BOM' as const : 'BASIC' as const,
description: template.description || undefined,
order_no: selectedPage.sections.length + 1,
is_collapsible: true,
is_default_open: true,
fields: [],
bomItems: template.section_type === 'BOM' ? [] : undefined
};
console.log('Loading template to section:', template.template_name, 'newSection:', newSection);
addSectionToPage(selectedPage.id, newSection);
setSelectedTemplateId(null);
setIsLoadTemplateDialogOpen(false);
toast.success('섹션이 불러와졌습니다');
};
// 템플릿 필드 추가
const handleAddTemplateField = () => {
if (!currentTemplateId || !templateFieldName.trim() || !templateFieldKey.trim()) {
toast.error('모든 필수 항목을 입력해주세요');
return;
}
const template = sectionTemplates.find(t => t.id === currentTemplateId);
if (!template) return;
// 마스터 필드에 없으면 자동 추가
const existingMasterField = itemMasterFields.find(f => f.id.toString() === templateFieldKey);
if (!existingMasterField && !editingTemplateFieldId) {
const newMasterFieldData: Omit<ItemMasterField, 'id' | 'tenant_id' | 'created_by' | 'updated_by' | 'created_at' | 'updated_at'> = {
field_name: templateFieldName,
field_type: templateFieldInputType,
category: '공통',
description: templateFieldDescription || null,
is_common: false,
default_value: null,
options: templateFieldInputType === 'dropdown' && templateFieldOptions.trim()
? templateFieldOptions.split(',').map(o => ({ label: o.trim(), value: o.trim() }))
: null,
validation_rules: null,
properties: {
inputType: templateFieldInputType,
required: templateFieldRequired,
multiColumn: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') ? templateFieldMultiColumn : undefined,
columnCount: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnCount : undefined,
columnNames: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnNames : undefined
},
};
addItemMasterField(newMasterFieldData as any);
toast.success('항목 탭에 자동으로 추가되었습니다');
}
// TemplateField 형식으로 생성
const newField: TemplateField = {
id: String(editingTemplateFieldId || Date.now()),
name: templateFieldName,
fieldKey: templateFieldKey,
property: {
inputType: templateFieldInputType,
required: templateFieldRequired,
options: templateFieldInputType === 'dropdown' && templateFieldOptions.trim()
? templateFieldOptions.split(',').map(o => o.trim())
: undefined,
multiColumn: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') ? templateFieldMultiColumn : undefined,
columnCount: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnCount : undefined,
columnNames: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnNames : undefined
},
description: templateFieldDescription || undefined
};
let updatedFields;
const currentFields = template.default_fields
? (typeof template.default_fields === 'string' ? JSON.parse(template.default_fields) : template.default_fields)
: [];
if (editingTemplateFieldId) {
updatedFields = Array.isArray(currentFields)
? currentFields.map((f: any) => String(f.id) === String(editingTemplateFieldId) ? newField : f)
: [];
toast.success('항목이 수정되었습니다');
} else {
updatedFields = Array.isArray(currentFields) ? [...currentFields, newField] : [newField];
toast.success('항목이 추가되었습니다');
}
updateSectionTemplate(currentTemplateId, { default_fields: updatedFields });
resetTemplateFieldForm();
};
// 템플릿 필드 수정 시작
const handleEditTemplateField = (templateId: number, field: TemplateField) => {
setCurrentTemplateId(templateId);
setEditingTemplateFieldId(Number(field.id));
setTemplateFieldName(field.name);
setTemplateFieldKey(field.fieldKey);
setTemplateFieldInputType(field.property.inputType);
setTemplateFieldRequired(field.property.required);
setTemplateFieldOptions(field.property.options?.join(', ') || '');
setTemplateFieldDescription(field.description || '');
setTemplateFieldMultiColumn(field.property.multiColumn || false);
setTemplateFieldColumnCount(field.property.columnCount || 2);
setTemplateFieldColumnNames(field.property.columnNames || ['컬럼1', '컬럼2']);
setIsTemplateFieldDialogOpen(true);
};
// 템플릿 필드 삭제
const handleDeleteTemplateField = (templateId: number, fieldId: string) => {
if (!confirm('이 항목을 삭제하시겠습니까?')) return;
const template = sectionTemplates.find(t => t.id === templateId);
if (!template) return;
const currentFields = template.default_fields
? (typeof template.default_fields === 'string' ? JSON.parse(template.default_fields) : template.default_fields)
: [];
const updatedFields = Array.isArray(currentFields)
? currentFields.filter((f: any) => String(f.id) !== String(fieldId))
: [];
updateSectionTemplate(templateId, { default_fields: updatedFields });
toast.success('항목이 삭제되었습니다');
};
// BOM 항목 추가
const handleAddBOMItemToTemplate = (templateId: number, item: Omit<BOMItem, 'id' | 'created_at' | 'updated_at' | 'tenant_id' | 'section_id'>) => {
const newItem: BOMItem = {
...item,
id: Date.now(),
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
tenant_id: tenantId ?? 0,
section_id: 0
};
const template = sectionTemplates.find(t => t.id === templateId);
if (!template) return;
const updatedBomItems = [...(template.bomItems || []), newItem];
updateSectionTemplate(templateId, { bomItems: updatedBomItems });
};
// BOM 항목 수정
const handleUpdateBOMItemInTemplate = (templateId: number, itemId: number, item: Partial<BOMItem>) => {
const template = sectionTemplates.find(t => t.id === templateId);
if (!template || !template.bomItems) return;
const updatedBomItems = template.bomItems.map(bom =>
bom.id === itemId ? { ...bom, ...item } : bom
);
updateSectionTemplate(templateId, { bomItems: updatedBomItems });
};
// BOM 항목 삭제
const handleDeleteBOMItemFromTemplate = (templateId: number, itemId: number) => {
const template = sectionTemplates.find(t => t.id === templateId);
if (!template || !template.bomItems) return;
const updatedBomItems = template.bomItems.filter(bom => bom.id !== itemId);
updateSectionTemplate(templateId, { bomItems: updatedBomItems });
};
// 섹션 템플릿 폼 초기화
const resetSectionTemplateForm = () => {
setEditingSectionTemplateId(null);
setNewSectionTemplateTitle('');
setNewSectionTemplateDescription('');
setNewSectionTemplateCategory([]);
setNewSectionTemplateType('fields');
setIsSectionTemplateDialogOpen(false);
};
// 템플릿 필드 폼 초기화
const resetTemplateFieldForm = () => {
setTemplateFieldName('');
setTemplateFieldKey('');
setTemplateFieldInputType('textbox');
setTemplateFieldRequired(false);
setTemplateFieldOptions('');
setTemplateFieldDescription('');
setTemplateFieldMultiColumn(false);
setTemplateFieldColumnCount(2);
setTemplateFieldColumnNames(['컬럼1', '컬럼2']);
setEditingTemplateFieldId(null);
setTemplateFieldInputMode('custom');
setTemplateFieldShowMasterFieldList(false);
setTemplateFieldSelectedMasterFieldId('');
setIsTemplateFieldDialogOpen(false);
};
return {
// 섹션 템플릿 다이얼로그 상태
isSectionTemplateDialogOpen,
setIsSectionTemplateDialogOpen,
editingSectionTemplateId,
setEditingSectionTemplateId,
// 섹션 템플릿 폼 상태
newSectionTemplateTitle,
setNewSectionTemplateTitle,
newSectionTemplateDescription,
setNewSectionTemplateDescription,
newSectionTemplateCategory,
setNewSectionTemplateCategory,
newSectionTemplateType,
setNewSectionTemplateType,
// 템플릿 불러오기 다이얼로그
isLoadTemplateDialogOpen,
setIsLoadTemplateDialogOpen,
selectedTemplateId,
setSelectedTemplateId,
// 템플릿 필드 다이얼로그 상태
isTemplateFieldDialogOpen,
setIsTemplateFieldDialogOpen,
currentTemplateId,
setCurrentTemplateId,
editingTemplateFieldId,
setEditingTemplateFieldId,
// 템플릿 필드 폼 상태
templateFieldName,
setTemplateFieldName,
templateFieldKey,
setTemplateFieldKey,
templateFieldInputType,
setTemplateFieldInputType,
templateFieldRequired,
setTemplateFieldRequired,
templateFieldOptions,
setTemplateFieldOptions,
templateFieldDescription,
setTemplateFieldDescription,
templateFieldMultiColumn,
setTemplateFieldMultiColumn,
templateFieldColumnCount,
setTemplateFieldColumnCount,
templateFieldColumnNames,
setTemplateFieldColumnNames,
// 템플릿 필드 마스터 항목 관련
templateFieldInputMode,
setTemplateFieldInputMode,
templateFieldShowMasterFieldList,
setTemplateFieldShowMasterFieldList,
templateFieldSelectedMasterFieldId,
setTemplateFieldSelectedMasterFieldId,
// 핸들러
handleAddSectionTemplate,
handleEditSectionTemplate,
handleUpdateSectionTemplate,
handleDeleteSectionTemplate,
handleLoadTemplate,
handleAddTemplateField,
handleEditTemplateField,
handleDeleteTemplateField,
handleAddBOMItemToTemplate,
handleUpdateBOMItemInTemplate,
handleDeleteBOMItemFromTemplate,
resetSectionTemplateForm,
resetTemplateFieldForm,
};
}

View File

@@ -25,8 +25,8 @@ interface HierarchyTabProps {
setEditingPathPageId: (id: number | null) => void;
editingAbsolutePath: string;
setEditingAbsolutePath: (path: string) => void;
editingSectionId: string | null;
setEditingSectionId: (id: string | null) => void;
editingSectionId: number | null;
setEditingSectionId: (id: number | null) => void;
editingSectionTitle: string;
setEditingSectionTitle: (title: string) => void;
hasUnsavedChanges: boolean;
@@ -51,7 +51,7 @@ interface HierarchyTabProps {
setIsPageDialogOpen: (open: boolean) => void;
setIsSectionDialogOpen: (open: boolean) => void;
setIsFieldDialogOpen: (open: boolean) => void;
handleEditSectionTitle: (sectionId: string, title: string) => void;
handleEditSectionTitle: (sectionId: number, title: string) => void;
handleSaveSectionTitle: () => void;
moveSection: (dragIndex: number, hoverIndex: number) => void;
deleteSection: (pageId: number, sectionId: number) => void;