{/* 오늘의 이슈 (새 리스트 형태) */}
{dashboardSettings.todayIssueList && (
-
+
+
+
)}
{/* 일일 일보 (Enhanced) */}
{dashboardSettings.dailyReport && (
-
+
+
+
)}
{/* 현황판 (Enhanced - 아이콘 + 컬러 테마) */}
{(dashboardSettings.statusBoard?.enabled ?? dashboardSettings.todayIssue.enabled) && (
-
+
+
+
)}
{/* 당월 예상 지출 내역 (Enhanced) */}
diff --git a/src/components/business/construction/estimates/EstimateDetailForm.tsx b/src/components/business/construction/estimates/EstimateDetailForm.tsx
index fbe2e6aa..95467636 100644
--- a/src/components/business/construction/estimates/EstimateDetailForm.tsx
+++ b/src/components/business/construction/estimates/EstimateDetailForm.tsx
@@ -1,6 +1,7 @@
'use client';
import { useState, useCallback, useMemo, useEffect } from 'react';
+import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import { Loader2 } from 'lucide-react';
import { getExpenseItemOptions, updateEstimate, type ExpenseItemOption } from './actions';
@@ -21,8 +22,12 @@ import type {
PriceAdjustmentData,
} from './types';
import { getEmptyEstimateDetailFormData, estimateDetailToFormData } from './types';
-import { ElectronicApprovalModal } from './modals/ElectronicApprovalModal';
-import { EstimateDocumentModal } from './modals/EstimateDocumentModal';
+const ElectronicApprovalModal = dynamic(
+ () => import('./modals/ElectronicApprovalModal').then(mod => ({ default: mod.ElectronicApprovalModal })),
+);
+const EstimateDocumentModal = dynamic(
+ () => import('./modals/EstimateDocumentModal').then(mod => ({ default: mod.EstimateDocumentModal })),
+);
// MOCK_MATERIALS 제거됨 - API 데이터 사용
import {
EstimateInfoSection,
diff --git a/src/components/business/construction/management/ConstructionDetailClient.tsx b/src/components/business/construction/management/ConstructionDetailClient.tsx
index 3b5ad154..4ab4bbcf 100644
--- a/src/components/business/construction/management/ConstructionDetailClient.tsx
+++ b/src/components/business/construction/management/ConstructionDetailClient.tsx
@@ -1,6 +1,7 @@
'use client';
import { useState, useEffect, useCallback, useMemo } from 'react';
+import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import { getTodayString, formatDate } from '@/lib/utils/date';
import { Plus, Trash2, FileText, Upload, X } from 'lucide-react';
@@ -26,7 +27,9 @@ import {
completeConstruction,
} from './actions';
import { getOrderDetailFull } from '../order-management/actions';
-import { OrderDocumentModal } from '../order-management/modals/OrderDocumentModal';
+const OrderDocumentModal = dynamic(
+ () => import('../order-management/modals/OrderDocumentModal').then(mod => ({ default: mod.OrderDocumentModal })),
+);
import type {
ConstructionManagementDetail,
ConstructionDetailFormData,
diff --git a/src/components/hr/AttendanceManagement/index.tsx b/src/components/hr/AttendanceManagement/index.tsx
index 20ec00b8..87793e34 100644
--- a/src/components/hr/AttendanceManagement/index.tsx
+++ b/src/components/hr/AttendanceManagement/index.tsx
@@ -252,16 +252,16 @@ export function AttendanceManagement() {
// 테이블 컬럼 정의
const tableColumns = useMemo(() => [
{ key: 'no', label: '번호', className: 'w-[60px] text-center' },
- { key: 'department', label: '부서', className: 'min-w-[80px]' },
- { key: 'position', label: '직책', className: 'min-w-[100px]' },
- { key: 'name', label: '이름', className: 'min-w-[60px]' },
- { key: 'rank', label: '직급', className: 'min-w-[60px]' },
- { key: 'baseDate', label: '기준일', className: 'min-w-[100px]' },
- { key: 'checkIn', label: '출근', className: 'min-w-[60px]' },
- { key: 'checkOut', label: '퇴근', className: 'min-w-[60px]' },
- { key: 'breakTime', label: '휴게', className: 'min-w-[60px]' },
- { key: 'overtime', label: '연장근무', className: 'min-w-[80px]' },
- { key: 'reason', label: '사유', className: 'min-w-[80px]' },
+ { key: 'department', label: '부서', className: 'min-w-[80px]', sortable: true },
+ { key: 'position', label: '직책', className: 'min-w-[100px]', sortable: true },
+ { key: 'name', label: '이름', className: 'min-w-[60px]', sortable: true },
+ { key: 'rank', label: '직급', className: 'min-w-[60px]', sortable: true },
+ { key: 'baseDate', label: '기준일', className: 'min-w-[100px]', sortable: true },
+ { key: 'checkIn', label: '출근', className: 'min-w-[60px]', sortable: true },
+ { key: 'checkOut', label: '퇴근', className: 'min-w-[60px]', sortable: true },
+ { key: 'breakTime', label: '휴게', className: 'min-w-[60px]', sortable: true },
+ { key: 'overtime', label: '연장근무', className: 'min-w-[80px]', sortable: true },
+ { key: 'reason', label: '사유', className: 'min-w-[80px]', sortable: true },
], []);
// 체크박스 토글
diff --git a/src/components/hr/SalaryManagement/index.tsx b/src/components/hr/SalaryManagement/index.tsx
index 072158a6..c81c6ae4 100644
--- a/src/components/hr/SalaryManagement/index.tsx
+++ b/src/components/hr/SalaryManagement/index.tsx
@@ -304,18 +304,18 @@ export function SalaryManagement() {
// ===== 테이블 컬럼 (부서, 직책, 이름, 직급, 기본급, 수당, 초과근무, 상여, 공제, 실지급액, 일자, 상태, 작업) =====
const tableColumns = useMemo(() => [
- { key: 'department', label: '부서' },
- { key: 'position', label: '직책' },
- { key: 'name', label: '이름' },
- { key: 'rank', label: '직급' },
- { key: 'baseSalary', label: '기본급', className: 'text-right' },
- { key: 'allowance', label: '수당', className: 'text-right' },
- { key: 'overtime', label: '초과근무', className: 'text-right' },
- { key: 'bonus', label: '상여', className: 'text-right' },
- { key: 'deduction', label: '공제', className: 'text-right' },
- { key: 'netPayment', label: '실지급액', className: 'text-right' },
- { key: 'paymentDate', label: '일자', className: 'text-center' },
- { key: 'status', label: '상태', className: 'text-center' },
+ { key: 'department', label: '부서', sortable: true },
+ { key: 'position', label: '직책', sortable: true },
+ { key: 'name', label: '이름', sortable: true },
+ { key: 'rank', label: '직급', sortable: true },
+ { key: 'baseSalary', label: '기본급', className: 'text-right', sortable: true },
+ { key: 'allowance', label: '수당', className: 'text-right', sortable: true },
+ { key: 'overtime', label: '초과근무', className: 'text-right', sortable: true },
+ { key: 'bonus', label: '상여', className: 'text-right', sortable: true },
+ { key: 'deduction', label: '공제', className: 'text-right', sortable: true },
+ { key: 'netPayment', label: '실지급액', className: 'text-right', sortable: true },
+ { key: 'paymentDate', label: '일자', className: 'text-center', sortable: true },
+ { key: 'status', label: '상태', className: 'text-center', sortable: true },
], []);
// ===== filterConfig 기반 통합 필터 시스템 =====
diff --git a/src/components/hr/VacationManagement/index.tsx b/src/components/hr/VacationManagement/index.tsx
index 1cb27f2e..36f7fdff 100644
--- a/src/components/hr/VacationManagement/index.tsx
+++ b/src/components/hr/VacationManagement/index.tsx
@@ -391,41 +391,41 @@ export function VacationManagement() {
// 휴가 사용현황: 번호|부서|직책|이름|직급|입사일|기본|부여|사용|잔액
return [
{ key: 'no', label: '번호', className: 'w-[60px] text-center' },
- { key: 'department', label: '부서' },
- { key: 'position', label: '직책' },
- { key: 'name', label: '이름' },
- { key: 'rank', label: '직급' },
- { key: 'hireDate', label: '입사일' },
- { key: 'base', label: '기본', className: 'text-center' },
- { key: 'granted', label: '부여', className: 'text-center' },
- { key: 'used', label: '사용', className: 'text-center' },
- { key: 'remaining', label: '잔여', className: 'text-center' },
+ { key: 'department', label: '부서', sortable: true },
+ { key: 'position', label: '직책', sortable: true },
+ { key: 'name', label: '이름', sortable: true },
+ { key: 'rank', label: '직급', sortable: true },
+ { key: 'hireDate', label: '입사일', sortable: true },
+ { key: 'base', label: '기본', className: 'text-center', sortable: true },
+ { key: 'granted', label: '부여', className: 'text-center', sortable: true },
+ { key: 'used', label: '사용', className: 'text-center', sortable: true },
+ { key: 'remaining', label: '잔여', className: 'text-center', sortable: true },
];
} else if (mainTab === 'grant') {
// 휴가 부여현황: 번호|부서|직책|이름|직급|유형|부여일|부여휴가일수|사유
return [
{ key: 'no', label: '번호', className: 'w-[60px] text-center' },
- { key: 'department', label: '부서' },
- { key: 'position', label: '직책' },
- { key: 'name', label: '이름' },
- { key: 'rank', label: '직급' },
- { key: 'type', label: '유형' },
- { key: 'grantDate', label: '부여일' },
- { key: 'grantDays', label: '부여휴가일수', className: 'text-center' },
- { key: 'reason', label: '사유' },
+ { key: 'department', label: '부서', sortable: true },
+ { key: 'position', label: '직책', sortable: true },
+ { key: 'name', label: '이름', sortable: true },
+ { key: 'rank', label: '직급', sortable: true },
+ { key: 'type', label: '유형', sortable: true },
+ { key: 'grantDate', label: '부여일', sortable: true },
+ { key: 'grantDays', label: '부여휴가일수', className: 'text-center', sortable: true },
+ { key: 'reason', label: '사유', sortable: true },
];
} else {
// 휴가 신청현황: 번호|부서|직책|이름|직급|휴가기간|휴가일수|상태|신청일
return [
{ key: 'no', label: '번호', className: 'w-[60px] text-center' },
- { key: 'department', label: '부서' },
- { key: 'position', label: '직책' },
- { key: 'name', label: '이름' },
- { key: 'rank', label: '직급' },
- { key: 'period', label: '휴가기간' },
- { key: 'days', label: '휴가일수', className: 'text-center' },
- { key: 'status', label: '상태', className: 'text-center' },
- { key: 'requestDate', label: '신청일' },
+ { key: 'department', label: '부서', sortable: true },
+ { key: 'position', label: '직책', sortable: true },
+ { key: 'name', label: '이름', sortable: true },
+ { key: 'rank', label: '직급', sortable: true },
+ { key: 'period', label: '휴가기간', sortable: true },
+ { key: 'days', label: '휴가일수', className: 'text-center', sortable: true },
+ { key: 'status', label: '상태', className: 'text-center', sortable: true },
+ { key: 'requestDate', label: '신청일', sortable: true },
];
}
}, [mainTab]);
diff --git a/src/components/items/ItemMasterDataManagement.tsx b/src/components/items/ItemMasterDataManagement.tsx
index ec50b3dc..e9ea3477 100644
--- a/src/components/items/ItemMasterDataManagement.tsx
+++ b/src/components/items/ItemMasterDataManagement.tsx
@@ -1,6 +1,7 @@
'use client';
import { useState, useEffect, useMemo } from 'react';
+import dynamic from 'next/dynamic';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { useItemMaster } from '@/contexts/ItemMasterContext';
@@ -18,22 +19,52 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-// 다이얼로그 컴포넌트 import
-import { TabManagementDialogs } from './ItemMasterDataManagement/dialogs/TabManagementDialogs';
-import { OptionDialog } from './ItemMasterDataManagement/dialogs/OptionDialog';
-import { ColumnManageDialog } from './ItemMasterDataManagement/dialogs/ColumnManageDialog';
-import { PathEditDialog } from './ItemMasterDataManagement/dialogs/PathEditDialog';
-import { PageDialog } from './ItemMasterDataManagement/dialogs/PageDialog';
-import { SectionDialog } from './ItemMasterDataManagement/dialogs/SectionDialog';
-import { FieldDialog } from './ItemMasterDataManagement/dialogs/FieldDialog';
-import { FieldDrawer } from './ItemMasterDataManagement/dialogs/FieldDrawer';
-import { ColumnDialog } from './ItemMasterDataManagement/dialogs/ColumnDialog';
-import { MasterFieldDialog } from './ItemMasterDataManagement/dialogs/MasterFieldDialog';
-import { SectionTemplateDialog } from './ItemMasterDataManagement/dialogs/SectionTemplateDialog';
-import { TemplateFieldDialog } from './ItemMasterDataManagement/dialogs/TemplateFieldDialog';
-import { LoadTemplateDialog } from './ItemMasterDataManagement/dialogs/LoadTemplateDialog';
-import { ImportSectionDialog } from './ItemMasterDataManagement/dialogs/ImportSectionDialog';
-import { ImportFieldDialog } from './ItemMasterDataManagement/dialogs/ImportFieldDialog';
+// 다이얼로그 컴포넌트 - lazy load (사용자 클릭 시에만 로드)
+const TabManagementDialogs = dynamic(
+ () => import('./ItemMasterDataManagement/dialogs/TabManagementDialogs').then(mod => ({ default: mod.TabManagementDialogs })),
+);
+const OptionDialog = dynamic(
+ () => import('./ItemMasterDataManagement/dialogs/OptionDialog').then(mod => ({ default: mod.OptionDialog })),
+);
+const ColumnManageDialog = dynamic(
+ () => import('./ItemMasterDataManagement/dialogs/ColumnManageDialog').then(mod => ({ default: mod.ColumnManageDialog })),
+);
+const PathEditDialog = dynamic(
+ () => import('./ItemMasterDataManagement/dialogs/PathEditDialog').then(mod => ({ default: mod.PathEditDialog })),
+);
+const PageDialog = dynamic(
+ () => import('./ItemMasterDataManagement/dialogs/PageDialog').then(mod => ({ default: mod.PageDialog })),
+);
+const SectionDialog = dynamic(
+ () => import('./ItemMasterDataManagement/dialogs/SectionDialog').then(mod => ({ default: mod.SectionDialog })),
+);
+const FieldDialog = dynamic(
+ () => import('./ItemMasterDataManagement/dialogs/FieldDialog').then(mod => ({ default: mod.FieldDialog })),
+);
+const FieldDrawer = dynamic(
+ () => import('./ItemMasterDataManagement/dialogs/FieldDrawer').then(mod => ({ default: mod.FieldDrawer })),
+);
+const ColumnDialog = dynamic(
+ () => import('./ItemMasterDataManagement/dialogs/ColumnDialog').then(mod => ({ default: mod.ColumnDialog })),
+);
+const MasterFieldDialog = dynamic(
+ () => import('./ItemMasterDataManagement/dialogs/MasterFieldDialog').then(mod => ({ default: mod.MasterFieldDialog })),
+);
+const SectionTemplateDialog = dynamic(
+ () => import('./ItemMasterDataManagement/dialogs/SectionTemplateDialog').then(mod => ({ default: mod.SectionTemplateDialog })),
+);
+const TemplateFieldDialog = dynamic(
+ () => import('./ItemMasterDataManagement/dialogs/TemplateFieldDialog').then(mod => ({ default: mod.TemplateFieldDialog })),
+);
+const LoadTemplateDialog = dynamic(
+ () => import('./ItemMasterDataManagement/dialogs/LoadTemplateDialog').then(mod => ({ default: mod.LoadTemplateDialog })),
+);
+const ImportSectionDialog = dynamic(
+ () => import('./ItemMasterDataManagement/dialogs/ImportSectionDialog').then(mod => ({ default: mod.ImportSectionDialog })),
+);
+const ImportFieldDialog = dynamic(
+ () => import('./ItemMasterDataManagement/dialogs/ImportFieldDialog').then(mod => ({ default: mod.ImportFieldDialog })),
+);
// 커스텀 훅 import
import {
diff --git a/src/components/production/WorkOrders/WorkOrderList.tsx b/src/components/production/WorkOrders/WorkOrderList.tsx
index a3fb0477..fbb93316 100644
--- a/src/components/production/WorkOrders/WorkOrderList.tsx
+++ b/src/components/production/WorkOrders/WorkOrderList.tsx
@@ -437,19 +437,9 @@ export function WorkOrderList() {
[tabs, stats, processList, handleRowClick, activeTab]
);
- // processMap 로딩 완료 전에는 UniversalListPage를 마운트하지 않음
- // (초기 fetch에서 processId가 undefined로 전달되어 전체 데이터가 반환되는 문제 방지)
- if (!processMapLoaded) {
- return (
-
-
-
- );
- }
-
return (
<>
-
+
import('./InspectionInputModal').then(mod => ({ default: mod.InspectionInputModal })),
+);
+const MaterialInputModal = dynamic(
+ () => import('./MaterialInputModal').then(mod => ({ default: mod.MaterialInputModal })),
+);
+const WorkLogModal = dynamic(
+ () => import('./WorkLogModal').then(mod => ({ default: mod.WorkLogModal })),
+);
+const IssueReportModal = dynamic(
+ () => import('./IssueReportModal').then(mod => ({ default: mod.IssueReportModal })),
+);
+const WorkCompletionResultDialog = dynamic(
+ () => import('./WorkCompletionResultDialog').then(mod => ({ default: mod.WorkCompletionResultDialog })),
+);
+const InspectionReportModal = dynamic(
+ () => import('../WorkOrders/documents').then(mod => ({ default: mod.InspectionReportModal })),
+);
// ===== 목업 데이터 =====
const MOCK_ITEMS: Record = {
diff --git a/src/components/quality/InspectionManagement/InspectionCreate.tsx b/src/components/quality/InspectionManagement/InspectionCreate.tsx
index 1a6639cd..8137146e 100644
--- a/src/components/quality/InspectionManagement/InspectionCreate.tsx
+++ b/src/components/quality/InspectionManagement/InspectionCreate.tsx
@@ -11,6 +11,7 @@
*/
import { useState, useCallback, useMemo } from 'react';
+import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import { Plus, Trash2, ClipboardCheck } from 'lucide-react';
import { Button } from '@/components/ui/button';
@@ -40,8 +41,13 @@ import { toast } from 'sonner';
import { createInspection } from './actions';
import { isOrderSpecSame, calculateOrderSummary } from './mockData';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
-import { OrderSelectModal } from './OrderSelectModal';
-import { ProductInspectionInputModal } from './ProductInspectionInputModal';
+
+const OrderSelectModal = dynamic(
+ () => import('./OrderSelectModal').then(mod => ({ default: mod.OrderSelectModal })),
+);
+const ProductInspectionInputModal = dynamic(
+ () => import('./ProductInspectionInputModal').then(mod => ({ default: mod.ProductInspectionInputModal })),
+);
import type { InspectionFormData, OrderSettingItem, OrderSelectItem, OrderGroup, ProductInspectionData } from './types';
import {
emptyConstructionSite,
diff --git a/src/components/quality/InspectionManagement/InspectionDetail.tsx b/src/components/quality/InspectionManagement/InspectionDetail.tsx
index 152bc053..2e7319a8 100644
--- a/src/components/quality/InspectionManagement/InspectionDetail.tsx
+++ b/src/components/quality/InspectionManagement/InspectionDetail.tsx
@@ -11,6 +11,7 @@
*/
import { useState, useCallback, useEffect, useMemo } from 'react';
+import dynamic from 'next/dynamic';
import { useRouter, useSearchParams } from 'next/navigation';
import {
FileText,
@@ -71,11 +72,20 @@ import {
buildRequestDocumentData,
buildReportDocumentData,
} from './mockData';
-import { InspectionRequestModal } from './documents/InspectionRequestModal';
-import { InspectionReportModal } from './documents/InspectionReportModal';
-import { ProductInspectionInputModal } from './ProductInspectionInputModal';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
-import { OrderSelectModal } from './OrderSelectModal';
+
+const InspectionRequestModal = dynamic(
+ () => import('./documents/InspectionRequestModal').then(mod => ({ default: mod.InspectionRequestModal })),
+);
+const InspectionReportModal = dynamic(
+ () => import('./documents/InspectionReportModal').then(mod => ({ default: mod.InspectionReportModal })),
+);
+const ProductInspectionInputModal = dynamic(
+ () => import('./ProductInspectionInputModal').then(mod => ({ default: mod.ProductInspectionInputModal })),
+);
+const OrderSelectModal = dynamic(
+ () => import('./OrderSelectModal').then(mod => ({ default: mod.OrderSelectModal })),
+);
import type {
ProductInspection,
InspectionFormData,
diff --git a/src/components/templates/UniversalListPage/index.tsx b/src/components/templates/UniversalListPage/index.tsx
index b864684a..9d30a7cc 100644
--- a/src/components/templates/UniversalListPage/index.tsx
+++ b/src/components/templates/UniversalListPage/index.tsx
@@ -244,6 +244,13 @@ export function UniversalListPage({
? Math.ceil(totalCount / itemsPerPage)
: (isServerSearchFiltered ? (Math.ceil(filteredData.length / itemsPerPage) || 1) : serverTotalPages);
+ // 삭제 등으로 현재 페이지가 빈 페이지가 되면 마지막 유효 페이지로 이동
+ useEffect(() => {
+ if (totalPages > 0 && currentPage > totalPages) {
+ setCurrentPage(totalPages);
+ }
+ }, [totalPages, currentPage]);
+
// 표시할 데이터
// 서버 사이드 모드에서도 filteredData 사용 (클라이언트 사이드 정렬 반영)
const displayData = (config.clientSideFiltering || isServerSearchFiltered) ? paginatedData : filteredData;
diff --git a/src/lib/utils/search.ts b/src/lib/utils/search.ts
index b3c759f3..5b08be6a 100644
--- a/src/lib/utils/search.ts
+++ b/src/lib/utils/search.ts
@@ -64,7 +64,8 @@ export function filterByEnum(
allValue: string = 'all'
): T[] {
if (value === allValue) return data;
- return data.filter((item) => String(item[field]) === value);
+ const trimmedValue = value.trim();
+ return data.filter((item) => String(item[field]).trim() === trimmedValue);
}
/**