Files
sam-react-prod/src/components/items/ItemMasterDataManagement/hooks/useSectionManagement.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

245 lines
9.1 KiB
TypeScript

'use client';
import { useState } from 'react';
import { toast } from 'sonner';
import { useItemMaster } from '@/contexts/ItemMasterContext';
import type { ItemPage, ItemSection, SectionTemplate } from '@/contexts/ItemMasterContext';
import { sectionService } from '../services';
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) => Promise<void>;
handleLinkTemplate: (template: SectionTemplate, selectedPage: ItemPage | undefined) => Promise<void>;
handleEditSectionTitle: (sectionId: number, currentTitle: string) => void;
handleSaveSectionTitle: (selectedPage: ItemPage | undefined) => void;
handleUnlinkSection: (pageId: number, sectionId: number) => void; // 계층구조 탭용 - 연결 해제
handleDeleteSection: (pageId: number, sectionId: number) => void; // 섹션 탭용 - 실제 삭제
toggleSection: (sectionId: string) => void;
resetSectionForm: () => void;
}
export function useSectionManagement(): UseSectionManagementReturn {
const {
itemPages,
addSectionToPage,
updateSection,
deleteSection,
linkSectionToPage, // 2025-11-26: 기존 섹션을 페이지에 연결 (entity_relationships)
unlinkSectionFromPage, // 2025-11-26: EntityRelationship API 사용
} = 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 = async (selectedPage: ItemPage | undefined) => {
if (!selectedPage || !newSectionTitle.trim()) {
toast.error('하위섹션 제목을 입력해주세요');
return;
}
const sectionType: 'BASIC' | 'BOM' | 'CUSTOM' = newSectionType === 'bom' ? 'BOM' : 'BASIC';
const newSection: Omit<ItemSection, 'id' | 'created_at' | 'updated_at'> = {
page_id: selectedPage.id,
title: newSectionTitle,
section_type: sectionType,
description: newSectionDescription || undefined,
order_no: selectedPage.sections.length + 1,
is_template: false,
is_default: false,
is_collapsible: true,
is_default_open: true,
fields: [],
bom_items: sectionType === 'BOM' ? [] : undefined
};
console.log('Adding section to page:', {
pageId: selectedPage.id,
page_name: selectedPage.page_name,
sectionTitle: newSection.title,
sectionType: newSection.section_type,
currentSectionCount: selectedPage.sections.length,
});
try {
// 페이지에 섹션 추가 (API 호출)
// 2025-11-26: sectionsAsTemplates가 itemPages에서 useMemo로 파생되므로
// 별도의 addSectionTemplate 호출 불필요 (자동 동기화)
await addSectionToPage(selectedPage.id, newSection);
console.log('Section added to page:', {
sectionTitle: newSection.title
});
resetSectionForm();
toast.success(`${newSectionType === 'bom' ? 'BOM' : '일반'} 섹션이 추가되었습니다!`);
} catch (error) {
console.error('섹션 추가 실패:', error);
toast.error('섹션 추가에 실패했습니다. 다시 시도해주세요.');
}
};
// 기존 섹션을 페이지에 연결 (entity_relationships 테이블 사용)
// 2025-11-26: 새 섹션 생성이 아닌, 기존 섹션을 연결만 함
const handleLinkTemplate = async (template: SectionTemplate, selectedPage: ItemPage | undefined) => {
if (!selectedPage) {
toast.error('페이지를 먼저 선택해주세요');
return;
}
// 이미 연결된 섹션인지 확인
const isAlreadyLinked = selectedPage.sections.some(s => s.id === template.id);
if (isAlreadyLinked) {
toast.error('이미 페이지에 연결된 섹션입니다');
return;
}
console.log('Linking existing section to page:', {
sectionId: template.id,
sectionName: template.template_name,
pageId: selectedPage.id,
orderNo: selectedPage.sections.length + 1,
});
try {
// 기존 섹션을 페이지에 연결 (entity_relationships에 레코드 추가)
await linkSectionToPage(selectedPage.id, template.id, selectedPage.sections.length + 1);
resetSectionForm();
toast.success(`"${template.template_name}" 섹션이 페이지에 연결되었습니다!`);
} catch (error) {
console.error('섹션 연결 실패:', error);
toast.error('섹션 연결에 실패했습니다. 다시 시도해주세요.');
}
};
// 섹션 제목 수정 시작
const handleEditSectionTitle = (sectionId: number, currentTitle: string) => {
setEditingSectionId(sectionId);
setEditingSectionTitle(currentTitle);
};
// 섹션 제목 저장
const handleSaveSectionTitle = async (selectedPage: ItemPage | undefined) => {
if (!selectedPage || !editingSectionId || !editingSectionTitle.trim()) {
toast.error('하위섹션 제목을 입력해주세요');
return;
}
try {
await updateSection(editingSectionId, { title: editingSectionTitle });
setEditingSectionId(null);
setEditingSectionTitle('');
toast.success('섹션 제목이 수정되었습니다!');
} catch (error) {
console.error('섹션 제목 수정 실패:', error);
toast.error('섹션 제목 수정에 실패했습니다.');
}
};
// 섹션 연결 해제 (계층구조 탭용 - 페이지에서만 분리, 섹션 데이터는 유지)
// 2025-11-26: EntityRelationship API 사용 (DELETE /pages/{pageId}/unlink-section/{sectionId})
const handleUnlinkSection = async (pageId: number, sectionId: number) => {
try {
await unlinkSectionFromPage(pageId, sectionId);
console.log('섹션 연결 해제 완료:', { pageId, sectionId });
toast.success('섹션 연결이 해제되었습니다');
} catch (error) {
console.error('섹션 연결 해제 실패:', error);
toast.error('섹션 연결 해제에 실패했습니다.');
}
};
// 섹션 삭제 (섹션 탭용 - 실제 데이터 삭제)
const handleDeleteSection = async (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) || [];
try {
await deleteSection(sectionId);
console.log('섹션 삭제 완료:', {
sectionId,
removedFields: fieldIds.length
});
toast.success('섹션이 삭제되었습니다!');
} catch (error) {
console.error('섹션 삭제 실패:', error);
toast.error('섹션 삭제에 실패했습니다.');
}
};
// 섹션 확장/축소 토글
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,
handleUnlinkSection,
handleDeleteSection,
toggleSection,
resetSectionForm,
};
}