feat(WEB): QMS 검사 모달 개선, 전자결재/생산대시보드/템플릿 기능 수정

- QMS: InspectionModal/InspectionModalV2 개선, mockData 정리
- 전자결재: DocumentCreate 기능 수정
- 생산대시보드: ProductionDashboard 개선
- 템플릿: IntegratedDetailTemplate/UniversalListPage 기능 수정
- 문서: i18n 가이드 업데이트, 문서뷰어 아키텍처 계획 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-03 09:09:05 +09:00
parent ca6247286a
commit f0987127eb
10 changed files with 578 additions and 230 deletions

View File

@@ -10,7 +10,7 @@ import {
import { ZoomIn, ZoomOut, Download, Printer, AlertCircle, Maximize2 } from 'lucide-react';
import { Button } from "@/components/ui/button";
import { Document, DocumentItem } from '../types';
import { MOCK_ORDER_DATA, MOCK_WORK_ORDER, MOCK_SHIPMENT_DETAIL } from '../mockData';
import { MOCK_WORK_ORDER, MOCK_SHIPMENT_DETAIL } from '../mockData';
// 기존 문서 컴포넌트 import
import { DeliveryConfirmation } from '@/components/outbound/ShipmentManagement/documents/DeliveryConfirmation';
@@ -73,9 +73,9 @@ const PlaceholderDocument = ({ docType, docItem }: { docType: string; docItem: D
);
};
// 수주서 문서 컴포넌트 (간소화 버전)
// 수주서 문서 컴포넌트 (간소화 버전 - deprecated, V2 사용)
const OrderDocument = () => {
const data = MOCK_ORDER_DATA;
const data = { lotNumber: '', orderDate: '', client: '', siteName: '', manager: '', managerContact: '', deliveryRequestDate: '', expectedShipDate: '', deliveryMethod: '', address: '', items: [] as { id: string; name: string; specification: string; unit: string; quantity: number; unitPrice?: number; amount?: number }[], subtotal: 0, discountRate: 0, totalAmount: 0, remarks: '' };
return (
<div className="bg-white p-8 w-full text-sm shadow-sm">

View File

@@ -6,19 +6,28 @@ import { DocumentViewer } from '@/components/document-system';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { Document, DocumentItem } from '../types';
import { MOCK_ORDER_DATA, MOCK_SHIPMENT_DETAIL } from '../mockData';
import { MOCK_SHIPMENT_DETAIL } from '../mockData';
// 기존 문서 컴포넌트 import
import { DeliveryConfirmation } from '@/components/outbound/ShipmentManagement/documents/DeliveryConfirmation';
import { ShippingSlip } from '@/components/outbound/ShipmentManagement/documents/ShippingSlip';
// 수주서 문서 컴포넌트 import
import { SalesOrderDocument } from '@/components/orders/documents/SalesOrderDocument';
import type { ProductInfo } from '@/components/orders/documents/OrderDocumentModal';
import type { OrderItem } from '@/components/orders/actions';
// 품질검사 문서 컴포넌트 import
import {
ImportInspectionDocument,
ProductInspectionDocument,
JointbarInspectionDocument,
QualityDocumentUploader,
} from './documents';
// 제품검사 성적서 (신규 양식) import
import { InspectionReportDocument } from '@/components/quality/InspectionManagement/documents/InspectionReportDocument';
import { mockReportInspectionItems } from '@/components/quality/InspectionManagement/mockData';
import type { InspectionReportDocument as InspectionReportDocumentType } from '@/components/quality/InspectionManagement/types';
import type { ImportInspectionTemplate, ImportInspectionRef } from './documents/ImportInspectionDocument';
// 작업일지 + 중간검사 성적서 문서 컴포넌트 import (공정별 신규 버전)
@@ -44,6 +53,8 @@ interface InspectionModalV2Props {
itemName?: string;
specification?: string;
supplier?: string;
// 읽기 전용 모드 (QMS 심사 확인용)
readOnly?: boolean;
}
// 문서 타입별 정보
@@ -85,132 +96,38 @@ const PlaceholderDocument = ({ docType, docItem }: { docType: string; docItem: D
);
};
// 수주서 문서 컴포넌트 (간소화 버전)
const OrderDocument = () => {
const data = MOCK_ORDER_DATA;
// QMS용 수주서 Mock 데이터
const QMS_MOCK_PRODUCTS: ProductInfo[] = [
{ productName: '방화 스크린 셔터 (표준형)', productCategory: '스크린', openWidth: '3000', openHeight: '2500', quantity: 5, floor: '1F', code: 'FSS-01' },
{ productName: '방화 스크린 셔터 (방화형)', productCategory: '스크린', openWidth: '3000', openHeight: '2500', quantity: 3, floor: '2F', code: 'FSS-02' },
];
const QMS_MOCK_ORDER_ITEMS: OrderItem[] = [
{ id: 'mt-1', itemCode: 'MT-001', itemName: '모터(380V 단상)', specification: '150K', type: '모터', quantity: 8, unit: 'EA', unitPrice: 120000, supplyAmount: 960000, taxAmount: 96000, totalAmount: 1056000, sortOrder: 1 },
{ id: 'br-1', itemCode: 'BR-001', itemName: '브라켓트', specification: '380X180 [2-4"]', type: '브라켓', quantity: 16, unit: 'EA', unitPrice: 15000, supplyAmount: 240000, taxAmount: 24000, totalAmount: 264000, sortOrder: 2 },
{ id: 'gr-1', itemCode: 'GR-001', itemName: '가이드레일 백면형 (120X70)', specification: 'EGI 1.5ST', type: '가이드레일', quantity: 16, unit: 'EA', unitPrice: 25000, supplyAmount: 400000, taxAmount: 40000, totalAmount: 440000, width: 120, height: 2500, sortOrder: 3 },
{ id: 'cs-1', itemCode: 'CS-001', itemName: '케이스(셔터박스)', specification: 'EGI 1.5ST 380X180', type: '케이스', quantity: 8, unit: 'EA', unitPrice: 35000, supplyAmount: 280000, taxAmount: 28000, totalAmount: 308000, width: 380, height: 180, sortOrder: 4 },
{ id: 'bf-1', itemCode: 'BF-001', itemName: '하단마감재', specification: 'EGI 1.5ST', type: '하단마감재', quantity: 8, unit: 'EA', unitPrice: 18000, supplyAmount: 144000, taxAmount: 14400, totalAmount: 158400, sortOrder: 5 },
];
return (
<div className="bg-white p-8 w-full text-sm shadow-sm">
{/* 헤더 */}
<div className="flex justify-between items-start mb-6">
<div className="flex items-center gap-4">
<div className="text-2xl font-bold">KD</div>
<div className="text-xs"></div>
</div>
<div className="text-2xl font-bold tracking-[0.5rem]"> </div>
<table className="text-xs border-collapse">
<tbody>
<tr>
<td className="border px-2 py-1 bg-gray-100" rowSpan={3}>
<div className="flex flex-col items-center">
<span></span><span></span>
</div>
</td>
<td className="border px-2 py-1 bg-gray-100 text-center w-16"></td>
<td className="border px-2 py-1 bg-gray-100 text-center w-16"></td>
<td className="border px-2 py-1 bg-gray-100 text-center w-16"></td>
</tr>
<tr>
<td className="border px-2 py-1 h-10"></td>
<td className="border px-2 py-1 h-10"></td>
<td className="border px-2 py-1 h-10"></td>
</tr>
<tr>
<td className="border px-2 py-1 text-center bg-gray-50"></td>
<td className="border px-2 py-1 text-center bg-gray-50"></td>
<td className="border px-2 py-1 text-center bg-gray-50"></td>
</tr>
</tbody>
</table>
</div>
{/* 기본 정보 */}
<table className="w-full border-collapse mb-6 text-xs">
<tbody>
<tr>
<td className="border px-3 py-2 bg-gray-100 w-24">LOT NO.</td>
<td className="border px-3 py-2">{data.lotNumber}</td>
<td className="border px-3 py-2 bg-gray-100 w-24"></td>
<td className="border px-3 py-2">{data.orderDate}</td>
</tr>
<tr>
<td className="border px-3 py-2 bg-gray-100"></td>
<td className="border px-3 py-2">{data.client}</td>
<td className="border px-3 py-2 bg-gray-100"></td>
<td className="border px-3 py-2">{data.siteName}</td>
</tr>
<tr>
<td className="border px-3 py-2 bg-gray-100"></td>
<td className="border px-3 py-2">{data.manager}</td>
<td className="border px-3 py-2 bg-gray-100"></td>
<td className="border px-3 py-2">{data.managerContact}</td>
</tr>
<tr>
<td className="border px-3 py-2 bg-gray-100"></td>
<td className="border px-3 py-2">{data.deliveryRequestDate}</td>
<td className="border px-3 py-2 bg-gray-100"></td>
<td className="border px-3 py-2">{data.expectedShipDate}</td>
</tr>
<tr>
<td className="border px-3 py-2 bg-gray-100"></td>
<td className="border px-3 py-2">{data.deliveryMethod}</td>
<td className="border px-3 py-2 bg-gray-100"></td>
<td className="border px-3 py-2">{data.address}</td>
</tr>
</tbody>
</table>
{/* 품목 테이블 */}
<table className="w-full border-collapse mb-6 text-xs">
<thead>
<tr className="bg-gray-100">
<th className="border px-2 py-2 w-10">No</th>
<th className="border px-2 py-2"></th>
<th className="border px-2 py-2 w-24"></th>
<th className="border px-2 py-2 w-12"></th>
<th className="border px-2 py-2 w-12"></th>
<th className="border px-2 py-2 w-20"></th>
<th className="border px-2 py-2 w-24"></th>
</tr>
</thead>
<tbody>
{data.items.map((item, index) => (
<tr key={item.id}>
<td className="border px-2 py-2 text-center">{index + 1}</td>
<td className="border px-2 py-2">{item.name}</td>
<td className="border px-2 py-2 text-center">{item.specification}</td>
<td className="border px-2 py-2 text-center">{item.unit}</td>
<td className="border px-2 py-2 text-center">{item.quantity}</td>
<td className="border px-2 py-2 text-right">{item.unitPrice?.toLocaleString()}</td>
<td className="border px-2 py-2 text-right">{item.amount?.toLocaleString()}</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<td colSpan={5} className="border px-2 py-2 text-right bg-gray-50"></td>
<td colSpan={2} className="border px-2 py-2 text-right">{data.subtotal.toLocaleString()}</td>
</tr>
<tr>
<td colSpan={5} className="border px-2 py-2 text-right bg-gray-50"> ({data.discountRate}%)</td>
<td colSpan={2} className="border px-2 py-2 text-right text-red-600">-{(data.subtotal * data.discountRate / 100).toLocaleString()}</td>
</tr>
<tr>
<td colSpan={5} className="border px-2 py-2 text-right bg-gray-100 font-bold"></td>
<td colSpan={2} className="border px-2 py-2 text-right font-bold text-blue-600">{data.totalAmount.toLocaleString()}</td>
</tr>
</tfoot>
</table>
{/* 비고 */}
{data.remarks && (
<div className="border p-4">
<h3 className="font-medium mb-2 text-xs"></h3>
<p className="text-xs text-gray-600">{data.remarks}</p>
</div>
)}
</div>
);
// QMS용 제품검사 성적서 Mock 데이터
const QMS_MOCK_REPORT_DATA: InspectionReportDocumentType = {
documentNumber: 'RPT-KD-SS-2024-530',
createdDate: '2024-09-24',
approvalLine: [
{ role: '작성', name: '김검사', department: '품질관리부' },
{ role: '승인', name: '박승인', department: '품질관리부' },
],
productName: '방화스크린',
productLotNo: 'KD-SS-240924-19',
productCode: 'WY-SC780',
lotSize: '8',
client: '삼성물산(주)',
inspectionDate: '2024-09-26',
siteName: '강남 아파트 단지',
inspector: '김검사',
inspectionItems: mockReportInspectionItems,
specialNotes: '',
finalJudgment: '합격',
};
// QMS용 작업일지 Mock WorkOrder 생성
@@ -289,6 +206,7 @@ export const InspectionModalV2 = ({
itemName,
specification,
supplier,
readOnly = false,
}: InspectionModalV2Props) => {
// 수입검사 템플릿 상태
const [importTemplate, setImportTemplate] = useState<ImportInspectionTemplate | null>(null);
@@ -339,6 +257,23 @@ export const InspectionModalV2 = ({
}
};
// 수입검사 저장 핸들러 (hooks는 early return 전에 호출해야 함)
const handleImportSave = useCallback(async () => {
if (!importDocRef.current) return;
const data = importDocRef.current.getInspectionData();
setIsSaving(true);
try {
// TODO: 실제 저장 API 연동
console.log('[InspectionModalV2] 수입검사 저장 데이터:', data);
toast.success('검사 데이터가 저장되었습니다.');
} catch {
toast.error('저장 중 오류가 발생했습니다.');
} finally {
setIsSaving(false);
}
}, []);
if (!doc) return null;
const docInfo = DOCUMENT_INFO[doc.type] || { label: doc.title, hasTemplate: false, color: 'text-gray-600' };
@@ -391,23 +326,6 @@ export const InspectionModalV2 = ({
}
};
// 수입검사 저장 핸들러
const handleImportSave = useCallback(async () => {
if (!importDocRef.current) return;
const data = importDocRef.current.getInspectionData();
setIsSaving(true);
try {
// TODO: 실제 저장 API 연동
console.log('[InspectionModalV2] 수입검사 저장 데이터:', data);
toast.success('검사 데이터가 저장되었습니다.');
} catch {
toast.error('저장 중 오류가 발생했습니다.');
} finally {
setIsSaving(false);
}
}, []);
// 수입검사 문서 렌더링 (Lazy Loading)
const renderImportInspectionDocument = () => {
if (isLoadingTemplate) {
@@ -419,14 +337,35 @@ export const InspectionModalV2 = ({
}
// 템플릿이 로드되면 전달, 아니면 기본 템플릿 사용
return <ImportInspectionDocument ref={importDocRef} template={importTemplate || undefined} />;
return <ImportInspectionDocument ref={importDocRef} template={importTemplate || undefined} readOnly={readOnly} />;
};
// 문서 타입에 따른 컨텐츠 렌더링
const renderDocumentContent = () => {
switch (doc.type) {
case 'order':
return <OrderDocument />;
return (
<SalesOrderDocument
orderNumber="KD-SS-240924-19"
documentNumber="KD-SS-240924-19"
certificationNumber="KD-SS-240924-19"
orderDate="2024-09-24"
client="삼성물산(주)"
siteName="강남 아파트 단지"
manager="김담당"
managerContact="010-1234-5678"
deliveryRequestDate="2024-10-05"
expectedShipDate="2024-10-04"
deliveryMethod="직접배차"
address="서울시 강남구 테헤란로 123"
recipientName="김인수"
recipientContact="010-9876-5432"
shutterCount={8}
products={QMS_MOCK_PRODUCTS}
items={QMS_MOCK_ORDER_ITEMS}
remarks="납기일 엄수 요청"
/>
);
case 'log':
return renderWorkLogDocument();
case 'confirmation':
@@ -436,7 +375,7 @@ export const InspectionModalV2 = ({
case 'import':
return renderImportInspectionDocument();
case 'product':
return <ProductInspectionDocument />;
return <InspectionReportDocument data={QMS_MOCK_REPORT_DATA} />;
case 'report':
return renderReportDocument();
case 'quality':
@@ -444,6 +383,7 @@ export const InspectionModalV2 = ({
<QualityDocumentUploader
onFileUpload={handleQualityFileUpload}
onFileDelete={handleQualityFileDelete}
disabled={readOnly}
/>
);
default:
@@ -456,8 +396,8 @@ export const InspectionModalV2 = ({
console.log('[InspectionModalV2] 다운로드 요청:', doc.type);
};
// 수입검사 저장 버튼 (toolbarExtra)
const importToolbarExtra = doc.type === 'import' ? (
// 수입검사 저장 버튼 (toolbarExtra) - readOnly일 때 숨김
const importToolbarExtra = doc.type === 'import' && !readOnly ? (
<Button onClick={handleImportSave} disabled={isSaving} size="sm">
{isSaving ? (
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />

View File

@@ -2,59 +2,6 @@ import { InspectionReport, RouteItem, Document, ChecklistCategory, StandardDocum
import type { WorkOrder } from '@/components/production/ProductionDashboard/types';
import type { ShipmentDetail } from '@/components/outbound/ShipmentManagement/types';
// 품질검사용 수주서 아이템 타입 (자체 정의)
interface QualityOrderItem {
id: string;
name: string;
specification: string;
unit: string;
quantity: number;
unitPrice?: number;
amount?: number;
}
// 품질검사용 수주서 데이터 타입 (자체 정의)
export interface QualityOrderData {
lotNumber: string;
orderDate: string;
client: string;
siteName: string;
manager: string;
managerContact: string;
deliveryRequestDate: string;
expectedShipDate: string;
deliveryMethod: string;
address: string;
items: QualityOrderItem[];
subtotal: number;
discountRate: number;
totalAmount: number;
remarks?: string;
}
// 수주서 샘플 데이터
export const MOCK_ORDER_DATA: QualityOrderData = {
lotNumber: 'KD-SS-240924-19',
orderDate: '2024-09-24',
client: '삼성물산(주)',
siteName: '강남 아파트 단지',
manager: '김담당',
managerContact: '010-1234-5678',
deliveryRequestDate: '2024-10-05',
expectedShipDate: '2024-10-04',
deliveryMethod: '직접배차',
address: '서울시 강남구 테헤란로 123',
items: [
{ id: '1', name: '스크린 셔터 (표준형)', specification: '3000×2500', unit: 'SET', quantity: 5, unitPrice: 1200000, amount: 6000000 },
{ id: '2', name: '스크린 셔터 (방화형)', specification: '3000×2500', unit: 'SET', quantity: 3, unitPrice: 1500000, amount: 4500000 },
{ id: '3', name: '슬랫 패널', specification: '1000×500', unit: 'EA', quantity: 20, unitPrice: 50000, amount: 1000000 },
],
subtotal: 11500000,
discountRate: 5,
totalAmount: 10925000,
remarks: '납기일 엄수 요청',
};
// 작업일지 샘플 데이터
export const MOCK_WORK_ORDER: WorkOrder = {
id: 'wo-1',

View File

@@ -369,6 +369,7 @@ export default function QualityInspectionPage() {
onClose={() => setModalOpen(false)}
document={selectedDoc}
documentItem={selectedDocItem}
readOnly
/>
</div>
);

View File

@@ -2,6 +2,7 @@
import { useState, useCallback, useEffect, useTransition, useRef } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { usePermission } from '@/hooks/usePermission';
import { format } from 'date-fns';
import { Trash2, Send, Save, Eye, Loader2 } from 'lucide-react';
import { toast } from 'sonner';
@@ -87,6 +88,7 @@ export function DocumentCreate() {
const searchParams = useSearchParams();
const [isPending, startTransition] = useTransition();
const { currentUser } = useAuth();
const { canCreate, canDelete } = usePermission();
// 수정 모드 / 복제 모드 상태
const documentId = searchParams.get('id');
@@ -546,20 +548,22 @@ export function DocumentCreate() {
<Eye className="w-4 h-4 mr-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={handleDelete}
disabled={isPending}
>
<Trash2 className="w-4 h-4 mr-1" />
</Button>
{canDelete && (
<Button
variant="outline"
size="sm"
onClick={handleDelete}
disabled={isPending}
>
<Trash2 className="w-4 h-4 mr-1" />
</Button>
)}
<Button
variant="default"
size="sm"
onClick={handleSubmit}
disabled={isPending}
disabled={isPending || !canCreate}
>
{isPending ? (
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
@@ -583,7 +587,7 @@ export function DocumentCreate() {
</Button>
</div>
);
}, [handlePreview, handleDelete, handleSubmit, handleSaveDraft, isPending, isEditMode]);
}, [handlePreview, handleDelete, handleSubmit, handleSaveDraft, isPending, isEditMode, canCreate, canDelete]);
// 폼 컨텐츠 렌더링
const renderFormContent = useCallback(() => {

View File

@@ -12,6 +12,7 @@
import { useState, useMemo, useCallback, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { usePermission } from '@/hooks/usePermission';
import { Factory, Clock, PlayCircle, CheckCircle2, AlertTriangle, Timer, Users } from 'lucide-react';
import { StatCardGridSkeleton } from '@/components/ui/skeleton';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
@@ -27,6 +28,8 @@ import { STATUS_LABELS } from './types';
export default function ProductionDashboard() {
const router = useRouter();
const screenPerm = usePermission('/production/screen');
const workOrderPerm = usePermission('/production/work-order-management');
// ===== 상태 관리 =====
const [selectedTab, setSelectedTab] = useState<string>('all');
@@ -152,13 +155,17 @@ export default function ProductionDashboard() {
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handleWorkerScreenClick}>
<Users className="mr-2 h-4 w-4" />
</Button>
<Button variant="outline" onClick={handleWorkOrderListClick}>
</Button>
{screenPerm.canView && (
<Button variant="outline" onClick={handleWorkerScreenClick}>
<Users className="mr-2 h-4 w-4" />
</Button>
)}
{workOrderPerm.canView && (
<Button variant="outline" onClick={handleWorkOrderListClick}>
</Button>
)}
</div>
</div>

View File

@@ -11,6 +11,7 @@
import { useState, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react';
import { useRouter, useParams } from 'next/navigation';
import { usePermission } from '@/hooks/usePermission';
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
@@ -53,6 +54,7 @@ function IntegratedDetailTemplateInner<T extends Record<string, unknown>>(
const router = useRouter();
const params = useParams();
const locale = (params.locale as string) || 'ko';
const { canCreate: permCanCreate, canUpdate: permCanUpdate, canDelete: permCanDelete } = usePermission();
// ===== 상태 =====
const [mode, setMode] = useState<DetailMode>(initialMode);
@@ -104,14 +106,15 @@ function IntegratedDetailTemplateInner<T extends Record<string, unknown>>(
}), [formData, config, initialData]);
// ===== 권한 계산 =====
// config.permissions가 명시적으로 설정되면 우선, 아니면 usePermission() fallback
const permissions = useMemo(() => {
const p = config.permissions || {};
return {
canEdit: typeof p.canEdit === 'function' ? p.canEdit() : p.canEdit ?? true,
canDelete: typeof p.canDelete === 'function' ? p.canDelete() : p.canDelete ?? true,
canCreate: typeof p.canCreate === 'function' ? p.canCreate() : p.canCreate ?? true,
canEdit: typeof p.canEdit === 'function' ? p.canEdit() : (p.canEdit !== undefined ? p.canEdit : permCanUpdate),
canDelete: typeof p.canDelete === 'function' ? p.canDelete() : (p.canDelete !== undefined ? p.canDelete : permCanDelete),
canCreate: typeof p.canCreate === 'function' ? p.canCreate() : (p.canCreate !== undefined ? p.canCreate : permCanCreate),
};
}, [config.permissions]);
}, [config.permissions, permCanUpdate, permCanDelete, permCanCreate]);
// ===== 모드 헬퍼 =====
const isViewMode = mode === 'view';

View File

@@ -13,6 +13,7 @@
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { useRouter, useParams } from 'next/navigation';
import { usePermission } from '@/hooks/usePermission';
import { toast } from 'sonner';
import { Download, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
@@ -43,6 +44,7 @@ export function UniversalListPage<T>({
const router = useRouter();
const params = useParams();
const locale = (params.locale as string) || 'ko';
const { canCreate: permCanCreate, canDelete: permCanDelete } = usePermission();
// ===== 상태 관리 =====
// 원본 데이터 (클라이언트 사이드 필터링용)
@@ -825,7 +827,7 @@ export function UniversalListPage<T>({
onToggle: () => toggleSelection(id),
onRowClick: () => handleRowClick(item),
onEdit: () => handleEdit(item),
onDelete: () => handleDeleteClick(item),
onDelete: permCanDelete ? () => handleDeleteClick(item) : undefined,
});
},
[config, effectiveGetItemId, handleDeleteClick, handleEdit, handleRowClick, effectiveSelectedItems, toggleSelection]
@@ -838,7 +840,7 @@ export function UniversalListPage<T>({
onToggle,
onRowClick: () => handleRowClick(item),
onEdit: () => handleEdit(item),
onDelete: () => handleDeleteClick(item),
onDelete: permCanDelete ? () => handleDeleteClick(item) : undefined,
});
},
[config, handleDeleteClick, handleEdit, handleRowClick]
@@ -874,7 +876,7 @@ export function UniversalListPage<T>({
}
// 공통 헤더 옵션 (달력/등록버튼)
dateRangeSelector={config.dateRangeSelector}
createButton={config.createButton}
createButton={permCanCreate ? config.createButton : undefined}
// 탭 콘텐츠
tabsContent={config.tabsContent}
// 통계 카드