Files
sam-react-prod/src/components/items/ItemMasterDataManagement/hooks/useAttributeManagement.ts
byeongcheolryu 0552b02ba9 refactor: 품목기준관리 서비스 레이어 도입 및 버그 수정
서비스 레이어 리팩토링:
- services/ 폴더 생성 (fieldService, masterFieldService, sectionService, pageService, templateService, attributeService)
- 도메인 로직 중앙화 (validation, parsing, transform)
- hooks와 dialogs에서 서비스 호출로 변경

버그 수정:
- 섹션탭 실시간 동기화 문제 수정 (sectionsAsTemplates 중복 제거 순서 변경)
- 422 Validation Error 수정 (createIndependentField → addFieldToSection)
- 페이지 삭제 시 섹션-필드 연결 유지 (refreshIndependentSections 대신 직접 이동)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 14:23:57 +09:00

378 lines
14 KiB
TypeScript

'use client';
import { useState, useEffect, useRef } from 'react';
import { toast } from 'sonner';
import { useItemMaster } from '@/contexts/ItemMasterContext';
import type { MasterOption, OptionColumn } from '../types';
import { attributeService } from '../services';
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();
// 속성 옵션 상태 (기본값 하드코딩 - TODO: 나중에 백엔드 API로 대체)
const [unitOptions, setUnitOptions] = useState<MasterOption[]>([
{ id: 'unit-1', value: 'EA', label: 'EA (개)', isActive: true },
{ id: 'unit-2', value: 'KG', label: 'KG (킬로그램)', isActive: true },
{ id: 'unit-3', value: 'M', label: 'M (미터)', isActive: true },
{ id: 'unit-4', value: 'MM', label: 'MM (밀리미터)', isActive: true },
{ id: 'unit-5', value: 'L', label: 'L (리터)', isActive: true },
{ id: 'unit-6', value: 'SET', label: 'SET (세트)', isActive: true },
{ id: 'unit-7', value: 'BOX', label: 'BOX (박스)', isActive: true },
{ id: 'unit-8', value: 'ROLL', label: 'ROLL (롤)', isActive: true },
]);
const [materialOptions, setMaterialOptions] = useState<MasterOption[]>([
{ id: 'mat-1', value: 'SUS304', label: 'SUS304 (스테인리스)', isActive: true },
{ id: 'mat-2', value: 'SUS316', label: 'SUS316 (스테인리스)', isActive: true },
{ id: 'mat-3', value: 'AL6061', label: 'AL6061 (알루미늄)', isActive: true },
{ id: 'mat-4', value: 'AL5052', label: 'AL5052 (알루미늄)', isActive: true },
{ id: 'mat-5', value: 'SS400', label: 'SS400 (일반강)', isActive: true },
{ id: 'mat-6', value: 'S45C', label: 'S45C (탄소강)', isActive: true },
{ id: 'mat-7', value: 'POM', label: 'POM (폴리아세탈)', isActive: true },
{ id: 'mat-8', value: 'PEEK', label: 'PEEK (폴리에테르에테르케톤)', isActive: true },
]);
const [surfaceTreatmentOptions, setSurfaceTreatmentOptions] = useState<MasterOption[]>([
{ id: 'surf-1', value: 'NONE', label: '없음', isActive: true },
{ id: 'surf-2', value: 'ANODIZE', label: '아노다이징', isActive: true },
{ id: 'surf-3', value: 'PLATING', label: '도금', isActive: true },
{ id: 'surf-4', value: 'PAINTING', label: '도장', isActive: true },
{ id: 'surf-5', value: 'PASSIVATION', label: '부동태처리', isActive: true },
{ id: 'surf-6', value: 'SANDBLAST', label: '샌드블라스트', isActive: true },
{ id: 'surf-7', value: 'POLISHING', label: '폴리싱', isActive: true },
]);
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,
};
}