From 81affdc441b88d32a1482309f35ffd5f4c0611ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Wed, 11 Mar 2026 10:27:10 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20ESLint=20=EC=A0=95=EB=A6=AC=20=EB=B0=8F?= =?UTF-8?q?=20=EC=A0=84=EC=B2=B4=20=EC=BD=94=EB=93=9C=20=ED=92=88=EC=A7=88?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - eslint.config.mjs 규칙 강화 및 정리 - 전역 unused import/변수 제거 (312개 파일) - next.config.ts, middleware, proxy route 개선 - CopyableCell molecule 추가 - 회계/결재/HR/생산/건설/품질/영업 등 전 도메인 lint 정리 - IntegratedListTemplateV2, DataTable, MobileCard 등 공통 컴포넌트 개선 - execute-server-action 에러 핸들링 보강 --- ...IX-2026-03-10] eslint-cleanup-checklist.md | 54 ++ ...03-11] dynamic-multi-tenant-page-system.md | 671 ++++++++++++++++++ eslint.config.mjs | 34 +- next.config.ts | 3 + .../(protected)/board/[boardCode]/page.tsx | 10 +- .../boards/[boardCode]/[postId]/page.tsx | 2 +- .../(protected)/boards/[boardCode]/page.tsx | 10 +- .../order/base-info/categories/page.tsx | 2 + .../order/order-management/[id]/page.tsx | 2 +- .../project/contract/[id]/page.tsx | 2 +- .../contract/handover-report/[id]/page.tsx | 2 +- .../(protected)/dev/editable-table/page.tsx | 1 - .../(protected)/hr/attendance/page.tsx | 1 + .../(protected)/hr/documents/new/page.tsx | 14 +- .../employee-management/csv-upload/page.tsx | 2 +- .../hr/employee-management/page.tsx | 2 +- .../qms/components/AuditSettingsPanel.tsx | 1 - .../qms/components/Day1ChecklistPanel.tsx | 2 +- .../qms/components/InspectionModal.tsx | 4 +- .../documents/ImportInspectionDocument.tsx | 6 +- .../documents/QualityDocumentUploader.tsx | 2 +- .../client-management-sales-admin/page.tsx | 29 +- .../order-management-sales/[id]/page.tsx | 9 +- .../[id]/production-order/page.tsx | 12 +- .../sales/order-management-sales/page.tsx | 34 +- .../production-orders/[id]/page.tsx | 1 - .../production-orders/page.tsx | 14 +- .../sales/pricing-management/[id]/page.tsx | 4 +- .../sales/quote-management/[id]/page.tsx | 3 +- src/app/api/proxy/[...path]/route.ts | 29 +- .../BadDebtCollection/BadDebtDetail.tsx | 2 +- .../BadDebtDetailClientV2.tsx | 3 +- .../accounting/BadDebtCollection/actions.ts | 2 +- .../accounting/BadDebtCollection/index.tsx | 8 +- .../BankTransactionInquiry/index.tsx | 20 +- .../BillManagement/BillManagementClient.tsx | 15 +- .../accounting/BillManagement/index.tsx | 14 +- .../CardTransactionInquiry/actions.ts | 2 +- .../CardTransactionInquiry/index.tsx | 24 +- .../accounting/DailyReport/actions.ts | 2 - .../accounting/DepositManagement/index.tsx | 18 +- .../ExpectedExpenseManagement/index.tsx | 29 +- .../ManualJournalEntryModal.tsx | 2 +- .../accounting/GeneralJournalEntry/index.tsx | 18 +- .../GiftCertificateManagement/index.tsx | 14 +- .../PurchaseManagement/PurchaseDetail.tsx | 14 +- .../accounting/PurchaseManagement/index.tsx | 18 +- .../accounting/ReceivablesStatus/actions.ts | 2 +- .../accounting/ReceivablesStatus/index.tsx | 5 +- .../SalesManagement/SalesDetail.tsx | 4 +- .../accounting/SalesManagement/index.tsx | 16 +- .../accounting/SalesManagement/types.ts | 2 +- .../accounting/TaxInvoiceIssuance/index.tsx | 1 - .../JournalEntryModal.tsx | 4 +- .../accounting/TaxInvoiceManagement/index.tsx | 24 +- .../accounting/VendorLedger/index.tsx | 12 +- .../VendorManagement/VendorDetailClient.tsx | 1 - .../VendorManagementClient.tsx | 18 +- .../accounting/VendorManagement/index.tsx | 12 +- .../accounting/WithdrawalManagement/index.tsx | 16 +- src/components/approval/ApprovalBox/index.tsx | 28 +- .../approval/DocumentCreate/ProposalForm.tsx | 2 +- .../approval/DocumentCreate/actions.ts | 6 - .../DocumentDetail/DocumentDetailModalV2.tsx | 1 - .../approval/DocumentDetail/index.tsx | 27 - src/components/approval/DraftBox/index.tsx | 16 +- .../approval/ReferenceBox/index.tsx | 19 +- src/components/atoms/TabChip.tsx | 2 +- src/components/auth/LoginPage.tsx | 4 +- src/components/auth/SignupPage.tsx | 4 +- src/components/board/BoardDetail/index.tsx | 1 - src/components/board/BoardForm/index.tsx | 7 +- .../board/BoardList/BoardListUnified.tsx | 13 +- src/components/board/BoardList/index.tsx | 10 +- .../BoardManagement/BoardDetailClientV2.tsx | 3 +- .../board/BoardManagement/BoardForm.tsx | 2 +- .../board/BoardManagement/index.tsx | 14 +- src/components/board/CommentSection/index.tsx | 2 +- .../business/CEODashboard/CEODashboard.tsx | 8 +- .../business/CEODashboard/components.tsx | 8 - .../modals/DetailModalSections.tsx | 1 - .../CEODashboard/sections/CalendarSection.tsx | 4 +- .../sections/EnhancedSections.tsx | 4 - .../sections/PurchaseStatusSection.tsx | 2 +- .../sections/TodayIssueSection.tsx | 7 +- .../ConstructionMainDashboard.tsx | 3 - .../bidding/BiddingListClient.tsx | 20 +- .../contract/ContractDetailForm.tsx | 2 +- .../contract/ContractListClient.tsx | 18 +- .../estimates/EstimateListClient.tsx | 16 +- .../construction/estimates/actions.ts | 6 +- .../sections/EstimateDetailTableSection.tsx | 2 +- .../sections/PriceAdjustmentSection.tsx | 2 +- .../HandoverReportDetailForm.tsx | 2 +- .../HandoverReportListClient.tsx | 16 +- .../construction/handover-report/actions.ts | 2 +- .../issue-management/IssueDetailForm.tsx | 2 +- .../IssueManagementListClient.tsx | 26 +- .../construction/issue-management/actions.ts | 41 +- .../item-management/ItemManagementClient.tsx | 2 +- .../construction/item-management/constants.ts | 2 +- .../LaborManagementClient.tsx | 12 +- .../management/ConstructionDetailClient.tsx | 2 +- .../ConstructionManagementListClient.tsx | 16 +- .../management/ProjectDetailClient.tsx | 2 +- .../management/ProjectKanbanBoard.tsx | 3 +- .../management/ProjectListClient.tsx | 11 +- .../construction/management/StageCard.tsx | 2 +- .../construction/management/actions.ts | 38 +- .../OrderManagementListClient.tsx | 35 +- .../OrderManagementUnified.tsx | 32 +- .../cards/ConstructionDetailCard.tsx | 1 - .../hooks/useOrderDetailForm.ts | 7 +- .../tables/OrderDetailItemTable.tsx | 4 +- .../partners/PartnerListClient.tsx | 14 +- .../ProgressBillingManagementListClient.tsx | 16 +- .../construction/progress-billing/actions.ts | 2 +- .../hooks/useProgressBillingDetailForm.ts | 1 - .../site-briefings/SiteBriefingForm.tsx | 2 +- .../site-briefings/SiteBriefingListClient.tsx | 14 +- .../SiteManagementListClient.tsx | 8 +- .../StructureReviewDetailForm.tsx | 1 - .../StructureReviewListClient.tsx | 14 +- .../UtilityManagementListClient.tsx | 18 +- .../worker-status/WorkerStatusListClient.tsx | 20 +- .../construction/worker-status/actions.ts | 26 +- .../checklist-management/ChecklistForm.tsx | 2 +- .../ChecklistListClient.tsx | 8 +- .../checklist-management/actions.ts | 14 +- .../clients/ClientDetailClientV2.tsx | 2 +- src/components/clients/ClientRegistration.tsx | 2 +- .../common/EditableTable/EditableTable.tsx | 2 +- .../NoticePopupModal/NoticePopupContainer.tsx | 1 - src/components/common/ParentMenuRedirect.tsx | 2 +- .../common/ScheduleCalendar/ScheduleBar.tsx | 2 +- .../common/ScheduleCalendar/WeekView.tsx | 3 +- .../EventManagement/EventList.tsx | 12 +- .../customer-center/FAQManagement/FAQList.tsx | 4 +- .../InquiryManagement/InquiryList.tsx | 7 +- .../NoticeManagement/NoticeList.tsx | 10 +- src/components/dev/DevToolbar.tsx | 2 +- .../dev/generators/accountingData.ts | 11 +- src/components/dev/generators/quoteData.ts | 2 - .../document-system/viewer/DocumentViewer.tsx | 2 - .../AttendanceInfoDialog.tsx | 2 +- .../AttendanceManagement/ReasonInfoDialog.tsx | 1 - .../hr/AttendanceManagement/index.tsx | 37 +- .../hr/CalendarManagement/index.tsx | 14 +- src/components/hr/CardManagement/index.tsx | 12 +- .../hr/DepartmentManagement/index.tsx | 2 +- .../hr/EmployeeManagement/EmployeeForm.tsx | 8 +- .../hr/EmployeeManagement/EmployeeToolbar.tsx | 2 +- .../hr/EmployeeManagement/actions.ts | 2 +- .../hr/EmployeeManagement/index.tsx | 54 +- src/components/hr/EmployeeManagement/utils.ts | 1 - .../SalaryRegistrationDialog.tsx | 2 +- src/components/hr/SalaryManagement/index.tsx | 31 +- .../VacationGrantDialog.tsx | 1 - .../VacationRegisterDialog.tsx | 1 - .../VacationRequestDialog.tsx | 1 - .../hr/VacationManagement/actions.ts | 2 +- .../hr/VacationManagement/index.tsx | 56 +- .../DynamicItemForm/fields/ComputedField.tsx | 2 +- .../DynamicItemForm/fields/CurrencyField.tsx | 4 +- .../DynamicItemForm/fields/NumberField.tsx | 1 - src/components/items/ItemDetailClient.tsx | 2 +- src/components/items/ItemDetailView.tsx | 2 - .../items/ItemForm/BendingDiagramSection.tsx | 2 +- .../items/ItemForm/forms/ProductForm.tsx | 14 +- src/components/items/ItemListClient.tsx | 38 +- .../items/ItemMasterDataManagement.tsx | 1 - .../components/DraggableField.tsx | 4 +- .../components/DraggableSection.tsx | 2 +- .../components/ItemMasterDialogs.tsx | 5 +- .../dialogs/ColumnDialog.tsx | 2 +- .../dialogs/FieldDialog.tsx | 2 +- .../dialogs/LoadTemplateDialog.tsx | 2 +- .../dialogs/MasterFieldDialog.tsx | 2 +- .../hooks/useAttributeManagement.ts | 3 +- .../hooks/useDeleteManagement.ts | 6 +- .../hooks/useInitialDataLoading.ts | 2 +- .../hooks/useMasterFieldManagement.ts | 2 +- .../hooks/usePageManagement.ts | 6 +- .../hooks/useSectionManagement.ts | 3 +- .../hooks/useTabManagement.ts | 2 +- .../hooks/useTemplateManagement.ts | 16 +- .../services/masterFieldService.ts | 2 +- .../tabs/HierarchyTab/index.tsx | 2 +- .../tabs/MasterFieldTab/index.tsx | 8 +- .../tabs/SectionsTab.tsx | 8 +- .../ImportInspectionInputModal.tsx | 3 +- .../ReceivingManagement/ReceivingDetail.tsx | 2 +- .../ReceivingManagement/ReceivingList.tsx | 24 +- .../material/ReceivingManagement/actions.ts | 2 +- .../material/StockStatus/StockAuditModal.tsx | 2 +- .../StockStatus/StockStatusDetail.tsx | 2 +- .../material/StockStatus/StockStatusList.tsx | 16 +- src/components/molecules/CopyableCell.tsx | 61 ++ .../molecules/DateRangeSelector.tsx | 2 +- src/components/molecules/FormField.tsx | 7 +- src/components/molecules/MobileFilter.tsx | 2 +- src/components/molecules/StandardDialog.tsx | 2 +- src/components/molecules/StatusBadge.tsx | 3 +- src/components/molecules/index.ts | 4 +- src/components/orders/OrderRegistration.tsx | 3 +- .../orders/OrderSalesDetailEdit.tsx | 10 +- .../orders/OrderSalesDetailView.tsx | 7 +- src/components/orders/actions.ts | 2 +- .../orders/documents/ContractDocument.tsx | 4 +- .../orders/documents/SalesOrderDocument.tsx | 2 +- .../orders/documents/TransactionDocument.tsx | 8 +- src/components/organisms/DataTable.tsx | 17 +- src/components/organisms/MobileCard.tsx | 7 +- src/components/organisms/SearchFilter.tsx | 4 +- .../ShipmentManagement/ShipmentDetail.tsx | 2 +- .../ShipmentManagement/ShipmentList.tsx | 20 +- .../documents/TransactionStatement.tsx | 4 +- .../VehicleDispatchList.tsx | 22 +- .../PriceDistributionDetail.tsx | 2 +- .../PriceDistributionList.tsx | 10 +- .../PricingTableListClient.tsx | 24 +- src/components/pricing/PricingFormClient.tsx | 3 +- .../pricing/PricingHistoryDialog.tsx | 2 +- src/components/pricing/PricingListClient.tsx | 22 +- src/components/pricing/actions.ts | 2 +- src/components/pricing/types.ts | 1 - .../ProcessDetailClientV2.tsx | 4 +- .../process-management/ProcessForm.tsx | 8 +- .../process-management/ProcessListClient.tsx | 14 +- .../ProcessWorkLogContent.tsx | 2 +- .../process-management/RuleModal.tsx | 6 +- .../process-management/StepForm.tsx | 2 +- src/components/process-management/actions.ts | 2 +- .../production/ProductionDashboard/index.tsx | 4 +- .../production/ProductionOrders/actions.ts | 1 - .../WorkOrders/WipProductionModal.tsx | 2 +- .../production/WorkOrders/WorkOrderCreate.tsx | 2 +- .../production/WorkOrders/WorkOrderEdit.tsx | 2 +- .../production/WorkOrders/WorkOrderList.tsx | 22 +- .../documents/BendingWorkLogContent.tsx | 2 +- .../documents/SlatWorkLogContent.tsx | 2 +- .../documents/TemplateInspectionContent.tsx | 6 +- .../documents/bending/GuideRailSection.tsx | 2 +- .../WorkOrders/documents/bending/utils.ts | 6 +- .../documents/inspection-shared.tsx | 2 +- .../production/WorkResults/WorkResultList.tsx | 26 +- .../WorkerScreen/InspectionInputModal.tsx | 3 +- .../WorkerScreen/ProcessDetailSection.tsx | 1 - .../production/WorkerScreen/WorkItemCard.tsx | 4 +- .../production/WorkerScreen/index.tsx | 18 +- .../InspectionManagement/InspectionCreate.tsx | 4 +- .../InspectionManagement/InspectionDetail.tsx | 7 +- .../InspectionManagement/InspectionList.tsx | 21 +- .../ProductInspectionInputModal.tsx | 2 +- .../documents/FqcRequestDocumentContent.tsx | 2 +- .../PerformanceReportList.tsx | 28 +- src/components/quotes/DiscountModal.tsx | 1 - src/components/quotes/FormulaViewModal.tsx | 2 +- src/components/quotes/LocationDetailPanel.tsx | 10 +- src/components/quotes/LocationListPanel.tsx | 2 +- src/components/quotes/QuoteFooterBar.tsx | 2 +- .../quotes/QuoteManagementClient.tsx | 24 +- src/components/quotes/QuoteRegistration.tsx | 18 +- src/components/quotes/actions.ts | 4 +- src/components/quotes/types.ts | 4 +- .../reports/ComprehensiveAnalysis/index.tsx | 3 +- .../settings/AccountManagement/index.tsx | 18 +- .../AddCompanyDialog.tsx | 3 +- .../settings/CompanyInfoManagement/index.tsx | 2 +- .../settings/LeavePolicyManagement/index.tsx | 2 - .../PaymentHistoryClient.tsx | 20 +- .../PaymentHistoryManagement/index.tsx | 24 +- .../PermissionDetailClient.tsx | 2 +- .../settings/PermissionManagement/index.tsx | 10 +- .../settings/PopupManagement/PopupList.tsx | 10 +- .../SubscriptionClient.tsx | 2 +- .../settings/WorkScheduleManagement/index.tsx | 1 - .../IntegratedDetailTemplate/FieldInput.tsx | 1 - .../IntegratedDetailTemplate/types.ts | 4 +- .../templates/IntegratedListTemplateV2.tsx | 67 +- .../templates/UniversalListPage/index.tsx | 4 +- .../templates/UniversalListPage/types.ts | 4 +- src/components/ui/confirm-dialog.tsx | 3 +- src/components/ui/currency-input.tsx | 2 +- src/components/ui/loading-spinner.tsx | 2 +- src/components/ui/number-input.tsx | 4 +- src/components/ui/quantity-input.tsx | 2 +- src/components/ui/skeleton.tsx | 2 +- .../vehicle-management/ForkliftList/index.tsx | 24 +- .../VehicleDetail/index.tsx | 1 - .../vehicle-management/VehicleList/actions.ts | 2 +- .../vehicle-management/VehicleList/index.tsx | 24 +- .../VehicleLogDetail/index.tsx | 1 - .../VehicleLogList/index.tsx | 8 +- src/contexts/ApiErrorContext.tsx | 2 - src/contexts/ItemMasterContext.tsx | 7 - src/hooks/useAccountingListPage.ts | 2 +- src/hooks/useCEODashboard.ts | 2 +- src/hooks/useDashboardFetch.ts | 2 +- src/hooks/useStatsLoader.ts | 2 +- src/layouts/AuthenticatedLayout.tsx | 9 +- src/lib/api/execute-server-action.ts | 17 +- src/lib/api/item-master.ts | 5 - src/lib/api/quote.ts | 1 - src/lib/api/refresh-token.ts | 2 +- src/lib/auth/logout.ts | 8 +- src/lib/capacitor/fcm.ts | 2 +- src/lib/print-utils.ts | 2 +- src/lib/utils/excel-download.ts | 4 - src/lib/utils/locale.ts | 2 +- src/lib/utils/validation/form-schemas.ts | 4 +- src/lib/utils/validation/item-schemas.ts | 1 - src/middleware.ts | 2 +- src/stores/item-master/useItemMasterStore.ts | 3 - src/stores/utils/userStorage.ts | 2 +- 315 files changed, 1977 insertions(+), 1344 deletions(-) create mode 100644 claudedocs/[FIX-2026-03-10] eslint-cleanup-checklist.md create mode 100644 claudedocs/architecture/[PLAN-2026-03-11] dynamic-multi-tenant-page-system.md create mode 100644 src/components/molecules/CopyableCell.tsx diff --git a/claudedocs/[FIX-2026-03-10] eslint-cleanup-checklist.md b/claudedocs/[FIX-2026-03-10] eslint-cleanup-checklist.md new file mode 100644 index 00000000..b647ecaa --- /dev/null +++ b/claudedocs/[FIX-2026-03-10] eslint-cleanup-checklist.md @@ -0,0 +1,54 @@ +# ESLint 코드 정리 체크리스트 + +## 점검 결과 요약 +- **TypeScript**: 0건 (완벽) +- **ESLint**: 923 errors + 220 warnings (1,529개 파일 중 399개) + +## 수정 대상 (exhaustive-deps 제외 - 동작 변경 위험) + +### ✅ 완료 + +| 룰 | 건수 | 상태 | 수정 내용 | +|---|---|---|---| +| `no-unreachable` | 7 | ✅ 완료 | 도달 불가 catch 블록 제거 (construction actions 3파일) | +| `no-constant-binary-expression` | 6 | ✅ 완료 | `false && ...` 조건 제거 (MasterFieldTab, SectionsTab) | +| `no-useless-escape` | 6 | ✅ 완료 | 불필요한 `\` 제거 (CurrencyField, currency-input, number-input, locale.ts) | +| `no-case-declarations` | 21 | ✅ 완료 | switch case에 `{}` 블록 추가 (5파일) | + +### ⏳ 미완료 + +| 룰 | 건수 | 상태 | 수정 방법 | +|---|---|---|---| +| `no-unused-vars` | 707 | ⏳ 대기 | `eslint-plugin-unused-imports` 자동 수정 예정 | + +## unused-vars 수정 계획 + +### 준비 상태 +- `eslint-plugin-unused-imports` 이미 설치됨 (npm install -D 완료) +- eslint.config.mjs 아직 미수정 + +### 실행 순서 +```bash +# 1. eslint.config.mjs에 플러그인 임시 추가 +# 2. npx eslint --fix src/ (unused-imports 룰만) +# 3. eslint.config.mjs 원복 +# 4. npx eslint src/ 로 결과 확인 +# 5. eslint-plugin-unused-imports 패키지 제거 +``` + +### unused-vars 파일 분포 (284개 파일) +- src/app/: 44파일 +- src/components/business/: 33파일 +- src/components/accounting+hr/: 42파일 +- src/components/items+orders+quotes+production/: 55파일 +- src/components/ 기타: 95파일 +- src/lib+stores+types/: 15파일 + +## 수정하지 않는 항목 + +| 룰 | 건수 | 사유 | +|---|---|---| +| `no-explicit-any` | 155 | warning 수준, 타입 정의 필요 (별도 작업) | +| `exhaustive-deps` | 24 | useEffect 재실행 빈도 변경 위험 | +| `no-img-element` | 39 | next/image 전환은 별도 작업 | +| `no-undef` | 168 | globals 설정 추가 필요 (sessionStorage 등) | diff --git a/claudedocs/architecture/[PLAN-2026-03-11] dynamic-multi-tenant-page-system.md b/claudedocs/architecture/[PLAN-2026-03-11] dynamic-multi-tenant-page-system.md new file mode 100644 index 00000000..bdb8e3a3 --- /dev/null +++ b/claudedocs/architecture/[PLAN-2026-03-11] dynamic-multi-tenant-page-system.md @@ -0,0 +1,671 @@ +# 동적 멀티테넌트 페이지 시스템 설계 + +> 작성일: 2026-03-11 +> 상태: 초안 (백엔드 논의 필요) +> 관련 문서: +> - `[VISION-2026-02-19] dynamic-rendering-platform-strategy.md` +> - `[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md` +> - `[DESIGN-2026-02-11] dynamic-field-type-extension.md` + +--- + +## 1. 핵심 목표 + +``` +현재: 테넌트(업종)별 페이지를 하드코딩 → 신규 테넌트마다 개발 필요 +목표: 백엔드 기준관리에서 설정 → JSON API → 프론트 동적 렌더링 +결과: 프론트엔드 코드 변경 0줄로 새 테넌트 대응 +``` + +--- + +## 2. 전체 아키텍처 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 백엔드 어드민 (mng) │ +│ ┌───────────────────────────────────────────────────┐ │ +│ │ 기준관리 페이지 │ │ +│ │ 레이아웃 / 섹션 / 항목 / 속성 등록 │ │ +│ └───────────────┬───────────────────────────────────┘ │ +│ │ 저장 │ +│ ↓ │ +│ ┌───────────────────────────────────────────────────┐ │ +│ │ DB (테넌트별 페이지 config) │ │ +│ └───────────────┬───────────────────────────────────┘ │ +│ │ API │ +└──────────────────┼──────────────────────────────────────┘ + ↓ +┌──────────────────────────────────────────────────────────┐ +│ 프론트엔드 (Next.js) │ +│ │ +│ ┌────────────┐ ┌─────────────────────────────────┐ │ +│ │ 정적 페이지 │ │ 동적 페이지 │ │ +│ │ - 로그인 │ │ - catch-all route │ │ +│ │ - 회원가입 │ │ - JSON config → 동적 렌더링 │ │ +│ │ - 404 등 │ │ - pageType별 렌더러 선택 │ │ +│ └────────────┘ └─────────────────────────────────┘ │ +│ │ │ │ +│ └──── 공유 컴포넌트 ───────┘ │ +│ (ui/, molecules/, organisms/) │ +└──────────────────────────────────────────────────────────┘ +``` + +--- + +## 3. 규칙 정의 + +### 규칙 1: 기준관리 → 백엔드 어드민 + +| 항목 | 내용 | +|------|------| +| 현재 | 프론트 `ItemMasterDataManagement` 등에서 기준관리 | +| 변경 | 백엔드 어드민(mng) 페이지로 이동 | +| 이유 | 프론트 번들 크기 감소, 설정 변경 = 배포 불필요 | +| 담당 | 🔵 백엔드 | + +``` +Before: 프론트 기준관리 UI → 프론트 API 호출 → DB 저장 +After: 백엔드 어드민 UI → 직접 DB 저장 → API로 config 전달 +``` + +--- + +### 규칙 2: 페이지 정보를 JSON API로 제공 + +| 항목 | 내용 | +|------|------| +| 방식 | 메뉴 API처럼 페이지 config도 JSON API로 제공 | +| 엔드포인트 | `GET /api/v1/page-configs/{slug}` (제안) | +| 응답 | 페이지 타입, 레이아웃, 섹션, 필드, 검증규칙, API 매핑 등 | +| 담당 | 🔵 백엔드 API 설계 | + +**페이지 config JSON 구조 (제안)**: + +```jsonc +{ + "pageId": "sales-order-list", + "pageType": "list", // list | detail | form | dashboard | document + "title": "수주 관리", + "slug": "sales/order-management", + + // --- 규칙 11: API 엔드포인트 매핑 --- + "api": { + "list": "/api/v1/orders", + "detail": "/api/v1/orders/:id", + "create": "/api/v1/orders", + "update": "/api/v1/orders/:id", + "delete": "/api/v1/orders/:id" + }, + + // --- 규칙 4: 레이아웃 > 섹션 > 항목 > 속성 --- + "layout": { + "sections": [ + { + "sectionId": "filters", + "sectionType": "filter", + "fields": [ + { + "fieldId": "status", + "type": "select", + "label": "상태", + "options": [ + { "value": "all", "label": "전체" }, + { "value": "pending", "label": "대기" }, + { "value": "confirmed", "label": "확정" } + ], + "defaultValue": "all" + }, + { + "fieldId": "dateRange", + "type": "dateRange", + "label": "기간" + } + ] + }, + { + "sectionId": "table", + "sectionType": "dataTable", + "columns": [ + { "key": "orderNo", "label": "수주번호", "width": 120 }, + { "key": "clientName", "label": "거래처명", "width": 150 }, + { "key": "amount", "label": "금액", "type": "currency", "align": "right" }, + { "key": "status", "label": "상태", "type": "badge" } + ], + "actions": ["view", "edit", "delete"], + "pagination": true + } + ] + }, + + // --- 규칙 12: 검증 규칙 --- + "validation": { + "quantity": { "required": true, "min": 1, "message": "1 이상 입력하세요" }, + "clientId": { "required": true, "message": "거래처를 선택하세요" } + }, + + // --- 규칙 13: 필드 간 의존성 --- + "dependencies": [ + { + "type": "visibility", + "when": { "field": "itemType", "equals": "motor" }, + "show": ["motorSpec", "voltage"] + }, + { + "type": "computed", + "target": "amount", + "formula": "quantity * unitPrice" + }, + { + "type": "cascade", + "source": "category1", + "target": "category2", + "api": "/api/v1/categories/:parentId/children" + } + ], + + // --- 규칙 14: 권한 --- + "permissions": { + "fieldLevel": { + "unitPrice": { "view": ["admin", "sales_manager"], "edit": ["admin"] } + }, + "actionLevel": { + "delete": ["admin"], + "export": ["admin", "sales_manager"] + } + } +} +``` + +> ⚠️ **백엔드 논의 필요**: JSON 구조의 세부 스펙 확정 + +--- + +### 규칙 3: 정적 페이지 vs 동적 페이지 분류 + +| 분류 | 정적 페이지 | 동적 페이지 | +|------|------------|------------| +| 정의 | 테넌트 무관, 고정 UI | 테넌트 config 기반 동적 생성 | +| 예시 | 로그인, 회원가입, 404, 500 | 수주관리, 품목관리, 공정관리 등 | +| 라우팅 | 기존 파일 기반 라우트 | catch-all `[...slug]` | +| 컴포넌트 | 직접 코딩 | JSON → 동적 렌더러 | +| 변경 빈도 | 거의 없음 | 테넌트별/설정별 수시 변경 | + +**정적 페이지 목록 (확정)**: + +| 경로 | 페이지 | 이유 | +|------|--------|------| +| `/login` | 로그인 | 인증 전 접근, 공통 UI | +| `/signup` | 회원가입 | 인증 전 접근, 공통 UI | +| `/404` | Not Found | 에러 페이지 | +| `/500` | Server Error | 에러 페이지 | +| `/settings/*` | 설정 | 시스템 설정은 공통 | + +> ⚠️ **논의 필요**: 설정 페이지 중 일부(구독, 결제)도 동적 대상인지? +> ⚠️ **논의 필요**: 대시보드는 동적 페이지? 위젯 기반 별도 시스템? + +--- + +### 규칙 4: 계층 구조 — 레이아웃 > 섹션 > 항목 > 속성 + +``` +Page (pageType에 의해 렌더러 결정) + └─ Layout (전체 레이아웃: single-column, two-column, tabs 등) + └─ Section (논리적 그룹: 기본정보, 상세정보, 테이블 등) + └─ Field (개별 입력 항목: input, select, date 등) + └─ Attribute (필드의 속성: label, placeholder, validation 등) +``` + +| 계층 | 역할 | 기준관리 등록 항목 | 프론트 컴포넌트 | +|------|------|-------------------|----------------| +| Layout | 전체 배치 | 레이아웃 타입 선택 | `DynamicPageLayout` | +| Section | 논리적 그룹 | 섹션 추가/순서/조건부 표시 | `DynamicSection` | +| Field | 개별 항목 | 필드 타입/라벨/기본값 | `DynamicFieldRenderer` (14종) | +| Attribute | 필드 속성 | 검증규칙/옵션/의존성 | props로 전달 | + +--- + +### 규칙 5: 컴포넌트 책임 분리 + +``` +┌─────────────────────────────────────────────────┐ +│ 상위: 데이터 처리 컴포넌트 (Layout, Section) │ +│ - API 호출 / 데이터 가공 │ +│ - 조건부 표시 로직 │ +│ - props 전달 / 이벤트 핸들링 │ +│ - Zustand store 구독 │ +└──────────────────┬──────────────────────────────┘ + │ props (순수 데이터) + ↓ +┌─────────────────────────────────────────────────┐ +│ 하위: 순수 기능 컴포넌트 (Field, Attribute) │ +│ - UI 렌더링만 담당 │ +│ - 외부 의존성 없음 │ +│ - value + onChange 패턴 │ +│ - 테스트 용이 │ +└─────────────────────────────────────────────────┘ +``` + +| 구분 | 상위 (Layout/Section) | 하위 (Field/Attribute) | +|------|----------------------|----------------------| +| 역할 | 데이터 처리, 조건 분기 | 순수 렌더링 | +| 상태 | Zustand 구독 | props only | +| API | 호출 가능 | 호출 안 함 | +| 예시 | `DynamicSection`, `DynamicListPage` | `Input`, `Select`, `DatePicker` | +| 테스트 | 통합 테스트 | 단위 테스트 | + +--- + +### 규칙 6: Zustand 기반 상태 관리 + +``` +┌────────────────────────────────────────────────┐ +│ pageConfigStore (Zustand) │ +│ │ +│ state: │ +│ configs: Map │ +│ currentPage: PageConfig | null │ +│ loading: boolean │ +│ │ +│ actions: │ +│ fetchPageConfig(slug) → API 호출 + 캐시 │ +│ invalidateConfig(slug) → 캐시 무효화 │ +│ subscribeToPage(slug) → 실시간 구독 │ +└────────────────────────────────────────────────┘ + │ + │ 구독 + ↓ +┌────────────────┐ ┌────────────────┐ +│ DynamicListPage │ │ DynamicFormPage │ ... +└────────────────┘ └────────────────┘ +``` + +| 항목 | 설명 | +|------|------| +| Store 위치 | `src/stores/pageConfigStore.ts` (신규) | +| 캐시 전략 | 메모리(Zustand) → localStorage → API | +| 변경 감지 | 해시 비교 (메뉴 갱신과 동일 방식) | +| 테넌트 격리 | 기존 `TenantAwareCache` 패턴 재사용 | + +--- + +### 규칙 7: 테넌트 + 하위 구성요소별 화면 분기 + +``` +테넌트 A (셔터 제조업) + ├─ 메뉴: 품목관리, 생산관리, 출하관리 + ├─ 품목 폼: 셔터 규격 필드 포함 + └─ 생산 공정: 셔터 전용 공정 단계 + +테넌트 B (건설업) + ├─ 메뉴: 프로젝트관리, 공사관리, 기성관리 + ├─ 프로젝트 폼: 현장정보 필드 포함 + └─ 공사 공정: 건설 전용 단계 + +같은 테넌트 내에서도: + ├─ 부서 A → 메뉴 5개, 필드 20개 표시 + └─ 부서 B → 메뉴 3개, 필드 12개 표시 +``` + +| 분기 기준 | 설명 | 예시 | +|----------|------|------| +| 테넌트 (company) | 업종별 전체 화면 구성 | 셔터업 vs 건설업 | +| 부서 (department) | 같은 테넌트 내 부서별 | 영업팀 vs 생산팀 | +| 역할 (role) | 같은 부서 내 역할별 | 관리자 vs 일반 | +| 사용자 (user) | 개인 설정 | 즐겨찾기, 컬럼 순서 | + +> ⚠️ **백엔드 논의 필요**: 분기 우선순위 및 상속 정책 +> (테넌트 설정 → 부서 설정으로 오버라이드 → 사용자 설정으로 오버라이드?) + +--- + +### 규칙 8: 정적/동적 컴포넌트 공유 + +``` +src/components/ + ├── ui/ ← 공유 (정적+동적 모두 사용) + │ ├── Input.tsx + │ ├── Select.tsx + │ ├── DatePicker.tsx + │ └── ... + │ + ├── molecules/ ← 공유 + │ ├── FormField.tsx + │ ├── SearchFilter.tsx + │ └── ... + │ + ├── organisms/ ← 공유 + │ ├── DataTable.tsx + │ ├── MobileCard.tsx + │ └── ... + │ + ├── dynamic/ ← 동적 전용 (신규) + │ ├── renderers/ + │ │ ├── DynamicListPage.tsx + │ │ ├── DynamicDetailPage.tsx + │ │ ├── DynamicFormPage.tsx + │ │ └── DynamicDashboardPage.tsx + │ ├── sections/ + │ │ ├── DynamicSection.tsx + │ │ ├── DynamicFilterSection.tsx + │ │ └── DynamicTableSection.tsx ← 기존 이동 + │ ├── fields/ + │ │ └── DynamicFieldRenderer.tsx ← 기존 이동 (14종) + │ └── store/ + │ └── pageConfigStore.ts + │ + └── static/ ← 정적 전용 (기존 유지) + ├── auth/LoginPage.tsx + └── auth/SignupPage.tsx +``` + +| 레이어 | 공유 여부 | 예시 | +|--------|----------|------| +| ui/ | ✅ 100% 공유 | Input, Select, Button | +| molecules/ | ✅ 100% 공유 | FormField, StatusBadge | +| organisms/ | ✅ 대부분 공유 | DataTable, SearchFilter | +| dynamic/renderers/ | ❌ 동적 전용 | DynamicListPage | +| 기존 도메인 컴포넌트 | ❌ 정적 전용 (점진적 전환) | OrderSalesDetailEdit | + +--- + +### 규칙 9: 페이지 타입 분류 체계 + +| pageType | 용도 | 핵심 구성 요소 | 기존 대응 패턴 | +|----------|------|--------------|---------------| +| `list` | 목록 조회 | 필터 + 테이블 + 페이지네이션 + 액션 | UniversalListPage | +| `detail` | 상세 보기 | 읽기전용 섹션 + 수정/삭제 버튼 | IntegratedDetailTemplate | +| `form` | 등록/수정 | 입력 섹션 + 저장/취소 | DynamicItemForm (범용화) | +| `dashboard` | 대시보드 | 위젯/카드 그리드 | CEODashboard | +| `document` | 문서/프린트 | 프린트 레이아웃 + 결재란 | ContractDocument 등 | + +``` +pageType 결정 흐름: + +API 응답의 pageType 값 + │ + ├─ "list" → + ├─ "detail" → + ├─ "form" → + ├─ "dashboard" → + ├─ "document" → + └─ 미지원 → (에러 표시) +``` + +--- + +### 규칙 10: 동적 라우팅 전략 + +``` +src/app/[locale]/(protected)/ + │ + ├── (static-pages)/ ← 정적 페이지 그룹 + │ ├── settings/ + │ └── ... + │ + └── [...slug]/ ← 동적 페이지 catch-all + └── page.tsx ← 아래 로직 수행 +``` + +**catch-all page.tsx 동작 흐름**: + +``` +1. URL에서 slug 추출 (예: ["sales", "order-management"]) +2. slug로 pageConfigStore에서 config 조회 (캐시 우선) +3. 캐시 없으면 → API 호출: GET /api/v1/page-configs/sales/order-management +4. config.pageType으로 렌더러 선택 +5. 렌더러에 config 전달 → 동적 페이지 렌더링 +``` + +| 라우트 우선순위 | 경로 | 설명 | +|---------------|------|------| +| 1 (최우선) | `/login`, `/signup` | 정적 페이지 (파일 존재) | +| 2 | `/settings/*` | 정적 그룹 (파일 존재) | +| 3 (폴백) | `/*` (나머지 전부) | catch-all → 동적 처리 | + +> Next.js 라우팅 규칙: 구체적 경로 > catch-all → 충돌 없음 + +> ⚠️ **논의 필요**: 기존 정적 페이지를 동적으로 전환 시, 해당 파일 삭제 후 catch-all로 자연스럽게 이관 + +--- + +### 규칙 11: API 엔드포인트 동적 매핑 + +| API 유형 | config 키 | 용도 | +|---------|----------|------| +| `list` | `api.list` | 목록 조회 (GET) | +| `detail` | `api.detail` | 상세 조회 (GET) | +| `create` | `api.create` | 등록 (POST) | +| `update` | `api.update` | 수정 (PUT/PATCH) | +| `delete` | `api.delete` | 삭제 (DELETE) | +| `export` | `api.export` | 엑셀 다운로드 (GET) | +| `custom` | `api.custom[actionName]` | 커스텀 액션 | + +``` +동적 페이지의 데이터 흐름: + +config.api.list = "/api/v1/orders" + ↓ +DynamicListPage → Server Action (buildApiUrl 사용) + ↓ +API 프록시 → 백엔드 → 응답 + ↓ +테이블 렌더링 (config.columns 기준) +``` + +> ⚠️ **백엔드 논의 필요**: API 응답 구조 통일 (list는 `{ data: [], meta: { total, page } }` 등) + +--- + +### 규칙 12: 검증(Validation) 규칙 + +| 검증 타입 | JSON 표현 | 프론트 변환 | +|----------|----------|------------| +| 필수값 | `{ "required": true }` | `z.string().min(1)` | +| 최솟값 | `{ "min": 1 }` | `z.number().min(1)` | +| 최댓값 | `{ "max": 100 }` | `z.number().max(100)` | +| 정규식 | `{ "pattern": "^\\d{3}-\\d{2}$" }` | `z.string().regex()` | +| 커스텀 메시지 | `{ "message": "올바른 형식이 아닙니다" }` | 에러 메시지 | +| 이메일 | `{ "type": "email" }` | `z.string().email()` | +| 전화번호 | `{ "type": "phone" }` | `z.string().regex()` | + +``` +JSON validation config + ↓ 런타임 변환 +Zod 스키마 자동 생성 + ↓ +react-hook-form zodResolver에 주입 + ↓ +폼 검증 자동 적용 +``` + +--- + +### 규칙 13: 필드 간 의존성 + +| 의존성 타입 | 설명 | 예시 | +|------------|------|------| +| `visibility` | 조건부 표시/숨김 | 품목타입=모터 → 전압 필드 표시 | +| `computed` | 자동 계산 | 수량 × 단가 = 금액 | +| `cascade` | 연쇄 선택 | 대분류 → 중분류 → 소분류 | +| `setValue` | 값 자동 설정 | 거래처 선택 → 담당자 자동 입력 | +| `disable` | 조건부 비활성화 | 상태=확정 → 수량 수정 불가 | + +``` +기존 자산 활용: +DynamicItemForm의 DisplayCondition → visibility 타입으로 범용화 +DynamicItemForm의 ComputedField → computed 타입으로 범용화 +``` + +> ⚠️ **백엔드 논의 필요**: 복잡한 계산식(견적 할인율 등)은 백엔드 API 위임 vs 프론트 formula + +--- + +### 규칙 14: 권한 통합 + +| 권한 레벨 | 적용 대상 | 동작 | +|----------|----------|------| +| 페이지 | 메뉴/라우트 | 메뉴에 없으면 접근 불가 (기존 방식 유지) | +| 액션 | 버튼/기능 | 삭제, 엑셀 다운로드 등 특정 액션 | +| 필드 | 개별 입력 항목 | 보기/수정 권한 분리 | + +``` +권한 적용 흐름: + +1. 페이지 접근: 메뉴 API에 없으면 → 접근 차단 (기존) +2. 액션 권한: config.permissions.actionLevel → 버튼 표시/숨김 +3. 필드 권한: config.permissions.fieldLevel → 읽기전용/숨김 처리 +``` + +> ⚠️ **백엔드 논의 필요**: 권한 정보를 페이지 config에 포함 vs 별도 권한 API + +--- + +### 규칙 15: 캐싱 & 성능 전략 + +``` +요청 흐름: + +1차 캐시 (Zustand 메모리) + ↓ miss +2차 캐시 (localStorage, 테넌트별 격리) + ↓ miss +3차 (API 호출) + ↓ 응답 +1차 + 2차 캐시 갱신 +``` + +| 전략 | 방법 | 갱신 주기 | +|------|------|----------| +| 초기 로드 | 로그인 시 전체 config 프리페치 | 1회 | +| 변경 감지 | 해시 비교 (메뉴 갱신과 동일) | 30초~5분 | +| 강제 갱신 | 관리자가 기준관리 변경 시 push | 즉시 | +| 캐시 무효화 | 테넌트 전환 시 전체 클리어 | 즉시 | + +--- + +### 규칙 16: 비즈니스 로직 처리 (하이브리드) + +| 로직 복잡도 | 처리 방식 | 예시 | +|------------|----------|------| +| 단순 계산 | config formula (프론트) | 수량 × 단가 = 금액 | +| 중간 로직 | 등록된 로직 블록 (프론트) | 세금 계산, 할인율 적용 | +| 복잡한 로직 | 백엔드 API 위임 | 견적 복합 계산, 재고 검증 | + +```jsonc +// config에서 로직 블록 지정 +{ + "businessLogic": { + // 단순: 프론트 formula + "amount": { "type": "formula", "expression": "quantity * unitPrice" }, + + // 중간: 프론트 등록 블록 + "tax": { "type": "block", "blockId": "taxCalculation" }, + + // 복잡: 백엔드 위임 + "totalDiscount": { + "type": "api", + "endpoint": "/api/v1/quotes/:id/calculate-discount", + "trigger": "onFieldChange", + "watchFields": ["quantity", "unitPrice", "discountRate"] + } + } +} +``` + +**프론트 로직 블록 레지스트리**: + +``` +src/components/dynamic/logic-blocks/ + ├── taxCalculation.ts ← 세금 계산 + ├── currencyConversion.ts ← 환율 변환 + ├── stockValidation.ts ← 재고 검증 (API 호출) + └── registry.ts ← 블록 등록/조회 +``` + +> 새 블록이 필요할 때만 개발자 개입 → 등록 후 config에서 선택 사용 + +--- + +### 규칙 17: 점진적 마이그레이션 전략 + +| Phase | 범위 | 예상 기간 | 상태 | +|-------|------|----------|------| +| **Phase 0** | 인프라 구축 | 2-3주 | ⏳ 준비 | +| | - catch-all 라우터 | | | +| | - pageConfigStore | | | +| | - DynamicListPage/FormPage 렌더러 | | | +| | - 백엔드 page-config API | | | +| **Phase 1** | 신규 테넌트/페이지만 동적 | 2-4주 | ⏳ | +| | - 새로 추가되는 페이지는 동적으로 생성 | | | +| | - 기존 페이지는 그대로 유지 | | | +| **Phase 2** | 단순 CRUD 페이지 전환 | 4-6주 | ⏳ | +| | - 리스트+상세만 있는 단순 페이지 | | | +| | - 거래처관리, 설비관리 등 | | | +| **Phase 3** | 복잡한 비즈니스 페이지 전환 | 6-8주 | ⏳ | +| | - 견적, 수주, 생산 등 로직 있는 페이지 | | | +| | - 로직 블록 구축 병행 | | | +| **Phase 4** | 기존 정적 → 동적 완전 전환 | 지속적 | ⏳ | +| | - 남은 하드코딩 페이지 점진적 전환 | | | + +``` +전환 판단 기준: + +[쉬움] 순수 CRUD (리스트+폼) → Phase 2에서 전환 +[보통] CRUD + 단순 계산 → Phase 2~3 +[어려움] 복잡한 비즈니스 로직 → Phase 3 +[마지막] 문서/프린트, 대시보드 → Phase 4 +``` + +--- + +## 4. 이미 있는 자산 → 재사용 매핑 + +| 기존 자산 | 현재 용도 | 동적 시스템에서의 역할 | +|----------|----------|---------------------| +| DynamicFieldRenderer (14종) | 품목 폼 필드 | → 모든 동적 폼 필드 | +| DynamicTableSection | 품목 BOM 테이블 | → 모든 동적 테이블 | +| DisplayCondition | 품목 조건부 표시 | → 범용 visibility 규칙 | +| ComputedField | 품목 자동 계산 | → 범용 computed 규칙 | +| UniversalListPage | 리스트 페이지 템플릿 | → DynamicListPage 기반 | +| IntegratedDetailTemplate | 상세 페이지 템플릿 | → DynamicDetailPage 기반 | +| TenantAwareCache | 캐시 격리 | → pageConfigStore 캐시 | +| menuRefresh (해시 비교) | 메뉴 갱신 | → config 변경 감지 | +| buildApiUrl | URL 빌더 | → 동적 API 호출에 재사용 | + +--- + +## 5. 백엔드 논의 체크리스트 + +백엔드와 확정해야 할 사항: + +- [ ] **page-config API 스펙**: `GET /api/v1/page-configs/{slug}` 응답 구조 +- [ ] **기준관리 어드민 UI**: mng에서 레이아웃/섹션/필드 등록 화면 설계 +- [ ] **JSON config 구조**: 위 제안 구조 검토 및 수정 +- [ ] **API 응답 통일**: list/detail/create/update/delete 응답 포맷 표준화 +- [ ] **권한 통합 방식**: config에 포함 vs 별도 API +- [ ] **분기 우선순위**: 테넌트 → 부서 → 역할 → 사용자 오버라이드 정책 +- [ ] **복잡한 비즈니스 로직**: 프론트 formula vs 백엔드 API 위임 기준 +- [ ] **캐시 무효화**: 기준관리 변경 시 프론트 캐시 갱신 방법 (polling vs push) +- [ ] **프리페치 범위**: 로그인 시 전체 config vs 페이지 접근 시 개별 로드 +- [ ] **검증 규칙 표현**: JSON validation 스펙 확정 +- [ ] **의존성 표현**: visibility/computed/cascade 등 JSON 스펙 확정 +- [ ] **마이그레이션 순서**: 어떤 페이지부터 동적 전환할지 + +--- + +## 6. 관련 문서 + +| 문서 | 위치 | 내용 | +|------|------|------| +| 동적 렌더링 플랫폼 비전 | `architecture/[VISION-2026-02-19]` | 전체 비전 및 자산 현황 | +| 멀티테넌시 최적화 로드맵 | `architecture/[PLAN-2026-02-06]` | 테넌트 격리/최적화 8 Phase | +| 동적 필드 타입 설계 | `architecture/[DESIGN-2026-02-11]` | 4-Level 구조, 14종 필드 | +| 동적 필드 구현 현황 | `architecture/[IMPL-2026-02-11]` | Phase 1~3 프론트 구현 완료 | +| 백엔드 API 스펙 | `item-master/[API-REQUEST-2026-02-12]` | 동적 필드 타입 백엔드 요청서 | + +--- + +**문서 버전**: 1.0 (초안) +**마지막 업데이트**: 2026-03-11 +**다음 단계**: 백엔드와 논의 → 체크리스트 항목 확정 → v2.0 작성 diff --git a/eslint.config.mjs b/eslint.config.mjs index 3e772ff2..6e9e06a9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -15,6 +15,7 @@ const eslintConfig = [ "node_modules/**", "next-env.d.ts", "src/components/_unused/**", // Archived unused components + "src/components/settings/AccountManagement/_legacy/**", // Legacy files "src/hooks/useCurrentTime.ts", // Demo hook ], }, @@ -76,9 +77,38 @@ const eslintConfig = [ HTMLTableCaptionElement: "readonly", HTMLTextAreaElement: "readonly", HTMLCanvasElement: "readonly", + HTMLDivElement: "readonly", + HTMLElement: "readonly", + HTMLImageElement: "readonly", ImageData: "readonly", Image: "readonly", prompt: "readonly", + Audio: "readonly", + Blob: "readonly", + CSSStyleDeclaration: "readonly", + CustomEvent: "readonly", + Element: "readonly", + ErrorEvent: "readonly", + Event: "readonly", + FileList: "readonly", + FileReader: "readonly", + Headers: "readonly", + IntersectionObserver: "readonly", + KeyboardEvent: "readonly", + MouseEvent: "readonly", + Node: "readonly", + NodeJS: "readonly", + PromiseRejectionEvent: "readonly", + RequestCache: "readonly", + ResizeObserver: "readonly", + Storage: "readonly", + cancelAnimationFrame: "readonly", + crypto: "readonly", + getComputedStyle: "readonly", + google: "readonly", + navigator: "readonly", + requestAnimationFrame: "readonly", + sessionStorage: "readonly", }, }, plugins: { @@ -95,7 +125,9 @@ const eslintConfig = [ "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_" + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_", + "destructuredArrayIgnorePattern": "^_" }], }, }, diff --git a/next.config.ts b/next.config.ts index c68ac77b..76b70de7 100644 --- a/next.config.ts +++ b/next.config.ts @@ -28,6 +28,9 @@ const nextConfig: NextConfig = { }, // Capacitor 패키지는 모바일 앱 전용 - 웹 빌드에서 제외 webpack: (config, { isServer }) => { + // macOS 26 호환성: webpack 캐시 비활성화 (rename ENOENT 방지) + config.cache = false; + if (!isServer) { config.resolve.fallback = { ...config.resolve.fallback, diff --git a/src/app/[locale]/(protected)/board/[boardCode]/page.tsx b/src/app/[locale]/(protected)/board/[boardCode]/page.tsx index bf9489ab..0762896d 100644 --- a/src/app/[locale]/(protected)/board/[boardCode]/page.tsx +++ b/src/app/[locale]/(protected)/board/[boardCode]/page.tsx @@ -103,7 +103,7 @@ function BoardListContent({ boardCode }: { boardCode: string }) { // 게시글 목록 const [posts, setPosts] = useState([]); - const [isLoading, setIsLoading] = useState(true); + const [, setIsLoading] = useState(true); const [error, setError] = useState(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 }, ], []); // 테이블 행 렌더링 diff --git a/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/page.tsx b/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/page.tsx index e8f9a3b4..ce0044ee 100644 --- a/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/page.tsx +++ b/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/page.tsx @@ -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'; diff --git a/src/app/[locale]/(protected)/boards/[boardCode]/page.tsx b/src/app/[locale]/(protected)/boards/[boardCode]/page.tsx index fa0a3698..35d7c99d 100644 --- a/src/app/[locale]/(protected)/boards/[boardCode]/page.tsx +++ b/src/app/[locale]/(protected)/boards/[boardCode]/page.tsx @@ -110,7 +110,7 @@ function DynamicBoardListContent({ boardCode }: { boardCode: string }) { // 게시글 목록 const [posts, setPosts] = useState([]); - const [isLoading, setIsLoading] = useState(true); + const [, setIsLoading] = useState(true); const [error, setError] = useState(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 }, ], []); // 테이블 행 렌더링 diff --git a/src/app/[locale]/(protected)/construction/order/base-info/categories/page.tsx b/src/app/[locale]/(protected)/construction/order/base-info/categories/page.tsx index 21d41fe4..97878424 100644 --- a/src/app/[locale]/(protected)/construction/order/base-info/categories/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/base-info/categories/page.tsx @@ -1,3 +1,5 @@ +'use client'; + import { CategoryManagement } from '@/components/business/construction/category-management'; export default function CategoriesPage() { diff --git a/src/app/[locale]/(protected)/construction/order/order-management/[id]/page.tsx b/src/app/[locale]/(protected)/construction/order/order-management/[id]/page.tsx index 9673157c..076458f5 100644 --- a/src/app/[locale]/(protected)/construction/order/order-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/order-management/[id]/page.tsx @@ -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>['data']>(undefined); diff --git a/src/app/[locale]/(protected)/construction/project/contract/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/contract/[id]/page.tsx index c25aa623..f809632d 100644 --- a/src/app/[locale]/(protected)/construction/project/contract/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/contract/[id]/page.tsx @@ -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>['data']>(undefined); diff --git a/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/page.tsx index 9288b165..979cc5b4 100644 --- a/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/page.tsx @@ -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>['data']>(undefined); diff --git a/src/app/[locale]/(protected)/dev/editable-table/page.tsx b/src/app/[locale]/(protected)/dev/editable-table/page.tsx index e445b695..25b1208a 100644 --- a/src/app/[locale]/(protected)/dev/editable-table/page.tsx +++ b/src/app/[locale]/(protected)/dev/editable-table/page.tsx @@ -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 { diff --git a/src/app/[locale]/(protected)/hr/attendance/page.tsx b/src/app/[locale]/(protected)/hr/attendance/page.tsx index 23d7a935..e141c0a2 100644 --- a/src/app/[locale]/(protected)/hr/attendance/page.tsx +++ b/src/app/[locale]/(protected)/hr/attendance/page.tsx @@ -75,6 +75,7 @@ export default function AttendancePage() { setSiteLocation(finalLocation); } else { + // no fallback location needed } } catch (error) { console.error('[AttendancePage] loadSettings error:', error); diff --git a/src/app/[locale]/(protected)/hr/documents/new/page.tsx b/src/app/[locale]/(protected)/hr/documents/new/page.tsx index 6447f230..f38300d5 100644 --- a/src/app/[locale]/(protected)/hr/documents/new/page.tsx +++ b/src/app/[locale]/(protected)/hr/documents/new/page.tsx @@ -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'; // 문서 유형 라벨 diff --git a/src/app/[locale]/(protected)/hr/employee-management/csv-upload/page.tsx b/src/app/[locale]/(protected)/hr/employee-management/csv-upload/page.tsx index dcac80c4..02a3c8d1 100644 --- a/src/app/[locale]/(protected)/hr/employee-management/csv-upload/page.tsx +++ b/src/app/[locale]/(protected)/hr/employee-management/csv-upload/page.tsx @@ -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 연동 }; diff --git a/src/app/[locale]/(protected)/hr/employee-management/page.tsx b/src/app/[locale]/(protected)/hr/employee-management/page.tsx index 8677750a..b46ccf5e 100644 --- a/src/app/[locale]/(protected)/hr/employee-management/page.tsx +++ b/src/app/[locale]/(protected)/hr/employee-management/page.tsx @@ -50,7 +50,7 @@ function EmployeeManagementContent() { toast.error(errorMessage); return { success: false, error: errorMessage }; } - } catch (error) { + } catch (_error) { toast.error('서버 오류가 발생했습니다.'); return { success: false, error: '서버 오류가 발생했습니다.' }; } diff --git a/src/app/[locale]/(protected)/quality/qms/components/AuditSettingsPanel.tsx b/src/app/[locale]/(protected)/quality/qms/components/AuditSettingsPanel.tsx index 544080de..d5667fc7 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/AuditSettingsPanel.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/AuditSettingsPanel.tsx @@ -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 { diff --git a/src/app/[locale]/(protected)/quality/qms/components/Day1ChecklistPanel.tsx b/src/app/[locale]/(protected)/quality/qms/components/Day1ChecklistPanel.tsx index 6c4f5500..fe16573f 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/Day1ChecklistPanel.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/Day1ChecklistPanel.tsx @@ -123,7 +123,7 @@ export function Day1ChecklistPanel({ 검색 결과가 없습니다 ) : ( - filteredCategories.map((category, categoryIndex) => { + filteredCategories.map((category, _categoryIndex) => { const isExpanded = expandedCategories.has(category.id); const progress = getCategoryProgress(category); const allCompleted = progress.completed === progress.total; diff --git a/src/app/[locale]/(protected)/quality/qms/components/InspectionModal.tsx b/src/app/[locale]/(protected)/quality/qms/components/InspectionModal.tsx index df6569ee..1cc805bf 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/InspectionModal.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/InspectionModal.tsx @@ -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 = () => { diff --git a/src/app/[locale]/(protected)/quality/qms/components/documents/ImportInspectionDocument.tsx b/src/app/[locale]/(protected)/quality/qms/components/documents/ImportInspectionDocument.tsx index 25ac8dea..d685cce2 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/documents/ImportInspectionDocument.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/documents/ImportInspectionDocument.tsx @@ -457,7 +457,7 @@ export const ImportInspectionDocument = forwardRef { + const _handleResultChange = useCallback((itemId: string, result: JudgmentResult) => { if (readOnly) return; setValues((prev) => { @@ -773,8 +773,8 @@ export const ImportInspectionDocument = forwardRef - {inspectionItems.map((item, idx) => { - const itemValue = values[item.id]; + {inspectionItems.map((item, _idx) => { + const _itemValue = values[item.id]; // 그룹핑 정보 const hasCategory = !!item.subName; diff --git a/src/app/[locale]/(protected)/quality/qms/components/documents/QualityDocumentUploader.tsx b/src/app/[locale]/(protected)/quality/qms/components/documents/QualityDocumentUploader.tsx index 415daf98..4d422a63 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/documents/QualityDocumentUploader.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/documents/QualityDocumentUploader.tsx @@ -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 { diff --git a/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx b/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx index b7c008fc..e969c882 100644 --- a/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx +++ b/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx @@ -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>(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; diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx index 2a470ab4..51f28df9 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx @@ -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); }; diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx index d9276708..c32ba1bf 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx @@ -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; diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx index 49a3f351..2a38594f 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx @@ -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개 컬럼: 체크박스, 번호, 로트번호, 현장명, 출고예정일, 접수일, 수주처, 제품명, 수신자, 수신주소, 수신처, 배송, 담당자, 틀수, 상태, 비고) diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx index e7f000d8..c400eaf3 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx @@ -54,7 +54,6 @@ import type { ProductionOrderDetail, ProductionStatus, ProductionWorkOrder, - BomProcessGroup, } from "@/components/production/ProductionOrders/types"; // 공정 진행 현황 컴포넌트 diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx index cf874358..f80ae94f 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx @@ -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" }, ]; diff --git a/src/app/[locale]/(protected)/sales/pricing-management/[id]/page.tsx b/src/app/[locale]/(protected)/sales/pricing-management/[id]/page.tsx index 1f5d1537..ce36c30b 100644 --- a/src/app/[locale]/(protected)/sales/pricing-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/sales/pricing-management/[id]/page.tsx @@ -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(null); const [isLoading, setIsLoading] = useState(true); diff --git a/src/app/[locale]/(protected)/sales/quote-management/[id]/page.tsx b/src/app/[locale]/(protected)/sales/quote-management/[id]/page.tsx index e94ee20b..96696769 100644 --- a/src/app/[locale]/(protected)/sales/quote-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/sales/quote-management/[id]/page.tsx @@ -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 } } diff --git a/src/app/api/proxy/[...path]/route.ts b/src/app/api/proxy/[...path]/route.ts index 24fb7aae..c03dd477 100644 --- a/src/app/api/proxy/[...path]/route.ts +++ b/src/app/api/proxy/[...path]/route.ts @@ -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. 토큰이 갱신되었으면 새 쿠키 설정 diff --git a/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx b/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx index 9f451a88..f1fe1639 100644 --- a/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx +++ b/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx @@ -271,7 +271,7 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp }, [router, formData.vendorId]); // 파일 다운로드 핸들러 - const handleFileDownload = useCallback((fileName: string) => { + const handleFileDownload = useCallback((_fileName: string) => { // TODO: 실제 다운로드 로직 }, []); diff --git a/src/components/accounting/BadDebtCollection/BadDebtDetailClientV2.tsx b/src/components/accounting/BadDebtCollection/BadDebtDetailClientV2.tsx index a237fd3f..cdfee579 100644 --- a/src/components/accounting/BadDebtCollection/BadDebtDetailClientV2.tsx +++ b/src/components/accounting/BadDebtCollection/BadDebtDetailClientV2.tsx @@ -8,7 +8,7 @@ */ import { useState, useEffect } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; +import { useSearchParams } from 'next/navigation'; import { BadDebtDetail } from './BadDebtDetail'; import { getBadDebtById } from './actions'; import type { BadDebtRecord } from './types'; @@ -26,7 +26,6 @@ interface BadDebtDetailClientV2Props { const BASE_PATH = '/ko/accounting/bad-debt-collection'; export function BadDebtDetailClientV2({ recordId, initialMode }: BadDebtDetailClientV2Props) { - const router = useRouter(); const searchParams = useSearchParams(); // URL 쿼리에서 모드 결정 diff --git a/src/components/accounting/BadDebtCollection/actions.ts b/src/components/accounting/BadDebtCollection/actions.ts index 3dd07fe6..48938915 100644 --- a/src/components/accounting/BadDebtCollection/actions.ts +++ b/src/components/accounting/BadDebtCollection/actions.ts @@ -18,7 +18,7 @@ import { revalidatePath } from 'next/cache'; import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action'; import { buildApiUrl } from '@/lib/api/query-params'; import type { PaginatedApiResponse } from '@/lib/api/types'; -import type { BadDebtRecord, BadDebtItem, CollectionStatus } from './types'; +import type { BadDebtRecord, CollectionStatus } from './types'; // ===== API 응답 타입 ===== diff --git a/src/components/accounting/BadDebtCollection/index.tsx b/src/components/accounting/BadDebtCollection/index.tsx index b3a2750a..643c5aec 100644 --- a/src/components/accounting/BadDebtCollection/index.tsx +++ b/src/components/accounting/BadDebtCollection/index.tsx @@ -51,10 +51,10 @@ import { deleteBadDebt, toggleBadDebt } from './actions'; // ===== 테이블 컬럼 정의 ===== const tableColumns = [ { key: 'no', label: 'No.', className: 'text-center w-[60px]' }, - { key: 'vendorName', label: '거래처', className: 'w-[100px]', sortable: true }, - { key: 'debtAmount', label: '채권금액', className: 'text-right w-[140px]', sortable: true }, - { key: 'occurrenceDate', label: '발생일', className: 'text-center w-[110px]', sortable: true }, - { key: 'overdueDays', label: '연체일수', className: 'text-center w-[100px]', sortable: true }, + { key: 'vendorName', label: '거래처', className: 'w-[100px]', sortable: true, copyable: true }, + { key: 'debtAmount', label: '채권금액', className: 'text-right w-[140px]', sortable: true, copyable: true }, + { key: 'occurrenceDate', label: '발생일', className: 'text-center w-[110px]', sortable: true, copyable: true }, + { key: 'overdueDays', label: '연체일수', className: 'text-center w-[100px]', sortable: true, copyable: true }, { key: 'managerName', label: '담당자', className: 'w-[100px]', sortable: true }, { key: 'status', label: '상태', className: 'text-center w-[100px]', sortable: true }, { key: 'setting', label: '설정', className: 'text-center w-[80px]' }, diff --git a/src/components/accounting/BankTransactionInquiry/index.tsx b/src/components/accounting/BankTransactionInquiry/index.tsx index 10eddf10..e70c8ee5 100644 --- a/src/components/accounting/BankTransactionInquiry/index.tsx +++ b/src/components/accounting/BankTransactionInquiry/index.tsx @@ -76,15 +76,15 @@ const excelColumns: ExcelColumn>[] = [ // ===== 테이블 컬럼 정의 (체크박스 제외 10개) ===== const tableColumns = [ { key: 'rowNumber', label: 'No.', className: 'text-center w-[50px]' }, - { key: 'transactionDate', label: '거래일시', sortable: true }, - { key: 'type', label: '구분', className: 'text-center', sortable: true }, - { key: 'accountInfo', label: '계좌정보', sortable: true }, - { key: 'note', label: '적요/내용', sortable: true }, - { key: 'depositAmount', label: '입금', className: 'text-right', sortable: true }, - { key: 'withdrawalAmount', label: '출금', className: 'text-right', sortable: true }, - { key: 'balance', label: '잔액', className: 'text-right', sortable: true }, - { key: 'branch', label: '취급점', className: 'text-center', sortable: true }, - { key: 'depositorName', label: '상대계좌예금주명', sortable: true }, + { key: 'transactionDate', label: '거래일시', sortable: true, copyable: true }, + { key: 'type', label: '구분', className: 'text-center', sortable: true, copyable: true }, + { key: 'accountInfo', label: '계좌정보', sortable: true, copyable: true }, + { key: 'note', label: '적요/내용', sortable: true, copyable: true }, + { key: 'depositAmount', label: '입금', className: 'text-right', sortable: true, copyable: true }, + { key: 'withdrawalAmount', label: '출금', className: 'text-right', sortable: true, copyable: true }, + { key: 'balance', label: '잔액', className: 'text-right', sortable: true, copyable: true }, + { key: 'branch', label: '취급점', className: 'text-center', sortable: true, copyable: true }, + { key: 'depositorName', label: '상대계좌예금주명', sortable: true, copyable: true }, ]; // ===== 기본 Summary ===== @@ -112,7 +112,7 @@ export function BankTransactionInquiry() { // 필터 상태 const [searchQuery, setSearchQuery] = useState(''); - const [sortOption, setSortOption] = useState('latest'); + const [sortOption] = useState('latest'); const [accountCategoryFilter, setAccountCategoryFilter] = useState('all'); const [financialInstitutionFilter, setFinancialInstitutionFilter] = useState('all'); const [currentPage, setCurrentPage] = useState(1); diff --git a/src/components/accounting/BillManagement/BillManagementClient.tsx b/src/components/accounting/BillManagement/BillManagementClient.tsx index 58e5f827..be7c05bc 100644 --- a/src/components/accounting/BillManagement/BillManagementClient.tsx +++ b/src/components/accounting/BillManagement/BillManagementClient.tsx @@ -42,7 +42,6 @@ import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; import { toast } from 'sonner'; import type { BillRecord, - BillType, BillStatus, SortOption, } from './types'; @@ -82,7 +81,7 @@ export function BillManagementClient({ const [pagination, setPagination] = useState(initialPagination); const [isLoading, setIsLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(''); - const [sortOption, setSortOption] = useState('latest'); + const [sortOption] = useState('latest'); const [billTypeFilter, setBillTypeFilter] = useState(initialBillType || 'received'); const [vendorFilter, setVendorFilter] = useState(initialVendorId || 'all'); const [statusFilter, setStatusFilter] = useState('all'); @@ -173,13 +172,13 @@ export function BillManagementClient({ // ===== 테이블 컬럼 ===== const tableColumns: TableColumn[] = useMemo(() => [ { key: 'no', label: '번호', className: 'text-center w-[60px]' }, - { key: 'billNumber', label: '어음번호', sortable: true }, + { key: 'billNumber', label: '어음번호', sortable: true, copyable: true }, { key: 'billType', label: '구분', className: 'text-center', sortable: true }, - { key: 'vendorName', label: '거래처', sortable: true }, - { key: 'amount', label: '금액', className: 'text-right', sortable: true }, - { key: 'issueDate', label: '발행일', sortable: true }, - { key: 'maturityDate', label: '만기일', sortable: true }, - { key: 'installmentCount', label: '차수', className: 'text-center', sortable: true }, + { key: 'vendorName', label: '거래처', sortable: true, copyable: true }, + { key: 'amount', label: '금액', className: 'text-right', sortable: true, copyable: true }, + { key: 'issueDate', label: '발행일', sortable: true, copyable: true }, + { key: 'maturityDate', label: '만기일', sortable: true, copyable: true }, + { key: 'installmentCount', label: '차수', className: 'text-center', sortable: true, copyable: true }, { key: 'status', label: '상태', className: 'text-center', sortable: true }, ], []); diff --git a/src/components/accounting/BillManagement/index.tsx b/src/components/accounting/BillManagement/index.tsx index 77b0b86d..34e89ca6 100644 --- a/src/components/accounting/BillManagement/index.tsx +++ b/src/components/accounting/BillManagement/index.tsx @@ -62,13 +62,13 @@ import { // ===== 테이블 컬럼 정의 ===== const tableColumns = [ { key: 'no', label: '번호', className: 'text-center w-[60px]' }, - { key: 'billNumber', label: '어음번호', sortable: true }, + { key: 'billNumber', label: '어음번호', sortable: true, copyable: true }, { key: 'billType', label: '구분', className: 'text-center', sortable: true }, - { key: 'vendorName', label: '거래처', sortable: true }, - { key: 'amount', label: '금액', className: 'text-right', sortable: true }, - { key: 'issueDate', label: '발행일', sortable: true }, - { key: 'maturityDate', label: '만기일', sortable: true }, - { key: 'installmentCount', label: '차수', className: 'text-center', sortable: true }, + { key: 'vendorName', label: '거래처', sortable: true, copyable: true }, + { key: 'amount', label: '금액', className: 'text-right', sortable: true, copyable: true }, + { key: 'issueDate', label: '발행일', sortable: true, copyable: true }, + { key: 'maturityDate', label: '만기일', sortable: true, copyable: true }, + { key: 'installmentCount', label: '차수', className: 'text-center', sortable: true, copyable: true }, { key: 'status', label: '상태', className: 'text-center', sortable: true }, { key: 'actions', label: '작업', className: 'text-center w-[80px]' }, ]; @@ -94,7 +94,7 @@ export function BillManagement({ initialVendorId, initialBillType }: BillManagem const [billTypeFilter, setBillTypeFilter] = useState(initialBillType || 'received'); const [vendorFilter, setVendorFilter] = useState(initialVendorId || 'all'); const [statusFilter, setStatusFilter] = useState('all'); - const [sortOption, setSortOption] = useState('latest'); + const [sortOption, _setSortOption] = useState('latest'); // 페이지네이션 const [currentPage, setCurrentPage] = useState(1); diff --git a/src/components/accounting/CardTransactionInquiry/actions.ts b/src/components/accounting/CardTransactionInquiry/actions.ts index 6dca9d71..9a3dd16c 100644 --- a/src/components/accounting/CardTransactionInquiry/actions.ts +++ b/src/components/accounting/CardTransactionInquiry/actions.ts @@ -48,7 +48,7 @@ interface CardTransactionApiSummary { // ===== API → Frontend 변환 ===== function transformItem(item: CardTransactionApiItem): CardTransaction { const card = item.card; - const cardDisplay = card ? `${card.card_company} ${card.card_number_last4}` : '-'; + const _cardDisplay = card ? `${card.card_company} ${card.card_number_last4}` : '-'; const usedAtRaw = item.used_at || item.withdrawal_date; const usedAtDate = new Date(usedAtRaw); const usedAt = item.used_at ? usedAtDate.toISOString().slice(0, 16).replace('T', ' ') : item.withdrawal_date; diff --git a/src/components/accounting/CardTransactionInquiry/index.tsx b/src/components/accounting/CardTransactionInquiry/index.tsx index e95c7182..53a93980 100644 --- a/src/components/accounting/CardTransactionInquiry/index.tsx +++ b/src/components/accounting/CardTransactionInquiry/index.tsx @@ -83,19 +83,19 @@ const excelColumns: ExcelColumn[] = [ // ===== 테이블 컬럼 정의 (체크박스/No. 제외 15개) ===== const tableColumns = [ { key: 'rowNumber', label: 'No.', className: 'text-center w-[50px]' }, - { key: 'usedAt', label: '사용일시', className: 'min-w-[130px]' }, - { key: 'cardCompany', label: '카드사', className: 'min-w-[80px]' }, - { key: 'card', label: '카드번호', className: 'min-w-[100px]' }, - { key: 'cardName', label: '카드명', className: 'min-w-[80px]' }, + { key: 'usedAt', label: '사용일시', className: 'min-w-[130px]', copyable: true }, + { key: 'cardCompany', label: '카드사', className: 'min-w-[80px]', copyable: true }, + { key: 'card', label: '카드번호', className: 'min-w-[100px]', copyable: true }, + { key: 'cardName', label: '카드명', className: 'min-w-[80px]', copyable: true }, { key: 'deductionType', label: '공제', className: 'min-w-[95px]', sortable: false }, - { key: 'businessNumber', label: '사업자번호', className: 'min-w-[110px]' }, - { key: 'merchantName', label: '가맹점명', className: 'min-w-[100px]' }, - { key: 'vendorName', label: '증빙/판매자상호', className: 'min-w-[160px]', sortable: false }, - { key: 'description', label: '내역', className: 'min-w-[120px]', sortable: false }, - { key: 'totalAmount', label: '합계금액', className: 'min-w-[100px] text-right' }, - { key: 'supplyAmount', label: '공급가액', className: 'min-w-[110px] text-right', sortable: false }, - { key: 'taxAmount', label: '세액', className: 'min-w-[90px] text-right', sortable: false }, - { key: 'accountSubject', label: '계정과목', className: 'min-w-[100px]', sortable: false }, + { key: 'businessNumber', label: '사업자번호', className: 'min-w-[110px]', copyable: true }, + { key: 'merchantName', label: '가맹점명', className: 'min-w-[100px]', copyable: true }, + { key: 'vendorName', label: '증빙/판매자상호', className: 'min-w-[160px]', sortable: false, copyable: true }, + { key: 'description', label: '내역', className: 'min-w-[120px]', sortable: false, copyable: true }, + { key: 'totalAmount', label: '합계금액', className: 'min-w-[100px] text-right', copyable: true }, + { key: 'supplyAmount', label: '공급가액', className: 'min-w-[110px] text-right', sortable: false, copyable: true }, + { key: 'taxAmount', label: '세액', className: 'min-w-[90px] text-right', sortable: false, copyable: true }, + { key: 'accountSubject', label: '계정과목', className: 'min-w-[100px]', sortable: false, copyable: true }, { key: 'journalEntry', label: '분개', className: 'w-16 text-center', sortable: false }, { key: 'hide', label: '숨김', className: 'w-16 text-center', sortable: false }, ]; diff --git a/src/components/accounting/DailyReport/actions.ts b/src/components/accounting/DailyReport/actions.ts index c5d442ad..5eb09c13 100644 --- a/src/components/accounting/DailyReport/actions.ts +++ b/src/components/accounting/DailyReport/actions.ts @@ -1,8 +1,6 @@ 'use server'; -import { isNextRedirectError } from '@/lib/utils/redirect-error'; -import { cookies } from 'next/headers'; import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action'; import { buildApiUrl } from '@/lib/api/query-params'; import type { NoteReceivableItem, DailyAccountItem, MatchStatus } from './types'; diff --git a/src/components/accounting/DepositManagement/index.tsx b/src/components/accounting/DepositManagement/index.tsx index 74a743ca..5360f754 100644 --- a/src/components/accounting/DepositManagement/index.tsx +++ b/src/components/accounting/DepositManagement/index.tsx @@ -20,10 +20,8 @@ import { Plus, Save, RefreshCw, - Search, } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { @@ -84,13 +82,13 @@ import { // ===== 테이블 컬럼 정의 ===== const tableColumns = [ - { key: 'depositDate', label: '입금일', sortable: true }, - { key: 'accountName', label: '입금계좌', sortable: true }, - { key: 'depositorName', label: '입금자명', sortable: true }, - { key: 'depositAmount', label: '입금금액', className: 'text-right', sortable: true }, - { key: 'vendorName', label: '거래처', sortable: true }, - { key: 'note', label: '적요', sortable: true }, - { key: 'depositType', label: '입금유형', className: 'text-center', sortable: true }, + { key: 'depositDate', label: '입금일', sortable: true, copyable: true }, + { key: 'accountName', label: '입금계좌', sortable: true, copyable: true }, + { key: 'depositorName', label: '입금자명', sortable: true, copyable: true }, + { key: 'depositAmount', label: '입금금액', className: 'text-right', sortable: true, copyable: true }, + { key: 'vendorName', label: '거래처', sortable: true, copyable: true }, + { key: 'note', label: '적요', sortable: true, copyable: true }, + { key: 'depositType', label: '입금유형', className: 'text-center', sortable: true, copyable: true }, ]; // ===== 컴포넌트 Props ===== @@ -104,7 +102,7 @@ interface DepositManagementProps { }; } -export function DepositManagement({ initialData, initialPagination }: DepositManagementProps) { +export function DepositManagement({ initialData, initialPagination: _initialPagination }: DepositManagementProps) { const router = useRouter(); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== diff --git a/src/components/accounting/ExpectedExpenseManagement/index.tsx b/src/components/accounting/ExpectedExpenseManagement/index.tsx index 98793138..a3a39d06 100644 --- a/src/components/accounting/ExpectedExpenseManagement/index.tsx +++ b/src/components/accounting/ExpectedExpenseManagement/index.tsx @@ -59,7 +59,7 @@ import { type RowClickHandlers, type StatCard, } from '@/components/templates/UniversalListPage'; -import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; +import { ListMobileCard } from '@/components/organisms/MobileCard'; import type { ExpectedExpenseRecord, TransactionType, @@ -82,7 +82,6 @@ import { getClients, getBankAccounts, } from './actions'; -import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { CurrencyInput } from '@/components/ui/currency-input'; @@ -219,7 +218,7 @@ export function ExpectedExpenseManagement({ }, [resetForm]); // ===== 수정 다이얼로그 열기 ===== - const handleOpenEditDialog = useCallback((item: ExpectedExpenseRecord) => { + const _handleOpenEditDialog = useCallback((item: ExpectedExpenseRecord) => { setEditingItem(item); setFormData({ expectedPaymentDate: item.expectedPaymentDate, @@ -485,7 +484,7 @@ export function ExpectedExpenseManagement({ // ===== 액션 핸들러 ===== // 상세페이지 없음 - 행 클릭 시 이동하지 않음 - const handleDeleteClick = useCallback((id: string) => { + const _handleDeleteClick = useCallback((id: string) => { setDeleteTargetId(id); setShowDeleteDialog(true); }, []); @@ -561,11 +560,11 @@ export function ExpectedExpenseManagement({ // ===== 테이블 컬럼 ===== const tableColumns = useMemo(() => [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'expectedPaymentDate', label: '예상 지급일', sortable: true }, - { key: 'accountSubject', label: '항목', sortable: true }, - { key: 'amount', label: '지출금액', className: 'text-right', sortable: true }, - { key: 'vendorName', label: '거래처', sortable: true }, - { key: 'bankAccount', label: '계좌', sortable: true }, + { key: 'expectedPaymentDate', label: '예상 지급일', sortable: true, copyable: true }, + { key: 'accountSubject', label: '항목', sortable: true, copyable: true }, + { key: 'amount', label: '지출금액', className: 'text-right', sortable: true, copyable: true }, + { key: 'vendorName', label: '거래처', sortable: true, copyable: true }, + { key: 'bankAccount', label: '계좌', sortable: true, copyable: true }, { key: 'approvalStatus', label: '전자결재', className: 'text-center', sortable: true }, ], []); @@ -588,9 +587,9 @@ export function ExpectedExpenseManagement({ // 컬럼 순서: 체크박스 + 번호 + 예상 지급일 + 항목 + 지출금액 + 거래처 + 계좌 + 전자결재 + 작업 const renderTableRow = useCallback(( item: TableRowData, - index: number, - globalIndex: number, - handlers: SelectionHandlers & RowClickHandlers + _index: number, + _globalIndex: number, + _handlers: SelectionHandlers & RowClickHandlers ) => { // 월 헤더 행 (9개 컬럼) if (item.rowType === 'monthHeader') { @@ -700,9 +699,9 @@ export function ExpectedExpenseManagement({ // ===== 모바일 카드 렌더링 ===== const renderMobileCard = useCallback(( item: TableRowData, - index: number, - globalIndex: number, - handlers: SelectionHandlers & RowClickHandlers + _index: number, + _globalIndex: number, + _handlers: SelectionHandlers & RowClickHandlers ) => { const isSelected = selectedItems.has(item.id); const onToggle = () => toggleSelection(item.id); diff --git a/src/components/accounting/GeneralJournalEntry/ManualJournalEntryModal.tsx b/src/components/accounting/GeneralJournalEntry/ManualJournalEntryModal.tsx index 8d284055..1007ca14 100644 --- a/src/components/accounting/GeneralJournalEntry/ManualJournalEntryModal.tsx +++ b/src/components/accounting/GeneralJournalEntry/ManualJournalEntryModal.tsx @@ -44,7 +44,7 @@ import { } from '@/components/ui/table'; import { AccountSubjectSelect } from '@/components/accounting/common'; import { createManualJournal, getVendorList } from './actions'; -import type { JournalEntryRow, JournalSide, VendorOption } from './types'; +import type { JournalEntryRow, VendorOption } from './types'; import { JOURNAL_SIDE_OPTIONS } from './types'; import { getTodayString } from '@/lib/utils/date'; diff --git a/src/components/accounting/GeneralJournalEntry/index.tsx b/src/components/accounting/GeneralJournalEntry/index.tsx index 90483132..a208e8bf 100644 --- a/src/components/accounting/GeneralJournalEntry/index.tsx +++ b/src/components/accounting/GeneralJournalEntry/index.tsx @@ -42,15 +42,15 @@ import { invalidateDashboard } from '@/lib/dashboard-invalidation'; // ===== 테이블 컬럼 (기획서 기준 10개) ===== const tableColumns = [ - { key: 'date', label: '날짜', className: 'text-center w-[100px]' }, - { key: 'description', label: '적요', className: 'w-[140px]' }, - { key: 'depositAmount', label: '입금', className: 'text-right w-[100px]' }, - { key: 'withdrawalAmount', label: '출금', className: 'text-right w-[100px]' }, - { key: 'balance', label: '잔액', className: 'text-right w-[100px]' }, - { key: 'division', label: '구분', className: 'text-center w-[70px]' }, - { key: 'journalDescription', label: '내역', className: 'w-[100px]' }, - { key: 'debitAmount', label: '차변', className: 'text-right w-[90px]' }, - { key: 'creditAmount', label: '대변', className: 'text-right w-[90px]' }, + { key: 'date', label: '날짜', className: 'text-center w-[100px]', copyable: true }, + { key: 'description', label: '적요', className: 'w-[140px]', copyable: true }, + { key: 'depositAmount', label: '입금', className: 'text-right w-[100px]', copyable: true }, + { key: 'withdrawalAmount', label: '출금', className: 'text-right w-[100px]', copyable: true }, + { key: 'balance', label: '잔액', className: 'text-right w-[100px]', copyable: true }, + { key: 'division', label: '구분', className: 'text-center w-[70px]', copyable: true }, + { key: 'journalDescription', label: '내역', className: 'w-[100px]', copyable: true }, + { key: 'debitAmount', label: '차변', className: 'text-right w-[90px]', copyable: true }, + { key: 'creditAmount', label: '대변', className: 'text-right w-[90px]', copyable: true }, { key: 'journalAction', label: '분개', className: 'text-center w-[70px]', sortable: false }, ]; diff --git a/src/components/accounting/GiftCertificateManagement/index.tsx b/src/components/accounting/GiftCertificateManagement/index.tsx index 4c17969e..c9be569c 100644 --- a/src/components/accounting/GiftCertificateManagement/index.tsx +++ b/src/components/accounting/GiftCertificateManagement/index.tsx @@ -55,12 +55,12 @@ import { useDateRange } from '@/hooks'; // ===== 테이블 컬럼 정의 (체크박스/No. 제외) ===== const tableColumns = [ { key: 'rowNumber', label: '번호', className: 'text-center' }, - { key: 'serialNumber', label: '일련번호', sortable: true }, - { key: 'name', label: '상품권명', sortable: true }, - { key: 'faceValue', label: '액면가', className: 'text-right', sortable: true }, - { key: 'purchaseDate', label: '구입일', className: 'text-center', sortable: true }, - { key: 'usedDate', label: '사용일', className: 'text-center', sortable: true }, - { key: 'entertainmentExpense', label: '접대비', className: 'text-center', sortable: true }, + { key: 'serialNumber', label: '일련번호', sortable: true, copyable: true }, + { key: 'name', label: '상품권명', sortable: true, copyable: true }, + { key: 'faceValue', label: '액면가', className: 'text-right', sortable: true, copyable: true }, + { key: 'purchaseDate', label: '구입일', className: 'text-center', sortable: true, copyable: true }, + { key: 'usedDate', label: '사용일', className: 'text-center', sortable: true, copyable: true }, + { key: 'entertainmentExpense', label: '접대비', className: 'text-center', sortable: true, copyable: true }, { key: 'status', label: '상태', className: 'text-center', sortable: true }, ]; @@ -87,7 +87,7 @@ export function GiftCertificateManagement() { // 필터 상태 const [statusFilter, setStatusFilter] = useState('all'); const [entertainmentFilter, setEntertainmentFilter] = useState('all'); - const [searchQuery, setSearchQuery] = useState(''); + const [, setSearchQuery] = useState(''); // 날짜 범위 const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentMonth'); diff --git a/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx b/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx index d60ceec3..897343bd 100644 --- a/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx +++ b/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx @@ -1,14 +1,12 @@ 'use client'; -import { useState, useCallback, useMemo, useEffect } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { format } from 'date-fns'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; -import { Badge } from '@/components/ui/badge'; -import { getPresetStyle } from '@/lib/utils/status-config'; import { Select, SelectContent, @@ -64,7 +62,7 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) { // ===== 로딩 상태 ===== const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); + const [, setIsSaving] = useState(false); // ===== 거래처 목록 ===== const [clients, setClients] = useState([]); @@ -73,20 +71,20 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) { const [purchaseNo, setPurchaseNo] = useState(''); const [purchaseDate, setPurchaseDate] = useState(format(new Date(), 'yyyy-MM-dd')); const [vendorId, setVendorId] = useState(''); - const [vendorName, setVendorName] = useState(''); + const [, setVendorName] = useState(''); // purchaseType 삭제됨 (기획서 P.109) const [items, setItems] = useState([createEmptyItem()]); const [taxInvoiceReceived, setTaxInvoiceReceived] = useState(false); // ===== 문서 관련 상태 (sourceDocument는 API에서 아직 미지원) ===== const [sourceDocument, setSourceDocument] = useState(undefined); - const [withdrawalAccount, setWithdrawalAccount] = useState(undefined); - const [createdAt, setCreatedAt] = useState(''); + const [, setWithdrawalAccount] = useState(undefined); + const [, setCreatedAt] = useState(''); // ===== 문서 열람 모달 상태 ===== const [documentModalOpen, setDocumentModalOpen] = useState(false); const [modalData, setModalData] = useState(null); - const [isModalLoading, setIsModalLoading] = useState(false); + const [, setIsModalLoading] = useState(false); const [approvalId, setApprovalId] = useState(undefined); // ===== 품목 관리 (공통 훅) ===== diff --git a/src/components/accounting/PurchaseManagement/index.tsx b/src/components/accounting/PurchaseManagement/index.tsx index 8f1ecd6e..de97b91d 100644 --- a/src/components/accounting/PurchaseManagement/index.tsx +++ b/src/components/accounting/PurchaseManagement/index.tsx @@ -21,10 +21,8 @@ import { toast } from 'sonner'; import { Receipt, Save, - Search, } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; import { Checkbox } from '@/components/ui/checkbox'; // Badge, getPresetStyle removed (매입유형/연결문서 컬럼 삭제) import { Switch } from '@/components/ui/switch'; @@ -66,12 +64,12 @@ import { formatNumber } from '@/lib/utils/amount'; // ===== 테이블 컬럼 정의 ===== const tableColumns = [ { key: 'no', label: 'No.', className: 'w-[60px] text-center' }, - { key: 'purchaseNo', label: '매입번호', sortable: true }, - { key: 'purchaseDate', label: '매입일', sortable: true }, - { key: 'vendorName', label: '거래처', sortable: true }, - { key: 'supplyAmount', label: '공급가액', className: 'text-right', sortable: true }, - { key: 'vat', label: '부가세', className: 'text-right', sortable: true }, - { key: 'totalAmount', label: '합계금액', className: 'text-right', sortable: true }, + { key: 'purchaseNo', label: '매입번호', sortable: true, copyable: true }, + { key: 'purchaseDate', label: '매입일', sortable: true, copyable: true }, + { key: 'vendorName', label: '거래처', sortable: true, copyable: true }, + { key: 'supplyAmount', label: '공급가액', className: 'text-right', sortable: true, copyable: true }, + { key: 'vat', label: '부가세', className: 'text-right', sortable: true, copyable: true }, + { key: 'totalAmount', label: '합계금액', className: 'text-right', sortable: true, copyable: true }, { key: 'taxInvoice', label: '세금계산서 수취 확인', className: 'text-center' }, ]; @@ -173,12 +171,12 @@ export function PurchaseManagement() { ], [vendorOptions]); // 필터 변경 핸들러 - const handleFilterChange = useCallback((key: string, value: string | string[]) => { + const _handleFilterChange = useCallback((key: string, value: string | string[]) => { setFilterValues(prev => ({ ...prev, [key]: value })); }, []); // 필터 초기화 핸들러 - const handleFilterReset = useCallback(() => { + const _handleFilterReset = useCallback(() => { setFilterValues({ vendor: 'all', taxInvoiceReceived: 'all', diff --git a/src/components/accounting/ReceivablesStatus/actions.ts b/src/components/accounting/ReceivablesStatus/actions.ts index dfb03f1a..a97a7980 100644 --- a/src/components/accounting/ReceivablesStatus/actions.ts +++ b/src/components/accounting/ReceivablesStatus/actions.ts @@ -5,7 +5,7 @@ import { executeServerAction, type ActionResult } from '@/lib/api/execute-server import { buildApiUrl } from '@/lib/api/query-params'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { cookies } from 'next/headers'; -import type { VendorReceivables, CategoryType, MonthlyAmount, ReceivablesListResponse, MemoUpdateRequest } from './types'; +import type { VendorReceivables, CategoryType, ReceivablesListResponse, MemoUpdateRequest } from './types'; // ===== API 응답 타입 ===== interface CategoryAmountApi { diff --git a/src/components/accounting/ReceivablesStatus/index.tsx b/src/components/accounting/ReceivablesStatus/index.tsx index a79736cb..ea5c651a 100644 --- a/src/components/accounting/ReceivablesStatus/index.tsx +++ b/src/components/accounting/ReceivablesStatus/index.tsx @@ -5,7 +5,6 @@ import { Download, FileText, Save, Loader2, RefreshCw, ChevronDown, ChevronUp } import { formatNumber } from '@/lib/utils/amount'; import { Button } from '@/components/ui/button'; import { Switch } from '@/components/ui/switch'; -import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; @@ -66,7 +65,7 @@ const generateYearOptions = (): Array<{ value: number; label: string }> => { }; // ===== 카테고리 순서 (메모 제외) ===== -const categoryOrder: CategoryType[] = ['sales', 'deposit', 'bill', 'receivable']; +const _categoryOrder: CategoryType[] = ['sales', 'deposit', 'bill', 'receivable']; export function ReceivablesStatus({ highlightVendorId, initialData, initialSummary }: ReceivablesStatusProps) { const { canExport } = usePermission(); @@ -82,7 +81,7 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma const [data, setData] = useState(initialData?.items || []); const [originalOverdueMap, setOriginalOverdueMap] = useState>(new Map()); const [originalMemoMap, setOriginalMemoMap] = useState>(new Map()); - const [summary, setSummary] = useState(initialSummary || { + const [, setSummary] = useState(initialSummary || { totalCarryForward: 0, totalSales: 0, totalDeposits: 0, diff --git a/src/components/accounting/SalesManagement/SalesDetail.tsx b/src/components/accounting/SalesManagement/SalesDetail.tsx index 28052552..c62bd182 100644 --- a/src/components/accounting/SalesManagement/SalesDetail.tsx +++ b/src/components/accounting/SalesManagement/SalesDetail.tsx @@ -68,7 +68,7 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) { // ===== 로딩 상태 ===== const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); + const [, setIsSaving] = useState(false); // ===== 거래처 목록 ===== const [clients, setClients] = useState([]); @@ -77,7 +77,7 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) { const [salesNo, setSalesNo] = useState(''); const [salesDate, setSalesDate] = useState(format(new Date(), 'yyyy-MM-dd')); const [vendorId, setVendorId] = useState(''); - const [vendorName, setVendorName] = useState(''); + const [, setVendorName] = useState(''); const [items, setItems] = useState([createEmptyItem()]); const [taxInvoiceIssued, setTaxInvoiceIssued] = useState(false); const [transactionStatementIssued, setTransactionStatementIssued] = useState(false); diff --git a/src/components/accounting/SalesManagement/index.tsx b/src/components/accounting/SalesManagement/index.tsx index 3dd10606..63b4296a 100644 --- a/src/components/accounting/SalesManagement/index.tsx +++ b/src/components/accounting/SalesManagement/index.tsx @@ -20,10 +20,8 @@ import { toast } from 'sonner'; import { Receipt, Save, - Search, } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; import { Checkbox } from '@/components/ui/checkbox'; import { Switch } from '@/components/ui/switch'; import { @@ -73,12 +71,12 @@ import { applyFilters, enumFilter } from '@/lib/utils/search'; // ===== 테이블 컬럼 정의 ===== const tableColumns = [ { key: 'no', label: '번호', className: 'text-center w-[60px]' }, - { key: 'salesNo', label: '매출번호', sortable: true }, - { key: 'salesDate', label: '매출일', sortable: true }, - { key: 'vendorName', label: '거래처', sortable: true }, - { key: 'supplyAmount', label: '공급가액', className: 'text-right', sortable: true }, - { key: 'vat', label: '부가세', className: 'text-right', sortable: true }, - { key: 'totalAmount', label: '합계금액', className: 'text-right', sortable: true }, + { key: 'salesNo', label: '매출번호', sortable: true, copyable: true }, + { key: 'salesDate', label: '매출일', sortable: true, copyable: true }, + { key: 'vendorName', label: '거래처', sortable: true, copyable: true }, + { key: 'totalSupplyAmount', label: '공급가액', className: 'text-right', sortable: true, copyable: true }, + { key: 'totalVat', label: '부가세', className: 'text-right', sortable: true, copyable: true }, + { key: 'totalAmount', label: '합계금액', className: 'text-right', sortable: true, copyable: true }, { key: 'taxInvoice', label: '세금계산서 발행완료', className: 'text-center' }, { key: 'transactionStatement', label: '거래명세서 발행완료', className: 'text-center' }, ]; @@ -197,7 +195,7 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem if ((!initialData || initialData.length === 0) && !isLoading) { loadData(); } - // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // ===== 페이지 변경 ===== diff --git a/src/components/accounting/SalesManagement/types.ts b/src/components/accounting/SalesManagement/types.ts index 77c1fa41..0d7d2689 100644 --- a/src/components/accounting/SalesManagement/types.ts +++ b/src/components/accounting/SalesManagement/types.ts @@ -224,7 +224,7 @@ export const API_STATUS_MAP: Record = { // API 응답 → 프론트엔드 타입 변환 export function transformApiToFrontend(apiData: SaleApiData): SalesRecord { // 수주(Order)의 품목(items)을 매출 품목으로 변환 - const items: SalesItem[] = (apiData.order?.items ?? []).map((item, index) => ({ + const items: SalesItem[] = (apiData.order?.items ?? []).map((item, _index) => ({ id: String(item.id), itemName: item.item_name ?? '', quantity: parseFloat(String(item.quantity)) || 0, diff --git a/src/components/accounting/TaxInvoiceIssuance/index.tsx b/src/components/accounting/TaxInvoiceIssuance/index.tsx index a4a32678..e44cedc6 100644 --- a/src/components/accounting/TaxInvoiceIssuance/index.tsx +++ b/src/components/accounting/TaxInvoiceIssuance/index.tsx @@ -42,7 +42,6 @@ import { SORT_BY_OPTIONS, SORT_ORDER_OPTIONS, TAX_INVOICE_STATUS_MAP, - createEmptyBusinessEntity, } from './types'; import type { TaxInvoiceRecord, diff --git a/src/components/accounting/TaxInvoiceManagement/JournalEntryModal.tsx b/src/components/accounting/TaxInvoiceManagement/JournalEntryModal.tsx index 111a1987..de66dad2 100644 --- a/src/components/accounting/TaxInvoiceManagement/JournalEntryModal.tsx +++ b/src/components/accounting/TaxInvoiceManagement/JournalEntryModal.tsx @@ -127,12 +127,12 @@ export function JournalEntryModal({ }, [open, invoice]); // 행 추가 - const handleAddRow = useCallback(() => { + const _handleAddRow = useCallback(() => { setRows((prev) => [...prev, createEmptyRow()]); }, []); // 행 삭제 - const handleRemoveRow = useCallback((rowId: string) => { + const _handleRemoveRow = useCallback((rowId: string) => { setRows((prev) => { if (prev.length <= 1) return prev; return prev.filter((r) => r.id !== rowId); diff --git a/src/components/accounting/TaxInvoiceManagement/index.tsx b/src/components/accounting/TaxInvoiceManagement/index.tsx index f64a2ca3..82aa2138 100644 --- a/src/components/accounting/TaxInvoiceManagement/index.tsx +++ b/src/components/accounting/TaxInvoiceManagement/index.tsx @@ -103,18 +103,18 @@ const excelColumns: ExcelColumn>[ // ===== 테이블 컬럼 ===== const tableColumns = [ - { key: 'writeDate', label: '작성일자', className: 'text-center', sortable: true }, - { key: 'issueDate', label: '발급일자', className: 'text-center', sortable: true }, - { key: 'vendorName', label: '거래처', sortable: true }, - { key: 'vendorBusinessNumber', label: '사업자번호\n(주민번호)', className: 'text-center', sortable: true }, - { key: 'taxType', label: '과세형태', className: 'text-center', sortable: true }, - { key: 'itemName', label: '품목', sortable: true }, - { key: 'supplyAmount', label: '공급가액', className: 'text-right', sortable: true }, - { key: 'taxAmount', label: '세액', className: 'text-right', sortable: true }, - { key: 'totalAmount', label: '합계', className: 'text-right', sortable: true }, - { key: 'receiptType', label: '영수청구', className: 'text-center', sortable: true }, - { key: 'documentType', label: '문서형태', className: 'text-center', sortable: true }, - { key: 'issueType', label: '발급형태', className: 'text-center', sortable: true }, + { key: 'writeDate', label: '작성일자', className: 'text-center', sortable: true, copyable: true }, + { key: 'issueDate', label: '발급일자', className: 'text-center', sortable: true, copyable: true }, + { key: 'vendorName', label: '거래처', sortable: true, copyable: true }, + { key: 'vendorBusinessNumber', label: '사업자번호\n(주민번호)', className: 'text-center', sortable: true, copyable: true }, + { key: 'taxType', label: '과세형태', className: 'text-center', sortable: true, copyable: true }, + { key: 'itemName', label: '품목', sortable: true, copyable: true }, + { key: 'supplyAmount', label: '공급가액', className: 'text-right', sortable: true, copyable: true }, + { key: 'taxAmount', label: '세액', className: 'text-right', sortable: true, copyable: true }, + { key: 'totalAmount', label: '합계', className: 'text-right', sortable: true, copyable: true }, + { key: 'receiptType', label: '영수청구', className: 'text-center', sortable: true, copyable: true }, + { key: 'documentType', label: '문서형태', className: 'text-center', sortable: true, copyable: true }, + { key: 'issueType', label: '발급형태', className: 'text-center', sortable: true, copyable: true }, { key: 'status', label: '상태', className: 'text-center', sortable: true }, { key: 'journal', label: '분개', className: 'text-center w-[80px]' }, ]; diff --git a/src/components/accounting/VendorLedger/index.tsx b/src/components/accounting/VendorLedger/index.tsx index fcf1ba23..ec1e1ac0 100644 --- a/src/components/accounting/VendorLedger/index.tsx +++ b/src/components/accounting/VendorLedger/index.tsx @@ -36,12 +36,12 @@ import { usePermission } from '@/hooks/usePermission'; // ===== 테이블 컬럼 정의 ===== const tableColumns = [ { key: 'no', label: 'No.', className: 'text-center w-[60px]' }, - { key: 'vendorName', label: '거래처명', sortable: true }, - { key: 'carryoverBalance', label: '이월잔액', className: 'text-right w-[120px]', sortable: true }, - { key: 'sales', label: '매출', className: 'text-right w-[120px]', sortable: true }, - { key: 'collection', label: '수금', className: 'text-right w-[120px]', sortable: true }, - { key: 'balance', label: '잔액', className: 'text-right w-[120px]', sortable: true }, - { key: 'paymentDate', label: '결제일', className: 'text-center w-[100px]', sortable: true }, + { key: 'vendorName', label: '거래처명', sortable: true, copyable: true }, + { key: 'carryoverBalance', label: '이월잔액', className: 'text-right w-[120px]', sortable: true, copyable: true }, + { key: 'sales', label: '매출', className: 'text-right w-[120px]', sortable: true, copyable: true }, + { key: 'collection', label: '수금', className: 'text-right w-[120px]', sortable: true, copyable: true }, + { key: 'balance', label: '잔액', className: 'text-right w-[120px]', sortable: true, copyable: true }, + { key: 'paymentDate', label: '결제일', className: 'text-center w-[100px]', sortable: true, copyable: true }, ]; // ===== 엑셀 컬럼 정의 ===== diff --git a/src/components/accounting/VendorManagement/VendorDetailClient.tsx b/src/components/accounting/VendorManagement/VendorDetailClient.tsx index 695e0794..d3c0bcd5 100644 --- a/src/components/accounting/VendorManagement/VendorDetailClient.tsx +++ b/src/components/accounting/VendorManagement/VendorDetailClient.tsx @@ -3,7 +3,6 @@ import { useState, useCallback, useEffect } from 'react'; import { invalidateDashboard } from '@/lib/dashboard-invalidation'; import { useRouter } from 'next/navigation'; -import { formatNumber } from '@/lib/utils/amount'; import { Plus, Trash2, Upload } from 'lucide-react'; import { Switch } from '@/components/ui/switch'; import { CurrencyInput } from '@/components/ui/currency-input'; diff --git a/src/components/accounting/VendorManagement/VendorManagementClient.tsx b/src/components/accounting/VendorManagement/VendorManagementClient.tsx index 82b1f271..47fcfb19 100644 --- a/src/components/accounting/VendorManagement/VendorManagementClient.tsx +++ b/src/components/accounting/VendorManagement/VendorManagementClient.tsx @@ -42,15 +42,9 @@ import { type RowClickHandlers, } from '@/components/templates/UniversalListPage'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; -import { toast } from 'sonner'; import { deleteClient } from './actions'; import type { Vendor, - VendorCategory, - CreditRating, - TransactionGrade, - BadDebtStatus, - VendorStatus, SortOption, } from './types'; import { @@ -75,7 +69,7 @@ interface VendorManagementClientProps { initialTotal: number; } -export function VendorManagementClient({ initialData, initialTotal }: VendorManagementClientProps) { +export function VendorManagementClient({ initialData, initialTotal: _initialTotal }: VendorManagementClientProps) { const router = useRouter(); // ===== 상태 관리 ===== @@ -203,13 +197,13 @@ export function VendorManagementClient({ initialData, initialTotal }: VendorMana // ===== 테이블 컬럼 ===== const tableColumns: TableColumn[] = useMemo(() => [ { key: 'no', label: '번호', className: 'text-center w-[60px]' }, - { key: 'category', label: '구분', className: 'text-center w-[100px]', sortable: true }, - { key: 'vendorName', label: '거래처명', sortable: true }, - { key: 'purchasePaymentDay', label: '매입 결제일', className: 'text-center w-[100px]', sortable: true }, - { key: 'salesPaymentDay', label: '매출 결제일', className: 'text-center w-[100px]', sortable: true }, + { key: 'category', label: '구분', className: 'text-center w-[100px]', sortable: true, copyable: true }, + { key: 'vendorName', label: '거래처명', sortable: true, copyable: true }, + { key: 'purchasePaymentDay', label: '매입 결제일', className: 'text-center w-[100px]', sortable: true, copyable: true }, + { key: 'salesPaymentDay', label: '매출 결제일', className: 'text-center w-[100px]', sortable: true, copyable: true }, { key: 'creditRating', label: '신용등급', className: 'text-center w-[90px]', sortable: true }, { key: 'transactionGrade', label: '거래등급', className: 'text-center w-[100px]', sortable: true }, - { key: 'outstandingAmount', label: '미수금', className: 'text-right w-[120px]', sortable: true }, + { key: 'outstandingAmount', label: '미수금', className: 'text-right w-[120px]', sortable: true, copyable: true }, { key: 'badDebtStatus', label: '악성채권', className: 'text-center w-[90px]', sortable: true }, { key: 'status', label: '상태', className: 'text-center w-[80px]', sortable: true }, { key: 'actions', label: '작업', className: 'text-center w-[150px]' }, diff --git a/src/components/accounting/VendorManagement/index.tsx b/src/components/accounting/VendorManagement/index.tsx index 8df638aa..11479cb0 100644 --- a/src/components/accounting/VendorManagement/index.tsx +++ b/src/components/accounting/VendorManagement/index.tsx @@ -56,13 +56,13 @@ import { toast } from 'sonner'; // ===== 테이블 컬럼 정의 ===== const tableColumns = [ { key: 'no', label: '번호', className: 'text-center w-[60px]' }, - { key: 'category', label: '구분', className: 'text-center w-[100px]', sortable: true }, - { key: 'vendorName', label: '거래처명', sortable: true }, - { key: 'purchasePaymentDay', label: '매입 결제일', className: 'text-center w-[100px]', sortable: true }, - { key: 'salesPaymentDay', label: '매출 결제일', className: 'text-center w-[100px]', sortable: true }, + { key: 'category', label: '구분', className: 'text-center w-[100px]', sortable: true, copyable: true }, + { key: 'vendorName', label: '거래처명', sortable: true, copyable: true }, + { key: 'purchasePaymentDay', label: '매입 결제일', className: 'text-center w-[100px]', sortable: true, copyable: true }, + { key: 'salesPaymentDay', label: '매출 결제일', className: 'text-center w-[100px]', sortable: true, copyable: true }, { key: 'creditRating', label: '신용등급', className: 'text-center w-[90px]', sortable: true }, { key: 'transactionGrade', label: '거래등급', className: 'text-center w-[100px]', sortable: true }, - { key: 'outstandingAmount', label: '미수금', className: 'text-right w-[120px]', sortable: true }, + { key: 'outstandingAmount', label: '미수금', className: 'text-right w-[120px]', sortable: true, copyable: true }, { key: 'badDebtStatus', label: '악성채권', className: 'text-center w-[90px]', sortable: true }, { key: 'status', label: '상태', className: 'text-center w-[80px]', sortable: true }, ]; @@ -73,7 +73,7 @@ interface VendorManagementProps { initialTotal: number; } -export function VendorManagement({ initialData, initialTotal }: VendorManagementProps) { +export function VendorManagement({ initialData, initialTotal: _initialTotal }: VendorManagementProps) { const router = useRouter(); // ===== 날짜 범위 상태 ===== diff --git a/src/components/accounting/WithdrawalManagement/index.tsx b/src/components/accounting/WithdrawalManagement/index.tsx index eaf2469c..f2557eb4 100644 --- a/src/components/accounting/WithdrawalManagement/index.tsx +++ b/src/components/accounting/WithdrawalManagement/index.tsx @@ -83,13 +83,13 @@ import { // ===== 테이블 컬럼 정의 ===== const tableColumns = [ - { key: 'withdrawalDate', label: '출금일', className: 'w-[100px]', sortable: true }, - { key: 'accountName', label: '출금계좌', className: 'min-w-[120px]', sortable: true }, - { key: 'recipientName', label: '수취인명', className: 'min-w-[100px]', sortable: true }, - { key: 'withdrawalAmount', label: '출금금액', className: 'text-right w-[110px]', sortable: true }, - { key: 'vendorName', label: '거래처', className: 'min-w-[100px]', sortable: true }, - { key: 'note', label: '적요', className: 'min-w-[150px]', sortable: true }, - { key: 'withdrawalType', label: '출금유형', className: 'text-center w-[90px]', sortable: true }, + { key: 'withdrawalDate', label: '출금일', className: 'w-[100px]', sortable: true, copyable: true }, + { key: 'accountName', label: '출금계좌', className: 'min-w-[120px]', sortable: true, copyable: true }, + { key: 'recipientName', label: '수취인명', className: 'min-w-[100px]', sortable: true, copyable: true }, + { key: 'withdrawalAmount', label: '출금금액', className: 'text-right w-[110px]', sortable: true, copyable: true }, + { key: 'vendorName', label: '거래처', className: 'min-w-[100px]', sortable: true, copyable: true }, + { key: 'note', label: '적요', className: 'min-w-[150px]', sortable: true, copyable: true }, + { key: 'withdrawalType', label: '출금유형', className: 'text-center w-[90px]', sortable: true, copyable: true }, ]; // ===== 컴포넌트 Props ===== @@ -103,7 +103,7 @@ interface WithdrawalManagementProps { }; } -export function WithdrawalManagement({ initialData, initialPagination }: WithdrawalManagementProps) { +export function WithdrawalManagement({ initialData, initialPagination: _initialPagination }: WithdrawalManagementProps) { const router = useRouter(); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== diff --git a/src/components/approval/ApprovalBox/index.tsx b/src/components/approval/ApprovalBox/index.tsx index 1ff38345..7f2cd85f 100644 --- a/src/components/approval/ApprovalBox/index.tsx +++ b/src/components/approval/ApprovalBox/index.tsx @@ -14,7 +14,6 @@ import { import { toast } from 'sonner'; import { getInbox, - getInboxSummary, approveDocument, rejectDocument, approveDocumentsBulk, @@ -80,17 +79,9 @@ import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { usePermission } from '@/hooks/usePermission'; import { InspectionReportModal } from '@/components/production/WorkOrders/documents/InspectionReportModal'; -// ===== 통계 타입 ===== -interface InboxSummary { - total: number; - pending: number; - approved: number; - rejected: number; -} - export function ApprovalBox() { const router = useRouter(); - const [isPending, startTransition] = useTransition(); + const [, startTransition] = useTransition(); const { canApprove } = usePermission(); // ===== 상태 관리 ===== @@ -115,7 +106,7 @@ export function ApprovalBox() { const [isModalOpen, setIsModalOpen] = useState(false); const [selectedDocument, setSelectedDocument] = useState(null); const [modalData, setModalData] = useState(null); - const [isModalLoading, setIsModalLoading] = useState(false); + const [, setIsModalLoading] = useState(false); // ===== 검사성적서 모달 상태 (work_order 연결 문서용) ===== const [isInspectionModalOpen, setIsInspectionModalOpen] = useState(false); @@ -390,7 +381,7 @@ export function ApprovalBox() { drafter, }; break; - default: + default: { // 품의서 const uploadedFileUrls = (formData.proposalData?.uploadedFiles || []).map(f => `/api/proxy/files/${f.id}/download` @@ -409,6 +400,7 @@ export function ApprovalBox() { drafter, }; break; + } } setModalData(convertedData); @@ -527,12 +519,12 @@ export function ApprovalBox() { columns: [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'documentNo', label: '문서번호' }, - { key: 'approvalType', label: '문서유형' }, - { key: 'title', label: '제목' }, - { key: 'drafter', label: '기안자' }, - { key: 'approver', label: '결재자' }, - { key: 'draftDate', label: '기안일시' }, + { key: 'documentNo', label: '문서번호', copyable: true }, + { key: 'approvalType', label: '문서유형', copyable: true }, + { key: 'title', label: '제목', copyable: true }, + { key: 'drafter', label: '기안자', copyable: true }, + { key: 'approver', label: '결재자', copyable: true }, + { key: 'draftDate', label: '기안일시', copyable: true }, { key: 'status', label: '상태', className: 'text-center' }, ], diff --git a/src/components/approval/DocumentCreate/ProposalForm.tsx b/src/components/approval/DocumentCreate/ProposalForm.tsx index ff83373c..aa72bea9 100644 --- a/src/components/approval/DocumentCreate/ProposalForm.tsx +++ b/src/components/approval/DocumentCreate/ProposalForm.tsx @@ -18,7 +18,7 @@ import { SelectValue, } from '@/components/ui/select'; import { getClients } from '@/components/accounting/VendorManagement/actions'; -import type { ProposalData, UploadedFile } from './types'; +import type { ProposalData } from './types'; // 거래처 옵션 타입 interface ClientOption { diff --git a/src/components/approval/DocumentCreate/actions.ts b/src/components/approval/DocumentCreate/actions.ts index 3fa675ee..75e5ba23 100644 --- a/src/components/approval/DocumentCreate/actions.ts +++ b/src/components/approval/DocumentCreate/actions.ts @@ -26,12 +26,6 @@ import type { // API 응답 타입 정의 // ============================================ -interface ApiResponse { - success: boolean; - data: T; - message: string; -} - // 비용견적서 API 응답 타입 interface ExpenseEstimateApiItem { id: number; diff --git a/src/components/approval/DocumentDetail/DocumentDetailModalV2.tsx b/src/components/approval/DocumentDetail/DocumentDetailModalV2.tsx index ae49943e..aba29a7c 100644 --- a/src/components/approval/DocumentDetail/DocumentDetailModalV2.tsx +++ b/src/components/approval/DocumentDetail/DocumentDetailModalV2.tsx @@ -6,7 +6,6 @@ import { ExpenseReportDocument } from './ExpenseReportDocument'; import { ExpenseEstimateDocument } from './ExpenseEstimateDocument'; import { LinkedDocumentContent } from './LinkedDocumentContent'; import type { - DocumentType, DocumentDetailModalProps, ProposalDocumentData, ExpenseReportDocumentData, diff --git a/src/components/approval/DocumentDetail/index.tsx b/src/components/approval/DocumentDetail/index.tsx index b00858a0..929bec5a 100644 --- a/src/components/approval/DocumentDetail/index.tsx +++ b/src/components/approval/DocumentDetail/index.tsx @@ -6,12 +6,6 @@ import { DialogTitle, VisuallyHidden, } from '@/components/ui/dialog'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; import { Button } from '@/components/ui/button'; import { Copy, @@ -19,12 +13,6 @@ import { X as XIcon, CheckCircle, Printer, - Share2, - ChevronDown, - FileText, - Mail, - Phone, - MessageCircle, Send, } from 'lucide-react'; import { ProposalDocument } from './ProposalDocument'; @@ -32,7 +20,6 @@ import { ExpenseReportDocument } from './ExpenseReportDocument'; import { ExpenseEstimateDocument } from './ExpenseEstimateDocument'; import { printArea } from '@/lib/print-utils'; import type { - DocumentType, DocumentDetailModalProps, ProposalDocumentData, ExpenseReportDocumentData, @@ -53,8 +40,6 @@ export function DocumentDetailModal({ onSubmit, }: DocumentDetailModalProps) { // 기안함 모드에서 임시저장 상태일 때만 수정/상신 가능 - const canEdit = mode === 'inbox' || documentStatus === 'draft'; - const canSubmit = mode === 'draft' && documentStatus === 'draft'; const getDocumentTitle = () => { switch (documentType) { case 'proposal': @@ -72,18 +57,6 @@ export function DocumentDetailModal({ printArea({ title: `${getDocumentTitle()} 인쇄` }); }; - const handleSharePdf = () => { - }; - - const handleShareEmail = () => { - }; - - const handleShareFax = () => { - }; - - const handleShareKakao = () => { - }; - const renderDocument = () => { switch (documentType) { case 'proposal': diff --git a/src/components/approval/DraftBox/index.tsx b/src/components/approval/DraftBox/index.tsx index 7ff81c26..b2395221 100644 --- a/src/components/approval/DraftBox/index.tsx +++ b/src/components/approval/DraftBox/index.tsx @@ -9,7 +9,6 @@ import { Send, Trash2, Plus, - Bell, } from 'lucide-react'; import { toast } from 'sonner'; import { @@ -49,7 +48,6 @@ import type { } from '@/components/approval/DocumentDetail/types'; import type { DraftRecord, - DocumentStatus, Approver, SortOption, FilterOption, @@ -73,7 +71,7 @@ interface DraftsSummary { export function DraftBox() { const router = useRouter(); - const [isPending, startTransition] = useTransition(); + const [, startTransition] = useTransition(); // ===== 상태 관리 ===== const [searchQuery, setSearchQuery] = useState(''); @@ -464,11 +462,11 @@ export function DraftBox() { columns: [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'documentNo', label: '문서번호' }, - { key: 'documentType', label: '문서유형' }, - { key: 'title', label: '제목' }, - { key: 'approvers', label: '결재자' }, - { key: 'draftDate', label: '기안일시' }, + { key: 'documentNo', label: '문서번호', copyable: true }, + { key: 'documentType', label: '문서유형', copyable: true }, + { key: 'title', label: '제목', copyable: true }, + { key: 'approvers', label: '결재자', copyable: true }, + { key: 'draftDate', label: '기안일시', copyable: true }, { key: 'status', label: '상태', className: 'text-center' }, ], @@ -624,7 +622,7 @@ export function DraftBox() { ), renderTableRow: (item, index, globalIndex, handlers) => { - const { isSelected, onToggle, onRowClick } = handlers; + const { isSelected, onToggle } = handlers; return ( ('all'); @@ -152,14 +151,14 @@ export function ReferenceBox() { // 마운트 시 1회만 실행 (summary 로드) useEffect(() => { loadSummary(); - // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // ===== 데이터 로드 (의존성 명시적 관리) ===== // currentPage, searchQuery, filterOption, sortOption, activeTab 변경 시 데이터 재로드 useEffect(() => { loadData(); - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentPage, searchQuery, filterOption, sortOption, activeTab]); // ===== 검색어/필터/탭 변경 시 페이지 초기화 ===== @@ -383,11 +382,11 @@ export function ReferenceBox() { // 문서번호, 문서유형, 제목, 기안자, 기안일시, 상태 const tableColumns = useMemo(() => [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'documentNo', label: '문서번호' }, - { key: 'approvalType', label: '문서유형' }, - { key: 'title', label: '제목' }, - { key: 'drafter', label: '기안자' }, - { key: 'draftDate', label: '기안일시' }, + { key: 'documentNo', label: '문서번호', copyable: true }, + { key: 'approvalType', label: '문서유형', copyable: true }, + { key: 'title', label: '제목', copyable: true }, + { key: 'drafter', label: '기안자', copyable: true }, + { key: 'draftDate', label: '기안일시', copyable: true }, { key: 'status', label: '상태', className: 'text-center' }, ], []); @@ -513,7 +512,7 @@ export function ReferenceBox() { ), renderTableRow: (item, index, globalIndex, handlers) => { - const { isSelected, onToggle, onRowClick } = handlers; + const { isSelected, onToggle, onRowClick: _onRowClick } = handlers; return ( { + const _handleBusinessNumberChange = (value: string) => { const formatted = formatBusinessNumber(value); handleInputChange("businessNumber", formatted); }; @@ -107,7 +107,7 @@ export function SignupPage() { } }; - const handlePhoneNumberChange = (value: string) => { + const _handlePhoneNumberChange = (value: string) => { const formatted = formatPhoneNumber(value); handleInputChange("phone", formatted); }; diff --git a/src/components/board/BoardDetail/index.tsx b/src/components/board/BoardDetail/index.tsx index 00df72e4..cf047757 100644 --- a/src/components/board/BoardDetail/index.tsx +++ b/src/components/board/BoardDetail/index.tsx @@ -33,7 +33,6 @@ import { useDeleteDialog } from '@/hooks/useDeleteDialog'; import { CommentSection } from '../CommentSection'; import { deletePost } from '../actions'; import type { Post, Comment } from '../types'; -import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { useMenuStore } from '@/stores/menuStore'; import { sanitizeHTML } from '@/lib/sanitize'; diff --git a/src/components/board/BoardForm/index.tsx b/src/components/board/BoardForm/index.tsx index 8d4a68ef..fbd98d0b 100644 --- a/src/components/board/BoardForm/index.tsx +++ b/src/components/board/BoardForm/index.tsx @@ -13,13 +13,11 @@ import { useState, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { format } from 'date-fns'; -import { Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { FileDropzone } from '@/components/ui/file-dropzone'; import { FileList, type NewFile, type ExistingFile } from '@/components/ui/file-list'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { boardCreateConfig, boardEditConfig } from './boardFormConfig'; -import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; @@ -40,7 +38,6 @@ import { import { AlertDialog, AlertDialogAction, - AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, @@ -50,7 +47,7 @@ import { import dynamic from 'next/dynamic'; const RichTextEditor = dynamic(() => import('../RichTextEditor'), { ssr: false }); -import type { Post, Attachment } from '../types'; +import type { Post } from '../types'; import { createPost, updatePost } from '../actions'; import { getBoards } from '../BoardManagement/actions'; import type { Board } from '../BoardManagement/types'; @@ -107,7 +104,7 @@ export function BoardForm({ mode, initialData }: BoardFormProps) { // 게시판 목록 상태 const [boards, setBoards] = useState([]); const [isBoardsLoading, setIsBoardsLoading] = useState(true); - const [isSubmitting, setIsSubmitting] = useState(false); + const [, setIsSubmitting] = useState(false); // ===== 게시판 목록 조회 ===== useEffect(() => { diff --git a/src/components/board/BoardList/BoardListUnified.tsx b/src/components/board/BoardList/BoardListUnified.tsx index 76decc00..e894a716 100644 --- a/src/components/board/BoardList/BoardListUnified.tsx +++ b/src/components/board/BoardList/BoardListUnified.tsx @@ -29,7 +29,6 @@ import type { Post } from '../types'; import { getBoards } from '../BoardManagement/actions'; import { getPosts, getMyPosts, deletePost } from '../actions'; import type { Board } from '../BoardManagement/types'; -import { toast } from 'sonner'; import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; export function BoardListUnified() { @@ -133,7 +132,7 @@ export function BoardListUnified() { totalCount: 0, }; }, - deleteItem: async (id: string) => { + deleteItem: async (_id: string) => { // 게시글 삭제는 boardCode가 필요하므로 별도 처리 // UniversalListPage에서는 사용하지 않고 커스텀 삭제 처리 return { success: false, error: 'Use custom delete handler' }; @@ -143,10 +142,10 @@ export function BoardListUnified() { // ===== 테이블 컬럼 ===== columns: [ { key: 'no', label: 'No.', className: 'w-[60px] text-center' }, - { key: 'title', label: '제목', className: 'min-w-[300px]' }, - { key: 'author', label: '작성자', className: 'w-[120px]' }, - { key: 'createdAt', label: '등록일', className: 'w-[120px]' }, - { key: 'viewCount', label: '조회수', className: 'w-[80px] text-center' }, + { key: 'title', label: '제목', className: 'min-w-[300px]', copyable: true }, + { key: 'author', label: '작성자', className: 'w-[120px]', copyable: true }, + { key: 'createdAt', label: '등록일', className: 'w-[120px]', copyable: true }, + { key: 'viewCount', label: '조회수', className: 'w-[80px] text-center', copyable: true }, { key: 'actions', label: '작업', className: 'w-[100px] text-center' }, ], @@ -161,7 +160,7 @@ export function BoardListUnified() { detailMode: 'none', // 커스텀 라우팅 사용 (boardCode 포함) // ===== 헤더 액션 ===== - headerActions: ({ onCreate }) => ( + headerActions: ({ onCreate: _onCreate }) => ( <> { router.push(`/ko/board/${item.boardCode}/${item.id}?mode=view`); }, @@ -224,10 +224,10 @@ export function BoardList() { columns: [ { key: 'no', label: 'No.', className: 'w-[60px] text-center' }, - { key: 'title', label: '제목', className: 'min-w-[300px]', sortable: true }, - { key: 'author', label: '작성자', className: 'w-[120px]', sortable: true }, - { key: 'createdAt', label: '등록일', className: 'w-[120px]', sortable: true }, - { key: 'viewCount', label: '조회수', className: 'w-[80px] text-center', sortable: true }, + { key: 'title', label: '제목', className: 'min-w-[300px]', sortable: true, copyable: true }, + { key: 'author', label: '작성자', className: 'w-[120px]', sortable: true, copyable: true }, + { key: 'createdAt', label: '등록일', className: 'w-[120px]', sortable: true, copyable: true }, + { key: 'viewCount', label: '조회수', className: 'w-[80px] text-center', sortable: true, copyable: true }, { key: 'actions', label: '작업', className: 'w-[100px] text-center' }, ], diff --git a/src/components/board/BoardManagement/BoardDetailClientV2.tsx b/src/components/board/BoardManagement/BoardDetailClientV2.tsx index c313fbfb..9e06755e 100644 --- a/src/components/board/BoardManagement/BoardDetailClientV2.tsx +++ b/src/components/board/BoardManagement/BoardDetailClientV2.tsx @@ -7,7 +7,7 @@ * 기존 BoardDetail, BoardForm 컴포넌트 활용 */ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { Loader2 } from 'lucide-react'; import { BoardDetail } from './BoardDetail'; @@ -17,7 +17,6 @@ import { forceRefreshMenus } from '@/lib/utils/menuRefresh'; import type { Board, BoardFormData } from './types'; import { DetailPageSkeleton } from '@/components/ui/skeleton'; import { ErrorCard } from '@/components/ui/error-card'; -import { Button } from '@/components/ui/button'; import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { useDeleteDialog } from '@/hooks/useDeleteDialog'; import { toast } from 'sonner'; diff --git a/src/components/board/BoardManagement/BoardForm.tsx b/src/components/board/BoardManagement/BoardForm.tsx index af25f518..d98703e9 100644 --- a/src/components/board/BoardManagement/BoardForm.tsx +++ b/src/components/board/BoardManagement/BoardForm.tsx @@ -17,7 +17,7 @@ import { SelectValue, } from '@/components/ui/select'; import { Checkbox } from '@/components/ui/checkbox'; -import { ClipboardList, ArrowLeft, Save, X } from 'lucide-react'; +import { ClipboardList, Save, X } from 'lucide-react'; import { useMenuStore } from '@/stores/menuStore'; import type { Board, BoardFormData, BoardTarget, BoardStatus } from './types'; import { BOARD_TARGETS, BOARD_STATUS_LABELS } from './types'; diff --git a/src/components/board/BoardManagement/index.tsx b/src/components/board/BoardManagement/index.tsx index 70f83775..956ecca7 100644 --- a/src/components/board/BoardManagement/index.tsx +++ b/src/components/board/BoardManagement/index.tsx @@ -39,7 +39,7 @@ const getTargetDisplay = (board: Board) => { }; // 탭 옵션 계산 -const computeTabs = (data: Board[]): TabOption[] => { +const _computeTabs = (data: Board[]): TabOption[] => { const activeCount = data.filter((b) => b.status === 'active').length; const inactiveCount = data.filter((b) => b.status === 'inactive').length; @@ -82,11 +82,11 @@ const createBoardManagementConfig = (router: ReturnType): Univ // 테이블 컬럼 columns: [ { key: 'rowNumber', label: 'No.', className: 'w-[60px] text-center' }, - { key: 'target', label: '대상', className: 'min-w-[100px]' }, - { key: 'boardName', label: '게시판명', className: 'min-w-[150px]' }, + { key: 'target', label: '대상', className: 'min-w-[100px]', copyable: true }, + { key: 'boardName', label: '게시판명', className: 'min-w-[150px]', copyable: true }, { key: 'status', label: '상태', className: 'min-w-[80px]' }, - { key: 'authorName', label: '작성자', className: 'min-w-[100px]' }, - { key: 'createdAt', label: '등록일시', className: 'min-w-[120px]' }, + { key: 'authorName', label: '작성자', className: 'min-w-[100px]', copyable: true }, + { key: 'createdAt', label: '등록일시', className: 'min-w-[120px]', copyable: true }, ], // 탭 설정 (클라이언트 사이드 계산) @@ -135,7 +135,7 @@ const createBoardManagementConfig = (router: ReturnType): Univ // 테이블 행 렌더링 renderTableRow: (item, index, globalIndex, handlers) => { - const { isSelected, onToggle, onRowClick, onEdit, onDelete } = handlers; + const { isSelected, onToggle, onRowClick, onEdit: _onEdit, onDelete: _onDelete } = handlers; return ( ): Univ // 모바일 카드 렌더링 renderMobileCard: (item, index, globalIndex, handlers) => { - const { isSelected, onToggle, onRowClick, onEdit, onDelete } = handlers; + const { isSelected, onToggle, onRowClick, onEdit: _onEdit, onDelete: _onDelete } = handlers; return ( { - }, []); // 카드/가지급금 관리 카드 클릭 → 모두 가지급금 상세(cm2) 모달 // 기획서 P52: 카드, 경조사, 상품권, 접대비, 총합계 모두 동일한 가지급금 상세 모달 - const handleCardManagementCardClick = useCallback(async (cardId: string) => { + const handleCardManagementCardClick = useCallback(async (_cardId: string) => { try { const modalData = await cardManagementModals.fetchModalData('cm2'); const config = getCardManagementModalConfigWithData('cm2', modalData); @@ -349,7 +345,7 @@ export function CEODashboard() { }, [cardManagementModals]); // 접대비 현황 카드 클릭 - API 데이터로 모달 열기 - const handleEntertainmentCardClick = useCallback(async (cardId: string) => { + const handleEntertainmentCardClick = useCallback(async (_cardId: string) => { setCurrentModalCardId('entertainment_detail'); const apiConfig = await entertainmentDetailData.refetch(); if (apiConfig) { diff --git a/src/components/business/CEODashboard/components.tsx b/src/components/business/CEODashboard/components.tsx index ff59e89c..3c8b3ab2 100644 --- a/src/components/business/CEODashboard/components.tsx +++ b/src/components/business/CEODashboard/components.tsx @@ -34,14 +34,6 @@ export const SECTION_THEME_STYLES: Record { - const formatted = new Intl.NumberFormat('ko-KR').format(amount); - return showUnit ? formatted + '원' : formatted; -}; - /** * USD 달러 포맷 함수 */ diff --git a/src/components/business/CEODashboard/modals/DetailModalSections.tsx b/src/components/business/CEODashboard/modals/DetailModalSections.tsx index d4c3a227..0f86614b 100644 --- a/src/components/business/CEODashboard/modals/DetailModalSections.tsx +++ b/src/components/business/CEODashboard/modals/DetailModalSections.tsx @@ -2,7 +2,6 @@ import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; import { Search as SearchIcon } from 'lucide-react'; -import { Button } from '@/components/ui/button'; import { Select, SelectContent, diff --git a/src/components/business/CEODashboard/sections/CalendarSection.tsx b/src/components/business/CEODashboard/sections/CalendarSection.tsx index fbe61573..ba010adb 100644 --- a/src/components/business/CEODashboard/sections/CalendarSection.tsx +++ b/src/components/business/CEODashboard/sections/CalendarSection.tsx @@ -126,7 +126,7 @@ export function CalendarSection({ const router = useRouter(); const [selectedDate, setSelectedDate] = useState(new Date()); const [currentDate, setCurrentDate] = useState(new Date()); - const [viewType, setViewType] = useState('month'); + const [, _setViewType] = useState('month'); const [deptFilter, setDeptFilter] = useState('all'); const [taskFilter, setTaskFilter] = useState('all'); @@ -508,7 +508,7 @@ export function CalendarSection({ }; const dotColor = colorMap[evType] || 'bg-gray-400'; const title = evData?.name as string || evData?.title as string || ev.title; - const cleanTitle = title?.replace(/^[🔴🟠]\s*/, '') || ''; + const cleanTitle = title?.replace(/^[🔴🟠]\s*/u, '') || ''; const mobileScheduleLink = isSelected && evType !== 'holiday' && evType !== 'tax' && evType !== 'issue' ? getScheduleLink(evData as unknown as CalendarScheduleItem) : null; diff --git a/src/components/business/CEODashboard/sections/EnhancedSections.tsx b/src/components/business/CEODashboard/sections/EnhancedSections.tsx index 88c4bd62..1b1364f4 100644 --- a/src/components/business/CEODashboard/sections/EnhancedSections.tsx +++ b/src/components/business/CEODashboard/sections/EnhancedSections.tsx @@ -2,9 +2,7 @@ import { Wallet, - DollarSign, TrendingUp, - TrendingDown, AlertTriangle, CheckCircle2, Clock, @@ -17,8 +15,6 @@ import { Receipt, Briefcase, AlertCircle, - ArrowUpRight, - ArrowDownRight, Banknote, CircleDollarSign, LayoutGrid, diff --git a/src/components/business/CEODashboard/sections/PurchaseStatusSection.tsx b/src/components/business/CEODashboard/sections/PurchaseStatusSection.tsx index aa276257..b1df2c7d 100644 --- a/src/components/business/CEODashboard/sections/PurchaseStatusSection.tsx +++ b/src/components/business/CEODashboard/sections/PurchaseStatusSection.tsx @@ -134,7 +134,7 @@ export function PurchaseStatusSection({ data }: PurchaseStatusSectionProps) { ))} - [formatKoreanAmount(value ?? 0), '금액']} /> + [formatKoreanAmount(Number(value) || 0), '금액']} /> { + const _handleDismiss = (item: TodayIssueListItem) => { setDismissedIds((prev) => new Set(prev).add(item.id)); toast.success(`"${item.content}" 확인 완료`); }; // 승인 버튼 클릭 - const handleApprove = (item: TodayIssueListItem) => { + const _handleApprove = (item: TodayIssueListItem) => { setDismissedIds((prev) => new Set(prev).add(item.id)); toast.success(`"${item.content}" 승인 처리되었습니다.`); }; // 반려 버튼 클릭 - const handleReject = (item: TodayIssueListItem) => { + const _handleReject = (item: TodayIssueListItem) => { setDismissedIds((prev) => new Set(prev).add(item.id)); toast.error(`"${item.content}" 반려 처리되었습니다.`); }; diff --git a/src/components/business/construction/ConstructionMainDashboard.tsx b/src/components/business/construction/ConstructionMainDashboard.tsx index 0927ffe6..9d30f26d 100644 --- a/src/components/business/construction/ConstructionMainDashboard.tsx +++ b/src/components/business/construction/ConstructionMainDashboard.tsx @@ -1,6 +1,5 @@ 'use client'; -import { useEffect, useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -9,11 +8,9 @@ import { Building2, MapPin, HardHat, - Truck, AlertTriangle, FileText, Calendar as CalendarIcon, - CheckCircle2, Clock, ArrowUpRight, ClipboardList, diff --git a/src/components/business/construction/bidding/BiddingListClient.tsx b/src/components/business/construction/bidding/BiddingListClient.tsx index 8f6a336b..20c7c521 100644 --- a/src/components/business/construction/bidding/BiddingListClient.tsx +++ b/src/components/business/construction/bidding/BiddingListClient.tsx @@ -40,17 +40,17 @@ import { formatDate } from '@/lib/utils/date'; // 테이블 컬럼 정의 const tableColumns = [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'biddingCode', label: '입찰번호', className: 'w-[120px]' }, - { key: 'partnerName', label: '거래처', className: 'w-[100px]' }, - { key: 'projectName', label: '현장명', className: 'min-w-[120px]' }, - { key: 'bidderName', label: '입찰자', className: 'w-[80px]' }, - { key: 'totalCount', label: '총 개소', className: 'w-[80px] text-center' }, - { key: 'biddingAmount', label: '입찰금액', className: 'w-[120px] text-right' }, - { key: 'bidDate', label: '입찰일', className: 'w-[100px] text-center' }, - { key: 'submissionDate', label: '투찰일', className: 'w-[100px] text-center' }, - { key: 'confirmDate', label: '확정일', className: 'w-[100px] text-center' }, + { key: 'biddingCode', label: '입찰번호', className: 'w-[120px]', copyable: true }, + { key: 'partnerName', label: '거래처', className: 'w-[100px]', copyable: true }, + { key: 'projectName', label: '현장명', className: 'min-w-[120px]', copyable: true }, + { key: 'bidderName', label: '입찰자', className: 'w-[80px]', copyable: true }, + { key: 'totalCount', label: '총 개소', className: 'w-[80px] text-center', copyable: true }, + { key: 'biddingAmount', label: '입찰금액', className: 'w-[120px] text-right', copyable: true }, + { key: 'bidDate', label: '입찰일', className: 'w-[100px] text-center', copyable: true }, + { key: 'submissionDate', label: '투찰일', className: 'w-[100px] text-center', copyable: true }, + { key: 'confirmDate', label: '확정일', className: 'w-[100px] text-center', copyable: true }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, - { key: 'remarks', label: '비고', className: 'w-[120px]' }, + { key: 'remarks', label: '비고', className: 'w-[120px]', copyable: true }, { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ]; diff --git a/src/components/business/construction/contract/ContractDetailForm.tsx b/src/components/business/construction/contract/ContractDetailForm.tsx index fec34bf7..32c5cd77 100644 --- a/src/components/business/construction/contract/ContractDetailForm.tsx +++ b/src/components/business/construction/contract/ContractDetailForm.tsx @@ -78,7 +78,7 @@ export default function ContractDetailForm({ const [isContractFileDeleted, setIsContractFileDeleted] = useState(false); // 로딩 상태 - const [isLoading, setIsLoading] = useState(false); + const [, _setIsLoading] = useState(false); // 모달 상태 const [showDocumentModal, setShowDocumentModal] = useState(false); diff --git a/src/components/business/construction/contract/ContractListClient.tsx b/src/components/business/construction/contract/ContractListClient.tsx index b88b90ad..517a2fce 100644 --- a/src/components/business/construction/contract/ContractListClient.tsx +++ b/src/components/business/construction/contract/ContractListClient.tsx @@ -34,19 +34,19 @@ import { } from './types'; import { getContractList, getContractStats, deleteContract, deleteContracts } from './actions'; import { formatNumber } from '@/lib/utils/amount'; -import { formatDate, formatDateRange } from '@/lib/utils/date'; +import { formatDateRange } from '@/lib/utils/date'; // 테이블 컬럼 정의 const tableColumns = [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'contractCode', label: '계약번호', className: 'w-[120px]' }, - { key: 'partnerName', label: '거래처', className: 'w-[100px]' }, - { key: 'projectName', label: '현장명', className: 'min-w-[150px]' }, - { key: 'contractManager', label: '계약담당자', className: 'w-[100px] text-center' }, - { key: 'constructionPM', label: '공사PM', className: 'w-[80px] text-center' }, - { key: 'totalLocations', label: '총 개소', className: 'w-[80px] text-center' }, - { key: 'contractAmount', label: '계약금액', className: 'w-[120px] text-right' }, - { key: 'contractPeriod', label: '계약기간', className: 'w-[180px] text-center' }, + { key: 'contractCode', label: '계약번호', className: 'w-[120px]', copyable: true }, + { key: 'partnerName', label: '거래처', className: 'w-[100px]', copyable: true }, + { key: 'projectName', label: '현장명', className: 'min-w-[150px]', copyable: true }, + { key: 'contractManager', label: '계약담당자', className: 'w-[100px] text-center', copyable: true }, + { key: 'constructionPM', label: '공사PM', className: 'w-[80px] text-center', copyable: true }, + { key: 'totalLocations', label: '총 개소', className: 'w-[80px] text-center', copyable: true }, + { key: 'contractAmount', label: '계약금액', className: 'w-[120px] text-right', copyable: true }, + { key: 'contractPeriod', label: '계약기간', className: 'w-[180px] text-center', copyable: true }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ]; diff --git a/src/components/business/construction/estimates/EstimateListClient.tsx b/src/components/business/construction/estimates/EstimateListClient.tsx index e3ba0dd0..a64e6c38 100644 --- a/src/components/business/construction/estimates/EstimateListClient.tsx +++ b/src/components/business/construction/estimates/EstimateListClient.tsx @@ -39,14 +39,14 @@ import { formatNumber } from '@/lib/utils/amount'; // 테이블 컬럼 정의 const tableColumns = [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'estimateCode', label: '견적번호', className: 'w-[100px]' }, - { key: 'partnerName', label: '거래처', className: 'w-[120px]' }, - { key: 'projectName', label: '현장명', className: 'min-w-[150px]' }, - { key: 'estimatorName', label: '견적자', className: 'w-[80px] text-center' }, - { key: 'itemCount', label: '총 개소', className: 'w-[80px] text-center' }, - { key: 'estimateAmount', label: '견적금액', className: 'w-[120px] text-right' }, - { key: 'completedDate', label: '견적완료일', className: 'w-[110px] text-center' }, - { key: 'bidDate', label: '입찰일', className: 'w-[110px] text-center' }, + { key: 'estimateCode', label: '견적번호', className: 'w-[100px]', copyable: true }, + { key: 'partnerName', label: '거래처', className: 'w-[120px]', copyable: true }, + { key: 'projectName', label: '현장명', className: 'min-w-[150px]', copyable: true }, + { key: 'estimatorName', label: '견적자', className: 'w-[80px] text-center', copyable: true }, + { key: 'itemCount', label: '총 개소', className: 'w-[80px] text-center', copyable: true }, + { key: 'estimateAmount', label: '견적금액', className: 'w-[120px] text-right', copyable: true }, + { key: 'completedDate', label: '견적완료일', className: 'w-[110px] text-center', copyable: true }, + { key: 'bidDate', label: '입찰일', className: 'w-[110px] text-center', copyable: true }, { key: 'status', label: '상태', className: 'w-[100px] text-center' }, { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ]; diff --git a/src/components/business/construction/estimates/actions.ts b/src/components/business/construction/estimates/actions.ts index f0225a42..cda1ad44 100644 --- a/src/components/business/construction/estimates/actions.ts +++ b/src/components/business/construction/estimates/actions.ts @@ -205,14 +205,14 @@ interface ApiSiteBriefing { }; } -interface ApiQuoteStats { +interface _ApiQuoteStats { total_count: number; pending_count: number; completed_count: number; } // Legacy API types (for backward compatibility) -interface ApiSiteBriefingInfo { +interface _ApiSiteBriefingInfo { briefing_code: string; partner_name: string; company_name: string; @@ -220,7 +220,7 @@ interface ApiSiteBriefingInfo { attendee: string; } -interface ApiBidInfo { +interface _ApiBidInfo { project_name: string; bid_date: string; site_count: number; diff --git a/src/components/business/construction/estimates/sections/EstimateDetailTableSection.tsx b/src/components/business/construction/estimates/sections/EstimateDetailTableSection.tsx index 6993184d..7554151d 100644 --- a/src/components/business/construction/estimates/sections/EstimateDetailTableSection.tsx +++ b/src/components/business/construction/estimates/sections/EstimateDetailTableSection.tsx @@ -160,7 +160,7 @@ export function EstimateDetailTableSection({ onSelectItem, onSelectAll, onApplyAdjustedPrice, - onReset, + onReset: _onReset, }: EstimateDetailTableSectionProps) { const [detailAddCount, setDetailAddCount] = useState(1); // API 옵션이 없으면 기본 옵션 사용 diff --git a/src/components/business/construction/estimates/sections/PriceAdjustmentSection.tsx b/src/components/business/construction/estimates/sections/PriceAdjustmentSection.tsx index 8f327cfb..61a2b673 100644 --- a/src/components/business/construction/estimates/sections/PriceAdjustmentSection.tsx +++ b/src/components/business/construction/estimates/sections/PriceAdjustmentSection.tsx @@ -52,7 +52,7 @@ export function PriceAdjustmentSection({ onPriceChange, onSave, onApplyAll, - onReset, + onReset: _onReset, }: PriceAdjustmentSectionProps) { return ( diff --git a/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx b/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx index 379eb6a2..f5bdb4ff 100644 --- a/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx +++ b/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx @@ -73,7 +73,7 @@ export default function HandoverReportDetailForm({ ); // 로딩 상태 - const [isLoading, setIsLoading] = useState(false); + const [, _setIsLoading] = useState(false); // 모달 상태 const [showDocumentModal, setShowDocumentModal] = useState(false); diff --git a/src/components/business/construction/handover-report/HandoverReportListClient.tsx b/src/components/business/construction/handover-report/HandoverReportListClient.tsx index cef9bc42..b51b4318 100644 --- a/src/components/business/construction/handover-report/HandoverReportListClient.tsx +++ b/src/components/business/construction/handover-report/HandoverReportListClient.tsx @@ -40,14 +40,14 @@ import { useDateRange } from '@/hooks'; // 테이블 컬럼 정의 const tableColumns = [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'reportNumber', label: '보고서번호', className: 'w-[100px]' }, - { key: 'partnerName', label: '거래처', className: 'w-[100px]' }, - { key: 'siteName', label: '현장명', className: 'min-w-[100px]' }, - { key: 'contractManager', label: '계약담당자', className: 'w-[100px] text-center' }, - { key: 'constructionPM', label: '공사PM', className: 'w-[80px] text-center' }, - { key: 'totalSites', label: '총 개소', className: 'w-[80px] text-center' }, - { key: 'contractAmount', label: '계약금액(공급가액)', className: 'w-[140px] text-right' }, - { key: 'contractPeriod', label: '계약기간', className: 'w-[180px] text-center' }, + { key: 'reportNumber', label: '보고서번호', className: 'w-[100px]', copyable: true }, + { key: 'partnerName', label: '거래처', className: 'w-[100px]', copyable: true }, + { key: 'siteName', label: '현장명', className: 'min-w-[100px]', copyable: true }, + { key: 'contractManager', label: '계약담당자', className: 'w-[100px] text-center', copyable: true }, + { key: 'constructionPM', label: '공사PM', className: 'w-[80px] text-center', copyable: true }, + { key: 'totalSites', label: '총 개소', className: 'w-[80px] text-center', copyable: true }, + { key: 'contractAmount', label: '계약금액(공급가액)', className: 'w-[140px] text-right', copyable: true }, + { key: 'contractPeriod', label: '계약기간', className: 'w-[180px] text-center', copyable: true }, { key: 'status', label: '상태', className: 'w-[100px] text-center' }, { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ]; diff --git a/src/components/business/construction/handover-report/actions.ts b/src/components/business/construction/handover-report/actions.ts index 4e479227..f18e683f 100644 --- a/src/components/business/construction/handover-report/actions.ts +++ b/src/components/business/construction/handover-report/actions.ts @@ -184,7 +184,7 @@ function transformContractToHandoverReport(apiData: ApiContractWithHandover): Ha /** * API 응답 → HandoverReport 타입 변환 (상세용 - 기존 유지) */ -function transformHandoverReport(apiData: ApiHandoverReport): HandoverReport { +function _transformHandoverReport(apiData: ApiHandoverReport): HandoverReport { return { id: String(apiData.id), reportNumber: apiData.report_number || '', diff --git a/src/components/business/construction/issue-management/IssueDetailForm.tsx b/src/components/business/construction/issue-management/IssueDetailForm.tsx index 0c6d954c..9b1c0fd2 100644 --- a/src/components/business/construction/issue-management/IssueDetailForm.tsx +++ b/src/components/business/construction/issue-management/IssueDetailForm.tsx @@ -66,7 +66,7 @@ export default function IssueDetailForm({ issue, mode = 'view' }: IssueDetailFor images: issue?.images || [], }); - const [isSubmitting, setIsSubmitting] = useState(false); + const [, setIsSubmitting] = useState(false); // 시공번호 변경 시 관련 정보 자동 채움 useEffect(() => { diff --git a/src/components/business/construction/issue-management/IssueManagementListClient.tsx b/src/components/business/construction/issue-management/IssueManagementListClient.tsx index 7ce0a315..68cf3b66 100644 --- a/src/components/business/construction/issue-management/IssueManagementListClient.tsx +++ b/src/components/business/construction/issue-management/IssueManagementListClient.tsx @@ -54,16 +54,16 @@ import { useDateRange } from '@/hooks'; // 테이블 컬럼 정의 const tableColumns = [ { key: 'no', label: '번호', className: 'w-[50px] text-center' }, - { key: 'issueNumber', label: '이슈번호', className: 'w-[120px]' }, - { key: 'constructionNumber', label: '시공번호', className: 'w-[100px]' }, - { key: 'partnerName', label: '거래처', className: 'w-[100px]' }, - { key: 'siteName', label: '현장', className: 'min-w-[120px]' }, - { key: 'category', label: '구분', className: 'w-[80px] text-center' }, - { key: 'title', label: '제목', className: 'min-w-[150px]' }, - { key: 'reporter', label: '보고자', className: 'w-[80px]' }, - { key: 'reportDate', label: '이슈보고일', className: 'w-[100px]' }, - { key: 'resolvedDate', label: '이슈해결일', className: 'w-[100px]' }, - { key: 'assignee', label: '담당자', className: 'w-[80px]' }, + { key: 'issueNumber', label: '이슈번호', className: 'w-[120px]', copyable: true }, + { key: 'constructionNumber', label: '시공번호', className: 'w-[100px]', copyable: true }, + { key: 'partnerName', label: '거래처', className: 'w-[100px]', copyable: true }, + { key: 'siteName', label: '현장', className: 'min-w-[120px]', copyable: true }, + { key: 'category', label: '구분', className: 'w-[80px] text-center', copyable: true }, + { key: 'title', label: '제목', className: 'min-w-[150px]', copyable: true }, + { key: 'reporter', label: '보고자', className: 'w-[80px]', copyable: true }, + { key: 'reportDate', label: '이슈보고일', className: 'w-[100px]', copyable: true }, + { key: 'resolvedDate', label: '이슈해결일', className: 'w-[100px]', copyable: true }, + { key: 'assignee', label: '담당자', className: 'w-[80px]', copyable: true }, { key: 'priority', label: '중요도', className: 'w-[80px] text-center' }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, @@ -317,14 +317,16 @@ export default function IssueManagementListClient({ case 'reportDate': sorted.sort((a, b) => new Date(b.reportDate).getTime() - new Date(a.reportDate).getTime()); break; - case 'priorityHigh': + case 'priorityHigh': { const priorityOrderHigh: Record = { urgent: 0, normal: 1 }; sorted.sort((a, b) => (priorityOrderHigh[a.priority] ?? 2) - (priorityOrderHigh[b.priority] ?? 2)); break; - case 'priorityLow': + } + case 'priorityLow': { const priorityOrderLow: Record = { urgent: 1, normal: 0 }; sorted.sort((a, b) => (priorityOrderLow[a.priority] ?? 2) - (priorityOrderLow[b.priority] ?? 2)); break; + } default: // latest sorted.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); break; diff --git a/src/components/business/construction/issue-management/actions.ts b/src/components/business/construction/issue-management/actions.ts index 7dd8ff71..d1fd1bc9 100644 --- a/src/components/business/construction/issue-management/actions.ts +++ b/src/components/business/construction/issue-management/actions.ts @@ -277,14 +277,16 @@ export async function getIssueList( case 'reportDate': filtered.sort((a, b) => new Date(b.reportDate).getTime() - new Date(a.reportDate).getTime()); break; - case 'priorityHigh': + case 'priorityHigh': { const priorityOrder: Record = { urgent: 0, normal: 1 }; filtered.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]); break; - case 'priorityLow': + } + case 'priorityLow': { const priorityOrderLow: Record = { urgent: 1, normal: 0 }; filtered.sort((a, b) => priorityOrderLow[a.priority] - priorityOrderLow[b.priority]); break; + } } } @@ -356,16 +358,11 @@ export async function getIssue( // 이슈 수정 export async function updateIssue( - id: string, - data: Partial + _id: string, + _data: Partial ): Promise<{ success: boolean; error?: string }> { - try { - // 실제 구현에서는 DB 업데이트 - return { success: true }; - } catch (error) { - console.error('updateIssue error:', error); - return { success: false, error: '이슈 수정에 실패했습니다.' }; - } + // 실제 구현에서는 DB 업데이트 + return { success: true }; } // 이슈 생성 @@ -388,26 +385,16 @@ export async function createIssue( // 이슈 철회 (단일) export async function withdrawIssue( - id: string + _id: string ): Promise<{ success: boolean; error?: string }> { - try { - // 실제 구현에서는 DB 상태 업데이트 (삭제가 아닌 철회 상태로 변경) - return { success: true }; - } catch (error) { - console.error('withdrawIssue error:', error); - return { success: false, error: '이슈 철회에 실패했습니다.' }; - } + // 실제 구현에서는 DB 상태 업데이트 (삭제가 아닌 철회 상태로 변경) + return { success: true }; } // 이슈 철회 (다중) export async function withdrawIssues( - ids: string[] + _ids: string[] ): Promise<{ success: boolean; error?: string }> { - try { - // 실제 구현에서는 DB 상태 일괄 업데이트 - return { success: true }; - } catch (error) { - console.error('withdrawIssues error:', error); - return { success: false, error: '이슈 일괄 철회에 실패했습니다.' }; - } + // 실제 구현에서는 DB 상태 일괄 업데이트 + return { success: true }; } \ No newline at end of file diff --git a/src/components/business/construction/item-management/ItemManagementClient.tsx b/src/components/business/construction/item-management/ItemManagementClient.tsx index 13e0e5d1..0031d0f3 100644 --- a/src/components/business/construction/item-management/ItemManagementClient.tsx +++ b/src/components/business/construction/item-management/ItemManagementClient.tsx @@ -259,7 +259,7 @@ export default function ItemManagementClient({ } }, [deleteTargetId]); - const handleBulkDeleteClick = useCallback(() => { + const _handleBulkDeleteClick = useCallback(() => { if (selectedItems.size === 0) { toast.warning('삭제할 항목을 선택해주세요.'); return; diff --git a/src/components/business/construction/item-management/constants.ts b/src/components/business/construction/item-management/constants.ts index a3fe2e47..cc30a42d 100644 --- a/src/components/business/construction/item-management/constants.ts +++ b/src/components/business/construction/item-management/constants.ts @@ -1,6 +1,6 @@ // 품목관리 상수 정의 -import type { ItemType, Specification, OrderType, ItemStatus } from './types'; +import type { ItemType, OrderType, ItemStatus } from './types'; // 물품유형 옵션 export const ITEM_TYPE_OPTIONS: { value: ItemType | 'all'; label: string }[] = [ diff --git a/src/components/business/construction/labor-management/LaborManagementClient.tsx b/src/components/business/construction/labor-management/LaborManagementClient.tsx index 1141a6ae..fe61bf6e 100644 --- a/src/components/business/construction/labor-management/LaborManagementClient.tsx +++ b/src/components/business/construction/labor-management/LaborManagementClient.tsx @@ -27,7 +27,7 @@ import { type RowClickHandlers, type StatCard, } from '@/components/templates/UniversalListPage'; -import type { Labor, LaborStats, LaborCategory, LaborStatus } from './types'; +import type { Labor, LaborStats } from './types'; import { CATEGORY_OPTIONS, STATUS_OPTIONS, SORT_OPTIONS, DEFAULT_PAGE_SIZE } from './constants'; import { getLaborList, deleteLabor, deleteLaborBulk, getLaborStats } from './actions'; import { formatNumber } from '@/lib/utils/amount'; @@ -35,11 +35,11 @@ import { formatNumber } from '@/lib/utils/amount'; // 테이블 컬럼 정의 const tableColumns = [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'laborNumber', label: '노임번호', className: 'w-[120px]' }, - { key: 'category', label: '구분', className: 'w-[100px] text-center' }, - { key: 'minM', label: '최소 M', className: 'w-[100px] text-right' }, - { key: 'maxM', label: '최대 M', className: 'w-[100px] text-right' }, - { key: 'laborPrice', label: '노임단가', className: 'w-[120px] text-right' }, + { key: 'laborNumber', label: '노임번호', className: 'w-[120px]', copyable: true }, + { key: 'category', label: '구분', className: 'w-[100px] text-center', copyable: true }, + { key: 'minM', label: '최소 M', className: 'w-[100px] text-right', copyable: true }, + { key: 'maxM', label: '최대 M', className: 'w-[100px] text-right', copyable: true }, + { key: 'laborPrice', label: '노임단가', className: 'w-[120px] text-right', copyable: true }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, { key: 'actions', label: '작업', className: 'w-[100px] text-center' }, ]; diff --git a/src/components/business/construction/management/ConstructionDetailClient.tsx b/src/components/business/construction/management/ConstructionDetailClient.tsx index 02df109a..e9856d99 100644 --- a/src/components/business/construction/management/ConstructionDetailClient.tsx +++ b/src/components/business/construction/management/ConstructionDetailClient.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect, useCallback, useMemo } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { invalidateDashboard } from '@/lib/dashboard-invalidation'; import dynamic from 'next/dynamic'; import { useRouter } from 'next/navigation'; diff --git a/src/components/business/construction/management/ConstructionManagementListClient.tsx b/src/components/business/construction/management/ConstructionManagementListClient.tsx index bf82a82f..2e607da7 100644 --- a/src/components/business/construction/management/ConstructionManagementListClient.tsx +++ b/src/components/business/construction/management/ConstructionManagementListClient.tsx @@ -57,14 +57,14 @@ import { useDateRange } from '@/hooks'; // 테이블 컬럼 정의 const tableColumns = [ { key: 'no', label: '번호', className: 'w-[50px] text-center' }, - { key: 'constructionNumber', label: '시공번호', className: 'w-[100px]' }, - { key: 'partnerName', label: '거래처', className: 'w-[100px]' }, - { key: 'siteName', label: '현장명', className: 'min-w-[120px]' }, - { key: 'constructionPM', label: '공사PM', className: 'w-[80px]' }, - { key: 'workTeamLeader', label: '작업반장', className: 'w-[80px]' }, - { key: 'worker', label: '작업자', className: 'w-[80px]' }, - { key: 'constructionStartDate', label: '시공투입일', className: 'w-[100px]' }, - { key: 'constructionEndDate', label: '시공완료일', className: 'w-[100px]' }, + { key: 'constructionNumber', label: '시공번호', className: 'w-[100px]', copyable: true }, + { key: 'partnerName', label: '거래처', className: 'w-[100px]', copyable: true }, + { key: 'siteName', label: '현장명', className: 'min-w-[120px]', copyable: true }, + { key: 'constructionPM', label: '공사PM', className: 'w-[80px]', copyable: true }, + { key: 'workTeamLeader', label: '작업반장', className: 'w-[80px]', copyable: true }, + { key: 'worker', label: '작업자', className: 'w-[80px]', copyable: true }, + { key: 'constructionStartDate', label: '시공투입일', className: 'w-[100px]', copyable: true }, + { key: 'constructionEndDate', label: '시공완료일', className: 'w-[100px]', copyable: true }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ]; diff --git a/src/components/business/construction/management/ProjectDetailClient.tsx b/src/components/business/construction/management/ProjectDetailClient.tsx index 88dc56d0..69bf06a5 100644 --- a/src/components/business/construction/management/ProjectDetailClient.tsx +++ b/src/components/business/construction/management/ProjectDetailClient.tsx @@ -18,7 +18,7 @@ interface ProjectDetailClientProps { projectId?: string; } -export default function ProjectDetailClient({ projectId }: ProjectDetailClientProps) { +export default function ProjectDetailClient({ projectId: _projectId }: ProjectDetailClientProps) { // 데이터 상태 const [projects, setProjects] = useState([]); const [stats, setStats] = useState({ total: 0, inProgress: 0, completed: 0 }); diff --git a/src/components/business/construction/management/ProjectKanbanBoard.tsx b/src/components/business/construction/management/ProjectKanbanBoard.tsx index e8321b36..018fa1b7 100644 --- a/src/components/business/construction/management/ProjectKanbanBoard.tsx +++ b/src/components/business/construction/management/ProjectKanbanBoard.tsx @@ -14,7 +14,6 @@ import ProjectCard from './ProjectCard'; import StageCard from './StageCard'; import DetailAccordion from './DetailAccordion'; import type { ProjectDetail, Stage, DetailCategory, SelectOption } from './types'; -import { STAGE_LABELS } from './types'; import { getDetailCategories } from './actions'; interface ProjectKanbanBoardProps { @@ -71,7 +70,7 @@ export default function ProjectKanbanBoard({ }, [selectedProject]); // 선택된 단계 정보 - const selectedStage = useMemo(() => { + const _selectedStage = useMemo(() => { return stages.find((s) => s.id === selectedStageId) || null; }, [stages, selectedStageId]); diff --git a/src/components/business/construction/management/ProjectListClient.tsx b/src/components/business/construction/management/ProjectListClient.tsx index 2a07e5da..b4ab8272 100644 --- a/src/components/business/construction/management/ProjectListClient.tsx +++ b/src/components/business/construction/management/ProjectListClient.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useMemo, useCallback, useEffect, Fragment } from 'react'; +import { useState, useMemo, useCallback, useEffect } from 'react'; import { FolderKanban, ClipboardList, PlayCircle, CheckCircle2 } from 'lucide-react'; import { useListHandlers } from '@/hooks/useListHandlers'; import { Button } from '@/components/ui/button'; @@ -20,7 +20,6 @@ import { PageHeader } from '@/components/organisms/PageHeader'; import { MobileCard } from '@/components/organisms/MobileCard'; import { formatAmountWon as formatAmount } from '@/lib/utils/amount'; import { toast } from 'sonner'; -import { cn } from '@/lib/utils'; import type { Project, ProjectStats, ChartViewMode, SelectOption } from './types'; import { STATUS_OPTIONS, SORT_OPTIONS } from './types'; import { DateRangeSelector } from '@/components/molecules/DateRangeSelector'; @@ -37,7 +36,7 @@ import ProjectGanttChart from './ProjectGanttChart'; // 다중 선택 셀렉트 컴포넌트 function MultiSelectFilter({ - label, + label: _label, options, value, onChange, @@ -110,14 +109,14 @@ interface ProjectListClientProps { export default function ProjectListClient({ initialData = [], initialStats }: ProjectListClientProps) { // ===== 공통 핸들러 Hook ===== - const { handleRowClick, router } = useListHandlers('construction/project/execution-management'); + const { handleRowClick, router: _router } = useListHandlers('construction/project/execution-management'); // 상태 const [projects, setProjects] = useState(initialData); const [stats, setStats] = useState( initialStats ?? { total: 0, inProgress: 0, completed: 0 } ); - const [isLoading, setIsLoading] = useState(false); + const [, setIsLoading] = useState(false); // 날짜 범위 (기간 선택) const [filterStartDate, setFilterStartDate] = useState(() => format(startOfMonth(new Date()), 'yyyy-MM-dd')); @@ -543,7 +542,7 @@ export default function ProjectListClient({ initialData = [], initialStats }: Pr 검색 결과가 없습니다. ) : ( - projects.map((project, index) => { + projects.map((project, _index) => { const isSelected = selectedItems.has(project.id); return ( { - try { - return { success: true }; - } catch (error) { - console.error('deleteConstructionManagement error:', error); - return { success: false, error: '시공 정보 삭제에 실패했습니다.' }; - } + return { success: true }; } // 시공관리 일괄 삭제 @@ -1063,9 +1056,6 @@ export async function deleteConstructionManagements( import type { ConstructionManagementDetail, ConstructionDetailFormData, - WorkerInfo, - WorkProgressInfo, - PhotoInfo, } from './types'; // 시공 상세 목업 데이터 @@ -1191,27 +1181,17 @@ export async function getConstructionManagementDetail( // 시공 상세 수정 export async function updateConstructionManagementDetail( - id: string, - data: Partial + _id: string, + _data: Partial ): Promise<{ success: boolean; error?: string }> { - try { - // 실제 구현에서는 DB 업데이트 - return { success: true }; - } catch (error) { - console.error('updateConstructionManagementDetail error:', error); - return { success: false, error: '시공 상세 수정에 실패했습니다.' }; - } + // 실제 구현에서는 DB 업데이트 + return { success: true }; } // 시공 완료 처리 export async function completeConstruction( - id: string + _id: string ): Promise<{ success: boolean; error?: string }> { - try { - // 실제 구현에서는 상태를 completed로 변경 - return { success: true }; - } catch (error) { - console.error('completeConstruction error:', error); - return { success: false, error: '시공 완료 처리에 실패했습니다.' }; - } + // 실제 구현에서는 상태를 completed로 변경 + return { success: true }; } \ No newline at end of file diff --git a/src/components/business/construction/order-management/OrderManagementListClient.tsx b/src/components/business/construction/order-management/OrderManagementListClient.tsx index 4ac69ed9..69be09b2 100644 --- a/src/components/business/construction/order-management/OrderManagementListClient.tsx +++ b/src/components/business/construction/order-management/OrderManagementListClient.tsx @@ -48,7 +48,6 @@ import { import { formatDate } from '@/lib/utils/date'; import { getOrderList, - getOrderStats, deleteOrder, deleteOrders, } from './actions'; @@ -57,22 +56,22 @@ import { useDateRange } from '@/hooks'; // 테이블 컬럼 정의 const tableColumns = [ { key: 'no', label: '번호', className: 'w-[50px] text-center' }, - { key: 'contractNumber', label: '계약번호', className: 'w-[100px]' }, - { key: 'partnerName', label: '거래처', className: 'w-[80px]' }, - { key: 'siteName', label: '현장명', className: 'min-w-[100px]' }, - { key: 'name', label: '명칭', className: 'w-[80px]' }, - { key: 'constructionPM', label: '공사PM', className: 'w-[70px]' }, - { key: 'orderManager', label: '발주담당자', className: 'w-[80px]' }, - { key: 'orderNumber', label: '발주번호', className: 'w-[100px]' }, - { key: 'orderCompany', label: '발주처명', className: 'w-[80px]' }, - { key: 'workTeamLeader', label: '작업반장', className: 'w-[70px]' }, - { key: 'constructionStartDate', label: '시공투입일', className: 'w-[90px]' }, - { key: 'orderType', label: '구분', className: 'w-[80px] text-center' }, - { key: 'item', label: '품목', className: 'w-[80px]' }, - { key: 'quantity', label: '수량', className: 'w-[60px] text-right' }, - { key: 'orderDate', label: '발주일', className: 'w-[90px]' }, - { key: 'plannedDeliveryDate', label: '계획인수일', className: 'w-[90px]' }, - { key: 'actualDeliveryDate', label: '실제인수일', className: 'w-[90px]' }, + { key: 'contractNumber', label: '계약번호', className: 'w-[100px]', copyable: true }, + { key: 'partnerName', label: '거래처', className: 'w-[80px]', copyable: true }, + { key: 'siteName', label: '현장명', className: 'min-w-[100px]', copyable: true }, + { key: 'name', label: '명칭', className: 'w-[80px]', copyable: true }, + { key: 'constructionPM', label: '공사PM', className: 'w-[70px]', copyable: true }, + { key: 'orderManager', label: '발주담당자', className: 'w-[80px]', copyable: true }, + { key: 'orderNumber', label: '발주번호', className: 'w-[100px]', copyable: true }, + { key: 'orderCompany', label: '발주처명', className: 'w-[80px]', copyable: true }, + { key: 'workTeamLeader', label: '작업반장', className: 'w-[70px]', copyable: true }, + { key: 'constructionStartDate', label: '시공투입일', className: 'w-[90px]', copyable: true }, + { key: 'orderType', label: '구분', className: 'w-[80px] text-center', copyable: true }, + { key: 'item', label: '품목', className: 'w-[80px]', copyable: true }, + { key: 'quantity', label: '수량', className: 'w-[60px] text-right', copyable: true }, + { key: 'orderDate', label: '발주일', className: 'w-[90px]', copyable: true }, + { key: 'plannedDeliveryDate', label: '계획인수일', className: 'w-[90px]', copyable: true }, + { key: 'actualDeliveryDate', label: '실제인수일', className: 'w-[90px]', copyable: true }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ]; @@ -84,7 +83,7 @@ interface OrderManagementListClientProps { export default function OrderManagementListClient({ initialData = [], - initialStats, + initialStats: _initialStats, }: OrderManagementListClientProps) { // ===== 공통 핸들러 Hook ===== const { handleRowClick, handleEdit, router } = useListHandlers( diff --git a/src/components/business/construction/order-management/OrderManagementUnified.tsx b/src/components/business/construction/order-management/OrderManagementUnified.tsx index 7b2a7ac3..dba645b7 100644 --- a/src/components/business/construction/order-management/OrderManagementUnified.tsx +++ b/src/components/business/construction/order-management/OrderManagementUnified.tsx @@ -223,22 +223,22 @@ export function OrderManagementUnified({ initialData }: OrderManagementUnifiedPr // ===== 테이블 컬럼 ===== columns: [ { key: 'no', label: '번호', className: 'w-[50px] text-center' }, - { key: 'contractNumber', label: '계약번호', className: 'w-[100px]' }, - { key: 'partnerName', label: '거래처', className: 'w-[80px]' }, - { key: 'siteName', label: '현장명', className: 'min-w-[100px]' }, - { key: 'name', label: '명칭', className: 'w-[80px]' }, - { key: 'constructionPM', label: '공사PM', className: 'w-[70px]' }, - { key: 'orderManager', label: '발주담당자', className: 'w-[80px]' }, - { key: 'orderNumber', label: '발주번호', className: 'w-[100px]' }, - { key: 'orderCompany', label: '발주처명', className: 'w-[80px]' }, - { key: 'workTeamLeader', label: '작업반장', className: 'w-[70px]' }, - { key: 'constructionStartDate', label: '시공투입일', className: 'w-[90px]' }, - { key: 'orderType', label: '구분', className: 'w-[80px] text-center' }, - { key: 'item', label: '품목', className: 'w-[80px]' }, - { key: 'quantity', label: '수량', className: 'w-[60px] text-right' }, - { key: 'orderDate', label: '발주일', className: 'w-[90px]' }, - { key: 'plannedDeliveryDate', label: '계획인수일', className: 'w-[90px]' }, - { key: 'actualDeliveryDate', label: '실제인수일', className: 'w-[90px]' }, + { key: 'contractNumber', label: '계약번호', className: 'w-[100px]', copyable: true }, + { key: 'partnerName', label: '거래처', className: 'w-[80px]', copyable: true }, + { key: 'siteName', label: '현장명', className: 'min-w-[100px]', copyable: true }, + { key: 'name', label: '명칭', className: 'w-[80px]', copyable: true }, + { key: 'constructionPM', label: '공사PM', className: 'w-[70px]', copyable: true }, + { key: 'orderManager', label: '발주담당자', className: 'w-[80px]', copyable: true }, + { key: 'orderNumber', label: '발주번호', className: 'w-[100px]', copyable: true }, + { key: 'orderCompany', label: '발주처명', className: 'w-[80px]', copyable: true }, + { key: 'workTeamLeader', label: '작업반장', className: 'w-[70px]', copyable: true }, + { key: 'constructionStartDate', label: '시공투입일', className: 'w-[90px]', copyable: true }, + { key: 'orderType', label: '구분', className: 'w-[80px] text-center', copyable: true }, + { key: 'item', label: '품목', className: 'w-[80px]', copyable: true }, + { key: 'quantity', label: '수량', className: 'w-[60px] text-right', copyable: true }, + { key: 'orderDate', label: '발주일', className: 'w-[90px]', copyable: true }, + { key: 'plannedDeliveryDate', label: '계획인수일', className: 'w-[90px]', copyable: true }, + { key: 'actualDeliveryDate', label: '실제인수일', className: 'w-[90px]', copyable: true }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ], diff --git a/src/components/business/construction/order-management/cards/ConstructionDetailCard.tsx b/src/components/business/construction/order-management/cards/ConstructionDetailCard.tsx index e08aa789..96098a0e 100644 --- a/src/components/business/construction/order-management/cards/ConstructionDetailCard.tsx +++ b/src/components/business/construction/order-management/cards/ConstructionDetailCard.tsx @@ -1,7 +1,6 @@ 'use client'; import { Label } from '@/components/ui/label'; -import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { diff --git a/src/components/business/construction/order-management/hooks/useOrderDetailForm.ts b/src/components/business/construction/order-management/hooks/useOrderDetailForm.ts index d9ca5705..1184ee61 100644 --- a/src/components/business/construction/order-management/hooks/useOrderDetailForm.ts +++ b/src/components/business/construction/order-management/hooks/useOrderDetailForm.ts @@ -8,13 +8,8 @@ import type { OrderDetailFormData, OrderDetailItem, OrderDetailCategory, - OrderStatus, - OrderType, } from '../types'; import { - MOCK_PARTNERS, - MOCK_CONSTRUCTION_PM, - MOCK_CATEGORIES, getEmptyOrderDetailItem, getEmptyOrderDetailCategory, getEmptyOrderDetailFormData, @@ -144,7 +139,7 @@ export function useOrderDetailForm({ const [addCounts, setAddCounts] = useState>(new Map()); // Category filters - const [categoryFilters, setCategoryFilters] = useState>(new Map()); + const [categoryFilters, _setCategoryFilters] = useState>(new Map()); // Calendar states const [calendarDate, setCalendarDate] = useState(new Date()); diff --git a/src/components/business/construction/order-management/tables/OrderDetailItemTable.tsx b/src/components/business/construction/order-management/tables/OrderDetailItemTable.tsx index f8de7847..c74b5987 100644 --- a/src/components/business/construction/order-management/tables/OrderDetailItemTable.tsx +++ b/src/components/business/construction/order-management/tables/OrderDetailItemTable.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Plus, Trash2, Image as ImageIcon } from 'lucide-react'; +import { Trash2, Image as ImageIcon } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; @@ -53,7 +53,7 @@ interface OrderDetailItemTableProps { export function OrderDetailItemTable({ category, isEditMode, - isViewMode, + isViewMode: _isViewMode, selectedItems, addCount, onAddCountChange, diff --git a/src/components/business/construction/partners/PartnerListClient.tsx b/src/components/business/construction/partners/PartnerListClient.tsx index b8222c08..4c8741a1 100644 --- a/src/components/business/construction/partners/PartnerListClient.tsx +++ b/src/components/business/construction/partners/PartnerListClient.tsx @@ -32,13 +32,13 @@ import { getPartnerList, deletePartner, deletePartners, getPartnerStats } from ' // 테이블 컬럼 정의 const tableColumns = [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'partnerCode', label: '거래처번호', className: 'w-[100px]' }, - { key: 'category', label: '구분', className: 'w-[80px] text-center' }, - { key: 'partnerName', label: '거래처명', className: 'min-w-[150px]' }, - { key: 'representative', label: '대표자', className: 'w-[100px]' }, - { key: 'manager', label: '담당자', className: 'w-[100px]' }, - { key: 'phone', label: '전화번호', className: 'w-[130px]' }, - { key: 'paymentDay', label: '매출 결제일', className: 'w-[100px] text-center' }, + { key: 'partnerCode', label: '거래처번호', className: 'w-[100px]', copyable: true }, + { key: 'category', label: '구분', className: 'w-[80px] text-center', copyable: true }, + { key: 'partnerName', label: '거래처명', className: 'min-w-[150px]', copyable: true }, + { key: 'representative', label: '대표자', className: 'w-[100px]', copyable: true }, + { key: 'manager', label: '담당자', className: 'w-[100px]', copyable: true }, + { key: 'phone', label: '전화번호', className: 'w-[130px]', copyable: true }, + { key: 'paymentDay', label: '매출 결제일', className: 'w-[100px] text-center', copyable: true }, { key: 'isBadDebt', label: '악성채권', className: 'w-[90px] text-center' }, { key: 'actions', label: '작업', className: 'w-[100px] text-center' }, ]; diff --git a/src/components/business/construction/progress-billing/ProgressBillingManagementListClient.tsx b/src/components/business/construction/progress-billing/ProgressBillingManagementListClient.tsx index b339c727..9c392daf 100644 --- a/src/components/business/construction/progress-billing/ProgressBillingManagementListClient.tsx +++ b/src/components/business/construction/progress-billing/ProgressBillingManagementListClient.tsx @@ -44,14 +44,14 @@ import { useDateRange } from '@/hooks'; // 테이블 컬럼 정의 const tableColumns = [ { key: 'no', label: '번호', className: 'w-[50px] text-center' }, - { key: 'billingNumber', label: '기성청구번호', className: 'w-[140px]' }, - { key: 'partnerName', label: '거래처', className: 'w-[120px]' }, - { key: 'siteName', label: '현장명', className: 'min-w-[150px]' }, - { key: 'round', label: '회차', className: 'w-[60px] text-center' }, - { key: 'billingYearMonth', label: '기성청구연월', className: 'w-[110px] text-center' }, - { key: 'previousBilling', label: '전회기성', className: 'w-[120px] text-right' }, - { key: 'currentBilling', label: '금회기성', className: 'w-[120px] text-right' }, - { key: 'cumulativeBilling', label: '누계기성', className: 'w-[120px] text-right' }, + { key: 'billingNumber', label: '기성청구번호', className: 'w-[140px]', copyable: true }, + { key: 'partnerName', label: '거래처', className: 'w-[120px]', copyable: true }, + { key: 'siteName', label: '현장명', className: 'min-w-[150px]', copyable: true }, + { key: 'round', label: '회차', className: 'w-[60px] text-center', copyable: true }, + { key: 'billingYearMonth', label: '기성청구연월', className: 'w-[110px] text-center', copyable: true }, + { key: 'previousBilling', label: '전회기성', className: 'w-[120px] text-right', copyable: true }, + { key: 'currentBilling', label: '금회기성', className: 'w-[120px] text-right', copyable: true }, + { key: 'cumulativeBilling', label: '누계기성', className: 'w-[120px] text-right', copyable: true }, { key: 'status', label: '상태', className: 'w-[100px] text-center' }, { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ]; diff --git a/src/components/business/construction/progress-billing/actions.ts b/src/components/business/construction/progress-billing/actions.ts index 0578fc0a..ab062e05 100644 --- a/src/components/business/construction/progress-billing/actions.ts +++ b/src/components/business/construction/progress-billing/actions.ts @@ -255,7 +255,7 @@ export async function saveProgressBilling( /** * 기성청구 삭제 */ -export async function deleteProgressBilling(id: string): Promise<{ +export async function deleteProgressBilling(_id: string): Promise<{ success: boolean; error?: string; }> { diff --git a/src/components/business/construction/progress-billing/hooks/useProgressBillingDetailForm.ts b/src/components/business/construction/progress-billing/hooks/useProgressBillingDetailForm.ts index 31ba87b5..8bfc9ed1 100644 --- a/src/components/business/construction/progress-billing/hooks/useProgressBillingDetailForm.ts +++ b/src/components/business/construction/progress-billing/hooks/useProgressBillingDetailForm.ts @@ -9,7 +9,6 @@ import type { } from '../types'; import { progressBillingDetailToFormData, - getEmptyProgressBillingDetailFormData, MOCK_PROGRESS_BILLING_DETAIL, } from '../types'; diff --git a/src/components/business/construction/site-briefings/SiteBriefingForm.tsx b/src/components/business/construction/site-briefings/SiteBriefingForm.tsx index d482e41a..620b57b9 100644 --- a/src/components/business/construction/site-briefings/SiteBriefingForm.tsx +++ b/src/components/business/construction/site-briefings/SiteBriefingForm.tsx @@ -109,7 +109,7 @@ export default function SiteBriefingForm({ mode, briefingId, initialData }: Site }); // 로딩 상태 - const [isLoading, setIsLoading] = useState(false); + const [, _setIsLoading] = useState(false); // 다이얼로그 상태 (현장 신규 등록은 별도로 관리) diff --git a/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx b/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx index 7e02be93..5ba3ff03 100644 --- a/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx +++ b/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx @@ -29,13 +29,13 @@ import { getSiteBriefingList, deleteSiteBriefing, deleteSiteBriefings } from './ // 테이블 컬럼 정의 const tableColumns = [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'briefingCode', label: '현설번호', className: 'w-[100px]' }, - { key: 'partnerName', label: '거래처', className: 'w-[120px]' }, - { key: 'projectName', label: '현장명', className: 'min-w-[150px]' }, - { key: 'briefingDate', label: '현장설명회일', className: 'w-[120px] text-center' }, - { key: 'briefingType', label: '구분', className: 'w-[80px] text-center' }, - { key: 'attendee', label: '참석자', className: 'w-[100px] text-center' }, - { key: 'bidDate', label: '입찰일', className: 'w-[120px] text-center' }, + { key: 'briefingCode', label: '현설번호', className: 'w-[100px]', copyable: true }, + { key: 'partnerName', label: '거래처', className: 'w-[120px]', copyable: true }, + { key: 'projectName', label: '현장명', className: 'min-w-[150px]', copyable: true }, + { key: 'briefingDate', label: '현장설명회일', className: 'w-[120px] text-center', copyable: true }, + { key: 'briefingType', label: '구분', className: 'w-[80px] text-center', copyable: true }, + { key: 'attendee', label: '참석자', className: 'w-[100px] text-center', copyable: true }, + { key: 'bidDate', label: '입찰일', className: 'w-[120px] text-center', copyable: true }, { key: 'status', label: '상태', className: 'w-[100px] text-center' }, { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ]; diff --git a/src/components/business/construction/site-management/SiteManagementListClient.tsx b/src/components/business/construction/site-management/SiteManagementListClient.tsx index 05668a79..0f09f10b 100644 --- a/src/components/business/construction/site-management/SiteManagementListClient.tsx +++ b/src/components/business/construction/site-management/SiteManagementListClient.tsx @@ -38,10 +38,10 @@ import { useDateRange } from '@/hooks'; // 테이블 컬럼 정의 const tableColumns = [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'siteCode', label: '현장번호', className: 'w-[120px]' }, - { key: 'partnerName', label: '거래처', className: 'w-[100px]' }, - { key: 'siteName', label: '현장명', className: 'min-w-[150px]' }, - { key: 'address', label: '위치', className: 'min-w-[200px]' }, + { key: 'siteCode', label: '현장번호', className: 'w-[120px]', copyable: true }, + { key: 'partnerName', label: '거래처', className: 'w-[100px]', copyable: true }, + { key: 'siteName', label: '현장명', className: 'min-w-[150px]', copyable: true }, + { key: 'address', label: '위치', className: 'min-w-[200px]', copyable: true }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ]; diff --git a/src/components/business/construction/structure-review/StructureReviewDetailForm.tsx b/src/components/business/construction/structure-review/StructureReviewDetailForm.tsx index d438c7d3..cfa1e783 100644 --- a/src/components/business/construction/structure-review/StructureReviewDetailForm.tsx +++ b/src/components/business/construction/structure-review/StructureReviewDetailForm.tsx @@ -9,7 +9,6 @@ import { useState, useCallback, useMemo } from 'react'; import { useRouter } from 'next/navigation'; import { FileDropzone } from '@/components/ui/file-dropzone'; import { FileList, type NewFile, type ExistingFile } from '@/components/ui/file-list'; -import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; import { Label } from '@/components/ui/label'; diff --git a/src/components/business/construction/structure-review/StructureReviewListClient.tsx b/src/components/business/construction/structure-review/StructureReviewListClient.tsx index 98f9c0b3..ab230eac 100644 --- a/src/components/business/construction/structure-review/StructureReviewListClient.tsx +++ b/src/components/business/construction/structure-review/StructureReviewListClient.tsx @@ -44,13 +44,13 @@ import { useDateRange } from '@/hooks'; // 테이블 컬럼 정의 const tableColumns = [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'reviewNumber', label: '검토번호', className: 'w-[100px]' }, - { key: 'partnerName', label: '거래처', className: 'w-[100px]' }, - { key: 'siteName', label: '현장명', className: 'min-w-[120px]' }, - { key: 'requestDate', label: '구조검토 의뢰일', className: 'w-[120px]' }, - { key: 'reviewCompany', label: '검토회사', className: 'w-[100px]' }, - { key: 'reviewerName', label: '검토자', className: 'w-[80px]' }, - { key: 'completionDate', label: '구조검토완료일', className: 'w-[120px]' }, + { key: 'reviewNumber', label: '검토번호', className: 'w-[100px]', copyable: true }, + { key: 'partnerName', label: '거래처', className: 'w-[100px]', copyable: true }, + { key: 'siteName', label: '현장명', className: 'min-w-[120px]', copyable: true }, + { key: 'requestDate', label: '구조검토 의뢰일', className: 'w-[120px]', copyable: true }, + { key: 'reviewCompany', label: '검토회사', className: 'w-[100px]', copyable: true }, + { key: 'reviewerName', label: '검토자', className: 'w-[80px]', copyable: true }, + { key: 'completionDate', label: '구조검토완료일', className: 'w-[120px]', copyable: true }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, { key: 'actions', label: '작업', className: 'w-[100px] text-center' }, ]; diff --git a/src/components/business/construction/utility-management/UtilityManagementListClient.tsx b/src/components/business/construction/utility-management/UtilityManagementListClient.tsx index eb616245..b89bb0c6 100644 --- a/src/components/business/construction/utility-management/UtilityManagementListClient.tsx +++ b/src/components/business/construction/utility-management/UtilityManagementListClient.tsx @@ -49,15 +49,15 @@ import { useDateRange } from '@/hooks'; // 테이블 컬럼 정의 const tableColumns = [ { key: 'no', label: '번호', className: 'w-[50px] text-center' }, - { key: 'utilityNumber', label: '공과번호', className: 'w-[120px]' }, - { key: 'partnerName', label: '거래처', className: 'w-[100px]' }, - { key: 'siteName', label: '현장명', className: 'min-w-[120px]' }, - { key: 'constructionPM', label: '공사PM', className: 'w-[80px]' }, - { key: 'utilityType', label: '공과', className: 'w-[80px]' }, - { key: 'scheduledDate', label: '공과예정일시', className: 'w-[110px]' }, - { key: 'amount', label: '금액', className: 'w-[100px] text-right' }, - { key: 'workTeamLeader', label: '작업반장', className: 'w-[80px]' }, - { key: 'constructionStartDate', label: '시공투입일', className: 'w-[100px]' }, + { key: 'utilityNumber', label: '공과번호', className: 'w-[120px]', copyable: true }, + { key: 'partnerName', label: '거래처', className: 'w-[100px]', copyable: true }, + { key: 'siteName', label: '현장명', className: 'min-w-[120px]', copyable: true }, + { key: 'constructionPM', label: '공사PM', className: 'w-[80px]', copyable: true }, + { key: 'utilityType', label: '공과', className: 'w-[80px]', copyable: true }, + { key: 'scheduledDate', label: '공과예정일시', className: 'w-[110px]', copyable: true }, + { key: 'amount', label: '금액', className: 'w-[100px] text-right', copyable: true }, + { key: 'workTeamLeader', label: '작업반장', className: 'w-[80px]', copyable: true }, + { key: 'constructionStartDate', label: '시공투입일', className: 'w-[100px]', copyable: true }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, { key: 'actions', label: '작업', className: 'w-[100px] text-center' }, ]; diff --git a/src/components/business/construction/worker-status/WorkerStatusListClient.tsx b/src/components/business/construction/worker-status/WorkerStatusListClient.tsx index f5312fae..8a708c01 100644 --- a/src/components/business/construction/worker-status/WorkerStatusListClient.tsx +++ b/src/components/business/construction/worker-status/WorkerStatusListClient.tsx @@ -44,16 +44,16 @@ import { useDateRange } from '@/hooks'; // 테이블 컬럼 정의 const tableColumns = [ { key: 'no', label: '번호', className: 'w-[50px] text-center' }, - { key: 'partnerName', label: '거래처', className: 'w-[100px]' }, - { key: 'siteName', label: '현장', className: 'min-w-[100px]' }, - { key: 'category', label: '구분', className: 'w-[80px] text-center' }, - { key: 'department', label: '부서', className: 'w-[80px]' }, - { key: 'workerName', label: '이름', className: 'w-[80px]' }, - { key: 'baseDate', label: '기준일', className: 'w-[100px]' }, - { key: 'checkInTime', label: '출근', className: 'w-[80px] text-center' }, - { key: 'checkOutTime', label: '퇴근', className: 'w-[80px] text-center' }, - { key: 'constructionNumber', label: '시공번호', className: 'w-[120px]' }, - { key: 'laborCost', label: '노임', className: 'w-[100px] text-right' }, + { key: 'partnerName', label: '거래처', className: 'w-[100px]', copyable: true }, + { key: 'siteName', label: '현장', className: 'min-w-[100px]', copyable: true }, + { key: 'category', label: '구분', className: 'w-[80px] text-center', copyable: true }, + { key: 'department', label: '부서', className: 'w-[80px]', copyable: true }, + { key: 'workerName', label: '이름', className: 'w-[80px]', copyable: true }, + { key: 'baseDate', label: '기준일', className: 'w-[100px]', copyable: true }, + { key: 'checkInTime', label: '출근', className: 'w-[80px] text-center', copyable: true }, + { key: 'checkOutTime', label: '퇴근', className: 'w-[80px] text-center', copyable: true }, + { key: 'constructionNumber', label: '시공번호', className: 'w-[120px]', copyable: true }, + { key: 'laborCost', label: '노임', className: 'w-[100px] text-right', copyable: true }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, { key: 'actions', label: '작업', className: 'w-[60px] text-center' }, ]; diff --git a/src/components/business/construction/worker-status/actions.ts b/src/components/business/construction/worker-status/actions.ts index ae6f54ab..d126e9dd 100644 --- a/src/components/business/construction/worker-status/actions.ts +++ b/src/components/business/construction/worker-status/actions.ts @@ -91,21 +91,13 @@ interface GetWorkerStatusStatsResult { } export async function getWorkerStatusStats(): Promise { - try { - // Mock 통계 데이터 - return { - success: true, - data: { - allContract: 25, - pending: 8, - completed: 17, - }, - }; - } catch (error) { - console.error('Failed to fetch worker status stats:', error); - return { - success: false, - error: '통계 정보를 불러오는데 실패했습니다.', - }; - } + // Mock 통계 데이터 + return { + success: true, + data: { + allContract: 25, + pending: 8, + completed: 17, + }, + }; } diff --git a/src/components/checklist-management/ChecklistForm.tsx b/src/components/checklist-management/ChecklistForm.tsx index dc7bb6dc..aa5b2985 100644 --- a/src/components/checklist-management/ChecklistForm.tsx +++ b/src/components/checklist-management/ChecklistForm.tsx @@ -59,7 +59,7 @@ export function ChecklistForm({ mode, initialData }: ChecklistFormProps) { const [checklistName, setChecklistName] = useState(initialData?.checklistName || ''); const [status, setStatus] = useState(initialData?.status || '사용'); - const [isLoading, setIsLoading] = useState(false); + const [, setIsLoading] = useState(false); const handleSubmit = async (): Promise<{ success: boolean; error?: string }> => { if (!checklistName.trim()) { diff --git a/src/components/checklist-management/ChecklistListClient.tsx b/src/components/checklist-management/ChecklistListClient.tsx index f43ca468..22237ff8 100644 --- a/src/components/checklist-management/ChecklistListClient.tsx +++ b/src/components/checklist-management/ChecklistListClient.tsx @@ -42,7 +42,7 @@ export default function ChecklistListClient() { // ===== 상태 ===== const [allChecklists, setAllChecklists] = useState([]); - const [stats, setStats] = useState({ total: 0, active: 0, inactive: 0 }); + const [, setStats] = useState({ total: 0, active: 0, inactive: 0 }); const [isLoading, setIsLoading] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteTargetId, setDeleteTargetId] = useState(null); @@ -256,7 +256,7 @@ export default function ChecklistListClient() { idField: 'id', actions: { - getList: async (params?: ListParams) => { + getList: async (_params?: ListParams) => { try { const [listResult, statsResult] = await Promise.all([ getChecklistList(), @@ -295,8 +295,8 @@ export default function ChecklistListClient() { { key: 'drag', label: '', className: 'w-[40px]' }, { key: 'checkbox', label: '', className: 'w-[50px]' }, { key: 'no', label: 'No.', className: 'w-[60px] text-center' }, - { key: 'checklistCode', label: '점검표 번호', className: 'w-[120px]' }, - { key: 'checklistName', label: '점검표', className: 'min-w-[200px]' }, + { key: 'checklistCode', label: '점검표 번호', className: 'w-[120px]', copyable: true }, + { key: 'checklistName', label: '점검표', className: 'min-w-[200px]', copyable: true }, { key: 'items', label: '항목', className: 'w-[80px] text-center' }, { key: 'documents', label: '문서', className: 'w-[80px] text-center' }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, diff --git a/src/components/checklist-management/actions.ts b/src/components/checklist-management/actions.ts index ead690d9..cfe4c595 100644 --- a/src/components/checklist-management/actions.ts +++ b/src/components/checklist-management/actions.ts @@ -218,7 +218,7 @@ export async function updateChecklist( return { success: true, data: updated }; } -export async function deleteChecklist(id: string): Promise<{ +export async function deleteChecklist(_id: string): Promise<{ success: boolean; error?: string; }> { @@ -248,7 +248,7 @@ export async function toggleChecklistStatus(id: string): Promise<{ } export async function reorderChecklists( - items: { id: string; order: number }[] + _items: { id: string; order: number }[] ): Promise<{ success: boolean; error?: string }> { return { success: true }; } @@ -273,7 +273,7 @@ export async function getChecklistStats(): Promise<{ // 점검표 항목 CRUD // ============================================================================ -export async function getChecklistItems(checklistId: string): Promise<{ +export async function getChecklistItems(_checklistId: string): Promise<{ success: boolean; data?: ChecklistItem[]; error?: string; @@ -328,15 +328,15 @@ export async function updateChecklistItem( } export async function deleteChecklistItem( - checklistId: string, - itemId: string + _checklistId: string, + _itemId: string ): Promise<{ success: boolean; error?: string }> { return { success: true }; } export async function reorderChecklistItems( - checklistId: string, - items: { id: string; order: number }[] + _checklistId: string, + _items: { id: string; order: number }[] ): Promise<{ success: boolean; error?: string }> { return { success: true }; } \ No newline at end of file diff --git a/src/components/clients/ClientDetailClientV2.tsx b/src/components/clients/ClientDetailClientV2.tsx index 1ffffd2f..c333dd1f 100644 --- a/src/components/clients/ClientDetailClientV2.tsx +++ b/src/components/clients/ClientDetailClientV2.tsx @@ -12,7 +12,7 @@ import { useRouter, useSearchParams } from 'next/navigation'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import type { DetailMode, IntegratedDetailTemplateRef } from '@/components/templates/IntegratedDetailTemplate/types'; import type { Client, ClientFormData } from '@/hooks/useClientList'; -import { useClientList, transformClientToApiCreate, transformClientToApiUpdate } from '@/hooks/useClientList'; +import { useClientList } from '@/hooks/useClientList'; import { clientDetailConfig } from './clientDetailConfig'; import { toast } from 'sonner'; import { useDevFillContext } from '@/components/dev/DevFillContext'; diff --git a/src/components/clients/ClientRegistration.tsx b/src/components/clients/ClientRegistration.tsx index f45e3277..8de8b8d2 100644 --- a/src/components/clients/ClientRegistration.tsx +++ b/src/components/clients/ClientRegistration.tsx @@ -133,7 +133,7 @@ export function ClientRegistration({ editingClient ? '거래처가 수정되었습니다.' : '거래처가 등록되었습니다.' ); onBack(); - } catch (error) { + } catch (_error) { toast.error('저장 중 오류가 발생했습니다.'); } finally { setIsSaving(false); diff --git a/src/components/common/EditableTable/EditableTable.tsx b/src/components/common/EditableTable/EditableTable.tsx index ea5339c9..3e623119 100644 --- a/src/components/common/EditableTable/EditableTable.tsx +++ b/src/components/common/EditableTable/EditableTable.tsx @@ -12,7 +12,7 @@ import { import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Plus, Trash2, GripVertical } from 'lucide-react'; +import { Plus, Trash2 } from 'lucide-react'; import { cn } from '@/lib/utils'; export interface EditableColumn { diff --git a/src/components/common/NoticePopupModal/NoticePopupContainer.tsx b/src/components/common/NoticePopupModal/NoticePopupContainer.tsx index 50b14e14..c6518351 100644 --- a/src/components/common/NoticePopupModal/NoticePopupContainer.tsx +++ b/src/components/common/NoticePopupModal/NoticePopupContainer.tsx @@ -4,7 +4,6 @@ import { useEffect, useState } from 'react'; import { NoticePopupModal, isPopupDismissedForToday } from './NoticePopupModal'; import { getActivePopups } from './actions'; import type { NoticePopupData } from './NoticePopupModal'; -import type { Popup } from '@/components/settings/PopupManagement/types'; /** * 활성 팝업을 자동으로 가져와 순차적으로 표시하는 컨테이너 diff --git a/src/components/common/ParentMenuRedirect.tsx b/src/components/common/ParentMenuRedirect.tsx index 6482c4ce..88c2d76a 100644 --- a/src/components/common/ParentMenuRedirect.tsx +++ b/src/components/common/ParentMenuRedirect.tsx @@ -19,7 +19,7 @@ interface ParentMenuRedirectProps { */ export function ParentMenuRedirect({ parentPath, fallbackPath }: ParentMenuRedirectProps) { const router = useRouter(); - const pathname = usePathname(); + const _pathname = usePathname(); useEffect(() => { try { diff --git a/src/components/common/ScheduleCalendar/ScheduleBar.tsx b/src/components/common/ScheduleCalendar/ScheduleBar.tsx index f3b7453f..0ceae566 100644 --- a/src/components/common/ScheduleCalendar/ScheduleBar.tsx +++ b/src/components/common/ScheduleCalendar/ScheduleBar.tsx @@ -50,7 +50,7 @@ export function ScheduleBar({ const leftPercent = (startCol / 7) * 100; // 툴팁 내용 생성 - const tooltipContent = `${event.title}\n기간: ${event.startDate} ~ ${event.endDate}`; + const _tooltipContent = `${event.title}\n기간: ${event.startDate} ~ ${event.endDate}`; return ( diff --git a/src/components/common/ScheduleCalendar/WeekView.tsx b/src/components/common/ScheduleCalendar/WeekView.tsx index b5d7bf0e..a92bd474 100644 --- a/src/components/common/ScheduleCalendar/WeekView.tsx +++ b/src/components/common/ScheduleCalendar/WeekView.tsx @@ -2,7 +2,6 @@ import { useMemo } from 'react'; import { cn } from '@/components/ui/utils'; -import { DayCell } from './DayCell'; import { ScheduleBar } from './ScheduleBar'; import { MorePopover } from './MorePopover'; import type { WeekViewProps } from './types'; @@ -132,7 +131,7 @@ export function WeekView({ {/* 날짜 셀들 */} {weekDays.map((date, colIndex) => { const dayOfWeek = getDay(date); - const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; + const _isWeekend = dayOfWeek === 0 || dayOfWeek === 6; const badge = getBadgeForDate(badges, date); const hiddenCount = hiddenEventCounts.get(colIndex) || 0; const dayEvents = getEventsForDate(events, date); diff --git a/src/components/customer-center/EventManagement/EventList.tsx b/src/components/customer-center/EventManagement/EventList.tsx index c62e85a0..a4e1c12a 100644 --- a/src/components/customer-center/EventManagement/EventList.tsx +++ b/src/components/customer-center/EventManagement/EventList.tsx @@ -50,7 +50,7 @@ export function EventList() { ); // ===== 탭 카운트 계산 함수 ===== - const computeTabs = useCallback( + const _computeTabs = useCallback( (data: Event[]): TabOption[] => { const ongoing = data.filter((item) => item.endDate >= today).length; const ended = data.filter((item) => item.endDate < today).length; @@ -93,10 +93,10 @@ export function EventList() { // 테이블 컬럼 columns: [ { key: 'no', label: 'No.', className: 'w-[60px] text-center' }, - { key: 'title', label: '제목', className: 'min-w-[200px]' }, - { key: 'author', label: '작성자', className: 'w-[100px] text-center' }, - { key: 'period', label: '기간', className: 'w-[200px] text-center' }, - { key: 'viewCount', label: '조회수', className: 'w-[80px] text-center' }, + { key: 'title', label: '제목', className: 'min-w-[200px]', copyable: true }, + { key: 'author', label: '작성자', className: 'w-[100px] text-center', copyable: true }, + { key: 'period', label: '기간', className: 'w-[200px] text-center', copyable: true }, + { key: 'viewCount', label: '조회수', className: 'w-[80px] text-center', copyable: true }, ], // 클라이언트 사이드 필터링 @@ -129,7 +129,7 @@ export function EventList() { }, // 커스텀 필터 (날짜) - customFilterFn: (items, filterValues) => { + customFilterFn: (items, _filterValues) => { if (!items || items.length === 0) return items; if (!startDate || !endDate) return items; return items.filter((item) => { diff --git a/src/components/customer-center/FAQManagement/FAQList.tsx b/src/components/customer-center/FAQManagement/FAQList.tsx index 345c00a2..2b0ae58a 100644 --- a/src/components/customer-center/FAQManagement/FAQList.tsx +++ b/src/components/customer-center/FAQManagement/FAQList.tsx @@ -23,8 +23,8 @@ import { getPosts } from '../shared/actions'; export function FAQList() { // ===== 상태 관리 ===== const [faqs, setFaqs] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); + const [, setIsLoading] = useState(true); + const [, setError] = useState(null); // ===== API 데이터 로드 ===== useEffect(() => { diff --git a/src/components/customer-center/InquiryManagement/InquiryList.tsx b/src/components/customer-center/InquiryManagement/InquiryList.tsx index c9641cb8..60487bc1 100644 --- a/src/components/customer-center/InquiryManagement/InquiryList.tsx +++ b/src/components/customer-center/InquiryManagement/InquiryList.tsx @@ -25,7 +25,6 @@ import { } from '@/components/templates/UniversalListPage'; import { type Inquiry, - type InquiryCategory, type InquiryStatus, type SortOption, CATEGORY_FILTER_OPTIONS, @@ -97,10 +96,10 @@ export function InquiryList() { // 테이블 컬럼 columns: [ { key: 'no', label: 'No.', className: 'w-[60px] text-center' }, - { key: 'category', label: '상담분류', className: 'w-[120px]' }, - { key: 'title', label: '제목', className: 'min-w-[200px]' }, + { key: 'category', label: '상담분류', className: 'w-[120px]', copyable: true }, + { key: 'title', label: '제목', className: 'min-w-[200px]', 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 }, ], // 클라이언트 사이드 필터링 diff --git a/src/components/customer-center/NoticeManagement/NoticeList.tsx b/src/components/customer-center/NoticeManagement/NoticeList.tsx index 3d41d4dc..d26c1155 100644 --- a/src/components/customer-center/NoticeManagement/NoticeList.tsx +++ b/src/components/customer-center/NoticeManagement/NoticeList.tsx @@ -75,10 +75,10 @@ export function NoticeList() { // 테이블 컬럼 columns: [ { key: 'no', label: 'No.', className: 'w-[60px] text-center' }, - { key: 'title', label: '제목', className: 'min-w-[200px]' }, - { key: 'author', label: '작성자', className: 'w-[100px] text-center' }, - { key: 'createdAt', label: '등록일', className: 'w-[120px] text-center' }, - { key: 'viewCount', label: '조회수', className: 'w-[80px] text-center' }, + { key: 'title', label: '제목', className: 'min-w-[200px]', copyable: true }, + { key: 'author', label: '작성자', className: 'w-[100px] text-center', copyable: true }, + { key: 'createdAt', label: '등록일', className: 'w-[120px] text-center', copyable: true }, + { key: 'viewCount', label: '조회수', className: 'w-[80px] text-center', copyable: true }, ], // 클라이언트 사이드 필터링 @@ -96,7 +96,7 @@ export function NoticeList() { }, // 커스텀 필터 (날짜) - customFilterFn: (items, filterValues) => { + customFilterFn: (items, _filterValues) => { if (!items || items.length === 0) return items; // 날짜 필터는 외부 상태 사용 if (!startDate || !endDate) return items; diff --git a/src/components/dev/DevToolbar.tsx b/src/components/dev/DevToolbar.tsx index ac97c644..6ac60fb1 100644 --- a/src/components/dev/DevToolbar.tsx +++ b/src/components/dev/DevToolbar.tsx @@ -110,7 +110,7 @@ export function DevToolbar() { isEnabled, isVisible, setIsVisible, - currentPage, + currentPage: _currentPage, fillForm, hasRegisteredForm, flowData, diff --git a/src/components/dev/generators/accountingData.ts b/src/components/dev/generators/accountingData.ts index 45f1c7c5..54c2a3e8 100644 --- a/src/components/dev/generators/accountingData.ts +++ b/src/components/dev/generators/accountingData.ts @@ -6,7 +6,6 @@ import { randomPick, randomInt, today, - randomRemark, } from './index'; // ===== 공통 상수 ===== @@ -21,7 +20,7 @@ const SAMPLE_VENDORS = [ ]; // 계좌명 목록 -const ACCOUNT_NAMES = [ +const _ACCOUNT_NAMES = [ '기업은행 1234-5678-9012', '국민은행 111-22-33333', '신한은행 110-123-456789', @@ -32,7 +31,7 @@ const ACCOUNT_NAMES = [ // ===== 입금 관련 ===== // 입금 유형 -const DEPOSIT_TYPES = ['revenue', 'deposit', 'sales', 'other', 'unset']; +const _DEPOSIT_TYPES = ['revenue', 'deposit', 'sales', 'other', 'unset']; // 입금자명 const DEPOSITOR_NAMES = [ @@ -92,7 +91,7 @@ export function generateDepositData(options: GenerateDepositDataOptions = {}): D // ===== 출금 관련 ===== // 출금 유형 -const WITHDRAWAL_TYPES = ['expense', 'payment', 'purchase', 'salary', 'other', 'unset']; +const _WITHDRAWAL_TYPES = ['expense', 'payment', 'purchase', 'salary', 'other', 'unset']; // 수취인명 const RECIPIENT_NAMES = [ @@ -153,7 +152,7 @@ export function generateWithdrawalData(options: GenerateWithdrawalDataOptions = // ===== 매입(지출결의서) 관련 ===== // 문서 유형 -const DOCUMENT_TYPES = ['proposal', 'expenseReport', 'expenseEstimate']; +const _DOCUMENT_TYPES = ['proposal', 'expenseReport', 'expenseEstimate']; // 지출결의서 제목 const PROPOSAL_TITLES = [ @@ -314,7 +313,7 @@ const CARD_MEMOS = [ ]; // 사용유형 (usageType) - 카드 결제 분류 -const CARD_USAGE_TYPES = ['unset', 'meal', 'transport', 'supplies', 'entertainment', 'other']; +const _CARD_USAGE_TYPES = ['unset', 'meal', 'transport', 'supplies', 'entertainment', 'other']; export interface CardTransactionFormData { cardId: string; diff --git a/src/components/dev/generators/quoteData.ts b/src/components/dev/generators/quoteData.ts index 15cc4a6e..b2bf85a2 100644 --- a/src/components/dev/generators/quoteData.ts +++ b/src/components/dev/generators/quoteData.ts @@ -16,8 +16,6 @@ import { tempId, } from './index'; import type { QuoteFormData, QuoteFormItem } from '@/components/quotes/types'; -import type { Vendor } from '@/components/accounting/VendorManagement/types'; -import type { FinishedGoods } from '@/components/quotes/actions'; // 제품 카테고리 const PRODUCT_CATEGORIES = ['SCREEN', 'STEEL']; diff --git a/src/components/document-system/viewer/DocumentViewer.tsx b/src/components/document-system/viewer/DocumentViewer.tsx index f382e735..ccf088f7 100644 --- a/src/components/document-system/viewer/DocumentViewer.tsx +++ b/src/components/document-system/viewer/DocumentViewer.tsx @@ -14,11 +14,9 @@ import { DocumentContent } from './DocumentContent'; import { useZoom, useDrag } from './hooks'; import { mergeWithPreset } from '../presets'; import { - DocumentConfig, DocumentViewerProps, ActionType, DocumentFeatures, - PdfMeta, } from '../types'; import { getTodayString } from '@/lib/utils/date'; diff --git a/src/components/hr/AttendanceManagement/AttendanceInfoDialog.tsx b/src/components/hr/AttendanceManagement/AttendanceInfoDialog.tsx index 80e25776..b1972129 100644 --- a/src/components/hr/AttendanceManagement/AttendanceInfoDialog.tsx +++ b/src/components/hr/AttendanceManagement/AttendanceInfoDialog.tsx @@ -90,7 +90,7 @@ export function AttendanceInfoDialog({ }; // 선택된 사원 정보 - const selectedEmployee = employees.find(e => e.id === formData.employeeId); + const _selectedEmployee = employees.find(e => e.id === formData.employeeId); return ( diff --git a/src/components/hr/AttendanceManagement/ReasonInfoDialog.tsx b/src/components/hr/AttendanceManagement/ReasonInfoDialog.tsx index 70bb1f95..fc6aadbe 100644 --- a/src/components/hr/AttendanceManagement/ReasonInfoDialog.tsx +++ b/src/components/hr/AttendanceManagement/ReasonInfoDialog.tsx @@ -22,7 +22,6 @@ import { format } from 'date-fns'; import type { ReasonInfoDialogProps, ReasonFormData, - ReasonType, } from './types'; import { REASON_TYPE_LABELS } from './types'; diff --git a/src/components/hr/AttendanceManagement/index.tsx b/src/components/hr/AttendanceManagement/index.tsx index 7e01e403..bdd3716c 100644 --- a/src/components/hr/AttendanceManagement/index.tsx +++ b/src/components/hr/AttendanceManagement/index.tsx @@ -10,12 +10,10 @@ import { Calendar, Plus, FileText, - Search, } from 'lucide-react'; import type { ExcelColumn } from '@/lib/utils/excel-download'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; import { Checkbox } from '@/components/ui/checkbox'; import { TableRow, TableCell } from '@/components/ui/table'; import { format } from 'date-fns'; @@ -27,7 +25,6 @@ import { type FilterFieldConfig, type FilterValues, } from '@/components/templates/UniversalListPage'; -import { DateRangeSelector } from '@/components/molecules/DateRangeSelector'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; import { AttendanceInfoDialog } from './AttendanceInfoDialog'; import { ReasonInfoDialog } from './ReasonInfoDialog'; @@ -62,11 +59,11 @@ export function AttendanceManagement() { const [employees, setEmployees] = useState([]); const [isLoading, setIsLoading] = useState(true); const isInitialLoadDone = useRef(false); - const [total, setTotal] = useState(0); + const [, setTotal] = useState(0); // 검색 및 필터 상태 const [searchValue, setSearchValue] = useState(''); - const [activeTab, setActiveTab] = useState('all'); + const [activeTab, _setActiveTab] = useState('all'); const [filterOption, setFilterOption] = useState('all'); const [sortOption, setSortOption] = useState('dateDesc'); const [currentPage, setCurrentPage] = useState(1); @@ -84,7 +81,7 @@ export function AttendanceManagement() { const [selectedAttendance, setSelectedAttendance] = useState(null); const [reasonDialogOpen, setReasonDialogOpen] = useState(false); const [selectedItems, setSelectedItems] = useState>(new Set()); - const [isSaving, setIsSaving] = useState(false); + const [, setIsSaving] = useState(false); // 데이터 로드 useEffect(() => { @@ -253,20 +250,20 @@ export function AttendanceManagement() { // 테이블 컬럼 정의 const tableColumns = useMemo(() => [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'department', label: '부서', className: 'min-w-[80px]', sortable: true }, - { key: 'position', label: '직책', className: 'min-w-[100px]', sortable: true }, - { key: 'name', label: '이름', className: 'min-w-[60px]', sortable: true }, - { key: 'rank', label: '직급', className: 'min-w-[60px]', sortable: true }, - { key: 'baseDate', label: '기준일', className: 'min-w-[100px]', sortable: true }, - { key: 'checkIn', label: '출근', className: 'min-w-[60px]', sortable: true }, - { key: 'checkOut', label: '퇴근', className: 'min-w-[60px]', sortable: true }, - { key: 'breakTime', label: '휴게', className: 'min-w-[60px]', sortable: true }, - { key: 'overtime', label: '연장근무', className: 'min-w-[80px]', sortable: true }, - { key: 'reason', label: '사유', className: 'min-w-[80px]', sortable: true }, + { key: 'department', label: '부서', className: 'min-w-[80px]', sortable: true, copyable: true }, + { key: 'position', label: '직책', className: 'min-w-[100px]', sortable: true, copyable: true }, + { key: 'name', label: '이름', className: 'min-w-[60px]', sortable: true, copyable: true }, + { key: 'rank', label: '직급', className: 'min-w-[60px]', sortable: true, copyable: true }, + { key: 'baseDate', label: '기준일', className: 'min-w-[100px]', sortable: true, copyable: true }, + { key: 'checkIn', label: '출근', className: 'min-w-[60px]', sortable: true, copyable: true }, + { key: 'checkOut', label: '퇴근', className: 'min-w-[60px]', sortable: true, copyable: true }, + { key: 'breakTime', label: '휴게', className: 'min-w-[60px]', sortable: true, copyable: true }, + { key: 'overtime', label: '연장근무', className: 'min-w-[80px]', sortable: true, copyable: true }, + { key: 'reason', label: '사유', className: 'min-w-[80px]', sortable: true, copyable: true }, ], []); // 체크박스 토글 - const toggleSelection = useCallback((id: string) => { + const _toggleSelection = useCallback((id: string) => { setSelectedItems(prev => { const newSet = new Set(prev); if (newSet.has(id)) { @@ -279,7 +276,7 @@ export function AttendanceManagement() { }, []); // 전체 선택/해제 - const toggleSelectAll = useCallback(() => { + const _toggleSelectAll = useCallback(() => { if (selectedItems.size === paginatedData.length && paginatedData.length > 0) { setSelectedItems(new Set()); } else { @@ -393,7 +390,7 @@ export function AttendanceManagement() { sort: sortOption, }), [filterOption, sortOption]); - const handleFilterChange = useCallback((key: string, value: string | string[]) => { + const _handleFilterChange = useCallback((key: string, value: string | string[]) => { switch (key) { case 'filter': setFilterOption(value as FilterOption); @@ -405,7 +402,7 @@ export function AttendanceManagement() { setCurrentPage(1); }, []); - const handleFilterReset = useCallback(() => { + const _handleFilterReset = useCallback(() => { setFilterOption('all'); setSortOption('dateDesc'); setCurrentPage(1); diff --git a/src/components/hr/CalendarManagement/index.tsx b/src/components/hr/CalendarManagement/index.tsx index 82f74248..ee092a8d 100644 --- a/src/components/hr/CalendarManagement/index.tsx +++ b/src/components/hr/CalendarManagement/index.tsx @@ -30,13 +30,13 @@ import type { TableColumn } from '@/components/templates/UniversalListPage/types const LIST_COLUMNS: TableColumn[] = [ { key: 'no', label: 'No.', className: 'text-center w-[60px]' }, - { key: 'type', label: '유형', className: 'w-[100px]' }, - { key: 'name', label: '일정명' }, - { key: 'startDate', label: '시작일', className: 'w-[120px]' }, - { key: 'endDate', label: '종료일', className: 'w-[120px]' }, - { key: 'days', label: '일수', className: 'text-center w-[70px]' }, + { key: 'type', label: '유형', className: 'w-[100px]', copyable: true }, + { key: 'name', label: '일정명', copyable: true }, + { key: 'startDate', label: '시작일', className: 'w-[120px]', copyable: true }, + { key: 'endDate', label: '종료일', className: 'w-[120px]', copyable: true }, + { key: 'days', label: '일수', className: 'text-center w-[70px]', copyable: true }, { key: 'isRecurring', label: '반복', className: 'text-center w-[70px]' }, - { key: 'memo', label: '메모' }, + { key: 'memo', label: '메모', copyable: true }, ]; export function CalendarManagement() { @@ -46,7 +46,7 @@ export function CalendarManagement() { const [schedules, setSchedules] = useState([]); const [stats, setStats] = useState({ totalCount: 0, totalHolidayDays: 0, publicHolidayCount: 0 }); - const [isLoading, setIsLoading] = useState(true); + const [, setIsLoading] = useState(true); // Dialog states const [detailOpen, setDetailOpen] = useState(false); diff --git a/src/components/hr/CardManagement/index.tsx b/src/components/hr/CardManagement/index.tsx index 59abca3e..aa80884d 100644 --- a/src/components/hr/CardManagement/index.tsx +++ b/src/components/hr/CardManagement/index.tsx @@ -148,12 +148,12 @@ export function CardManagement() { // 테이블 컬럼 columns: [ { key: 'no', label: 'No.', className: 'text-center w-[60px]' }, - { key: 'cardCompany', label: '카드사', className: 'min-w-[90px]' }, - { key: 'cardNumber', label: '카드번호', className: 'min-w-[160px]' }, - { key: 'cardName', label: '카드명', className: 'min-w-[150px]' }, - { key: 'department', label: '부서', className: 'min-w-[80px]' }, - { key: 'user', label: '사용자', className: 'min-w-[80px]' }, - { key: 'usage', label: '사용현황', className: 'min-w-[180px]' }, + { key: 'cardCompany', label: '카드사', className: 'min-w-[90px]', copyable: true }, + { key: 'cardNumber', label: '카드번호', className: 'min-w-[160px]', copyable: true }, + { key: 'cardName', label: '카드명', className: 'min-w-[150px]', copyable: true }, + { key: 'department', label: '부서', className: 'min-w-[80px]', copyable: true }, + { key: 'user', label: '사용자', className: 'min-w-[80px]', copyable: true }, + { key: 'usage', label: '사용현황', className: 'min-w-[180px]', copyable: true }, { key: 'status', label: '상태', className: 'text-center min-w-[70px]' }, ], diff --git a/src/components/hr/DepartmentManagement/index.tsx b/src/components/hr/DepartmentManagement/index.tsx index 55cf1fc6..24bedfb2 100644 --- a/src/components/hr/DepartmentManagement/index.tsx +++ b/src/components/hr/DepartmentManagement/index.tsx @@ -39,7 +39,7 @@ export function DepartmentManagement() { const [departments, setDepartments] = useState([]); // 로딩/처리 상태 - const [isLoading, setIsLoading] = useState(false); + const [, setIsLoading] = useState(false); const [isProcessing, setIsProcessing] = useState(false); // 선택 상태 diff --git a/src/components/hr/EmployeeManagement/EmployeeForm.tsx b/src/components/hr/EmployeeManagement/EmployeeForm.tsx index 370ca0e0..a21c2011 100644 --- a/src/components/hr/EmployeeManagement/EmployeeForm.tsx +++ b/src/components/hr/EmployeeManagement/EmployeeForm.tsx @@ -38,14 +38,12 @@ import type { } from './types'; import { EMPLOYMENT_TYPE_LABELS, - GENDER_LABELS, USER_ROLE_LABELS, USER_ACCOUNT_STATUS_LABELS, EMPLOYEE_STATUS_LABELS, DEFAULT_FIELD_SETTINGS, } from './types'; import { getPositions, getDepartments, uploadProfileImage, type PositionItem, type DepartmentItem } from './actions'; -import { getProfileImageUrl } from './utils'; import { extractDigits } from '@/lib/formatters'; // 부서 트리 구조 타입 @@ -191,8 +189,8 @@ export function EmployeeForm({ const [showFieldSettings, setShowFieldSettings] = useState(false); const [fieldSettings, setFieldSettings] = useState(initialFieldSettings); - const title = mode === 'create' ? '사원 등록' : mode === 'edit' ? '사원 수정' : '사원 상세'; - const description = mode === 'create' + const _title = mode === 'create' ? '사원 등록' : mode === 'edit' ? '사원 수정' : '사원 상세'; + const _description = mode === 'create' ? '새로운 사원 정보를 입력합니다' : mode === 'edit' ? '사원 정보를 수정합니다' @@ -380,7 +378,7 @@ export function EmployeeForm({ }; // 부서/직책 변경 - const handleDepartmentPositionChange = (id: string, field: keyof DepartmentPosition, value: string) => { + const _handleDepartmentPositionChange = (id: string, field: keyof DepartmentPosition, value: string) => { setFormData(prev => ({ ...prev, departmentPositions: prev.departmentPositions.map(dp => diff --git a/src/components/hr/EmployeeManagement/EmployeeToolbar.tsx b/src/components/hr/EmployeeManagement/EmployeeToolbar.tsx index e229e5c9..52b1f8b0 100644 --- a/src/components/hr/EmployeeManagement/EmployeeToolbar.tsx +++ b/src/components/hr/EmployeeManagement/EmployeeToolbar.tsx @@ -15,7 +15,7 @@ interface EmployeeToolbarProps { export function EmployeeToolbar({ dateRange, - onDateRangeChange, + onDateRangeChange: _onDateRangeChange, onAddEmployee, onCSVUpload, onUserInvite, diff --git a/src/components/hr/EmployeeManagement/actions.ts b/src/components/hr/EmployeeManagement/actions.ts index 550ebe5d..a68bdcb8 100644 --- a/src/components/hr/EmployeeManagement/actions.ts +++ b/src/components/hr/EmployeeManagement/actions.ts @@ -28,7 +28,7 @@ import { transformApiToFrontend, transformFrontendToApi, type EmployeeApiData } // API 응답 타입 정의 // ============================================ -interface ApiResponse { +interface _ApiResponse { success: boolean; data: T; message: string; diff --git a/src/components/hr/EmployeeManagement/index.tsx b/src/components/hr/EmployeeManagement/index.tsx index f048024d..c62c1458 100644 --- a/src/components/hr/EmployeeManagement/index.tsx +++ b/src/components/hr/EmployeeManagement/index.tsx @@ -2,11 +2,10 @@ import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import { useRouter } from 'next/navigation'; -import { Users, Edit, Trash2, UserCheck, UserX, Clock, Calendar, Mail, Plus, Upload, Loader2, Search } from 'lucide-react'; -import { getEmployees, deleteEmployee, deleteEmployees, getEmployeeStats } from './actions'; +import { Users, Edit, Trash2, UserCheck, UserX, Clock, Calendar, Mail, Plus, Upload } from 'lucide-react'; +import { getEmployees, deleteEmployee, deleteEmployees } from './actions'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; import { Checkbox } from '@/components/ui/checkbox'; import { TableRow, TableCell } from '@/components/ui/table'; import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; @@ -18,13 +17,11 @@ import { type FilterFieldConfig, type FilterValues, } from '@/components/templates/UniversalListPage'; -import { DateRangeSelector } from '@/components/molecules/DateRangeSelector'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; import { FieldSettingsDialog } from './FieldSettingsDialog'; import { UserInviteDialog } from './UserInviteDialog'; import type { Employee, - EmployeeStatus, FieldSettings, } from './types'; import { @@ -69,11 +66,11 @@ export function EmployeeManagement() { const [employees, setEmployees] = useState([]); const [isLoading, setIsLoading] = useState(true); const isInitialLoadDone = useRef(false); - const [total, setTotal] = useState(0); + const [, setTotal] = useState(0); // 검색 및 필터 상태 const [searchValue, setSearchValue] = useState(''); - const [activeTab, setActiveTab] = useState('all'); + const [activeTab, _setActiveTab] = useState('all'); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 20; @@ -162,22 +159,25 @@ export function EmployeeManagement() { // 정렬 filtered = [...filtered].sort((a, b) => { switch (sortOption) { - case 'rank': + case 'rank': { // 직급 순서 정의 (높은 직급이 먼저) const rankOrder: Record = { '회장': 1, '사장': 2, '부사장': 3, '전무': 4, '상무': 5, '이사': 6, '부장': 7, '차장': 8, '과장': 9, '대리': 10, '주임': 11, '사원': 12 }; return (rankOrder[a.rank || ''] || 99) - (rankOrder[b.rank || ''] || 99); + } case 'hireDateDesc': return new Date(b.hireDate || 0).getTime() - new Date(a.hireDate || 0).getTime(); case 'hireDateAsc': return new Date(a.hireDate || 0).getTime() - new Date(b.hireDate || 0).getTime(); - case 'departmentAsc': + case 'departmentAsc': { const deptA = a.departmentPositions?.[0]?.departmentName || ''; const deptB = b.departmentPositions?.[0]?.departmentName || ''; return deptA.localeCompare(deptB, 'ko'); - case 'departmentDesc': + } + case 'departmentDesc': { const deptA2 = a.departmentPositions?.[0]?.departmentName || ''; const deptB2 = b.departmentPositions?.[0]?.departmentName || ''; return deptB2.localeCompare(deptA2, 'ko'); + } case 'nameAsc': return a.name.localeCompare(b.name, 'ko'); case 'nameDesc': @@ -255,22 +255,22 @@ export function EmployeeManagement() { // 테이블 컬럼 정의 const tableColumns = useMemo(() => [ { key: 'rowNumber', label: '번호', className: 'w-[60px] text-center' }, - { key: 'employeeCode', label: '사원코드', className: 'min-w-[100px]', sortable: true }, + { key: 'employeeCode', label: '사원코드', className: 'min-w-[100px]', sortable: true, copyable: true }, { key: 'department', label: '부서', className: 'min-w-[100px]', sortable: true }, { key: 'position', label: '직책', className: 'min-w-[100px]', sortable: true }, - { key: 'name', label: '이름', className: 'min-w-[80px]', sortable: true }, - { key: 'rank', label: '직급', className: 'min-w-[80px]', sortable: true }, - { key: 'phone', label: '휴대폰', className: 'min-w-[120px]', sortable: true }, - { key: 'email', label: '이메일', className: 'min-w-[150px]', sortable: true }, - { key: 'hireDate', label: '입사일', className: 'min-w-[100px]', sortable: true }, + { key: 'name', label: '이름', className: 'min-w-[80px]', sortable: true, copyable: true }, + { key: 'rank', label: '직급', className: 'min-w-[80px]', sortable: true, copyable: true }, + { key: 'phone', label: '휴대폰', className: 'min-w-[120px]', sortable: true, copyable: true }, + { key: 'email', label: '이메일', className: 'min-w-[150px]', sortable: true, copyable: true }, + { key: 'hireDate', label: '입사일', className: 'min-w-[100px]', sortable: true, copyable: true }, { key: 'status', label: '상태', className: 'min-w-[80px]', sortable: true }, { key: 'userId', label: '사용자아이디', className: 'min-w-[100px]', sortable: true }, - { key: 'userRole', label: '권한', className: 'min-w-[80px]', sortable: true }, + { key: 'userRole', label: '권한', className: 'min-w-[80px]', sortable: true, copyable: true }, { key: 'actions', label: '작업', className: 'w-[100px] text-right' }, ], []); // 체크박스 토글 - const toggleSelection = useCallback((id: string) => { + const _toggleSelection = useCallback((id: string) => { setSelectedItems(prev => { const newSet = new Set(prev); if (newSet.has(id)) { @@ -283,7 +283,7 @@ export function EmployeeManagement() { }, []); // 전체 선택/해제 - const toggleSelectAll = useCallback(() => { + const _toggleSelectAll = useCallback(() => { if (selectedItems.size === paginatedData.length && paginatedData.length > 0) { setSelectedItems(new Set()); } else { @@ -293,7 +293,7 @@ export function EmployeeManagement() { }, [selectedItems.size, paginatedData]); // 일괄 삭제 핸들러 - const handleBulkDelete = useCallback(async () => { + const _handleBulkDelete = useCallback(async () => { const ids = Array.from(selectedItems); if (ids.length === 0) return; @@ -385,7 +385,7 @@ export function EmployeeManagement() { sort: sortOption, }), [filterOption, sortOption]); - const handleFilterChange = useCallback((key: string, value: string | string[]) => { + const _handleFilterChange = useCallback((key: string, value: string | string[]) => { switch (key) { case 'filter': setFilterOption(value as FilterOption); @@ -397,7 +397,7 @@ export function EmployeeManagement() { setCurrentPage(1); }, []); - const handleFilterReset = useCallback(() => { + const _handleFilterReset = useCallback(() => { setFilterOption('all'); setSortOption('rank'); setCurrentPage(1); @@ -539,14 +539,16 @@ export function EmployeeManagement() { return new Date(b.hireDate || 0).getTime() - new Date(a.hireDate || 0).getTime(); case 'hireDateAsc': return new Date(a.hireDate || 0).getTime() - new Date(b.hireDate || 0).getTime(); - case 'departmentAsc': + case 'departmentAsc': { const deptA = a.departmentPositions?.[0]?.departmentName || ''; const deptB = b.departmentPositions?.[0]?.departmentName || ''; return deptA.localeCompare(deptB, 'ko'); - case 'departmentDesc': + } + case 'departmentDesc': { const deptA2 = a.departmentPositions?.[0]?.departmentName || ''; const deptB2 = b.departmentPositions?.[0]?.departmentName || ''; return deptB2.localeCompare(deptA2, 'ko'); + } case 'nameAsc': return a.name.localeCompare(b.name, 'ko'); case 'nameDesc': @@ -558,7 +560,7 @@ export function EmployeeManagement() { }, renderTableRow: (item, index, globalIndex, handlers) => { - const { isSelected, onToggle, onRowClick } = handlers; + const { isSelected, onToggle, onRowClick: _onRowClick } = handlers; return ( { + onInvite={(_data) => { setUserInviteOpen(false); }} /> diff --git a/src/components/hr/EmployeeManagement/utils.ts b/src/components/hr/EmployeeManagement/utils.ts index 6664ae50..400b7361 100644 --- a/src/components/hr/EmployeeManagement/utils.ts +++ b/src/components/hr/EmployeeManagement/utils.ts @@ -15,7 +15,6 @@ import type { Address, UserInfo, UserRole, - UserAccountStatus, } from './types'; // ============================================ diff --git a/src/components/hr/SalaryManagement/SalaryRegistrationDialog.tsx b/src/components/hr/SalaryManagement/SalaryRegistrationDialog.tsx index 67681471..36e942d4 100644 --- a/src/components/hr/SalaryManagement/SalaryRegistrationDialog.tsx +++ b/src/components/hr/SalaryManagement/SalaryRegistrationDialog.tsx @@ -70,7 +70,7 @@ function EditableRow({ label, value, onChange, - prefix, + prefix: _prefix, }: { label: string; value: number; diff --git a/src/components/hr/SalaryManagement/index.tsx b/src/components/hr/SalaryManagement/index.tsx index 8f82085c..205fb378 100644 --- a/src/components/hr/SalaryManagement/index.tsx +++ b/src/components/hr/SalaryManagement/index.tsx @@ -12,12 +12,10 @@ import { Gift, MinusCircle, Loader2, - Search, Plus, } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { TableRow, TableCell } from '@/components/ui/table'; @@ -43,7 +41,6 @@ import { import type { SalaryRecord, SalaryDetail, - PaymentStatus, SortOption, } from './types'; import { @@ -164,7 +161,7 @@ const MOCK_SALARY_DETAILS: Record = { }; export function SalaryManagement() { - const { canExport } = usePermission(); + const { canExport: _canExport } = usePermission(); // ===== 상태 관리 ===== const [searchQuery, setSearchQuery] = useState(''); const [sortOption, setSortOption] = useState('rank'); @@ -522,17 +519,17 @@ export function SalaryManagement() { // ===== 테이블 컬럼 (부서, 직책, 이름, 직급, 기본급, 수당, 초과근무, 상여, 공제, 실지급액, 일자, 상태, 작업) ===== const tableColumns = useMemo(() => [ - { key: 'department', label: '부서', sortable: true }, - { key: 'position', label: '직책', sortable: true }, - { key: 'name', label: '이름', sortable: true }, - { key: 'rank', label: '직급', sortable: true }, - { key: 'baseSalary', label: '기본급', className: 'text-right', sortable: true }, - { key: 'allowance', label: '수당', className: 'text-right', sortable: true }, - { key: 'overtime', label: '초과근무', className: 'text-right', sortable: true }, - { key: 'bonus', label: '상여', className: 'text-right', sortable: true }, - { key: 'deduction', label: '공제', className: 'text-right', sortable: true }, - { key: 'netPayment', label: '실지급액', className: 'text-right', sortable: true }, - { key: 'paymentDate', label: '일자', className: 'text-center', sortable: true }, + { key: 'department', label: '부서', sortable: true, copyable: true }, + { key: 'position', label: '직책', sortable: true, copyable: true }, + { key: 'name', label: '이름', sortable: true, copyable: true }, + { key: 'rank', label: '직급', sortable: true, copyable: true }, + { key: 'baseSalary', label: '기본급', className: 'text-right', sortable: true, copyable: true }, + { key: 'allowance', label: '수당', className: 'text-right', sortable: true, copyable: true }, + { key: 'overtime', label: '초과근무', className: 'text-right', sortable: true, copyable: true }, + { key: 'bonus', label: '상여', className: 'text-right', sortable: true, copyable: true }, + { key: 'deduction', label: '공제', className: 'text-right', sortable: true, copyable: true }, + { key: 'netPayment', label: '실지급액', className: 'text-right', sortable: true, copyable: true }, + { key: 'paymentDate', label: '일자', className: 'text-center', sortable: true, copyable: true }, { key: 'status', label: '상태', className: 'text-center', sortable: true }, ], []); @@ -553,7 +550,7 @@ export function SalaryManagement() { sort: sortOption, }), [sortOption]); - const handleFilterChange = useCallback((key: string, value: string | string[]) => { + const _handleFilterChange = useCallback((key: string, value: string | string[]) => { switch (key) { case 'sort': setSortOption(value as SortOption); @@ -562,7 +559,7 @@ export function SalaryManagement() { setCurrentPage(1); }, []); - const handleFilterReset = useCallback(() => { + const _handleFilterReset = useCallback(() => { setSortOption('rank'); setCurrentPage(1); }, []); diff --git a/src/components/hr/VacationManagement/VacationGrantDialog.tsx b/src/components/hr/VacationManagement/VacationGrantDialog.tsx index b3ff6d52..f88639d8 100644 --- a/src/components/hr/VacationManagement/VacationGrantDialog.tsx +++ b/src/components/hr/VacationManagement/VacationGrantDialog.tsx @@ -11,7 +11,6 @@ import { DialogFooter, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; diff --git a/src/components/hr/VacationManagement/VacationRegisterDialog.tsx b/src/components/hr/VacationManagement/VacationRegisterDialog.tsx index 05434dd7..0d136f17 100644 --- a/src/components/hr/VacationManagement/VacationRegisterDialog.tsx +++ b/src/components/hr/VacationManagement/VacationRegisterDialog.tsx @@ -9,7 +9,6 @@ import { DialogFooter, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { NumberInput } from '@/components/ui/number-input'; diff --git a/src/components/hr/VacationManagement/VacationRequestDialog.tsx b/src/components/hr/VacationManagement/VacationRequestDialog.tsx index b5422b94..1a20d47c 100644 --- a/src/components/hr/VacationManagement/VacationRequestDialog.tsx +++ b/src/components/hr/VacationManagement/VacationRequestDialog.tsx @@ -11,7 +11,6 @@ import { DialogFooter, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; import { Label } from '@/components/ui/label'; import { diff --git a/src/components/hr/VacationManagement/actions.ts b/src/components/hr/VacationManagement/actions.ts index f0f68aac..e83f95ba 100644 --- a/src/components/hr/VacationManagement/actions.ts +++ b/src/components/hr/VacationManagement/actions.ts @@ -160,7 +160,7 @@ export interface LeaveBalanceRecord { remainingDays: number; } -interface ApiResponse { +interface _ApiResponse { success: boolean; data: T; message: string; diff --git a/src/components/hr/VacationManagement/index.tsx b/src/components/hr/VacationManagement/index.tsx index 5276d9e1..5d1ef37d 100644 --- a/src/components/hr/VacationManagement/index.tsx +++ b/src/components/hr/VacationManagement/index.tsx @@ -67,7 +67,7 @@ import { formatDate } from '@/lib/utils/date'; // ===== Mock 데이터 생성 (request 탭용 - 신청 현황은 leaves API 사용 예정) ===== -const generateRequestData = (): VacationRequestRecord[] => { +const _generateRequestData = (): VacationRequestRecord[] => { const departments = ['개발팀', '디자인팀', '기획팀', '영업팀', '인사팀']; const positions = ['팀장', '파트장', '선임', '주임', '사원']; const ranks = ['부장', '차장', '과장', '대리', '사원']; @@ -123,7 +123,7 @@ export function VacationManagement() { const [usageData, setUsageData] = useState([]); const [grantData, setGrantData] = useState([]); const [requestData, setRequestData] = useState([]); - const [apiLeaveRecords, setApiLeaveRecords] = useState([]); + const [, setApiLeaveRecords] = useState([]); // ===== API 데이터 로드 ===== /** @@ -394,41 +394,41 @@ export function VacationManagement() { // 휴가 사용현황: 번호|부서|직책|이름|직급|입사일|기본|부여|사용|잔액 return [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'department', label: '부서', sortable: true }, - { key: 'position', label: '직책', sortable: true }, - { key: 'name', label: '이름', sortable: true }, - { key: 'rank', label: '직급', sortable: true }, - { key: 'hireDate', label: '입사일', sortable: true }, - { key: 'base', label: '기본', className: 'text-center', sortable: true }, - { key: 'granted', label: '부여', className: 'text-center', sortable: true }, - { key: 'used', label: '사용', className: 'text-center', sortable: true }, - { key: 'remaining', label: '잔여', className: 'text-center', sortable: true }, + { key: 'department', label: '부서', sortable: true, copyable: true }, + { key: 'position', label: '직책', sortable: true, copyable: true }, + { key: 'name', label: '이름', sortable: true, copyable: true }, + { key: 'rank', label: '직급', sortable: true, copyable: true }, + { key: 'hireDate', label: '입사일', sortable: true, copyable: true }, + { key: 'base', label: '기본', className: 'text-center', sortable: true, copyable: true }, + { key: 'granted', label: '부여', className: 'text-center', sortable: true, copyable: true }, + { key: 'used', label: '사용', className: 'text-center', sortable: true, copyable: true }, + { key: 'remaining', label: '잔여', className: 'text-center', sortable: true, copyable: true }, ]; } else if (mainTab === 'grant') { // 휴가 부여현황: 번호|부서|직책|이름|직급|유형|부여일|부여휴가일수|사유 return [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'department', label: '부서', sortable: true }, - { key: 'position', label: '직책', sortable: true }, - { key: 'name', label: '이름', sortable: true }, - { key: 'rank', label: '직급', sortable: true }, - { key: 'type', label: '유형', sortable: true }, - { key: 'grantDate', label: '부여일', sortable: true }, - { key: 'grantDays', label: '부여휴가일수', className: 'text-center', sortable: true }, - { key: 'reason', label: '사유', sortable: true }, + { key: 'department', label: '부서', sortable: true, copyable: true }, + { key: 'position', label: '직책', sortable: true, copyable: true }, + { key: 'name', label: '이름', sortable: true, copyable: true }, + { key: 'rank', label: '직급', sortable: true, copyable: true }, + { key: 'type', label: '유형', sortable: true, copyable: true }, + { key: 'grantDate', label: '부여일', sortable: true, copyable: true }, + { key: 'grantDays', label: '부여휴가일수', className: 'text-center', sortable: true, copyable: true }, + { key: 'reason', label: '사유', sortable: true, copyable: true }, ]; } else { // 휴가 신청현황: 번호|부서|직책|이름|직급|휴가기간|휴가일수|상태|신청일 return [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'department', label: '부서', sortable: true }, - { key: 'position', label: '직책', sortable: true }, - { key: 'name', label: '이름', sortable: true }, - { key: 'rank', label: '직급', sortable: true }, - { key: 'period', label: '휴가기간', sortable: true }, - { key: 'days', label: '휴가일수', className: 'text-center', sortable: true }, + { key: 'department', label: '부서', sortable: true, copyable: true }, + { key: 'position', label: '직책', sortable: true, copyable: true }, + { key: 'name', label: '이름', sortable: true, copyable: true }, + { key: 'rank', label: '직급', sortable: true, copyable: true }, + { key: 'period', label: '휴가기간', sortable: true, copyable: true }, + { key: 'days', label: '휴가일수', className: 'text-center', sortable: true, copyable: true }, { key: 'status', label: '상태', className: 'text-center', sortable: true }, - { key: 'requestDate', label: '신청일', sortable: true }, + { key: 'requestDate', label: '신청일', sortable: true, copyable: true }, ]; } }, [mainTab]); @@ -642,7 +642,7 @@ export function VacationManagement() { }, ], []); - const filterValues: FilterValues = useMemo(() => ({ + const _filterValues: FilterValues = useMemo(() => ({ filter: filterOption, sort: sortOption, }), [filterOption, sortOption]); @@ -660,7 +660,7 @@ export function VacationManagement() { }); }, []); - const handleFilterReset = useCallback(() => { + const _handleFilterReset = useCallback(() => { setFilterOption('all'); setSortOption('rank'); }, []); diff --git a/src/components/items/DynamicItemForm/fields/ComputedField.tsx b/src/components/items/DynamicItemForm/fields/ComputedField.tsx index b664a15c..f7711e87 100644 --- a/src/components/items/DynamicItemForm/fields/ComputedField.tsx +++ b/src/components/items/DynamicItemForm/fields/ComputedField.tsx @@ -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'; /** * 안전한 수식 평가기 diff --git a/src/components/items/DynamicItemForm/fields/CurrencyField.tsx b/src/components/items/DynamicItemForm/fields/CurrencyField.tsx index fa23e245..ee45b220 100644 --- a/src/components/items/DynamicItemForm/fields/CurrencyField.tsx +++ b/src/components/items/DynamicItemForm/fields/CurrencyField.tsx @@ -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) => { 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]); diff --git a/src/components/items/DynamicItemForm/fields/NumberField.tsx b/src/components/items/DynamicItemForm/fields/NumberField.tsx index a08250bb..f7347144 100644 --- a/src/components/items/DynamicItemForm/fields/NumberField.tsx +++ b/src/components/items/DynamicItemForm/fields/NumberField.tsx @@ -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; diff --git a/src/components/items/ItemDetailClient.tsx b/src/components/items/ItemDetailClient.tsx index 1ae5099e..f845456c 100644 --- a/src/components/items/ItemDetailClient.tsx +++ b/src/components/items/ItemDetailClient.tsx @@ -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'; diff --git a/src/components/items/ItemDetailView.tsx b/src/components/items/ItemDetailView.tsx index 2ed51fea..85f67a0d 100644 --- a/src/components/items/ItemDetailView.tsx +++ b/src/components/items/ItemDetailView.tsx @@ -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(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); diff --git a/src/components/items/ItemForm/BendingDiagramSection.tsx b/src/components/items/ItemForm/BendingDiagramSection.tsx index 1522856b..099741d6 100644 --- a/src/components/items/ItemForm/BendingDiagramSection.tsx +++ b/src/components/items/ItemForm/BendingDiagramSection.tsx @@ -55,7 +55,7 @@ export default function BendingDiagramSection({ widthSumFieldKey, setValue, isSubmitting, - existingBendingDiagram, + existingBendingDiagram: _existingBendingDiagram, existingBendingDiagramFileName, existingBendingDiagramFileId, onDeleteExistingFile, diff --git a/src/components/items/ItemForm/forms/ProductForm.tsx b/src/components/items/ItemForm/forms/ProductForm.tsx index ab1fcd63..77ac77ef 100644 --- a/src/components/items/ItemForm/forms/ProductForm.tsx +++ b/src/components/items/ItemForm/forms/ProductForm.tsx @@ -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, diff --git a/src/components/items/ItemListClient.tsx b/src/components/items/ItemListClient.tsx index 97972230..cc16667c 100644 --- a/src/components/items/ItemListClient.tsx +++ b/src/components/items/ItemListClient.tsx @@ -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(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]' }, ], diff --git a/src/components/items/ItemMasterDataManagement.tsx b/src/components/items/ItemMasterDataManagement.tsx index e9ea3477..ac09eaab 100644 --- a/src/components/items/ItemMasterDataManagement.tsx +++ b/src/components/items/ItemMasterDataManagement.tsx @@ -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'; diff --git a/src/components/items/ItemMasterDataManagement/components/DraggableField.tsx b/src/components/items/ItemMasterDataManagement/components/DraggableField.tsx index 879c2476..0c8ea8f9 100644 --- a/src/components/items/ItemMasterDataManagement/components/DraggableField.tsx +++ b/src/components/items/ItemMasterDataManagement/components/DraggableField.tsx @@ -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 - 다른 타입의 드래그 데이터 } }; diff --git a/src/components/items/ItemMasterDataManagement/components/DraggableSection.tsx b/src/components/items/ItemMasterDataManagement/components/DraggableSection.tsx index 0db73067..70085e3e 100644 --- a/src/components/items/ItemMasterDataManagement/components/DraggableSection.tsx +++ b/src/components/items/ItemMasterDataManagement/components/DraggableSection.tsx @@ -70,7 +70,7 @@ export function DraggableSection({ if (data.index !== index) { moveSection(data.index, index); } - } catch (err) { + } catch { // Ignore - 다른 타입의 드래그 데이터 } }; diff --git a/src/components/items/ItemMasterDataManagement/components/ItemMasterDialogs.tsx b/src/components/items/ItemMasterDataManagement/components/ItemMasterDialogs.tsx index 62444a28..ce4c946e 100644 --- a/src/components/items/ItemMasterDataManagement/components/ItemMasterDialogs.tsx +++ b/src/components/items/ItemMasterDataManagement/components/ItemMasterDialogs.tsx @@ -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'; diff --git a/src/components/items/ItemMasterDataManagement/dialogs/ColumnDialog.tsx b/src/components/items/ItemMasterDataManagement/dialogs/ColumnDialog.tsx index 4d6e49a7..d5b2dfd6 100644 --- a/src/components/items/ItemMasterDataManagement/dialogs/ColumnDialog.tsx +++ b/src/components/items/ItemMasterDataManagement/dialogs/ColumnDialog.tsx @@ -29,7 +29,7 @@ export function ColumnDialog({ setColumnName, columnKey, setColumnKey, - textboxColumns, + textboxColumns: _textboxColumns, setTextboxColumns, }: ColumnDialogProps) { const [isSubmitted, setIsSubmitted] = useState(false); diff --git a/src/components/items/ItemMasterDataManagement/dialogs/FieldDialog.tsx b/src/components/items/ItemMasterDataManagement/dialogs/FieldDialog.tsx index d03a2f6d..ade9b07d 100644 --- a/src/components/items/ItemMasterDataManagement/dialogs/FieldDialog.tsx +++ b/src/components/items/ItemMasterDataManagement/dialogs/FieldDialog.tsx @@ -118,7 +118,7 @@ export function FieldDialog({ setNewFieldConditionTargetType, newFieldConditionFields, setNewFieldConditionFields, - newFieldConditionSections, + newFieldConditionSections: _newFieldConditionSections, setNewFieldConditionSections, tempConditionValue, setTempConditionValue, diff --git a/src/components/items/ItemMasterDataManagement/dialogs/LoadTemplateDialog.tsx b/src/components/items/ItemMasterDataManagement/dialogs/LoadTemplateDialog.tsx index 40d12db9..e7061e13 100644 --- a/src/components/items/ItemMasterDataManagement/dialogs/LoadTemplateDialog.tsx +++ b/src/components/items/ItemMasterDataManagement/dialogs/LoadTemplateDialog.tsx @@ -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: '자재' }, diff --git a/src/components/items/ItemMasterDataManagement/dialogs/MasterFieldDialog.tsx b/src/components/items/ItemMasterDataManagement/dialogs/MasterFieldDialog.tsx index 68e4d66b..67066ef5 100644 --- a/src/components/items/ItemMasterDataManagement/dialogs/MasterFieldDialog.tsx +++ b/src/components/items/ItemMasterDataManagement/dialogs/MasterFieldDialog.tsx @@ -62,7 +62,7 @@ export function MasterFieldDialog({ setNewMasterFieldInputType, newMasterFieldRequired, setNewMasterFieldRequired, - newMasterFieldCategory, + newMasterFieldCategory: _newMasterFieldCategory, setNewMasterFieldCategory, newMasterFieldDescription, setNewMasterFieldDescription, diff --git a/src/components/items/ItemMasterDataManagement/hooks/useAttributeManagement.ts b/src/components/items/ItemMasterDataManagement/hooks/useAttributeManagement.ts index 46f7d647..ec849032 100644 --- a/src/components/items/ItemMasterDataManagement/hooks/useAttributeManagement.ts +++ b/src/components/items/ItemMasterDataManagement/hooks/useAttributeManagement.ts @@ -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]); // 옵션 추가 diff --git a/src/components/items/ItemMasterDataManagement/hooks/useDeleteManagement.ts b/src/components/items/ItemMasterDataManagement/hooks/useDeleteManagement.ts index 49100312..80fb5498 100644 --- a/src/components/items/ItemMasterDataManagement/hooks/useDeleteManagement.ts +++ b/src/components/items/ItemMasterDataManagement/hooks/useDeleteManagement.ts @@ -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]); diff --git a/src/components/items/ItemMasterDataManagement/hooks/useInitialDataLoading.ts b/src/components/items/ItemMasterDataManagement/hooks/useInitialDataLoading.ts index 5ff96e8d..d563c299 100644 --- a/src/components/items/ItemMasterDataManagement/hooks/useInitialDataLoading.ts +++ b/src/components/items/ItemMasterDataManagement/hooks/useInitialDataLoading.ts @@ -152,7 +152,7 @@ export function useInitialDataLoading({ } hasInitialLoadRun.current = true; loadInitialData(); - // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return { diff --git a/src/components/items/ItemMasterDataManagement/hooks/useMasterFieldManagement.ts b/src/components/items/ItemMasterDataManagement/hooks/useMasterFieldManagement.ts index 53e23ca7..c06620e6 100644 --- a/src/components/items/ItemMasterDataManagement/hooks/useMasterFieldManagement.ts +++ b/src/components/items/ItemMasterDataManagement/hooks/useMasterFieldManagement.ts @@ -61,7 +61,7 @@ export interface UseMasterFieldManagementReturn { export function useMasterFieldManagement(): UseMasterFieldManagementReturn { const { - itemMasterFields, + itemMasterFields: _itemMasterFields, addItemMasterField, updateItemMasterField, deleteItemMasterField, diff --git a/src/components/items/ItemMasterDataManagement/hooks/usePageManagement.ts b/src/components/items/ItemMasterDataManagement/hooks/usePageManagement.ts index 7a00085f..0b3f8e5a 100644 --- a/src/components/items/ItemMasterDataManagement/hooks/usePageManagement.ts +++ b/src/components/items/ItemMasterDataManagement/hooks/usePageManagement.ts @@ -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); diff --git a/src/components/items/ItemMasterDataManagement/hooks/useSectionManagement.ts b/src/components/items/ItemMasterDataManagement/hooks/useSectionManagement.ts index 3d62232a..175489ad 100644 --- a/src/components/items/ItemMasterDataManagement/hooks/useSectionManagement.ts +++ b/src/components/items/ItemMasterDataManagement/hooks/useSectionManagement.ts @@ -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); diff --git a/src/components/items/ItemMasterDataManagement/hooks/useTabManagement.ts b/src/components/items/ItemMasterDataManagement/hooks/useTabManagement.ts index 92cec2a3..ae754d92 100644 --- a/src/components/items/ItemMasterDataManagement/hooks/useTabManagement.ts +++ b/src/components/items/ItemMasterDataManagement/hooks/useTabManagement.ts @@ -204,7 +204,7 @@ export function useTabManagement(): UseTabManagementReturn { if (isNumericKey && !currentFieldIds.has(activeAttributeTab)) { setActiveAttributeTab('units'); } - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [itemMasterFields]); // 메인 탭 핸들러 diff --git a/src/components/items/ItemMasterDataManagement/hooks/useTemplateManagement.ts b/src/components/items/ItemMasterDataManagement/hooks/useTemplateManagement.ts index f8deef97..b8454fa5 100644 --- a/src/components/items/ItemMasterDataManagement/hooks/useTemplateManagement.ts +++ b/src/components/items/ItemMasterDataManagement/hooks/useTemplateManagement.ts @@ -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 기반 필드 연결/해제 diff --git a/src/components/items/ItemMasterDataManagement/services/masterFieldService.ts b/src/components/items/ItemMasterDataManagement/services/masterFieldService.ts index ab2d2b81..56df2ca3 100644 --- a/src/components/items/ItemMasterDataManagement/services/masterFieldService.ts +++ b/src/components/items/ItemMasterDataManagement/services/masterFieldService.ts @@ -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 ===== diff --git a/src/components/items/ItemMasterDataManagement/tabs/HierarchyTab/index.tsx b/src/components/items/ItemMasterDataManagement/tabs/HierarchyTab/index.tsx index cb01efe5..0ae44550 100644 --- a/src/components/items/ItemMasterDataManagement/tabs/HierarchyTab/index.tsx +++ b/src/components/items/ItemMasterDataManagement/tabs/HierarchyTab/index.tsx @@ -119,7 +119,7 @@ export function HierarchyTab({ handleEditField, moveField, // 2025-11-26 추가: 섹션/필드 불러오기 - setIsImportSectionDialogOpen, + setIsImportSectionDialogOpen: _setIsImportSectionDialogOpen, setIsImportFieldDialogOpen, setImportFieldTargetSectionId, // 2025-11-27 추가: BOM 항목 API 함수 diff --git a/src/components/items/ItemMasterDataManagement/tabs/MasterFieldTab/index.tsx b/src/components/items/ItemMasterDataManagement/tabs/MasterFieldTab/index.tsx index 6ffd5ff4..8889fc5f 100644 --- a/src/components/items/ItemMasterDataManagement/tabs/MasterFieldTab/index.tsx +++ b/src/components/items/ItemMasterDataManagement/tabs/MasterFieldTab/index.tsx @@ -47,8 +47,8 @@ export function MasterFieldTab({ setIsMasterFieldDialogOpen, handleEditMasterField, handleDeleteMasterField, - hasUnsavedChanges, - pendingChanges + hasUnsavedChanges: _hasUnsavedChanges, + pendingChanges: _pendingChanges }: MasterFieldTabProps) { return ( @@ -60,11 +60,11 @@ export function MasterFieldTab({ 재사용 가능한 항목을 관리합니다. 섹션에 연결하여 사용할 수 있습니다. {/* 변경사항 배지 - 나중에 사용 예정으로 임시 숨김 */} - {false && hasUnsavedChanges && pendingChanges.masterFields.length > 0 && ( + {/* {hasUnsavedChanges && pendingChanges.masterFields.length > 0 && ( {pendingChanges.masterFields.length}개 변경 - )} + )} */} + + ); +} diff --git a/src/components/molecules/DateRangeSelector.tsx b/src/components/molecules/DateRangeSelector.tsx index 8150d15f..06dc86dc 100644 --- a/src/components/molecules/DateRangeSelector.tsx +++ b/src/components/molecules/DateRangeSelector.tsx @@ -90,7 +90,7 @@ export function DateRangeSelector({ extraActions, hidePresets = false, hideDateInputs = false, - dateInputWidth = 'w-[140px]', + dateInputWidth: _dateInputWidth = 'w-[140px]', presetsPosition = 'inline', variant = 'combined', }: DateRangeSelectorProps) { diff --git a/src/components/molecules/FormField.tsx b/src/components/molecules/FormField.tsx index 637718c7..b775def8 100644 --- a/src/components/molecules/FormField.tsx +++ b/src/components/molecules/FormField.tsx @@ -11,7 +11,6 @@ import { AlertCircle } from "lucide-react"; import { PhoneInput } from "../ui/phone-input"; import { BusinessNumberInput } from "../ui/business-number-input"; import { PersonalNumberInput } from "../ui/personal-number-input"; -import { NumberInput } from "../ui/number-input"; import { CurrencyInput } from "../ui/currency-input"; import { QuantityInput } from "../ui/quantity-input"; import { DatePicker } from "../ui/date-picker"; @@ -104,9 +103,9 @@ export function FormField({ // 새 입력 타입 전용 옵션 showValidation, maskBack, - allowDecimal, - decimalPlaces, - useComma, + allowDecimal: _allowDecimal, + decimalPlaces: _decimalPlaces, + useComma: _useComma, suffix, showButtons, maxLength, diff --git a/src/components/molecules/MobileFilter.tsx b/src/components/molecules/MobileFilter.tsx index e6fdfedb..78b1e91a 100644 --- a/src/components/molecules/MobileFilter.tsx +++ b/src/components/molecules/MobileFilter.tsx @@ -96,7 +96,7 @@ function countActiveFilters( /** * 필터 필드 요약 텍스트 생성 */ -function getFieldSummary( +function _getFieldSummary( field: FilterFieldConfig, value: string | string[] | undefined ): string { diff --git a/src/components/molecules/StandardDialog.tsx b/src/components/molecules/StandardDialog.tsx index 914ae90d..7b55282e 100644 --- a/src/components/molecules/StandardDialog.tsx +++ b/src/components/molecules/StandardDialog.tsx @@ -64,7 +64,7 @@ export function StandardDialog({ children, footer, size = "md", - showClose = true, + showClose: _showClose = true, className, }: StandardDialogProps) { return ( diff --git a/src/components/molecules/StatusBadge.tsx b/src/components/molecules/StatusBadge.tsx index a1de77fc..d25107dc 100644 --- a/src/components/molecules/StatusBadge.tsx +++ b/src/components/molecules/StatusBadge.tsx @@ -3,7 +3,6 @@ import { Badge } from "@/components/ui/badge"; import { BadgeSm } from "@/components/atoms/BadgeSm"; import { LucideIcon } from "lucide-react"; -import { BADGE_STYLE_PRESETS, type StatusStylePreset } from "@/lib/utils/status-config"; /** * 상태 뱃지 컴포넌트 @@ -61,7 +60,7 @@ export function StatusBadge({ icon: Icon, className = "", size = "md", - showDot + showDot: _showDot }: StatusBadgeProps) { // variant에 따른 기본 스타일 const variantStyles: Record = { diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts index 18d26d22..c36fc378 100644 --- a/src/components/molecules/index.ts +++ b/src/components/molecules/index.ts @@ -15,4 +15,6 @@ export type { Quarter } from "./YearQuarterFilter"; export { GenericCRUDDialog } from "./GenericCRUDDialog"; export type { GenericCRUDDialogProps, CRUDFieldDefinition } from "./GenericCRUDDialog"; -export { ReorderButtons } from "./ReorderButtons"; \ No newline at end of file +export { ReorderButtons } from "./ReorderButtons"; + +export { CopyableCell } from "./CopyableCell"; \ No newline at end of file diff --git a/src/components/orders/OrderRegistration.tsx b/src/components/orders/OrderRegistration.tsx index f1425c01..a9ecbba1 100644 --- a/src/components/orders/OrderRegistration.tsx +++ b/src/components/orders/OrderRegistration.tsx @@ -22,7 +22,6 @@ import { Button } from "@/components/ui/button"; import { QuantityInput } from "@/components/ui/quantity-input"; import { NumberInput } from "@/components/ui/number-input"; import { PhoneInput } from "@/components/ui/phone-input"; -import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { getPresetStyle } from "@/lib/utils/status-config"; @@ -187,7 +186,7 @@ export function OrderRegistration({ }); const [isQuotationDialogOpen, setIsQuotationDialogOpen] = useState(false); const [isItemDialogOpen, setIsItemDialogOpen] = useState(false); - const [isSaving, setIsSaving] = useState(false); + const [, setIsSaving] = useState(false); const [fieldErrors, setFieldErrors] = useState({}); // Config 선택 diff --git a/src/components/orders/OrderSalesDetailEdit.tsx b/src/components/orders/OrderSalesDetailEdit.tsx index 5313b87f..cdfeee65 100644 --- a/src/components/orders/OrderSalesDetailEdit.tsx +++ b/src/components/orders/OrderSalesDetailEdit.tsx @@ -15,8 +15,6 @@ import { useRouter } from "next/navigation"; import { Input } from "@/components/ui/input"; import { DatePicker } from "@/components/ui/date-picker"; import { Textarea } from "@/components/ui/textarea"; -import { PhoneInput } from "@/components/ui/phone-input"; -import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { @@ -39,7 +37,7 @@ import { toast } from "sonner"; import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate"; import { orderSalesConfig } from "./orderSalesConfig"; import { BadgeSm } from "@/components/atoms/BadgeSm"; -import { formatAmount, formatNumber } from "@/lib/utils/amount"; +import { formatNumber } from "@/lib/utils/amount"; import { OrderItem, getOrderById, @@ -91,7 +89,7 @@ interface EditFormData { } // 옵션 타입 정의 -interface SelectOption { +interface _SelectOption { value: string; label: string; } @@ -129,7 +127,7 @@ export function OrderSalesDetailEdit({ orderId }: OrderSalesDetailEditProps) { const [form, setForm] = useState(null); const [loading, setLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); + const [, _setIsSaving] = useState(false); const [expandedProducts, setExpandedProducts] = useState>(new Set()); // 공통코드 옵션 (useCommonCodes 훅) @@ -261,7 +259,7 @@ export function OrderSalesDetailEdit({ orderId }: OrderSalesDetailEditProps) { }, [orderId, router]); - const handleCancel = () => { + const _handleCancel = () => { // V2 패턴: ?mode=view로 이동 router.push(`/sales/order-management-sales/${orderId}?mode=view`); }; diff --git a/src/components/orders/OrderSalesDetailView.tsx b/src/components/orders/OrderSalesDetailView.tsx index 2eda838d..ddef99aa 100644 --- a/src/components/orders/OrderSalesDetailView.tsx +++ b/src/components/orders/OrderSalesDetailView.tsx @@ -23,7 +23,6 @@ import { TableRow, } from "@/components/ui/table"; import { - FileText, Factory, XCircle, FileSpreadsheet, @@ -259,11 +258,11 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) { loadOrder(); }, [orderId]); - const handleBack = () => { + const _handleBack = () => { router.push("/sales/order-management-sales"); }; - const handleEdit = () => { + const _handleEdit = () => { // V2 패턴: ?mode=edit로 이동 router.push(`/sales/order-management-sales/${orderId}?mode=edit`); }; @@ -273,7 +272,7 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) { router.push(`/sales/order-management-sales/${orderId}/production-order`); }; - const handleViewProductionOrder = () => { + const _handleViewProductionOrder = () => { // 생산지시 목록 페이지로 이동 (수주관리 내부) router.push(`/sales/order-management-sales/production-orders`); }; diff --git a/src/components/orders/actions.ts b/src/components/orders/actions.ts index 0d168503..98ac761d 100644 --- a/src/components/orders/actions.ts +++ b/src/components/orders/actions.ts @@ -218,7 +218,7 @@ interface ApiOrderStats { confirmed_amount: number; } -interface ApiResponse { +interface _ApiResponse { success: boolean; message: string; data: T; diff --git a/src/components/orders/documents/ContractDocument.tsx b/src/components/orders/documents/ContractDocument.tsx index a2647b3d..b59a80ee 100644 --- a/src/components/orders/documents/ContractDocument.tsx +++ b/src/components/orders/documents/ContractDocument.tsx @@ -61,11 +61,11 @@ export function ContractDocument({ companyBusinessNumber, companyContact, companyAddress, - items = [], + items: _items = [], products, subtotal = 0, discountRate = 0, - totalAmount = 0, + totalAmount: _totalAmount = 0, remarks, }: ContractDocumentProps) { const discountAmount = Math.round(subtotal * (discountRate / 100)); diff --git a/src/components/orders/documents/SalesOrderDocument.tsx b/src/components/orders/documents/SalesOrderDocument.tsx index b1d70b10..ef5ce868 100644 --- a/src/components/orders/documents/SalesOrderDocument.tsx +++ b/src/components/orders/documents/SalesOrderDocument.tsx @@ -111,7 +111,7 @@ export function SalesOrderDocument({ recipientName = "-", recipientContact = "-", shutterCount = 0, - items = [], + items: _items = [], products = [], remarks, }: SalesOrderDocumentProps) { diff --git a/src/components/orders/documents/TransactionDocument.tsx b/src/components/orders/documents/TransactionDocument.tsx index f10c9392..ba1983db 100644 --- a/src/components/orders/documents/TransactionDocument.tsx +++ b/src/components/orders/documents/TransactionDocument.tsx @@ -55,20 +55,20 @@ export function TransactionDocument({ orderNumber, orderDate, client, - clientBusinessNumber = "123-45-67890", + clientBusinessNumber: _clientBusinessNumber = "123-45-67890", clientCeo = "대표자", clientContact = "010-0123-4567", - clientAddress = "서울시 강남구", + clientAddress: _clientAddress = "서울시 강남구", clientSiteName = "-", companyName = "(주)케이디산업", companyCeo = "홍길동", companyBusinessNumber = "123-45-67890", - companyContact = "02-1234-5678", + companyContact: _companyContact = "02-1234-5678", companyAddress = "서울 강남구 테헤란로 123", items = [], subtotal = 0, discountRate = 0, - totalAmount = 0, + totalAmount: _totalAmount = 0, }: TransactionDocumentProps) { const discountAmount = Math.round(subtotal * (discountRate / 100)); const afterDiscount = subtotal - discountAmount; diff --git a/src/components/organisms/DataTable.tsx b/src/components/organisms/DataTable.tsx index 30b7033b..9e9006d3 100644 --- a/src/components/organisms/DataTable.tsx +++ b/src/components/organisms/DataTable.tsx @@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button"; import { ChevronLeft, ChevronRight, LucideIcon } from "lucide-react"; import { StatusBadge, StatusType } from "@/components/molecules/StatusBadge"; import { IconWithBadge } from "@/components/molecules/IconWithBadge"; -import { TableActions, TableAction } from "@/components/molecules/TableActions"; +import { TableActions } from "@/components/molecules/TableActions"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { formatNumber } from '@/lib/utils/amount'; @@ -100,20 +100,22 @@ function renderCell(column: Column, value: any, row: T, index?: number): R case "number": return {formatNumber(formattedValue)}; - case "currency": + case "currency": { const locale = column.currencyConfig?.locale || "ko-KR"; const currency = column.currencyConfig?.currency || "KRW"; const currencyValue = typeof formattedValue === "number" ? formattedValue.toLocaleString(locale, { style: "currency", currency }) : formattedValue; return {currencyValue}; + } - case "date": + case "date": { if (!formattedValue) return "-"; const dateValue = new Date(formattedValue); return {dateValue.toLocaleDateString("ko-KR")}; + } - case "datetime": + case "datetime": { if (!formattedValue) return "-"; const datetimeValue = new Date(formattedValue); return ( @@ -121,6 +123,7 @@ function renderCell(column: Column, value: any, row: T, index?: number): R {datetimeValue.toLocaleDateString("ko-KR")} {datetimeValue.toLocaleTimeString("ko-KR", { hour: "2-digit", minute: "2-digit" })} ); + } case "status": return ( @@ -141,7 +144,7 @@ function renderCell(column: Column, value: any, row: T, index?: number): R ); - case "icon": + case "icon": { const IconComponent = formattedValue as LucideIcon; return IconComponent ? ( (column: Column, value: any, row: T, index?: number): R size={column.iconConfig?.size} /> ) : null; + } case "iconBadge": // value should be { icon, label, badge } @@ -168,10 +172,11 @@ function renderCell(column: Column, value: any, row: T, index?: number): R } return formattedValue; - case "actions": + case "actions": { // render 함수가 TableAction[] 배열을 반환해야 함 const actions = Array.isArray(formattedValue) ? formattedValue : []; return actions.length > 0 ? : null; + } case "custom": // 커스텀 타입은 이미 render 함수에서 처리됨 diff --git a/src/components/organisms/MobileCard.tsx b/src/components/organisms/MobileCard.tsx index 43a13b66..ad4a2903 100644 --- a/src/components/organisms/MobileCard.tsx +++ b/src/components/organisms/MobileCard.tsx @@ -1,13 +1,14 @@ 'use client'; import { ReactNode, ComponentType, memo, useState } from 'react'; -import { Card, CardContent } from '@/components/ui/card'; +import {} from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Separator } from '@/components/ui/separator'; import { cn } from '@/lib/utils'; -import { LucideIcon, ChevronDown } from 'lucide-react'; +import { ChevronDown } from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline'; @@ -121,7 +122,7 @@ export interface MobileCardProps { export function MobileCard({ // 공통 title, - id, + id: _id, subtitle, description, icon, diff --git a/src/components/organisms/SearchFilter.tsx b/src/components/organisms/SearchFilter.tsx index bc95ec1f..c58196a3 100644 --- a/src/components/organisms/SearchFilter.tsx +++ b/src/components/organisms/SearchFilter.tsx @@ -17,8 +17,8 @@ export function SearchFilter({ searchValue, onSearchChange, searchPlaceholder = "검색", - filterButton = true, - onFilterClick, + filterButton: _filterButton = true, + onFilterClick: _onFilterClick, extraActions }: SearchFilterProps) { const [isMobile, setIsMobile] = useState(false); diff --git a/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx b/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx index ed443ae5..a412657f 100644 --- a/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx +++ b/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx @@ -139,7 +139,7 @@ export function ShipmentDetail({ id }: ShipmentDetailProps) { loadData(); }, [loadData]); - const handleGoBack = useCallback(() => { + const _handleGoBack = useCallback(() => { router.push('/ko/outbound/shipments'); }, [router]); diff --git a/src/components/outbound/ShipmentManagement/ShipmentList.tsx b/src/components/outbound/ShipmentManagement/ShipmentList.tsx index 399a66bc..d9e4a478 100644 --- a/src/components/outbound/ShipmentManagement/ShipmentList.tsx +++ b/src/components/outbound/ShipmentManagement/ShipmentList.tsx @@ -44,7 +44,7 @@ import { SHIPMENT_STATUS_STYLES, DELIVERY_METHOD_LABELS, } from './types'; -import type { ShipmentItem, ShipmentStatus, ShipmentStats } from './types'; +import type { ShipmentItem, ShipmentStatus } from './types'; import { parseISO } from 'date-fns'; import { getLocalDateString } from '@/lib/utils/date'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; @@ -228,16 +228,16 @@ export function ShipmentList() { // 테이블 컬럼 (11개) columns: [ { key: 'no', label: '번호', className: 'w-[50px] text-center' }, - { key: 'lotNo', label: '로트번호', className: 'min-w-[120px]' }, - { key: 'siteName', label: '현장명', className: 'min-w-[100px]' }, - { key: 'orderCustomer', label: '수주처', className: 'min-w-[100px]' }, - { key: 'receiver', label: '수신자', className: 'w-[80px] text-center' }, - { key: 'receiverAddress', label: '수신주소', className: 'min-w-[140px]' }, - { key: 'receiverCompany', label: '수신처', className: 'min-w-[100px]' }, + { key: 'lotNo', label: '로트번호', className: 'min-w-[120px]', copyable: true }, + { key: 'siteName', label: '현장명', className: 'min-w-[100px]', copyable: true }, + { key: 'orderCustomer', label: '수주처', className: 'min-w-[100px]', copyable: true }, + { key: 'receiver', label: '수신자', className: 'w-[80px] text-center', copyable: true }, + { key: 'receiverAddress', label: '수신주소', className: 'min-w-[140px]', copyable: true }, + { key: 'receiverCompany', label: '수신처', className: 'min-w-[100px]', copyable: true }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, - { key: 'dispatch', label: '배차', className: 'w-[80px] text-center' }, - { key: 'writer', label: '작성자', className: 'w-[80px] text-center' }, - { key: 'shipmentDate', label: '출고일', className: 'w-[100px] text-center' }, + { key: 'dispatch', label: '배차', className: 'w-[80px] text-center', copyable: true }, + { key: 'writer', label: '작성자', className: 'w-[80px] text-center', copyable: true }, + { key: 'shipmentDate', label: '출고일', className: 'w-[100px] text-center', copyable: true }, ], // 배송방식 필터 diff --git a/src/components/outbound/ShipmentManagement/documents/TransactionStatement.tsx b/src/components/outbound/ShipmentManagement/documents/TransactionStatement.tsx index 29e1d175..957eb0b3 100644 --- a/src/components/outbound/ShipmentManagement/documents/TransactionStatement.tsx +++ b/src/components/outbound/ShipmentManagement/documents/TransactionStatement.tsx @@ -16,7 +16,7 @@ interface TransactionStatementProps { export function TransactionStatement({ data }: TransactionStatementProps) { // 제품 합계 계산 - const totalAmount = data.products.reduce((sum, product) => { + const totalAmount = data.products.reduce((sum, _product) => { // 실제로는 단가 * 수량 return sum + 0; }, 0); @@ -107,7 +107,7 @@ export function TransactionStatement({ data }: TransactionStatementProps) { - {data.products.map((product, index) => ( + {data.products.map((product, _index) => ( {product.no} {product.itemName} diff --git a/src/components/outbound/VehicleDispatchManagement/VehicleDispatchList.tsx b/src/components/outbound/VehicleDispatchManagement/VehicleDispatchList.tsx index 30f3dbf9..95e22a2f 100644 --- a/src/components/outbound/VehicleDispatchManagement/VehicleDispatchList.tsx +++ b/src/components/outbound/VehicleDispatchManagement/VehicleDispatchList.tsx @@ -163,18 +163,18 @@ export function VehicleDispatchList() { // 테이블 컬럼 (13개) columns: [ { key: 'no', label: 'No.', className: 'w-[50px] text-center' }, - { key: 'dispatchNo', label: '배차번호', className: 'min-w-[130px]' }, - { key: 'lotNo', label: '로트번호', className: 'min-w-[120px]' }, - { key: 'siteName', label: '현장명', className: 'min-w-[100px]' }, - { key: 'orderCustomer', label: '수주처', className: 'min-w-[100px]' }, - { key: 'logisticsCompany', label: '물류업체', className: 'min-w-[90px]' }, - { key: 'supplyAmount', label: '공급가액', className: 'w-[100px] text-right' }, - { key: 'vat', label: '부가세', className: 'w-[90px] text-right' }, - { key: 'totalAmount', label: '합계', className: 'w-[100px] text-right' }, - { key: 'freightCostType', label: '선/착불', className: 'w-[70px] text-center' }, - { key: 'writer', label: '작성자', className: 'w-[80px] text-center' }, + { key: 'dispatchNo', label: '배차번호', className: 'min-w-[130px]', copyable: true }, + { key: 'lotNo', label: '로트번호', className: 'min-w-[120px]', copyable: true }, + { key: 'siteName', label: '현장명', className: 'min-w-[100px]', copyable: true }, + { key: 'orderCustomer', label: '수주처', className: 'min-w-[100px]', copyable: true }, + { key: 'logisticsCompany', label: '물류업체', className: 'min-w-[90px]', copyable: true }, + { key: 'supplyAmount', label: '공급가액', className: 'w-[100px] text-right', copyable: true }, + { key: 'vat', label: '부가세', className: 'w-[90px] text-right', copyable: true }, + { key: 'totalAmount', label: '합계', className: 'w-[100px] text-right', copyable: true }, + { key: 'freightCostType', label: '선/착불', className: 'w-[70px] text-center', copyable: true }, + { key: 'writer', label: '작성자', className: 'w-[80px] text-center', copyable: true }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, - { key: 'remarks', label: '비고', className: 'min-w-[100px]' }, + { key: 'remarks', label: '비고', className: 'min-w-[100px]', copyable: true }, ], // 상태 필터 diff --git a/src/components/pricing-distribution/PriceDistributionDetail.tsx b/src/components/pricing-distribution/PriceDistributionDetail.tsx index 395b16c4..25c453cd 100644 --- a/src/components/pricing-distribution/PriceDistributionDetail.tsx +++ b/src/components/pricing-distribution/PriceDistributionDetail.tsx @@ -201,7 +201,7 @@ export function PriceDistributionDetail({ id, mode: propMode }: Props) { }; // 상태 뱃지 - const renderStatusBadge = (status: DistributionStatus) => { + const _renderStatusBadge = (status: DistributionStatus) => { const style = DISTRIBUTION_STATUS_STYLES[status]; const label = DISTRIBUTION_STATUS_LABELS[status]; return ( diff --git a/src/components/pricing-distribution/PriceDistributionList.tsx b/src/components/pricing-distribution/PriceDistributionList.tsx index c6d45537..597228d4 100644 --- a/src/components/pricing-distribution/PriceDistributionList.tsx +++ b/src/components/pricing-distribution/PriceDistributionList.tsx @@ -50,7 +50,7 @@ import { export function PriceDistributionList() { const router = useRouter(); const [data, setData] = useState([]); - const [isLoading, setIsLoading] = useState(true); + const [_isLoading, setIsLoading] = useState(true); const [showRegisterDialog, setShowRegisterDialog] = useState(false); const [isRegistering, setIsRegistering] = useState(false); const pageSize = 20; @@ -150,11 +150,11 @@ export function PriceDistributionList() { // 테이블 컬럼 const tableColumns: TableColumn[] = useMemo(() => [ { key: 'rowNumber', label: '번호', className: 'w-[60px] text-center' }, - { key: 'distributionNo', label: '단가배포번호', className: 'min-w-[120px]' }, - { key: 'distributionName', label: '단가배포명', className: 'min-w-[150px]' }, + { key: 'distributionNo', label: '단가배포번호', className: 'min-w-[120px]', copyable: true }, + { key: 'distributionName', label: '단가배포명', className: 'min-w-[150px]', copyable: true }, { key: 'status', label: '상태', className: 'min-w-[100px]' }, - { key: 'author', label: '작성자', className: 'min-w-[100px]' }, - { key: 'createdAt', label: '등록일', className: 'min-w-[120px]' }, + { key: 'author', label: '작성자', className: 'min-w-[100px]', copyable: true }, + { key: 'createdAt', label: '등록일', className: 'min-w-[120px]', copyable: true }, ], []); // 테이블 행 렌더링 diff --git a/src/components/pricing-table-management/PricingTableListClient.tsx b/src/components/pricing-table-management/PricingTableListClient.tsx index 7441ae7f..88e5cd60 100644 --- a/src/components/pricing-table-management/PricingTableListClient.tsx +++ b/src/components/pricing-table-management/PricingTableListClient.tsx @@ -30,7 +30,7 @@ export default function PricingTableListClient() { // ===== 상태 ===== const [allItems, setAllItems] = useState([]); - const [stats, setStats] = useState({ total: 0, active: 0, inactive: 0 }); + const [_stats, setStats] = useState({ total: 0, active: 0, inactive: 0 }); const [isLoading, setIsLoading] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteTargetId, setDeleteTargetId] = useState(null); @@ -182,19 +182,19 @@ export default function PricingTableListClient() { columns: [ { key: 'no', label: '번호', className: 'w-[50px] text-center' }, - { key: 'pricingCode', label: '단가번호', className: 'w-[100px]' }, - { key: 'itemCode', label: '품목코드', className: 'w-[100px]' }, + { key: 'pricingCode', label: '단가번호', className: 'w-[100px]', copyable: true }, + { key: 'itemCode', label: '품목코드', className: 'w-[100px]', copyable: true }, { key: 'itemType', label: '품목유형', className: 'w-[80px]' }, - { key: 'itemName', label: '품목명', className: 'min-w-[120px]' }, - { key: 'specification', label: '규격', className: 'w-[70px]' }, - { key: 'unit', label: '단위', className: 'w-[50px] text-center' }, - { key: 'purchasePrice', label: '매입단가', className: 'w-[90px] text-right' }, - { key: 'processingCost', label: '가공비', className: 'w-[80px] text-right' }, - { key: 'marginRate', label: '마진율', className: 'w-[70px] text-right' }, - { key: 'sellingPrice', label: '판매단가', className: 'w-[90px] text-right' }, + { key: 'itemName', label: '품목명', className: 'min-w-[120px]', copyable: true }, + { key: 'specification', label: '규격', className: 'w-[70px]', copyable: true }, + { key: 'unit', label: '단위', className: 'w-[50px] text-center', copyable: true }, + { key: 'purchasePrice', label: '매입단가', className: 'w-[90px] text-right', copyable: true }, + { key: 'processingCost', label: '가공비', className: 'w-[80px] text-right', copyable: true }, + { key: 'marginRate', label: '마진율', className: 'w-[70px] text-right', copyable: true }, + { key: 'sellingPrice', label: '판매단가', className: 'w-[90px] text-right', copyable: true }, { key: 'status', label: '상태', className: 'w-[70px] text-center' }, - { key: 'author', label: '작성자', className: 'w-[80px] text-center' }, - { key: 'changedDate', label: '변경일', className: 'w-[100px] text-center' }, + { key: 'author', label: '작성자', className: 'w-[80px] text-center', copyable: true }, + { key: 'changedDate', label: '변경일', className: 'w-[100px] text-center', copyable: true }, ], clientSideFiltering: true, diff --git a/src/components/pricing/PricingFormClient.tsx b/src/components/pricing/PricingFormClient.tsx index 3f420176..410b31f3 100644 --- a/src/components/pricing/PricingFormClient.tsx +++ b/src/components/pricing/PricingFormClient.tsx @@ -57,7 +57,6 @@ const FIELD_NAME_MAP: Record = { import type { PricingData, - PricingFormData, ItemInfo, RoundingRule, ItemType, @@ -192,7 +191,7 @@ export function PricingFormClient({ setSalesPrice(finalPrice); } } - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [purchasePrice, processingCost, loss, roundingRule, roundingUnit]); // 마진 금액 계산 diff --git a/src/components/pricing/PricingHistoryDialog.tsx b/src/components/pricing/PricingHistoryDialog.tsx index 56493bd7..748d66b3 100644 --- a/src/components/pricing/PricingHistoryDialog.tsx +++ b/src/components/pricing/PricingHistoryDialog.tsx @@ -13,7 +13,7 @@ import { } from '@/components/ui/dialog'; import { Badge } from '@/components/ui/badge'; import { getPresetStyle } from '@/lib/utils/status-config'; -import { Separator } from '@/components/ui/separator'; +// Separator removed - unused import { History } from 'lucide-react'; import { formatNumber } from '@/lib/utils/amount'; import type { PricingData } from './types'; diff --git a/src/components/pricing/PricingListClient.tsx b/src/components/pricing/PricingListClient.tsx index 3217dc02..9c817408 100644 --- a/src/components/pricing/PricingListClient.tsx +++ b/src/components/pricing/PricingListClient.tsx @@ -63,7 +63,7 @@ export function PricingListClient({ }; // 탭별 데이터 수 계산 (통계용) - const filteredData = useMemo(() => { + const _filteredData = useMemo(() => { let result = [...data]; // 탭 필터 @@ -174,7 +174,7 @@ export function PricingListClient({ router.push(`/sales/pricing-management/${item.id}?mode=edit`); }; - const handleHistory = (item: PricingListItem) => { + const _handleHistory = (_item: PricingListItem) => { // TODO: 이력 다이얼로그 열기 }; @@ -200,15 +200,15 @@ export function PricingListClient({ const tableColumns: TableColumn[] = useMemo(() => [ { key: 'rowNumber', label: '번호', className: 'w-[60px] text-center' }, { key: 'itemType', label: '품목유형', className: 'min-w-[100px]' }, - { key: 'itemCode', label: '품목코드', className: 'min-w-[120px]' }, - { key: 'itemName', label: '품목명', className: 'min-w-[150px]' }, - { key: 'specification', label: '규격', className: 'min-w-[100px]', hideOnMobile: true }, - { key: 'unit', label: '단위', className: 'min-w-[60px]', hideOnMobile: true }, - { key: 'purchasePrice', label: '매입단가', className: 'min-w-[100px] text-right', hideOnTablet: true }, - { key: 'processingCost', label: '가공비', className: 'min-w-[80px] text-right', hideOnTablet: true }, - { key: 'salesPrice', label: '판매단가', className: 'min-w-[100px] text-right' }, - { key: 'marginRate', label: '마진율', className: 'min-w-[80px] text-right', hideOnMobile: true }, - { key: 'effectiveDate', label: '적용일', className: 'min-w-[100px]', hideOnMobile: true }, + { key: 'itemCode', label: '품목코드', className: 'min-w-[120px]', copyable: true }, + { key: 'itemName', label: '품목명', className: 'min-w-[150px]', copyable: true }, + { key: 'specification', label: '규격', className: 'min-w-[100px]', hideOnMobile: true, copyable: true }, + { key: 'unit', label: '단위', className: 'min-w-[60px]', hideOnMobile: true, copyable: true }, + { key: 'purchasePrice', label: '매입단가', className: 'min-w-[100px] text-right', hideOnTablet: true, copyable: true }, + { key: 'processingCost', label: '가공비', className: 'min-w-[80px] text-right', hideOnTablet: true, copyable: true }, + { key: 'salesPrice', label: '판매단가', className: 'min-w-[100px] text-right', copyable: true }, + { key: 'marginRate', label: '마진율', className: 'min-w-[80px] text-right', hideOnMobile: true, copyable: true }, + { key: 'effectiveDate', label: '적용일', className: 'min-w-[100px]', hideOnMobile: true, copyable: true }, { key: 'status', label: '상태', className: 'min-w-[80px]' }, ], []); diff --git a/src/components/pricing/actions.ts b/src/components/pricing/actions.ts index 2cc2738e..cd77875c 100644 --- a/src/components/pricing/actions.ts +++ b/src/components/pricing/actions.ts @@ -18,7 +18,7 @@ import { executeServerAction } from '@/lib/api/execute-server-action'; import type { PricingData, ItemInfo } from './types'; // API 응답 타입 -interface ApiResponse { +interface _ApiResponse { success: boolean; data: T; message: string; diff --git a/src/components/pricing/types.ts b/src/components/pricing/types.ts index c6cf81d4..63480dd0 100644 --- a/src/components/pricing/types.ts +++ b/src/components/pricing/types.ts @@ -2,7 +2,6 @@ * 단가관리 타입 정의 */ -import type { LucideIcon } from 'lucide-react'; // ===== 단가 리비전 ===== diff --git a/src/components/process-management/ProcessDetailClientV2.tsx b/src/components/process-management/ProcessDetailClientV2.tsx index 2de27134..72a1d2c6 100644 --- a/src/components/process-management/ProcessDetailClientV2.tsx +++ b/src/components/process-management/ProcessDetailClientV2.tsx @@ -7,7 +7,7 @@ * 기존 ProcessDetail, ProcessForm 컴포넌트 활용 */ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { ProcessDetail } from './ProcessDetail'; import { ProcessForm } from './ProcessForm'; @@ -27,7 +27,7 @@ interface ProcessDetailClientV2Props { const BASE_PATH = '/ko/master-data/process-management'; export function ProcessDetailClientV2({ processId, initialMode }: ProcessDetailClientV2Props) { - const router = useRouter(); + const _router = useRouter(); const searchParams = useSearchParams(); // URL 쿼리에서 모드 결정 diff --git a/src/components/process-management/ProcessForm.tsx b/src/components/process-management/ProcessForm.tsx index d1a63cb7..12fb6a53 100644 --- a/src/components/process-management/ProcessForm.tsx +++ b/src/components/process-management/ProcessForm.tsx @@ -32,7 +32,7 @@ import { import { RuleModal } from './RuleModal'; import { toast } from 'sonner'; import type { Process, ClassificationRule, ProcessType, ProcessStep } from '@/types/process'; -import { PROCESS_TYPE_OPTIONS, PROCESS_CATEGORY_OPTIONS } from '@/types/process'; +import { PROCESS_CATEGORY_OPTIONS } from '@/types/process'; import { createProcess, updateProcess, @@ -54,7 +54,7 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) { // 기본 정보 상태 const [processName, setProcessName] = useState(initialData?.processName || ''); - const [processType, setProcessType] = useState( + const [processType, _setProcessType] = useState( initialData?.processType || '생산' ); const [department, setDepartment] = useState(initialData?.department || ''); @@ -68,7 +68,7 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) { const [isActive, setIsActive] = useState( initialData ? initialData.status === '사용중' : true ); - const [isLoading, setIsLoading] = useState(false); + const [, setIsLoading] = useState(false); // 중간검사/작업일지 설정 (Process 레벨) const [needsInspection, setNeedsInspection] = useState( @@ -122,7 +122,7 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) { } else if (processCategory && !categoryOptions.find(o => o.value === processCategory)) { setProcessCategory(''); } - }, [categoryOptions]); // eslint-disable-line react-hooks/exhaustive-deps + }, [categoryOptions]); // 개별 품목 목록 추출 const individualItems = classificationRules diff --git a/src/components/process-management/ProcessListClient.tsx b/src/components/process-management/ProcessListClient.tsx index 99fdf981..1778aa63 100644 --- a/src/components/process-management/ProcessListClient.tsx +++ b/src/components/process-management/ProcessListClient.tsx @@ -40,7 +40,7 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr // ===== 상태 ===== const [allProcesses, setAllProcesses] = useState(initialData); - const [stats, setStats] = useState(initialStats ?? { total: 0, active: 0, inactive: 0 }); + const [, setStats] = useState(initialStats ?? { total: 0, active: 0, inactive: 0 }); const [isLoading, setIsLoading] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteTargetId, setDeleteTargetId] = useState(null); @@ -100,11 +100,11 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr router.push('/ko/master-data/process-management?mode=new'); }, [router]); - const handleEdit = useCallback((process: Process) => { + const _handleEdit = useCallback((process: Process) => { router.push(`/ko/master-data/process-management/${process.id}?mode=edit`); }, [router]); - const handleDeleteClick = useCallback((processId: string) => { + const _handleDeleteClick = useCallback((processId: string) => { setDeleteTargetId(processId); setDeleteDialogOpen(true); }, []); @@ -270,7 +270,7 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr // API 액션 actions: { - getList: async (params?: ListParams) => { + getList: async (_params?: ListParams) => { try { const [listResult, statsResult] = await Promise.all([ getProcessList({ size: 1000 }), @@ -316,9 +316,9 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr { key: 'drag', label: '', className: 'w-[40px]' }, { key: 'checkbox', label: '', className: 'w-[50px]' }, { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'processCode', label: '공정번호', className: 'w-[120px]' }, - { key: 'processName', label: '공정명', className: 'min-w-[200px]' }, - { key: 'department', label: '담당부서', className: 'w-[120px]' }, + { key: 'processCode', label: '공정번호', className: 'w-[120px]', copyable: true }, + { key: 'processName', label: '공정명', className: 'min-w-[200px]', copyable: true }, + { key: 'department', label: '담당부서', className: 'w-[120px]', copyable: true }, { key: 'items', label: '품목', className: 'w-[80px] text-center' }, { key: 'inspection', label: '중간검사', className: 'w-[90px] text-center' }, { key: 'workLog', label: '작업일지', className: 'w-[90px] text-center' }, diff --git a/src/components/process-management/ProcessWorkLogContent.tsx b/src/components/process-management/ProcessWorkLogContent.tsx index 54689379..3053f772 100644 --- a/src/components/process-management/ProcessWorkLogContent.tsx +++ b/src/components/process-management/ProcessWorkLogContent.tsx @@ -9,7 +9,7 @@ */ import type { Process } from '@/types/process'; -import { DocumentHeader, SectionHeader } from '@/components/document-system'; +import { DocumentHeader } from '@/components/document-system'; const getDocumentCode = (processName: string): string => { if (processName.includes('스크린')) return 'WL-SCR'; diff --git a/src/components/process-management/RuleModal.tsx b/src/components/process-management/RuleModal.tsx index 95ff1972..9a43793c 100644 --- a/src/components/process-management/RuleModal.tsx +++ b/src/components/process-management/RuleModal.tsx @@ -77,19 +77,19 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule, processId, proc // 공정/구분 필터 상태 const [processFilter, setProcessFilter] = useState('all'); - const [categoryFilter, setCategoryFilter] = useState('all'); + const [, setCategoryFilter] = useState('all'); // 품목 목록 API 상태 const [itemList, setItemList] = useState([]); const [isItemsLoading, setIsItemsLoading] = useState(false); // 품목 유형 옵션 (common_codes에서 동적 조회) - const [itemTypeOptions, setItemTypeOptions] = useState>([ + const [, setItemTypeOptions] = useState>([ { value: 'all', label: '전체' }, ]); // 구분 필터 옵션 (공정 필터에 따라 변경) - const categoryFilterOptions = getCategoryFilterOptions(processFilter); + const _categoryFilterOptions = getCategoryFilterOptions(processFilter); // 품목 목록 로드 const loadItems = useCallback(async (q?: string, itemType?: string) => { diff --git a/src/components/process-management/StepForm.tsx b/src/components/process-management/StepForm.tsx index 9d88d383..f9ff0877 100644 --- a/src/components/process-management/StepForm.tsx +++ b/src/components/process-management/StepForm.tsx @@ -120,7 +120,7 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) { const [isInspectionSettingOpen, setIsInspectionSettingOpen] = useState(false); const [isInspectionPreviewOpen, setIsInspectionPreviewOpen] = useState(false); - const [isLoading, setIsLoading] = useState(false); + const [, setIsLoading] = useState(false); // 검사여부가 "사용"인지 확인 const isInspectionEnabled = needsInspection === '사용'; diff --git a/src/components/process-management/actions.ts b/src/components/process-management/actions.ts index fc40925b..84621187 100644 --- a/src/components/process-management/actions.ts +++ b/src/components/process-management/actions.ts @@ -69,7 +69,7 @@ interface ApiProcessItem { }; } -interface ApiResponse { +interface _ApiResponse { success: boolean; message: string; data: T; diff --git a/src/components/production/ProductionDashboard/index.tsx b/src/components/production/ProductionDashboard/index.tsx index a770299d..6733b04d 100644 --- a/src/components/production/ProductionDashboard/index.tsx +++ b/src/components/production/ProductionDashboard/index.tsx @@ -34,7 +34,7 @@ export default function ProductionDashboard() { // ===== 상태 관리 ===== const [selectedTab, setSelectedTab] = useState('all'); - const [processOptions, setProcessOptions] = useState([]); + const [, setProcessOptions] = useState([]); const [tabOptions, setTabOptions] = useState([{ value: 'all', label: '전체' }]); const [workOrders, setWorkOrders] = useState([]); const [workerStatus, setWorkerStatus] = useState([]); @@ -443,7 +443,7 @@ interface WorkerStatusRowProps { } function WorkerStatusRow({ worker }: WorkerStatusRowProps) { - const progressPercent = worker.assigned > 0 + const _progressPercent = worker.assigned > 0 ? Math.round((worker.completed / worker.assigned) * 100) : 0; diff --git a/src/components/production/ProductionOrders/actions.ts b/src/components/production/ProductionOrders/actions.ts index 29af77c5..9a964b9c 100644 --- a/src/components/production/ProductionOrders/actions.ts +++ b/src/components/production/ProductionOrders/actions.ts @@ -10,7 +10,6 @@ import type { ProductionOrderDetail, ProductionOrderStats, ProductionOrderListParams, - ProductionStatus, } from './types'; // ===== 변환 함수 ===== diff --git a/src/components/production/WorkOrders/WipProductionModal.tsx b/src/components/production/WorkOrders/WipProductionModal.tsx index e5771e7c..a58d571c 100644 --- a/src/components/production/WorkOrders/WipProductionModal.tsx +++ b/src/components/production/WorkOrders/WipProductionModal.tsx @@ -14,7 +14,7 @@ */ import { useState, useCallback, useMemo } from 'react'; -import { Search, X } from 'lucide-react'; +import { Search } from 'lucide-react'; import { Dialog, DialogContent, diff --git a/src/components/production/WorkOrders/WorkOrderCreate.tsx b/src/components/production/WorkOrders/WorkOrderCreate.tsx index 13904ba4..74198bd3 100644 --- a/src/components/production/WorkOrders/WorkOrderCreate.tsx +++ b/src/components/production/WorkOrders/WorkOrderCreate.tsx @@ -89,7 +89,7 @@ export function WorkOrderCreate() { const [isAssigneeModalOpen, setIsAssigneeModalOpen] = useState(false); const [assigneeNames, setAssigneeNames] = useState([]); const [validationErrors, setValidationErrors] = useState>({}); - const [isSubmitting, setIsSubmitting] = useState(false); + const [, setIsSubmitting] = useState(false); const [processOptions, setProcessOptions] = useState([]); const [isLoadingProcesses, setIsLoadingProcesses] = useState(true); diff --git a/src/components/production/WorkOrders/WorkOrderEdit.tsx b/src/components/production/WorkOrders/WorkOrderEdit.tsx index cf9d3b80..b2eacbc4 100644 --- a/src/components/production/WorkOrders/WorkOrderEdit.tsx +++ b/src/components/production/WorkOrders/WorkOrderEdit.tsx @@ -92,7 +92,7 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) { const [assigneeNames, setAssigneeNames] = useState([]); const [validationErrors, setValidationErrors] = useState>({}); const [isLoading, setIsLoading] = useState(true); - const [isSubmitting, setIsSubmitting] = useState(false); + const [, setIsSubmitting] = useState(false); const [processOptions, setProcessOptions] = useState([]); const [isLoadingProcesses, setIsLoadingProcesses] = useState(true); diff --git a/src/components/production/WorkOrders/WorkOrderList.tsx b/src/components/production/WorkOrders/WorkOrderList.tsx index 757ac8cf..39980432 100644 --- a/src/components/production/WorkOrders/WorkOrderList.tsx +++ b/src/components/production/WorkOrders/WorkOrderList.tsx @@ -129,7 +129,7 @@ export function WorkOrderList() { ); // ===== 등록 핸들러 ===== - const handleCreate = useCallback(() => { + const _handleCreate = useCallback(() => { router.push('/ko/production/work-orders?mode=new'); }, [router]); @@ -272,18 +272,18 @@ export function WorkOrderList() { // 테이블 컬럼 (기획서 기반 + 공정 추가) columns: [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'workOrderNo', label: '작업번호', className: 'min-w-[140px]' }, - { key: 'salesOrderDate', label: '수주일', className: 'w-[100px]' }, - { key: 'shipmentDate', label: '출고예정일', className: 'w-[110px]' }, - { key: 'lotNo', label: '로트번호', className: 'min-w-[120px]' }, - { key: 'client', label: '수주처', className: 'min-w-[120px]' }, - { key: 'projectName', label: '현장명', className: 'min-w-[150px]' }, - { key: 'shutterCount', label: '틀수', className: 'w-[70px] text-center' }, - { key: 'category', label: '구분', className: 'w-[80px]' }, + { key: 'workOrderNo', label: '작업번호', className: 'min-w-[140px]', copyable: true }, + { key: 'salesOrderDate', label: '수주일', className: 'w-[100px]', copyable: true }, + { key: 'shipmentDate', label: '출고예정일', className: 'w-[110px]', copyable: true }, + { key: 'lotNo', label: '로트번호', className: 'min-w-[120px]', copyable: true }, + { key: 'client', label: '수주처', className: 'min-w-[120px]', copyable: true }, + { key: 'projectName', label: '현장명', className: 'min-w-[150px]', copyable: true }, + { key: 'shutterCount', label: '틀수', className: 'w-[70px] text-center', copyable: true }, + { key: 'category', label: '구분', className: 'w-[80px]', copyable: true }, { key: 'status', label: '상태', className: 'w-[90px]' }, { key: 'priority', label: '우선순위', className: 'w-[80px]' }, - { key: 'department', label: '부서', className: 'w-[90px]' }, - { key: 'note', label: '비고', className: 'min-w-[120px]' }, + { key: 'department', label: '부서', className: 'w-[90px]', copyable: true }, + { key: 'note', label: '비고', className: 'min-w-[120px]', copyable: true }, ], // 서버 사이드 페이지네이션 diff --git a/src/components/production/WorkOrders/documents/BendingWorkLogContent.tsx b/src/components/production/WorkOrders/documents/BendingWorkLogContent.tsx index 785414f1..99f90d10 100644 --- a/src/components/production/WorkOrders/documents/BendingWorkLogContent.tsx +++ b/src/components/production/WorkOrders/documents/BendingWorkLogContent.tsx @@ -47,7 +47,7 @@ export function BendingWorkLogContent({ data: order, lotNoMap }: BendingWorkLogC const documentNo = order.workOrderNo || 'ABC123'; const primaryAssignee = order.assignees?.find(a => a.isPrimary)?.name || order.assignee || '-'; - const items = order.items || []; + const _items = order.items || []; const formattedDueDate = order.dueDate !== '-' ? new Date(order.dueDate).toLocaleDateString('ko-KR', { diff --git a/src/components/production/WorkOrders/documents/SlatWorkLogContent.tsx b/src/components/production/WorkOrders/documents/SlatWorkLogContent.tsx index 8a6e61ec..764b3588 100644 --- a/src/components/production/WorkOrders/documents/SlatWorkLogContent.tsx +++ b/src/components/production/WorkOrders/documents/SlatWorkLogContent.tsx @@ -30,7 +30,7 @@ interface SlatWorkLogContentProps { materialLots?: MaterialInputLot[]; } -export function SlatWorkLogContent({ data: order, materialLots = [] }: SlatWorkLogContentProps) { +export function SlatWorkLogContent({ data: order, materialLots: _materialLots = [] }: SlatWorkLogContentProps) { const today = new Date().toLocaleDateString('ko-KR', { year: 'numeric', month: '2-digit', diff --git a/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx b/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx index d14c2b4d..d733714c 100644 --- a/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx +++ b/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx @@ -543,7 +543,7 @@ export const TemplateInspectionContent = forwardRef 0) setInadequateContent(remarks.join('\n')); - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [inspectionDataMap, workItems]); // ===== Bending: document_data EAV 레코드에서 복원 ===== @@ -621,7 +621,7 @@ export const TemplateInspectionContent = forwardRef 0) { setCellValues(prev => ({ ...prev, ...initial })); } - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [documentRecords, isBending, bendingProducts]); // ===== Bending: inspectionDataMap의 products 배열에서 셀 값 복원 ===== @@ -739,7 +739,7 @@ export const TemplateInspectionContent = forwardRef 0) { setCellValues(prev => ({ ...prev, ...initial })); } - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isBending, inspectionDataMap, workItems, bendingProducts, template.columns, gapColumnId]); const updateCell = (key: string, update: Partial) => { diff --git a/src/components/production/WorkOrders/documents/bending/GuideRailSection.tsx b/src/components/production/WorkOrders/documents/bending/GuideRailSection.tsx index 39289b56..5be79ff5 100644 --- a/src/components/production/WorkOrders/documents/bending/GuideRailSection.tsx +++ b/src/components/production/WorkOrders/documents/bending/GuideRailSection.tsx @@ -17,7 +17,7 @@ interface GuideRailSectionProps { lotNoMap?: Record; // BD-{prefix}-{lengthCode} → LOT NO } -function PartTable({ title, rows, imageUrl, lotNo, baseSize, lotNoMap }: { +function PartTable({ title, rows, imageUrl, lotNo: _lotNo, baseSize, lotNoMap }: { title: string; rows: GuideRailPartRow[]; imageUrl: string; diff --git a/src/components/production/WorkOrders/documents/bending/utils.ts b/src/components/production/WorkOrders/documents/bending/utils.ts index 133ee708..3bcc51ef 100644 --- a/src/components/production/WorkOrders/documents/bending/utils.ts +++ b/src/components/production/WorkOrders/documents/bending/utils.ts @@ -43,9 +43,9 @@ const BOX_FINISH_MATERIAL = 'EGI 1.55T'; const BOX_COVER_LENGTH = 1219; // 상부덮개 고정 길이 // 길이 버킷 -const GUIDE_RAIL_LENGTH_BUCKETS = [2438, 3000, 3500, 4000, 4300]; -const SHUTTER_BOX_LENGTH_BUCKETS = [1219, 2438, 3000, 3500, 4000, 4150]; -const SMOKE_W50_LENGTH_BUCKETS = [2438, 3000, 3500, 4000, 4300]; +const _GUIDE_RAIL_LENGTH_BUCKETS = [2438, 3000, 3500, 4000, 4300]; +const _SHUTTER_BOX_LENGTH_BUCKETS = [1219, 2438, 3000, 3500, 4000, 4150]; +const _SMOKE_W50_LENGTH_BUCKETS = [2438, 3000, 3500, 4000, 4300]; // ============================================================ // 하부BASE 치수 파싱 헬퍼 diff --git a/src/components/production/WorkOrders/documents/inspection-shared.tsx b/src/components/production/WorkOrders/documents/inspection-shared.tsx index 4e9e9709..d9ff837a 100644 --- a/src/components/production/WorkOrders/documents/inspection-shared.tsx +++ b/src/components/production/WorkOrders/documents/inspection-shared.tsx @@ -10,7 +10,7 @@ * - 날짜 유틸, 공통 스타일 */ -import { ReactNode, useState } from 'react'; +import { ReactNode } from 'react'; import type { WorkOrder } from '../types'; import type { InspectionSetting } from '@/types/process'; diff --git a/src/components/production/WorkResults/WorkResultList.tsx b/src/components/production/WorkResults/WorkResultList.tsx index 5ac7ed91..8798466b 100644 --- a/src/components/production/WorkResults/WorkResultList.tsx +++ b/src/components/production/WorkResults/WorkResultList.tsx @@ -10,7 +10,6 @@ */ import { useState, useEffect, useMemo, useCallback } from 'react'; -import { useRouter } from 'next/navigation'; import { BarChart3, Package, @@ -32,7 +31,6 @@ import { type ListParams, } from '@/components/templates/UniversalListPage'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; -import { toast } from 'sonner'; import { getWorkResults, getWorkResultStats } from './actions'; import type { WorkResult, WorkResultStats } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; @@ -67,7 +65,7 @@ export function WorkResultList() { }, []); // ===== 상세 보기 핸들러 ===== - const handleView = useCallback((item: WorkResult) => { + const handleView = useCallback((_item: WorkResult) => { // TODO: 상세 보기 기능 구현 }, []); @@ -203,19 +201,19 @@ export function WorkResultList() { // 테이블 컬럼 columns: [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'lotNo', label: '로트번호', className: 'min-w-[150px]' }, - { key: 'workDate', label: '작업일', className: 'w-[100px]' }, - { key: 'workOrderNo', label: '작업지시번호', className: 'min-w-[140px]' }, - { key: 'processName', label: '공정', className: 'w-[80px]' }, - { key: 'productName', label: '품목명', className: 'min-w-[180px]' }, - { key: 'specification', label: '규격', className: 'w-[100px]' }, - { key: 'productionQty', label: '생산수량', className: 'w-[80px] text-center' }, - { key: 'goodQty', label: '양품수량', className: 'w-[80px] text-center' }, - { key: 'defectQty', label: '불량수량', className: 'w-[80px] text-center' }, - { key: 'defectRate', label: '불량률', className: 'w-[80px] text-center' }, + { key: 'lotNo', label: '로트번호', className: 'min-w-[150px]', copyable: true }, + { key: 'workDate', label: '작업일', className: 'w-[100px]', copyable: true }, + { key: 'workOrderNo', label: '작업지시번호', className: 'min-w-[140px]', copyable: true }, + { key: 'processName', label: '공정', className: 'w-[80px]', copyable: true }, + { key: 'productName', label: '품목명', className: 'min-w-[180px]', copyable: true }, + { key: 'specification', label: '규격', className: 'w-[100px]', copyable: true }, + { key: 'productionQty', label: '생산수량', className: 'w-[80px] text-center', copyable: true }, + { key: 'goodQty', label: '양품수량', className: 'w-[80px] text-center', copyable: true }, + { key: 'defectQty', label: '불량수량', className: 'w-[80px] text-center', copyable: true }, + { key: 'defectRate', label: '불량률', className: 'w-[80px] text-center', copyable: true }, { key: 'inspection', label: '검사', className: 'w-[60px] text-center' }, { key: 'packaging', label: '포장', className: 'w-[60px] text-center' }, - { key: 'workerName', label: '작업자', className: 'w-[80px]' }, + { key: 'workerName', label: '작업자', className: 'w-[80px]', copyable: true }, ], // 서버 사이드 페이지네이션 diff --git a/src/components/production/WorkerScreen/InspectionInputModal.tsx b/src/components/production/WorkerScreen/InspectionInputModal.tsx index 2020ab16..c5a76e1d 100644 --- a/src/components/production/WorkerScreen/InspectionInputModal.tsx +++ b/src/components/production/WorkerScreen/InspectionInputModal.tsx @@ -25,7 +25,6 @@ import { cn } from '@/lib/utils'; import type { InspectionTemplateData, InspectionTemplateSectionItem } from './types'; import { formatNumber } from '@/lib/utils/amount'; import { getInspectionConfig } from '@/components/production/WorkOrders/actions'; -import type { InspectionConfigData } from '@/components/production/WorkOrders/actions'; // 중간검사 공정 타입 export type InspectionProcessType = @@ -565,7 +564,7 @@ export function InspectionInputModal({ const skipAutoJudgmentRef = useRef(false); // 절곡용 간격 포인트 초기화 (레거시 — bending_wip 등에서 사용) - const [gapPoints, setGapPoints] = useState<{ left: number | null; right: number | null }[]>( + const [, setGapPoints] = useState<{ left: number | null; right: number | null }[]>( Array(5).fill(null).map(() => ({ left: null, right: null })) ); diff --git a/src/components/production/WorkerScreen/ProcessDetailSection.tsx b/src/components/production/WorkerScreen/ProcessDetailSection.tsx index dbc95b48..e5a396f5 100644 --- a/src/components/production/WorkerScreen/ProcessDetailSection.tsx +++ b/src/components/production/WorkerScreen/ProcessDetailSection.tsx @@ -20,7 +20,6 @@ import { getPresetStyle } from '@/lib/utils/status-config'; import { Button } from '@/components/ui/button'; import { AlertDialog, - AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, diff --git a/src/components/production/WorkerScreen/WorkItemCard.tsx b/src/components/production/WorkerScreen/WorkItemCard.tsx index 5f16010d..afa0b427 100644 --- a/src/components/production/WorkerScreen/WorkItemCard.tsx +++ b/src/components/production/WorkerScreen/WorkItemCard.tsx @@ -50,8 +50,8 @@ export const WorkItemCard = memo(function WorkItemCard({ onEditMaterial, onDeleteMaterial, onInspectionClick, - inspectionChecked, - onInspectionToggle, + inspectionChecked: _inspectionChecked, + onInspectionToggle: _onInspectionToggle, }: WorkItemCardProps) { const [isMaterialListOpen, setIsMaterialListOpen] = useState(false); diff --git a/src/components/production/WorkerScreen/index.tsx b/src/components/production/WorkerScreen/index.tsx index 288f679e..90a9084c 100644 --- a/src/components/production/WorkerScreen/index.tsx +++ b/src/components/production/WorkerScreen/index.tsx @@ -245,10 +245,10 @@ export default function WorkerScreen() { const [inspectionStepName, setInspectionStepName] = useState(''); // 중간검사 체크 상태 관리: { [itemId]: boolean } - const [inspectionCheckedMap, setInspectionCheckedMap] = useState>({}); + const [inspectionCheckedMap, _setInspectionCheckedMap] = useState>({}); // 체크된 검사 항목 수 계산 - const checkedInspectionCount = useMemo(() => { + const _checkedInspectionCount = useMemo(() => { return Object.values(inspectionCheckedMap).filter(Boolean).length; }, [inspectionCheckedMap]); @@ -270,7 +270,7 @@ export default function WorkerScreen() { const [editMaterialQty, setEditMaterialQty] = useState(''); // 완료 토스트 상태 - const [toastInfo, setToastInfo] = useState(null); + const [toastInfo, _setToastInfo] = useState(null); // 공정 목록 캐시 const [processListCache, setProcessListCache] = useState([]); @@ -343,7 +343,7 @@ export default function WorkerScreen() { } }; loadStepProgress(); - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedSidebarOrderId]); // ===== 탭별 필터링된 작업 ===== @@ -705,7 +705,7 @@ export default function WorkerScreen() { } }; loadInspectionData(); - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedSidebarOrderId, workItems.length]); // ===== 작업지시 변경 시 작업 정보 자동 세팅 ===== @@ -728,7 +728,7 @@ export default function WorkerScreen() { setProductionManagerId(''); setProductionDate(''); } - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedSidebarOrderId, filteredWorkOrders, departmentList]); // ===== 수주 정보 (사이드바 선택 항목 기반) ===== @@ -929,7 +929,7 @@ export default function WorkerScreen() { try { const refreshResult = await getMyWorkOrders(); if (refreshResult.success) setWorkOrders(refreshResult.data); - } catch {} + } catch { /* refresh failed silently */ } } else { toast.error(result.error || '수정에 실패했습니다.'); } @@ -969,7 +969,7 @@ export default function WorkerScreen() { // 로컬 오버라이드 모두 제거 (API 데이터가 최신) setInputMaterialsMap(new Map()); } - } catch {} + } catch { /* refresh failed silently */ } } else { toast.error(result.error || '삭제에 실패했습니다.'); } @@ -1218,7 +1218,7 @@ export default function WorkerScreen() { }, [activeProcessTabKey, slatSubMode]); // 재공품 통합 문서 (작업일지 + 중간검사) 핸들러 - const handleWipInspection = useCallback(() => { + const _handleWipInspection = useCallback(() => { const target = getTargetOrder(); if (target) { setSelectedOrder(target); diff --git a/src/components/quality/InspectionManagement/InspectionCreate.tsx b/src/components/quality/InspectionManagement/InspectionCreate.tsx index a13cd301..f58c5757 100644 --- a/src/components/quality/InspectionManagement/InspectionCreate.tsx +++ b/src/components/quality/InspectionManagement/InspectionCreate.tsx @@ -15,8 +15,6 @@ import dynamic from 'next/dynamic'; import { useRouter } from 'next/navigation'; import { Plus, Trash2, ClipboardCheck } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { getPresetStyle } from '@/lib/utils/status-config'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; @@ -39,7 +37,7 @@ import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetai import { qualityInspectionCreateConfig } from './inspectionConfig'; import { toast } from 'sonner'; import { createInspection } from './actions'; -import { isOrderSpecSame, calculateOrderSummary } from './mockData'; +import { calculateOrderSummary } from './mockData'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; const OrderSelectModal = dynamic( diff --git a/src/components/quality/InspectionManagement/InspectionDetail.tsx b/src/components/quality/InspectionManagement/InspectionDetail.tsx index 91e18775..1e38f786 100644 --- a/src/components/quality/InspectionManagement/InspectionDetail.tsx +++ b/src/components/quality/InspectionManagement/InspectionDetail.tsx @@ -20,12 +20,10 @@ import { Loader2, Plus, Trash2, - ChevronDown, ClipboardCheck, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; -import { getPresetStyle } from '@/lib/utils/status-config'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; @@ -63,7 +61,6 @@ import { import { getFqcStatus, type FqcStatusItem } from './fqcActions'; import { statusColorMap, - isOrderSpecSame, calculateOrderSummary, buildRequestDocumentData, buildReportDocumentData, @@ -623,7 +620,7 @@ export function InspectionDetail({ id }: InspectionDetailProps) { return (
- {groups.map((group, groupIndex) => ( + {groups.map((group, _groupIndex) => (
{group.orderNumber} @@ -694,7 +691,7 @@ export function InspectionDetail({ id }: InspectionDetailProps) { return (
- {groups.map((group, groupIndex) => ( + {groups.map((group, _groupIndex) => (
{group.orderNumber} diff --git a/src/components/quality/InspectionManagement/InspectionList.tsx b/src/components/quality/InspectionManagement/InspectionList.tsx index dff2ca6a..2975a8be 100644 --- a/src/components/quality/InspectionManagement/InspectionList.tsx +++ b/src/components/quality/InspectionManagement/InspectionList.tsx @@ -20,7 +20,6 @@ import { Loader2, CheckCircle2, } from 'lucide-react'; -import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { getPresetStyle } from '@/lib/utils/status-config'; import { TableCell, TableRow } from '@/components/ui/table'; @@ -48,7 +47,7 @@ import { statusColorMap } from './mockData'; import { parseISO } from 'date-fns'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { getLocalDateString } from '@/lib/utils/date'; -import type { ProductInspection, InspectionStats, InspectionStatus } from './types'; +import type { ProductInspection, InspectionStatus } from './types'; const ITEMS_PER_PAGE = 20; @@ -266,16 +265,16 @@ export function InspectionList() { // 테이블 컬럼 columns: [ { key: 'no', label: 'No.', className: 'w-[50px] text-center' }, - { key: 'qualityDocNumber', label: '품질관리서 번호', className: 'min-w-[120px]' }, - { key: 'siteName', label: '현장명', className: 'min-w-[100px]' }, - { key: 'client', label: '수주처', className: 'min-w-[80px]' }, - { key: 'locationCount', label: '개소', className: 'w-[60px] text-center' }, - { key: 'requiredInfo', label: '필수정보', className: 'w-[90px] text-center' }, - { key: 'inspectionPeriod', label: '검사기간', className: 'min-w-[140px]' }, - { key: 'inspector', label: '검사자', className: 'w-[80px] text-center' }, + { key: 'qualityDocNumber', label: '품질관리서 번호', className: 'min-w-[120px]', copyable: true }, + { key: 'siteName', label: '현장명', className: 'min-w-[100px]', copyable: true }, + { key: 'client', label: '수주처', className: 'min-w-[80px]', copyable: true }, + { key: 'locationCount', label: '개소', className: 'w-[60px] text-center', copyable: true }, + { key: 'requiredInfo', label: '필수정보', className: 'w-[90px] text-center', copyable: true }, + { key: 'inspectionPeriod', label: '검사기간', className: 'min-w-[140px]', copyable: true }, + { key: 'inspector', label: '검사자', className: 'w-[80px] text-center', copyable: true }, { key: 'status', label: '상태', className: 'w-[70px] text-center' }, - { key: 'author', label: '작성자', className: 'w-[80px] text-center' }, - { key: 'receptionDate', label: '접수일', className: 'w-[100px] text-center' }, + { key: 'author', label: '작성자', className: 'w-[80px] text-center', copyable: true }, + { key: 'receptionDate', label: '접수일', className: 'w-[100px] text-center', copyable: true }, ], // 서버 사이드 페이지네이션 diff --git a/src/components/quality/InspectionManagement/ProductInspectionInputModal.tsx b/src/components/quality/InspectionManagement/ProductInspectionInputModal.tsx index 185d5377..efab49bc 100644 --- a/src/components/quality/InspectionManagement/ProductInspectionInputModal.tsx +++ b/src/components/quality/InspectionManagement/ProductInspectionInputModal.tsx @@ -72,7 +72,7 @@ export function ProductInspectionInputModal({ }: ProductInspectionInputModalProps) { // FQC 모드 상태 const [fqcTemplate, setFqcTemplate] = useState(null); - const [fqcDocData, setFqcDocData] = useState([]); + const [, setFqcDocData] = useState([]); const [isLoadingFqc, setIsLoadingFqc] = useState(false); const [isSaving, setIsSaving] = useState(false); diff --git a/src/components/quality/InspectionManagement/documents/FqcRequestDocumentContent.tsx b/src/components/quality/InspectionManagement/documents/FqcRequestDocumentContent.tsx index 64684a9d..1c8e1e05 100644 --- a/src/components/quality/InspectionManagement/documents/FqcRequestDocumentContent.tsx +++ b/src/components/quality/InspectionManagement/documents/FqcRequestDocumentContent.tsx @@ -60,7 +60,7 @@ function getSectionItemValue( /** EAV 데이터에서 테이블 행 데이터 조회 */ function getTableRows( data: FqcDocumentData[] | undefined, - columns: FqcTemplate['columns'], + _columns: FqcTemplate['columns'], ): Array> { if (!data) return []; // column_id가 있는 데이터만 필터 → row_index로 그룹핑 diff --git a/src/components/quality/PerformanceReportManagement/PerformanceReportList.tsx b/src/components/quality/PerformanceReportManagement/PerformanceReportList.tsx index 430abd78..4db613dc 100644 --- a/src/components/quality/PerformanceReportManagement/PerformanceReportList.tsx +++ b/src/components/quality/PerformanceReportManagement/PerformanceReportList.tsx @@ -357,24 +357,24 @@ export function PerformanceReportList() { columnsPerTab: { quarterly: [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'qualityDocNumber', label: '품질관리서 번호', className: 'min-w-[130px]' }, - { key: 'createdDate', label: '작성일', className: 'w-[100px]' }, - { key: 'siteName', label: '현장명', className: 'min-w-[120px]' }, - { key: 'client', label: '수주처', className: 'min-w-[80px]' }, - { key: 'locationCount', label: '개소', className: 'w-[60px] text-center' }, - { key: 'requiredInfo', label: '필수정보', className: 'w-[90px] text-center' }, + { key: 'qualityDocNumber', label: '품질관리서 번호', className: 'min-w-[130px]', copyable: true }, + { key: 'createdDate', label: '작성일', className: 'w-[100px]', copyable: true }, + { key: 'siteName', label: '현장명', className: 'min-w-[120px]', copyable: true }, + { key: 'client', label: '수주처', className: 'min-w-[80px]', copyable: true }, + { key: 'locationCount', label: '개소', className: 'w-[60px] text-center', copyable: true }, + { key: 'requiredInfo', label: '필수정보', className: 'w-[90px] text-center', copyable: true }, { key: 'confirmStatus', label: '확정상태', className: 'w-[80px] text-center' }, - { key: 'confirmDate', label: '확정일', className: 'w-[100px]' }, - { key: 'memo', label: '메모', className: 'min-w-[120px]' }, + { key: 'confirmDate', label: '확정일', className: 'w-[100px]', copyable: true }, + { key: 'memo', label: '메모', className: 'min-w-[120px]', copyable: true }, ], missed: [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'qualityDocNumber', label: '품질관리서 번호', className: 'min-w-[130px]' }, - { key: 'siteName', label: '현장명', className: 'min-w-[120px]' }, - { key: 'client', label: '수주처', className: 'min-w-[80px]' }, - { key: 'locationCount', label: '개소', className: 'w-[60px] text-center' }, - { key: 'inspectionCompleteDate', label: '제품검사완료일', className: 'w-[120px]' }, - { key: 'memo', label: '메모', className: 'min-w-[120px]' }, + { key: 'qualityDocNumber', label: '품질관리서 번호', className: 'min-w-[130px]', copyable: true }, + { key: 'siteName', label: '현장명', className: 'min-w-[120px]', copyable: true }, + { key: 'client', label: '수주처', className: 'min-w-[80px]', copyable: true }, + { key: 'locationCount', label: '개소', className: 'w-[60px] text-center', copyable: true }, + { key: 'inspectionCompleteDate', label: '제품검사완료일', className: 'w-[120px]', copyable: true }, + { key: 'memo', label: '메모', className: 'min-w-[120px]', copyable: true }, ], }, columns: [], // columnsPerTab 사용 diff --git a/src/components/quotes/DiscountModal.tsx b/src/components/quotes/DiscountModal.tsx index caae6405..53fbd7d8 100644 --- a/src/components/quotes/DiscountModal.tsx +++ b/src/components/quotes/DiscountModal.tsx @@ -11,7 +11,6 @@ "use client"; import { useState, useEffect, useCallback } from "react"; -import { Percent } from "lucide-react"; import { formatNumber } from "@/lib/utils/amount"; import { diff --git a/src/components/quotes/FormulaViewModal.tsx b/src/components/quotes/FormulaViewModal.tsx index 3ed3feb4..464ed9ae 100644 --- a/src/components/quotes/FormulaViewModal.tsx +++ b/src/components/quotes/FormulaViewModal.tsx @@ -195,7 +195,7 @@ function DebugStepCard({ step }: { step: BomDebugStep }) { ); } -function FormulaTable({ formulas, stepName }: { formulas: FormulaItem[]; stepName: string }) { +function FormulaTable({ formulas, stepName: _stepName }: { formulas: FormulaItem[]; stepName: string }) { // 입력값 (Step 1) if (formulas[0]?.var && formulas[0]?.value !== undefined) { return ( diff --git a/src/components/quotes/LocationDetailPanel.tsx b/src/components/quotes/LocationDetailPanel.tsx index b6faf9f7..44f95e43 100644 --- a/src/components/quotes/LocationDetailPanel.tsx +++ b/src/components/quotes/LocationDetailPanel.tsx @@ -127,7 +127,7 @@ interface LocationDetailPanelProps { export function LocationDetailPanel({ location, onUpdateLocation, - onDeleteLocation, + onDeleteLocation: _onDeleteLocation, onCalculateLocation, onSaveItems, finishedGoods, @@ -181,7 +181,7 @@ export function LocationDetailPanel({ // --------------------------------------------------------------------------- // 제품 정보 - const product = useMemo(() => { + const _product = useMemo(() => { if (!location?.productCode) return null; return finishedGoods.find((fg) => fg.item_code === location.productCode); }, [location?.productCode, finishedGoods]); @@ -238,7 +238,7 @@ export function LocationDetailPanel({ }, [location?.bomResult?.grouped_items, location?.bomResult?.items, detailTabs]); // 탭별 소계 - const tabSubtotals = useMemo(() => { + const _tabSubtotals = useMemo(() => { const result: Record = {}; Object.entries(bomItemsByTab).forEach(([tab, items]) => { result[tab] = items.reduce((sum: number, item: { total_price?: number }) => sum + (item.total_price || 0), 0); @@ -572,10 +572,10 @@ export function LocationDetailPanel({ if (!matchedItem) return; // items 배열에서 같은 item_code + item_name 매칭 (동일 코드 복수 가능하므로 순서 기반) - let matchCount = 0; + let _matchCount = 0; const updatedItems = (existingBomResult.items || []).map((bomItem: any) => { if (bomItem.item_code === targetCode && bomItem.item_name === targetName) { - matchCount++; + _matchCount++; // grouped_items 내 순서와 items 내 순서가 같은 아이템 찾기 if (bomItem.quantity === targetItem.quantity && bomItem.unit_price === targetItem.unit_price) { const newTotalPrice = (bomItem.unit_price || 0) * newQty; diff --git a/src/components/quotes/LocationListPanel.tsx b/src/components/quotes/LocationListPanel.tsx index a537de15..352872f6 100644 --- a/src/components/quotes/LocationListPanel.tsx +++ b/src/components/quotes/LocationListPanel.tsx @@ -96,7 +96,7 @@ export function LocationListPanel({ onAddLocation, onDeleteLocation, onCloneLocation, - onUpdateLocation, + onUpdateLocation: _onUpdateLocation, onExcelUpload, finishedGoods, locationCodes = [], diff --git a/src/components/quotes/QuoteFooterBar.tsx b/src/components/quotes/QuoteFooterBar.tsx index d1818c37..c03608b0 100644 --- a/src/components/quotes/QuoteFooterBar.tsx +++ b/src/components/quotes/QuoteFooterBar.tsx @@ -72,7 +72,7 @@ export function QuoteFooterBar({ onFormulaView, hasBomResult = false, isSaving = false, - disabled = false, + disabled: _disabled = false, isViewMode = false, }: QuoteFooterBarProps) { return ( diff --git a/src/components/quotes/QuoteManagementClient.tsx b/src/components/quotes/QuoteManagementClient.tsx index 8cfbf807..56723345 100644 --- a/src/components/quotes/QuoteManagementClient.tsx +++ b/src/components/quotes/QuoteManagementClient.tsx @@ -54,7 +54,7 @@ import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { useDeleteDialog } from '@/hooks/useDeleteDialog'; import { toast } from 'sonner'; import { formatAmount, formatAmountManwon } from '@/lib/utils/amount'; -import type { Quote, QuoteFilterType } from './types'; +import type { Quote } from './types'; import { PRODUCT_CATEGORY_LABELS } from './types'; import { getQuotes, deleteQuote, bulkDeleteQuotes } from './actions'; import type { PaginationMeta } from '@/lib/api/types'; @@ -67,7 +67,7 @@ interface QuoteManagementClientProps { export function QuoteManagementClient({ initialData, - initialPagination, + initialPagination: _initialPagination, }: QuoteManagementClientProps) { const router = useRouter(); const deleteDialog = useDeleteDialog({ @@ -106,7 +106,7 @@ export function QuoteManagementClient({ toast.info(`수정 이력: ${quote.quoteNumber} (${quote.currentRevision}차 수정)`); }, []); - const handleViewCalculation = useCallback((quote: Quote) => { + const _handleViewCalculation = useCallback((quote: Quote) => { setCalculationQuote(quote); setIsCalculationDialogOpen(true); }, []); @@ -226,16 +226,16 @@ export function QuoteManagementClient({ // 테이블 컬럼 columns: [ { key: 'rowNumber', label: '번호', className: 'w-[60px] text-center' }, - { key: 'quoteNumber', label: '견적번호', className: 'min-w-[120px]', sortable: true }, - { key: 'registrationDate', label: '접수일', className: 'w-[100px]', sortable: true }, + { key: 'quoteNumber', label: '견적번호', className: 'min-w-[120px]', sortable: true, copyable: true }, + { key: 'registrationDate', label: '접수일', className: 'w-[100px]', sortable: true, copyable: true }, { key: 'status', label: '상태', className: 'w-[80px]', sortable: true }, - { key: 'productCategory', label: '제품분류', className: 'w-[100px]', sortable: true }, - { key: 'quantity', label: '수량', className: 'w-[60px] text-center', sortable: true }, - { key: 'amount', label: '금액', className: 'w-[120px] text-right', sortable: true }, - { key: 'client', label: '발주처', className: 'min-w-[100px]', sortable: true }, - { key: 'site', label: '현장명', className: 'min-w-[120px]', sortable: true }, - { key: 'manager', label: '담당자', className: 'w-[80px]', sortable: true }, - { key: 'remarks', label: '비고', className: 'min-w-[150px]' }, + { key: 'productCategory', label: '제품분류', className: 'w-[100px]', sortable: true, copyable: true }, + { key: 'quantity', label: '수량', className: 'w-[60px] text-center', sortable: true, copyable: true }, + { key: 'totalAmount', label: '금액', className: 'w-[120px] text-right', sortable: true, copyable: true }, + { key: 'clientName', label: '발주처', className: 'min-w-[100px]', sortable: true, copyable: true }, + { key: 'siteName', label: '현장명', className: 'min-w-[120px]', sortable: true, copyable: true }, + { key: 'managerName', label: '담당자', className: 'w-[80px]', sortable: true, copyable: true }, + { key: 'description', label: '비고', className: 'min-w-[150px]', copyable: true }, { key: 'actions', label: '작업', className: 'w-[100px]' }, ], diff --git a/src/components/quotes/QuoteRegistration.tsx b/src/components/quotes/QuoteRegistration.tsx index e8ea1501..36729a5c 100644 --- a/src/components/quotes/QuoteRegistration.tsx +++ b/src/components/quotes/QuoteRegistration.tsx @@ -10,15 +10,13 @@ "use client"; import { useState, useEffect, useMemo, useCallback, useRef } from "react"; -import { FileText, Calculator, Download, Save, Check } from "lucide-react"; +import { FileText, Calculator } from "lucide-react"; import { toast } from "sonner"; import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; -import { Button } from "../ui/button"; import { Badge } from "../ui/badge"; import { Input } from "../ui/input"; import { DatePicker } from "../ui/date-picker"; -import { Textarea } from "../ui/textarea"; import { PhoneInput } from "../ui/phone-input"; import { Select, @@ -51,8 +49,8 @@ import { isNextRedirectError } from "@/lib/utils/redirect-error"; // 실제 로그인 사용자 정보는 localStorage('user')에 저장됨 (LoginPage.tsx 참조) import { useDevFill } from "@/components/dev/useDevFill"; import type { Vendor } from "../accounting/VendorManagement"; -import type { BomMaterial, CalculationResults, BomCalculationResultItem } from "./types"; -import { getLocalDateString, getDateAfterDays } from "@/lib/utils/date"; +import type { BomCalculationResultItem } from "./types"; +import { getLocalDateString } from "@/lib/utils/date"; import { formatNumber } from "@/lib/utils/amount"; // ============================================================================= @@ -110,7 +108,7 @@ export interface QuoteFormDataV2 { // ============================================================================= // 초기 개소 항목 -const createNewLocation = (): LocationItem => ({ +const _createNewLocation = (): LocationItem => ({ id: `loc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, floor: "", code: "", @@ -171,12 +169,12 @@ export function QuoteRegistration({ mode, onBack, onSave, - onCalculate, + onCalculate: _onCalculate, onEdit, onOrderRegister, onOrderView, initialData, - isLoading = false, + isLoading: _isLoading = false, hideHeader = false, }: QuoteRegistrationProps) { // --------------------------------------------------------------------------- @@ -203,7 +201,7 @@ export function QuoteRegistration({ const [siteNames, setSiteNames] = useState([]); const [locationCodes, setLocationCodes] = useState([]); const [isLoadingClients, setIsLoadingClients] = useState(false); - const [isLoadingProducts, setIsLoadingProducts] = useState(false); + const [, setIsLoadingProducts] = useState(false); // handleCalculate 참조 (DevFill에서 사용) const calculateRef = useRef<(() => Promise) | null>(null); @@ -330,7 +328,7 @@ export function QuoteRegistration({ }, []); // 개소별 합계 - const locationTotals = useMemo(() => { + const _locationTotals = useMemo(() => { return formData.locations.map((loc) => ({ id: loc.id, label: `${loc.floor} / ${loc.code}`, diff --git a/src/components/quotes/actions.ts b/src/components/quotes/actions.ts index d14f2d57..04f5b33a 100644 --- a/src/components/quotes/actions.ts +++ b/src/components/quotes/actions.ts @@ -29,11 +29,9 @@ import type { Quote, QuoteApiData, QuoteListParams, - QuoteStatus, - ProductCategory, BomCalculationResult, } from './types'; -import { transformApiToFrontend, transformFrontendToApi } from './types'; +import { transformApiToFrontend } from './types'; // ===== 견적 목록 조회 ===== export async function getQuotes(params?: QuoteListParams) { return executePaginatedAction({ diff --git a/src/components/quotes/types.ts b/src/components/quotes/types.ts index 96506679..5fae9e10 100644 --- a/src/components/quotes/types.ts +++ b/src/components/quotes/types.ts @@ -540,7 +540,7 @@ export function transformQuoteToFormData(quote: Quote): QuoteFormData { unitPrice: Math.round(amountPerItem / (calcInput.quantity || 1)), totalAmount: amountPerItem, })) - : quote.items.map((item, index) => { + : quote.items.map((item, _index) => { const spec = item.specification || ''; // specification에서 width x height 추출 (예: "3000x2500mm") const sizeMatch = spec.match(/(\d+)\s*x\s*(\d+)/i); @@ -639,7 +639,7 @@ export function transformApiDataToFormData(apiData: QuoteApiData): QuoteFormData unitPrice: Math.round(amountPerItem / (calcInput.quantity || 1)), totalAmount: amountPerItem, })) - : (apiData.items || []).map((item, index) => { + : (apiData.items || []).map((item, _index) => { const spec = item.specification || ''; // specification에서 width x height 추출 (예: "3000x2500mm") const sizeMatch = spec.match(/(\d+)\s*x\s*(\d+)/i); diff --git a/src/components/reports/ComprehensiveAnalysis/index.tsx b/src/components/reports/ComprehensiveAnalysis/index.tsx index 25f021d8..e576c84c 100644 --- a/src/components/reports/ComprehensiveAnalysis/index.tsx +++ b/src/components/reports/ComprehensiveAnalysis/index.tsx @@ -29,7 +29,6 @@ import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { getComprehensiveAnalysis, approveIssue, rejectIssue } from '../actions'; import type { CheckPoint, AmountCard, TodayIssueItem, ComprehensiveAnalysisData } from '../types'; -import { isNextRedirectError } from '@/lib/utils/redirect-error'; // 금액 포맷 함수 const formatAmount = (amount: number): string => { @@ -271,7 +270,7 @@ const IssueTableRow = ({ export default function ComprehensiveAnalysis({ initialData }: ComprehensiveAnalysisProps) { const router = useRouter(); - const [isPending, startTransition] = useTransition(); + const [, startTransition] = useTransition(); const [data, setData] = useState(initialData || defaultData); const [isLoading, setIsLoading] = useState(!initialData); const [issueFilter, setIssueFilter] = useState('전체필터'); diff --git a/src/components/settings/AccountManagement/index.tsx b/src/components/settings/AccountManagement/index.tsx index b8f7a1b3..be8240f7 100644 --- a/src/components/settings/AccountManagement/index.tsx +++ b/src/components/settings/AccountManagement/index.tsx @@ -40,7 +40,7 @@ import { type StatCard, } from '@/components/templates/UniversalListPage'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; -import type { Account, AccountCategory } from './types'; +import type { Account } from './types'; import { BANK_LABELS, ACCOUNT_CATEGORY_LABELS, @@ -215,11 +215,11 @@ export function AccountManagement() { // 테이블 컬럼 columns: [ { key: 'no', label: 'No.', className: 'text-center w-[60px]' }, - { key: 'category', label: '구분', className: 'min-w-[80px]' }, - { key: 'accountType', label: '유형', className: 'min-w-[80px]' }, - { key: 'institution', label: '금융기관', className: 'min-w-[100px]' }, - { key: 'accountNumber', label: '계좌번호', className: 'min-w-[160px]' }, - { key: 'accountName', label: '계좌명', className: 'min-w-[120px]' }, + { key: 'category', label: '구분', className: 'min-w-[80px]', copyable: true }, + { key: 'accountType', label: '유형', className: 'min-w-[80px]', copyable: true }, + { key: 'institution', label: '금융기관', className: 'min-w-[100px]', copyable: true }, + { key: 'accountNumber', label: '계좌번호', className: 'min-w-[160px]', copyable: true }, + { key: 'accountName', label: '계좌명', className: 'min-w-[120px]', copyable: true }, { key: 'status', label: '상태', className: 'min-w-[70px]' }, ], @@ -279,7 +279,7 @@ export function AccountManagement() { item: Account, _index: number, globalIndex: number, - handlers: SelectionHandlers & RowClickHandlers + _handlers: SelectionHandlers & RowClickHandlers ) => { return ( + _globalIndex: number, + _handlers: SelectionHandlers & RowClickHandlers ) => { return ( ('latest'); + const [, _setSortOption] = useState('latest'); const itemsPerPage = initialPagination.perPage; // 거래명세서 팝업 상태 const [showInvoiceDialog, setShowInvoiceDialog] = useState(false); - const [selectedPaymentId, setSelectedPaymentId] = useState(null); + const [, setSelectedPaymentId] = useState(null); // ===== 거래명세서 버튼 클릭 ===== const handleViewInvoice = useCallback((paymentId: string) => { @@ -101,11 +101,11 @@ export function PaymentHistoryClient({ // 테이블 컬럼 columns: [ - { key: 'paymentDate', label: '결제일' }, - { key: 'subscriptionName', label: '구독명' }, - { key: 'paymentMethod', label: '결제 수단' }, - { key: 'subscriptionPeriod', label: '구독 기간' }, - { key: 'amount', label: '금액', className: 'text-right' }, + { key: 'paymentDate', label: '결제일', copyable: true }, + { key: 'subscriptionName', label: '구독명', copyable: true }, + { key: 'paymentMethod', label: '결제 수단', copyable: true }, + { key: 'subscriptionPeriod', label: '구독 기간', copyable: true }, + { key: 'amount', label: '금액', className: 'text-right', copyable: true }, { key: 'invoice', label: '거래명세서', className: 'text-center' }, ], @@ -124,8 +124,8 @@ export function PaymentHistoryClient({ renderTableRow: ( item: PaymentHistory, index: number, - globalIndex: number, - handlers: SelectionHandlers & RowClickHandlers + _globalIndex: number, + _handlers: SelectionHandlers & RowClickHandlers ) => { const isLatest = index === 0; @@ -175,7 +175,7 @@ export function PaymentHistoryClient({ item: PaymentHistory, index: number, globalIndex: number, - handlers: SelectionHandlers & RowClickHandlers + _handlers: SelectionHandlers & RowClickHandlers ) => { const isLatest = globalIndex === 1; diff --git a/src/components/settings/PaymentHistoryManagement/index.tsx b/src/components/settings/PaymentHistoryManagement/index.tsx index 7b24b4cb..9d15498e 100644 --- a/src/components/settings/PaymentHistoryManagement/index.tsx +++ b/src/components/settings/PaymentHistoryManagement/index.tsx @@ -17,7 +17,7 @@ import { type UniversalListConfig, type TableColumn, } from '@/components/templates/UniversalListPage'; -import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; +import { ListMobileCard } from '@/components/organisms/MobileCard'; import { formatNumber } from '@/lib/utils/amount'; import type { PaymentHistory, SortOption } from './types'; @@ -36,8 +36,8 @@ export function PaymentHistoryManagement({ initialPagination, }: PaymentHistoryManagementProps) { // ===== 상태 관리 ===== - const [searchQuery, setSearchQuery] = useState(''); - const [sortOption, setSortOption] = useState('latest'); + const [searchQuery, _setSearchQuery] = useState(''); + const [sortOption, _setSortOption] = useState('latest'); const [currentPage, setCurrentPage] = useState(initialPagination.currentPage); const itemsPerPage = initialPagination.perPage; @@ -74,7 +74,7 @@ export function PaymentHistoryManagement({ return result; }, [data, searchQuery, sortOption]); - const paginatedData = useMemo(() => { + const _paginatedData = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; return filteredData.slice(startIndex, startIndex + itemsPerPage); }, [filteredData, currentPage, itemsPerPage]); @@ -88,11 +88,11 @@ export function PaymentHistoryManagement({ // ===== 테이블 컬럼 ===== const tableColumns: TableColumn[] = useMemo(() => [ - { key: 'paymentDate', label: '결제일' }, - { key: 'subscriptionName', label: '구독명' }, - { key: 'paymentMethod', label: '결제 수단' }, - { key: 'subscriptionPeriod', label: '구독 기간' }, - { key: 'amount', label: '금액', className: 'text-right' }, + { key: 'paymentDate', label: '결제일', copyable: true }, + { key: 'subscriptionName', label: '구독명', copyable: true }, + { key: 'paymentMethod', label: '결제 수단', copyable: true }, + { key: 'subscriptionPeriod', label: '구독 기간', copyable: true }, + { key: 'amount', label: '금액', className: 'text-right', copyable: true }, { key: 'invoice', label: '거래명세서', className: 'text-center' }, ], []); @@ -100,8 +100,8 @@ export function PaymentHistoryManagement({ const renderTableRow = useCallback(( item: PaymentHistory, index: number, - globalIndex: number, - handlers: { isSelected: boolean; onToggle: () => void; onRowClick?: () => void } + _globalIndex: number, + _handlers: { isSelected: boolean; onToggle: () => void; onRowClick?: () => void } ) => { const isLatest = index === 0; // 최신 항목 (초록색 버튼) @@ -145,7 +145,7 @@ export function PaymentHistoryManagement({ item: PaymentHistory, index: number, globalIndex: number, - handlers: { isSelected: boolean; onToggle: () => void; onRowClick?: () => void } + _handlers: { isSelected: boolean; onToggle: () => void; onRowClick?: () => void } ) => { const isLatest = globalIndex === 1; diff --git a/src/components/settings/PermissionManagement/PermissionDetailClient.tsx b/src/components/settings/PermissionManagement/PermissionDetailClient.tsx index b1a5cf6f..05cc3fe9 100644 --- a/src/components/settings/PermissionManagement/PermissionDetailClient.tsx +++ b/src/components/settings/PermissionManagement/PermissionDetailClient.tsx @@ -140,7 +140,7 @@ export function PermissionDetailClient({ permissionId, isNew = false, mode = 'vi // 권한 매트릭스 데이터 const [menuTree, setMenuTree] = useState([]); - const [permissionTypes, setPermissionTypes] = useState([]); + const [, setPermissionTypes] = useState([]); const [matrix, setMatrix] = useState(null); // UI 상태 diff --git a/src/components/settings/PermissionManagement/index.tsx b/src/components/settings/PermissionManagement/index.tsx index e7d2a26a..3ce69f47 100644 --- a/src/components/settings/PermissionManagement/index.tsx +++ b/src/components/settings/PermissionManagement/index.tsx @@ -7,13 +7,11 @@ import { Shield, Plus, Pencil, - Edit, Trash2, Settings, Eye, EyeOff, Users, - Loader2, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; @@ -102,7 +100,7 @@ export function PermissionManagement() { }); }, []); - const toggleSelectAll = useCallback(() => { + const _toggleSelectAll = useCallback(() => { if (selectedItems.size === filteredData.length && filteredData.length > 0) { setSelectedItems(new Set()); } else { @@ -247,10 +245,10 @@ export function PermissionManagement() { const tableColumns: TableColumn[] = useMemo(() => { const baseColumns: TableColumn[] = [ { key: 'index', label: '번호', className: 'text-center w-[80px]' }, - { key: 'name', label: '역할', className: 'flex-1' }, - { key: 'description', label: '설명', className: 'flex-1' }, + { key: 'name', label: '역할', className: 'flex-1', copyable: true }, + { key: 'description', label: '설명', className: 'flex-1', copyable: true }, { key: 'status', label: '상태', className: 'text-center w-[100px]' }, - { key: 'createdAt', label: '등록일', className: 'text-center w-[120px]' }, + { key: 'createdAt', label: '등록일', className: 'text-center w-[120px]', copyable: true }, ]; if (selectedItems.size > 0) { diff --git a/src/components/settings/PopupManagement/PopupList.tsx b/src/components/settings/PopupManagement/PopupList.tsx index 2fe9a1e3..dbbf36da 100644 --- a/src/components/settings/PopupManagement/PopupList.tsx +++ b/src/components/settings/PopupManagement/PopupList.tsx @@ -71,12 +71,12 @@ export function PopupList({ initialData }: PopupListProps) { // 테이블 컬럼 columns: [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'target', label: '대상', className: 'w-[80px] text-center' }, - { key: 'title', label: '제목', className: 'min-w-[150px]' }, + { key: 'target', label: '대상', className: 'w-[80px] text-center', copyable: true }, + { key: 'title', label: '제목', className: 'min-w-[150px]', copyable: true }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, - { 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: 'author', label: '작성자', className: 'w-[100px] text-center', copyable: true }, + { key: 'createdAt', label: '등록일', className: 'w-[110px] text-center', copyable: true }, + { key: 'period', label: '기간', className: 'w-[180px] text-center', copyable: true }, ], // 클라이언트 사이드 필터링 diff --git a/src/components/settings/SubscriptionManagement/SubscriptionClient.tsx b/src/components/settings/SubscriptionManagement/SubscriptionClient.tsx index adede05c..e59e583b 100644 --- a/src/components/settings/SubscriptionManagement/SubscriptionClient.tsx +++ b/src/components/settings/SubscriptionManagement/SubscriptionClient.tsx @@ -49,7 +49,7 @@ export function SubscriptionClient({ initialData }: SubscriptionClientProps) { } else { toast.error(result.error || '내보내기 요청에 실패했습니다.'); } - } catch (error) { + } catch (_error) { toast.error('서버 오류가 발생했습니다.'); } finally { setIsExporting(false); diff --git a/src/components/settings/WorkScheduleManagement/index.tsx b/src/components/settings/WorkScheduleManagement/index.tsx index c53ef090..c72f6e65 100644 --- a/src/components/settings/WorkScheduleManagement/index.tsx +++ b/src/components/settings/WorkScheduleManagement/index.tsx @@ -6,7 +6,6 @@ import { PageHeader } from '@/components/organisms/PageHeader'; import { Clock, Save, Loader2 } from 'lucide-react'; import { getWorkSetting, updateWorkSetting } from './actions'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; import { TimePicker } from '@/components/ui/time-picker'; import { Label } from '@/components/ui/label'; import { QuantityInput } from '@/components/ui/quantity-input'; diff --git a/src/components/templates/IntegratedDetailTemplate/FieldInput.tsx b/src/components/templates/IntegratedDetailTemplate/FieldInput.tsx index 6b537427..c17e1b18 100644 --- a/src/components/templates/IntegratedDetailTemplate/FieldInput.tsx +++ b/src/components/templates/IntegratedDetailTemplate/FieldInput.tsx @@ -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'; diff --git a/src/components/templates/IntegratedDetailTemplate/types.ts b/src/components/templates/IntegratedDetailTemplate/types.ts index fa3cdeee..b2ec5dd0 100644 --- a/src/components/templates/IntegratedDetailTemplate/types.ts +++ b/src/components/templates/IntegratedDetailTemplate/types.ts @@ -154,8 +154,8 @@ export interface PermissionConfig { } // ===== 상세 페이지 설정 ===== -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface DetailConfig> { + +export interface DetailConfig<_T = Record> { /** 페이지 제목 */ title: string; /** 페이지 설명 */ diff --git a/src/components/templates/IntegratedListTemplateV2.tsx b/src/components/templates/IntegratedListTemplateV2.tsx index d9ba06f2..29891862 100644 --- a/src/components/templates/IntegratedListTemplateV2.tsx +++ b/src/components/templates/IntegratedListTemplateV2.tsx @@ -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({ 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({ onBulkDelete, selectionActions, showCheckbox = true, // 기본값 true - showRowNumber = true, // 기본값 true (번호 컬럼은 renderTableRow에서 처리) + showRowNumber: _showRowNumber = true, // 기본값 true (번호 컬럼은 renderTableRow에서 처리) renderTableRow, renderMobileCard, pagination, - devMetadata, + devMetadata: _devMetadata, isLoading, columnSettings, }: IntegratedListTemplateV2Props) { const [showDeleteDialog, setShowDeleteDialog] = useState(false); + // ===== copyable 컬럼 자동 래핑 ===== + // copyable: true인 컬럼의 셀 인덱스 맵 (cell index → column key) + const copyableMap = useCallback(() => { + const map = new Map(); + 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)[colKey]; + const copyValue = rawValue != null ? String(rawValue) : ''; + if (!copyValue) return cell; + + const cellEl = cell as React.ReactElement<{ children?: ReactNode }>; + return cloneElement(cellEl, {}, + {cellEl.props.children} + ); + }); + + return cloneElement(rowEl, {}, ...newCells); + }, [copyableMap]); + // ===== 서버 사이드 모바일 인피니티 스크롤 ===== // 모바일에서 누적 데이터를 관리하여 스크롤 시 계속 추가 const [accumulatedMobileData, setAccumulatedMobileData] = useState([]); @@ -368,7 +407,7 @@ export function IntegratedListTemplateV2({ setAccumulatedMobileData([]); setLastAccumulatedPage(0); } - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeTab]); // activeTab만 감지 - 탭 변경 시에만 리셋 // 클라이언트 사이드: allData가 변경되면 displayCount 리셋 @@ -574,7 +613,7 @@ export function IntegratedListTemplateV2({ }; // 헤더 액션 스켈레톤 (달력 + 프리셋 버튼 + 등록 버튼) - const renderHeaderActionSkeleton = () => ( + const _renderHeaderActionSkeleton = () => (
{dateRangeSelector?.enabled && ( <> @@ -974,11 +1013,14 @@ export function IntegratedListTemplateV2({ return ( onSort(column.key) : undefined} + className={`${column.className || ''} ${columnSettings ? 'relative' : ''}`} > {column.key === "actions" && selectedItems.size === 0 ? "" : ( -
+
+ { e.stopPropagation(); onSort(column.key); } : undefined} + > {column.label} {isSortable && ( @@ -993,6 +1035,7 @@ export function IntegratedListTemplateV2({ )} )} +
)} {columnSettings && ( @@ -1047,7 +1090,7 @@ export function IntegratedListTemplateV2({ const globalIndex = startIndex + index + 1; return ( - {renderTableRow(item, index, globalIndex)} + {applyCopyableCells(renderTableRow(item, index, globalIndex), item)} ); }) diff --git a/src/components/templates/UniversalListPage/index.tsx b/src/components/templates/UniversalListPage/index.tsx index dff79ee4..bd6ea562 100644 --- a/src/components/templates/UniversalListPage/index.tsx +++ b/src/components/templates/UniversalListPage/index.tsx @@ -353,7 +353,7 @@ export function UniversalListPage({ // ⚠️ config.onDataChange를 deps에서 제외: 콜백 참조 변경으로 인한 무한 루프 방지 useEffect(() => { config.onDataChange?.(rawData); - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [rawData]); // 서버 사이드 필터링: 의존성 변경 시 데이터 새로고침 @@ -545,7 +545,7 @@ export function UniversalListPage({ useEffect(() => { onSearchChange?.(debouncedSearchValue); config.onSearchChange?.(debouncedSearchValue); - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [debouncedSearchValue, onSearchChange]); // ===== 필터 핸들러 ===== diff --git a/src/components/templates/UniversalListPage/types.ts b/src/components/templates/UniversalListPage/types.ts index 90fb2e41..57fad422 100644 --- a/src/components/templates/UniversalListPage/types.ts +++ b/src/components/templates/UniversalListPage/types.ts @@ -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; } /** diff --git a/src/components/ui/confirm-dialog.tsx b/src/components/ui/confirm-dialog.tsx index 4db106fe..120fe20a 100644 --- a/src/components/ui/confirm-dialog.tsx +++ b/src/components/ui/confirm-dialog.tsx @@ -204,8 +204,7 @@ export function SaveConfirmDialog({ /** * 취소 확인 다이얼로그 프리셋 */ -export interface CancelConfirmDialogProps - extends Omit {} +export type CancelConfirmDialogProps = Omit; export function CancelConfirmDialog({ description = '작업을 취소하시겠습니까? 변경사항이 저장되지 않습니다.', diff --git a/src/components/ui/currency-input.tsx b/src/components/ui/currency-input.tsx index 1f95fb04..a221ee22 100644 --- a/src/components/ui/currency-input.tsx +++ b/src/components/ui/currency-input.tsx @@ -113,7 +113,7 @@ const CurrencyInput = React.forwardRef( // 숫자와 음수 기호만 허용 if (allowNegative) { - result = result.replace(/[^\d\-]/g, ""); + result = result.replace(/[^\d-]/g, ""); // 음수 기호는 맨 앞에만 if (result.includes("-")) { const isNegative = result.startsWith("-"); diff --git a/src/components/ui/loading-spinner.tsx b/src/components/ui/loading-spinner.tsx index 0d10e6e4..cd748207 100644 --- a/src/components/ui/loading-spinner.tsx +++ b/src/components/ui/loading-spinner.tsx @@ -92,7 +92,7 @@ interface TableLoadingSpinnerProps { export const TableLoadingSpinner: React.FC = ({ text = '데이터를 불러오는 중...', - rows = 5 + rows: _rows = 5 }) => { return (
diff --git a/src/components/ui/number-input.tsx b/src/components/ui/number-input.tsx index 81db645e..26a69dc2 100644 --- a/src/components/ui/number-input.tsx +++ b/src/components/ui/number-input.tsx @@ -103,11 +103,11 @@ const NumberInput = React.forwardRef( // 허용 문자만 남기기 if (allowDecimal && allowNegative) { - result = result.replace(/[^\d.\-]/g, ""); + result = result.replace(/[^\d.-]/g, ""); } else if (allowDecimal) { result = result.replace(/[^\d.]/g, ""); } else if (allowNegative) { - result = result.replace(/[^\d\-]/g, ""); + result = result.replace(/[^\d-]/g, ""); } else { result = result.replace(/\D/g, ""); } diff --git a/src/components/ui/quantity-input.tsx b/src/components/ui/quantity-input.tsx index 70d7c321..bed25021 100644 --- a/src/components/ui/quantity-input.tsx +++ b/src/components/ui/quantity-input.tsx @@ -63,7 +63,7 @@ const QuantityInput = React.forwardRef( disabled, onFocus, onBlur, - // eslint-disable-next-line @typescript-eslint/no-unused-vars + defaultValue: _defaultValue, // controlled component이므로 defaultValue 무시 ...props }, diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx index a345451e..84db53d7 100644 --- a/src/components/ui/skeleton.tsx +++ b/src/components/ui/skeleton.tsx @@ -47,7 +47,7 @@ function TableRowSkeleton({ showCheckbox = true, showActions = true, }: TableRowSkeletonProps) { - const totalCols = columns + (showCheckbox ? 1 : 0) + (showActions ? 1 : 0); + const _totalCols = columns + (showCheckbox ? 1 : 0) + (showActions ? 1 : 0); return ( diff --git a/src/components/vehicle-management/ForkliftList/index.tsx b/src/components/vehicle-management/ForkliftList/index.tsx index 53ffc881..b67caebe 100644 --- a/src/components/vehicle-management/ForkliftList/index.tsx +++ b/src/components/vehicle-management/ForkliftList/index.tsx @@ -99,18 +99,18 @@ export function ForkliftList({ initialData }: ForkliftListProps) { // 레거시 컬럼 구조 (5130 기준) - 13개 컬럼 columns: [ { key: 'rowNumber', label: '번호', className: 'w-[50px] text-center' }, - { key: 'vehicleNumber', label: '차량번호', className: 'min-w-[100px]' }, - { key: 'vehicleType', label: '차종', className: 'w-[90px]' }, - { key: 'manager', label: '담당자', className: 'w-[80px]' }, - { key: 'purchaseCompany', label: '구입업체', className: 'w-[90px]' }, - { key: 'purchaseCompanyContact', label: '구입업체 연락처', className: 'w-[110px]' }, - { key: 'firstRegistrationDate', label: '최초등록일', className: 'w-[90px]' }, - { key: 'purchaseDate', label: '구매일자', className: 'w-[90px]' }, - { key: 'purchaseType', label: '구매 유형', className: 'w-[80px]' }, - { key: 'partsChangeCycle', label: '부속품 교환 주기', className: 'w-[100px]' }, - { key: 'partsChangeRecords', label: '부속품 교환일', className: 'min-w-[150px]' }, - { key: 'maintenanceRecords', label: '정비 정보', className: 'min-w-[150px]' }, - { key: 'remarks', label: '비고', className: 'min-w-[150px]' }, + { key: 'vehicleNumber', label: '차량번호', className: 'min-w-[100px]', copyable: true }, + { key: 'vehicleType', label: '차종', className: 'w-[90px]', copyable: true }, + { key: 'manager', label: '담당자', className: 'w-[80px]', copyable: true }, + { key: 'purchaseCompany', label: '구입업체', className: 'w-[90px]', copyable: true }, + { key: 'purchaseCompanyContact', label: '구입업체 연락처', className: 'w-[110px]', copyable: true }, + { key: 'firstRegistrationDate', label: '최초등록일', className: 'w-[90px]', copyable: true }, + { key: 'purchaseDate', label: '구매일자', className: 'w-[90px]', copyable: true }, + { key: 'purchaseType', label: '구매 유형', className: 'w-[80px]', copyable: true }, + { key: 'partsChangeCycle', label: '부속품 교환 주기', className: 'w-[100px]', copyable: true }, + { key: 'partsChangeRecords', label: '부속품 교환일', className: 'min-w-[150px]', copyable: true }, + { key: 'maintenanceRecords', label: '정비 정보', className: 'min-w-[150px]', copyable: true }, + { key: 'remarks', label: '비고', className: 'min-w-[150px]', copyable: true }, ], clientSideFiltering: true, diff --git a/src/components/vehicle-management/VehicleDetail/index.tsx b/src/components/vehicle-management/VehicleDetail/index.tsx index 03856b02..19bbf987 100644 --- a/src/components/vehicle-management/VehicleDetail/index.tsx +++ b/src/components/vehicle-management/VehicleDetail/index.tsx @@ -12,7 +12,6 @@ import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetai import type { Vehicle, VehicleFormData } from '../types'; import { vehicleDetailConfig, vehicleCreateConfig, vehicleEditConfig } from './config'; import { - getVehicleById, createVehicle, updateVehicle, deleteVehicle, diff --git a/src/components/vehicle-management/VehicleList/actions.ts b/src/components/vehicle-management/VehicleList/actions.ts index 993f03ae..abaa5d2d 100644 --- a/src/components/vehicle-management/VehicleList/actions.ts +++ b/src/components/vehicle-management/VehicleList/actions.ts @@ -6,7 +6,7 @@ */ import { isNextRedirectError } from '@/lib/utils/redirect-error'; -import type { Vehicle, VehicleFormData, ActionResponse, ListResponse, OilChangeRecord, MaintenanceRecord } from '../types'; +import type { Vehicle, VehicleFormData, ActionResponse, ListResponse } from '../types'; // ===== Mock 데이터 (API 연동 전까지 사용) ===== const mockVehicles: Vehicle[] = [ diff --git a/src/components/vehicle-management/VehicleList/index.tsx b/src/components/vehicle-management/VehicleList/index.tsx index 0f3aa147..8678a824 100644 --- a/src/components/vehicle-management/VehicleList/index.tsx +++ b/src/components/vehicle-management/VehicleList/index.tsx @@ -30,7 +30,7 @@ interface VehicleListProps { export function VehicleList({ initialData }: VehicleListProps) { const router = useRouter(); - const [allData, setAllData] = useState(initialData); + const [, setAllData] = useState(initialData); const deleteDialog = useDeleteDialog({ onDelete: deleteVehicle, @@ -99,17 +99,17 @@ export function VehicleList({ initialData }: VehicleListProps) { // 레거시 컬럼 구조 (5130 기준) - 12개 컬럼 columns: [ { key: 'rowNumber', label: '번호', className: 'w-[50px] text-center' }, - { key: 'vehicleNumber', label: '차량번호', className: 'min-w-[120px]' }, - { key: 'managerMain', label: '담당자', className: 'w-[80px]' }, - { key: 'insuranceCompany', label: '보험사', className: 'w-[100px]' }, - { key: 'insuranceContact', label: '보험사 연락처', className: 'w-[110px]' }, - { key: 'firstRegistrationDate', label: '최초등록일', className: 'w-[90px]' }, - { key: 'purchaseDate', label: '구매일자', className: 'w-[90px]' }, - { key: 'purchaseType', label: '구매 유형', className: 'w-[80px]' }, - { key: 'oilChangeCycle', label: '엔진오일 교환 주기', className: 'w-[110px]' }, - { key: 'oilChangeRecords', label: '엔진오일교환일', className: 'min-w-[150px]' }, - { key: 'maintenanceRecords', label: '정비 정보', className: 'min-w-[180px]' }, - { key: 'remarks', label: '비고', className: 'min-w-[120px]' }, + { key: 'vehicleNumber', label: '차량번호', className: 'min-w-[120px]', copyable: true }, + { key: 'managerMain', label: '담당자', className: 'w-[80px]', copyable: true }, + { key: 'insuranceCompany', label: '보험사', className: 'w-[100px]', copyable: true }, + { key: 'insuranceContact', label: '보험사 연락처', className: 'w-[110px]', copyable: true }, + { key: 'firstRegistrationDate', label: '최초등록일', className: 'w-[90px]', copyable: true }, + { key: 'purchaseDate', label: '구매일자', className: 'w-[90px]', copyable: true }, + { key: 'purchaseType', label: '구매 유형', className: 'w-[80px]', copyable: true }, + { key: 'oilChangeCycle', label: '엔진오일 교환 주기', className: 'w-[110px]', copyable: true }, + { key: 'oilChangeRecords', label: '엔진오일교환일', className: 'min-w-[150px]', copyable: true }, + { key: 'maintenanceRecords', label: '정비 정보', className: 'min-w-[180px]', copyable: true }, + { key: 'remarks', label: '비고', className: 'min-w-[120px]', copyable: true }, ], clientSideFiltering: true, diff --git a/src/components/vehicle-management/VehicleLogDetail/index.tsx b/src/components/vehicle-management/VehicleLogDetail/index.tsx index e4d8635c..1b38ede6 100644 --- a/src/components/vehicle-management/VehicleLogDetail/index.tsx +++ b/src/components/vehicle-management/VehicleLogDetail/index.tsx @@ -11,7 +11,6 @@ import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetai import type { VehicleLog, VehicleLogFormData } from '../types'; import { vehicleLogDetailConfig, vehicleLogCreateConfig, vehicleLogEditConfig } from './config'; import { - getVehicleLogById, createVehicleLog, updateVehicleLog, deleteVehicleLog, diff --git a/src/components/vehicle-management/VehicleLogList/index.tsx b/src/components/vehicle-management/VehicleLogList/index.tsx index 1123d543..2cd8e44d 100644 --- a/src/components/vehicle-management/VehicleLogList/index.tsx +++ b/src/components/vehicle-management/VehicleLogList/index.tsx @@ -102,10 +102,10 @@ export function VehicleLogList({ initialData }: VehicleLogListProps) { // 레거시 컬럼 구조 (5130 기준) - 5개 컬럼 columns: [ { key: 'rowNumber', label: '번호', className: 'w-[60px] text-center' }, - { key: 'writeDate', label: '작성일', className: 'w-[100px]' }, - { key: 'vehicleInfo', label: '차량종류', className: 'min-w-[180px]' }, - { key: 'writer', label: '작성자', className: 'w-[80px]' }, - { key: 'title', label: '글제목', className: 'min-w-[250px]' }, + { key: 'writeDate', label: '작성일', className: 'w-[100px]', copyable: true }, + { key: 'vehicleInfo', label: '차량종류', className: 'min-w-[180px]', copyable: true }, + { key: 'writer', label: '작성자', className: 'w-[80px]', copyable: true }, + { key: 'title', label: '글제목', className: 'min-w-[250px]', copyable: true }, ], clientSideFiltering: true, diff --git a/src/contexts/ApiErrorContext.tsx b/src/contexts/ApiErrorContext.tsx index 00a93687..2f520465 100644 --- a/src/contexts/ApiErrorContext.tsx +++ b/src/contexts/ApiErrorContext.tsx @@ -8,7 +8,6 @@ */ import { createContext, useContext, useCallback, useRef, type ReactNode } from 'react'; -import { useRouter } from 'next/navigation'; import { isAuthError, isApiError, type ApiErrorResponse } from '@/lib/api/errors'; import { callLogoutAPI } from '@/lib/auth/logout'; @@ -33,7 +32,6 @@ const ApiErrorContext = createContext(null); * Protected Layout에 추가하여 모든 하위 페이지에서 사용 */ export function ApiErrorProvider({ children }: { children: ReactNode }) { - const router = useRouter(); const isRedirecting = useRef(false); /** diff --git a/src/contexts/ItemMasterContext.tsx b/src/contexts/ItemMasterContext.tsx index 29e96c5c..182404c3 100644 --- a/src/contexts/ItemMasterContext.tsx +++ b/src/contexts/ItemMasterContext.tsx @@ -16,8 +16,6 @@ import type { IndependentSectionRequest, IndependentFieldRequest, IndependentBomItemRequest, - LinkSectionRequest, - LinkFieldRequest, SectionUsageResponse, FieldUsageResponse, } from '@/types/item-master-api'; @@ -49,11 +47,8 @@ export type { } from '@/types/item-master.types'; import type { - BendingDetail, - BOMLine, SpecificationMaster, MaterialItemName, - ItemRevision, ItemMaster, ItemCategory, ItemUnit, @@ -62,9 +57,7 @@ import type { PartTypeOption, PartUsageOption, GuideRailOption, - ItemFieldProperty, ItemMasterField, - FieldDisplayCondition, ItemField, BOMItem, ItemSection, diff --git a/src/hooks/useAccountingListPage.ts b/src/hooks/useAccountingListPage.ts index 829b8a47..d83c522a 100644 --- a/src/hooks/useAccountingListPage.ts +++ b/src/hooks/useAccountingListPage.ts @@ -72,7 +72,7 @@ export function useAccountingListPage( }) .finally(() => setIsLoading(false)); // mode 변경 시에만 재실행 - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mode]); return { data, pagination, isLoading, mode }; diff --git a/src/hooks/useCEODashboard.ts b/src/hooks/useCEODashboard.ts index f2d1611d..1ab25b55 100644 --- a/src/hooks/useCEODashboard.ts +++ b/src/hooks/useCEODashboard.ts @@ -782,7 +782,7 @@ export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashbo us.refetch(); cs.refetch(); da.refetch(); - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dr.refetch, rv.refetch, dc.refetch, me.refetch, fetchCM, sb.refetch, ss.refetch, ps.refetch, dp.refetch, us.refetch, cs.refetch, da.refetch]); // 섹션별 refetch 함수 맵 (targeted invalidation용) diff --git a/src/hooks/useDashboardFetch.ts b/src/hooks/useDashboardFetch.ts index 949a39cc..caa57425 100644 --- a/src/hooks/useDashboardFetch.ts +++ b/src/hooks/useDashboardFetch.ts @@ -145,7 +145,7 @@ export function useDashboardMultiFetch( } finally { setLoading(false); } - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sourcesKey, transformer]); useEffect(() => { diff --git a/src/hooks/useStatsLoader.ts b/src/hooks/useStatsLoader.ts index e1454669..7f46e540 100644 --- a/src/hooks/useStatsLoader.ts +++ b/src/hooks/useStatsLoader.ts @@ -41,7 +41,7 @@ export function useStatsLoader( useEffect(() => { if (initialData != null) return; reload(); - // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return { data, setData, reload }; diff --git a/src/layouts/AuthenticatedLayout.tsx b/src/layouts/AuthenticatedLayout.tsx index ca84ec75..817cb55f 100644 --- a/src/layouts/AuthenticatedLayout.tsx +++ b/src/layouts/AuthenticatedLayout.tsx @@ -23,7 +23,6 @@ import { DollarSign, ChevronLeft, Home, - X, Bell, Clock, Minus, @@ -31,7 +30,6 @@ import { Type, CalendarDays, } from 'lucide-react'; -import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Sheet, SheetContent, SheetTrigger, SheetHeader, SheetTitle } from '@/components/ui/sheet'; import { @@ -115,7 +113,6 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro useCalendarScheduleInit(new Date().getFullYear()); // 폰트 크기 조절 (12~20px, 기본 16px) - const FONT_SIZES = [12, 13, 14, 15, 16, 17, 18, 19, 20] as const; const [fontSize, setFontSize] = useState(16); useEffect(() => { @@ -147,8 +144,8 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro // 사용자 정보 상태 const [userName, setUserName] = useState("사용자"); const [userPosition, setUserPosition] = useState("직책"); - const [userEmail, setUserEmail] = useState(""); - const [userCompany, setUserCompany] = useState(""); + const [_userEmail, setUserEmail] = useState(""); + const [_userCompany, setUserCompany] = useState(""); // 회사 선택 상태 (목업) const [selectedCompany, setSelectedCompany] = useState("all"); @@ -524,7 +521,7 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro }); } } - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pathname, menuItems, setActiveMenu]); const handleMenuClick = (menuId: string, path: string) => { diff --git a/src/lib/api/execute-server-action.ts b/src/lib/api/execute-server-action.ts index 96316da3..6e3a6138 100644 --- a/src/lib/api/execute-server-action.ts +++ b/src/lib/api/execute-server-action.ts @@ -114,8 +114,21 @@ export async function executeServerAction( return { success: true }; } - // JSON 파싱 - const result = await response.json(); + // JSON 파싱 (백엔드가 HTML 에러 페이지 반환 시 안전 처리) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let result: any; + try { + result = await response.json(); + } catch { + const status = response.status; + console.error(`[executeServerAction] JSON 파싱 실패 (${method} ${url}, status: ${status})`); + return { + success: false, + error: status === 404 + ? '요청한 API를 찾을 수 없습니다.' + : `서버 오류가 발생했습니다. (${status})`, + }; + } // API 실패 응답 if (!response.ok || !result.success) { diff --git a/src/lib/api/item-master.ts b/src/lib/api/item-master.ts index 8937465a..027aa69e 100644 --- a/src/lib/api/item-master.ts +++ b/src/lib/api/item-master.ts @@ -38,11 +38,6 @@ import type { LinkEntityRequest, LinkBomRequest, ReorderRelationshipsRequest, - // 2025-12-21 추가: 재질/표면처리 타입 - MaterialOptionRequest, - MaterialOptionResponse, - TreatmentOptionRequest, - TreatmentOptionResponse, } from '@/types/item-master-api'; import { getAuthHeaders } from './auth-headers'; import { handleApiError } from './error-handler'; diff --git a/src/lib/api/quote.ts b/src/lib/api/quote.ts index 83f5cb5c..5147ebb5 100644 --- a/src/lib/api/quote.ts +++ b/src/lib/api/quote.ts @@ -1,7 +1,6 @@ // lib/api/quote.ts // 견적 자동산출 API 클라이언트 import { ApiClient } from './client'; -import { AUTH_CONFIG } from './auth/auth-config'; // =================================== // 타입 정의 diff --git a/src/lib/api/refresh-token.ts b/src/lib/api/refresh-token.ts index 1e8aa7c2..3e6713e7 100644 --- a/src/lib/api/refresh-token.ts +++ b/src/lib/api/refresh-token.ts @@ -101,7 +101,7 @@ async function doRefreshToken(refreshToken: string): Promise { */ export async function refreshAccessToken( refreshToken: string, - caller: string = 'unknown' + _caller: string = 'unknown' ): Promise { const cache = getRefreshCache(); const now = Date.now(); diff --git a/src/lib/auth/logout.ts b/src/lib/auth/logout.ts index 10a1209c..d92e4158 100644 --- a/src/lib/auth/logout.ts +++ b/src/lib/auth/logout.ts @@ -206,21 +206,21 @@ export function debugCacheStatus(): void { console.group('[Debug] Cache Status'); // sessionStorage - const sessionKeys = Object.keys(sessionStorage).filter((key) => + Object.keys(sessionStorage).filter((key) => SESSION_STORAGE_PREFIXES.some((prefix) => key.startsWith(prefix)) ); // localStorage - const localKeys = Object.keys(localStorage).filter( + Object.keys(localStorage).filter( (key) => LOCAL_STORAGE_KEYS.includes(key) || LOCAL_STORAGE_PREFIXES.some((prefix) => key.startsWith(prefix)) ); // Zustand - const masterDataState = useMasterDataStore.getState(); + useMasterDataStore.getState(); - const itemMasterState = useItemMasterStore.getState(); + useItemMasterStore.getState(); console.groupEnd(); } \ No newline at end of file diff --git a/src/lib/capacitor/fcm.ts b/src/lib/capacitor/fcm.ts index 32011a35..e35fdcf3 100644 --- a/src/lib/capacitor/fcm.ts +++ b/src/lib/capacitor/fcm.ts @@ -283,7 +283,7 @@ export async function unregisterFCMToken(): Promise { credentials: 'include', body: JSON.stringify({ token }), }); - } catch (e) { + } catch (__e) { console.warn('[FCM] Unregister failed, clearing local token'); } diff --git a/src/lib/print-utils.ts b/src/lib/print-utils.ts index d6ae1a32..73f03258 100644 --- a/src/lib/print-utils.ts +++ b/src/lib/print-utils.ts @@ -62,7 +62,7 @@ export function printElement( .join('\n'); return ``; } - } catch (e) { + } catch (__e) { // CORS 에러 등으로 접근 불가한 스타일시트는 무시 if (styleSheet.href) { return ``; diff --git a/src/lib/utils/excel-download.ts b/src/lib/utils/excel-download.ts index 1e89b5d6..7376b5a1 100644 --- a/src/lib/utils/excel-download.ts +++ b/src/lib/utils/excel-download.ts @@ -432,7 +432,6 @@ export async function parseExcelFile>( dataRows.forEach((row, rowIndex) => { const rowNumber = rowIndex + skipRows + 1; const rowData: Record = {}; - let hasError = false; columns.forEach((col) => { const colIndex = columnIndexMap.get(col.key); @@ -448,7 +447,6 @@ export async function parseExcelFile>( column: col.header, message: `${col.header}은(는) 필수입니다`, }); - hasError = true; } // 타입 검사 @@ -459,7 +457,6 @@ export async function parseExcelFile>( column: col.header, message: `${col.header}은(는) 숫자여야 합니다`, }); - hasError = true; } if (col.type === 'select' && col.options && !col.options.includes(value)) { @@ -468,7 +465,6 @@ export async function parseExcelFile>( column: col.header, message: `${col.header}은(는) [${col.options.join(', ')}] 중 하나여야 합니다`, }); - hasError = true; } } diff --git a/src/lib/utils/locale.ts b/src/lib/utils/locale.ts index d4cf2fa1..7a3f2153 100644 --- a/src/lib/utils/locale.ts +++ b/src/lib/utils/locale.ts @@ -1,6 +1,6 @@ import { locales } from '@/i18n/config'; -const LOCALE_PREFIX_RE = new RegExp(`^\\/(${locales.join('|')})(\/|$)`); +const LOCALE_PREFIX_RE = new RegExp(`^\\/(${locales.join('|')})(/|$)`); const LOCALE_PREFIX_SLASH_RE = new RegExp(`^\\/(${locales.join('|')})\\/`); /** URL에서 locale prefix 제거 (/ko/hr/... → /hr/...) */ diff --git a/src/lib/utils/validation/form-schemas.ts b/src/lib/utils/validation/form-schemas.ts index 25cd6988..b008e20d 100644 --- a/src/lib/utils/validation/form-schemas.ts +++ b/src/lib/utils/validation/form-schemas.ts @@ -5,15 +5,13 @@ */ import { z } from 'zod'; -import { itemTypeSchema, bendingDetailSchema } from './common'; +import { itemTypeSchema } from './common'; import { productSchema, productSchemaBase, partSchemaBase, materialSchemaBase, - materialSchema, consumableSchemaBase, - consumableSchema, } from './item-schemas'; // ===== 폼 데이터 스키마 (생성/수정용) ===== diff --git a/src/lib/utils/validation/item-schemas.ts b/src/lib/utils/validation/item-schemas.ts index 0d910b59..80a3a6b9 100644 --- a/src/lib/utils/validation/item-schemas.ts +++ b/src/lib/utils/validation/item-schemas.ts @@ -9,7 +9,6 @@ import { itemNameSchema, itemTypeSchema, dateSchema, - positiveNumberSchema, bomLineSchema, bendingDetailSchema, } from './common'; diff --git a/src/middleware.ts b/src/middleware.ts index b94ecf2b..9aa46438 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -257,7 +257,7 @@ export async function middleware(request: NextRequest) { } // 4️⃣ 인증 체크 - const { isAuthenticated, authMode } = checkAuthentication(request); + const { isAuthenticated } = checkAuthentication(request); // 4.5️⃣ MVP: /signup 접근 차단 → /login 리다이렉트 (2025-12-04) // 회원가입 기능은 운영 페이지로 이동 예정 diff --git a/src/stores/item-master/useItemMasterStore.ts b/src/stores/item-master/useItemMasterStore.ts index c70712ce..ffd09b3b 100644 --- a/src/stores/item-master/useItemMasterStore.ts +++ b/src/stores/item-master/useItemMasterStore.ts @@ -21,10 +21,7 @@ import type { import { itemMasterApi } from '@/lib/api/item-master'; import { normalizeInitResponse, - normalizePageResponse, normalizeSectionResponse, - normalizeFieldResponse, - normalizeBomItemResponse, denormalizePageForRequest, denormalizeSectionForRequest, denormalizeFieldForRequest, diff --git a/src/stores/utils/userStorage.ts b/src/stores/utils/userStorage.ts index ed710782..bfad6142 100644 --- a/src/stores/utils/userStorage.ts +++ b/src/stores/utils/userStorage.ts @@ -24,7 +24,7 @@ export function createUserStorage(baseKey: string) { const key = getStorageKey(baseKey); localStorage.setItem(key, JSON.stringify(value)); }, - removeItem: (name: string) => { + removeItem: (_name: string) => { const key = getStorageKey(baseKey); localStorage.removeItem(key); },