fix: 페이지 삭제 시 섹션 동기화 및 코드 정리

- 페이지 삭제 시 독립 섹션 목록 갱신 추가 (독립 엔티티 아키텍처)
- ItemForm 컴포넌트 분리 완료 (1607→415줄, 74% 감소)
- ItemMasterDataManagement 중복 코드 제거 (getInputTypeLabel 헬퍼)
- 문서 업데이트 (realtime-sync-fixes.md)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-11-28 15:25:33 +09:00
parent 65a8510c0b
commit 9d0cb073ba
16 changed files with 1462 additions and 1435 deletions

View File

@@ -1,6 +1,6 @@
# claudedocs 문서 맵
> 프로젝트 기술 문서 인덱스 (Last Updated: 2025-11-27)
> 프로젝트 기술 문서 인덱스 (Last Updated: 2025-11-28)
## 폴더 구조
@@ -39,7 +39,9 @@ claudedocs/
| 파일 | 설명 |
|------|------|
| `[IMPL-2025-11-27] realtime-sync-fixes.md` | ⭐ **최신** - 실시간 동기화 수정 (BOM, 섹션 복제, 항목 수정) |
| `[API-REQUEST-2025-11-28] dynamic-page-rendering-api.md` | ⭐ **최신** - 동적 페이지 렌더링 API 요청서 |
| `[PLAN-2025-11-27] item-form-component-separation.md` | ✅ **완료** - ItemForm 컴포넌트 분리 (1607→415줄, 74% 감소) |
| `[IMPL-2025-11-27] realtime-sync-fixes.md` | 실시간 동기화 수정 (BOM, 섹션 복제, 항목 수정, **페이지 삭제 시 섹션 동기화** 2025-11-28) |
| `item-master-api-pending-tasks.md` | 진행중인 API 연동 작업 |
| `item-master-pending-integration.md` | 대기중인 통합 작업 |
| `item-master-specification.md` | API 명세 |

File diff suppressed because it is too large Load Diff

View File

@@ -171,11 +171,84 @@ console.log('[ItemMasterDataManagement] 📋 sectionsAsTemplates changed:', {...
---
### 4. 페이지 삭제 시 섹션 동기화 (`ItemMasterContext.tsx`) - 2025-11-28
**문제**: 계층구조에서 페이지 삭제 시 연결된 섹션들이 섹션탭에서 사라짐
**예상 동작**: 페이지 삭제 → 연결된 섹션은 unlink만 되고 독립 섹션으로 복귀 → 섹션탭에서 표시
**실제 동작**: 페이지 삭제 → 섹션이 UI에서 완전히 사라짐 (삭제된 것처럼 보임)
**원인 분석**:
1. **백엔드는 정상**: `ItemPageService.php``destroy()` 메서드가 독립 엔티티 아키텍처 준수
- `entity_relationships`에서 페이지-섹션 관계만 삭제 (섹션 자체는 유지)
- 섹션/필드는 독립 엔티티로 계속 존재
2. **프론트엔드 상태 동기화 누락**:
- `deleteItemPage``setItemPages`만 업데이트
- `independentSections` 상태는 갱신 안 됨
- `sectionsAsTemplates` useMemo가 `[itemPages, independentSections]` 의존
- 결과: 섹션이 `itemPages`에서 제거되었지만 `independentSections`에도 없어서 UI에서 사라짐
**해결**: 페이지 삭제 후 `refreshIndependentSections()` 호출 추가
```typescript
// deleteItemPage 함수 수정
const deleteItemPage = async (id: number) => {
try {
const response = await itemMasterApi.pages.delete(id);
if (!response.success) {
throw new Error(response.message || '페이지 삭제 실패');
}
// state 업데이트
setItemPages(prev => prev.filter(page => page.id !== id));
// 2025-11-28: 페이지 삭제 후 독립 섹션 목록 갱신
// 백엔드에서 섹션은 삭제되지 않고 연결만 해제되므로 (독립 엔티티 아키텍처)
// 독립 섹션 목록을 새로고침해야 섹션 탭에서 해당 섹션이 표시됨
try {
await refreshIndependentSections();
console.log('[ItemMasterContext] 페이지 삭제 후 독립 섹션 갱신 완료');
} catch (refreshError) {
// 갱신 실패해도 페이지 삭제는 성공한 상태이므로 경고만 출력
console.warn('[ItemMasterContext] 독립 섹션 갱신 실패:', refreshError);
}
console.log('[ItemMasterContext] 페이지 삭제 성공:', id);
} catch (error) {
// ...
}
};
```
**백엔드 코드 참조** (`ItemPageService.php:95-129`):
```php
/**
* 페이지 삭제 (Soft Delete)
* 독립 엔티티 아키텍처: 페이지만 삭제하고 연결된 섹션/필드는 unlink만 수행
*/
public function destroy(int $id): void
{
// 1. entity_relationships에서 이 페이지의 모든 자식 관계 해제
EntityRelationship::where('parent_type', EntityRelationship::TYPE_PAGE)
->where('parent_id', $id)
->where('is_locked', false)
->delete();
// 2. 페이지만 Soft Delete (섹션/필드는 독립 엔티티로 유지)
$page->update(['deleted_by' => $userId]);
$page->delete();
}
```
---
## 수정된 파일 목록
| 파일 | 수정 내용 |
|---|---|
| `src/contexts/ItemMasterContext.tsx` | BOM 동기화, 섹션 복제 수정 |
| `src/contexts/ItemMasterContext.tsx` | BOM 동기화, 섹션 복제 수정, **페이지 삭제 시 독립 섹션 갱신 (2025-11-28)** |
| `src/components/items/ItemMasterDataManagement/hooks/useTemplateManagement.ts` | 항목 수정 로직 추가 |
| `src/components/items/ItemMasterDataManagement/dialogs/TemplateFieldDialog.tsx` | prop 타입 수정 (void → void \| Promise<void>) |
@@ -185,4 +258,8 @@ console.log('[ItemMasterDataManagement] 📋 sectionsAsTemplates changed:', {...
1. **양방향 동기화 필수**: 데이터 변경 시 `itemPages``independentSections` 모두 업데이트해야 함
2. **null vs undefined**: API 응답의 null 값이 transformer 거치면서 undefined로 바뀔 수 있음 → `== null` 사용 권장
3. **useMemo 의존성**: `sectionsAsTemplates`의 의존성 배열 확인 → 두 상태가 모두 업데이트되어야 재계산됨
3. **useMemo 의존성**: `sectionsAsTemplates`의 의존성 배열 확인 → 두 상태가 모두 업데이트되어야 재계산됨
4. **독립 엔티티 아키텍처 인지**: 페이지/섹션/필드는 각각 독립 엔티티이며 `entity_relationships`로 연결됨
- 페이지 삭제 시 → 섹션은 삭제 안 됨 (연결만 해제) → `refreshIndependentSections()` 필요
- 섹션 삭제 시 → 필드는 삭제 안 됨 (연결만 해제) → `refreshIndependentFields()` 필요 (해당 시)
- 프론트엔드에서 연결 해제된 엔티티들이 적절히 표시되도록 상태 갱신 필수

View File

@@ -1,6 +1,14 @@
# ItemForm.tsx 컴포넌트 분리 계획
## 작업 일자: 2025-11-27 (분석)
## 작업 일자: 2025-11-27 (분석) → 2025-11-28 (완료)
## ✅ 최종 결과 요약
| 항목 | Before | After | 감소율 |
|------|--------|-------|--------|
| **index.tsx** | 1,607줄 | **415줄** | **74%** |
| **useState 수** | 25개+ | **0개** (훅으로 이동) | **100%** |
| **파일 수** | 1개 | **21개** | 모듈화 완료 |
---
@@ -264,13 +272,55 @@ src/components/items/ItemForm/
### Phase 5: 훅 & 컨텍스트 ✅ 완료 (2025-11-27)
- [x] context/ItemFormContext.tsx 생성 (~80줄)
- [x] context/index.ts export 파일 생성
- [x] hooks/useItemFormState.ts 생성 (~280줄) - 25+ useState 통합
- [x] hooks/useItemFormState.ts 생성 (~364줄) - 25+ useState 통합
- [x] hooks/useBOMManagement.ts 생성 (~180줄) - BOM 라인 관리
- [x] hooks/useBendingDetails.ts 생성 (~150줄) - 전개도 계산
- [x] hooks/index.ts export 파일 생성
### Phase 6: 테스트 & 검증
### Phase 6: index.tsx에 훅 적용 ✅ 완료 (2025-11-28)
- [x] useItemFormState 훅을 index.tsx에 적용
- [x] 25+ useState → 훅에서 구조 분해 할당
- [x] handleItemTypeChange → resetAllStates 헬퍼 함수 활용
- [x] 미사용 import 정리
- [x] 빌드 테스트 통과
### Phase 7: 테스트 & 검증 (추후 진행)
- [ ] 모든 품목 유형 등록 테스트
- [ ] 수정 모드 테스트
- [ ] 폼 검증 테스트
- [ ] BOM 추가/삭제 테스트
- [ ] BOM 추가/삭제 테스트
---
## 9. 최종 파일 구조
```
src/components/items/ItemForm/
├── index.tsx (415줄) ← 1,607줄에서 74% 감소
├── constants.ts (72줄)
├── types.ts (50줄)
├── ValidationAlert.tsx (45줄)
├── FormHeader.tsx (63줄)
├── BendingDiagramSection.tsx (~300줄)
├── BOMSection.tsx (~280줄)
├── context/
│ ├── ItemFormContext.tsx (~80줄)
│ └── index.ts
├── hooks/
│ ├── useItemFormState.ts (364줄) ← 상태 관리 통합
│ ├── useBOMManagement.ts (~180줄)
│ ├── useBendingDetails.ts (~150줄)
│ └── index.ts
└── forms/
├── index.ts
├── ProductForm.tsx (~120줄)
├── MaterialForm.tsx (~350줄)
├── PartForm.tsx (~273줄)
└── parts/
├── index.ts
├── AssemblyPartForm.tsx (~300줄)
├── BendingPartForm.tsx (~280줄)
└── PurchasedPartForm.tsx (~270줄)
```
**총 21개 파일로 모듈화 완료**

View File

@@ -1,4 +1,4 @@
import { Loader2 } from 'lucide-react';
import { PageLoadingSpinner } from '@/components/ui/loading-spinner';
/**
* Protected Group Loading UI
@@ -7,20 +7,13 @@ import { Loader2 } from 'lucide-react';
* - DashboardLayout 내에서 표시됨 (사이드바, 헤더 유지)
* - React Suspense 자동 적용
* - 페이지 전환 시 즉각적인 피드백
* - 대시보드 스타일로 통일
*/
export default function ProtectedLoading() {
return (
<div className="flex items-center justify-center min-h-[calc(100vh-200px)]">
<div className="text-center space-y-4">
<div className="relative inline-flex">
<div className="w-16 h-16 border-4 border-primary/20 border-t-primary rounded-full animate-spin" />
<Loader2 className="w-8 h-8 text-primary absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 animate-pulse" />
</div>
<div className="space-y-2">
<p className="text-lg font-medium text-foreground"> ...</p>
<p className="text-sm text-muted-foreground"> </p>
</div>
</div>
</div>
<PageLoadingSpinner
text="페이지를 불러오는 중..."
minHeight="min-h-[calc(100vh-200px)]"
/>
);
}

View File

@@ -279,7 +279,7 @@ export function LoginPage() {
>
{isLoggingIn ? (
<>
<div className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-solid border-current border-r-transparent mr-2"></div>
<div className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-solid border-primary-foreground border-r-transparent mr-2"></div>
{t('loggingIn') || '로그인 중...'}
</>
) : (

View File

@@ -258,9 +258,9 @@ export function SignupPage() {
if (isChecking) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
<p className="text-muted-foreground">Loading...</p>
<div className="text-center space-y-4">
<div className="inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent"></div>
<p className="text-muted-foreground font-medium"> ...</p>
</div>
</div>
);

View File

@@ -2,6 +2,7 @@
import { Suspense } from "react";
import { MainDashboard } from "./MainDashboard";
import { PageLoadingSpinner } from "@/components/ui/loading-spinner";
/**
* Dashboard - 통합 대시보드 컴포넌트
@@ -14,20 +15,10 @@ import { MainDashboard } from "./MainDashboard";
* - 권한 제어: 백엔드에서 역할에 따라 데이터 제한
*/
// 공통 로딩 컴포넌트
const DashboardLoading = () => (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-center space-y-4">
<div className="inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent"></div>
<p className="text-muted-foreground font-medium"> ...</p>
</div>
</div>
);
export function Dashboard() {
console.log('🎨 Dashboard component rendering...');
return (
<Suspense fallback={<DashboardLoading />}>
<Suspense fallback={<PageLoadingSpinner text="대시보드를 불러오는 중..." />}>
<MainDashboard />
</Suspense>
);

File diff suppressed because it is too large Load Diff

View File

@@ -30,7 +30,6 @@ import {
transformPagesResponse,
transformSectionsResponse,
transformSectionTemplatesResponse,
transformMasterFieldsResponse,
transformFieldsResponse,
transformCustomTabsResponse,
transformUnitOptionsResponse,
@@ -78,41 +77,40 @@ const INPUT_TYPE_OPTIONS = [
{ value: 'textarea', label: '텍스트영역' }
];
// 입력 타입 라벨 변환 헬퍼 함수 (중복 코드 제거)
const getInputTypeLabel = (inputType: string | undefined): string => {
const labels: Record<string, string> = {
textbox: '텍스트박스',
number: '숫자',
dropdown: '드롭다운',
checkbox: '체크박스',
date: '날짜',
textarea: '텍스트영역',
};
return labels[inputType || ''] || '텍스트박스';
};
export function ItemMasterDataManagement() {
const {
itemPages,
loadItemPages,
addItemPage: _addItemPage,
updateItemPage,
deleteItemPage,
addSectionToPage: _addSectionToPage,
updateSection,
deleteSection,
addFieldToSection: _addFieldToSection,
updateField: _updateField,
deleteField: _deleteField,
reorderFields,
itemMasterFields,
loadItemMasterFields,
addItemMasterField: _addItemMasterField,
updateItemMasterField: _updateItemMasterField,
deleteItemMasterField: _deleteItemMasterField,
sectionTemplates,
loadSectionTemplates,
addSectionTemplate: _addSectionTemplate,
updateSectionTemplate: _updateSectionTemplate,
deleteSectionTemplate: _deleteSectionTemplate,
resetAllData,
tenantId: _tenantId,
// 2025-11-26 추가: 독립 엔티티 관리
independentSections,
loadIndependentSections,
independentFields: _independentFields,
loadIndependentFields,
refreshIndependentSections,
refreshIndependentFields,
linkSectionToPage,
unlinkSectionFromPage: _unlinkSectionFromPage,
linkFieldToSection,
unlinkFieldFromSection,
getSectionUsage,
@@ -133,7 +131,7 @@ export function ItemMasterDataManagement() {
pageCount: itemPages.length,
pages: itemPages.map(p => ({
id: p.id,
name: p.name,
name: p.page_name,
sectionsCount: p.sections.length,
sections: p.sections.map(s => ({
id: s.id,
@@ -160,8 +158,7 @@ export function ItemMasterDataManagement() {
isPageDialogOpen, setIsPageDialogOpen,
newPageName, setNewPageName, newPageItemType, setNewPageItemType,
editingPathPageId, setEditingPathPageId, editingAbsolutePath, setEditingAbsolutePath,
handleAddPage, handleDuplicatePage, handleDeletePage: _handleDeletePage,
handleUpdatePageName: _handleUpdatePageName, handleUpdateAbsolutePath: _handleUpdateAbsolutePath,
handleAddPage, handleDuplicatePage,
} = pageManagement;
const {
@@ -173,10 +170,9 @@ export function ItemMasterDataManagement() {
newSectionType, setNewSectionType,
sectionInputMode, setSectionInputMode,
selectedSectionTemplateId, setSelectedSectionTemplateId,
expandedSections: _expandedSections, setExpandedSections: _setExpandedSections,
handleAddSection, handleLinkTemplate,
handleEditSectionTitle, handleSaveSectionTitle,
handleUnlinkSection, handleDeleteSection: _handleDeleteSection, toggleSection: _toggleSection,
handleUnlinkSection,
} = sectionManagement;
const {
@@ -202,7 +198,7 @@ export function ItemMasterDataManagement() {
newFieldConditionFields, setNewFieldConditionFields,
newFieldConditionSections, setNewFieldConditionSections,
tempConditionValue, setTempConditionValue,
handleAddField, handleEditField, handleDeleteField: _handleDeleteField,
handleAddField, handleEditField,
} = fieldManagement;
const {
@@ -864,15 +860,7 @@ export function ItemMasterDataManagement() {
{unitOptions.map((option) => {
const columns = attributeColumns['units'] || [];
const hasColumns = columns.length > 0 && option.columnValues;
const inputTypeLabel =
option.inputType === 'textbox' ? '텍스트박스' :
option.inputType === 'number' ? '숫자' :
option.inputType === 'dropdown' ? '드롭다운' :
option.inputType === 'checkbox' ? '체크박스' :
option.inputType === 'date' ? '날짜' :
option.inputType === 'textarea' ? '텍스트영역' :
'텍스트박스';
return (
<div key={option.id} className="p-4 border rounded hover:bg-gray-50 transition-colors">
<div className="flex items-start justify-between gap-3">
@@ -880,7 +868,7 @@ export function ItemMasterDataManagement() {
<div className="flex items-center gap-2">
<span className="font-medium text-base">{option.label}</span>
{option.inputType && (
<Badge variant="outline" className="text-xs">{inputTypeLabel}</Badge>
<Badge variant="outline" className="text-xs">{getInputTypeLabel(option.inputType)}</Badge>
)}
{option.required && (
<Badge variant="destructive" className="text-xs"></Badge>
@@ -966,15 +954,7 @@ export function ItemMasterDataManagement() {
{materialOptions.map((option) => {
const columns = attributeColumns['materials'] || [];
const hasColumns = columns.length > 0 && option.columnValues;
const inputTypeLabel =
option.inputType === 'textbox' ? '텍스트박스' :
option.inputType === 'number' ? '숫자' :
option.inputType === 'dropdown' ? '드롭다운' :
option.inputType === 'checkbox' ? '체크박스' :
option.inputType === 'date' ? '날짜' :
option.inputType === 'textarea' ? '텍스트영역' :
'텍스트박스';
return (
<div key={option.id} className="p-4 border rounded hover:bg-gray-50 transition-colors">
<div className="flex items-start justify-between gap-3">
@@ -982,7 +962,7 @@ export function ItemMasterDataManagement() {
<div className="flex items-center gap-2">
<span className="font-medium text-base">{option.label}</span>
{option.inputType && (
<Badge variant="outline" className="text-xs">{inputTypeLabel}</Badge>
<Badge variant="outline" className="text-xs">{getInputTypeLabel(option.inputType)}</Badge>
)}
{option.required && (
<Badge variant="destructive" className="text-xs"></Badge>
@@ -1068,15 +1048,8 @@ export function ItemMasterDataManagement() {
{surfaceTreatmentOptions.map((option) => {
const columns = attributeColumns['surface'] || [];
const hasColumns = columns.length > 0 && option.columnValues;
const inputTypeLabel =
option.inputType === 'textbox' ? '텍스트박스' :
option.inputType === 'number' ? '숫자' :
option.inputType === 'dropdown' ? '드롭다운' :
option.inputType === 'checkbox' ? '체크박스' :
option.inputType === 'date' ? '날짜' :
option.inputType === 'textarea' ? '텍스트영역' :
'텍스트박스';
const inputTypeLabel = getInputTypeLabel(option.inputType);
return (
<div key={option.id} className="p-4 border rounded hover:bg-gray-50 transition-colors">
<div className="flex items-start justify-between gap-3">
@@ -1173,15 +1146,8 @@ export function ItemMasterDataManagement() {
<div className="space-y-3">
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
{propertiesArray.map((property: any) => {
const inputTypeLabel =
property.type === 'textbox' ? '텍스트박스' :
property.type === 'number' ? '숫자' :
property.type === 'dropdown' ? '드롭다운' :
property.type === 'checkbox' ? '체크박스' :
property.type === 'date' ? '날짜' :
property.type === 'textarea' ? '텍스트영역' :
'텍스트박스';
const inputTypeLabel = getInputTypeLabel(property.type);
return (
<div key={property.id} className="p-4 border rounded hover:bg-gray-50 transition-colors">
<div className="flex items-start justify-between gap-3">
@@ -1283,15 +1249,8 @@ export function ItemMasterDataManagement() {
{currentOptions.map((option) => {
const columns = attributeColumns[currentTabKey] || [];
const hasColumns = columns.length > 0 && option.columnValues;
const inputTypeLabel =
option.inputType === 'textbox' ? '텍스트박스' :
option.inputType === 'number' ? '숫자' :
option.inputType === 'dropdown' ? '드롭다운' :
option.inputType === 'checkbox' ? '체크박스' :
option.inputType === 'date' ? '날짜' :
option.inputType === 'textarea' ? '텍스트영역' :
'텍스트박스';
const inputTypeLabel = getInputTypeLabel(option.inputType);
return (
<div key={option.id} className="p-4 border rounded hover:bg-gray-50 transition-colors">
<div className="flex items-start justify-between gap-3">

View File

@@ -5,7 +5,7 @@ import { Badge } from '@/components/ui/badge';
import {
GripVertical,
Edit,
X
Unlink
} from 'lucide-react';
// 입력방식 옵션 (ItemMasterDataManagement에서 사용하는 상수)
@@ -111,8 +111,9 @@ export function DraggableField({ field, index, moveField, onDelete, onEdit }: Dr
size="sm"
variant="ghost"
onClick={onDelete}
title="섹션에서 연결 해제"
>
<X className="h-4 w-4 text-red-500" />
<Unlink className="h-4 w-4 text-orange-500" />
</Button>
</div>
</div>

View File

@@ -8,7 +8,7 @@ import {
Edit,
Check,
X,
Trash2
Unlink
} from 'lucide-react';
interface DraggableSectionProps {
@@ -120,7 +120,7 @@ export function DraggableSection({
onClick={onDelete}
title="페이지에서 연결 해제"
>
<X className="h-4 w-4 text-gray-500" />
<Unlink className="h-4 w-4 text-orange-500" />
</Button>
</div>
</div>

View File

@@ -17,7 +17,7 @@ import {
import { type ItemType } from '@/types/item';
interface ItemTypeSelectProps {
value?: ItemType;
value?: ItemType | '';
onChange: (value: ItemType) => void;
disabled?: boolean;
required?: boolean;
@@ -56,7 +56,7 @@ export default function ItemTypeSelect({
)}
<Select
value={value}
value={value || undefined}
onValueChange={onChange}
disabled={disabled}
>

View File

@@ -1,5 +1,6 @@
// 로딩 스피너 컴포넌트
// API 호출 중 로딩 상태 표시용
// 대시보드 스타일로 통일 (border-4 border-solid border-primary border-r-transparent)
import React from 'react';
@@ -15,15 +16,35 @@ export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
text
}) => {
const sizeClasses = {
sm: 'h-4 w-4',
md: 'h-8 w-8',
lg: 'h-12 w-12'
sm: 'h-4 w-4 border-2',
md: 'h-8 w-8 border-4',
lg: 'h-12 w-12 border-4'
};
return (
<div className={`flex flex-col items-center justify-center gap-2 ${className}`}>
<div className={`animate-spin rounded-full border-b-2 border-primary ${sizeClasses[size]}`} />
<div className={`animate-spin rounded-full border-solid border-primary border-r-transparent ${sizeClasses[size]}`} />
{text && <p className="text-sm text-muted-foreground">{text}</p>}
</div>
);
};
// 페이지 레벨 로딩 스피너 (전체 화면 중앙 배치)
interface PageLoadingSpinnerProps {
text?: string;
minHeight?: string;
}
export const PageLoadingSpinner: React.FC<PageLoadingSpinnerProps> = ({
text = '불러오는 중...',
minHeight = 'min-h-[60vh]'
}) => {
return (
<div className={`flex items-center justify-center ${minHeight}`}>
<div className="text-center space-y-4">
<div className="inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent"></div>
<p className="text-muted-foreground font-medium">{text}</p>
</div>
</div>
);
};

View File

@@ -1303,6 +1303,17 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) {
// state 업데이트
setItemPages(prev => prev.filter(page => page.id !== id));
// 2025-11-28: 페이지 삭제 후 독립 섹션 목록 갱신
// 백엔드에서 섹션은 삭제되지 않고 연결만 해제되므로 (독립 엔티티 아키텍처)
// 독립 섹션 목록을 새로고침해야 섹션 탭에서 해당 섹션이 표시됨
try {
await refreshIndependentSections();
console.log('[ItemMasterContext] 페이지 삭제 후 독립 섹션 갱신 완료');
} catch (refreshError) {
// 갱신 실패해도 페이지 삭제는 성공한 상태이므로 경고만 출력
console.warn('[ItemMasterContext] 독립 섹션 갱신 실패:', refreshError);
}
console.log('[ItemMasterContext] 페이지 삭제 성공:', id);
} catch (error) {
const errorMessage = getErrorMessage(error);

File diff suppressed because one or more lines are too long