feat(WEB): 리스트 페이지 권한 시스템 통합 및 중복 권한 로직 제거
- PermissionContext 기능 확장 (권한 조회 액션 추가) - usePermission 훅 개선 - 회계 모듈 권한 통합: 매입/매출/입금/지출/채권/거래처/어음/일보/부실채권 - 인사 모듈 권한 통합: 근태/카드/급여 관리 - 전자결재 권한 통합: 기안함/결재함 - 게시판/품목/단가/팝업/구독 리스트 권한 적용 - UniversalListPage 권한 연동 - 각 컴포넌트 중복 권한 체크 코드 제거 (-828줄) - 권한 검증 QA 체크리스트 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,8 +15,7 @@ export { BadDebtDetailClientV2 } from './BadDebtDetailClientV2';
|
||||
|
||||
import { useState, useMemo, useCallback, useTransition } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { AlertTriangle, Pencil, Trash2, Eye } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
@@ -55,7 +54,6 @@ const tableColumns = [
|
||||
{ key: 'managerName', label: '담당자', className: 'w-[100px]' },
|
||||
{ key: 'status', label: '상태', className: 'text-center w-[100px]' },
|
||||
{ key: 'setting', label: '설정', className: 'text-center w-[80px]' },
|
||||
{ key: 'actions', label: '작업', className: 'text-center w-[120px]' },
|
||||
];
|
||||
|
||||
// ===== Props 타입 정의 =====
|
||||
@@ -103,13 +101,6 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(item: BadDebtRecord) => {
|
||||
router.push(`/ko/accounting/bad-debt-collection/${item.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// 설정 토글 핸들러 (API 호출)
|
||||
const handleSettingToggle = useCallback(
|
||||
(id: string, checked: boolean) => {
|
||||
@@ -406,29 +397,6 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
|
||||
disabled={isPending}
|
||||
/>
|
||||
</TableCell>
|
||||
{/* 작업 */}
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
{handlers.isSelected && (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => handleEdit(item)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-red-500 hover:text-red-600 hover:bg-red-50"
|
||||
onClick={() => handlers.onDelete?.(item)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
|
||||
@@ -454,25 +422,6 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
|
||||
{ label: '발생일', value: item.occurrenceDate },
|
||||
{ label: '담당자', value: item.assignedManager?.name || '-' },
|
||||
]}
|
||||
actions={
|
||||
handlers.isSelected ? (
|
||||
<div className="flex gap-2 w-full">
|
||||
<Button variant="outline" className="flex-1" onClick={() => handleRowClick(item)}>
|
||||
<Eye className="w-4 h-4 mr-2" /> 상세
|
||||
</Button>
|
||||
<Button variant="outline" className="flex-1" onClick={() => handleEdit(item)}>
|
||||
<Pencil className="w-4 h-4 mr-2" /> 수정
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1 text-red-500 border-red-200 hover:bg-red-50 hover:text-red-600"
|
||||
onClick={() => handlers.onDelete?.(item)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" /> 삭제
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
@@ -484,7 +433,6 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
|
||||
sortOption,
|
||||
statsData,
|
||||
handleRowClick,
|
||||
handleEdit,
|
||||
handleSettingToggle,
|
||||
isPending,
|
||||
]
|
||||
|
||||
@@ -15,8 +15,6 @@ import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
FileText,
|
||||
Plus,
|
||||
Pencil,
|
||||
Trash2,
|
||||
Save,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -196,7 +194,6 @@ export function BillManagementClient({
|
||||
{ key: 'maturityDate', label: '만기일' },
|
||||
{ key: 'installmentCount', label: '차수', className: 'text-center' },
|
||||
{ key: 'status', label: '상태', className: 'text-center' },
|
||||
{ key: 'actions', label: '작업', className: 'text-center w-[80px]' },
|
||||
], []);
|
||||
|
||||
// ===== 테이블 행 렌더링 =====
|
||||
@@ -232,31 +229,9 @@ export function BillManagementClient({
|
||||
{getBillStatusLabel(item.billType, item.status)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
{handlers.isSelected && (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-gray-600 hover:text-gray-700 hover:bg-gray-50"
|
||||
onClick={() => router.push(`/ko/accounting/bills/${item.id}?mode=edit`)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-red-500 hover:text-red-600 hover:bg-red-50"
|
||||
onClick={() => handleDeleteClick(item.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}, [handleRowClick, handleDeleteClick, router]);
|
||||
}, [handleRowClick]);
|
||||
|
||||
// ===== 모바일 카드 렌더링 =====
|
||||
const renderMobileCard = useCallback((
|
||||
@@ -289,26 +264,10 @@ export function BillManagementClient({
|
||||
<InfoField label="만기일" value={item.maturityDate} />
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
handlers.isSelected ? (
|
||||
<div className="flex gap-2 w-full">
|
||||
<Button variant="outline" className="flex-1" onClick={() => router.push(`/ko/accounting/bills/${item.id}?mode=edit`)}>
|
||||
<Pencil className="w-4 h-4 mr-2" /> 수정
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1 text-red-500 border-red-200 hover:bg-red-50 hover:text-red-600"
|
||||
onClick={() => handleDeleteClick(item.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" /> 삭제
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
onCardClick={() => handleRowClick(item)}
|
||||
/>
|
||||
);
|
||||
}, [handleRowClick, handleDeleteClick, router]);
|
||||
}, [handleRowClick]);
|
||||
|
||||
// ===== 거래처 목록 (필터용) =====
|
||||
const vendorOptions = useMemo(() => {
|
||||
|
||||
@@ -24,6 +24,7 @@ import { MATCH_STATUS_LABELS, MATCH_STATUS_COLORS } from './types';
|
||||
import { getNoteReceivables, getDailyAccounts, getDailyReportSummary, exportDailyReportExcel } from './actions';
|
||||
import { toast } from 'sonner';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
|
||||
// ===== Props 인터페이스 =====
|
||||
interface DailyReportProps {
|
||||
@@ -32,6 +33,7 @@ interface DailyReportProps {
|
||||
}
|
||||
|
||||
export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts = [] }: DailyReportProps) {
|
||||
const { canExport } = usePermission();
|
||||
// ===== 상태 관리 =====
|
||||
const [selectedDate, setSelectedDate] = useState(() => format(new Date(), 'yyyy-MM-dd'));
|
||||
const [noteReceivables, setNoteReceivables] = useState<NoteReceivableItem[]>(initialNoteReceivables);
|
||||
@@ -217,10 +219,12 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
|
||||
)}
|
||||
새로고침
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleExcelDownload}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
엑셀 다운로드
|
||||
</Button>
|
||||
{canExport && (
|
||||
<Button variant="outline" size="sm" onClick={handleExcelDownload}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
엑셀 다운로드
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -17,10 +17,8 @@ import { useState, useMemo, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
Banknote,
|
||||
Pencil,
|
||||
Plus,
|
||||
Save,
|
||||
Trash2,
|
||||
RefreshCw,
|
||||
Search,
|
||||
} from 'lucide-react';
|
||||
@@ -83,7 +81,6 @@ const tableColumns = [
|
||||
{ key: 'vendorName', label: '거래처' },
|
||||
{ key: 'note', label: '적요' },
|
||||
{ key: 'depositType', label: '입금유형', className: 'text-center' },
|
||||
{ key: 'actions', label: '작업', className: 'text-center w-[80px]' },
|
||||
];
|
||||
|
||||
// ===== 컴포넌트 Props =====
|
||||
@@ -150,10 +147,6 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
router.push(`/ko/accounting/deposits/${item.id}?mode=view`);
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback((item: DepositRecord) => {
|
||||
router.push(`/ko/accounting/deposits/${item.id}?mode=edit`);
|
||||
}, [router]);
|
||||
|
||||
// 새로고침 핸들러
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setIsRefreshing(true);
|
||||
@@ -451,7 +444,6 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
|
||||
@@ -495,28 +487,6 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
{DEPOSIT_TYPE_LABELS[item.depositType]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
{handlers.isSelected && (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-gray-600 hover:text-gray-700 hover:bg-gray-50"
|
||||
onClick={() => handleEdit(item)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-red-500 hover:text-red-600 hover:bg-red-50"
|
||||
onClick={() => handlers.onDelete?.(item)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
},
|
||||
@@ -542,22 +512,6 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
{ label: '입금액', value: `${(item.depositAmount ?? 0).toLocaleString()}원` },
|
||||
{ label: '거래처', value: item.vendorName || '-' },
|
||||
]}
|
||||
actions={
|
||||
handlers.isSelected ? (
|
||||
<div className="flex gap-2 w-full">
|
||||
<Button variant="outline" className="flex-1" onClick={() => handleEdit(item)}>
|
||||
<Pencil className="w-4 h-4 mr-2" /> 수정
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1 text-red-500 border-red-200 hover:bg-red-50 hover:text-red-600"
|
||||
onClick={() => handlers.onDelete?.(item)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" /> 삭제
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
@@ -575,7 +529,6 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
isRefreshing,
|
||||
searchQuery,
|
||||
handleRowClick,
|
||||
handleEdit,
|
||||
handleRefresh,
|
||||
handleSaveAccountSubject,
|
||||
]
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
Calendar as CalendarIcon,
|
||||
FileText,
|
||||
Plus,
|
||||
Pencil,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -573,7 +572,6 @@ export function ExpectedExpenseManagement({
|
||||
{ key: 'vendorName', label: '거래처' },
|
||||
{ key: 'bankAccount', label: '계좌' },
|
||||
{ key: 'approvalStatus', label: '전자결재', className: 'text-center' },
|
||||
{ key: 'actions', label: '작업', className: 'w-[100px] text-center' },
|
||||
], []);
|
||||
|
||||
// ===== 전자결재 상태 Badge 스타일 =====
|
||||
@@ -603,7 +601,7 @@ export function ExpectedExpenseManagement({
|
||||
if (item.rowType === 'monthHeader') {
|
||||
return (
|
||||
<TableRow key={item.id} className="bg-gray-100 hover:bg-gray-100">
|
||||
<TableCell colSpan={9} className="py-2 font-semibold text-gray-700">
|
||||
<TableCell colSpan={8} className="py-2 font-semibold text-gray-700">
|
||||
{item.monthLabel}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -621,7 +619,7 @@ export function ExpectedExpenseManagement({
|
||||
<TableCell className="text-right font-bold text-blue-700">
|
||||
{item.subtotalAmount?.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell colSpan={4}></TableCell>
|
||||
<TableCell colSpan={3}></TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
@@ -637,7 +635,7 @@ export function ExpectedExpenseManagement({
|
||||
<TableCell className="text-right font-bold text-red-600">
|
||||
{item.subtotalAmount?.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell colSpan={4}></TableCell>
|
||||
<TableCell colSpan={3}></TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
@@ -653,7 +651,7 @@ export function ExpectedExpenseManagement({
|
||||
<TableCell className="text-right font-bold">
|
||||
{item.subtotalAmount?.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell colSpan={4}></TableCell>
|
||||
<TableCell colSpan={3}></TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
@@ -669,7 +667,7 @@ export function ExpectedExpenseManagement({
|
||||
<TableCell className="text-right font-bold text-orange-600">
|
||||
{item.subtotalAmount?.toLocaleString()}원
|
||||
</TableCell>
|
||||
<TableCell colSpan={4}></TableCell>
|
||||
<TableCell colSpan={3}></TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
@@ -700,35 +698,9 @@ export function ExpectedExpenseManagement({
|
||||
<TableCell className="text-center">
|
||||
{getApprovalStatusBadge(item.approvalStatus)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOpenEditDialog(item);
|
||||
}}
|
||||
>
|
||||
<Pencil className="h-4 w-4 text-gray-500" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteClick(item.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}, [selectedItems, toggleSelection, handleOpenEditDialog, handleDeleteClick]);
|
||||
}, [selectedItems, toggleSelection]);
|
||||
|
||||
// ===== 모바일 카드 렌더링 =====
|
||||
const renderMobileCard = useCallback((
|
||||
|
||||
@@ -19,9 +19,7 @@ import { useRouter } from 'next/navigation';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
Receipt,
|
||||
Pencil,
|
||||
Save,
|
||||
Trash2,
|
||||
Search,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -76,7 +74,6 @@ const tableColumns = [
|
||||
{ key: 'totalAmount', label: '합계금액', className: 'text-right' },
|
||||
{ key: 'purchaseType', label: '매입유형', className: 'text-center' },
|
||||
{ key: 'taxInvoice', label: '세금계산서 수취 확인', className: 'text-center' },
|
||||
{ key: 'actions', label: '작업', className: 'text-center w-[100px]' },
|
||||
];
|
||||
|
||||
export function PurchaseManagement() {
|
||||
@@ -206,10 +203,6 @@ export function PurchaseManagement() {
|
||||
router.push(`/ko/accounting/purchase/${item.id}?mode=view`);
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback((item: PurchaseRecord) => {
|
||||
router.push(`/ko/accounting/purchase/${item.id}?mode=edit`);
|
||||
}, [router]);
|
||||
|
||||
// 토글 핸들러
|
||||
const handleTaxInvoiceToggle = useCallback(async (itemId: string, checked: boolean) => {
|
||||
setPurchaseData(prev => prev.map(item =>
|
||||
@@ -418,7 +411,6 @@ export function PurchaseManagement() {
|
||||
<TableCell className="text-right font-bold">{tableTotals.totalAmount.toLocaleString()}</TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
|
||||
@@ -479,28 +471,6 @@ export function PurchaseManagement() {
|
||||
{item.taxInvoiceReceived && <span className="text-xs text-orange-500 font-medium">수취</span>}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
{handlers.isSelected && (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-gray-600 hover:text-gray-700 hover:bg-gray-50"
|
||||
onClick={() => handleEdit(item)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-red-500 hover:text-red-600 hover:bg-red-50"
|
||||
onClick={() => handlers.onDelete?.(item)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
},
|
||||
@@ -527,22 +497,6 @@ export function PurchaseManagement() {
|
||||
{ label: '공급가액', value: `${item.supplyAmount.toLocaleString()}원` },
|
||||
{ label: '합계금액', value: `${item.totalAmount.toLocaleString()}원` },
|
||||
]}
|
||||
actions={
|
||||
handlers.isSelected ? (
|
||||
<div className="flex gap-2 w-full">
|
||||
<Button variant="outline" className="flex-1" onClick={() => handleEdit(item)}>
|
||||
<Pencil className="w-4 h-4 mr-2" /> 수정
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1 text-red-500 border-red-200 hover:bg-red-50 hover:text-red-600"
|
||||
onClick={() => handlers.onDelete?.(item)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" /> 삭제
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
@@ -557,7 +511,6 @@ export function PurchaseManagement() {
|
||||
tableTotals,
|
||||
searchQuery,
|
||||
handleRowClick,
|
||||
handleEdit,
|
||||
handleTaxInvoiceToggle,
|
||||
handleSaveAccountSubject,
|
||||
]
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
import { getReceivablesList, getReceivablesSummary, updateOverdueStatus, updateMemos, exportReceivablesExcel } from './actions';
|
||||
import { toast } from 'sonner';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
|
||||
// ===== Props 인터페이스 =====
|
||||
interface ReceivablesStatusProps {
|
||||
@@ -63,6 +64,7 @@ const generateYearOptions = (): Array<{ value: number; label: string }> => {
|
||||
const categoryOrder: CategoryType[] = ['sales', 'deposit', 'bill', 'receivable'];
|
||||
|
||||
export function ReceivablesStatus({ highlightVendorId, initialData, initialSummary }: ReceivablesStatusProps) {
|
||||
const { canExport } = usePermission();
|
||||
// ===== Refs =====
|
||||
const highlightRowRef = useRef<HTMLTableRowElement>(null);
|
||||
|
||||
@@ -403,14 +405,16 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma
|
||||
)}
|
||||
새로고침
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleExcelDownload}
|
||||
>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
엑셀 다운로드
|
||||
</Button>
|
||||
{canExport && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleExcelDownload}
|
||||
>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
엑셀 다운로드
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
|
||||
@@ -19,9 +19,7 @@ import { useRouter } from 'next/navigation';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
Receipt,
|
||||
Pencil,
|
||||
Save,
|
||||
Trash2,
|
||||
Search,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -78,7 +76,6 @@ const tableColumns = [
|
||||
{ key: 'salesType', label: '매출유형', className: 'text-center' },
|
||||
{ key: 'taxInvoice', label: '세금계산서 발행완료', className: 'text-center' },
|
||||
{ key: 'transactionStatement', label: '거래명세서 발행완료', className: 'text-center' },
|
||||
{ key: 'actions', label: '작업', className: 'text-center w-[80px]' },
|
||||
];
|
||||
|
||||
// ===== Props 타입 =====
|
||||
@@ -186,10 +183,6 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
router.push(`/ko/accounting/sales/${item.id}?mode=view`);
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback((item: SalesRecord) => {
|
||||
router.push(`/ko/accounting/sales/${item.id}?mode=edit`);
|
||||
}, [router]);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push('/ko/accounting/sales?mode=new');
|
||||
}, [router]);
|
||||
@@ -424,7 +417,6 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
|
||||
@@ -479,28 +471,6 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
{item.transactionStatementIssued && <span className="text-xs text-orange-500 font-medium">발행</span>}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
{handlers.isSelected && (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-gray-600 hover:text-gray-700 hover:bg-gray-50"
|
||||
onClick={() => handleEdit(item)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-red-500 hover:text-red-600 hover:bg-red-50"
|
||||
onClick={() => handlers.onDelete?.(item)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
|
||||
@@ -525,22 +495,6 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
{ label: '매출금액', value: `${item.totalAmount.toLocaleString()}원` },
|
||||
{ label: '미수금액', value: item.outstandingAmount > 0 ? `${item.outstandingAmount.toLocaleString()}원` : '-' },
|
||||
]}
|
||||
actions={
|
||||
handlers.isSelected ? (
|
||||
<div className="flex gap-2 w-full">
|
||||
<Button variant="outline" className="flex-1" onClick={() => handleEdit(item)}>
|
||||
<Pencil className="w-4 h-4 mr-2" /> 수정
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1 text-red-500 border-red-200 hover:bg-red-50 hover:text-red-600"
|
||||
onClick={() => handlers.onDelete?.(item)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" /> 삭제
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
@@ -555,7 +509,6 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
tableTotals,
|
||||
searchQuery,
|
||||
handleRowClick,
|
||||
handleEdit,
|
||||
handleCreate,
|
||||
handleTaxInvoiceToggle,
|
||||
handleTransactionStatementToggle,
|
||||
|
||||
@@ -29,6 +29,7 @@ import type { VendorLedgerItem, VendorLedgerSummary } from './types';
|
||||
import { getVendorLedgerList, getVendorLedgerSummary, exportVendorLedgerExcel } from './actions';
|
||||
import { toast } from 'sonner';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
|
||||
// ===== 테이블 컬럼 정의 =====
|
||||
const tableColumns = [
|
||||
@@ -59,6 +60,7 @@ export function VendorLedger({
|
||||
initialPagination,
|
||||
}: VendorLedgerProps) {
|
||||
const router = useRouter();
|
||||
const { canExport } = usePermission();
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [data, setData] = useState<VendorLedgerItem[]>(initialData);
|
||||
@@ -221,10 +223,12 @@ export function VendorLedger({
|
||||
|
||||
// 헤더 액션 (엑셀 다운로드) - 함수로 변환
|
||||
headerActions: () => (
|
||||
<Button variant="outline" size="sm" onClick={handleExcelDownload}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
엑셀 다운로드
|
||||
</Button>
|
||||
canExport ? (
|
||||
<Button variant="outline" size="sm" onClick={handleExcelDownload}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
엑셀 다운로드
|
||||
</Button>
|
||||
) : null
|
||||
),
|
||||
|
||||
// 테이블 푸터 (합계 행)
|
||||
|
||||
@@ -14,11 +14,7 @@ import { useState, useMemo, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
Building2,
|
||||
Pencil,
|
||||
Trash2,
|
||||
Eye,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableRow, TableCell } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -63,7 +59,6 @@ const tableColumns = [
|
||||
{ key: 'transactionGrade', label: '거래등급', className: 'text-center w-[100px]' },
|
||||
{ key: 'outstandingAmount', label: '미수금', className: 'text-right w-[120px]' },
|
||||
{ key: 'badDebtStatus', label: '악성채권', className: 'text-center w-[90px]' },
|
||||
{ key: 'actions', label: '작업', className: 'text-center w-[150px]' },
|
||||
];
|
||||
|
||||
// ===== 컴포넌트 Props =====
|
||||
@@ -94,13 +89,6 @@ export function VendorManagement({ initialData, initialTotal }: VendorManagement
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(vendor: Vendor) => {
|
||||
router.push(`/ko/accounting/vendors/${vendor.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// ===== UniversalListPage Config =====
|
||||
const config: UniversalListConfig<Vendor> = useMemo(
|
||||
() => ({
|
||||
@@ -363,29 +351,6 @@ export function VendorManagement({ initialData, initialTotal }: VendorManagement
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
{/* 작업 */}
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
{handlers.isSelected && (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => handleEdit(vendor)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-red-500 hover:text-red-600 hover:bg-red-50"
|
||||
onClick={() => handlers.onDelete?.(vendor)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
|
||||
@@ -413,29 +378,10 @@ export function VendorManagement({ initialData, initialTotal }: VendorManagement
|
||||
},
|
||||
{ label: '결제일', value: `매입 ${vendor.purchasePaymentDay}일 / 매출 ${vendor.salesPaymentDay}일` },
|
||||
]}
|
||||
actions={
|
||||
handlers.isSelected ? (
|
||||
<div className="flex gap-2 w-full">
|
||||
<Button variant="outline" className="flex-1" onClick={() => handleRowClick(vendor)}>
|
||||
<Eye className="w-4 h-4 mr-2" /> 상세
|
||||
</Button>
|
||||
<Button variant="outline" className="flex-1" onClick={() => handleEdit(vendor)}>
|
||||
<Pencil className="w-4 h-4 mr-2" /> 수정
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1 text-red-500 border-red-200 hover:bg-red-50 hover:text-red-600"
|
||||
onClick={() => handlers.onDelete?.(vendor)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" /> 삭제
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
[initialData, stats, handleRowClick, handleEdit, router]
|
||||
[initialData, stats, handleRowClick, router]
|
||||
);
|
||||
|
||||
return <UniversalListPage config={config} initialData={initialData} />;
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Clock,
|
||||
FileX,
|
||||
Files,
|
||||
Edit,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
@@ -75,6 +74,7 @@ import {
|
||||
APPROVAL_STATUS_COLORS,
|
||||
} from './types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
|
||||
// ===== 통계 타입 =====
|
||||
interface InboxSummary {
|
||||
@@ -87,6 +87,7 @@ interface InboxSummary {
|
||||
export function ApprovalBox() {
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const { canApprove } = usePermission();
|
||||
|
||||
// ===== 상태 관리 =====
|
||||
const [activeTab, setActiveTab] = useState<ApprovalTabType>('all');
|
||||
@@ -402,13 +403,6 @@ export function ApprovalBox() {
|
||||
}
|
||||
}, [selectedDocument, router]);
|
||||
|
||||
const handleEditClick = useCallback(
|
||||
(item: ApprovalRecord) => {
|
||||
router.push(`/ko/approval/draft/new?id=${item.id}&mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleModalCopy = useCallback(() => {
|
||||
toast.info('문서 복제 기능은 준비 중입니다.');
|
||||
setIsModalOpen(false);
|
||||
@@ -508,7 +502,6 @@ export function ApprovalBox() {
|
||||
{ key: 'approver', label: '결재자' },
|
||||
{ key: 'draftDate', label: '기안일시' },
|
||||
{ key: 'status', label: '상태', className: 'text-center' },
|
||||
{ key: 'actions', label: '작업', className: 'w-[80px] text-center' },
|
||||
],
|
||||
|
||||
tabs: tabs,
|
||||
@@ -591,7 +584,7 @@ export function ApprovalBox() {
|
||||
|
||||
headerActions: ({ selectedItems, onClearSelection }) => (
|
||||
<>
|
||||
{selectedItems.size > 0 && (
|
||||
{selectedItems.size > 0 && canApprove && (
|
||||
<div className="ml-auto flex gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
@@ -676,18 +669,6 @@ export function ApprovalBox() {
|
||||
{APPROVAL_STATUS_LABELS[item.status]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
{isSelected && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditClick(item)}
|
||||
title="기안함 수정 페이지로 이동"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
},
|
||||
@@ -720,7 +701,7 @@ export function ApprovalBox() {
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
item.status === 'pending' && isSelected ? (
|
||||
item.status === 'pending' && isSelected && canApprove ? (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
@@ -813,8 +794,8 @@ export function ApprovalBox() {
|
||||
mode="inbox"
|
||||
onEdit={handleModalEdit}
|
||||
onCopy={handleModalCopy}
|
||||
onApprove={handleModalApprove}
|
||||
onReject={handleModalReject}
|
||||
onApprove={canApprove ? handleModalApprove : undefined}
|
||||
onReject={canApprove ? handleModalReject : undefined}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -834,7 +815,6 @@ export function ApprovalBox() {
|
||||
filterOption,
|
||||
sortOption,
|
||||
handleDocumentClick,
|
||||
handleEditClick,
|
||||
approveDialogOpen,
|
||||
pendingSelectedItems,
|
||||
handleApproveConfirm,
|
||||
@@ -848,6 +828,7 @@ export function ApprovalBox() {
|
||||
handleModalCopy,
|
||||
handleModalApprove,
|
||||
handleModalReject,
|
||||
canApprove,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Send,
|
||||
Trash2,
|
||||
Plus,
|
||||
Pencil,
|
||||
Bell,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
@@ -466,7 +465,6 @@ export function DraftBox() {
|
||||
{ key: 'approvers', label: '결재자' },
|
||||
{ key: 'draftDate', label: '기안일시' },
|
||||
{ key: 'status', label: '상태', className: 'text-center' },
|
||||
{ key: 'actions', label: '작업', className: 'text-center' },
|
||||
],
|
||||
|
||||
// 검색창 (공통 컴포넌트에서 자동 생성)
|
||||
@@ -644,28 +642,6 @@ export function DraftBox() {
|
||||
{DOCUMENT_STATUS_LABELS[item.status]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
{isSelected && item.status === 'draft' && (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-gray-600 hover:text-gray-700 hover:bg-gray-50"
|
||||
onClick={() => handleDocumentClick(item)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() => handleDeleteSingle(item.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
},
|
||||
@@ -698,26 +674,6 @@ export function DraftBox() {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
isSelected && item.status === 'draft' ? (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => handleDocumentClick(item)}
|
||||
>
|
||||
<Pencil className="w-4 h-4 mr-2" /> 수정
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1 text-red-600"
|
||||
onClick={() => handleDeleteSingle(item.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" /> 삭제
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
onClick={() => handleDocumentClick(item)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
export { BoardDetailClientV2 } from './BoardDetailClientV2';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ClipboardList, Edit, Trash2, Plus } from 'lucide-react';
|
||||
import { ClipboardList, Plus } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { TableRow, TableCell } from '@/components/ui/table';
|
||||
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
|
||||
@@ -88,7 +87,6 @@ const createBoardManagementConfig = (router: ReturnType<typeof useRouter>): Univ
|
||||
{ key: 'status', label: '상태', className: 'min-w-[80px]' },
|
||||
{ key: 'authorName', label: '작성자', className: 'min-w-[100px]' },
|
||||
{ key: 'createdAt', label: '등록일시', className: 'min-w-[120px]' },
|
||||
{ key: 'actions', label: '작업', className: 'w-[100px] text-right' },
|
||||
],
|
||||
|
||||
// 탭 설정 (클라이언트 사이드 계산)
|
||||
@@ -160,18 +158,6 @@ const createBoardManagementConfig = (router: ReturnType<typeof useRouter>): Univ
|
||||
</TableCell>
|
||||
<TableCell>{item.authorName}</TableCell>
|
||||
<TableCell>{formatDate(item.createdAt)}</TableCell>
|
||||
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
|
||||
{isSelected && (
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button variant="ghost" size="sm" onClick={() => onEdit?.()} title="수정">
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => onDelete?.()} title="삭제">
|
||||
<Trash2 className="w-4 h-4 text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
},
|
||||
@@ -209,36 +195,6 @@ const createBoardManagementConfig = (router: ReturnType<typeof useRouter>): Univ
|
||||
<InfoField label="등록일시" value={formatDate(item.createdAt)} />
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
isSelected ? (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
variant="default"
|
||||
size="default"
|
||||
className="flex-1 min-w-[100px] h-11"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit?.();
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
수정
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="flex-1 min-w-[100px] h-11 border-red-200 text-red-600 hover:border-red-300 bg-transparent"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete?.();
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -41,7 +41,7 @@ export function ParentMenuRedirect({ parentPath, fallbackPath }: ParentMenuRedir
|
||||
const findParentMenu = (items: any[], targetPath: string): any | null => {
|
||||
for (const item of items) {
|
||||
// 경로가 일치하는지 확인 (locale prefix 제거 후 비교)
|
||||
const itemPath = item.path?.replace(/^\/[a-z]{2}\//, '/') || '';
|
||||
const itemPath = item.path?.replace(/^\/(ko|en|ja)\//, '/') || '';
|
||||
if (itemPath === targetPath || item.path === targetPath) {
|
||||
return item;
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export function ParentMenuRedirect({ parentPath, fallbackPath }: ParentMenuRedir
|
||||
if (parentMenu && parentMenu.children && parentMenu.children.length > 0) {
|
||||
// 첫 번째 자식 메뉴의 경로로 리다이렉트
|
||||
const firstChild = parentMenu.children[0];
|
||||
const firstChildPath = firstChild.path?.replace(/^\/[a-z]{2}\//, '/') || fallbackPath;
|
||||
const firstChildPath = firstChild.path?.replace(/^\/(ko|en|ja)\//, '/') || fallbackPath;
|
||||
router.replace(firstChildPath);
|
||||
} else {
|
||||
// 자식이 없으면 fallback으로 이동
|
||||
|
||||
@@ -40,6 +40,7 @@ export function PermissionGuard({
|
||||
update: permission.canUpdate,
|
||||
delete: permission.canDelete,
|
||||
approve: permission.canApprove,
|
||||
export: permission.canExport,
|
||||
};
|
||||
|
||||
if (!actionMap[action]) {
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Calendar,
|
||||
Plus,
|
||||
FileText,
|
||||
Edit,
|
||||
Search,
|
||||
} from 'lucide-react';
|
||||
import type { ExcelColumn } from '@/lib/utils/excel-download';
|
||||
@@ -277,7 +276,6 @@ export function AttendanceManagement() {
|
||||
{ key: 'breakTime', label: '휴게', className: 'min-w-[60px]' },
|
||||
{ key: 'overtime', label: '연장근무', className: 'min-w-[80px]' },
|
||||
{ key: 'reason', label: '사유', className: 'min-w-[80px]' },
|
||||
{ key: 'actions', label: '작업', className: 'w-[60px] text-center' },
|
||||
], []);
|
||||
|
||||
// 체크박스 토글
|
||||
@@ -631,18 +629,6 @@ export function AttendanceManagement() {
|
||||
</Button>
|
||||
) : '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{isSelected && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditAttendance(item)}
|
||||
title="수정"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
},
|
||||
@@ -687,21 +673,6 @@ export function AttendanceManagement() {
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
isSelected ? (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
variant="default"
|
||||
size="default"
|
||||
className="flex-1 min-w-[100px] h-11"
|
||||
onClick={(e) => { e.stopPropagation(); handleEditAttendance(item); }}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
수정
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { CreditCard, Edit, Trash2, Plus, Search, RefreshCw } from 'lucide-react';
|
||||
import { CreditCard, Plus, Search, RefreshCw } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -126,7 +126,6 @@ export function CardManagement({ initialData }: CardManagementProps) {
|
||||
{ key: 'department', label: '부서', className: 'min-w-[100px]' },
|
||||
{ key: 'userName', label: '사용자', className: 'min-w-[100px]' },
|
||||
{ key: 'position', label: '직책', className: 'min-w-[100px]' },
|
||||
{ key: 'actions', label: '작업', className: 'w-[100px] text-right' },
|
||||
], []);
|
||||
|
||||
// 체크박스 토글
|
||||
@@ -188,15 +187,6 @@ export function CardManagement({ initialData }: CardManagementProps) {
|
||||
router.push(`/ko/hr/card-management/${row.id}?mode=view`);
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback((id: string) => {
|
||||
router.push(`/ko/hr/card-management/${id}?mode=edit`);
|
||||
}, [router]);
|
||||
|
||||
const openDeleteDialog = useCallback((card: Card) => {
|
||||
setCardToDelete(card);
|
||||
setDeleteDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
// ===== UniversalListPage 설정 =====
|
||||
const cardManagementConfig: UniversalListConfig<Card> = useMemo(() => ({
|
||||
title: '카드관리',
|
||||
@@ -286,28 +276,6 @@ export function CardManagement({ initialData }: CardManagementProps) {
|
||||
<TableCell>{item.user?.departmentName || '-'}</TableCell>
|
||||
<TableCell>{item.user?.employeeName || '-'}</TableCell>
|
||||
<TableCell>{item.user?.positionName || '-'}</TableCell>
|
||||
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
|
||||
{isSelected && (
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(item.id)}
|
||||
title="수정"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => openDeleteDialog(item)}
|
||||
title="삭제"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
},
|
||||
@@ -346,30 +314,6 @@ export function CardManagement({ initialData }: CardManagementProps) {
|
||||
<InfoField label="직책" value={item.user?.positionName || '-'} />
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
isSelected ? (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
variant="default"
|
||||
size="default"
|
||||
className="flex-1 min-w-[100px] h-11"
|
||||
onClick={(e) => { e.stopPropagation(); handleEdit(item.id); }}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
수정
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="flex-1 min-w-[100px] h-11 border-red-200 text-red-600 hover:border-red-300 bg-transparent"
|
||||
onClick={(e) => { e.stopPropagation(); openDeleteDialog(item); }}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -398,8 +342,6 @@ export function CardManagement({ initialData }: CardManagementProps) {
|
||||
activeTab,
|
||||
handleAddCard,
|
||||
handleRowClick,
|
||||
handleEdit,
|
||||
openDeleteDialog,
|
||||
deleteDialogOpen,
|
||||
cardToDelete,
|
||||
handleDeleteCard,
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
type FilterFieldConfig,
|
||||
type FilterValues,
|
||||
} from '@/components/templates/UniversalListPage';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
|
||||
import { SalaryDetailDialog } from './SalaryDetailDialog';
|
||||
import {
|
||||
@@ -52,6 +53,7 @@ import {
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
export function SalaryManagement() {
|
||||
const { canExport } = usePermission();
|
||||
// ===== 상태 관리 =====
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [sortOption, setSortOption] = useState<SortOption>('rank');
|
||||
@@ -427,10 +429,12 @@ export function SalaryManagement() {
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button variant="outline" onClick={() => toast.info('엑셀 다운로드 기능은 준비 중입니다.')}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
엑셀 다운로드
|
||||
</Button>
|
||||
{canExport && (
|
||||
<Button variant="outline" onClick={() => toast.info('엑셀 다운로드 기능은 준비 중입니다.')}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
엑셀 다운로드
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
|
||||
|
||||
@@ -13,12 +13,11 @@ import { useRouter } from 'next/navigation';
|
||||
import type { ItemMaster } from '@/types/item';
|
||||
import { ITEM_TYPE_LABELS } from '@/types/item';
|
||||
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { TableRow, TableCell } from '@/components/ui/table';
|
||||
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { Search, Plus, Edit, Trash2, Package, FileDown, Upload } from 'lucide-react';
|
||||
import { Plus, Package, FileDown, Upload } from 'lucide-react';
|
||||
import { downloadExcelTemplate, parseExcelFile, type ExcelColumn, type TemplateColumn } from '@/lib/utils/excel-download';
|
||||
import { useItemList } from '@/hooks/useItemList';
|
||||
import { handleApiError } from '@/lib/api/error-handler';
|
||||
@@ -418,7 +417,6 @@ export default function ItemListClient() {
|
||||
{ key: 'specification', label: '규격', className: 'min-w-[100px]' },
|
||||
{ key: 'unit', label: '단위', className: 'min-w-[60px]' },
|
||||
{ key: 'isActive', label: '품목상태', className: 'min-w-[80px]' },
|
||||
{ key: 'actions', label: '작업', className: 'w-[120px] text-right' },
|
||||
],
|
||||
|
||||
// 클라이언트 사이드 필터링 (외부 useItemList 훅 사용)
|
||||
@@ -485,8 +483,8 @@ export default function ItemListClient() {
|
||||
handlers: SelectionHandlers & RowClickHandlers<ItemMaster>
|
||||
) => {
|
||||
return (
|
||||
<TableRow key={item.id} className="hover:bg-muted/50">
|
||||
<TableCell className="text-center">
|
||||
<TableRow key={item.id} className="hover:bg-muted/50 cursor-pointer" onClick={() => handleView(item.itemCode, item.itemType, item.id)}>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
checked={handlers.isSelected}
|
||||
onCheckedChange={handlers.onToggle}
|
||||
@@ -524,34 +522,6 @@ export default function ItemListClient() {
|
||||
{item.isActive ? '활성' : '비활성'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => { e.stopPropagation(); handleView(item.itemCode, item.itemType, item.id); }}
|
||||
title="상세 보기"
|
||||
>
|
||||
<Search className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => { e.stopPropagation(); handleEdit(item.itemCode, item.itemType, item.id); }}
|
||||
title="수정"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => { e.stopPropagation(); openDeleteDialog(item.id, item.itemCode, item.itemType); }}
|
||||
title="삭제"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
},
|
||||
@@ -599,39 +569,6 @@ export default function ItemListClient() {
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
handlers.isSelected ? (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
variant="default"
|
||||
size="default"
|
||||
className="flex-1 min-w-[100px] h-11"
|
||||
onClick={(e) => { e.stopPropagation(); handleView(item.itemCode, item.itemType, item.id); }}
|
||||
>
|
||||
<Search className="h-4 w-4 mr-2" />
|
||||
상세
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="default"
|
||||
className="flex-1 min-w-[100px] h-11"
|
||||
onClick={(e) => { e.stopPropagation(); handleEdit(item.itemCode, item.itemType, item.id); }}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
수정
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="flex-1 min-w-[100px] h-11 border-red-200 text-red-600 hover:border-red-300 bg-[rgba(255,255,255,0)]"
|
||||
onClick={(e) => { e.stopPropagation(); openDeleteDialog(item.id, item.itemCode, item.itemType); }}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -13,9 +13,6 @@ import {
|
||||
Package,
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Plus,
|
||||
Edit,
|
||||
History,
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -213,7 +210,6 @@ export function PricingListClient({
|
||||
{ key: 'marginRate', label: '마진율', className: 'min-w-[80px] text-right', hideOnMobile: true },
|
||||
{ key: 'effectiveDate', label: '적용일', className: 'min-w-[100px]', hideOnMobile: true },
|
||||
{ key: 'status', label: '상태', className: 'min-w-[80px]' },
|
||||
{ key: 'actions', label: '작업', className: 'w-[120px] text-right' },
|
||||
];
|
||||
|
||||
// 테이블 행 렌더링
|
||||
@@ -284,41 +280,6 @@ export function PricingListClient({
|
||||
: '-'}
|
||||
</TableCell>
|
||||
<TableCell>{renderStatusBadge(item)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
{item.status === 'not_registered' ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => { e.stopPropagation(); handleRegister(item); }}
|
||||
title="단가 등록"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => { e.stopPropagation(); handleEdit(item); }}
|
||||
title="수정"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
{item.currentRevision > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => { e.stopPropagation(); handleHistory(item); }}
|
||||
title="이력"
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
@@ -363,46 +324,6 @@ export function PricingListClient({
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
isSelected ? (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{item.status === 'not_registered' ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="default"
|
||||
className="flex-1 min-w-[100px] h-11"
|
||||
onClick={(e) => { e.stopPropagation(); handleRegister(item); }}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
등록
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="default"
|
||||
size="default"
|
||||
className="flex-1 min-w-[100px] h-11"
|
||||
onClick={(e) => { e.stopPropagation(); handleEdit(item); }}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
수정
|
||||
</Button>
|
||||
{item.currentRevision > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="flex-1 min-w-[100px] h-11"
|
||||
onClick={(e) => { e.stopPropagation(); handleHistory(item); }}
|
||||
>
|
||||
<History className="h-4 w-4 mr-2" />
|
||||
이력
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Megaphone, Pencil, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Megaphone } from 'lucide-react';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -41,13 +40,6 @@ export function PopupList({ initialData }: PopupListProps) {
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(id: string) => {
|
||||
router.push(`/ko/settings/popup-management/${id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push('/ko/settings/popup-management?mode=new');
|
||||
}, [router]);
|
||||
@@ -85,7 +77,6 @@ export function PopupList({ initialData }: PopupListProps) {
|
||||
{ key: 'author', label: '작성자', className: 'w-[100px] text-center' },
|
||||
{ key: 'createdAt', label: '등록일', className: 'w-[110px] text-center' },
|
||||
{ key: 'period', label: '기간', className: 'w-[180px] text-center' },
|
||||
{ key: 'actions', label: '작업', className: 'w-[180px] text-center' },
|
||||
],
|
||||
|
||||
// 클라이언트 사이드 필터링
|
||||
@@ -148,29 +139,6 @@ export function PopupList({ initialData }: PopupListProps) {
|
||||
<TableCell className="text-center">
|
||||
{item.startDate}~{item.endDate}
|
||||
</TableCell>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
{handlers.isSelected && (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(item.id)}
|
||||
title="수정"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handlers.onDelete?.(item)}
|
||||
title="삭제"
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
},
|
||||
@@ -213,21 +181,6 @@ export function PopupList({ initialData }: PopupListProps) {
|
||||
<div className="text-sm text-muted-foreground">
|
||||
기간: {item.startDate} ~ {item.endDate}
|
||||
</div>
|
||||
{handlers.isSelected && (
|
||||
<div className="flex gap-2 pt-2" onClick={(e) => e.stopPropagation()}>
|
||||
<Button variant="outline" size="sm" onClick={() => handleEdit(item.id)}>
|
||||
수정
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-destructive"
|
||||
onClick={() => handlers.onDelete?.(item)}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -235,7 +188,7 @@ export function PopupList({ initialData }: PopupListProps) {
|
||||
);
|
||||
},
|
||||
}),
|
||||
[handleRowClick, handleEdit, handleCreate]
|
||||
[handleRowClick, handleCreate]
|
||||
);
|
||||
|
||||
return <UniversalListPage config={config} initialData={initialData} />;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { toast } from 'sonner';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { cancelSubscription, requestDataExport } from './actions';
|
||||
import type { SubscriptionInfo } from './types';
|
||||
import { PLAN_LABELS, SUBSCRIPTION_STATUS_LABELS } from './types';
|
||||
@@ -36,6 +37,7 @@ const formatCurrency = (amount: number): string => {
|
||||
};
|
||||
|
||||
export function SubscriptionClient({ initialData }: SubscriptionClientProps) {
|
||||
const { canExport } = usePermission();
|
||||
const [subscription, setSubscription] = useState<SubscriptionInfo>(initialData);
|
||||
const [showCancelDialog, setShowCancelDialog] = useState(false);
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
@@ -101,14 +103,16 @@ export function SubscriptionClient({ initialData }: SubscriptionClientProps) {
|
||||
icon={CreditCard}
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleExportData}
|
||||
disabled={isExporting}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
{isExporting ? '처리 중...' : '자료 내보내기'}
|
||||
</Button>
|
||||
{canExport && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleExportData}
|
||||
disabled={isExporting}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
{isExporting ? '처리 중...' : '자료 내보내기'}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-red-200 text-red-600 hover:bg-red-50 hover:border-red-300"
|
||||
|
||||
@@ -44,7 +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();
|
||||
const { canCreate: permCanCreate, canDelete: permCanDelete, canExport } = usePermission();
|
||||
|
||||
// ===== 상태 관리 =====
|
||||
// 원본 데이터 (클라이언트 사이드 필터링용)
|
||||
@@ -679,7 +679,7 @@ export function UniversalListPage<T>({
|
||||
|
||||
// 엑셀 다운로드 버튼 렌더링
|
||||
const renderExcelDownloadButton = useMemo(() => {
|
||||
if (!config.excelDownload || config.excelDownload.enabled === false) {
|
||||
if (!config.excelDownload || config.excelDownload.enabled === false || !canExport) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user