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:
유병철
2026-03-11 10:27:10 +09:00
parent 924726cba1
commit 81affdc441
315 changed files with 1977 additions and 1344 deletions

View File

@@ -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';

View File

@@ -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;
/** 페이지 설명 */

View File

@@ -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>
);
})

View File

@@ -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]);
// ===== 필터 핸들러 =====

View File

@@ -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;
}
/**