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

@@ -103,7 +103,7 @@ function BoardListContent({ boardCode }: { boardCode: string }) {
// 게시글 목록
const [posts, setPosts] = useState<BoardPost[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// 필터 및 검색
@@ -239,11 +239,11 @@ function BoardListContent({ boardCode }: { boardCode: string }) {
// 테이블 컬럼
const tableColumns: TableColumn[] = useMemo(() => [
{ key: 'no', label: 'No.', className: 'w-[60px] text-center' },
{ key: 'title', label: '제목', className: 'min-w-[200px]' },
{ key: 'author', label: '작성자', className: 'w-[120px]' },
{ key: 'views', label: '조회수', className: 'w-[80px] text-center' },
{ key: 'title', label: '제목', className: 'min-w-[200px]', copyable: true },
{ key: 'author', label: '작성자', className: 'w-[120px]', copyable: true },
{ key: 'views', label: '조회수', className: 'w-[80px] text-center', copyable: true },
{ key: 'status', label: '상태', className: 'w-[100px] text-center' },
{ key: 'createdAt', label: '등록일', className: 'w-[120px] text-center' },
{ key: 'createdAt', label: '등록일', className: 'w-[120px] text-center', copyable: true },
], []);
// 테이블 행 렌더링

View File

@@ -29,7 +29,7 @@ import {
deleteDynamicBoardPost,
} from '@/components/board/DynamicBoard/actions';
import { getBoardByCode } from '@/components/board/BoardManagement/actions';
import { transformApiToComment, type CommentApiData } from '@/components/customer-center/shared/types';
import { transformApiToComment } from '@/components/customer-center/shared/types';
import type { PostApiData } from '@/components/customer-center/shared/types';
import { sanitizeHTML } from '@/lib/sanitize';

View File

@@ -110,7 +110,7 @@ function DynamicBoardListContent({ boardCode }: { boardCode: string }) {
// 게시글 목록
const [posts, setPosts] = useState<BoardPost[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// 필터 및 검색
@@ -246,11 +246,11 @@ function DynamicBoardListContent({ boardCode }: { boardCode: string }) {
// 테이블 컬럼
const tableColumns: TableColumn[] = useMemo(() => [
{ key: 'no', label: 'No.', className: 'w-[60px] text-center' },
{ key: 'title', label: '제목', className: 'min-w-[200px]' },
{ key: 'author', label: '작성자', className: 'w-[120px]' },
{ key: 'views', label: '조회수', className: 'w-[80px] text-center' },
{ key: 'title', label: '제목', className: 'min-w-[200px]', copyable: true },
{ key: 'author', label: '작성자', className: 'w-[120px]', copyable: true },
{ key: 'views', label: '조회수', className: 'w-[80px] text-center', copyable: true },
{ key: 'status', label: '상태', className: 'w-[100px] text-center' },
{ key: 'createdAt', label: '등록일', className: 'w-[120px] text-center' },
{ key: 'createdAt', label: '등록일', className: 'w-[120px] text-center', copyable: true },
], []);
// 테이블 행 렌더링

View File

@@ -1,3 +1,5 @@
'use client';
import { CategoryManagement } from '@/components/business/construction/category-management';
export default function CategoriesPage() {

View File

@@ -18,7 +18,7 @@ interface OrderDetailPageProps {
export default function OrderDetailPage({ params }: OrderDetailPageProps) {
const { id } = use(params);
const router = useRouter();
const _router = useRouter();
const searchParams = useSearchParams();
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
const [data, setData] = useState<Awaited<ReturnType<typeof getOrderDetailFull>>['data']>(undefined);

View File

@@ -13,7 +13,7 @@ interface ContractDetailPageProps {
export default function ContractDetailPage({ params }: ContractDetailPageProps) {
const { id } = use(params);
const router = useRouter();
const _router = useRouter();
const searchParams = useSearchParams();
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
const [data, setData] = useState<Awaited<ReturnType<typeof getContractDetail>>['data']>(undefined);

View File

@@ -15,7 +15,7 @@ interface HandoverReportDetailPageProps {
export default function HandoverReportDetailPage({ params }: HandoverReportDetailPageProps) {
const { id } = use(params);
const router = useRouter();
const _router = useRouter();
const searchParams = useSearchParams();
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
const [data, setData] = useState<Awaited<ReturnType<typeof getHandoverReportDetail>>['data']>(undefined);

View File

@@ -4,7 +4,6 @@ import { useState, useCallback } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout';
import { EditableTable, EditableColumn } from '@/components/common/EditableTable';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import {

View File

@@ -75,6 +75,7 @@ export default function AttendancePage() {
setSiteLocation(finalLocation);
} else {
// no fallback location needed
}
} catch (error) {
console.error('[AttendancePage] loadSettings error:', error);

View File

@@ -11,23 +11,17 @@
'use client';
import { useSearchParams, useRouter } from 'next/navigation';
import { useState, useEffect, useMemo, Suspense } from 'react';
import { FileText, ArrowLeft, Calendar, User, Clock, MapPin, FileCheck } from 'lucide-react';
import { useState, useMemo, Suspense } from 'react';
import { FileText, ArrowLeft, Calendar, Clock, MapPin, FileCheck } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { FormSectionSkeleton } from '@/components/ui/skeleton';
import { format } from 'date-fns';
import { ko } from 'date-fns/locale';
import { toast } from 'sonner';
// 문서 유형 라벨

View File

@@ -4,7 +4,7 @@ import { CSVUploadPage } from '@/components/hr/EmployeeManagement/CSVUploadPage'
import type { Employee } from '@/components/hr/EmployeeManagement/types';
export default function EmployeeCSVUploadPage() {
const handleUpload = (employees: Employee[]) => {
const handleUpload = (_employees: Employee[]) => {
// TODO: API 연동
};

View File

@@ -50,7 +50,7 @@ function EmployeeManagementContent() {
toast.error(errorMessage);
return { success: false, error: errorMessage };
}
} catch (error) {
} catch (_error) {
toast.error('서버 오류가 발생했습니다.');
return { success: false, error: '서버 오류가 발생했습니다.' };
}

View File

@@ -2,7 +2,6 @@
import React from 'react';
import { Settings, X, Eye, EyeOff } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Switch } from '@/components/ui/switch';
export interface AuditDisplaySettings {

View File

@@ -123,7 +123,7 @@ export function Day1ChecklistPanel({
</div>
) : (
filteredCategories.map((category, categoryIndex) => {
filteredCategories.map((category, _categoryIndex) => {
const isExpanded = expandedCategories.has(category.id);
const progress = getCategoryProgress(category);
const allCompleted = progress.completed === progress.total;

View File

@@ -334,7 +334,7 @@ export const InspectionModal = ({
const handleImportSave = useCallback(async () => {
if (!importDocRef.current) return;
const data = importDocRef.current.getInspectionData();
const _data = importDocRef.current.getInspectionData();
setIsSaving(true);
try {
// TODO: 실제 저장 API 연동
@@ -354,7 +354,7 @@ export const InspectionModal = ({
: docInfo.label;
// 품질관리서 PDF 업로드 핸들러
const handleQualityFileUpload = (file: File) => {
const handleQualityFileUpload = (_file: File) => {
};
const handleQualityFileDelete = () => {

View File

@@ -457,7 +457,7 @@ export const ImportInspectionDocument = forwardRef<ImportInspectionRef, ImportIn
});
// OK/NG 선택 핸들러
const handleResultChange = useCallback((itemId: string, result: JudgmentResult) => {
const _handleResultChange = useCallback((itemId: string, result: JudgmentResult) => {
if (readOnly) return;
setValues((prev) => {
@@ -773,8 +773,8 @@ export const ImportInspectionDocument = forwardRef<ImportInspectionRef, ImportIn
</tr>
</thead>
<tbody>
{inspectionItems.map((item, idx) => {
const itemValue = values[item.id];
{inspectionItems.map((item, _idx) => {
const _itemValue = values[item.id];
// 그룹핑 정보
const hasCategory = !!item.subName;

View File

@@ -1,7 +1,7 @@
'use client';
import React, { useState, useRef, useCallback } from 'react';
import { Upload, FileText, Download, Trash2, Eye, RefreshCw, X } from 'lucide-react';
import { Upload, FileText, Download, Trash2, Eye, RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
export interface QualityDocumentFile {

View File

@@ -24,7 +24,6 @@ import {
Plus,
Users,
CheckCircle,
XCircle,
Loader2,
Bell,
} from "lucide-react";
@@ -58,13 +57,13 @@ export default function CustomerAccountManagementPage() {
const {
clients,
pagination,
isLoading,
isLoading: _isLoading,
fetchClients,
deleteClient: deleteClientApi,
} = useClientList();
const [searchTerm, setSearchTerm] = useState("");
const [filterType, setFilterType] = useState("all");
const [searchTerm, _setSearchTerm] = useState("");
const [filterType, _setFilterType] = useState("all");
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 20;
@@ -176,7 +175,7 @@ export default function CustomerAccountManagementPage() {
const paginatedClients = filteredClients;
// 모바일용 인피니티 스크롤 데이터
const mobileClients = filteredClients.slice(0, mobileDisplayCount);
const _mobileClients = filteredClients.slice(0, mobileDisplayCount);
// Intersection Observer를 이용한 인피니티 스크롤
useEffect(() => {
@@ -262,12 +261,12 @@ export default function CustomerAccountManagementPage() {
// 테이블 컬럼 정의 (Hooks 순서 보장을 위해 조건부 return 전에 정의)
const tableColumns: TableColumn[] = useMemo(() => [
{ key: "rowNumber", label: "번호", className: "px-4" },
{ key: "code", label: "코드", className: "px-4", sortable: true },
{ key: "code", label: "코드", className: "px-4", sortable: true, copyable: true },
{ key: "clientType", label: "구분", className: "px-4", sortable: true },
{ key: "name", label: "거래처명", className: "px-4", sortable: true },
{ key: "representative", label: "대표자", className: "px-4", sortable: true },
{ key: "manager", label: "담당자", className: "px-4", sortable: true },
{ key: "phone", label: "전화번호", className: "px-4", sortable: true },
{ key: "name", label: "거래처명", className: "px-4", sortable: true, copyable: true },
{ key: "representative", label: "대표자", className: "px-4", sortable: true, copyable: true },
{ key: "managerName", label: "담당자", className: "px-4", sortable: true, copyable: true },
{ key: "phone", label: "전화번호", className: "px-4", sortable: true, copyable: true },
], []);
// 핸들러 - 페이지 기반 네비게이션
@@ -275,7 +274,7 @@ export default function CustomerAccountManagementPage() {
router.push("/sales/client-management-sales-admin?mode=new");
};
const handleEdit = (customer: Client) => {
const _handleEdit = (customer: Client) => {
router.push(`/sales/client-management-sales-admin/${customer.id}?mode=edit`);
};
@@ -283,7 +282,7 @@ export default function CustomerAccountManagementPage() {
router.push(`/sales/client-management-sales-admin/${customer.id}?mode=view`);
};
const handleDelete = (customerId: string) => {
const _handleDelete = (customerId: string) => {
setDeleteTargetId(customerId);
setIsDeleteDialogOpen(true);
};
@@ -304,7 +303,7 @@ export default function CustomerAccountManagementPage() {
};
// 체크박스 선택
const toggleSelection = (id: string) => {
const _toggleSelection = (id: string) => {
const newSelection = new Set(selectedItems);
if (newSelection.has(id)) {
newSelection.delete(id);
@@ -314,7 +313,7 @@ export default function CustomerAccountManagementPage() {
setSelectedItems(newSelection);
};
const toggleSelectAll = () => {
const _toggleSelectAll = () => {
if (
selectedItems.size === paginatedClients.length &&
paginatedClients.length > 0
@@ -326,7 +325,7 @@ export default function CustomerAccountManagementPage() {
};
// 일괄 삭제
const handleBulkDelete = () => {
const _handleBulkDelete = () => {
if (selectedItems.size === 0) {
toast.error("삭제할 항목을 선택해주세요");
return;

View File

@@ -27,9 +27,6 @@ import {
PenLine,
Factory,
XCircle,
FileSpreadsheet,
FileCheck,
ClipboardList,
Eye,
CheckCircle2,
RotateCcw,
@@ -275,12 +272,12 @@ export default function OrderDetailPage() {
setIsProductionSuccessDialogOpen(false);
};
const handleViewProductionOrder = () => {
const _handleViewProductionOrder = () => {
// 생산지시 목록 페이지로 이동 (수주관리 내부)
router.push(`/sales/order-management-sales/production-orders`);
};
const handleCancel = () => {
const _handleCancel = () => {
setCancelReason("");
setCancelDetail("");
setIsCancelDialogOpen(true);
@@ -432,7 +429,7 @@ export default function OrderDetailPage() {
};
// 수주 삭제
const handleDelete = () => {
const _handleDelete = () => {
setIsDeleteDialogOpen(true);
};

View File

@@ -54,7 +54,7 @@ import type { Process } from "@/types/process";
import { formatAmount } from "@/lib/utils/amount";
// 수주 정보 타입
interface OrderInfo {
interface _OrderInfo {
orderNumber: string;
client: string;
siteName: string;
@@ -76,7 +76,7 @@ interface PriorityConfig {
}
// 작업지시 카드 타입
interface WorkOrderCard {
interface _WorkOrderCard {
id: string;
type: string;
orderNumber: string;
@@ -86,7 +86,7 @@ interface WorkOrderCard {
}
// 자재 소요량 타입
interface MaterialRequirement {
interface _MaterialRequirement {
materialCode: string;
materialName: string;
unit: string;
@@ -109,7 +109,7 @@ interface ScreenItemDetail {
}
// 가이드레일 BOM 타입
interface GuideRailBom {
interface _GuideRailBom {
type: string;
spec: string;
code: string;
@@ -118,14 +118,14 @@ interface GuideRailBom {
}
// 케이스 BOM 타입
interface CaseBom {
interface _CaseBom {
item: string;
length: string;
quantity: number;
}
// 하단 마감재 BOM 타입
interface BottomFinishBom {
interface _BottomFinishBom {
item: string;
spec: string;
length: string;

View File

@@ -100,7 +100,7 @@ function CreateOrderContent() {
} else {
toast.error(result.error || "수주 등록에 실패했습니다.");
}
} catch (error) {
} catch (_error) {
toast.error("수주 등록 중 오류가 발생했습니다.");
}
};
@@ -231,14 +231,14 @@ function OrderListContent() {
});
// 페이지네이션
const totalPages = Math.ceil(filteredOrders.length / itemsPerPage);
const _totalPages = Math.ceil(filteredOrders.length / itemsPerPage);
const paginatedOrders = filteredOrders.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
// 모바일용 인피니티 스크롤 데이터
const mobileOrders = filteredOrders.slice(0, mobileDisplayCount);
const _mobileOrders = filteredOrders.slice(0, mobileDisplayCount);
// Intersection Observer를 이용한 인피니티 스크롤
useEffect(() => {
@@ -367,7 +367,7 @@ function OrderListContent() {
// 다중 선택 삭제 (IntegratedListTemplateV2에서 확인 후 호출됨)
// 템플릿 내부에서 이미 확인 팝업을 처리하므로 바로 삭제 실행
const handleBulkDelete = async () => {
const _handleBulkDelete = async () => {
const selectedIds = Array.from(selectedItems);
if (selectedIds.length > 0) {
setIsDeleting(true);
@@ -532,20 +532,20 @@ function OrderListContent() {
// 테이블 컬럼 정의 (16개: 체크박스, 번호, 로트번호, 현장명, 출고예정일, 접수일, 수주처, 제품명, 수신자, 수신주소, 수신처, 배송, 담당자, 틀수, 상태, 비고)
const tableColumns: TableColumn[] = useMemo(() => [
{ key: "rowNumber", label: "번호", className: "px-2 text-center" },
{ key: "lotNumber", label: "로트번호", className: "px-2", sortable: true },
{ key: "siteName", label: "현장명", className: "px-2", sortable: true },
{ key: "expectedShipDate", label: "출고예정일", className: "px-2", sortable: true },
{ key: "orderDate", label: "수주일", className: "px-2", sortable: true },
{ key: "client", label: "수주처", className: "px-2", sortable: true },
{ key: "productName", label: "제품명", className: "px-2", sortable: true },
{ key: "receiver", label: "수신자", className: "px-2", sortable: true },
{ key: "receiverAddress", label: "수신주소", className: "px-2", sortable: true },
{ key: "receiverPlace", label: "수신처", className: "px-2", sortable: true },
{ key: "deliveryMethod", label: "배송", className: "px-2", sortable: true },
{ key: "manager", label: "담당자", className: "px-2", sortable: true },
{ key: "frameCount", label: "틀수", className: "px-2 text-center", sortable: true },
{ key: "lotNumber", label: "로트번호", className: "px-2", sortable: true, copyable: true },
{ key: "siteName", label: "현장명", className: "px-2", sortable: true, copyable: true },
{ key: "expectedShipDate", label: "출고예정일", className: "px-2", sortable: true, copyable: true },
{ key: "orderDate", label: "수주일", className: "px-2", sortable: true, copyable: true },
{ key: "client", label: "수주처", className: "px-2", sortable: true, copyable: true },
{ key: "productName", label: "제품명", className: "px-2", sortable: true, copyable: true },
{ key: "receiver", label: "수신자", className: "px-2", sortable: true, copyable: true },
{ key: "receiverAddress", label: "수신주소", className: "px-2", sortable: true, copyable: true },
{ key: "receiverPlace", label: "수신처", className: "px-2", sortable: true, copyable: true },
{ key: "deliveryMethod", label: "배송", className: "px-2", sortable: true, copyable: true },
{ key: "manager", label: "담당자", className: "px-2", sortable: true, copyable: true },
{ key: "frameCount", label: "틀수", className: "px-2 text-center", sortable: true, copyable: true },
{ key: "status", label: "상태", className: "px-2", sortable: true },
{ key: "remarks", label: "비고", className: "px-2" },
{ key: "remarks", label: "비고", className: "px-2", copyable: true },
], []);
// 테이블 행 렌더링 (16개 컬럼: 체크박스, 번호, 로트번호, 현장명, 출고예정일, 접수일, 수주처, 제품명, 수신자, 수신주소, 수신처, 배송, 담당자, 틀수, 상태, 비고)

View File

@@ -54,7 +54,6 @@ import type {
ProductionOrderDetail,
ProductionStatus,
ProductionWorkOrder,
BomProcessGroup,
} from "@/components/production/ProductionOrders/types";
// 공정 진행 현황 컴포넌트

View File

@@ -161,14 +161,14 @@ function getStatusBadge(status: ProductionStatus) {
// 테이블 컬럼 정의
const TABLE_COLUMNS: TableColumn[] = [
{ key: "no", label: "번호", className: "w-[60px] text-center" },
{ key: "orderNumber", label: "수주번호", className: "min-w-[150px]" },
{ key: "siteName", label: "현장명", className: "min-w-[180px]" },
{ key: "clientName", label: "거래처", className: "min-w-[120px]" },
{ key: "nodeCount", label: "개소", className: "w-[80px] text-center" },
{ key: "deliveryDate", label: "납기", className: "w-[110px]" },
{ key: "productionOrderedAt", label: "생산지시일", className: "w-[110px]" },
{ key: "orderNumber", label: "수주번호", className: "min-w-[150px]", copyable: true },
{ key: "siteName", label: "현장명", className: "min-w-[180px]", copyable: true },
{ key: "clientName", label: "거래처", className: "min-w-[120px]", copyable: true },
{ key: "nodeCount", label: "개소", className: "w-[80px] text-center", copyable: true },
{ key: "deliveryDate", label: "납기", className: "w-[110px]", copyable: true },
{ key: "productionOrderedAt", label: "생산지시일", className: "w-[110px]", copyable: true },
{ key: "status", label: "상태", className: "w-[100px]" },
{ key: "workOrderCount", label: "작업지시", className: "w-[80px] text-center" },
{ key: "workOrderCount", label: "작업지시", className: "w-[80px] text-center", copyable: true },
{ key: "actions", label: "작업", className: "w-[100px] text-center" },
];

View File

@@ -24,8 +24,8 @@ interface PricingDetailPageProps {
export default function PricingDetailPage({ params }: PricingDetailPageProps) {
const { id } = use(params);
const router = useRouter();
const searchParams = useSearchParams();
const _router = useRouter();
const _searchParams = useSearchParams();
const mode: 'create' | 'edit' = 'edit';
const [data, setData] = useState<PricingData | null>(null);
const [isLoading, setIsLoading] = useState(true);

View File

@@ -74,7 +74,7 @@ export default function QuoteDetailPage() {
if (calcResult.success && calcResult.data?.items) {
// 재계산 결과를 locations에 적용
const updatedLocations = v2Data.locations.map((loc, index) => {
const updatedLocations = v2Data.locations.map((loc, _index) => {
// productCode가 있고 bomResult가 없는 경우에만 업데이트
if (!loc.bomResult && loc.productCode) {
const calcItem = calcResult.data?.items.find(
@@ -89,6 +89,7 @@ export default function QuoteDetailPage() {
v2Data.locations = updatedLocations;
} else {
// no BOM result to merge
}
}

View File

@@ -192,12 +192,29 @@ async function proxyRequest(
} else {
const responseData = await backendResponse.text();
clientResponse = new NextResponse(responseData, {
status: backendResponse.status,
headers: {
'Content-Type': responseContentType,
},
});
// 백엔드가 HTML 에러 페이지를 반환한 경우 (404/500 등)
// HTML을 그대로 전달하면 클라이언트 response.json()에서 SyntaxError 발생
// → 안전한 JSON 에러 응답으로 변환
if (!backendResponse.ok && !responseData.trimStart().startsWith('{') && !responseData.trimStart().startsWith('[')) {
const status = backendResponse.status;
clientResponse = NextResponse.json(
{
success: false,
message: status === 404
? '요청한 API를 찾을 수 없습니다.'
: `서버 오류가 발생했습니다. (${status})`,
error: { code: status },
},
{ status }
);
} else {
clientResponse = new NextResponse(responseData, {
status: backendResponse.status,
headers: {
'Content-Type': responseContentType,
},
});
}
}
// 8. 토큰이 갱신되었으면 새 쿠키 설정