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

@@ -11,7 +11,7 @@
import { useEffect, useRef } from 'react';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import type { DynamicFieldRendererProps, ComputedConfig, DynamicFormData } from '../types';
import type { DynamicFieldRendererProps, ComputedConfig } from '../types';
/**
* 안전한 수식 평가기

View File

@@ -30,7 +30,7 @@ function formatCurrency(num: number, precision: number): string {
}
function parseCurrency(str: string): number {
const cleaned = str.replace(/[^0-9.\-]/g, '');
const cleaned = str.replace(/[^0-9.-]/g, '');
const num = parseFloat(cleaned);
return isNaN(num) ? 0 : num;
}
@@ -77,7 +77,7 @@ export function CurrencyField({
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const raw = e.target.value;
// 숫자, 점, 마이너스만 허용
const pattern = allowNegative ? /[^0-9.\-]/g : /[^0-9.]/g;
const pattern = allowNegative ? /[^0-9.-]/g : /[^0-9.]/g;
const cleaned = raw.replace(pattern, '');
setInputValue(cleaned);
}, [allowNegative]);

View File

@@ -17,7 +17,6 @@ export function NumberField({
disabled,
}: DynamicFieldRendererProps) {
const fieldKey = field.field_key || `field_${field.id}`;
const stringValue = value !== null && value !== undefined ? String(value) : '';
// properties에서 단위, 정밀도 등 추출
const unit = field.properties?.unit as string | undefined;

View File

@@ -8,7 +8,7 @@
import { useRouter } from 'next/navigation';
import type { ItemMaster } from '@/types/item';
import { ITEM_TYPE_LABELS, PART_TYPE_LABELS, PART_USAGE_LABELS, PRODUCT_CATEGORY_LABELS } from '@/types/item';
import { ITEM_TYPE_LABELS, PRODUCT_CATEGORY_LABELS } from '@/types/item';
import { getItemTypeStyle } from '@/lib/utils/status-config';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';

View File

@@ -7,7 +7,6 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { notFound } from 'next/navigation';
import ItemDetailClient from '@/components/items/ItemDetailClient';
import type { ItemMaster, ItemType, ProductCategory, PartType, PartUsage } from '@/types/item';
@@ -151,7 +150,6 @@ interface ItemDetailViewProps {
* 품목 상세 보기 컴포넌트
*/
export function ItemDetailView({ itemCode, itemType, itemId }: ItemDetailViewProps) {
const router = useRouter();
const [item, setItem] = useState<ItemMaster | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

View File

@@ -55,7 +55,7 @@ export default function BendingDiagramSection({
widthSumFieldKey,
setValue,
isSubmitting,
existingBendingDiagram,
existingBendingDiagram: _existingBendingDiagram,
existingBendingDiagramFileName,
existingBendingDiagramFileId,
onDeleteExistingFile,

View File

@@ -50,13 +50,13 @@ export default function ProductForm({
setProductStatus,
remarks,
setRemarks,
needsBOM,
setNeedsBOM,
specificationFile,
setSpecificationFile,
certificationFile,
setCertificationFile,
isSubmitting,
needsBOM: _needsBOM,
setNeedsBOM: _setNeedsBOM,
specificationFile: _specificationFile,
setSpecificationFile: _setSpecificationFile,
certificationFile: _certificationFile,
setCertificationFile: _setCertificationFile,
isSubmitting: _isSubmitting,
register,
setValue,
getValues,

View File

@@ -18,8 +18,8 @@ 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 { Plus, Package, FileDown, Upload } from 'lucide-react';
import { downloadExcelTemplate, parseExcelFile, type ExcelColumn, type TemplateColumn } from '@/lib/utils/excel-download';
import { Plus, Package } from 'lucide-react';
import { parseExcelFile, type ExcelColumn, type TemplateColumn } from '@/lib/utils/excel-download';
import { useItemList } from '@/hooks/useItemList';
import { handleApiError } from '@/lib/api/error-handler';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
@@ -80,16 +80,12 @@ export default function ItemListClient() {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [itemToDelete, setItemToDelete] = useState<{ id: string; code: string; itemType: string } | null>(null);
// Materials 타입 (SM, RM, CS는 Material 테이블 사용)
const MATERIAL_TYPES = ['SM', 'RM', 'CS'];
// API에서 품목 목록 및 테이블 컬럼 조회 (서버 사이드 검색/필터링)
const {
items,
pagination,
totalStats,
isLoading,
isSearching,
refresh,
search,
} = useItemList();
@@ -139,17 +135,6 @@ export default function ItemListClient() {
router.push(`/production/screen-production/${encodeURIComponent(itemCode)}?mode=view&type=${itemType}&id=${itemId}`);
};
const handleEdit = (itemCode: string, itemType: string, itemId: string) => {
// itemType을 query param으로 전달 (Materials 조회를 위해)
router.push(`/production/screen-production/${encodeURIComponent(itemCode)}?mode=edit&type=${itemType}&id=${itemId}`);
};
// 삭제 확인 다이얼로그 열기
const openDeleteDialog = (itemId: string, itemCode: string, itemType: string) => {
setItemToDelete({ id: itemId, code: itemCode, itemType });
setDeleteDialogOpen(true);
};
// 삭제 실행
const handleConfirmDelete = async () => {
if (!itemToDelete) return;
@@ -288,17 +273,6 @@ export default function ItemListClient() {
{ header: '활성상태', key: 'isActive', type: 'boolean', sampleValue: 'Y', description: 'Y:활성/N:비활성', width: 10 },
];
// 양식 다운로드
const handleTemplateDownload = async () => {
await downloadExcelTemplate({
columns: templateColumns,
filename: '품목등록_양식',
sheetName: '품목등록',
includeSampleRow: true,
includeGuideRow: true,
});
};
// 파일 업로드 input ref
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -400,11 +374,11 @@ export default function ItemListClient() {
// 테이블 컬럼 (sortable: true로 정렬 가능)
columns: [
{ key: 'rowNumber', label: '번호', className: 'w-[60px] text-center' },
{ key: 'itemCode', label: '품목코드', className: 'min-w-[120px]' },
{ key: 'itemCode', label: '품목코드', className: 'min-w-[120px]', copyable: true },
{ key: 'itemType', label: '품목유형', className: 'min-w-[100px]' },
{ key: 'itemName', label: '품목명', className: 'min-w-[150px]' },
{ key: 'specification', label: '규격', className: 'min-w-[100px]' },
{ key: 'unit', label: '단위', className: 'min-w-[60px]' },
{ key: 'itemName', label: '품목명', className: 'min-w-[150px]', copyable: true },
{ key: 'specification', label: '규격', className: 'min-w-[100px]', copyable: true },
{ key: 'unit', label: '단위', className: 'min-w-[60px]', copyable: true },
{ key: 'isActive', label: '품목상태', className: 'min-w-[80px]' },
],

View File

@@ -15,7 +15,6 @@ import {
Database,
FileText,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';

View File

@@ -30,7 +30,7 @@ interface DraggableFieldProps {
nextFieldId?: number;
}
export function DraggableField({ field, index, moveField, onDelete, onEdit, prevFieldId, nextFieldId }: DraggableFieldProps) {
export function DraggableField({ field, index: _index, moveField, onDelete, onEdit, prevFieldId, nextFieldId }: DraggableFieldProps) {
const [isDragging, setIsDragging] = useState(false);
const handleDragStart = (e: React.DragEvent) => {
@@ -63,7 +63,7 @@ export function DraggableField({ field, index, moveField, onDelete, onEdit, prev
if (data.id !== field.id) {
moveField(data.id, field.id);
}
} catch (err) {
} catch {
// Ignore - 다른 타입의 드래그 데이터
}
};

View File

@@ -70,7 +70,7 @@ export function DraggableSection({
if (data.index !== index) {
moveSection(data.index, index);
}
} catch (err) {
} catch {
// Ignore - 다른 타입의 드래그 데이터
}
};

View File

@@ -1,7 +1,7 @@
'use client';
import type { ItemPage, SectionTemplate, ItemMasterField, ItemSection, BOMItem } from '@/contexts/ItemMasterContext';
import { FieldDialog, type InputType } from '../dialogs/FieldDialog';
import type { ItemPage, SectionTemplate, ItemMasterField, ItemSection } from '@/contexts/ItemMasterContext';
import { FieldDialog } from '../dialogs/FieldDialog';
import { FieldDrawer } from '../dialogs/FieldDrawer';
import { TabManagementDialogs } from '../dialogs/TabManagementDialogs';
import { OptionDialog } from '../dialogs/OptionDialog';
@@ -17,7 +17,6 @@ import { SectionTemplateDialog } from '../dialogs/SectionTemplateDialog';
import { ImportSectionDialog } from '../dialogs/ImportSectionDialog';
import { ImportFieldDialog } from '../dialogs/ImportFieldDialog';
import type { CustomTab, AttributeSubTab } from '../hooks/useTabManagement';
import type { OptionColumn } from '../types';
import type { ConditionalFieldConfig } from '../components/ConditionalDisplayUI';
import type { SectionUsageResponse, FieldUsageResponse } from '@/types/item-master-api';

View File

@@ -29,7 +29,7 @@ export function ColumnDialog({
setColumnName,
columnKey,
setColumnKey,
textboxColumns,
textboxColumns: _textboxColumns,
setTextboxColumns,
}: ColumnDialogProps) {
const [isSubmitted, setIsSubmitted] = useState(false);

View File

@@ -118,7 +118,7 @@ export function FieldDialog({
setNewFieldConditionTargetType,
newFieldConditionFields,
setNewFieldConditionFields,
newFieldConditionSections,
newFieldConditionSections: _newFieldConditionSections,
setNewFieldConditionSections,
tempConditionValue,
setTempConditionValue,

View File

@@ -16,7 +16,7 @@ interface LoadTemplateDialogProps {
handleLoadTemplate: () => void;
}
const ITEM_TYPE_OPTIONS = [
const _ITEM_TYPE_OPTIONS = [
{ value: 'product', label: '제품' },
{ value: 'part', label: '부품' },
{ value: 'material', label: '자재' },

View File

@@ -62,7 +62,7 @@ export function MasterFieldDialog({
setNewMasterFieldInputType,
newMasterFieldRequired,
setNewMasterFieldRequired,
newMasterFieldCategory,
newMasterFieldCategory: _newMasterFieldCategory,
setNewMasterFieldCategory,
newMasterFieldDescription,
setNewMasterFieldDescription,

View File

@@ -4,7 +4,6 @@ import { useState, useEffect, useRef } from 'react';
import { toast } from 'sonner';
import { useItemMaster } from '@/contexts/ItemMasterContext';
import type { MasterOption, OptionColumn } from '../types';
import { attributeService } from '../services';
export interface UseAttributeManagementReturn {
// 속성 옵션 상태
@@ -189,7 +188,7 @@ export function useAttributeManagement(): UseAttributeManagementReturn {
}
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [unitOptions, materialOptions, surfaceTreatmentOptions, customAttributeOptions]);
// 옵션 추가

View File

@@ -44,8 +44,8 @@ export function useDeleteManagement({ itemPages }: UseDeleteManagementProps): Us
// 페이지 삭제 핸들러
const handleDeletePage = useCallback((pageId: number) => {
const pageToDelete = itemPages.find(p => p.id === pageId);
const sectionIds = pageToDelete?.sections.map(s => s.id) || [];
const fieldIds = pageToDelete?.sections.flatMap(s => s.fields?.map(f => f.id) || []) || [];
const _sectionIds = pageToDelete?.sections.map(s => s.id) || [];
const _fieldIds = pageToDelete?.sections.flatMap(s => s.fields?.map(f => f.id) || []) || [];
deleteItemPage(pageId);
}, [itemPages, deleteItemPage]);
@@ -53,7 +53,7 @@ export function useDeleteManagement({ itemPages }: UseDeleteManagementProps): Us
const handleDeleteSection = useCallback((pageId: number, sectionId: number) => {
const page = itemPages.find(p => p.id === pageId);
const sectionToDelete = page?.sections.find(s => s.id === sectionId);
const fieldIds = sectionToDelete?.fields?.map(f => f.id) || [];
const _fieldIds = sectionToDelete?.fields?.map(f => f.id) || [];
deleteSection(Number(sectionId));
}, [itemPages, deleteSection]);

View File

@@ -152,7 +152,7 @@ export function useInitialDataLoading({
}
hasInitialLoadRun.current = true;
loadInitialData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return {

View File

@@ -61,7 +61,7 @@ export interface UseMasterFieldManagementReturn {
export function useMasterFieldManagement(): UseMasterFieldManagementReturn {
const {
itemMasterFields,
itemMasterFields: _itemMasterFields,
addItemMasterField,
updateItemMasterField,
deleteItemMasterField,

View File

@@ -80,7 +80,7 @@ export function usePageManagement(): UsePageManagementReturn {
migrationDoneRef.current.add(page.id);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [itemPages.length]); // itemPages 길이가 변경될 때만 체크
// 페이지 추가
@@ -168,8 +168,8 @@ export function usePageManagement(): UsePageManagementReturn {
// 2025-12-01: 페이지 삭제 시 섹션들은 독립 섹션으로 이동 (필드 연결 유지)
const handleDeletePage = (pageId: number) => {
const pageToDelete = itemPages.find(p => p.id === pageId);
const sectionCount = pageToDelete?.sections.length || 0;
const fieldCount = pageToDelete?.sections.flatMap(s => s.fields || []).length || 0;
const _sectionCount = pageToDelete?.sections.length || 0;
const _fieldCount = pageToDelete?.sections.flatMap(s => s.fields || []).length || 0;
deleteItemPage(pageId);

View File

@@ -4,7 +4,6 @@ import { useState } from 'react';
import { toast } from 'sonner';
import { useItemMaster } from '@/contexts/ItemMasterContext';
import type { ItemPage, ItemSection, SectionTemplate } from '@/contexts/ItemMasterContext';
import { sectionService } from '../services';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
// 입력 모드 타입: 'new'/'existing' 또는 'custom'/'template' 모두 지원
@@ -172,7 +171,7 @@ export function useSectionManagement(): UseSectionManagementReturn {
const handleDeleteSection = async (pageId: number, sectionId: number) => {
const page = itemPages.find(p => p.id === pageId);
const sectionToDelete = page?.sections.find(s => s.id === sectionId);
const fieldIds = sectionToDelete?.fields?.map(f => f.id) || [];
const _fieldIds = sectionToDelete?.fields?.map(f => f.id) || [];
try {
await deleteSection(sectionId);

View File

@@ -204,7 +204,7 @@ export function useTabManagement(): UseTabManagementReturn {
if (isNumericKey && !currentFieldIds.has(activeAttributeTab)) {
setActiveAttributeTab('units');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [itemMasterFields]);
// 메인 탭 핸들러

View File

@@ -4,7 +4,7 @@ import { useState } from 'react';
import { toast } from 'sonner';
import { useItemMaster } from '@/contexts/ItemMasterContext';
import { useErrorAlert } from '../contexts';
import type { ItemPage, SectionTemplate, TemplateField, BOMItem, ItemMasterField } from '@/contexts/ItemMasterContext';
import type { ItemPage, SectionTemplate, TemplateField, BOMItem } from '@/contexts/ItemMasterContext';
import { templateService } from '../services';
import { ApiError } from '@/lib/api/error-handler';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
@@ -91,18 +91,18 @@ export interface UseTemplateManagementReturn {
export function useTemplateManagement(): UseTemplateManagementReturn {
const {
sectionTemplates,
addSectionTemplate,
updateSectionTemplate,
deleteSectionTemplate,
addSectionTemplate: _addSectionTemplate,
updateSectionTemplate: _updateSectionTemplate,
deleteSectionTemplate: _deleteSectionTemplate,
addSectionToPage,
addItemMasterField,
itemMasterFields,
tenantId,
addItemMasterField: _addItemMasterField,
itemMasterFields: _itemMasterFields,
tenantId: _tenantId,
// 2025-11-26: sectionsAsTemplates가 itemPages에서 파생되므로
// 섹션 탭에서 수정/삭제 시 실제 섹션 API를 호출해야 함
updateSection,
deleteSection,
itemPages,
itemPages: _itemPages,
// 2025-11-26: 섹션 탭에서 새 섹션 추가 시 독립 섹션으로 생성
createIndependentSection,
// 2025-11-27: entity_relationships 기반 필드 연결/해제

View File

@@ -9,7 +9,7 @@
import type { ItemMasterField } from '@/contexts/ItemMasterContext';
import type { ItemFieldType } from '@/types/item-master-api';
import { fieldService, type SingleFieldValidation } from './fieldService';
import { fieldService } from './fieldService';
// ===== Types =====

View File

@@ -119,7 +119,7 @@ export function HierarchyTab({
handleEditField,
moveField,
// 2025-11-26 추가: 섹션/필드 불러오기
setIsImportSectionDialogOpen,
setIsImportSectionDialogOpen: _setIsImportSectionDialogOpen,
setIsImportFieldDialogOpen,
setImportFieldTargetSectionId,
// 2025-11-27 추가: BOM 항목 API 함수

View File

@@ -47,8 +47,8 @@ export function MasterFieldTab({
setIsMasterFieldDialogOpen,
handleEditMasterField,
handleDeleteMasterField,
hasUnsavedChanges,
pendingChanges
hasUnsavedChanges: _hasUnsavedChanges,
pendingChanges: _pendingChanges
}: MasterFieldTabProps) {
return (
<Card>
@@ -60,11 +60,11 @@ export function MasterFieldTab({
<CardDescription> . .</CardDescription>
</div>
{/* 변경사항 배지 - 나중에 사용 예정으로 임시 숨김 */}
{false && hasUnsavedChanges && pendingChanges.masterFields.length > 0 && (
{/* {hasUnsavedChanges && pendingChanges.masterFields.length > 0 && (
<Badge variant="destructive" className="animate-pulse">
{pendingChanges.masterFields.length}개 변경
</Badge>
)}
)} */}
</div>
<Button onClick={() => setIsMasterFieldDialogOpen(true)}>
<Plus className="h-4 w-4 mr-2" />

View File

@@ -63,8 +63,8 @@ export function SectionsTab({
ITEM_TYPE_OPTIONS,
INPUT_TYPE_OPTIONS,
unitOptions = [],
hasUnsavedChanges = false,
pendingChanges = { sectionTemplates: [] },
hasUnsavedChanges: _hasUnsavedChanges = false,
pendingChanges: _pendingChanges = { sectionTemplates: [] },
onCloneSection,
setIsImportFieldDialogOpen,
setImportFieldTargetSectionId,
@@ -83,11 +83,11 @@ export function SectionsTab({
<CardDescription className="truncate"> 릿 </CardDescription>
</div>
{/* 변경사항 배지 - 나중에 사용 예정으로 임시 숨김 */}
{false && hasUnsavedChanges && pendingChanges.sectionTemplates.length > 0 && (
{/* {hasUnsavedChanges && pendingChanges.sectionTemplates.length > 0 && (
<Badge variant="destructive" className="animate-pulse">
{pendingChanges.sectionTemplates.length}개 변경
</Badge>
)}
)} */}
</div>
<Button size="sm" className="shrink-0" onClick={() => setIsSectionTemplateDialogOpen(true)}>
<Plus className="h-4 w-4 md:mr-2" /><span className="hidden md:inline"></span>