feat: ESLint 정리 및 전체 코드 품질 개선
- eslint.config.mjs 규칙 강화 및 정리 - 전역 unused import/변수 제거 (312개 파일) - next.config.ts, middleware, proxy route 개선 - CopyableCell molecule 추가 - 회계/결재/HR/생산/건설/품질/영업 등 전 도메인 lint 정리 - IntegratedListTemplateV2, DataTable, MobileCard 등 공통 컴포넌트 개선 - execute-server-action 에러 핸들링 보강
This commit is contained in:
@@ -26,7 +26,6 @@ import { CardNumberInput } from '@/components/ui/card-number-input';
|
||||
import { AccountNumberInput } from '@/components/ui/account-number-input';
|
||||
import { CurrencyInput } from '@/components/ui/currency-input';
|
||||
import { QuantityInput } from '@/components/ui/quantity-input';
|
||||
import { NumberInput } from '@/components/ui/number-input';
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { formatPhoneNumber, formatBusinessNumber, formatCardNumber, formatAccountNumber, formatNumber } from '@/lib/formatters';
|
||||
|
||||
@@ -154,8 +154,8 @@ export interface PermissionConfig {
|
||||
}
|
||||
|
||||
// ===== 상세 페이지 설정 =====
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export interface DetailConfig<T = Record<string, unknown>> {
|
||||
|
||||
export interface DetailConfig<_T = Record<string, unknown>> {
|
||||
/** 페이지 제목 */
|
||||
title: string;
|
||||
/** 페이지 설명 */
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, Fragment, useState, useEffect, useRef, useCallback } from "react";
|
||||
import { ReactNode, Fragment, useState, useEffect, useRef, useCallback, Children, isValidElement, cloneElement } from "react";
|
||||
import { LucideIcon, Trash2, Plus, Loader2, ArrowUpDown, ArrowUp, ArrowDown, Search } from "lucide-react";
|
||||
import { DateRangeSelector } from "@/components/molecules/DateRangeSelector";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
||||
import { TableSkeleton, MobileCardGridSkeleton, StatCardGridSkeleton, ListPageSkeleton } from "@/components/ui/skeleton";
|
||||
import { TableSkeleton, MobileCardGridSkeleton } from "@/components/ui/skeleton";
|
||||
import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -27,6 +27,7 @@ import { TabChip } from "@/components/atoms/TabChip";
|
||||
import { MultiSelectCombobox } from "@/components/ui/multi-select-combobox";
|
||||
import { MobileFilter, FilterFieldConfig, FilterValues } from "@/components/molecules/MobileFilter";
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
import { CopyableCell } from '@/components/molecules';
|
||||
|
||||
/**
|
||||
* 기본 통합 목록_버젼2
|
||||
@@ -55,6 +56,8 @@ export interface TableColumn {
|
||||
hideOnTablet?: boolean;
|
||||
/** 정렬 가능 여부 */
|
||||
sortable?: boolean;
|
||||
/** 셀 hover 시 복사 버튼 표시 (CopyableCell 자동 래핑) */
|
||||
copyable?: boolean;
|
||||
}
|
||||
|
||||
export interface PaginationConfig {
|
||||
@@ -281,14 +284,14 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
beforeTableContent,
|
||||
afterTableContent,
|
||||
tableColumns,
|
||||
tableTitle,
|
||||
tableTitle: _tableTitle,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
onSort,
|
||||
renderCustomTableHeader,
|
||||
tableFooter,
|
||||
data,
|
||||
totalCount,
|
||||
totalCount: _totalCount,
|
||||
allData,
|
||||
mobileDisplayCount,
|
||||
onLoadMore,
|
||||
@@ -301,17 +304,53 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
onBulkDelete,
|
||||
selectionActions,
|
||||
showCheckbox = true, // 기본값 true
|
||||
showRowNumber = true, // 기본값 true (번호 컬럼은 renderTableRow에서 처리)
|
||||
showRowNumber: _showRowNumber = true, // 기본값 true (번호 컬럼은 renderTableRow에서 처리)
|
||||
renderTableRow,
|
||||
renderMobileCard,
|
||||
pagination,
|
||||
devMetadata,
|
||||
devMetadata: _devMetadata,
|
||||
isLoading,
|
||||
columnSettings,
|
||||
}: IntegratedListTemplateV2Props<T>) {
|
||||
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
|
||||
// ===== copyable 컬럼 자동 래핑 =====
|
||||
// copyable: true인 컬럼의 셀 인덱스 맵 (cell index → column key)
|
||||
const copyableMap = useCallback(() => {
|
||||
const map = new Map<number, string>();
|
||||
const offset = showCheckbox ? 1 : 0;
|
||||
tableColumns.forEach((col, i) => {
|
||||
if (col.copyable) map.set(i + offset, col.key);
|
||||
});
|
||||
return map;
|
||||
}, [tableColumns, showCheckbox]);
|
||||
|
||||
const applyCopyableCells = useCallback((row: ReactNode, item: T): ReactNode => {
|
||||
const map = copyableMap();
|
||||
if (map.size === 0 || !isValidElement(row)) return row;
|
||||
|
||||
const rowEl = row as React.ReactElement<{ children?: ReactNode }>;
|
||||
const cells = Children.toArray(rowEl.props.children);
|
||||
if (cells.length === 0) return row;
|
||||
|
||||
const newCells = cells.map((cell, idx) => {
|
||||
const colKey = map.get(idx);
|
||||
if (!colKey || !isValidElement(cell)) return cell;
|
||||
|
||||
const rawValue = (item as Record<string, unknown>)[colKey];
|
||||
const copyValue = rawValue != null ? String(rawValue) : '';
|
||||
if (!copyValue) return cell;
|
||||
|
||||
const cellEl = cell as React.ReactElement<{ children?: ReactNode }>;
|
||||
return cloneElement(cellEl, {},
|
||||
<CopyableCell value={copyValue}>{cellEl.props.children}</CopyableCell>
|
||||
);
|
||||
});
|
||||
|
||||
return cloneElement(rowEl, {}, ...newCells);
|
||||
}, [copyableMap]);
|
||||
|
||||
// ===== 서버 사이드 모바일 인피니티 스크롤 =====
|
||||
// 모바일에서 누적 데이터를 관리하여 스크롤 시 계속 추가
|
||||
const [accumulatedMobileData, setAccumulatedMobileData] = useState<T[]>([]);
|
||||
@@ -368,7 +407,7 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
setAccumulatedMobileData([]);
|
||||
setLastAccumulatedPage(0);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
}, [activeTab]); // activeTab만 감지 - 탭 변경 시에만 리셋
|
||||
|
||||
// 클라이언트 사이드: allData가 변경되면 displayCount 리셋
|
||||
@@ -574,7 +613,7 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
};
|
||||
|
||||
// 헤더 액션 스켈레톤 (달력 + 프리셋 버튼 + 등록 버튼)
|
||||
const renderHeaderActionSkeleton = () => (
|
||||
const _renderHeaderActionSkeleton = () => (
|
||||
<div className="flex items-center gap-2 flex-wrap w-full">
|
||||
{dateRangeSelector?.enabled && (
|
||||
<>
|
||||
@@ -974,11 +1013,14 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
return (
|
||||
<TableHead
|
||||
key={column.key}
|
||||
className={`${column.className || ''} ${isSortable ? 'cursor-pointer select-none hover:bg-muted/50' : ''} ${columnSettings ? 'relative' : ''}`}
|
||||
onClick={isSortable ? () => onSort(column.key) : undefined}
|
||||
className={`${column.className || ''} ${columnSettings ? 'relative' : ''}`}
|
||||
>
|
||||
{column.key === "actions" && selectedItems.size === 0 ? "" : (
|
||||
<div className={`flex items-center gap-1 ${isSortable ? 'group' : ''} ${(column.className || '').includes('text-right') ? 'justify-end' : (column.className || '').includes('text-center') ? 'justify-center' : ''}`}>
|
||||
<div className={`flex items-center ${(column.className || '').includes('text-right') ? 'justify-end' : (column.className || '').includes('text-center') ? 'justify-center' : ''}`}>
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 ${isSortable ? 'group cursor-pointer select-none rounded px-1 hover:bg-muted/50' : ''}`}
|
||||
onClick={isSortable ? (e) => { e.stopPropagation(); onSort(column.key); } : undefined}
|
||||
>
|
||||
<span>{column.label}</span>
|
||||
{isSortable && (
|
||||
<span className="text-muted-foreground">
|
||||
@@ -993,6 +1035,7 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{columnSettings && (
|
||||
@@ -1047,7 +1090,7 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
const globalIndex = startIndex + index + 1;
|
||||
return (
|
||||
<Fragment key={itemId}>
|
||||
{renderTableRow(item, index, globalIndex)}
|
||||
{applyCopyableCells(renderTableRow(item, index, globalIndex), item)}
|
||||
</Fragment>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -353,7 +353,7 @@ export function UniversalListPage<T>({
|
||||
// ⚠️ config.onDataChange를 deps에서 제외: 콜백 참조 변경으로 인한 무한 루프 방지
|
||||
useEffect(() => {
|
||||
config.onDataChange?.(rawData);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
}, [rawData]);
|
||||
|
||||
// 서버 사이드 필터링: 의존성 변경 시 데이터 새로고침
|
||||
@@ -545,7 +545,7 @@ export function UniversalListPage<T>({
|
||||
useEffect(() => {
|
||||
onSearchChange?.(debouncedSearchValue);
|
||||
config.onSearchChange?.(debouncedSearchValue);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
}, [debouncedSearchValue, onSearchChange]);
|
||||
|
||||
// ===== 필터 핸들러 =====
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 기존 기능 100% 유지, 테이블 영역만 공통화
|
||||
*/
|
||||
|
||||
import { ReactNode, RefObject } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
import type { FilterFieldConfig, FilterValues } from '@/components/molecules/MobileFilter';
|
||||
import type { ExcelColumn } from '@/lib/utils/excel-download';
|
||||
@@ -29,6 +29,8 @@ export interface TableColumn {
|
||||
hideOnTablet?: boolean;
|
||||
/** 정렬 가능 여부 (기본값: true, NON_SORTABLE_KEYS에 해당하는 키는 자동 false) */
|
||||
sortable?: boolean;
|
||||
/** 셀 hover 시 복사 버튼 표시 (CopyableCell 래핑) */
|
||||
copyable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user