From 44b74ff4170090673b6eeacd8c528e0abf47b7aa Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Mon, 1 Dec 2025 19:52:06 +0900 Subject: (김준회) deprecated code 정리 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docu-list-rule/code-groups/page.tsx | 54 - .../docu-list-rule/combo-box-settings/page.tsx | 53 - .../docu-list-rule/document-class/page.tsx | 52 - .../(engineering)/docu-list-rule/layout.tsx | 69 - .../docu-list-rule/number-type-configs/page.tsx | 60 - .../docu-list-rule/number-types/page.tsx | 52 - .../(engineering)/docu-list-rule/page.tsx | 12 - .../(engineering)/document-list-only/layout.tsx | 17 - .../(engineering)/document-list-only/page.tsx | 98 - .../(engineering)/document-list-ship/page.tsx | 144 - .../(engineering)/faq/manage/actions.ts | 48 - .../engineering/(engineering)/faq/manage/page.tsx | 38 - app/[lng]/engineering/(engineering)/faq/page.tsx | 62 - .../engineering/(engineering)/form-list/page.tsx | 75 - app/[lng]/engineering/(engineering)/items/page.tsx | 68 - app/[lng]/engineering/(engineering)/layout.tsx | 18 - .../engineering/(engineering)/projects/page.tsx | 75 - .../engineering/(engineering)/report/page.tsx | 105 - .../(engineering)/tag-numbering/page.tsx | 74 - app/[lng]/engineering/(engineering)/tasks/page.tsx | 63 - app/[lng]/engineering/(engineering)/tbe/page.tsx | 113 - .../(engineering)/vendor-check-list/page.tsx | 74 - .../[formId]/[projectId]/[contractId]/page.tsx | 79 - .../(engineering)/vendor-data/layout.tsx | 67 - .../engineering/(engineering)/vendor-data/page.tsx | 28 - .../(engineering)/vendor-data/tag/[id]/page.tsx | 43 - .../(engineering)/vendor-investigation/page.tsx | 65 - app/[lng]/engineering/page.tsx | 21 - .../evcp/(evcp)/(master-data)/p-items/page.tsx | 62 + .../(evcp)/(procurement)/legal-review/page.tsx | 87 - .../evcp/(evcp)/(procurement)/p-items/page.tsx | 62 - app/[lng]/partners/(partners)/cbe/page.tsx | 89 - .../rfq-answer/[vendorId]/[rfqRecordId]/page.tsx | 174 - app/[lng]/partners/(partners)/rfq-answer/page.tsx | 213 -- .../partners/(partners)/rfq-ship/[id]/page.tsx | 81 - app/[lng]/partners/(partners)/rfq-ship/page.tsx | 174 - app/[lng]/partners/(partners)/rfq/page.tsx | 136 - app/[lng]/partners/(partners)/tbe/page.tsx | 88 - .../(procurement)/b-rfq/[id]/final/page.tsx | 0 .../(procurement)/b-rfq/[id]/initial/page.tsx | 52 - .../(procurement)/b-rfq/[id]/layout.tsx | 87 - .../procurement/(procurement)/b-rfq/[id]/page.tsx | 53 - app/[lng]/procurement/(procurement)/b-rfq/page.tsx | 79 - .../(procurement)/basic-contract-template/page.tsx | 74 - .../(procurement)/basic-contract/page.tsx | 74 - app/[lng]/procurement/(procurement)/bqcbe/page.tsx | 74 - app/[lng]/procurement/(procurement)/bqtbe/page.tsx | 72 - .../(procurement)/budgetary-rfq/[id]/cbe/page.tsx | 56 - .../(procurement)/budgetary-rfq/[id]/layout.tsx | 90 - .../(procurement)/budgetary-rfq/[id]/page.tsx | 57 - .../(procurement)/budgetary-rfq/[id]/tbe/page.tsx | 55 - .../(procurement)/budgetary-rfq/page.tsx | 86 - .../(procurement)/budgetary/[id]/cbe/page.tsx | 56 - .../(procurement)/budgetary/[id]/layout.tsx | 90 - .../(procurement)/budgetary/[id]/page.tsx | 57 - .../(procurement)/budgetary/[id]/tbe/page.tsx | 55 - .../procurement/(procurement)/budgetary/page.tsx | 86 - .../procurement/(procurement)/dashboard/page.tsx | 17 - .../procurement/(procurement)/equip-class/page.tsx | 75 - .../(procurement)/esg-check-list/page.tsx | 74 - .../(procurement)/evaluation-check-list/page.tsx | 81 - .../(procurement)/evaluation-input/[id]/page.tsx | 22 - .../(procurement)/evaluation-input/page.tsx | 135 - .../(procurement)/evaluation-target-list/page.tsx | 118 - .../procurement/(procurement)/evaluation/page.tsx | 181 - .../(procurement)/faq/manage/actions.ts | 48 - .../procurement/(procurement)/faq/manage/page.tsx | 38 - app/[lng]/procurement/(procurement)/faq/page.tsx | 62 - .../procurement/(procurement)/incoterms/page.tsx | 53 - .../(procurement)/items-tech/layout.tsx | 38 - .../procurement/(procurement)/items-tech/page.tsx | 67 - app/[lng]/procurement/(procurement)/items/page.tsx | 68 - app/[lng]/procurement/(procurement)/layout.tsx | 18 - .../procurement/(procurement)/menu-list/page.tsx | 70 - .../(procurement)/payment-conditions/page.tsx | 53 - .../procurement/(procurement)/po-rfq/page.tsx | 61 - app/[lng]/procurement/(procurement)/po/page.tsx | 65 - app/[lng]/procurement/(procurement)/poa/page.tsx | 61 - .../(procurement)/pq-criteria/[pqListId]/page.tsx | 68 - .../procurement/(procurement)/pq-criteria/page.tsx | 61 - .../pq_new/[vendorId]/[submissionId]/page.tsx | 206 - .../procurement/(procurement)/pq_new/page.tsx | 99 - .../procurement/(procurement)/project-gtc/page.tsx | 63 - .../(procurement)/project-vendors/page.tsx | 74 - .../procurement/(procurement)/projects/page.tsx | 75 - .../procurement/(procurement)/report/page.tsx | 105 - .../(procurement)/rfq/[id]/cbe/page.tsx | 55 - .../procurement/(procurement)/rfq/[id]/layout.tsx | 89 - .../procurement/(procurement)/rfq/[id]/page.tsx | 55 - .../(procurement)/rfq/[id]/tbe/page.tsx | 55 - app/[lng]/procurement/(procurement)/rfq/page.tsx | 80 - .../procurement/(procurement)/settings/layout.tsx | 68 - .../procurement/(procurement)/settings/page.tsx | 18 - .../(procurement)/settings/preferences/page.tsx | 17 - .../(procurement)/system/admin-users/page.tsx | 60 - .../procurement/(procurement)/system/layout.tsx | 80 - .../procurement/(procurement)/system/page.tsx | 56 - .../(procurement)/system/password-policy/page.tsx | 63 - .../(procurement)/system/permissions/page.tsx | 17 - .../(procurement)/system/roles/page.tsx | 68 - app/[lng]/procurement/(procurement)/tbe/page.tsx | 113 - .../(procurement)/vendor-candidates/page.tsx | 78 - .../(procurement)/vendor-check-list/page.tsx | 74 - .../(procurement)/vendor-investigation/page.tsx | 65 - .../procurement/(procurement)/vendor-type/page.tsx | 70 - .../(procurement)/vendors/[id]/info/items/page.tsx | 56 - .../(procurement)/vendors/[id]/info/layout.tsx | 94 - .../vendors/[id]/info/materials/page.tsx | 56 - .../(procurement)/vendors/[id]/info/page.tsx | 56 - .../vendors/[id]/info/rfq-history/page.tsx | 55 - .../procurement/(procurement)/vendors/page.tsx | 78 - app/[lng]/procurement/page.tsx | 21 - app/[lng]/sales/(sales)/bid-projects/page.tsx | 74 - app/[lng]/sales/(sales)/bqcbe/page.tsx | 74 - app/[lng]/sales/(sales)/bqtbe/page.tsx | 72 - .../sales/(sales)/budgetary-rfq/[id]/cbe/page.tsx | 56 - .../sales/(sales)/budgetary-rfq/[id]/layout.tsx | 90 - .../sales/(sales)/budgetary-rfq/[id]/page.tsx | 57 - .../sales/(sales)/budgetary-rfq/[id]/tbe/page.tsx | 55 - app/[lng]/sales/(sales)/budgetary-rfq/page.tsx | 86 - .../(sales)/budgetary-tech-sales-hull/page.tsx | 61 - .../(sales)/budgetary-tech-sales-ship/page.tsx | 61 - .../(sales)/budgetary-tech-sales-top/page.tsx | 61 - .../sales/(sales)/budgetary/[id]/cbe/page.tsx | 56 - app/[lng]/sales/(sales)/budgetary/[id]/layout.tsx | 90 - app/[lng]/sales/(sales)/budgetary/[id]/page.tsx | 57 - .../sales/(sales)/budgetary/[id]/tbe/page.tsx | 55 - app/[lng]/sales/(sales)/budgetary/page.tsx | 86 - app/[lng]/sales/(sales)/dashboard/page.tsx | 17 - app/[lng]/sales/(sales)/esg-check-list/page.tsx | 74 - .../sales/(sales)/evaluation-check-list/page.tsx | 81 - .../sales/(sales)/evaluation-target-list/page.tsx | 115 - app/[lng]/sales/(sales)/evaluation/page.tsx | 181 - app/[lng]/sales/(sales)/faq/manage/actions.ts | 48 - app/[lng]/sales/(sales)/faq/manage/page.tsx | 38 - app/[lng]/sales/(sales)/faq/page.tsx | 62 - app/[lng]/sales/(sales)/items-tech/layout.tsx | 38 - app/[lng]/sales/(sales)/items-tech/page.tsx | 67 - app/[lng]/sales/(sales)/items/page.tsx | 68 - app/[lng]/sales/(sales)/layout.tsx | 18 - app/[lng]/sales/(sales)/project-gtc/page.tsx | 63 - app/[lng]/sales/(sales)/project-vendors/page.tsx | 74 - app/[lng]/sales/(sales)/projects/page.tsx | 75 - app/[lng]/sales/(sales)/report/page.tsx | 105 - app/[lng]/sales/(sales)/settings/layout.tsx | 68 - app/[lng]/sales/(sales)/settings/page.tsx | 18 - .../sales/(sales)/settings/preferences/page.tsx | 17 - .../sales/(sales)/system/admin-users/page.tsx | 60 - app/[lng]/sales/(sales)/system/layout.tsx | 80 - app/[lng]/sales/(sales)/system/page.tsx | 56 - .../sales/(sales)/system/password-policy/page.tsx | 63 - .../sales/(sales)/system/permissions/page.tsx | 17 - app/[lng]/sales/(sales)/system/roles/page.tsx | 68 - app/[lng]/sales/(sales)/tbe/page.tsx | 113 - .../(sales)/tech-contact-possible-items/page.tsx | 56 - app/[lng]/sales/(sales)/tech-project-avl/page.tsx | 88 - .../(sales)/tech-vendors/[id]/info/layout.tsx | 82 - .../sales/(sales)/tech-vendors/[id]/info/page.tsx | 55 - .../tech-vendors/[id]/info/possible-items/page.tsx | 54 - .../tech-vendors/[id]/info/rfq-history/page.tsx | 56 - app/[lng]/sales/(sales)/tech-vendors/page.tsx | 60 - app/[lng]/sales/(sales)/vendor-candidates/page.tsx | 78 - app/[lng]/sales/page.tsx | 21 - .../[rfqId]/vendors/[vendorId]/comments/route.ts | 145 - app/api/rfq-attachments/download/route.ts | 474 --- app/api/tbe-download/route.ts | 417 --- app/api/vendor-responses/update-comment/route.ts | 62 - app/api/vendor-responses/update/route.ts | 118 - app/api/vendor-responses/upload/route.ts | 105 - app/api/vendor-responses/waive/route.ts | 69 - components/ProjectSelector.tsx | 9 +- components/bidding/ProjectSelectorBid.tsx | 9 +- components/layout/Header.tsx | 57 +- config/menuConfig.ts | 18 - lib/b-rfq/attachment/add-attachment-dialog.tsx | 355 -- lib/b-rfq/attachment/add-revision-dialog.tsx | 336 -- lib/b-rfq/attachment/attachment-columns.tsx | 286 -- lib/b-rfq/attachment/attachment-table.tsx | 190 - lib/b-rfq/attachment/attachment-toolbar-action.tsx | 60 - lib/b-rfq/attachment/confirm-documents-dialog.tsx | 141 - lib/b-rfq/attachment/delete-attachment-dialog.tsx | 182 - lib/b-rfq/attachment/request-revision-dialog.tsx | 205 - lib/b-rfq/attachment/revision-dialog.tsx | 196 - lib/b-rfq/attachment/tbe-request-dialog.tsx | 200 - lib/b-rfq/attachment/vendor-responses-panel.tsx | 386 -- lib/b-rfq/final/final-rfq-detail-columns.tsx | 589 --- lib/b-rfq/final/final-rfq-detail-table.tsx | 297 -- .../final/final-rfq-detail-toolbar-actions.tsx | 201 - lib/b-rfq/final/update-final-rfq-sheet.tsx | 70 - lib/b-rfq/initial/add-initial-rfq-dialog.tsx | 584 --- lib/b-rfq/initial/delete-initial-rfq-dialog.tsx | 149 - lib/b-rfq/initial/initial-rfq-detail-columns.tsx | 446 --- lib/b-rfq/initial/initial-rfq-detail-table.tsx | 267 -- .../initial/initial-rfq-detail-toolbar-actions.tsx | 287 -- lib/b-rfq/initial/short-list-confirm-dialog.tsx | 269 -- lib/b-rfq/initial/update-initial-rfq-sheet.tsx | 496 --- lib/b-rfq/repository.ts | 0 lib/b-rfq/service.ts | 2976 --------------- lib/b-rfq/summary-table/add-new-rfq-dialog.tsx | 523 --- lib/b-rfq/summary-table/summary-rfq-columns.tsx | 499 --- .../summary-table/summary-rfq-filter-sheet.tsx | 617 --- .../summary-rfq-table-toolbar-actions.tsx | 68 - lib/b-rfq/summary-table/summary-rfq-table.tsx | 285 -- lib/b-rfq/validations.ts | 447 --- lib/b-rfq/vendor-response/comment-edit-dialog.tsx | 187 - .../vendor-response/response-detail-columns.tsx | 653 ---- .../vendor-response/response-detail-sheet.tsx | 358 -- .../vendor-response/response-detail-table.tsx | 161 - .../vendor-response/upload-response-dialog.tsx | 325 -- .../vendor-responses-table-columns.tsx | 351 -- .../vendor-response/vendor-responses-table.tsx | 152 - .../vendor-response/waive-response-dialog.tsx | 210 -- lib/cbe/table/cbe-table-columns.tsx | 241 -- lib/cbe/table/cbe-table-toolbar-actions.tsx | 72 - lib/cbe/table/cbe-table.tsx | 192 - lib/cbe/table/comments-sheet.tsx | 345 -- lib/cbe/table/invite-vendors-dialog.tsx | 428 --- lib/legal-review/service.ts | 738 ---- .../status/create-legal-work-dialog.tsx | 506 --- .../status/delete-legal-works-dialog.tsx | 152 - lib/legal-review/status/legal-table copy.tsx | 583 --- lib/legal-review/status/legal-table.tsx | 546 --- .../status/legal-work-detail-dialog.tsx | 409 -- .../status/legal-work-filter-sheet.tsx | 897 ----- lib/legal-review/status/legal-works-columns.tsx | 222 -- .../status/legal-works-toolbar-actions.tsx | 286 -- lib/legal-review/status/request-review-dialog.tsx | 983 ----- .../status/update-legal-work-dialog.tsx | 385 -- lib/legal-review/validations.ts | 40 - lib/procurement-rfqs/repository.ts | 50 - lib/procurement-rfqs/services.ts | 2050 ---------- .../table/detail-table/add-vendor-dialog.tsx | 512 --- .../table/detail-table/delete-vendor-dialog.tsx | 150 - .../table/detail-table/rfq-detail-column.tsx | 393 -- .../table/detail-table/rfq-detail-table.tsx | 521 --- .../table/detail-table/update-vendor-sheet.tsx | 449 --- .../detail-table/vendor-communication-drawer.tsx | 518 --- .../vendor-quotation-comparison-dialog.tsx | 665 ---- lib/procurement-rfqs/table/pr-item-dialog.tsx | 258 -- lib/procurement-rfqs/table/rfq-filter-sheet.tsx | 686 ---- lib/procurement-rfqs/table/rfq-table-column.tsx | 373 -- .../table/rfq-table-toolbar-actions.tsx | 279 -- lib/procurement-rfqs/table/rfq-table.tsx | 412 -- lib/procurement-rfqs/validations.ts | 61 - .../vendor-response/buyer-communication-drawer.tsx | 522 --- .../vendor-response/quotation-editor.tsx | 955 ----- .../vendor-response/quotation-item-editor.tsx | 664 ---- .../table/vendor-quotations-table-columns.tsx | 333 -- .../table/vendor-quotations-table.tsx | 152 - lib/projects/service.ts | 37 +- lib/rfqs/cbe-table/cbe-table-columns.tsx | 245 -- lib/rfqs/cbe-table/cbe-table-toolbar-actions.tsx | 67 - lib/rfqs/cbe-table/cbe-table.tsx | 178 - lib/rfqs/cbe-table/comments-sheet.tsx | 328 -- lib/rfqs/cbe-table/invite-vendors-dialog.tsx | 423 --- lib/rfqs/cbe-table/vendor-contact-dialog.tsx | 71 - lib/rfqs/repository.ts | 232 -- lib/rfqs/service.ts | 3951 -------------------- lib/rfqs/table/ItemsDialog.tsx | 752 ---- lib/rfqs/table/ParentRfqSelector.tsx | 307 -- lib/rfqs/table/add-rfq-dialog.tsx | 468 --- lib/rfqs/table/attachment-rfq-sheet.tsx | 429 --- lib/rfqs/table/delete-rfqs-dialog.tsx | 149 - lib/rfqs/table/feature-flags-provider.tsx | 108 - lib/rfqs/table/feature-flags.tsx | 96 - lib/rfqs/table/rfqs-table-columns.tsx | 315 -- lib/rfqs/table/rfqs-table-floating-bar.tsx | 338 -- lib/rfqs/table/rfqs-table-toolbar-actions.tsx | 55 - lib/rfqs/table/rfqs-table.tsx | 263 -- lib/rfqs/table/update-rfq-sheet.tsx | 406 -- lib/rfqs/tbe-table/comments-sheet.tsx | 325 -- lib/rfqs/tbe-table/feature-flags-provider.tsx | 108 - lib/rfqs/tbe-table/file-dialog.tsx | 141 - lib/rfqs/tbe-table/invite-vendors-dialog.tsx | 220 -- lib/rfqs/tbe-table/tbe-result-dialog.tsx | 208 -- lib/rfqs/tbe-table/tbe-table-columns.tsx | 373 -- lib/rfqs/tbe-table/tbe-table-toolbar-actions.tsx | 67 - lib/rfqs/tbe-table/tbe-table.tsx | 220 -- lib/rfqs/tbe-table/vendor-contact-dialog.tsx | 71 - .../vendor-contact/vendor-contact-table-column.tsx | 70 - .../vendor-contact/vendor-contact-table.tsx | 89 - lib/rfqs/validations.ts | 297 -- lib/rfqs/vendor-table/add-vendor-dialog.tsx | 37 - lib/rfqs/vendor-table/comments-sheet.tsx | 318 -- lib/rfqs/vendor-table/feature-flags-provider.tsx | 108 - lib/rfqs/vendor-table/invite-vendors-dialog.tsx | 177 - .../vendor-list/vendor-list-table-column.tsx | 154 - .../vendor-table/vendor-list/vendor-list-table.tsx | 142 - lib/rfqs/vendor-table/vendors-table-columns.tsx | 276 -- .../vendor-table/vendors-table-floating-bar.tsx | 137 - .../vendor-table/vendors-table-toolbar-actions.tsx | 84 - lib/rfqs/vendor-table/vendors-table.tsx | 208 -- lib/tbe/service.ts | 0 lib/tbe/table/comments-sheet.tsx | 345 -- lib/tbe/table/feature-flags-provider.tsx | 108 - lib/tbe/table/file-dialog.tsx | 141 - lib/tbe/table/invite-vendors-dialog.tsx | 209 -- lib/tbe/table/tbe-result-dialog.tsx | 208 -- lib/tbe/table/tbe-table-columns.tsx | 344 -- lib/tbe/table/tbe-table-toolbar-actions.tsx | 72 - lib/tbe/table/tbe-table.tsx | 243 -- lib/tbe/table/vendor-contact-dialog.tsx | 71 - lib/vendor-rfq-response/service.ts | 464 --- lib/vendor-rfq-response/types.ts | 76 - .../vendor-cbe-table/cbe-table-columns.tsx | 365 -- .../vendor-cbe-table/cbe-table.tsx | 272 -- .../vendor-cbe-table/comments-sheet.tsx | 323 -- .../vendor-cbe-table/respond-cbe-sheet.tsx | 427 --- .../vendor-cbe-table/rfq-detail-dialog.tsx | 89 - .../rfq-items-table/rfq-items-table-column.tsx | 62 - .../rfq-items-table/rfq-items-table.tsx | 86 - .../vendor-rfq-table/ItemsDialog.tsx | 125 - .../vendor-rfq-table/attachment-rfq-sheet.tsx | 106 - .../vendor-rfq-table/comments-sheet.tsx | 320 -- .../vendor-rfq-table/feature-flags-provider.tsx | 108 - .../vendor-rfq-table/rfqs-table-columns.tsx | 435 --- .../rfqs-table-toolbar-actions.tsx | 40 - .../vendor-rfq-table/rfqs-table.tsx | 280 -- .../vendor-tbe-table/comments-sheet.tsx | 346 -- .../vendor-tbe-table/rfq-detail-dialog.tsx | 86 - .../vendor-tbe-table/tbe-table-columns.tsx | 350 -- .../vendor-tbe-table/tbe-table.tsx | 188 - .../vendor-tbe-table/tbeFileHandler.tsx | 355 -- lib/vendors/table/request-project-pq-dialog.tsx | 9 +- middleware.ts | 24 +- 325 files changed, 175 insertions(+), 63289 deletions(-) delete mode 100644 app/[lng]/engineering/(engineering)/docu-list-rule/code-groups/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/docu-list-rule/combo-box-settings/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/docu-list-rule/document-class/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/docu-list-rule/layout.tsx delete mode 100644 app/[lng]/engineering/(engineering)/docu-list-rule/number-type-configs/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/docu-list-rule/number-types/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/docu-list-rule/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/document-list-only/layout.tsx delete mode 100644 app/[lng]/engineering/(engineering)/document-list-only/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/document-list-ship/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/faq/manage/actions.ts delete mode 100644 app/[lng]/engineering/(engineering)/faq/manage/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/faq/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/form-list/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/items/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/layout.tsx delete mode 100644 app/[lng]/engineering/(engineering)/projects/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/report/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/tag-numbering/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/tasks/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/tbe/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/vendor-check-list/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/vendor-data/layout.tsx delete mode 100644 app/[lng]/engineering/(engineering)/vendor-data/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/vendor-data/tag/[id]/page.tsx delete mode 100644 app/[lng]/engineering/(engineering)/vendor-investigation/page.tsx delete mode 100644 app/[lng]/engineering/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/(master-data)/p-items/page.tsx delete mode 100644 app/[lng]/evcp/(evcp)/(procurement)/legal-review/page.tsx delete mode 100644 app/[lng]/evcp/(evcp)/(procurement)/p-items/page.tsx delete mode 100644 app/[lng]/partners/(partners)/cbe/page.tsx delete mode 100644 app/[lng]/partners/(partners)/rfq-answer/[vendorId]/[rfqRecordId]/page.tsx delete mode 100644 app/[lng]/partners/(partners)/rfq-answer/page.tsx delete mode 100644 app/[lng]/partners/(partners)/rfq-ship/[id]/page.tsx delete mode 100644 app/[lng]/partners/(partners)/rfq-ship/page.tsx delete mode 100644 app/[lng]/partners/(partners)/rfq/page.tsx delete mode 100644 app/[lng]/partners/(partners)/tbe/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/b-rfq/[id]/final/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/b-rfq/[id]/initial/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/b-rfq/[id]/layout.tsx delete mode 100644 app/[lng]/procurement/(procurement)/b-rfq/[id]/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/b-rfq/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/basic-contract-template/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/basic-contract/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/bqcbe/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/bqtbe/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/cbe/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/layout.tsx delete mode 100644 app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/tbe/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/budgetary-rfq/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/budgetary/[id]/cbe/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/budgetary/[id]/layout.tsx delete mode 100644 app/[lng]/procurement/(procurement)/budgetary/[id]/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/budgetary/[id]/tbe/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/budgetary/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/dashboard/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/equip-class/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/esg-check-list/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/evaluation-check-list/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/evaluation-input/[id]/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/evaluation-input/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/evaluation-target-list/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/evaluation/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/faq/manage/actions.ts delete mode 100644 app/[lng]/procurement/(procurement)/faq/manage/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/faq/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/incoterms/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/items-tech/layout.tsx delete mode 100644 app/[lng]/procurement/(procurement)/items-tech/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/items/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/layout.tsx delete mode 100644 app/[lng]/procurement/(procurement)/menu-list/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/payment-conditions/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/po-rfq/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/po/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/poa/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/pq-criteria/[pqListId]/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/pq-criteria/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/pq_new/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/project-gtc/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/project-vendors/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/projects/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/report/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/rfq/[id]/cbe/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/rfq/[id]/layout.tsx delete mode 100644 app/[lng]/procurement/(procurement)/rfq/[id]/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/rfq/[id]/tbe/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/rfq/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/settings/layout.tsx delete mode 100644 app/[lng]/procurement/(procurement)/settings/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/settings/preferences/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/system/admin-users/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/system/layout.tsx delete mode 100644 app/[lng]/procurement/(procurement)/system/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/system/password-policy/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/system/permissions/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/system/roles/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/tbe/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/vendor-candidates/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/vendor-check-list/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/vendor-investigation/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/vendor-type/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/vendors/[id]/info/items/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/vendors/[id]/info/layout.tsx delete mode 100644 app/[lng]/procurement/(procurement)/vendors/[id]/info/materials/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/vendors/[id]/info/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/vendors/[id]/info/rfq-history/page.tsx delete mode 100644 app/[lng]/procurement/(procurement)/vendors/page.tsx delete mode 100644 app/[lng]/procurement/page.tsx delete mode 100644 app/[lng]/sales/(sales)/bid-projects/page.tsx delete mode 100644 app/[lng]/sales/(sales)/bqcbe/page.tsx delete mode 100644 app/[lng]/sales/(sales)/bqtbe/page.tsx delete mode 100644 app/[lng]/sales/(sales)/budgetary-rfq/[id]/cbe/page.tsx delete mode 100644 app/[lng]/sales/(sales)/budgetary-rfq/[id]/layout.tsx delete mode 100644 app/[lng]/sales/(sales)/budgetary-rfq/[id]/page.tsx delete mode 100644 app/[lng]/sales/(sales)/budgetary-rfq/[id]/tbe/page.tsx delete mode 100644 app/[lng]/sales/(sales)/budgetary-rfq/page.tsx delete mode 100644 app/[lng]/sales/(sales)/budgetary-tech-sales-hull/page.tsx delete mode 100644 app/[lng]/sales/(sales)/budgetary-tech-sales-ship/page.tsx delete mode 100644 app/[lng]/sales/(sales)/budgetary-tech-sales-top/page.tsx delete mode 100644 app/[lng]/sales/(sales)/budgetary/[id]/cbe/page.tsx delete mode 100644 app/[lng]/sales/(sales)/budgetary/[id]/layout.tsx delete mode 100644 app/[lng]/sales/(sales)/budgetary/[id]/page.tsx delete mode 100644 app/[lng]/sales/(sales)/budgetary/[id]/tbe/page.tsx delete mode 100644 app/[lng]/sales/(sales)/budgetary/page.tsx delete mode 100644 app/[lng]/sales/(sales)/dashboard/page.tsx delete mode 100644 app/[lng]/sales/(sales)/esg-check-list/page.tsx delete mode 100644 app/[lng]/sales/(sales)/evaluation-check-list/page.tsx delete mode 100644 app/[lng]/sales/(sales)/evaluation-target-list/page.tsx delete mode 100644 app/[lng]/sales/(sales)/evaluation/page.tsx delete mode 100644 app/[lng]/sales/(sales)/faq/manage/actions.ts delete mode 100644 app/[lng]/sales/(sales)/faq/manage/page.tsx delete mode 100644 app/[lng]/sales/(sales)/faq/page.tsx delete mode 100644 app/[lng]/sales/(sales)/items-tech/layout.tsx delete mode 100644 app/[lng]/sales/(sales)/items-tech/page.tsx delete mode 100644 app/[lng]/sales/(sales)/items/page.tsx delete mode 100644 app/[lng]/sales/(sales)/layout.tsx delete mode 100644 app/[lng]/sales/(sales)/project-gtc/page.tsx delete mode 100644 app/[lng]/sales/(sales)/project-vendors/page.tsx delete mode 100644 app/[lng]/sales/(sales)/projects/page.tsx delete mode 100644 app/[lng]/sales/(sales)/report/page.tsx delete mode 100644 app/[lng]/sales/(sales)/settings/layout.tsx delete mode 100644 app/[lng]/sales/(sales)/settings/page.tsx delete mode 100644 app/[lng]/sales/(sales)/settings/preferences/page.tsx delete mode 100644 app/[lng]/sales/(sales)/system/admin-users/page.tsx delete mode 100644 app/[lng]/sales/(sales)/system/layout.tsx delete mode 100644 app/[lng]/sales/(sales)/system/page.tsx delete mode 100644 app/[lng]/sales/(sales)/system/password-policy/page.tsx delete mode 100644 app/[lng]/sales/(sales)/system/permissions/page.tsx delete mode 100644 app/[lng]/sales/(sales)/system/roles/page.tsx delete mode 100644 app/[lng]/sales/(sales)/tbe/page.tsx delete mode 100644 app/[lng]/sales/(sales)/tech-contact-possible-items/page.tsx delete mode 100644 app/[lng]/sales/(sales)/tech-project-avl/page.tsx delete mode 100644 app/[lng]/sales/(sales)/tech-vendors/[id]/info/layout.tsx delete mode 100644 app/[lng]/sales/(sales)/tech-vendors/[id]/info/page.tsx delete mode 100644 app/[lng]/sales/(sales)/tech-vendors/[id]/info/possible-items/page.tsx delete mode 100644 app/[lng]/sales/(sales)/tech-vendors/[id]/info/rfq-history/page.tsx delete mode 100644 app/[lng]/sales/(sales)/tech-vendors/page.tsx delete mode 100644 app/[lng]/sales/(sales)/vendor-candidates/page.tsx delete mode 100644 app/[lng]/sales/page.tsx delete mode 100644 app/api/procurement-rfqs/[rfqId]/vendors/[vendorId]/comments/route.ts delete mode 100644 app/api/rfq-attachments/download/route.ts delete mode 100644 app/api/tbe-download/route.ts delete mode 100644 app/api/vendor-responses/update-comment/route.ts delete mode 100644 app/api/vendor-responses/update/route.ts delete mode 100644 app/api/vendor-responses/upload/route.ts delete mode 100644 app/api/vendor-responses/waive/route.ts delete mode 100644 lib/b-rfq/attachment/add-attachment-dialog.tsx delete mode 100644 lib/b-rfq/attachment/add-revision-dialog.tsx delete mode 100644 lib/b-rfq/attachment/attachment-columns.tsx delete mode 100644 lib/b-rfq/attachment/attachment-table.tsx delete mode 100644 lib/b-rfq/attachment/attachment-toolbar-action.tsx delete mode 100644 lib/b-rfq/attachment/confirm-documents-dialog.tsx delete mode 100644 lib/b-rfq/attachment/delete-attachment-dialog.tsx delete mode 100644 lib/b-rfq/attachment/request-revision-dialog.tsx delete mode 100644 lib/b-rfq/attachment/revision-dialog.tsx delete mode 100644 lib/b-rfq/attachment/tbe-request-dialog.tsx delete mode 100644 lib/b-rfq/attachment/vendor-responses-panel.tsx delete mode 100644 lib/b-rfq/final/final-rfq-detail-columns.tsx delete mode 100644 lib/b-rfq/final/final-rfq-detail-table.tsx delete mode 100644 lib/b-rfq/final/final-rfq-detail-toolbar-actions.tsx delete mode 100644 lib/b-rfq/final/update-final-rfq-sheet.tsx delete mode 100644 lib/b-rfq/initial/add-initial-rfq-dialog.tsx delete mode 100644 lib/b-rfq/initial/delete-initial-rfq-dialog.tsx delete mode 100644 lib/b-rfq/initial/initial-rfq-detail-columns.tsx delete mode 100644 lib/b-rfq/initial/initial-rfq-detail-table.tsx delete mode 100644 lib/b-rfq/initial/initial-rfq-detail-toolbar-actions.tsx delete mode 100644 lib/b-rfq/initial/short-list-confirm-dialog.tsx delete mode 100644 lib/b-rfq/initial/update-initial-rfq-sheet.tsx delete mode 100644 lib/b-rfq/repository.ts delete mode 100644 lib/b-rfq/service.ts delete mode 100644 lib/b-rfq/summary-table/add-new-rfq-dialog.tsx delete mode 100644 lib/b-rfq/summary-table/summary-rfq-columns.tsx delete mode 100644 lib/b-rfq/summary-table/summary-rfq-filter-sheet.tsx delete mode 100644 lib/b-rfq/summary-table/summary-rfq-table-toolbar-actions.tsx delete mode 100644 lib/b-rfq/summary-table/summary-rfq-table.tsx delete mode 100644 lib/b-rfq/validations.ts delete mode 100644 lib/b-rfq/vendor-response/comment-edit-dialog.tsx delete mode 100644 lib/b-rfq/vendor-response/response-detail-columns.tsx delete mode 100644 lib/b-rfq/vendor-response/response-detail-sheet.tsx delete mode 100644 lib/b-rfq/vendor-response/response-detail-table.tsx delete mode 100644 lib/b-rfq/vendor-response/upload-response-dialog.tsx delete mode 100644 lib/b-rfq/vendor-response/vendor-responses-table-columns.tsx delete mode 100644 lib/b-rfq/vendor-response/vendor-responses-table.tsx delete mode 100644 lib/b-rfq/vendor-response/waive-response-dialog.tsx delete mode 100644 lib/cbe/table/cbe-table-columns.tsx delete mode 100644 lib/cbe/table/cbe-table-toolbar-actions.tsx delete mode 100644 lib/cbe/table/cbe-table.tsx delete mode 100644 lib/cbe/table/comments-sheet.tsx delete mode 100644 lib/cbe/table/invite-vendors-dialog.tsx delete mode 100644 lib/legal-review/service.ts delete mode 100644 lib/legal-review/status/create-legal-work-dialog.tsx delete mode 100644 lib/legal-review/status/delete-legal-works-dialog.tsx delete mode 100644 lib/legal-review/status/legal-table copy.tsx delete mode 100644 lib/legal-review/status/legal-table.tsx delete mode 100644 lib/legal-review/status/legal-work-detail-dialog.tsx delete mode 100644 lib/legal-review/status/legal-work-filter-sheet.tsx delete mode 100644 lib/legal-review/status/legal-works-columns.tsx delete mode 100644 lib/legal-review/status/legal-works-toolbar-actions.tsx delete mode 100644 lib/legal-review/status/request-review-dialog.tsx delete mode 100644 lib/legal-review/status/update-legal-work-dialog.tsx delete mode 100644 lib/legal-review/validations.ts delete mode 100644 lib/procurement-rfqs/repository.ts delete mode 100644 lib/procurement-rfqs/services.ts delete mode 100644 lib/procurement-rfqs/table/detail-table/add-vendor-dialog.tsx delete mode 100644 lib/procurement-rfqs/table/detail-table/delete-vendor-dialog.tsx delete mode 100644 lib/procurement-rfqs/table/detail-table/rfq-detail-column.tsx delete mode 100644 lib/procurement-rfqs/table/detail-table/rfq-detail-table.tsx delete mode 100644 lib/procurement-rfqs/table/detail-table/update-vendor-sheet.tsx delete mode 100644 lib/procurement-rfqs/table/detail-table/vendor-communication-drawer.tsx delete mode 100644 lib/procurement-rfqs/table/detail-table/vendor-quotation-comparison-dialog.tsx delete mode 100644 lib/procurement-rfqs/table/pr-item-dialog.tsx delete mode 100644 lib/procurement-rfqs/table/rfq-filter-sheet.tsx delete mode 100644 lib/procurement-rfqs/table/rfq-table-column.tsx delete mode 100644 lib/procurement-rfqs/table/rfq-table-toolbar-actions.tsx delete mode 100644 lib/procurement-rfqs/table/rfq-table.tsx delete mode 100644 lib/procurement-rfqs/validations.ts delete mode 100644 lib/procurement-rfqs/vendor-response/buyer-communication-drawer.tsx delete mode 100644 lib/procurement-rfqs/vendor-response/quotation-editor.tsx delete mode 100644 lib/procurement-rfqs/vendor-response/quotation-item-editor.tsx delete mode 100644 lib/procurement-rfqs/vendor-response/table/vendor-quotations-table-columns.tsx delete mode 100644 lib/procurement-rfqs/vendor-response/table/vendor-quotations-table.tsx delete mode 100644 lib/rfqs/cbe-table/cbe-table-columns.tsx delete mode 100644 lib/rfqs/cbe-table/cbe-table-toolbar-actions.tsx delete mode 100644 lib/rfqs/cbe-table/cbe-table.tsx delete mode 100644 lib/rfqs/cbe-table/comments-sheet.tsx delete mode 100644 lib/rfqs/cbe-table/invite-vendors-dialog.tsx delete mode 100644 lib/rfqs/cbe-table/vendor-contact-dialog.tsx delete mode 100644 lib/rfqs/repository.ts delete mode 100644 lib/rfqs/service.ts delete mode 100644 lib/rfqs/table/ItemsDialog.tsx delete mode 100644 lib/rfqs/table/ParentRfqSelector.tsx delete mode 100644 lib/rfqs/table/add-rfq-dialog.tsx delete mode 100644 lib/rfqs/table/attachment-rfq-sheet.tsx delete mode 100644 lib/rfqs/table/delete-rfqs-dialog.tsx delete mode 100644 lib/rfqs/table/feature-flags-provider.tsx delete mode 100644 lib/rfqs/table/feature-flags.tsx delete mode 100644 lib/rfqs/table/rfqs-table-columns.tsx delete mode 100644 lib/rfqs/table/rfqs-table-floating-bar.tsx delete mode 100644 lib/rfqs/table/rfqs-table-toolbar-actions.tsx delete mode 100644 lib/rfqs/table/rfqs-table.tsx delete mode 100644 lib/rfqs/table/update-rfq-sheet.tsx delete mode 100644 lib/rfqs/tbe-table/comments-sheet.tsx delete mode 100644 lib/rfqs/tbe-table/feature-flags-provider.tsx delete mode 100644 lib/rfqs/tbe-table/file-dialog.tsx delete mode 100644 lib/rfqs/tbe-table/invite-vendors-dialog.tsx delete mode 100644 lib/rfqs/tbe-table/tbe-result-dialog.tsx delete mode 100644 lib/rfqs/tbe-table/tbe-table-columns.tsx delete mode 100644 lib/rfqs/tbe-table/tbe-table-toolbar-actions.tsx delete mode 100644 lib/rfqs/tbe-table/tbe-table.tsx delete mode 100644 lib/rfqs/tbe-table/vendor-contact-dialog.tsx delete mode 100644 lib/rfqs/tbe-table/vendor-contact/vendor-contact-table-column.tsx delete mode 100644 lib/rfqs/tbe-table/vendor-contact/vendor-contact-table.tsx delete mode 100644 lib/rfqs/validations.ts delete mode 100644 lib/rfqs/vendor-table/add-vendor-dialog.tsx delete mode 100644 lib/rfqs/vendor-table/comments-sheet.tsx delete mode 100644 lib/rfqs/vendor-table/feature-flags-provider.tsx delete mode 100644 lib/rfqs/vendor-table/invite-vendors-dialog.tsx delete mode 100644 lib/rfqs/vendor-table/vendor-list/vendor-list-table-column.tsx delete mode 100644 lib/rfqs/vendor-table/vendor-list/vendor-list-table.tsx delete mode 100644 lib/rfqs/vendor-table/vendors-table-columns.tsx delete mode 100644 lib/rfqs/vendor-table/vendors-table-floating-bar.tsx delete mode 100644 lib/rfqs/vendor-table/vendors-table-toolbar-actions.tsx delete mode 100644 lib/rfqs/vendor-table/vendors-table.tsx delete mode 100644 lib/tbe/service.ts delete mode 100644 lib/tbe/table/comments-sheet.tsx delete mode 100644 lib/tbe/table/feature-flags-provider.tsx delete mode 100644 lib/tbe/table/file-dialog.tsx delete mode 100644 lib/tbe/table/invite-vendors-dialog.tsx delete mode 100644 lib/tbe/table/tbe-result-dialog.tsx delete mode 100644 lib/tbe/table/tbe-table-columns.tsx delete mode 100644 lib/tbe/table/tbe-table-toolbar-actions.tsx delete mode 100644 lib/tbe/table/tbe-table.tsx delete mode 100644 lib/tbe/table/vendor-contact-dialog.tsx delete mode 100644 lib/vendor-rfq-response/service.ts delete mode 100644 lib/vendor-rfq-response/types.ts delete mode 100644 lib/vendor-rfq-response/vendor-cbe-table/cbe-table-columns.tsx delete mode 100644 lib/vendor-rfq-response/vendor-cbe-table/cbe-table.tsx delete mode 100644 lib/vendor-rfq-response/vendor-cbe-table/comments-sheet.tsx delete mode 100644 lib/vendor-rfq-response/vendor-cbe-table/respond-cbe-sheet.tsx delete mode 100644 lib/vendor-rfq-response/vendor-cbe-table/rfq-detail-dialog.tsx delete mode 100644 lib/vendor-rfq-response/vendor-cbe-table/rfq-items-table/rfq-items-table-column.tsx delete mode 100644 lib/vendor-rfq-response/vendor-cbe-table/rfq-items-table/rfq-items-table.tsx delete mode 100644 lib/vendor-rfq-response/vendor-rfq-table/ItemsDialog.tsx delete mode 100644 lib/vendor-rfq-response/vendor-rfq-table/attachment-rfq-sheet.tsx delete mode 100644 lib/vendor-rfq-response/vendor-rfq-table/comments-sheet.tsx delete mode 100644 lib/vendor-rfq-response/vendor-rfq-table/feature-flags-provider.tsx delete mode 100644 lib/vendor-rfq-response/vendor-rfq-table/rfqs-table-columns.tsx delete mode 100644 lib/vendor-rfq-response/vendor-rfq-table/rfqs-table-toolbar-actions.tsx delete mode 100644 lib/vendor-rfq-response/vendor-rfq-table/rfqs-table.tsx delete mode 100644 lib/vendor-rfq-response/vendor-tbe-table/comments-sheet.tsx delete mode 100644 lib/vendor-rfq-response/vendor-tbe-table/rfq-detail-dialog.tsx delete mode 100644 lib/vendor-rfq-response/vendor-tbe-table/tbe-table-columns.tsx delete mode 100644 lib/vendor-rfq-response/vendor-tbe-table/tbe-table.tsx delete mode 100644 lib/vendor-rfq-response/vendor-tbe-table/tbeFileHandler.tsx diff --git a/app/[lng]/engineering/(engineering)/docu-list-rule/code-groups/page.tsx b/app/[lng]/engineering/(engineering)/docu-list-rule/code-groups/page.tsx deleted file mode 100644 index 5aebf15d..00000000 --- a/app/[lng]/engineering/(engineering)/docu-list-rule/code-groups/page.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as React from "react"; -import { type SearchParams } from "@/types/table"; -import { Shell } from "@/components/shell"; -import { Skeleton } from "@/components/ui/skeleton"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { getCodeGroups } from "@/lib/docu-list-rule/code-groups/service"; -import { CodeGroupsTable } from "@/lib/docu-list-rule/code-groups/table/code-groups-table"; -import { searchParamsCodeGroupsCache } from "@/lib/docu-list-rule/code-groups/validation"; -import { InformationButton } from "@/components/information/information-button"; - -interface IndexPageProps { - searchParams: Promise; -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams; - const search = searchParamsCodeGroupsCache.parse(searchParams); - - const promises = Promise.all([ - getCodeGroups({ - ...search, - }), - ]); - - return ( - -
-
-
-

Code Group 정의

- -
- {/*

- 문서 번호에 사용될 수 있는 다양한 코드 그룹의 정의를 관리하는 페이지입니다. -

*/} -
-
- }> - - } - > - - -
- ); -} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/docu-list-rule/combo-box-settings/page.tsx b/app/[lng]/engineering/(engineering)/docu-list-rule/combo-box-settings/page.tsx deleted file mode 100644 index cf0bf02e..00000000 --- a/app/[lng]/engineering/(engineering)/docu-list-rule/combo-box-settings/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import * as React from "react"; -import { Shell } from "@/components/shell"; -import { Skeleton } from "@/components/ui/skeleton"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { getComboBoxCodeGroups } from "@/lib/docu-list-rule/combo-box-settings/service"; -import { ComboBoxSettingsTable } from "@/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table"; -import { InformationButton } from "@/components/information/information-button"; -import { searchParamsCodeGroupsCache } from "@/lib/docu-list-rule/code-groups/validation"; - -interface IndexPageProps { - searchParams: Promise; -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams; - - const promises = Promise.all([ - getComboBoxCodeGroups( - searchParamsCodeGroupsCache.parse(searchParams) - ), - ]); - - return ( - -
-
-
-

Combo Box 설정

- -
- {/*

- Combo Box 옵션을 관리하는 페이지입니다. - 각 Code Group별로 Combo Box에 표시될 옵션들을 설정할 수 있습니다. -

*/} -
-
- }> - - } - > - - -
- ); -} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/docu-list-rule/document-class/page.tsx b/app/[lng]/engineering/(engineering)/docu-list-rule/document-class/page.tsx deleted file mode 100644 index 5c2c600e..00000000 --- a/app/[lng]/engineering/(engineering)/docu-list-rule/document-class/page.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import * as React from "react"; -import { Shell } from "@/components/shell"; -import { Skeleton } from "@/components/ui/skeleton"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { getDocumentClassCodeGroups } from "@/lib/docu-list-rule/document-class/service"; -import { DocumentClassTable } from "@/lib/docu-list-rule/document-class/table/document-class-table"; -import { InformationButton } from "@/components/information/information-button"; -import { searchParamsDocumentClassCache } from "@/lib/docu-list-rule/document-class/validation"; - -interface IndexPageProps { - searchParams: Promise; -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams; - - const promises = Promise.all([ - getDocumentClassCodeGroups( - searchParamsDocumentClassCache.parse(searchParams) - ), - ]); - - return ( - -
-
-
-

Document Class 관리

- -
- {/*

- Document Class를 관리합니다. -

*/} -
-
- }> - - } - > - - -
- ); -} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/docu-list-rule/layout.tsx b/app/[lng]/engineering/(engineering)/docu-list-rule/layout.tsx deleted file mode 100644 index 25023e4b..00000000 --- a/app/[lng]/engineering/(engineering)/docu-list-rule/layout.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" - -export const metadata: Metadata = { - title: "Document Numbering Rule", -} - - - -export default async function DocumentNumberingLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string } -}) { - const resolvedParams = await params - const lng = resolvedParams.lng - - const sidebarNavItems = [ - { - title: "Document Class 관리", - href: `/${lng}/engineering/docu-list-rule/document-class`, - }, - { - title: "Code Group 정의", - href: `/${lng}/engineering/docu-list-rule/code-groups`, - }, - { - title: "Combo Box 설정", - href: `/${lng}/engineering/docu-list-rule/combo-box-settings`, - }, - { - title: "Number Type 관리", - href: `/${lng}/engineering/docu-list-rule/number-types`, - }, - { - title: "Number Type별 설정", - href: `/${lng}/engineering/docu-list-rule/number-type-configs`, - }, - ] - - return ( - <> -
-
-
-
-

Document Numbering Rule (해양)

-

- 벤더 제출 문서 리스트 작성 시에 사용되는 넘버링 -

-
- - -
- -
{children}
-
-
-
-
- - ) -} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/docu-list-rule/number-type-configs/page.tsx b/app/[lng]/engineering/(engineering)/docu-list-rule/number-type-configs/page.tsx deleted file mode 100644 index 4195ba24..00000000 --- a/app/[lng]/engineering/(engineering)/docu-list-rule/number-type-configs/page.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from "react"; -import { Shell } from "@/components/shell"; -import { Skeleton } from "@/components/ui/skeleton"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { NumberTypeConfigsTable } from "@/lib/docu-list-rule/number-type-configs/table/number-type-configs-table"; -import { getNumberTypes } from "@/lib/docu-list-rule/number-types/service"; -import { InformationButton } from "@/components/information/information-button"; - -interface IndexPageProps { - searchParams: Promise; -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams; - - const promises = Promise.all([ - getNumberTypes({ - page: 1, - perPage: 1000, // 모든 Number Type을 가져오기 위해 큰 값 설정 - search: "", - sort: [{ id: "id", desc: false }], // DB 등록 순서대로 정렬 - filters: [], - joinOperator: "and", - flags: ["advancedTable"], - numberTypeId: "", - description: "", - isActive: "" - }), - ]); - - return ( - -
-
-
-

Number Type별 설정

- -
- {/*

- 각 문서 번호 유형별로 어떤 코드 그룹들을 어떤 순서로 사용할지 설정하는 페이지입니다. -

*/} -
-
- }> - - } - > - - -
- ); -} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/docu-list-rule/number-types/page.tsx b/app/[lng]/engineering/(engineering)/docu-list-rule/number-types/page.tsx deleted file mode 100644 index 6fa010c7..00000000 --- a/app/[lng]/engineering/(engineering)/docu-list-rule/number-types/page.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import * as React from "react"; -import { Shell } from "@/components/shell"; -import { Skeleton } from "@/components/ui/skeleton"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { NumberTypesTable } from "@/lib/docu-list-rule/number-types/table/number-types-table"; -import { getNumberTypes } from "@/lib/docu-list-rule/number-types/service"; -import { InformationButton } from "@/components/information/information-button"; -import { searchParamsNumberTypesCache } from "@/lib/docu-list-rule/number-types/validation"; - -interface IndexPageProps { - searchParams: Promise; -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams; - - const promises = Promise.all([ - getNumberTypes( - searchParamsNumberTypesCache.parse(searchParams) - ), - ]); - - return ( - -
-
-
-

Number Type 관리

- -
- {/*

- 문서 번호 유형을 추가, 수정, 삭제할 수 있는 페이지입니다. -

*/} -
-
- }> - - } - > - - -
- ); -} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/docu-list-rule/page.tsx b/app/[lng]/engineering/(engineering)/docu-list-rule/page.tsx deleted file mode 100644 index b8d3559f..00000000 --- a/app/[lng]/engineering/(engineering)/docu-list-rule/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { redirect } from "next/navigation" - - -export default async function DocumentNumberingPage({ - params, -}: { - params: Promise<{ lng: string }> -}) { - const resolvedParams = await params; - // Code Group 페이지로 리다이렉트 - redirect(`/${resolvedParams.lng}/engineering/docu-list-rule/document-class`) -} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/document-list-only/layout.tsx b/app/[lng]/engineering/(engineering)/document-list-only/layout.tsx deleted file mode 100644 index 17e78c0a..00000000 --- a/app/[lng]/engineering/(engineering)/document-list-only/layout.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Shell } from "@/components/shell" -import VendorDocumentListClientEvcp from "@/components/document-lists/vendor-doc-list-client-evcp" - -// Layout 컴포넌트는 서버 컴포넌트입니다 -export default async function EvcpDocuments({ - children, -}: { - children: React.ReactNode -}) { - return ( - - - {children} - - - ) -} diff --git a/app/[lng]/engineering/(engineering)/document-list-only/page.tsx b/app/[lng]/engineering/(engineering)/document-list-only/page.tsx deleted file mode 100644 index 5b49a6ef..00000000 --- a/app/[lng]/engineering/(engineering)/document-list-only/page.tsx +++ /dev/null @@ -1,98 +0,0 @@ -// evcp/document-list-only/page.tsx - 전체 계약 대상 문서 목록 -import * as React from "react" -import { Suspense } from "react" -import { Skeleton } from "@/components/ui/skeleton" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { DocumentStagesTable } from "@/lib/vendor-document-list/plant/document-stages-table" -import { documentStageSearchParamsCache } from "@/lib/vendor-document-list/plant/document-stage-validations" -import { getDocumentStagesOnly } from "@/lib/vendor-document-list/plant/document-stages-service" - -interface IndexPageProps { - searchParams: Promise -} - -// 문서 테이블 래퍼 컴포넌트 (전체 계약용) -async function DocumentTableWrapper({ - searchParams -}: { - searchParams: SearchParams -}) { - const search = documentStageSearchParamsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // 필터 타입 변환 - const convertedFilters = validFilters.map(filter => ({ - id: (filter.id || filter.rowId) as string, - value: filter.value, - operator: (filter.operator === 'iLike' ? 'ilike' : - filter.operator === 'notILike' ? 'notin' : - filter.operator === 'isEmpty' ? 'eq' : - filter.operator === 'isNotEmpty' ? 'ne' : - filter.operator === 'isBetween' ? 'eq' : - filter.operator === 'isRelativeToToday' ? 'eq' : - filter.operator || 'eq') as 'eq' | 'in' | 'ne' | 'lt' | 'lte' | 'gt' | 'gte' | 'like' | 'ilike' | 'notin' - })) - - // evcp: 전체 계약 대상으로 문서 조회 - const documentsPromise = getDocumentStagesOnly({ - ...search, - filters: convertedFilters, - }, -1) // 세션에서 자동으로 도메인 감지 - - return ( - - ) -} - -function TableLoadingSkeleton() { - return ( -
-
- -
- - -
-
-
-
-
- {Array.from({ length: 5 }).map((_, i) => ( -
- - - - - - -
- ))} -
-
-
-
- ) -} - -// 메인 페이지 컴포넌트 -export default async function DocumentStagesManagementPage({ - searchParams -}: IndexPageProps) { - const resolvedSearchParams = await searchParams - - return ( -
- {/* 문서 테이블 */} - }> - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/document-list-ship/page.tsx b/app/[lng]/engineering/(engineering)/document-list-ship/page.tsx deleted file mode 100644 index e3915419..00000000 --- a/app/[lng]/engineering/(engineering)/document-list-ship/page.tsx +++ /dev/null @@ -1,144 +0,0 @@ -// page.tsx (간단한 Promise 생성과 로그인 처리) -import * as React from "react" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsShipDocuCache } from "@/lib/vendor-document-list/validations" -import { getServerSession } from "next-auth" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import Link from "next/link" -import { Button } from "@/components/ui/button" -import { LogIn } from "lucide-react" -import { getUserVendorDocumentStats, getUserVendorDocumentStatsAll, getUserVendorDocuments, getUserVendorDocumentsAll } from "@/lib/vendor-document-list/enhanced-document-service" -import { UserVendorDocumentDisplay } from "@/components/ship-vendor-document/user-vendor-document-table-container" -import { InformationButton } from "@/components/information/information-button" -import { UserVendorALLDocumentDisplay } from "@/components/ship-vendor-document-all/user-vendor-document-table-container" -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsShipDocuCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // Get session - const session = await getServerSession(authOptions) - - // Check if user is logged in - if (!session || !session.user) { - return ( - -
-
-
-

- 문서 관리 -

- -
- {/*

- 소속 회사의 모든 도서/도면을 확인하고 관리합니다. -

*/} -
-
- -
-
-

로그인이 필요합니다

-

- 문서를 확인하려면 먼저 로그인하세요. -

- -
-
-
- ) - } - - // User is logged in, get user ID - const requesterId = session.user.id ? Number(session.user.id) : null - - if (!requesterId) { - return ( - -
-
-

- Document Management -

-
-
-
-
-

계정 오류

-

- 사용자 정보가 올바르게 설정되지 않았습니다. 관리자에게 문의하세요. -

-
-
-
- ) - } - - // 검색 파라미터 정리 - const searchInput = { - ...search, - filters: validFilters, - } - - // Promise 생성 (모든 데이터를 페이지에서 처리) - const documentsPromise = getUserVendorDocumentsAll(requesterId, searchInput) - const statsPromise = getUserVendorDocumentStatsAll(requesterId) - - // Promise.all로 감싸서 전달 - const allPromises = Promise.all([documentsPromise, statsPromise]) - const statsResult = await documentsPromise - - - return ( - -
-
-
-

- 조선 Document Management -

- -
-

- -

-
-
- - }> - {/* DateRangePicker can go here */} - - - - } - > - - -
- ) -} - diff --git a/app/[lng]/engineering/(engineering)/faq/manage/actions.ts b/app/[lng]/engineering/(engineering)/faq/manage/actions.ts deleted file mode 100644 index bc443a8a..00000000 --- a/app/[lng]/engineering/(engineering)/faq/manage/actions.ts +++ /dev/null @@ -1,48 +0,0 @@ -'use server'; - -import { promises as fs } from 'fs'; -import path from 'path'; -import { FaqCategory } from '@/components/faq/FaqCard'; -import { fallbackLng } from '@/i18n/settings'; - -const FAQ_CONFIG_PATH = path.join(process.cwd(), 'config', 'faqDataConfig.ts'); - -export async function updateFaqData(lng: string, newData: FaqCategory[]) { - try { - const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8'); - const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/); - if (!dataMatch) { - throw new Error('FAQ 데이터 형식이 올바르지 않습니다.'); - } - - const allData = eval(`(${dataMatch[1]})`); - const updatedData = { - ...allData, - [lng]: newData - }; - - const newFileContent = `import { FaqCategory } from "@/components/faq/FaqCard";\n\ninterface LocalizedFaqCategories {\n [lng: string]: FaqCategory[];\n}\n\nexport const faqCategories: LocalizedFaqCategories = ${JSON.stringify(updatedData, null, 4)};`; - await fs.writeFile(FAQ_CONFIG_PATH, newFileContent, 'utf-8'); - - return { success: true }; - } catch (error) { - console.error('FAQ 데이터 업데이트 중 오류 발생:', error); - return { success: false, error: '데이터 업데이트 중 오류가 발생했습니다.' }; - } -} - -export async function getFaqData(lng: string): Promise<{ data: FaqCategory[] }> { - try { - const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8'); - const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/); - if (!dataMatch) { - throw new Error('FAQ 데이터 형식이 올바르지 않습니다.'); - } - - const allData = eval(`(${dataMatch[1]})`); - return { data: allData[lng] || allData[fallbackLng] || [] }; - } catch (error) { - console.error('FAQ 데이터 읽기 중 오류 발생:', error); - return { data: [] }; - } -} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/faq/manage/page.tsx b/app/[lng]/engineering/(engineering)/faq/manage/page.tsx deleted file mode 100644 index 011bbfa4..00000000 --- a/app/[lng]/engineering/(engineering)/faq/manage/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FaqManager } from '@/components/faq/FaqManager'; -import { getFaqData, updateFaqData } from './actions'; -import { revalidatePath } from 'next/cache'; -import { FaqCategory } from '@/components/faq/FaqCard'; - -interface Props { - params: { - lng: string; - } -} - -export default async function FaqManagePage(props: Props) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const { data } = await getFaqData(lng); - - async function handleSave(newData: FaqCategory[]) { - 'use server'; - await updateFaqData(lng, newData); - revalidatePath(`/${lng}/evcp/faq`); - } - - return ( -
-
-
-
-

FAQ Management

-

- Manage FAQ categories and items for {lng.toUpperCase()} language. -

-
- -
-
-
- ); -} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/faq/page.tsx b/app/[lng]/engineering/(engineering)/faq/page.tsx deleted file mode 100644 index 9b62b7e4..00000000 --- a/app/[lng]/engineering/(engineering)/faq/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { faqCategories } from "@/config/faqDataConfig" -import { FaqCard } from "@/components/faq/FaqCard" -import { Button } from "@/components/ui/button" -import { Settings } from "lucide-react" -import Link from "next/link" -import { fallbackLng } from "@/i18n/settings" - -interface Props { - params: { - lng: string; - } -} - -export default async function FaqPage(props: Props) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const localizedFaqCategories = faqCategories[lng] || faqCategories[fallbackLng]; - - return ( -
-
-
-
-
-

Frequently Asked Questions

-

- Find answers to common questions about using the EVCP system. -

-
- - - -
- - - - - {localizedFaqCategories.map((category) => ( - - {category.label} - - ))} - - - {localizedFaqCategories.map((category) => ( - - {category.items.map((item, index) => ( - - ))} - - ))} - -
-
-
- ) -} diff --git a/app/[lng]/engineering/(engineering)/form-list/page.tsx b/app/[lng]/engineering/(engineering)/form-list/page.tsx deleted file mode 100644 index a2c6fbb9..00000000 --- a/app/[lng]/engineering/(engineering)/form-list/page.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/form-list/validation" -import { ItemsTable } from "@/lib/items/table/items-table" -import { getFormLists } from "@/lib/form-list/service" -import { FormListsTable } from "@/lib/form-list/table/formLists-table" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getFormLists({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- 레지스터 목록 from S-EDP -

- {/*

- 협력업체 데이터 입력을 위한 레지스터 목록 리스트입니다.{" "} - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/engineering/(engineering)/items/page.tsx b/app/[lng]/engineering/(engineering)/items/page.tsx deleted file mode 100644 index f8d9a5b1..00000000 --- a/app/[lng]/engineering/(engineering)/items/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -// app/items/page.tsx (업데이트) -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/items/validations" -import { getItems } from "@/lib/items/service" -import { ItemsTable } from "@/lib/items/table/items-table" -import { ViewModeToggle } from "@/components/data-table/view-mode-toggle" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - // pageSize 기반으로 모드 자동 결정 - const isInfiniteMode = search.perPage >= 1_000_000 - - // 페이지네이션 모드일 때만 서버에서 데이터 가져오기 - // 무한 스크롤 모드에서는 클라이언트에서 SWR로 데이터 로드 - const promises = isInfiniteMode - ? undefined - : Promise.all([ - getItems(search), // searchParamsCache의 결과를 그대로 사용 - ]) - - return ( - -
-
-
-

- 패키지 넘버 -

- {/*

- S-EDP로부터 수신된 패키지 정보이며 PR 전 입찰, 견적에 사용되며 벤더 데이터, 문서와 연결됩니다. -

*/} -
-
- -
- - }> - {/* DateRangePicker 등 추가 컴포넌트 */} - - - - } - > - {/* 통합된 ItemsTable 컴포넌트 사용 */} - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/layout.tsx b/app/[lng]/engineering/(engineering)/layout.tsx deleted file mode 100644 index 82b53307..00000000 --- a/app/[lng]/engineering/(engineering)/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ReactNode } from 'react'; -import { Header } from '@/components/layout/Header'; -import { SiteFooter } from '@/components/layout/Footer'; - -export default function EvcpLayout({ children }: { children: ReactNode }) { - return ( -
- {/*
*/} -
-
-
- {children} -
-
- -
- ); -} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/projects/page.tsx b/app/[lng]/engineering/(engineering)/projects/page.tsx deleted file mode 100644 index 199b175b..00000000 --- a/app/[lng]/engineering/(engineering)/projects/page.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { ItemsTable } from "@/lib/items/table/items-table" -import { getProjectLists } from "@/lib/projects/service" -import { ProjectsTable } from "@/lib/projects/table/projects-table" -import { searchParamsProjectsCache } from "@/lib/projects/validation" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsProjectsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getProjectLists({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- 프로젝트 리스트 from S-EDP -

- {/*

- S-EDP로부터 수신하는 프로젝트 리스트입니다. 향후 MDG로 전환됩니다.{" "} - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/engineering/(engineering)/report/page.tsx b/app/[lng]/engineering/(engineering)/report/page.tsx deleted file mode 100644 index 64778ef1..00000000 --- a/app/[lng]/engineering/(engineering)/report/page.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import * as React from "react"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Shell } from "@/components/shell"; -import { ErrorBoundary } from "@/components/error-boundary"; -import { getDashboardData } from "@/lib/dashboard/service"; -import { DashboardClient } from "@/lib/dashboard/dashboard-client"; - -// 데이터 fetch 시 비동기 함수 호출 후 await 하므로 static-pre-render 과정에서 dynamic-server-error 발생. -// 따라서, dynamic 속성을 force-dynamic 으로 설정하여 동적 렌더링 처리 -// getDashboardData 함수에 대한 Promise를 넘기는 식으로 수정하게 되면 force-dynamic 선언을 제거해도 됨. -export const dynamic = 'force-dynamic' - -export default async function IndexPage() { - // domain을 명시적으로 전달 - const domain = "engineering"; - - try { - // 서버에서 직접 데이터 fetch - const dashboardData = await getDashboardData(domain); - - return ( - - - - ); - } catch (error) { - console.error("Dashboard data fetch error:", error); - return ( - -
-
-

데이터를 불러오는데 실패했습니다.

-

{error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다."}

-
-
-
- ); - } -} - -function DashboardSkeleton() { - return ( -
- {/* 헤더 스켈레톤 */} -
-
- - -
- -
- - {/* 요약 카드 스켈레톤 */} -
- {[...Array(4)].map((_, i) => ( -
-
- - -
- - -
- ))} -
- - {/* 차트 스켈레톤 */} -
- {[...Array(2)].map((_, i) => ( -
-
- - -
- -
- ))} -
- - {/* 탭 스켈레톤 */} -
- -
- {[...Array(6)].map((_, i) => ( -
- -
-
- - -
-
- - - -
- -
-
- ))} -
-
-
- ); -} diff --git a/app/[lng]/engineering/(engineering)/tag-numbering/page.tsx b/app/[lng]/engineering/(engineering)/tag-numbering/page.tsx deleted file mode 100644 index 86ad2ec2..00000000 --- a/app/[lng]/engineering/(engineering)/tag-numbering/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/tag-numbering/validation" -import { getTagNumbering } from "@/lib/tag-numbering/service" -import { TagNumberingTable } from "@/lib/tag-numbering/table/tagNumbering-table" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTagNumbering({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- 태그 타입 목록 from S-EDP -

- {/*

- 태그 넘버링을 위한 룰셋을 S-EDP로부터 가져오고 확인할 수 있습니다{" "} - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/engineering/(engineering)/tasks/page.tsx b/app/[lng]/engineering/(engineering)/tasks/page.tsx deleted file mode 100644 index 91b946fb..00000000 --- a/app/[lng]/engineering/(engineering)/tasks/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { DateRangePicker } from "@/components/date-range-picker" -import { Shell } from "@/components/shell" - -import { FeatureFlagsProvider } from "@/lib/tasks/table/feature-flags-provider" -import { TasksTable } from "@/lib/tasks/table/tasks-table" -import { - getTaskPriorityCounts, - getTasks, - getTaskStatusCounts, -} from "@/lib/tasks/service" -import { searchParamsCache } from "@/lib/tasks/validations" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTasks({ - ...search, - filters: validFilters, - }), - getTaskStatusCounts(), - getTaskPriorityCounts(), - ]) - - return ( - - }> - - - - } - > - - - - ) -} diff --git a/app/[lng]/engineering/(engineering)/tbe/page.tsx b/app/[lng]/engineering/(engineering)/tbe/page.tsx deleted file mode 100644 index 211cf376..00000000 --- a/app/[lng]/engineering/(engineering)/tbe/page.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getAllTBE } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { AllTbeTable } from "@/lib/tbe/table/tbe-table" -import { RfqType } from "@/lib/rfqs/validations" -import * as React from "react" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" - -interface IndexPageProps { - params: { - lng: string - } - searchParams: Promise -} - -// 타입별 페이지 설명 구성 (Budgetary 제외) -const typeConfig: Record = { - "purchase": { - title: "Purchase RFQ Technical Bid Evaluation", - description: "실제 구매 발주 전 가격 요청을 위한 TBE입니다.", - rfqType: RfqType.PURCHASE - }, - "purchase-budgetary": { - title: "Purchase Budgetary RFQ Technical Bid Evaluation", - description: "프로젝트 수주 후, 공식 입찰 전 예산 책정을 위한 TBE입니다.", - rfqType: RfqType.PURCHASE_BUDGETARY - } -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - - // URL 쿼리 파라미터에서 타입 추출 - const searchParams = await props.searchParams - // 기본값으로 'purchase' 사용 - const typeParam = searchParams?.type as string || 'purchase' - - // 유효한 타입인지 확인하고 기본값 설정 - const validType = Object.keys(typeConfig).includes(typeParam) ? typeParam : 'purchase' - const rfqType = typeConfig[validType].rfqType - - // SearchParams 파싱 (Zod) - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // 현재 선택된 타입의 데이터 로드 - const promises = Promise.all([ - getAllTBE({ - ...search, - filters: validFilters, - rfqType - }) - ]) - - // 페이지 경로 생성 함수 - 단순화 - const getTabUrl = (type: string) => { - return `/${lng}/evcp/tbe?type=${type}`; - } - - return ( - -
-
-
-

- TBE 관리 -

- {/*

- 초대된 협력업체에게 TBE를 보낼 수 있습니다.
- 체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

*/} -
-
-
- - {/* 타입 선택 탭 (Budgetary 제외) */} - - - - Purchase - - - Purchase Budgetary - - - -
-

- {typeConfig[validType].description} -

-
-
- - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendor-check-list/page.tsx b/app/[lng]/engineering/(engineering)/vendor-check-list/page.tsx deleted file mode 100644 index e6f9ce82..00000000 --- a/app/[lng]/engineering/(engineering)/vendor-check-list/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getGenralEvaluationsSchema } from "@/lib/general-check-list/validation" -import { GeneralEvaluationsTable } from "@/lib/general-check-list/table/general-check-list-table" -import { getGeneralEvaluations } from "@/lib/general-check-list/service" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = getGenralEvaluationsSchema.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getGeneralEvaluations({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- 협력업체 평가자료 문항 관리 -

- {/*

- 협력업체 평가에 사용되는 정기평가 체크리스트를 관리{" "} - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/engineering/(engineering)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx b/app/[lng]/engineering/(engineering)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx deleted file mode 100644 index f69aa525..00000000 --- a/app/[lng]/engineering/(engineering)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import DynamicTable from "@/components/form-data/form-data-table"; -import { findContractItemId, getFormData, getFormId } from "@/lib/forms/services"; - -interface IndexPageProps { - params: { - lng: string; - packageId: string; - formId: string; - projectId: string; - contractId: string; - - - }; - searchParams?: { - mode?: string; - }; -} - -export default async function FormPage({ params, searchParams }: IndexPageProps) { - // 1) 구조 분해 할당 - const resolvedParams = await params; - - // 2) searchParams도 await 필요 - const resolvedSearchParams = await searchParams; - - // 3) 구조 분해 할당 - const { lng, packageId, formId: formCode, projectId,contractId } = resolvedParams; - - // URL 쿼리 파라미터에서 mode 가져오기 (await 해서 사용) - const mode = resolvedSearchParams?.mode === "ENG" ? "ENG" : "IM"; // 기본값은 IM - - // 4) 변환 - let packageIdAsNumber = Number(packageId); - const contractIdAsNumber = Number(contractId); - - // packageId가 0이면 contractId와 formCode로 실제 contractItemId 찾기 - if (packageIdAsNumber === 0 && contractIdAsNumber > 0) { - console.log(`packageId가 0이므로 contractId ${contractIdAsNumber}와 formCode ${formCode}로 contractItemId 조회`); - - const foundContractItemId = await findContractItemId(contractIdAsNumber, formCode); - - if (foundContractItemId) { - console.log(`contractItemId ${foundContractItemId}를 찾았습니다. 이 값을 사용합니다.`); - packageIdAsNumber = foundContractItemId; - } else { - console.warn(`contractItemId를 찾을 수 없습니다. packageId는 계속 0으로 유지됩니다.`); - } - } - - // 5) DB 조회 - const { columns, data, editableFieldsMap } = await getFormData(formCode, packageIdAsNumber); - - - // 6) formId 및 report temp file 조회 - const { formId } = await getFormId(String(packageIdAsNumber), formCode); - - // 7) 예외 처리 - if (!columns) { - return ( -

해당 폼의 메타 정보를 불러올 수 없습니다. ENG 모드의 경우에는 SHI 관리자에게 폼 생성 요청을 하시기 바랍니다.

- ); - } - - // 8) 렌더링 - return ( -
- -
- ); -} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendor-data/layout.tsx b/app/[lng]/engineering/(engineering)/vendor-data/layout.tsx deleted file mode 100644 index 7d00359c..00000000 --- a/app/[lng]/engineering/(engineering)/vendor-data/layout.tsx +++ /dev/null @@ -1,67 +0,0 @@ -// app/vendor-data/layout.tsx -import * as React from "react" -import { cookies } from "next/headers" -import { Shell } from "@/components/shell" -import { getVendorProjectsAndContracts } from "@/lib/vendor-data/services" -import { VendorDataContainer } from "@/components/vendor-data/vendor-data-container" -import { InformationButton } from "@/components/information/information-button" -// Layout 컴포넌트는 서버 컴포넌트입니다 -export default async function VendorDataLayout({ - children, -}: { - children: React.ReactNode -}) { - // evcp: 전체 계약 대상으로 프로젝트 데이터 가져오기 - const projects = await getVendorProjectsAndContracts() - - // 레이아웃 설정 쿠키 가져오기 - // Next.js 15에서는 cookies()가 Promise를 반환하므로 await 사용 - const cookieStore = await cookies() - - // 이제 cookieStore.get() 메서드 사용 가능 - const layout = cookieStore.get("react-resizable-panels:layout:mail") - const collapsed = cookieStore.get("react-resizable-panels:collapsed") - - const defaultLayout = layout ? JSON.parse(layout.value) : undefined - const defaultCollapsed = collapsed ? JSON.parse(collapsed.value) : undefined - - return ( - -
-
-
-
-

- 협력업체 데이터 입력 -

- -
- {/*

- 각종 Data 입력할 수 있습니다 -

*/} -
-
-
- -
-
- {projects.length === 0 ? ( -
- No projects found for this vendor. -
- ) : ( - - {/* 페이지별 콘텐츠가 여기에 들어갑니다 */} - {children} - - )} -
-
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendor-data/page.tsx b/app/[lng]/engineering/(engineering)/vendor-data/page.tsx deleted file mode 100644 index ddc21a2b..00000000 --- a/app/[lng]/engineering/(engineering)/vendor-data/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// evcp/vendor-data/page.tsx - 전체 계약 대상 협력업체 데이터 -import * as React from "react" -import { Separator } from "@/components/ui/separator" - -export default async function IndexPage() { - return ( -
-
-

전체 계약 협력업체 데이터 대시보드

-

- 모든 계약의 협력업체 데이터를 확인하고 관리할 수 있습니다. -

-
- -
-
-

사용 방법

-

- 1. 왼쪽 사이드바에서 계약을 선택하세요.
- 2. 선택한 계약의 패키지 항목을 클릭하세요.
- 3. 패키지의 태그 정보를 확인하고 관리할 수 있습니다.
- 4. 폼 항목을 클릭하여 칼럼 정보를 확인하고 관리할 수 있습니다. -

-
-
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendor-data/tag/[id]/page.tsx b/app/[lng]/engineering/(engineering)/vendor-data/tag/[id]/page.tsx deleted file mode 100644 index 7250732f..00000000 --- a/app/[lng]/engineering/(engineering)/vendor-data/tag/[id]/page.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { TagsTable } from "@/lib/tags/table/tag-table" -import { searchParamsCache } from "@/lib/tags/validations" -import { getTags } from "@/lib/tags/service" - -interface IndexPageProps { - params: { - id: string - } - searchParams: Promise -} - -export default async function TagPage(props: IndexPageProps) { - const resolvedParams = await props.params - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTags({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendor-investigation/page.tsx b/app/[lng]/engineering/(engineering)/vendor-investigation/page.tsx deleted file mode 100644 index af9f3e11..00000000 --- a/app/[lng]/engineering/(engineering)/vendor-investigation/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { VendorsInvestigationTable } from "@/lib/vendor-investigation/table/investigation-table" -import { getVendorsInvestigation } from "@/lib/vendor-investigation/service" -import { searchParamsInvestigationCache } from "@/lib/vendor-investigation/validations" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsInvestigationCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getVendorsInvestigation({ - ...search, - filters: validFilters, - }), - ]) - - return ( - - -
-
-
-

- 협력업체 실사 관리 -

- {/*

- 요청된 Vendor 실사에 대한 스케줄 정보를 관리하고 결과를 입력할 수 있습니다. - -

*/} -
-
-
- - - }> - - - } - > - - -
- ) -} diff --git a/app/[lng]/engineering/page.tsx b/app/[lng]/engineering/page.tsx deleted file mode 100644 index f9662cb7..00000000 --- a/app/[lng]/engineering/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Metadata } from "next" -import { Suspense } from "react" -import { LoginFormSkeleton } from "@/components/login/login-form-skeleton" -import { LoginFormSHI } from "@/components/login/login-form-shi" - -export const metadata: Metadata = { - title: "eVCP Portal", - description: "", -} - -export default function AuthenticationPage() { - - - return ( - <> - }> - - - - ) -} diff --git a/app/[lng]/evcp/(evcp)/(master-data)/p-items/page.tsx b/app/[lng]/evcp/(evcp)/(master-data)/p-items/page.tsx new file mode 100644 index 00000000..2b907a75 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(master-data)/p-items/page.tsx @@ -0,0 +1,62 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getProcurementItems } from "@/lib/procurement-items/service" +import { ProcurementItemsTable } from "@/lib/procurement-items/table/procurement-items-table" +import { searchParamsCache } from "@/lib/procurement-items/validations" +import { InformationButton } from "@/components/information/information-button" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getProcurementItems({ + ...search, + filters: validFilters, + }), + ]) + + return ( + +
+
+
+
+

+ 1회성 품목 관리 +

+ +
+

+ 입찰에서 사용하는 1회성 품목을 등록하고 관리합니다. +

+
+
+
+ + + } + > + + +
+ ) +} diff --git a/app/[lng]/evcp/(evcp)/(procurement)/legal-review/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/legal-review/page.tsx deleted file mode 100644 index 44150492..00000000 --- a/app/[lng]/evcp/(evcp)/(procurement)/legal-review/page.tsx +++ /dev/null @@ -1,87 +0,0 @@ -// app/(routes)/legal-works/page.tsx 수정 - -import * as React from "react"; -import { Metadata } from "next"; -import { type SearchParams } from "@/types/table"; -import { Shell } from "@/components/shell"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { InformationButton } from "@/components/information/information-button"; -import { Badge } from "@/components/ui/badge"; // ✅ Badge 추가 -import { SearchParamsCacheLegalWorks } from "@/lib/legal-review/validations"; -import { getLegalWorks } from "@/lib/legal-review/service"; -import { LegalWorksTable } from "@/lib/legal-review/status/legal-table"; - -export const dynamic = "force-dynamic"; -export const revalidate = 0; - -export const metadata: Metadata = { - title: "법무검토 관리", - description: "법무 검토 요청 및 답변을 관리합니다.", -}; - -interface LegalWorksPageProps { - searchParams: Promise; -} - -export default async function LegalWorksPage({ searchParams }: LegalWorksPageProps) { - const rawParams = await searchParams; - const parsedSearch = SearchParamsCacheLegalWorks.parse(rawParams); - - // ✅ EvaluationTargetsPage와 동일한 패턴으로 currentYear 추가 - const currentYear = new Date().getFullYear(); - - const promises = Promise.all([ - getLegalWorks(parsedSearch) - ]); - - return ( - - {/* Header - EvaluationTargetsPage와 동일한 패턴 */} -
-
-

법무검토 관리

- - {/* ✅ EvaluationTargetsPage와 동일하게 Badge 추가 */} - - {currentYear}년 - -
-
- - {/* Table */} - - } - > - {/* ✅ currentYear prop 추가 - EvaluationTargetsTable과 동일한 패턴 */} - - -
- ); -} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(procurement)/p-items/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/p-items/page.tsx deleted file mode 100644 index e3810b5b..00000000 --- a/app/[lng]/evcp/(evcp)/(procurement)/p-items/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getProcurementItems } from "@/lib/procurement-items/service" -import { ProcurementItemsTable } from "@/lib/procurement-items/table/procurement-items-table" -import { searchParamsCache } from "@/lib/procurement-items/validations" -import { InformationButton } from "@/components/information/information-button" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getProcurementItems({ - ...search, - filters: validFilters, - }), - ]) - - return ( - -
-
-
-
-

- 1회성 품목 관리 -

- -
-

- 입찰에서 사용하는 1회성 품목을 등록하고 관리합니다. -

-
-
-
- - - } - > - - -
- ) -} diff --git a/app/[lng]/partners/(partners)/cbe/page.tsx b/app/[lng]/partners/(partners)/cbe/page.tsx deleted file mode 100644 index 4655cb60..00000000 --- a/app/[lng]/partners/(partners)/cbe/page.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getCBEbyVendorId, } from "@/lib/rfqs/service" -import { searchParamsCBECache } from "@/lib/rfqs/validations" -import { getServerSession } from "next-auth" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import { TbeVendorTable } from "@/lib/vendor-rfq-response/vendor-tbe-table/tbe-table" -import * as React from "react" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { CbeVendorTable } from "@/lib/vendor-rfq-response/vendor-cbe-table/cbe-table" -import { InformationButton } from "@/components/information/information-button" -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function CBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsCBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const session = await getServerSession(authOptions) - const vendorId = session?.user.companyId - // const vendorId = "17" - - const idAsNumber = Number(vendorId) - - const promises = Promise.all([ - getCBEbyVendorId({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - - return ( - -
-
-
-
-

- CBE 관리 -

- -
- {/*

- CBE에 응답하고 커뮤니케이션을 할 수 있습니다.{" "} -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/partners/(partners)/rfq-answer/[vendorId]/[rfqRecordId]/page.tsx b/app/[lng]/partners/(partners)/rfq-answer/[vendorId]/[rfqRecordId]/page.tsx deleted file mode 100644 index 898dc41b..00000000 --- a/app/[lng]/partners/(partners)/rfq-answer/[vendorId]/[rfqRecordId]/page.tsx +++ /dev/null @@ -1,174 +0,0 @@ -// app/vendor/responses/[vendorId]/[rfqRecordId]/[rfqType]/page.tsx -import * as React from "react"; -import Link from "next/link"; -import { Metadata } from "next"; -import { getServerSession } from "next-auth/next"; -import { authOptions } from "@/app/api/auth/[...nextauth]/route"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Progress } from "@/components/ui/progress"; -import { - ArrowLeft, - FileText, - AlertTriangle, - TrendingUp, - CheckCircle2, - RefreshCw, - GitBranch, - Clock, - FileCheck, - Calendar -} from "lucide-react"; -import { Shell } from "@/components/shell"; -import { formatDate } from "@/lib/utils"; -import { getRfqAttachmentResponsesWithRevisions } from "@/lib/b-rfq/service"; -import { FinalRfqResponseTable } from "@/lib/b-rfq/vendor-response/response-detail-table"; - -export const metadata: Metadata = { - title: "RFQ 응답 상세", - description: "RFQ 첨부파일별 응답 관리 - 고급 리비전 추적", -}; - -interface RfqResponseDetailPageProps { - params: Promise<{ - vendorId: string; - rfqRecordId: string; - }>; -} - -export default async function RfqResponseDetailPage(props: RfqResponseDetailPageProps) { - const params = await props.params; - const { vendorId, rfqRecordId, rfqType } = params; - - // 인증 확인 - const session = await getServerSession(authOptions); - - if (!session || !session.user) { - return ( - -
-

로그인이 필요합니다.

-
-
- ); - } - - // 벤더 권한 확인 - if (session.user.domain !== "partners" || String(session.user.companyId) !== vendorId) { - return ( - -
-

접근 권한이 없습니다.

-
-
- ); - } - - // 데이터 조회 (뷰 기반 고급 리비전 정보 포함) - const { data: responses, rfqInfo, vendorInfo, statistics, progressSummary } = - await getRfqAttachmentResponsesWithRevisions(vendorId, rfqRecordId); - - console.log("Enhanced RFQ Data:", { responses, statistics, progressSummary }); - - if (!rfqInfo) { - return ( - -
-

RFQ 정보를 찾을 수 없습니다.

-
-
- ); - } - - const stats = statistics; - - return ( - - {/* 헤더 */} -
-
- -
-

- {rfqInfo.rfqCode} - RFQ 응답 관리 -

-

- 고급 리비전 추적 및 응답 상태 관리 -

-
-
- - {/* 마감일 표시 */} - {progressSummary?.daysToDeadline !== undefined && ( -
-
- - 마감까지 -
-
- {progressSummary.daysToDeadline < 0 - ? `${Math.abs(progressSummary.daysToDeadline)}일 초과` - : `${progressSummary.daysToDeadline}일 남음` - } -
-
- )} -
- - {/* 중요 알림들 */} -
- {stats.versionMismatch > 0 && ( - - - - {stats.versionMismatch}개 항목에서 발주처의 최신 리비전과 응답 리비전이 일치하지 않습니다. - 최신 버전으로 업데이트를 권장합니다. - - - )} - - {progressSummary?.daysToDeadline !== undefined && progressSummary.daysToDeadline <= 3 && progressSummary.daysToDeadline >= 0 && ( - - - - 마감일이 {progressSummary.daysToDeadline}일 남았습니다. - 미응답 항목({stats.pending}개)의 신속한 처리가 필요합니다. - - - )} - - {progressSummary?.attachmentsWithMultipleRevisions > 0 && ( - - - - {progressSummary.attachmentsWithMultipleRevisions}개 첨부파일에 - 다중 리비전이 있습니다. 히스토리를 확인하여 올바른 버전으로 응답해주세요. - - - )} -
- - - - -
- ); -} \ No newline at end of file diff --git a/app/[lng]/partners/(partners)/rfq-answer/page.tsx b/app/[lng]/partners/(partners)/rfq-answer/page.tsx deleted file mode 100644 index 6eae491e..00000000 --- a/app/[lng]/partners/(partners)/rfq-answer/page.tsx +++ /dev/null @@ -1,213 +0,0 @@ -// app/vendor/responses/page.tsx -import * as React from "react"; -import Link from "next/link"; -import { Metadata } from "next"; -import { getServerSession } from "next-auth/next"; -import { authOptions } from "@/app/api/auth/[...nextauth]/route"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { LogIn, FileX, Clock, CheckCircle, AlertTriangle } from "lucide-react"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { Shell } from "@/components/shell"; -import { getValidFilters } from "@/lib/data-table"; -import { type SearchParams } from "@/types/table"; -import { searchParamsVendorResponseCache } from "@/lib/b-rfq/validations"; -import { getVendorResponseProgress, getVendorResponseStatusCounts, getVendorRfqResponses } from "@/lib/b-rfq/service"; -import { VendorResponsesTable } from "@/lib/b-rfq/vendor-response/vendor-responses-table"; -import { InformationButton } from "@/components/information/information-button" -export const metadata: Metadata = { - title: "응답 관리", - description: "RFQ 첨부파일 응답 현황을 관리합니다", -}; - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsVendorResponseCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // 인증 확인 - const session = await getServerSession(authOptions); - - // 로그인 확인 - if (!session || !session.user) { - return ( - -
-
-
-

- 응답 관리 -

- -
- {/*

- RFQ 첨부파일 응답 현황을 확인하고 관리합니다. -

*/} -
-
- -
-
-

로그인이 필요합니다

-

- 응답 현황을 확인하려면 먼저 로그인하세요. -

- -
-
-
- ); - } - - // 벤더 ID 확인 - const vendorId = session.user.companyId ? String(session.user.companyId) : "0"; - - // 벤더 권한 확인 - if (session.user.domain !== "partners") { - return ( - -
-
-

- 접근 권한 없음 -

-
-
-
-
-

벤더 계정이 필요합니다

-

- 벤더 계정으로 로그인해주세요. -

-
-
-
- ); - } - - // 데이터 가져오기 - const responsesPromise = getVendorRfqResponses({ - ...search, - filters: validFilters - }, vendorId); - - // 상태별 개수 및 진행률 가져오기 - const [statusCounts, progress] = await Promise.all([ - getVendorResponseStatusCounts(vendorId), - getVendorResponseProgress(vendorId) - ]); - - // 프로미스 배열 - const promises = Promise.all([responsesPromise]); - - return ( - -
-
-

RFQ 응답 관리

-

- RFQ 첨부파일 응답 현황을 확인하고 관리합니다. -

-
-
- - {/* 상태별 통계 카드 */} -
- - - 전체 요청 - - - -
{progress.totalRequests}건
-

- 총 응답 요청 수 -

-
-
- - - - 미응답 - - - -
- {statusCounts.NOT_RESPONDED || 0}건 -
-

- 응답 대기 중 -

-
-
- - - - 응답완료 - - - -
- {statusCounts.RESPONDED || 0}건 -
-

- 응답률: {progress.responseRate}% -

-
-
- - - - 수정요청 - - - -
- {statusCounts.REVISION_REQUESTED || 0}건 -
-

- 재검토 필요 -

-
-
- - - - 포기 - - - -
- {statusCounts.WAIVED || 0}건 -
-

- 완료율: {progress.completionRate}% -

-
-
-
- - - } - > - - -
- ); -} \ No newline at end of file diff --git a/app/[lng]/partners/(partners)/rfq-ship/[id]/page.tsx b/app/[lng]/partners/(partners)/rfq-ship/[id]/page.tsx deleted file mode 100644 index 5b52e4a4..00000000 --- a/app/[lng]/partners/(partners)/rfq-ship/[id]/page.tsx +++ /dev/null @@ -1,81 +0,0 @@ -// app/vendor/quotations/[id]/page.tsx - 견적 응답 페이지 -import { Metadata } from "next" -import { notFound } from "next/navigation" -import db from "@/db/db"; -import { eq } from "drizzle-orm" -import { procurementVendorQuotations } from "@/db/schema" -import { getServerSession } from "next-auth/next" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import VendorQuotationEditor from "@/lib/procurement-rfqs/vendor-response/quotation-editor"; - - -interface PageProps { - params: Promise<{ - id: string - }> -} - -export async function generateMetadata(props: PageProps): Promise { - return { - title: "견적서 응답", - description: "RFQ에 대한 견적서 작성 및 제출", - } -} - -export default async function VendorQuotationPage(props: PageProps) { - const params = await props.params - const quotationId = parseInt(params.id) - - if (isNaN(quotationId)) { - notFound() - } - - // 인증 확인 - const session = await getServerSession(authOptions); - - if (!session?.user) { - return ( -
-
-

로그인이 필요합니다

-

견적서 응답을 위해 로그인해주세요.

-
-
- ) - } - - // 견적서 정보 가져오기 - const quotation = await db.query.procurementVendorQuotations.findFirst({ - where: eq(procurementVendorQuotations.id, quotationId), - with: { - rfq: true, // 관계 설정 필요 - vendor: true, // 관계 설정 필요 - items: true, // 관계 설정 필요 - } - }) - - if (!quotation) { - notFound() - } - - // 벤더 권한 확인 (필요한 경우) - const isAuthorized = session.user.domain === "partners" && - session.user.companyId === quotation.vendorId - - if (!isAuthorized) { - return ( -
-
-

접근 권한이 없습니다

-

이 견적서에 대한 권한이 없습니다.

-
-
- ) - } - - return ( -
- -
- ) -} \ No newline at end of file diff --git a/app/[lng]/partners/(partners)/rfq-ship/page.tsx b/app/[lng]/partners/(partners)/rfq-ship/page.tsx deleted file mode 100644 index 332cca2d..00000000 --- a/app/[lng]/partners/(partners)/rfq-ship/page.tsx +++ /dev/null @@ -1,174 +0,0 @@ -// app/vendor/quotations/page.tsx -import * as React from "react"; -import Link from "next/link"; -import { Metadata } from "next"; -import { getServerSession } from "next-auth/next"; -import { authOptions } from "@/app/api/auth/[...nextauth]/route"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { LogIn } from "lucide-react"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { Shell } from "@/components/shell"; -import { getValidFilters } from "@/lib/data-table"; -import { type SearchParams } from "@/types/table"; -import { searchParamsVendorRfqCache } from "@/lib/procurement-rfqs/validations"; -import { getQuotationStatusCounts, getVendorQuotations } from "@/lib/procurement-rfqs/services"; -import { VendorQuotationsTable } from "@/lib/procurement-rfqs/vendor-response/table/vendor-quotations-table"; -import { InformationButton } from "@/components/information/information-button" -export const metadata: Metadata = { - title: "견적 목록", - description: "진행 중인 견적서 목록", -}; - -interface IndexPageProps { - searchParams: Promise -} - - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsVendorRfqCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - // 인증 확인 - const session = await getServerSession(authOptions); - - // 로그인 확인 - if (!session || !session.user) { - return ( - -
-
-
-

- 견적 목록 -

- -
- {/*

- 진행 중인 견적서 목록을 확인하고 관리합니다. -

*/} -
-
- -
-
-

로그인이 필요합니다

-

- 견적서를 확인하려면 먼저 로그인하세요. -

- -
-
-
- ); - } - - // 벤더 ID 확인 - const vendorId = session.user.companyId ? String(session.user.companyId) : "0"; - - // 벤더 권한 확인 - if (session.user.domain !== "partners") { - return ( - -
-
-

- 접근 권한 없음 -

-
-
-
-
-

벤더 계정이 필요합니다

-

- 벤더 계정으로 로그인해주세요. -

-
-
-
- ); - } - - // 데이터 가져오기 - const quotationsPromise = getVendorQuotations({ - ...search, - filters: validFilters - }, vendorId); - - // 상태별 개수 가져오기 - const statusCountsPromise = getQuotationStatusCounts(vendorId); - - // 모든 프로미스 병렬 실행 - const promises = Promise.all([quotationsPromise]); - const statusCounts = await statusCountsPromise; - - return ( - -
-
-

견적 목록

-

- 진행 중인 견적서 목록을 확인하고 관리합니다. -

-
-
- -
- - - 전체 견적 - - -
- {Object.values(statusCounts).reduce((sum, count) => sum + count, 0)}건 -
-
-
- - - 작성 중 - - -
{statusCounts.Draft || 0}건
-
-
- - - 제출됨 - - -
- {(statusCounts.Submitted || 0) + (statusCounts.Revised || 0)}건 -
-
-
- - - 승인됨 - - -
{statusCounts.Accepted || 0}건
-
-
-
- - - } - > - - -
- ); -} \ No newline at end of file diff --git a/app/[lng]/partners/(partners)/rfq/page.tsx b/app/[lng]/partners/(partners)/rfq/page.tsx deleted file mode 100644 index 5cdb1dde..00000000 --- a/app/[lng]/partners/(partners)/rfq/page.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsRfqsForVendorsCache } from "@/lib/rfqs/validations" -import { RfqsVendorTable } from "@/lib/vendor-rfq-response/vendor-rfq-table/rfqs-table" -import { getServerSession } from "next-auth" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import Link from "next/link" -import { Button } from "@/components/ui/button" -import { LogIn } from "lucide-react" -import { getRfqResponsesForVendor } from "@/lib/vendor-rfq-response/service" -import { InformationButton } from "@/components/information/information-button" -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsRfqsForVendorsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // Get session - const session = await getServerSession(authOptions) - - // Check if user is logged in - if (!session || !session.user) { - // Return login required UI instead of redirecting - return ( - -
-
-

- RFQ -

- {/*

- RFQ를 응답하고 커뮤니케이션을 할 수 있습니다. -

*/} -
-
- -
-
-

로그인이 필요합니다

-

- RFQ를 확인하려면 먼저 로그인하세요. -

- -
-
-
- ) - } - - // User is logged in, proceed with vendor ID - const vendorId = session.user.companyId - - // Validate vendorId (should be a number) - const idAsNumber = Number(vendorId) - - if (isNaN(idAsNumber)) { - // Handle invalid vendor ID (this shouldn't happen if authentication is working properly) - return ( - -
-
-

- RFQ -

-
-
-
-
-

계정 오류

-

- 업체 정보가 올바르게 설정되지 않았습니다. 관리자에게 문의하세요. -

-
-
-
- ) - } - - // If we got here, we have a valid vendor ID - const promises = Promise.all([ - getRfqResponsesForVendor({ - ...search, - filters: validFilters, - }, idAsNumber) - ]) - - return ( - -
-
-
-
-

- RFQ -

- -
- {/*

- RFQ를 응답하고 커뮤니케이션을 할 수 있습니다. -

*/} -
-
-
- - }> - {/* DateRangePicker can go here */} - - - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/partners/(partners)/tbe/page.tsx b/app/[lng]/partners/(partners)/tbe/page.tsx deleted file mode 100644 index 38c24624..00000000 --- a/app/[lng]/partners/(partners)/tbe/page.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getTBEforVendor } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { getServerSession } from "next-auth" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import { TbeVendorTable } from "@/lib/vendor-rfq-response/vendor-tbe-table/tbe-table" -import * as React from "react" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { InformationButton } from "@/components/information/information-button" -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const session = await getServerSession(authOptions) - const vendorId = session?.user.companyId - // const vendorId = "17" - - const idAsNumber = Number(vendorId) - - const promises = Promise.all([ - getTBEforVendor({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - - return ( - -
-
-
-
-

- TBE 관리 -

- -
- {/*

- TBE에 응답하고 커뮤니케이션을 할 수 있습니다.{" "} -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/procurement/(procurement)/b-rfq/[id]/final/page.tsx b/app/[lng]/procurement/(procurement)/b-rfq/[id]/final/page.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/app/[lng]/procurement/(procurement)/b-rfq/[id]/initial/page.tsx b/app/[lng]/procurement/(procurement)/b-rfq/[id]/initial/page.tsx deleted file mode 100644 index 1af65fbc..00000000 --- a/app/[lng]/procurement/(procurement)/b-rfq/[id]/initial/page.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { InitialRfqDetailTable } from "@/lib/b-rfq/initial/initial-rfq-detail-table" -import { getInitialRfqDetail } from "@/lib/b-rfq/service" -import { searchParamsInitialRfqDetailCache } from "@/lib/b-rfq/validations" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsInitialRfqDetailCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = getInitialRfqDetail({ - ...search, - filters: validFilters, - }, idAsNumber) - - // 4) 렌더링 - return ( -
-
-

- Initial RFQ List -

-

- 설계로부터 받은 RFQ 문서와 구매 RFQ 문서 및 사전 계약자료를 Vendor에 발송하기 위한 RFQ 생성 및 관리하는 화면입니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/b-rfq/[id]/layout.tsx b/app/[lng]/procurement/(procurement)/b-rfq/[id]/layout.tsx deleted file mode 100644 index d6836437..00000000 --- a/app/[lng]/procurement/(procurement)/b-rfq/[id]/layout.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { Metadata } from "next" -import Link from "next/link" -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { formatDate } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { ArrowLeft } from "lucide-react" -import { RfqDashboardView } from "@/db/schema" -import { findBRfqById } from "@/lib/b-rfq/service" - -export const metadata: Metadata = { - title: "견적 RFQ 상세", -} - -export default async function RfqLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string, id: string } -}) { - - // 1) URL 파라미터에서 id 추출, Number로 변환 - const resolvedParams = await params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - // 2) DB에서 해당 협력업체 정보 조회 - const rfq: RfqDashboardView | null = await findBRfqById(idAsNumber) - - // 3) 사이드바 메뉴 - const sidebarNavItems = [ - { - title: "견적/입찰 문서관리", - href: `/${lng}/evcp/b-rfq/${id}`, - }, - { - title: "Initial RFQ 발송", - href: `/${lng}/evcp/b-rfq/${id}/initial`, - }, - { - title: "Final RFQ 발송", - href: `/${lng}/evcp/b-rfq/${id}/final`, - }, - - ] - - return ( - <> -
-
-
-
- - - -
-
- {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} -

- {rfq - ? `${rfq.rfqCode ?? ""} | ${rfq.packageNo ?? ""} | ${rfq.packageName ?? ""}` - : "Loading RFQ..."} -

- -

- PR발행 전 RFQ를 생성하여 관리하는 화면입니다. -

-

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate, "KR")}}

-
- -
- -
{children}
-
-
-
-
- - ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/b-rfq/[id]/page.tsx b/app/[lng]/procurement/(procurement)/b-rfq/[id]/page.tsx deleted file mode 100644 index 26dc45fb..00000000 --- a/app/[lng]/procurement/(procurement)/b-rfq/[id]/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsRfqAttachmentsCache } from "@/lib/b-rfq/validations" -import { getRfqAttachments } from "@/lib/b-rfq/service" -import { RfqAttachmentsTable } from "@/lib/b-rfq/attachment/attachment-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsRfqAttachmentsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = getRfqAttachments({ - ...search, - filters: validFilters, - }, idAsNumber) - - // 4) 렌더링 - return ( -
-
-

- 견적 RFQ 문서관리 -

-

- 설계로부터 받은 RFQ 문서와 구매 RFQ 문서를 관리하고 Vendor 회신을 점검/관리하는 화면입니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/b-rfq/page.tsx b/app/[lng]/procurement/(procurement)/b-rfq/page.tsx deleted file mode 100644 index a66d7b58..00000000 --- a/app/[lng]/procurement/(procurement)/b-rfq/page.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import * as React from "react" -import { Metadata } from "next" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { searchParamsRFQDashboardCache } from "@/lib/b-rfq/validations" -import { getRFQDashboard } from "@/lib/b-rfq/service" -import { RFQDashboardTable } from "@/lib/b-rfq/summary-table/summary-rfq-table" - -export const metadata: Metadata = { - title: "견적 RFQ", - description: "", -} - -interface PQReviewPageProps { - searchParams: Promise -} - -export default async function PQReviewPage(props: PQReviewPageProps) { - const searchParams = await props.searchParams - const search = searchParamsRFQDashboardCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // 기본 필터 처리 (통일된 이름 사용) - let basicFilters = [] - if (search.basicFilters && search.basicFilters.length > 0) { - basicFilters = search.basicFilters - console.log("Using search.basicFilters:", basicFilters); - } else { - console.log("No basic filters found"); - } - - // 모든 필터를 합쳐서 처리 - const allFilters = [...validFilters, ...basicFilters] - - // 조인 연산자도 통일된 이름 사용 - const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; - - // Promise.all로 감싸서 전달 - const promises = Promise.all([ - getRFQDashboard({ - ...search, - filters: allFilters, - joinOperator, - }) - ]) - - console.log(search, "견적") - - return ( - -
-
-
-

- 견적 RFQ -

-
-
-
- - {/* Items처럼 직접 테이블 렌더링 */} - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/basic-contract-template/page.tsx b/app/[lng]/procurement/(procurement)/basic-contract-template/page.tsx deleted file mode 100644 index 26108323..00000000 --- a/app/[lng]/procurement/(procurement)/basic-contract-template/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getBasicContractTemplates } from "@/lib/basic-contract/service" -import { searchParamsTemplatesCache } from "@/lib/basic-contract/validations" -import { BasicContractTemplateTable } from "@/lib/basic-contract/template/basic-contract-template" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsTemplatesCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getBasicContractTemplates({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- 기본 계약문서 관리 -

- {/*

- 기본계약서를 비롯하여 초기 서명이 필요한 문서를 등록하고 편집할 수 있습니다. 활성화된 템플릿이 서명 요청의 리스트에 나타나게 됩니다..{" "} - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/procurement/(procurement)/basic-contract/page.tsx b/app/[lng]/procurement/(procurement)/basic-contract/page.tsx deleted file mode 100644 index 19211d4e..00000000 --- a/app/[lng]/procurement/(procurement)/basic-contract/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getBasicContracts } from "@/lib/basic-contract/service" -import { searchParamsCache } from "@/lib/basic-contract/validations" -import { BasicContractsTable } from "@/lib/basic-contract/status/basic-contract-table" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getBasicContracts({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- 기본계약서 서명 현황 -

- {/*

- 기본계약서를 비롯하여 초기 서명이 필요한 문서의 서명 현황을 확인할 수 있고 서명된 문서들을 다운로드할 수 있습니다. {" "} - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/procurement/(procurement)/bqcbe/page.tsx b/app/[lng]/procurement/(procurement)/bqcbe/page.tsx deleted file mode 100644 index 831bb5a8..00000000 --- a/app/[lng]/procurement/(procurement)/bqcbe/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getAllCBE } from "@/lib/rfqs/service" -import { searchParamsCBECache } from "@/lib/rfqs/validations" - -import { AllCbeTable } from "@/lib/cbe/table/cbe-table" - -import { RfqType } from "@/lib/rfqs/validations" -import * as React from "react" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - } - searchParams: Promise - rfqType: RfqType -} - -export default async function RfqCBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - - const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsCBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getAllCBE({ - ...search, - filters: validFilters, - rfqType - } - ) - ]) - - // 4) 렌더링 - return ( - -
-
-
-

- CBE 관리 -

- {/*

- 초대된 협력업체에게 CBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

*/} -
-
-
- - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/bqtbe/page.tsx b/app/[lng]/procurement/(procurement)/bqtbe/page.tsx deleted file mode 100644 index 3e56cfaa..00000000 --- a/app/[lng]/procurement/(procurement)/bqtbe/page.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getAllTBE } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { AllTbeTable } from "@/lib/tbe/table/tbe-table" -import { RfqType } from "@/lib/rfqs/validations" -import * as React from "react" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - } - searchParams: Promise - rfqType: RfqType -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - - const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getAllTBE({ - ...search, - filters: validFilters, - rfqType - } - ) - ]) - - // 4) 렌더링 - return ( - -
-
-
-

- TBE 관리 -

- {/*

- 초대된 협력업체에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

*/} -
-
-
- - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/cbe/page.tsx b/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/cbe/page.tsx deleted file mode 100644 index 956facd3..00000000 --- a/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/cbe/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getCBE, getTBE } from "@/lib/rfqs/service" -import { searchParamsCBECache, } from "@/lib/rfqs/validations" -import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" -import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsCBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getCBE({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Commercial Bid Evaluation -

-

- 초대된 협력업체에게 CBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/layout.tsx b/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/layout.tsx deleted file mode 100644 index 2b80e64f..00000000 --- a/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/layout.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { Metadata } from "next" -import Link from "next/link" -import { ArrowLeft } from "lucide-react" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { RfqViewWithItems } from "@/db/schema/rfq" -import { findRfqById } from "@/lib/rfqs/service" -import { formatDate } from "@/lib/utils" -import { Button } from "@/components/ui/button" - -export const metadata: Metadata = { - title: "Vendor Detail", -} - -export default async function RfqLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string, id: string } -}) { - - // 1) URL 파라미터에서 id 추출, Number로 변환 - const resolvedParams = await params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - // 2) DB에서 해당 협력업체 정보 조회 - const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) - - // 3) 사이드바 메뉴 - const sidebarNavItems = [ - { - title: "Matched Vendors", - href: `/${lng}/evcp/budgetary/${id}`, - }, - { - title: "TBE", - href: `/${lng}/evcp/budgetary/${id}/tbe`, - }, - { - title: "CBE", - href: `/${lng}/evcp/budgetary/${id}/cbe`, - }, - - ] - - return ( - <> -
-
-
-
- - - -
-
- {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} -

- {rfq - ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` - : "Loading RFQ..."} -

- -

- {rfq - ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` - : ""} -

-

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate, "KR")}}

-
- -
- -
{children}
-
-
-
-
- - ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/page.tsx b/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/page.tsx deleted file mode 100644 index dd9df563..00000000 --- a/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/page.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getMatchedVendors } from "@/lib/rfqs/service" -import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" -import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" -import { RfqType } from "@/lib/rfqs/validations" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise - rfqType: RfqType -} - -export default async function RfqPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - const searchParams = await props.searchParams - const search = searchParamsMatchedVCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getMatchedVendors({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Vendors -

-

- 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/tbe/page.tsx b/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/tbe/page.tsx deleted file mode 100644 index ec894e1c..00000000 --- a/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/tbe/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getTBE } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTBE({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Technical Bid Evaluation -

-

- 초대된 협력업체에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary-rfq/page.tsx b/app/[lng]/procurement/(procurement)/budgetary-rfq/page.tsx deleted file mode 100644 index f342bbff..00000000 --- a/app/[lng]/procurement/(procurement)/budgetary-rfq/page.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { searchParamsCache } from "@/lib/rfqs/validations" -import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" -import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" -import { getAllItems } from "@/lib/items/service" -import { RfqType } from "@/lib/rfqs/validations" -import { Ellipsis } from "lucide-react" - -interface RfqPageProps { - searchParams: Promise; - rfqType: RfqType; - title: string; - description: string; -} - -export default async function RfqPage({ - searchParams, - rfqType = RfqType.PURCHASE_BUDGETARY, - title = "Budgetary Quote", - description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." -}: RfqPageProps) { - const search = searchParamsCache.parse(await searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRfqs({ - ...search, - filters: validFilters, - rfqType // 전달받은 rfqType 사용 - }), - getRfqStatusCounts(rfqType), // rfqType 전달 - getAllItems() - ]) - - return ( - -
-
-
-

- {title} -

- {/*

- {description} - 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, - - - 버튼 - 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary/[id]/cbe/page.tsx b/app/[lng]/procurement/(procurement)/budgetary/[id]/cbe/page.tsx deleted file mode 100644 index 956facd3..00000000 --- a/app/[lng]/procurement/(procurement)/budgetary/[id]/cbe/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getCBE, getTBE } from "@/lib/rfqs/service" -import { searchParamsCBECache, } from "@/lib/rfqs/validations" -import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" -import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsCBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getCBE({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Commercial Bid Evaluation -

-

- 초대된 협력업체에게 CBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary/[id]/layout.tsx b/app/[lng]/procurement/(procurement)/budgetary/[id]/layout.tsx deleted file mode 100644 index d58d8363..00000000 --- a/app/[lng]/procurement/(procurement)/budgetary/[id]/layout.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { Metadata } from "next" -import Link from "next/link" -import { ArrowLeft } from "lucide-react" -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { RfqViewWithItems } from "@/db/schema/rfq" -import { findRfqById } from "@/lib/rfqs/service" -import { formatDate } from "@/lib/utils" -import { Button } from "@/components/ui/button" - -export const metadata: Metadata = { - title: "Vendor Detail", -} - -export default async function RfqLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string, id: string } -}) { - - // 1) URL 파라미터에서 id 추출, Number로 변환 - const resolvedParams = await params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - // 2) DB에서 해당 협력업체 정보 조회 - const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) - - // 3) 사이드바 메뉴 - const sidebarNavItems = [ - { - title: "Matched Vendors", - href: `/${lng}/evcp/budgetary/${id}`, - }, - { - title: "TBE", - href: `/${lng}/evcp/budgetary/${id}/tbe`, - }, - { - title: "CBE", - href: `/${lng}/evcp/budgetary/${id}/cbe`, - }, - ] - - return ( - <> -
-
-
- {/* RFQ 목록으로 돌아가는 링크 추가 */} -
- - - -
- -
- {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} -

- {rfq - ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` - : "Loading RFQ..."} -

- -

- {rfq - ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` - : ""} -

-

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate, "KR")}}

-
- -
- -
{children}
-
-
-
-
- - ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary/[id]/page.tsx b/app/[lng]/procurement/(procurement)/budgetary/[id]/page.tsx deleted file mode 100644 index dd9df563..00000000 --- a/app/[lng]/procurement/(procurement)/budgetary/[id]/page.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getMatchedVendors } from "@/lib/rfqs/service" -import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" -import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" -import { RfqType } from "@/lib/rfqs/validations" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise - rfqType: RfqType -} - -export default async function RfqPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - const searchParams = await props.searchParams - const search = searchParamsMatchedVCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getMatchedVendors({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Vendors -

-

- 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary/[id]/tbe/page.tsx b/app/[lng]/procurement/(procurement)/budgetary/[id]/tbe/page.tsx deleted file mode 100644 index ec894e1c..00000000 --- a/app/[lng]/procurement/(procurement)/budgetary/[id]/tbe/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getTBE } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTBE({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Technical Bid Evaluation -

-

- 초대된 협력업체에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary/page.tsx b/app/[lng]/procurement/(procurement)/budgetary/page.tsx deleted file mode 100644 index 15b4cdd4..00000000 --- a/app/[lng]/procurement/(procurement)/budgetary/page.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { searchParamsCache } from "@/lib/rfqs/validations" -import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" -import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" -import { getAllItems } from "@/lib/items/service" -import { RfqType } from "@/lib/rfqs/validations" -import { Ellipsis } from "lucide-react" - -interface RfqPageProps { - searchParams: Promise; - rfqType: RfqType; - title: string; - description: string; -} - -export default async function RfqPage({ - searchParams, - rfqType = RfqType.BUDGETARY, - title = "Budgetary Quote", - description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." -}: RfqPageProps) { - const search = searchParamsCache.parse(await searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRfqs({ - ...search, - filters: validFilters, - rfqType // 전달받은 rfqType 사용 - }), - getRfqStatusCounts(rfqType), // rfqType 전달 - getAllItems() - ]) - - return ( - -
-
-
-

- {title} -

- {/*

- {description} - 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, - - - 버튼 - 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/dashboard/page.tsx b/app/[lng]/procurement/(procurement)/dashboard/page.tsx deleted file mode 100644 index 1d61dc16..00000000 --- a/app/[lng]/procurement/(procurement)/dashboard/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -// app/invalid-access/page.tsx - -export default function InvalidAccessPage() { - return ( -
-

부적절한 접근입니다

-

- 협력업체(Vendor)가 EVCP 화면에 접속하거나
- SHI 계정이 협력업체 화면에 접속하려고 시도하는 경우입니다. -

-

- 접근 권한이 없으므로, 다른 화면으로 이동해 주세요. -

-
- ); - } - \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/equip-class/page.tsx b/app/[lng]/procurement/(procurement)/equip-class/page.tsx deleted file mode 100644 index 34fd32b6..00000000 --- a/app/[lng]/procurement/(procurement)/equip-class/page.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/equip-class/validation" -import { FormListsTable } from "@/lib/form-list/table/formLists-table" -import { getTagClassists } from "@/lib/equip-class/service" -import { EquipClassTable } from "@/lib/equip-class/table/equipClass-table" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTagClassists({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- 객체 클래스 목록 from S-EDP -

- {/*

- 객체 클래스 목록을 확인할 수 있습니다.{" "} - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/procurement/(procurement)/esg-check-list/page.tsx b/app/[lng]/procurement/(procurement)/esg-check-list/page.tsx deleted file mode 100644 index 8bccd3b7..00000000 --- a/app/[lng]/procurement/(procurement)/esg-check-list/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getEsgEvaluations } from "@/lib/esg-check-list/service" -import { getEsgEvaluationsSchema } from "@/lib/esg-check-list/validation" -import { EsgEvaluationsTable } from "@/lib/esg-check-list/table/esg-table" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = getEsgEvaluationsSchema.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getEsgEvaluations({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- ESG 자가진단평가서 항목 관리 -

- {/*

- 협력업체 평가에 사용되는 ESG 자가진단표를 관리{" "} - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/procurement/(procurement)/evaluation-check-list/page.tsx b/app/[lng]/procurement/(procurement)/evaluation-check-list/page.tsx deleted file mode 100644 index 45da961b..00000000 --- a/app/[lng]/procurement/(procurement)/evaluation-check-list/page.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* IMPORT */ -import { DataTableSkeleton } from '@/components/data-table/data-table-skeleton'; -import { getRegEvalCriteria } from '@/lib/evaluation-criteria/service'; -import { getValidFilters } from '@/lib/data-table'; -import RegEvalCriteriaTable from '@/lib/evaluation-criteria/table/reg-eval-criteria-table'; -import { searchParamsCache } from '@/lib/evaluation-criteria/validations'; -import { Shell } from '@/components/shell'; -import { Skeleton } from '@/components/ui/skeleton'; -import { Suspense } from 'react'; -import { type SearchParams } from '@/types/table'; - -// ---------------------------------------------------------------------------------------------------- - -/* TYPES */ -interface EvaluationCriteriaPageProps { - searchParams: Promise -} - -// ---------------------------------------------------------------------------------------------------- - -/* REGULAR EVALUATION CRITERIA PAGE */ -async function EvaluationCriteriaPage(props: EvaluationCriteriaPageProps) { - const searchParams = await props.searchParams; - const search = searchParamsCache.parse(searchParams); - const validFilters = getValidFilters(search.filters); - const promises = Promise.all([ - getRegEvalCriteria({ - ...search, - filters: validFilters, - }), - ]); - - return ( - -
-
-
-

- 협력업체 평가기준표 관리 -

- {/*

- 협력업체 평가에 사용되는 평가기준표를 관리{" "} - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} - -// ---------------------------------------------------------------------------------------------------- - -/* EXPORT */ -export default EvaluationCriteriaPage; \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/evaluation-input/[id]/page.tsx b/app/[lng]/procurement/(procurement)/evaluation-input/[id]/page.tsx deleted file mode 100644 index 3a403620..00000000 --- a/app/[lng]/procurement/(procurement)/evaluation-input/[id]/page.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { EvaluationPage } from "@/lib/evaluation-submit/evaluation-page" -import { Metadata } from "next" - -export const metadata: Metadata = { - title: "평가 작성", - description: "협력업체 평가를 작성합니다", -} - -interface PageProps { - params: { - id: string - } -} - -export default function Page({ params }: PageProps) { - return -} - -export async function generateStaticParams() { - // 동적 경로이므로 빈 배열 반환 - return [] -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/evaluation-input/page.tsx b/app/[lng]/procurement/(procurement)/evaluation-input/page.tsx deleted file mode 100644 index 00f1820f..00000000 --- a/app/[lng]/procurement/(procurement)/evaluation-input/page.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getServerSession } from "next-auth" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import Link from "next/link" -import { Button } from "@/components/ui/button" -import { LogIn } from "lucide-react" -import { getSHIEvaluationSubmissions } from "@/lib/evaluation-submit/service" -import { getSHIEvaluationsSubmitSchema } from "@/lib/evaluation-submit/validation" -import { SHIEvaluationSubmissionsTable } from "@/lib/evaluation-submit/table/submit-table" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = getSHIEvaluationsSubmitSchema.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // Get session - const session = await getServerSession(authOptions) - - // Check if user is logged in - if (!session || !session.user) { - // Return login required UI instead of redirecting - return ( - -
-
-
-

- 정기평가 -

-
- {/*

- 요청된 정기평가를 입력하고 제출할 수 있습니다. -

*/} -
-
- -
-
-

로그인이 필요합니다

-

- 정기평가를 확인하려면 먼저 로그인하세요. -

- -
-
-
- ) - } - - const userId = session.user.id - - // Validate vendorId (should be a number) - const idAsNumber = Number(userId) - - - if (isNaN(idAsNumber)) { - // Handle invalid vendor ID (this shouldn't happen if authentication is working properly) - return ( - -
-
-

- 정기평가 -

-
-
-
-
-

계정 오류

-

- 관리자에게 문의하세요. -

-
-
-
- ) - } - - // If we got here, we have a valid vendor ID - const promises = Promise.all([ - getSHIEvaluationSubmissions({ - ...search, - filters: validFilters, - }, idAsNumber) - ]) - - return ( - -
-
-
-

- 정기평가 -

- {/*

- 요청된 정기평가를 입력하고 제출할 수 있습니다. -

*/} -
-
-
- - }> - {/* DateRangePicker can go here */} - - - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/evaluation-target-list/page.tsx b/app/[lng]/procurement/(procurement)/evaluation-target-list/page.tsx deleted file mode 100644 index a0523eea..00000000 --- a/app/[lng]/procurement/(procurement)/evaluation-target-list/page.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import * as React from "react" -import { Metadata } from "next" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { HelpCircle } from "lucide-react" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" - -import { getDefaultEvaluationYear, searchParamsEvaluationTargetsCache } from "@/lib/evaluation-target-list/validation" -import { getEvaluationTargets } from "@/lib/evaluation-target-list/service" -import { EvaluationTargetsTable } from "@/lib/evaluation-target-list/table/evaluation-target-table" -import { InformationButton } from "@/components/information/information-button" -export const metadata: Metadata = { - title: "협력업체 평가 대상 관리", - description: "협력업체 평가 대상을 확정하고 담당자를 지정합니다.", -} - -interface EvaluationTargetsPageProps { - searchParams: Promise -} - - - -export default async function EvaluationTargetsPage(props: EvaluationTargetsPageProps) { - const searchParams = await props.searchParams - const search = searchParamsEvaluationTargetsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // 기본 필터 처리 (통일된 이름 사용) - let basicFilters = [] - if (search.basicFilters && search.basicFilters.length > 0) { - basicFilters = search.basicFilters - console.log("Using search.basicFilters:", basicFilters); - } else { - console.log("No basic filters found"); - } - - // 모든 필터를 합쳐서 처리 - const allFilters = [...validFilters, ...basicFilters] - - // 조인 연산자도 통일된 이름 사용 - const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; - - // 현재 평가년도 (필터에서 가져오거나 기본값 사용) - const currentEvaluationYear = search.evaluationYear || getDefaultEvaluationYear() - - // Promise.all로 감싸서 전달 - const promises = Promise.all([ - getEvaluationTargets({ - ...search, - filters: allFilters, - joinOperator, - }) - ]) - - return ( - - {/* 간소화된 헤더 */} -
-
-
-
-

- 협력업체 평가 대상 관리 -

- -
- - {currentEvaluationYear}년도 - - -
-
-
- - {/* 메인 테이블 (통계는 테이블 내부로 이동) */} - - } - > - {currentEvaluationYear && - -} - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/evaluation/page.tsx b/app/[lng]/procurement/(procurement)/evaluation/page.tsx deleted file mode 100644 index 2d8cbed7..00000000 --- a/app/[lng]/procurement/(procurement)/evaluation/page.tsx +++ /dev/null @@ -1,181 +0,0 @@ -// ================================================================ -// 4. PERIODIC EVALUATIONS PAGE -// ================================================================ - -import * as React from "react" -import { Metadata } from "next" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { HelpCircle } from "lucide-react" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { PeriodicEvaluationsTable } from "@/lib/evaluation/table/evaluation-table" -import { getPeriodicEvaluations } from "@/lib/evaluation/service" -import { searchParamsEvaluationsCache } from "@/lib/evaluation/validation" - -export const metadata: Metadata = { - title: "협력업체 정기평가", - description: "협력업체 정기평가 진행 현황을 관리합니다.", -} - -interface PeriodicEvaluationsPageProps { - searchParams: Promise -} - -// 프로세스 안내 팝오버 컴포넌트 -function ProcessGuidePopover() { - return ( - - - - - -
-
-

정기평가 프로세스

- {/*

- 확정된 평가 대상 업체들에 대한 정기평가 절차입니다. -

*/} -
-
-
-
- 1 -
-
-

평가 대상 확정

-

평가 대상으로 확정된 업체들의 정기평가가 자동 생성됩니다.

-
-
-
-
- 2 -
-
-

업체 자료 제출

-

각 업체는 평가에 필요한 자료를 제출 마감일까지 제출해야 합니다.

-
-
-
-
- 3 -
-
-

평가자 검토

-

지정된 평가자들이 평가표를 기반으로 점수를 매기고 검토합니다.

-
-
-
-
- 4 -
-
-

최종 확정

-

모든 평가가 완료되면 최종 점수와 등급이 확정됩니다.

-
-
-
-
-
-
- ) -} - -// TODO: 이 함수들은 실제 서비스 파일에서 구현해야 함 -function getDefaultEvaluationYear() { - return new Date().getFullYear() -} - - - -export default async function PeriodicEvaluationsPage(props: PeriodicEvaluationsPageProps) { - const searchParams = await props.searchParams - const search = searchParamsEvaluationsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters || []) - - // 기본 필터 처리 - let basicFilters = [] - if (search.basicFilters && search.basicFilters.length > 0) { - basicFilters = search.basicFilters - } - - // 모든 필터를 합쳐서 처리 - const allFilters = [...validFilters, ...basicFilters] - - // 조인 연산자 - const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; - - // 현재 평가년도 - const currentEvaluationYear = search.evaluationYear || getDefaultEvaluationYear() - - // Promise.all로 감싸서 전달 - const promises = Promise.all([ - getPeriodicEvaluations({ - ...search, - filters: allFilters, - joinOperator, - }) - ]) - - return ( - - {/* 헤더 */} -
-
-
-

- 협력업체 정기평가 -

- - {currentEvaluationYear}년도 - -
-
-
- - {/* 메인 테이블 */} - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/faq/manage/actions.ts b/app/[lng]/procurement/(procurement)/faq/manage/actions.ts deleted file mode 100644 index bc443a8a..00000000 --- a/app/[lng]/procurement/(procurement)/faq/manage/actions.ts +++ /dev/null @@ -1,48 +0,0 @@ -'use server'; - -import { promises as fs } from 'fs'; -import path from 'path'; -import { FaqCategory } from '@/components/faq/FaqCard'; -import { fallbackLng } from '@/i18n/settings'; - -const FAQ_CONFIG_PATH = path.join(process.cwd(), 'config', 'faqDataConfig.ts'); - -export async function updateFaqData(lng: string, newData: FaqCategory[]) { - try { - const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8'); - const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/); - if (!dataMatch) { - throw new Error('FAQ 데이터 형식이 올바르지 않습니다.'); - } - - const allData = eval(`(${dataMatch[1]})`); - const updatedData = { - ...allData, - [lng]: newData - }; - - const newFileContent = `import { FaqCategory } from "@/components/faq/FaqCard";\n\ninterface LocalizedFaqCategories {\n [lng: string]: FaqCategory[];\n}\n\nexport const faqCategories: LocalizedFaqCategories = ${JSON.stringify(updatedData, null, 4)};`; - await fs.writeFile(FAQ_CONFIG_PATH, newFileContent, 'utf-8'); - - return { success: true }; - } catch (error) { - console.error('FAQ 데이터 업데이트 중 오류 발생:', error); - return { success: false, error: '데이터 업데이트 중 오류가 발생했습니다.' }; - } -} - -export async function getFaqData(lng: string): Promise<{ data: FaqCategory[] }> { - try { - const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8'); - const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/); - if (!dataMatch) { - throw new Error('FAQ 데이터 형식이 올바르지 않습니다.'); - } - - const allData = eval(`(${dataMatch[1]})`); - return { data: allData[lng] || allData[fallbackLng] || [] }; - } catch (error) { - console.error('FAQ 데이터 읽기 중 오류 발생:', error); - return { data: [] }; - } -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/faq/manage/page.tsx b/app/[lng]/procurement/(procurement)/faq/manage/page.tsx deleted file mode 100644 index 011bbfa4..00000000 --- a/app/[lng]/procurement/(procurement)/faq/manage/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FaqManager } from '@/components/faq/FaqManager'; -import { getFaqData, updateFaqData } from './actions'; -import { revalidatePath } from 'next/cache'; -import { FaqCategory } from '@/components/faq/FaqCard'; - -interface Props { - params: { - lng: string; - } -} - -export default async function FaqManagePage(props: Props) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const { data } = await getFaqData(lng); - - async function handleSave(newData: FaqCategory[]) { - 'use server'; - await updateFaqData(lng, newData); - revalidatePath(`/${lng}/evcp/faq`); - } - - return ( -
-
-
-
-

FAQ Management

-

- Manage FAQ categories and items for {lng.toUpperCase()} language. -

-
- -
-
-
- ); -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/faq/page.tsx b/app/[lng]/procurement/(procurement)/faq/page.tsx deleted file mode 100644 index 00956591..00000000 --- a/app/[lng]/procurement/(procurement)/faq/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { faqCategories } from "@/config/faqDataConfig" -import { FaqCard } from "@/components/faq/FaqCard" -import { Button } from "@/components/ui/button" -import { Settings } from "lucide-react" -import Link from "next/link" -import { fallbackLng } from "@/i18n/settings" - -interface Props { - params: { - lng: string; - } -} - -export default async function FaqPage(props: Props) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const localizedFaqCategories = faqCategories[lng] || faqCategories[fallbackLng]; - - return ( -
-
-
-
-
-

FAQ

- {/*

- Find answers to common questions about using the EVCP system. -

*/} -
- - - -
- - - - - {localizedFaqCategories.map((category) => ( - - {category.label} - - ))} - - - {localizedFaqCategories.map((category) => ( - - {category.items.map((item, index) => ( - - ))} - - ))} - -
-
-
- ) -} diff --git a/app/[lng]/procurement/(procurement)/incoterms/page.tsx b/app/[lng]/procurement/(procurement)/incoterms/page.tsx deleted file mode 100644 index 804bc5af..00000000 --- a/app/[lng]/procurement/(procurement)/incoterms/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import * as React from "react"; -import { type SearchParams } from "@/types/table"; -import { getValidFilters } from "@/lib/data-table"; -import { Shell } from "@/components/shell"; -import { Skeleton } from "@/components/ui/skeleton"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { SearchParamsCache } from "@/lib/incoterms/validations"; -import { getIncoterms } from "@/lib/incoterms/service"; -import { IncotermsTable } from "@/lib/incoterms/table/incoterms-table"; - -interface IndexPageProps { - searchParams: Promise; -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams; - const search = SearchParamsCache.parse(searchParams); - const validFilters = getValidFilters(search.filters); - - const promises = Promise.all([ - getIncoterms({ - ...search, - filters: validFilters, - }), - ]); - - return ( - -
-
-

인코텀즈 관리

- {/*

- 인코텀즈(Incoterms)를 등록, 수정, 삭제할 수 있습니다. -

*/} -
-
- }> - - } - > - - -
- ); -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/items-tech/layout.tsx b/app/[lng]/procurement/(procurement)/items-tech/layout.tsx deleted file mode 100644 index d375059b..00000000 --- a/app/[lng]/procurement/(procurement)/items-tech/layout.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from "react" -import { ItemTechContainer } from "@/components/items-tech/item-tech-container" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" - -// Layout 컴포넌트는 서버 컴포넌트입니다 -export default function ItemsShipLayout({ - children, -}: { - children: React.ReactNode -}) { - // 아이템 타입 정의 - const itemTypes = [ - { id: "ship", name: "조선 아이템" }, - { id: "top", name: "해양 TOP" }, - { id: "hull", name: "해양 HULL" }, - ] - - return ( - - - } - > - - {children} - - - - ) -} diff --git a/app/[lng]/procurement/(procurement)/items-tech/page.tsx b/app/[lng]/procurement/(procurement)/items-tech/page.tsx deleted file mode 100644 index 55ac9c63..00000000 --- a/app/[lng]/procurement/(procurement)/items-tech/page.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { shipbuildingSearchParamsCache, offshoreTopSearchParamsCache, offshoreHullSearchParamsCache } from "@/lib/items-tech/validations" -import { getShipbuildingItems, getOffshoreTopItems, getOffshoreHullItems } from "@/lib/items-tech/service" -import { OffshoreTopTable } from "@/lib/items-tech/table/top/offshore-top-table" -import { OffshoreHullTable } from "@/lib/items-tech/table/hull/offshore-hull-table" - -// 대소문자 문제 해결 - 실제 파일명에 맞게 import -import { ItemsShipTable } from "@/lib/items-tech/table/ship/Items-ship-table" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage({ searchParams }: IndexPageProps) { - const params = await searchParams - const shipbuildingSearch = shipbuildingSearchParamsCache.parse(params) - const offshoreTopSearch = offshoreTopSearchParamsCache.parse(params) - const offshoreHullSearch = offshoreHullSearchParamsCache.parse(params) - const validShipbuildingFilters = getValidFilters(shipbuildingSearch.filters || []) - const validOffshoreTopFilters = getValidFilters(offshoreTopSearch.filters || []) - const validOffshoreHullFilters = getValidFilters(offshoreHullSearch.filters || []) - - - // URL에서 아이템 타입 가져오기 - const itemType = params.type || "ship" - - return ( -
- {itemType === "ship" && ( - result)} - /> - )} - - {itemType === "top" && ( - result)} - /> - )} - - {itemType === "hull" && ( - result)} - /> - )} -
- ) -} diff --git a/app/[lng]/procurement/(procurement)/items/page.tsx b/app/[lng]/procurement/(procurement)/items/page.tsx deleted file mode 100644 index f8d9a5b1..00000000 --- a/app/[lng]/procurement/(procurement)/items/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -// app/items/page.tsx (업데이트) -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/items/validations" -import { getItems } from "@/lib/items/service" -import { ItemsTable } from "@/lib/items/table/items-table" -import { ViewModeToggle } from "@/components/data-table/view-mode-toggle" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - // pageSize 기반으로 모드 자동 결정 - const isInfiniteMode = search.perPage >= 1_000_000 - - // 페이지네이션 모드일 때만 서버에서 데이터 가져오기 - // 무한 스크롤 모드에서는 클라이언트에서 SWR로 데이터 로드 - const promises = isInfiniteMode - ? undefined - : Promise.all([ - getItems(search), // searchParamsCache의 결과를 그대로 사용 - ]) - - return ( - -
-
-
-

- 패키지 넘버 -

- {/*

- S-EDP로부터 수신된 패키지 정보이며 PR 전 입찰, 견적에 사용되며 벤더 데이터, 문서와 연결됩니다. -

*/} -
-
- -
- - }> - {/* DateRangePicker 등 추가 컴포넌트 */} - - - - } - > - {/* 통합된 ItemsTable 컴포넌트 사용 */} - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/layout.tsx b/app/[lng]/procurement/(procurement)/layout.tsx deleted file mode 100644 index 82b53307..00000000 --- a/app/[lng]/procurement/(procurement)/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ReactNode } from 'react'; -import { Header } from '@/components/layout/Header'; -import { SiteFooter } from '@/components/layout/Footer'; - -export default function EvcpLayout({ children }: { children: ReactNode }) { - return ( -
- {/*
*/} -
-
-
- {children} -
-
- -
- ); -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/menu-list/page.tsx b/app/[lng]/procurement/(procurement)/menu-list/page.tsx deleted file mode 100644 index dee45ab1..00000000 --- a/app/[lng]/procurement/(procurement)/menu-list/page.tsx +++ /dev/null @@ -1,70 +0,0 @@ -// app/evcp/menu-list/page.tsx - -import { Suspense } from "react"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { RefreshCw, Settings } from "lucide-react"; -import { getActiveUsers, getMenuAssignments } from "@/lib/menu-list/servcie"; -import { InitializeButton } from "@/lib/menu-list/table/initialize-button"; -import { MenuListTable } from "@/lib/menu-list/table/menu-list-table"; -import { Shell } from "@/components/shell" -import * as React from "react" - -export default async function MenuListPage() { - // 초기 데이터 로드 - const [menusResult, usersResult] = await Promise.all([ - getMenuAssignments(), - getActiveUsers() - ]); - - return ( - -
-
-
-

- 메뉴 관리 -

- {/*

- 각 메뉴별로 담당자를 지정하고 관리할 수 있습니다. -

*/} -
-
- -
- - - - - - - - 메뉴 리스트 - - - 시스템의 모든 메뉴와 담당자 정보를 확인할 수 있습니다. - {menusResult.data?.length > 0 && ( - - 총 {menusResult.data.length}개의 메뉴 - - )} - - - - 로딩 중...
}> - - - - - - - - ); -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/payment-conditions/page.tsx b/app/[lng]/procurement/(procurement)/payment-conditions/page.tsx deleted file mode 100644 index d001a39d..00000000 --- a/app/[lng]/procurement/(procurement)/payment-conditions/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import * as React from "react"; -import { type SearchParams } from "@/types/table"; -import { getValidFilters } from "@/lib/data-table"; -import { Shell } from "@/components/shell"; -import { Skeleton } from "@/components/ui/skeleton"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { SearchParamsCache } from "@/lib/payment-terms/validations"; -import { getPaymentTerms } from "@/lib/payment-terms/service"; -import { PaymentTermsTable } from "@/lib/payment-terms/table/payment-terms-table"; - -interface IndexPageProps { - searchParams: Promise; -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams; - const search = SearchParamsCache.parse(searchParams); - const validFilters = getValidFilters(search.filters); - - const promises = Promise.all([ - getPaymentTerms({ - ...search, - filters: validFilters, - }), - ]); - - return ( - -
-
-

지급 조건 관리

- {/*

- 지급 조건(Payment Terms)을 등록, 수정, 삭제할 수 있습니다. -

*/} -
-
- }> - - } - > - - -
- ); -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/po-rfq/page.tsx b/app/[lng]/procurement/(procurement)/po-rfq/page.tsx deleted file mode 100644 index 4a04d6a8..00000000 --- a/app/[lng]/procurement/(procurement)/po-rfq/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { getPORfqs } from "@/lib/procurement-rfqs/services" -import { searchParamsCache } from "@/lib/procurement-rfqs/validations" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { RFQListTable } from "@/lib/procurement-rfqs/table/rfq-table" -import { type SearchParams } from "@/types/table" -import * as React from "react" - -interface RfqPageProps { - searchParams: Promise -} - -export default async function RfqPage(props: RfqPageProps) { - // searchParams를 await하여 resolve - const searchParams = await props.searchParams - - // 파라미터 파싱 - const search = searchParamsCache.parse(searchParams); - const validFilters = getValidFilters(search.filters); - - // RFQ 서버는 기본필터와 고급필터를 분리해서 받으므로 그대로 전달 - const promises = Promise.all([ - getPORfqs({ - ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) - filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) - }) - ]) - - return ( - {/* fullscreen variant 사용 */} - {/* 고정 헤더 영역 */} -
-
-
-

- RFQ -

-
-
-
- - {/* 테이블 영역 - 남은 공간 모두 차지 */} -
- - } - > - - -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/po/page.tsx b/app/[lng]/procurement/(procurement)/po/page.tsx deleted file mode 100644 index b4dd914f..00000000 --- a/app/[lng]/procurement/(procurement)/po/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getPOs } from "@/lib/po/service" -import { searchParamsCache } from "@/lib/po/validations" -import { PoListsTable } from "@/lib/po/table/po-table" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getPOs({ - ...search, - filters: validFilters, - }), - ]) - - return ( - - -
-
-
-

- PO 확인 및 전자서명 -

- {/*

- 기간계 시스템으로부터 PO를 확인하고 협력업체에게 전자서명을 요청할 수 있습니다. 요쳥된 전자서명의 이력 또한 확인할 수 있습니다. - -

*/} -
-
-
- - - }> - - - } - > - - -
- ) -} diff --git a/app/[lng]/procurement/(procurement)/poa/page.tsx b/app/[lng]/procurement/(procurement)/poa/page.tsx deleted file mode 100644 index 1c244991..00000000 --- a/app/[lng]/procurement/(procurement)/poa/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getChangeOrders } from "@/lib/poa/service" -import { searchParamsCache } from "@/lib/poa/validations" -import { ChangeOrderListsTable } from "@/lib/poa/table/poa-table" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getChangeOrders({ - ...search, - filters: validFilters, - }), - ]) - - return ( - -
-
-
-

- 변경 PO 확인 및 전자서명 -

- {/*

- 발행된 PO의 변경 내역을 확인하고 관리할 수 있습니다. -

*/} -
-
-
- - }> - - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/pq-criteria/[pqListId]/page.tsx b/app/[lng]/procurement/(procurement)/pq-criteria/[pqListId]/page.tsx deleted file mode 100644 index 15cb3bf3..00000000 --- a/app/[lng]/procurement/(procurement)/pq-criteria/[pqListId]/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/pq/validations" -import { getPQsByListId } from "@/lib/pq/service" -import { PqsTable } from "@/lib/pq/pq-criteria/pq-table" -import { notFound } from "next/navigation" - -interface PQDetailPageProps { - params: Promise<{ pqListId: string }> - searchParams: Promise -} - -export default async function PQDetailPage(props: PQDetailPageProps) { - const params = await props.params - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const pqListId = parseInt(params.pqListId) - if (isNaN(pqListId)) { - notFound() - } - - // filters가 없는 경우를 처리 - const validFilters = getValidFilters(search.filters) - - // PQ 항목들 가져오기 - const promises = Promise.all([ - getPQsByListId(pqListId, { - ...search, - filters: validFilters, - }) - ]) - - return ( - -
-
-

- PQ 항목 관리 -

- {/*

- 선택한 PQ 목록의 세부 항목들을 관리할 수 있습니다. -

*/} -
-
- - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/pq-criteria/page.tsx b/app/[lng]/procurement/(procurement)/pq-criteria/page.tsx deleted file mode 100644 index 1a337cc9..00000000 --- a/app/[lng]/procurement/(procurement)/pq-criteria/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/pq/validations" -import { getPQLists } from "@/lib/pq/service" -import { PqListsTable } from "@/lib/pq/table/pq-lists-table" -import { getProjects } from "@/lib/pq/service" - -interface ProjectPageProps { - searchParams: Promise -} - -export default async function ProjectPage(props: ProjectPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - // filters가 없는 경우를 처리 - const validFilters = getValidFilters(search.filters) - - // // 프로젝트별 PQ 데이터 가져오기 - const promises = Promise.all([ - getPQLists({ - ...search, - filters: validFilters, - }), - getProjects() - ]) - - return ( - -
-
-

- PQ 리스트 관리 -

- {/*

- 협력업체 등록을 위한, 협력업체가 제출할 PQ 항목을: 프로젝트별로 관리할 수 있습니다. -

*/} -
-
- - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx b/app/[lng]/procurement/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx deleted file mode 100644 index b4b51363..00000000 --- a/app/[lng]/procurement/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import * as React from "react" -import { Metadata } from "next" -import Link from "next/link" -import { notFound } from "next/navigation" -import { ArrowLeft } from "lucide-react" -import { Shell } from "@/components/shell" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { Separator } from "@/components/ui/separator" -import { getPQById, getPQDataByVendorId } from "@/lib/pq/service" -import { unstable_noStore as noStore } from 'next/cache' -import { PQReviewWrapper } from "@/components/pq-input/pq-review-wrapper" -import { formatDate } from "@/lib/utils" - -export const metadata: Metadata = { - title: "PQ 검토", - description: "협력업체의 Pre-Qualification 답변을 검토합니다.", -} - -// 페이지가 기본적으로 동적임을 나타냄 -export const dynamic = "force-dynamic" - -interface PQReviewPageProps { - params: Promise<{ - vendorId: string; - submissionId: string; - }> -} - -export default async function PQReviewPage(props: PQReviewPageProps) { - // 캐시 비활성화 - noStore() - - const params = await props.params - const vendorId = parseInt(params.vendorId, 10) - const submissionId = parseInt(params.submissionId, 10) - - try { - // PQ Submission 정보 조회 - const pqSubmission = await getPQById(submissionId, vendorId) - - // PQ 데이터 조회 (질문과 답변) - const pqData = await getPQDataByVendorId(vendorId, pqSubmission.projectId || undefined) - - // 프로젝트 정보 (프로젝트 PQ인 경우) - const projectInfo = pqSubmission.projectId ? { - id: pqSubmission.projectId, - projectCode: pqSubmission.projectCode || '', - projectName: pqSubmission.projectName || '', - status: pqSubmission.status, - submittedAt: pqSubmission.submittedAt, - } : null - - // PQ 유형 및 상태 레이블 - const typeLabel = pqSubmission.type === "GENERAL" ? "일반 PQ" : - pqSubmission.type === "PROJECT" ? "프로젝트 PQ" : - pqSubmission.type === "NON_INSPECTION" ? "미실사 PQ" : "일반 PQ" - const statusLabel = getStatusLabel(pqSubmission.status) - const statusVariant = getStatusVariant(pqSubmission.status) - - // 수정 가능 여부 (SUBMITTED 상태일 때만 가능) - const canReview = pqSubmission.status === "SUBMITTED" - - return ( - -
-
- -
-

- {pqSubmission.vendorName} - {typeLabel} -

-
- {statusLabel} - {projectInfo && ( - - {projectInfo.projectName} ({projectInfo.projectCode}) - - )} -
-
-
-
- - {/* 상태별 알림 */} - {pqSubmission.status === "SUBMITTED" && ( - - 제출 완료 - - 협력업체가 {formatDate(pqSubmission.submittedAt, "kr")}에 PQ를 제출했습니다. 검토 후 승인 또는 거부할 수 있습니다. - - - )} - - {pqSubmission.status === "APPROVED" && ( - - 승인됨 - - {formatDate(pqSubmission.approvedAt, "kr")}에 승인되었습니다. - - - )} - - {pqSubmission.status === "REJECTED" && ( - - 거부됨 - - {formatDate(pqSubmission.rejectedAt, "kr")}에 거부되었습니다. - {pqSubmission.rejectReason && ( -
- 사유: {pqSubmission.rejectReason} -
- )} -
-
- )} - - - - {/* PQ 검토 컴포넌트 */} - - - PQ 검토 - 협력업체 정보 - - - - - - - -
-

협력업체 정보

-
-
-

업체명

-

{pqSubmission.vendorName}

-
-
-

업체 코드

-

{pqSubmission.vendorCode}

-
-
-

상태

-

{pqSubmission.vendorStatus}

-
- {/* 필요시 추가 정보 표시 */} -
-
-
-
-
- ) - } catch (error) { - console.error("Error loading PQ:", error) - notFound() - } -} - -// 상태 레이블 함수 -function getStatusLabel(status: string): string { - switch (status) { - case "REQUESTED": - return "요청됨"; - case "IN_PROGRESS": - return "진행 중"; - case "SUBMITTED": - return "제출됨"; - case "APPROVED": - return "승인됨"; - case "REJECTED": - return "거부됨"; - default: - return status; - } -} - -// 상태별 Badge 스타일 -function getStatusVariant(status: string): "default" | "outline" | "secondary" | "destructive" | "success" { - switch (status) { - case "REQUESTED": - return "outline"; - case "IN_PROGRESS": - return "secondary"; - case "SUBMITTED": - return "default"; - case "APPROVED": - return "success"; - case "REJECTED": - return "destructive"; - default: - return "outline"; - } -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/pq_new/page.tsx b/app/[lng]/procurement/(procurement)/pq_new/page.tsx deleted file mode 100644 index 6a992ee5..00000000 --- a/app/[lng]/procurement/(procurement)/pq_new/page.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import * as React from "react" -import { Metadata } from "next" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { searchParamsPQReviewCache } from "@/lib/pq/validations" -import { getPQSubmissions } from "@/lib/pq/service" -import { PQSubmissionsTable } from "@/lib/pq/pq-review-table-new/vendors-table" -import { InformationButton } from "@/components/information/information-button" -export const metadata: Metadata = { - title: "협력업체 PQ/실사 현황", - description: "", -} - -interface PQReviewPageProps { - searchParams: Promise -} - -export default async function PQReviewPage(props: PQReviewPageProps) { - const searchParams = await props.searchParams - const search = searchParamsPQReviewCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // 디버깅 로그 추가 - console.log("=== PQ Page Debug ==="); - console.log("Raw searchParams:", searchParams); - console.log("Raw basicFilters param:", searchParams.basicFilters); - console.log("Raw pqBasicFilters param:", searchParams.pqBasicFilters); - console.log("Parsed search:", search); - console.log("search.filters:", search.filters); - console.log("search.basicFilters:", search.basicFilters); - console.log("search.pqBasicFilters:", search.pqBasicFilters); - console.log("validFilters:", validFilters); - - // 기본 필터 처리 (통일된 이름 사용) - let basicFilters = [] - if (search.basicFilters && search.basicFilters.length > 0) { - basicFilters = search.basicFilters - console.log("Using search.basicFilters:", basicFilters); - } else if (search.pqBasicFilters && search.pqBasicFilters.length > 0) { - // 하위 호환성을 위해 기존 이름도 지원 - basicFilters = search.pqBasicFilters - console.log("Using search.pqBasicFilters:", basicFilters); - } else { - console.log("No basic filters found"); - } - - // 모든 필터를 합쳐서 처리 - const allFilters = [...validFilters, ...basicFilters] - - console.log("Final allFilters:", allFilters); - - // 조인 연산자도 통일된 이름 사용 - const joinOperator = search.basicJoinOperator || search.pqBasicJoinOperator || search.joinOperator || 'and'; - console.log("Final joinOperator:", joinOperator); - - // Promise.all로 감싸서 전달 - const promises = Promise.all([ - getPQSubmissions({ - ...search, - filters: allFilters, - joinOperator, - }) - ]) - - return ( - -
-
-
-
-

- 협력업체 PQ/실사 현황 -

- -
-
-
-
- - {/* Items처럼 직접 테이블 렌더링 */} - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/project-gtc/page.tsx b/app/[lng]/procurement/(procurement)/project-gtc/page.tsx deleted file mode 100644 index 554f17b0..00000000 --- a/app/[lng]/procurement/(procurement)/project-gtc/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getProjectGtcList } from "@/lib/project-gtc/service" -import { projectGtcSearchParamsSchema } from "@/lib/project-gtc/validations" -import { ProjectGtcTable } from "@/lib/project-gtc/table/project-gtc-table" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = projectGtcSearchParamsSchema.parse(searchParams) - - const promises = Promise.all([ - getProjectGtcList({ - page: search.page, - perPage: search.perPage, - search: search.search, - sort: search.sort, - }), - ]) - - return ( - -
-
-
-

- Project GTC 관리 -

- {/*

- 프로젝트별 GTC(General Terms and Conditions) 파일을 관리할 수 있습니다. - 각 프로젝트마다 하나의 GTC 파일을 업로드할 수 있으며, 파일 업로드 시 기존 파일은 자동으로 교체됩니다. -

*/} -
-
-
- - }> - {/* 추가 기능이 필요하면 여기에 추가 */} - - - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/project-vendors/page.tsx b/app/[lng]/procurement/(procurement)/project-vendors/page.tsx deleted file mode 100644 index 525cff07..00000000 --- a/app/[lng]/procurement/(procurement)/project-vendors/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { ProjectAVLTable } from "@/lib/project-avl/table/proejctAVL-table" -import { getProjecTAVL } from "@/lib/project-avl/service" -import { searchProjectAVLParamsCache } from "@/lib/project-avl/validations" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchProjectAVLParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getProjecTAVL({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- 프로젝트 AVL 리스트 -

- {/*

- 프로젝트 PQ를 통과한 벤더의 리스트를 보여줍니다.{" "} - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/procurement/(procurement)/projects/page.tsx b/app/[lng]/procurement/(procurement)/projects/page.tsx deleted file mode 100644 index 8c332c6c..00000000 --- a/app/[lng]/procurement/(procurement)/projects/page.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { ItemsTable } from "@/lib/items/table/items-table" -import { getProjectLists } from "@/lib/projects/service" -import { ProjectsTable } from "@/lib/projects/table/projects-table" -import { searchParamsProjectsCache } from "@/lib/projects/validation" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsProjectsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getProjectLists({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- 수행 프로젝트 리스트 from S-EDP -

- {/*

- S-EDP로부터 수신하는 프로젝트 리스트입니다. 향후 MDG로 전환됩니다.{" "} - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/procurement/(procurement)/report/page.tsx b/app/[lng]/procurement/(procurement)/report/page.tsx deleted file mode 100644 index 2782c3ac..00000000 --- a/app/[lng]/procurement/(procurement)/report/page.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import * as React from "react"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Shell } from "@/components/shell"; -import { ErrorBoundary } from "@/components/error-boundary"; -import { getDashboardData } from "@/lib/dashboard/service"; -import { DashboardClient } from "@/lib/dashboard/dashboard-client"; - -// 데이터 fetch 시 비동기 함수 호출 후 await 하므로 static-pre-render 과정에서 dynamic-server-error 발생. -// 따라서, dynamic 속성을 force-dynamic 으로 설정하여 동적 렌더링 처리 -// getDashboardData 함수에 대한 Promise를 넘기는 식으로 수정하게 되면 force-dynamic 선언을 제거해도 됨. -export const dynamic = 'force-dynamic' - -export default async function IndexPage() { - // domain을 명시적으로 전달 - const domain = "procurement"; - - try { - // 서버에서 직접 데이터 fetch - const dashboardData = await getDashboardData(domain); - - return ( - - - - ); - } catch (error) { - console.error("Dashboard data fetch error:", error); - return ( - -
-
-

데이터를 불러오는데 실패했습니다.

-

{error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다."}

-
-
-
- ); - } -} - -function DashboardSkeleton() { - return ( -
- {/* 헤더 스켈레톤 */} -
-
- - -
- -
- - {/* 요약 카드 스켈레톤 */} -
- {[...Array(4)].map((_, i) => ( -
-
- - -
- - -
- ))} -
- - {/* 차트 스켈레톤 */} -
- {[...Array(2)].map((_, i) => ( -
-
- - -
- -
- ))} -
- - {/* 탭 스켈레톤 */} -
- -
- {[...Array(6)].map((_, i) => ( -
- -
-
- - -
-
- - - -
- -
-
- ))} -
-
-
- ); -} diff --git a/app/[lng]/procurement/(procurement)/rfq/[id]/cbe/page.tsx b/app/[lng]/procurement/(procurement)/rfq/[id]/cbe/page.tsx deleted file mode 100644 index fb288a98..00000000 --- a/app/[lng]/procurement/(procurement)/rfq/[id]/cbe/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsCBECache } from "@/lib/rfqs/validations" -import { getCBE } from "@/lib/rfqs/service" -import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqCBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsCBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getCBE({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Commercial Bid Evaluation -

-

- 초대된 협력업체에게 CBE를 보낼 수 있습니다.
"발행하기" 버튼을 통해 CBE를 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/rfq/[id]/layout.tsx b/app/[lng]/procurement/(procurement)/rfq/[id]/layout.tsx deleted file mode 100644 index 92817b4b..00000000 --- a/app/[lng]/procurement/(procurement)/rfq/[id]/layout.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Metadata } from "next" -import Link from "next/link" -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { RfqViewWithItems } from "@/db/schema/rfq" -import { findRfqById } from "@/lib/rfqs/service" -import { formatDate } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { ArrowLeft } from "lucide-react" - -export const metadata: Metadata = { - title: "Vendor Detail", -} - -export default async function RfqLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string, id: string } -}) { - - // 1) URL 파라미터에서 id 추출, Number로 변환 - const resolvedParams = await params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - // 2) DB에서 해당 협력업체 정보 조회 - const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) - - // 3) 사이드바 메뉴 - const sidebarNavItems = [ - { - title: "Matched Vendors", - href: `/${lng}/evcp/rfq/${id}`, - }, - { - title: "TBE", - href: `/${lng}/evcp/rfq/${id}/tbe`, - }, - { - title: "CBE", - href: `/${lng}/evcp/rfq/${id}/cbe`, - }, - - ] - - return ( - <> -
-
-
-
- - - -
-
- {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} -

- {rfq - ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` - : "Loading RFQ..."} -

- -

- {rfq - ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` - : ""} -

-

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate, "KR")}}

-
- -
- -
{children}
-
-
-
-
- - ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/rfq/[id]/page.tsx b/app/[lng]/procurement/(procurement)/rfq/[id]/page.tsx deleted file mode 100644 index 1a9f4b18..00000000 --- a/app/[lng]/procurement/(procurement)/rfq/[id]/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getMatchedVendors } from "@/lib/rfqs/service" -import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" -import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsMatchedVCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getMatchedVendors({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Vendors -

-

- 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/rfq/[id]/tbe/page.tsx b/app/[lng]/procurement/(procurement)/rfq/[id]/tbe/page.tsx deleted file mode 100644 index 76eea302..00000000 --- a/app/[lng]/procurement/(procurement)/rfq/[id]/tbe/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getTBE } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTBE({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Technical Bid Evaluation -

-

- 초대된 협력업체에게 TBE를 보낼 수 있습니다.
"발행하기" 버튼을 통해 TBE를 전송하면 첨부파일과 함께 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/rfq/page.tsx b/app/[lng]/procurement/(procurement)/rfq/page.tsx deleted file mode 100644 index 26f49cfb..00000000 --- a/app/[lng]/procurement/(procurement)/rfq/page.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { searchParamsCache } from "@/lib/rfqs/validations" -import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" -import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" -import { getAllItems } from "@/lib/items/service" -import { RfqType } from "@/lib/rfqs/validations" - -interface RfqPageProps { - searchParams: Promise; - rfqType: RfqType; - title: string; - description: string; -} - -export default async function RfqPage({ - searchParams, - rfqType = RfqType.PURCHASE, - title = "RFQ", - description = "RFQ를 등록하고 관리할 수 있습니다." -}: RfqPageProps) { - const search = searchParamsCache.parse(await searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRfqs({ - ...search, - filters: validFilters, - rfqType // 전달받은 rfqType 사용 - }), - getRfqStatusCounts(rfqType), // rfqType 전달 - getAllItems() - ]) - - return ( - -
-
-
-

- {title} -

- {/*

- {description} -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/settings/layout.tsx b/app/[lng]/procurement/(procurement)/settings/layout.tsx deleted file mode 100644 index 6c380919..00000000 --- a/app/[lng]/procurement/(procurement)/settings/layout.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" - -export const metadata: Metadata = { - title: "Settings", - // description: "Advanced form example using react-hook-form and Zod.", -} - - -interface SettingsLayoutProps { - children: React.ReactNode - params: { lng: string } -} - -export default async function SettingsLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string } -}) { - const resolvedParams = await params - const lng = resolvedParams.lng - - - const sidebarNavItems = [ - - { - title: "Account", - href: `/${lng}/evcp/settings`, - }, - { - title: "Preferences", - href: `/${lng}/evcp/settings/preferences`, - } - - - ] - - - return ( - <> -
-
-
-
-

설정

- {/*

- Manage your account settings and preferences. -

*/} -
- -
- -
{children}
-
-
-
-
- - - - ) -} diff --git a/app/[lng]/procurement/(procurement)/settings/page.tsx b/app/[lng]/procurement/(procurement)/settings/page.tsx deleted file mode 100644 index eba5e948..00000000 --- a/app/[lng]/procurement/(procurement)/settings/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { AccountForm } from "@/components/settings/account-form" - -export default function SettingsAccountPage() { - return ( -
-
-

Account

- {/*

- Update your account settings. Set your preferred language and - timezone. -

*/} -
- - -
- ) -} diff --git a/app/[lng]/procurement/(procurement)/settings/preferences/page.tsx b/app/[lng]/procurement/(procurement)/settings/preferences/page.tsx deleted file mode 100644 index e2a88021..00000000 --- a/app/[lng]/procurement/(procurement)/settings/preferences/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { AppearanceForm } from "@/components/settings/appearance-form" - -export default function SettingsAppearancePage() { - return ( -
-
-

Preference

-

- Customize the preference of the app. -

-
- - -
- ) -} diff --git a/app/[lng]/procurement/(procurement)/system/admin-users/page.tsx b/app/[lng]/procurement/(procurement)/system/admin-users/page.tsx deleted file mode 100644 index 11a9e9fb..00000000 --- a/app/[lng]/procurement/(procurement)/system/admin-users/page.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { DateRangePicker } from "@/components/date-range-picker" -import { Separator } from "@/components/ui/separator" - -import { searchParamsCache } from "@/lib/admin-users/validations" -import { getAllCompanies, getAllRoles, getUserCountGroupByCompany, getUserCountGroupByRole, getUsers } from "@/lib/admin-users/service" -import { AdmUserTable } from "@/lib/admin-users/table/ausers-table" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function UserTable(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getUsers({ - ...search, - filters: validFilters, - }), - getUserCountGroupByCompany(), - getUserCountGroupByRole(), - getAllCompanies(), - getAllRoles() - ]) - - return ( - - } - > -
-
-

Vendor Admin User Management

-

- 협력업체의 유저 전체를 조회하고 어드민 유저를 생성할 수 있는 페이지입니다. 이곳에서 초기 유저를 생성시킬 수 있습니다.
생성 후에는 생성된 사용자의 이메일로 생성 통보 이메일이 발송되며 사용자는 이메일과 OTP로 로그인이 가능합니다. -

-
- - -
-
- - ) -} diff --git a/app/[lng]/procurement/(procurement)/system/layout.tsx b/app/[lng]/procurement/(procurement)/system/layout.tsx deleted file mode 100644 index 2776ed8b..00000000 --- a/app/[lng]/procurement/(procurement)/system/layout.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" - -export const metadata: Metadata = { - title: "System Setting", - // description: "Advanced form example using react-hook-form and Zod.", -} - - -interface SettingsLayoutProps { - children: React.ReactNode - params: { lng: string } -} - -export default async function SettingsLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string } -}) { - const resolvedParams = await params - const lng = resolvedParams.lng - - - const sidebarNavItems = [ - - { - title: "삼성중공업 사용자", - href: `/${lng}/evcp/system`, - }, - { - title: "Roles", - href: `/${lng}/evcp/system/roles`, - }, - { - title: "권한 통제", - href: `/${lng}/evcp/system/permissions`, - }, - { - title: "협력업체 사용자", - href: `/${lng}/evcp/system/admin-users`, - }, - - { - title: "비밀번호 정책", - href: `/${lng}/evcp/system/password-policy`, - }, - - ] - - - return ( - <> -
-
-
-
-

시스템 설정

- {/*

- 사용자, 롤, 접근 권한을 관리하세요. -

*/} -
- -
- -
{children}
-
-
-
-
- - - - ) -} diff --git a/app/[lng]/procurement/(procurement)/system/page.tsx b/app/[lng]/procurement/(procurement)/system/page.tsx deleted file mode 100644 index fe0a262c..00000000 --- a/app/[lng]/procurement/(procurement)/system/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import * as React from "react" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsCache } from "@/lib/admin-users/validations" -import { getAllRoles, getUsersEVCP } from "@/lib/users/service" -import { getUserCountGroupByRole } from "@/lib/admin-users/service" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { UserTable } from "@/lib/users/table/users-table" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function SystemUserPage(props: IndexPageProps) { - - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getUsersEVCP({ - ...search, - filters: validFilters, - }), - getUserCountGroupByRole(), - getAllRoles() - ]) - - return ( - - } - > -
-
-

SHI Users

-

- 시스템 전체 사용자들을 조회하고 관리할 수 있는 페이지입니다. 사용자에게 롤을 할당하는 것으로 메뉴별 권한을 관리할 수 있습니다. -

-
- - -
-
- - ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/system/password-policy/page.tsx b/app/[lng]/procurement/(procurement)/system/password-policy/page.tsx deleted file mode 100644 index 0f14fefe..00000000 --- a/app/[lng]/procurement/(procurement)/system/password-policy/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -// app/admin/password-policy/page.tsx - -import * as React from "react" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Separator } from "@/components/ui/separator" -import { Alert, AlertDescription } from "@/components/ui/alert" -import { AlertTriangle } from "lucide-react" -import SecuritySettingsTable from "@/components/system/passwordPolicy" -import { getSecuritySettings } from "@/lib/password-policy/service" - - -export default async function PasswordPolicyPage() { - try { - // 보안 설정 데이터 로드 - const securitySettings = await getSecuritySettings() - - return ( - - } - > -
-
-

협력업체 사용자 비밀번호 정책 설정

-

- 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다. -

-
- - -
-
- ) - } catch (error) { - console.error('Failed to load security settings:', error) - - return ( -
-
-

협력업체 사용자 비밀번호 정책 설정

-

- 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다. -

-
- - - - - 보안 설정을 불러오는 중 오류가 발생했습니다. 페이지를 새로고침하거나 관리자에게 문의하세요. - - -
- ) - } -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/system/permissions/page.tsx b/app/[lng]/procurement/(procurement)/system/permissions/page.tsx deleted file mode 100644 index 6aa2b693..00000000 --- a/app/[lng]/procurement/(procurement)/system/permissions/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import PermissionsTree from "@/components/system/permissionsTree" -import { Separator } from "@/components/ui/separator" - -export default function PermissionsPage() { - return ( -
-
-

Permissions

-

- Set permissions to the menu by Role -

-
- - -
- ) -} diff --git a/app/[lng]/procurement/(procurement)/system/roles/page.tsx b/app/[lng]/procurement/(procurement)/system/roles/page.tsx deleted file mode 100644 index fe074600..00000000 --- a/app/[lng]/procurement/(procurement)/system/roles/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Separator } from "@/components/ui/separator" - -import { searchParamsCache } from "@/lib/roles/validations" -import { searchParamsCache as searchParamsCache2 } from "@/lib/admin-users/validations" -import { RolesTable } from "@/lib/roles/table/roles-table" -import { getRolesWithCount } from "@/lib/roles/services" -import { getUsersAll } from "@/lib/users/service" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function UserTable(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - const search2 = searchParamsCache2.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRolesWithCount({ - ...search, - filters: validFilters, - }), - - - ]) - - - const promises2 = Promise.all([ - getUsersAll({ - ...search2, - filters: validFilters, - }, "evcp"), - ]) - - - return ( - - } - > -
-
-

Role Management

-

- 역할을 생성하고 역할에 유저를 할당할 수 있는 페이지입니다. 역할에 메뉴의 접근 권한 역시 할당할 수 있습니다. -

-
- - -
-
- - ) -} diff --git a/app/[lng]/procurement/(procurement)/tbe/page.tsx b/app/[lng]/procurement/(procurement)/tbe/page.tsx deleted file mode 100644 index 1a7fdf86..00000000 --- a/app/[lng]/procurement/(procurement)/tbe/page.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getAllTBE } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { AllTbeTable } from "@/lib/tbe/table/tbe-table" -import { RfqType } from "@/lib/rfqs/validations" -import * as React from "react" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" - -interface IndexPageProps { - params: { - lng: string - } - searchParams: Promise -} - -// 타입별 페이지 설명 구성 (Budgetary 제외) -const typeConfig: Record = { - "purchase": { - title: "Purchase RFQ Technical Bid Evaluation", - description: "실제 구매 발주 전 가격 요청을 위한 TBE입니다.", - rfqType: RfqType.PURCHASE - }, - "purchase-budgetary": { - title: "Purchase Budgetary RFQ Technical Bid Evaluation", - description: "프로젝트 수주 후, 공식 입찰 전 예산 책정을 위한 TBE입니다.", - rfqType: RfqType.PURCHASE_BUDGETARY - } -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - - // URL 쿼리 파라미터에서 타입 추출 - const searchParams = await props.searchParams - // 기본값으로 'purchase' 사용 - const typeParam = searchParams?.type as string || 'purchase' - - // 유효한 타입인지 확인하고 기본값 설정 - const validType = Object.keys(typeConfig).includes(typeParam) ? typeParam : 'purchase' - const rfqType = typeConfig[validType].rfqType - - // SearchParams 파싱 (Zod) - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // 현재 선택된 타입의 데이터 로드 - const promises = Promise.all([ - getAllTBE({ - ...search, - filters: validFilters, - rfqType - }) - ]) - - // 페이지 경로 생성 함수 - 단순화 - const getTabUrl = (type: string) => { - return `/${lng}/evcp/tbe?type=${type}`; - } - - return ( - -
-
-
-

- Technical Bid Evaluation -

-

- 초대된 협력업체에게 TBE를 보낼 수 있습니다.
- 체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

-
-
-
- - {/* 타입 선택 탭 (Budgetary 제외) */} - - - - Purchase - - - Purchase Budgetary - - - -
-

- {typeConfig[validType].description} -

-
-
- - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendor-candidates/page.tsx b/app/[lng]/procurement/(procurement)/vendor-candidates/page.tsx deleted file mode 100644 index fb80cf64..00000000 --- a/app/[lng]/procurement/(procurement)/vendor-candidates/page.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { getVendorCandidateCounts, getVendorCandidates } from "@/lib/vendor-candidates/service" -import { searchParamsCandidateCache } from "@/lib/vendor-candidates/validations" -import { VendorCandidateTable } from "@/lib/vendor-candidates/table/candidates-table" -import { DateRangePicker } from "@/components/date-range-picker" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCandidateCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getVendorCandidates({ - ...search, - filters: validFilters, - }), - getVendorCandidateCounts() - ]) - - return ( - - -
-
-
-

- 발굴업체 등록 관리 -

- {/*

- 수집한 협력업체 후보를 등록하고 초대 메일을 송부할 수 있습니다. -

*/} -
-
-
- - {/* 수집일 라벨과 DateRangePicker를 함께 배치 */} -
- {/* 수집일 기간 설정: */} - }> - - -
- - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendor-check-list/page.tsx b/app/[lng]/procurement/(procurement)/vendor-check-list/page.tsx deleted file mode 100644 index e6f9ce82..00000000 --- a/app/[lng]/procurement/(procurement)/vendor-check-list/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getGenralEvaluationsSchema } from "@/lib/general-check-list/validation" -import { GeneralEvaluationsTable } from "@/lib/general-check-list/table/general-check-list-table" -import { getGeneralEvaluations } from "@/lib/general-check-list/service" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = getGenralEvaluationsSchema.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getGeneralEvaluations({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- 협력업체 평가자료 문항 관리 -

- {/*

- 협력업체 평가에 사용되는 정기평가 체크리스트를 관리{" "} - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/procurement/(procurement)/vendor-investigation/page.tsx b/app/[lng]/procurement/(procurement)/vendor-investigation/page.tsx deleted file mode 100644 index af9f3e11..00000000 --- a/app/[lng]/procurement/(procurement)/vendor-investigation/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { VendorsInvestigationTable } from "@/lib/vendor-investigation/table/investigation-table" -import { getVendorsInvestigation } from "@/lib/vendor-investigation/service" -import { searchParamsInvestigationCache } from "@/lib/vendor-investigation/validations" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsInvestigationCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getVendorsInvestigation({ - ...search, - filters: validFilters, - }), - ]) - - return ( - - -
-
-
-

- 협력업체 실사 관리 -

- {/*

- 요청된 Vendor 실사에 대한 스케줄 정보를 관리하고 결과를 입력할 수 있습니다. - -

*/} -
-
-
- - - }> - - - } - > - - -
- ) -} diff --git a/app/[lng]/procurement/(procurement)/vendor-type/page.tsx b/app/[lng]/procurement/(procurement)/vendor-type/page.tsx deleted file mode 100644 index 96169e8a..00000000 --- a/app/[lng]/procurement/(procurement)/vendor-type/page.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/vendor-type/validations" -import { VendorTypesTable } from "@/lib/vendor-type/table/vendorTypes-table" -import { getVendorTypes } from "@/lib/vendor-type/service" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getVendorTypes({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- 업체 유형 -

- {/*

- 업체 유형을 등록하고 관리할 수 있습니다.{" "} - -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/procurement/(procurement)/vendors/[id]/info/items/page.tsx b/app/[lng]/procurement/(procurement)/vendors/[id]/info/items/page.tsx deleted file mode 100644 index 5d5838c6..00000000 --- a/app/[lng]/procurement/(procurement)/vendors/[id]/info/items/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { getVendorItems } from "@/lib/vendors/service" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsItemCache } from "@/lib/vendors/validations" -import { VendorItemsTable } from "@/lib/vendors/items-table/item-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function SettingsAccountPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsItemCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - - - const promises = Promise.all([ - getVendorItems({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - // 4) 렌더링 - return ( -
-
-

- 공급품목(패키지) -

-

- {/* 딜리버리가 가능한 아이템 리스트를 확인할 수 있습니다. */} -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendors/[id]/info/layout.tsx b/app/[lng]/procurement/(procurement)/vendors/[id]/info/layout.tsx deleted file mode 100644 index 7e2cd4f6..00000000 --- a/app/[lng]/procurement/(procurement)/vendors/[id]/info/layout.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { findVendorById } from "@/lib/vendors/service" // 가정: 여기에 findVendorById가 있다고 가정 -import { Vendor } from "@/db/schema/vendors" -import { Button } from "@/components/ui/button" -import { ArrowLeft } from "lucide-react" -import Link from "next/link" -export const metadata: Metadata = { - title: "Vendor Detail", -} - -export default async function SettingsLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string , id: string} -}) { - - // 1) URL 파라미터에서 id 추출, Number로 변환 - const resolvedParams = await params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - // 2) DB에서 해당 협력업체 정보 조회 - const vendor: Vendor | null = await findVendorById(idAsNumber) - - // 3) 사이드바 메뉴 - const sidebarNavItems = [ - { - title: "연락처", - href: `/${lng}/evcp/vendors/${id}/info`, - }, - { - title: "공급품목(패키지)", - href: `/${lng}/evcp/vendors/${id}/info/items`, - }, - { - title: "공급품목(자재그룹)", - href: `/${lng}/evcp/vendors/${id}/info/materials`, - }, - { - title: "견적 히스토리", - href: `/${lng}/evcp/vendors/${id}/info/rfq-history`, - }, - { - title: "입찰 히스토리", - href: `/${lng}/evcp/vendors/${id}/info/bid-history`, - }, - { - title: "계약 히스토리", - href: `/${lng}/evcp/vendors/${id}/info/contract-history`, - }, - ] - - return ( - <> -
-
-
- {/* RFQ 목록으로 돌아가는 링크 추가 */} -
- - - -
-
- {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} -

- {vendor - ? `${vendor.vendorCode ?? ""} - ${vendor.vendorName} 상세 정보` - : "Loading Vendor..."} -

-

협력업체 관련 상세사항을 확인하세요.

-
- -
- -
{children}
-
-
-
-
- - ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendors/[id]/info/materials/page.tsx b/app/[lng]/procurement/(procurement)/vendors/[id]/info/materials/page.tsx deleted file mode 100644 index 0ebb66ba..00000000 --- a/app/[lng]/procurement/(procurement)/vendors/[id]/info/materials/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsMaterialCache } from "@/lib/vendors/validations" -import { getVendorMaterials } from "@/lib/vendors/service" -import { VendorMaterialsTable } from "@/lib/vendors/materials-table/item-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function SettingsAccountPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsMaterialCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - - - const promises = Promise.all([ - getVendorMaterials({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - // 4) 렌더링 - return ( -
-
-

- 공급품목(자재 그룹) -

-

- {/* 딜리버리가 가능한 공급품목(자재 그룹)을 확인할 수 있습니다. */} -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendors/[id]/info/page.tsx b/app/[lng]/procurement/(procurement)/vendors/[id]/info/page.tsx deleted file mode 100644 index 6279e924..00000000 --- a/app/[lng]/procurement/(procurement)/vendors/[id]/info/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { getVendorContacts } from "@/lib/vendors/service" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsContactCache } from "@/lib/vendors/validations" -import { VendorContactsTable } from "@/lib/vendors/contacts-table/contact-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function SettingsAccountPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsContactCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - - - const promises = Promise.all([ - getVendorContacts({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - // 4) 렌더링 - return ( -
-
-

- Contacts -

-

- 업무별 담당자 정보를 확인하세요. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendors/[id]/info/rfq-history/page.tsx b/app/[lng]/procurement/(procurement)/vendors/[id]/info/rfq-history/page.tsx deleted file mode 100644 index c7f8f8b6..00000000 --- a/app/[lng]/procurement/(procurement)/vendors/[id]/info/rfq-history/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { getRfqHistory } from "@/lib/vendors/service" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsRfqHistoryCache } from "@/lib/vendors/validations" -import { VendorRfqHistoryTable } from "@/lib/vendors/rfq-history-table/rfq-history-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqHistoryPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsRfqHistoryCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRfqHistory({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- RFQ History -

-

- 협력업체의 RFQ 참여 이력을 확인할 수 있습니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendors/page.tsx b/app/[lng]/procurement/(procurement)/vendors/page.tsx deleted file mode 100644 index 02616999..00000000 --- a/app/[lng]/procurement/(procurement)/vendors/page.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - - -import { searchParamsCache } from "@/lib/vendors/validations" -import { getVendors, getVendorStatusCounts } from "@/lib/vendors/service" -import { VendorsTable } from "@/lib/vendors/table/vendors-table" -import { Ellipsis } from "lucide-react" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getVendors({ - ...search, - filters: validFilters, - }), - getVendorStatusCounts(), - ]) - - return ( - - -
-
-
-

- 협력업체 관리 -

- {/*

- 협력업체에 대한 요약 정보를 확인하고{" "} - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다.
벤더의 상태에 따라 가입을 승인해주거나 PQ 요청을 할 수 있고 검토가 완료된 벤더를 기간계 시스템에 전송하여 협력업체 코드를 따올 수 있습니다. -

*/} -
-
-
- - - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/procurement/page.tsx b/app/[lng]/procurement/page.tsx deleted file mode 100644 index f9662cb7..00000000 --- a/app/[lng]/procurement/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Metadata } from "next" -import { Suspense } from "react" -import { LoginFormSkeleton } from "@/components/login/login-form-skeleton" -import { LoginFormSHI } from "@/components/login/login-form-shi" - -export const metadata: Metadata = { - title: "eVCP Portal", - description: "", -} - -export default function AuthenticationPage() { - - - return ( - <> - }> - - - - ) -} diff --git a/app/[lng]/sales/(sales)/bid-projects/page.tsx b/app/[lng]/sales/(sales)/bid-projects/page.tsx deleted file mode 100644 index 38cbf91a..00000000 --- a/app/[lng]/sales/(sales)/bid-projects/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getBidProjectLists } from "@/lib/bidding-projects/service" -import { searchParamsBidProjectsCache } from "@/lib/bidding-projects/validation" -import { BidProjectsTable } from "@/lib/bidding-projects/table/projects-table" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsBidProjectsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getBidProjectLists({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- 견적 프로젝트 관리 -

- {/*

- SAP(S-ERP)로부터 수신한 견적 프로젝트 데이터입니다. 기술영업의 Budgetary RFQ에서 사용됩니다. - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/sales/(sales)/bqcbe/page.tsx b/app/[lng]/sales/(sales)/bqcbe/page.tsx deleted file mode 100644 index 30935645..00000000 --- a/app/[lng]/sales/(sales)/bqcbe/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getAllCBE } from "@/lib/rfqs/service" -import { searchParamsCBECache } from "@/lib/rfqs/validations" - -import { AllCbeTable } from "@/lib/cbe/table/cbe-table" - -import { RfqType } from "@/lib/rfqs/validations" -import * as React from "react" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - } - searchParams: Promise - rfqType: RfqType -} - -export default async function RfqCBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - - const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsCBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getAllCBE({ - ...search, - filters: validFilters, - rfqType - } - ) - ]) - - // 4) 렌더링 - return ( - -
-
-
-

- CBE 관리 -

- {/*

- 초대된 협력업체에게 CBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

*/} -
-
-
- - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/bqtbe/page.tsx b/app/[lng]/sales/(sales)/bqtbe/page.tsx deleted file mode 100644 index 3e56cfaa..00000000 --- a/app/[lng]/sales/(sales)/bqtbe/page.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getAllTBE } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { AllTbeTable } from "@/lib/tbe/table/tbe-table" -import { RfqType } from "@/lib/rfqs/validations" -import * as React from "react" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - } - searchParams: Promise - rfqType: RfqType -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - - const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getAllTBE({ - ...search, - filters: validFilters, - rfqType - } - ) - ]) - - // 4) 렌더링 - return ( - -
-
-
-

- TBE 관리 -

- {/*

- 초대된 협력업체에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

*/} -
-
-
- - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/cbe/page.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/cbe/page.tsx deleted file mode 100644 index 956facd3..00000000 --- a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/cbe/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getCBE, getTBE } from "@/lib/rfqs/service" -import { searchParamsCBECache, } from "@/lib/rfqs/validations" -import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" -import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsCBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getCBE({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Commercial Bid Evaluation -

-

- 초대된 협력업체에게 CBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/layout.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/layout.tsx deleted file mode 100644 index 2b80e64f..00000000 --- a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/layout.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { Metadata } from "next" -import Link from "next/link" -import { ArrowLeft } from "lucide-react" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { RfqViewWithItems } from "@/db/schema/rfq" -import { findRfqById } from "@/lib/rfqs/service" -import { formatDate } from "@/lib/utils" -import { Button } from "@/components/ui/button" - -export const metadata: Metadata = { - title: "Vendor Detail", -} - -export default async function RfqLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string, id: string } -}) { - - // 1) URL 파라미터에서 id 추출, Number로 변환 - const resolvedParams = await params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - // 2) DB에서 해당 협력업체 정보 조회 - const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) - - // 3) 사이드바 메뉴 - const sidebarNavItems = [ - { - title: "Matched Vendors", - href: `/${lng}/evcp/budgetary/${id}`, - }, - { - title: "TBE", - href: `/${lng}/evcp/budgetary/${id}/tbe`, - }, - { - title: "CBE", - href: `/${lng}/evcp/budgetary/${id}/cbe`, - }, - - ] - - return ( - <> -
-
-
-
- - - -
-
- {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} -

- {rfq - ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` - : "Loading RFQ..."} -

- -

- {rfq - ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` - : ""} -

-

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate, "KR")}}

-
- -
- -
{children}
-
-
-
-
- - ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/page.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/page.tsx deleted file mode 100644 index dd9df563..00000000 --- a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/page.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getMatchedVendors } from "@/lib/rfqs/service" -import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" -import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" -import { RfqType } from "@/lib/rfqs/validations" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise - rfqType: RfqType -} - -export default async function RfqPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - const searchParams = await props.searchParams - const search = searchParamsMatchedVCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getMatchedVendors({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Vendors -

-

- 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/tbe/page.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/tbe/page.tsx deleted file mode 100644 index ec894e1c..00000000 --- a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/tbe/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getTBE } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTBE({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Technical Bid Evaluation -

-

- 초대된 협력업체에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-rfq/page.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/page.tsx deleted file mode 100644 index f342bbff..00000000 --- a/app/[lng]/sales/(sales)/budgetary-rfq/page.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { searchParamsCache } from "@/lib/rfqs/validations" -import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" -import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" -import { getAllItems } from "@/lib/items/service" -import { RfqType } from "@/lib/rfqs/validations" -import { Ellipsis } from "lucide-react" - -interface RfqPageProps { - searchParams: Promise; - rfqType: RfqType; - title: string; - description: string; -} - -export default async function RfqPage({ - searchParams, - rfqType = RfqType.PURCHASE_BUDGETARY, - title = "Budgetary Quote", - description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." -}: RfqPageProps) { - const search = searchParamsCache.parse(await searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRfqs({ - ...search, - filters: validFilters, - rfqType // 전달받은 rfqType 사용 - }), - getRfqStatusCounts(rfqType), // rfqType 전달 - getAllItems() - ]) - - return ( - -
-
-
-

- {title} -

- {/*

- {description} - 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, - - - 버튼 - 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-tech-sales-hull/page.tsx b/app/[lng]/sales/(sales)/budgetary-tech-sales-hull/page.tsx deleted file mode 100644 index b1be29db..00000000 --- a/app/[lng]/sales/(sales)/budgetary-tech-sales-hull/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { searchParamsHullCache } from "@/lib/techsales-rfq/validations" -import { getTechSalesHullRfqsWithJoin } from "@/lib/techsales-rfq/service" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { RFQListTable } from "@/lib/techsales-rfq/table/rfq-table" -import { type SearchParams } from "@/types/table" -import * as React from "react" - -interface HullRfqPageProps { - searchParams: Promise -} - -export default async function HullRfqPage(props: HullRfqPageProps) { - // searchParams를 await하여 resolve - const searchParams = await props.searchParams - - // 해양 HULL용 파라미터 파싱 - const search = searchParamsHullCache.parse(searchParams); - const validFilters = getValidFilters(search.filters); - - // 기술영업 해양 Hull RFQ 데이터를 Promise.all로 감싸서 전달 - const promises = Promise.all([ - getTechSalesHullRfqsWithJoin({ - ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) - filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) - }) - ]) - - return ( - {/* fullscreen variant 사용 */} - {/* 고정 헤더 영역 */} -
-
-
-

- 기술영업-해양 Hull RFQ -

-
-
-
- - {/* 테이블 영역 - 남은 공간 모두 차지 */} -
- - } - > - - -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-tech-sales-ship/page.tsx b/app/[lng]/sales/(sales)/budgetary-tech-sales-ship/page.tsx deleted file mode 100644 index b7bf9d15..00000000 --- a/app/[lng]/sales/(sales)/budgetary-tech-sales-ship/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { searchParamsShipCache } from "@/lib/techsales-rfq/validations" -import { getTechSalesShipRfqsWithJoin } from "@/lib/techsales-rfq/service" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { RFQListTable } from "@/lib/techsales-rfq/table/rfq-table" -import { type SearchParams } from "@/types/table" -import * as React from "react" - -interface RfqPageProps { - searchParams: Promise -} - -export default async function RfqPage(props: RfqPageProps) { - // searchParams를 await하여 resolve - const searchParams = await props.searchParams - - // 조선용 파라미터 파싱 - const search = searchParamsShipCache.parse(searchParams); - const validFilters = getValidFilters(search.filters); - - // 기술영업 조선 RFQ 데이터를 Promise.all로 감싸서 전달 - const promises = Promise.all([ - getTechSalesShipRfqsWithJoin({ - ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) - filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) - }) - ]) - - return ( - {/* fullscreen variant 사용 */} - {/* 고정 헤더 영역 */} -
-
-
-

- 기술영업-조선 RFQ -

-
-
-
- - {/* 테이블 영역 - 남은 공간 모두 차지 */} -
- - } - > - - -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-tech-sales-top/page.tsx b/app/[lng]/sales/(sales)/budgetary-tech-sales-top/page.tsx deleted file mode 100644 index f84a9794..00000000 --- a/app/[lng]/sales/(sales)/budgetary-tech-sales-top/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { searchParamsTopCache } from "@/lib/techsales-rfq/validations" -import { getTechSalesTopRfqsWithJoin } from "@/lib/techsales-rfq/service" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { RFQListTable } from "@/lib/techsales-rfq/table/rfq-table" -import { type SearchParams } from "@/types/table" -import * as React from "react" - -interface HullRfqPageProps { - searchParams: Promise -} - -export default async function HullRfqPage(props: HullRfqPageProps) { - // searchParams를 await하여 resolve - const searchParams = await props.searchParams - - // 해양 TOP용 파라미터 파싱 - const search = searchParamsTopCache.parse(searchParams); - const validFilters = getValidFilters(search.filters); - - // 기술영업 해양 TOP RFQ 데이터를 Promise.all로 감싸서 전달 - const promises = Promise.all([ - getTechSalesTopRfqsWithJoin({ - ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) - filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) - }) - ]) - - return ( - {/* fullscreen variant 사용 */} - {/* 고정 헤더 영역 */} -
-
-
-

- 기술영업-해양 TOP RFQ -

-
-
-
- - {/* 테이블 영역 - 남은 공간 모두 차지 */} -
- - } - > - - -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary/[id]/cbe/page.tsx b/app/[lng]/sales/(sales)/budgetary/[id]/cbe/page.tsx deleted file mode 100644 index 956facd3..00000000 --- a/app/[lng]/sales/(sales)/budgetary/[id]/cbe/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getCBE, getTBE } from "@/lib/rfqs/service" -import { searchParamsCBECache, } from "@/lib/rfqs/validations" -import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" -import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsCBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getCBE({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Commercial Bid Evaluation -

-

- 초대된 협력업체에게 CBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary/[id]/layout.tsx b/app/[lng]/sales/(sales)/budgetary/[id]/layout.tsx deleted file mode 100644 index d58d8363..00000000 --- a/app/[lng]/sales/(sales)/budgetary/[id]/layout.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { Metadata } from "next" -import Link from "next/link" -import { ArrowLeft } from "lucide-react" -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { RfqViewWithItems } from "@/db/schema/rfq" -import { findRfqById } from "@/lib/rfqs/service" -import { formatDate } from "@/lib/utils" -import { Button } from "@/components/ui/button" - -export const metadata: Metadata = { - title: "Vendor Detail", -} - -export default async function RfqLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string, id: string } -}) { - - // 1) URL 파라미터에서 id 추출, Number로 변환 - const resolvedParams = await params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - // 2) DB에서 해당 협력업체 정보 조회 - const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) - - // 3) 사이드바 메뉴 - const sidebarNavItems = [ - { - title: "Matched Vendors", - href: `/${lng}/evcp/budgetary/${id}`, - }, - { - title: "TBE", - href: `/${lng}/evcp/budgetary/${id}/tbe`, - }, - { - title: "CBE", - href: `/${lng}/evcp/budgetary/${id}/cbe`, - }, - ] - - return ( - <> -
-
-
- {/* RFQ 목록으로 돌아가는 링크 추가 */} -
- - - -
- -
- {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} -

- {rfq - ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` - : "Loading RFQ..."} -

- -

- {rfq - ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` - : ""} -

-

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate, "KR")}}

-
- -
- -
{children}
-
-
-
-
- - ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary/[id]/page.tsx b/app/[lng]/sales/(sales)/budgetary/[id]/page.tsx deleted file mode 100644 index dd9df563..00000000 --- a/app/[lng]/sales/(sales)/budgetary/[id]/page.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getMatchedVendors } from "@/lib/rfqs/service" -import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" -import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" -import { RfqType } from "@/lib/rfqs/validations" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise - rfqType: RfqType -} - -export default async function RfqPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - const searchParams = await props.searchParams - const search = searchParamsMatchedVCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getMatchedVendors({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Vendors -

-

- 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary/[id]/tbe/page.tsx b/app/[lng]/sales/(sales)/budgetary/[id]/tbe/page.tsx deleted file mode 100644 index ec894e1c..00000000 --- a/app/[lng]/sales/(sales)/budgetary/[id]/tbe/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getTBE } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTBE({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Technical Bid Evaluation -

-

- 초대된 협력업체에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary/page.tsx b/app/[lng]/sales/(sales)/budgetary/page.tsx deleted file mode 100644 index 15b4cdd4..00000000 --- a/app/[lng]/sales/(sales)/budgetary/page.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { searchParamsCache } from "@/lib/rfqs/validations" -import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" -import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" -import { getAllItems } from "@/lib/items/service" -import { RfqType } from "@/lib/rfqs/validations" -import { Ellipsis } from "lucide-react" - -interface RfqPageProps { - searchParams: Promise; - rfqType: RfqType; - title: string; - description: string; -} - -export default async function RfqPage({ - searchParams, - rfqType = RfqType.BUDGETARY, - title = "Budgetary Quote", - description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." -}: RfqPageProps) { - const search = searchParamsCache.parse(await searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRfqs({ - ...search, - filters: validFilters, - rfqType // 전달받은 rfqType 사용 - }), - getRfqStatusCounts(rfqType), // rfqType 전달 - getAllItems() - ]) - - return ( - -
-
-
-

- {title} -

- {/*

- {description} - 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, - - - 버튼 - 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/dashboard/page.tsx b/app/[lng]/sales/(sales)/dashboard/page.tsx deleted file mode 100644 index 1d61dc16..00000000 --- a/app/[lng]/sales/(sales)/dashboard/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -// app/invalid-access/page.tsx - -export default function InvalidAccessPage() { - return ( -
-

부적절한 접근입니다

-

- 협력업체(Vendor)가 EVCP 화면에 접속하거나
- SHI 계정이 협력업체 화면에 접속하려고 시도하는 경우입니다. -

-

- 접근 권한이 없으므로, 다른 화면으로 이동해 주세요. -

-
- ); - } - \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/esg-check-list/page.tsx b/app/[lng]/sales/(sales)/esg-check-list/page.tsx deleted file mode 100644 index dd97c74c..00000000 --- a/app/[lng]/sales/(sales)/esg-check-list/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getEsgEvaluations } from "@/lib/esg-check-list/service" -import { getEsgEvaluationsSchema } from "@/lib/esg-check-list/validation" -import { EsgEvaluationsTable } from "@/lib/esg-check-list/table/esg-table" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = getEsgEvaluationsSchema.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getEsgEvaluations({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- ESG 자가진단평가 문항 관리 -

- {/*

- 협력업체 평가에 사용되는 ESG 자가진단표를 관리{" "} - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/sales/(sales)/evaluation-check-list/page.tsx b/app/[lng]/sales/(sales)/evaluation-check-list/page.tsx deleted file mode 100644 index 34409524..00000000 --- a/app/[lng]/sales/(sales)/evaluation-check-list/page.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* IMPORT */ -import { DataTableSkeleton } from '@/components/data-table/data-table-skeleton'; -import { getRegEvalCriteria } from '@/lib/evaluation-criteria/service'; -import { getValidFilters } from '@/lib/data-table'; -import RegEvalCriteriaTable from '@/lib/evaluation-criteria/table/reg-eval-criteria-table'; -import { searchParamsCache } from '@/lib/evaluation-criteria/validations'; -import { Shell } from '@/components/shell'; -import { Skeleton } from '@/components/ui/skeleton'; -import { Suspense } from 'react'; -import { type SearchParams } from '@/types/table'; - -// ---------------------------------------------------------------------------------------------------- - -/* TYPES */ -interface EvaluationCriteriaPageProps { - searchParams: Promise -} - -// ---------------------------------------------------------------------------------------------------- - -/* REGULAR EVALUATION CRITERIA PAGE */ -async function EvaluationCriteriaPage(props: EvaluationCriteriaPageProps) { - const searchParams = await props.searchParams; - const search = searchParamsCache.parse(searchParams); - const validFilters = getValidFilters(search.filters); - const promises = Promise.all([ - getRegEvalCriteria({ - ...search, - filters: validFilters, - }), - ]); - - return ( - -
-
-
-

- 협력업체 평가기준표 관리 -

- {/*

- 협력업체 평가에 사용되는 평가기준표를 관리{" "} - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} - -// ---------------------------------------------------------------------------------------------------- - -/* EXPORT */ -export default EvaluationCriteriaPage; \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/evaluation-target-list/page.tsx b/app/[lng]/sales/(sales)/evaluation-target-list/page.tsx deleted file mode 100644 index 56b8ecef..00000000 --- a/app/[lng]/sales/(sales)/evaluation-target-list/page.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import * as React from "react" -import { Metadata } from "next" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { HelpCircle } from "lucide-react" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" - -import { getDefaultEvaluationYear, searchParamsEvaluationTargetsCache } from "@/lib/evaluation-target-list/validation" -import { getEvaluationTargets } from "@/lib/evaluation-target-list/service" -import { EvaluationTargetsTable } from "@/lib/evaluation-target-list/table/evaluation-target-table" - -export const metadata: Metadata = { - title: "협력업체 평가 대상 관리", - description: "협력업체 평가 대상을 확정하고 담당자를 지정합니다.", -} - -interface EvaluationTargetsPageProps { - searchParams: Promise -} - - - -export default async function EvaluationTargetsPage(props: EvaluationTargetsPageProps) { - const searchParams = await props.searchParams - const search = searchParamsEvaluationTargetsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // 기본 필터 처리 (통일된 이름 사용) - let basicFilters = [] - if (search.basicFilters && search.basicFilters.length > 0) { - basicFilters = search.basicFilters - console.log("Using search.basicFilters:", basicFilters); - } else { - console.log("No basic filters found"); - } - - // 모든 필터를 합쳐서 처리 - const allFilters = [...validFilters, ...basicFilters] - - // 조인 연산자도 통일된 이름 사용 - const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; - - // 현재 평가년도 (필터에서 가져오거나 기본값 사용) - const currentEvaluationYear = search.evaluationYear || getDefaultEvaluationYear() - - // Promise.all로 감싸서 전달 - const promises = Promise.all([ - getEvaluationTargets({ - ...search, - filters: allFilters, - joinOperator, - }) - ]) - - return ( - - {/* 간소화된 헤더 */} -
-
-
-

- 협력업체 평가 대상 관리 -

- - {currentEvaluationYear}년도 - - -
-
-
- - {/* 메인 테이블 (통계는 테이블 내부로 이동) */} - - } - > - {currentEvaluationYear && - -} - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/evaluation/page.tsx b/app/[lng]/sales/(sales)/evaluation/page.tsx deleted file mode 100644 index 2d8cbed7..00000000 --- a/app/[lng]/sales/(sales)/evaluation/page.tsx +++ /dev/null @@ -1,181 +0,0 @@ -// ================================================================ -// 4. PERIODIC EVALUATIONS PAGE -// ================================================================ - -import * as React from "react" -import { Metadata } from "next" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { HelpCircle } from "lucide-react" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { PeriodicEvaluationsTable } from "@/lib/evaluation/table/evaluation-table" -import { getPeriodicEvaluations } from "@/lib/evaluation/service" -import { searchParamsEvaluationsCache } from "@/lib/evaluation/validation" - -export const metadata: Metadata = { - title: "협력업체 정기평가", - description: "협력업체 정기평가 진행 현황을 관리합니다.", -} - -interface PeriodicEvaluationsPageProps { - searchParams: Promise -} - -// 프로세스 안내 팝오버 컴포넌트 -function ProcessGuidePopover() { - return ( - - - - - -
-
-

정기평가 프로세스

- {/*

- 확정된 평가 대상 업체들에 대한 정기평가 절차입니다. -

*/} -
-
-
-
- 1 -
-
-

평가 대상 확정

-

평가 대상으로 확정된 업체들의 정기평가가 자동 생성됩니다.

-
-
-
-
- 2 -
-
-

업체 자료 제출

-

각 업체는 평가에 필요한 자료를 제출 마감일까지 제출해야 합니다.

-
-
-
-
- 3 -
-
-

평가자 검토

-

지정된 평가자들이 평가표를 기반으로 점수를 매기고 검토합니다.

-
-
-
-
- 4 -
-
-

최종 확정

-

모든 평가가 완료되면 최종 점수와 등급이 확정됩니다.

-
-
-
-
-
-
- ) -} - -// TODO: 이 함수들은 실제 서비스 파일에서 구현해야 함 -function getDefaultEvaluationYear() { - return new Date().getFullYear() -} - - - -export default async function PeriodicEvaluationsPage(props: PeriodicEvaluationsPageProps) { - const searchParams = await props.searchParams - const search = searchParamsEvaluationsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters || []) - - // 기본 필터 처리 - let basicFilters = [] - if (search.basicFilters && search.basicFilters.length > 0) { - basicFilters = search.basicFilters - } - - // 모든 필터를 합쳐서 처리 - const allFilters = [...validFilters, ...basicFilters] - - // 조인 연산자 - const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; - - // 현재 평가년도 - const currentEvaluationYear = search.evaluationYear || getDefaultEvaluationYear() - - // Promise.all로 감싸서 전달 - const promises = Promise.all([ - getPeriodicEvaluations({ - ...search, - filters: allFilters, - joinOperator, - }) - ]) - - return ( - - {/* 헤더 */} -
-
-
-

- 협력업체 정기평가 -

- - {currentEvaluationYear}년도 - -
-
-
- - {/* 메인 테이블 */} - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/faq/manage/actions.ts b/app/[lng]/sales/(sales)/faq/manage/actions.ts deleted file mode 100644 index bc443a8a..00000000 --- a/app/[lng]/sales/(sales)/faq/manage/actions.ts +++ /dev/null @@ -1,48 +0,0 @@ -'use server'; - -import { promises as fs } from 'fs'; -import path from 'path'; -import { FaqCategory } from '@/components/faq/FaqCard'; -import { fallbackLng } from '@/i18n/settings'; - -const FAQ_CONFIG_PATH = path.join(process.cwd(), 'config', 'faqDataConfig.ts'); - -export async function updateFaqData(lng: string, newData: FaqCategory[]) { - try { - const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8'); - const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/); - if (!dataMatch) { - throw new Error('FAQ 데이터 형식이 올바르지 않습니다.'); - } - - const allData = eval(`(${dataMatch[1]})`); - const updatedData = { - ...allData, - [lng]: newData - }; - - const newFileContent = `import { FaqCategory } from "@/components/faq/FaqCard";\n\ninterface LocalizedFaqCategories {\n [lng: string]: FaqCategory[];\n}\n\nexport const faqCategories: LocalizedFaqCategories = ${JSON.stringify(updatedData, null, 4)};`; - await fs.writeFile(FAQ_CONFIG_PATH, newFileContent, 'utf-8'); - - return { success: true }; - } catch (error) { - console.error('FAQ 데이터 업데이트 중 오류 발생:', error); - return { success: false, error: '데이터 업데이트 중 오류가 발생했습니다.' }; - } -} - -export async function getFaqData(lng: string): Promise<{ data: FaqCategory[] }> { - try { - const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8'); - const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/); - if (!dataMatch) { - throw new Error('FAQ 데이터 형식이 올바르지 않습니다.'); - } - - const allData = eval(`(${dataMatch[1]})`); - return { data: allData[lng] || allData[fallbackLng] || [] }; - } catch (error) { - console.error('FAQ 데이터 읽기 중 오류 발생:', error); - return { data: [] }; - } -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/faq/manage/page.tsx b/app/[lng]/sales/(sales)/faq/manage/page.tsx deleted file mode 100644 index 011bbfa4..00000000 --- a/app/[lng]/sales/(sales)/faq/manage/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FaqManager } from '@/components/faq/FaqManager'; -import { getFaqData, updateFaqData } from './actions'; -import { revalidatePath } from 'next/cache'; -import { FaqCategory } from '@/components/faq/FaqCard'; - -interface Props { - params: { - lng: string; - } -} - -export default async function FaqManagePage(props: Props) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const { data } = await getFaqData(lng); - - async function handleSave(newData: FaqCategory[]) { - 'use server'; - await updateFaqData(lng, newData); - revalidatePath(`/${lng}/evcp/faq`); - } - - return ( -
-
-
-
-

FAQ Management

-

- Manage FAQ categories and items for {lng.toUpperCase()} language. -

-
- -
-
-
- ); -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/faq/page.tsx b/app/[lng]/sales/(sales)/faq/page.tsx deleted file mode 100644 index 00956591..00000000 --- a/app/[lng]/sales/(sales)/faq/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { faqCategories } from "@/config/faqDataConfig" -import { FaqCard } from "@/components/faq/FaqCard" -import { Button } from "@/components/ui/button" -import { Settings } from "lucide-react" -import Link from "next/link" -import { fallbackLng } from "@/i18n/settings" - -interface Props { - params: { - lng: string; - } -} - -export default async function FaqPage(props: Props) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const localizedFaqCategories = faqCategories[lng] || faqCategories[fallbackLng]; - - return ( -
-
-
-
-
-

FAQ

- {/*

- Find answers to common questions about using the EVCP system. -

*/} -
- - - -
- - - - - {localizedFaqCategories.map((category) => ( - - {category.label} - - ))} - - - {localizedFaqCategories.map((category) => ( - - {category.items.map((item, index) => ( - - ))} - - ))} - -
-
-
- ) -} diff --git a/app/[lng]/sales/(sales)/items-tech/layout.tsx b/app/[lng]/sales/(sales)/items-tech/layout.tsx deleted file mode 100644 index d375059b..00000000 --- a/app/[lng]/sales/(sales)/items-tech/layout.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from "react" -import { ItemTechContainer } from "@/components/items-tech/item-tech-container" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" - -// Layout 컴포넌트는 서버 컴포넌트입니다 -export default function ItemsShipLayout({ - children, -}: { - children: React.ReactNode -}) { - // 아이템 타입 정의 - const itemTypes = [ - { id: "ship", name: "조선 아이템" }, - { id: "top", name: "해양 TOP" }, - { id: "hull", name: "해양 HULL" }, - ] - - return ( - - - } - > - - {children} - - - - ) -} diff --git a/app/[lng]/sales/(sales)/items-tech/page.tsx b/app/[lng]/sales/(sales)/items-tech/page.tsx deleted file mode 100644 index 55ac9c63..00000000 --- a/app/[lng]/sales/(sales)/items-tech/page.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { shipbuildingSearchParamsCache, offshoreTopSearchParamsCache, offshoreHullSearchParamsCache } from "@/lib/items-tech/validations" -import { getShipbuildingItems, getOffshoreTopItems, getOffshoreHullItems } from "@/lib/items-tech/service" -import { OffshoreTopTable } from "@/lib/items-tech/table/top/offshore-top-table" -import { OffshoreHullTable } from "@/lib/items-tech/table/hull/offshore-hull-table" - -// 대소문자 문제 해결 - 실제 파일명에 맞게 import -import { ItemsShipTable } from "@/lib/items-tech/table/ship/Items-ship-table" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage({ searchParams }: IndexPageProps) { - const params = await searchParams - const shipbuildingSearch = shipbuildingSearchParamsCache.parse(params) - const offshoreTopSearch = offshoreTopSearchParamsCache.parse(params) - const offshoreHullSearch = offshoreHullSearchParamsCache.parse(params) - const validShipbuildingFilters = getValidFilters(shipbuildingSearch.filters || []) - const validOffshoreTopFilters = getValidFilters(offshoreTopSearch.filters || []) - const validOffshoreHullFilters = getValidFilters(offshoreHullSearch.filters || []) - - - // URL에서 아이템 타입 가져오기 - const itemType = params.type || "ship" - - return ( -
- {itemType === "ship" && ( - result)} - /> - )} - - {itemType === "top" && ( - result)} - /> - )} - - {itemType === "hull" && ( - result)} - /> - )} -
- ) -} diff --git a/app/[lng]/sales/(sales)/items/page.tsx b/app/[lng]/sales/(sales)/items/page.tsx deleted file mode 100644 index f8d9a5b1..00000000 --- a/app/[lng]/sales/(sales)/items/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -// app/items/page.tsx (업데이트) -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/items/validations" -import { getItems } from "@/lib/items/service" -import { ItemsTable } from "@/lib/items/table/items-table" -import { ViewModeToggle } from "@/components/data-table/view-mode-toggle" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - // pageSize 기반으로 모드 자동 결정 - const isInfiniteMode = search.perPage >= 1_000_000 - - // 페이지네이션 모드일 때만 서버에서 데이터 가져오기 - // 무한 스크롤 모드에서는 클라이언트에서 SWR로 데이터 로드 - const promises = isInfiniteMode - ? undefined - : Promise.all([ - getItems(search), // searchParamsCache의 결과를 그대로 사용 - ]) - - return ( - -
-
-
-

- 패키지 넘버 -

- {/*

- S-EDP로부터 수신된 패키지 정보이며 PR 전 입찰, 견적에 사용되며 벤더 데이터, 문서와 연결됩니다. -

*/} -
-
- -
- - }> - {/* DateRangePicker 등 추가 컴포넌트 */} - - - - } - > - {/* 통합된 ItemsTable 컴포넌트 사용 */} - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/layout.tsx b/app/[lng]/sales/(sales)/layout.tsx deleted file mode 100644 index 82b53307..00000000 --- a/app/[lng]/sales/(sales)/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ReactNode } from 'react'; -import { Header } from '@/components/layout/Header'; -import { SiteFooter } from '@/components/layout/Footer'; - -export default function EvcpLayout({ children }: { children: ReactNode }) { - return ( -
- {/*
*/} -
-
-
- {children} -
-
- -
- ); -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/project-gtc/page.tsx b/app/[lng]/sales/(sales)/project-gtc/page.tsx deleted file mode 100644 index d5cb467a..00000000 --- a/app/[lng]/sales/(sales)/project-gtc/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getProjectGtcList } from "@/lib/project-gtc/service" -import { projectGtcSearchParamsSchema } from "@/lib/project-gtc/validations" -import { ProjectGtcTable } from "@/lib/project-gtc/table/project-gtc-table" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = projectGtcSearchParamsSchema.parse(searchParams) - - const promises = Promise.all([ - getProjectGtcList({ - page: search.page, - perPage: search.perPage, - search: search.search, - sort: search.sort, - }), - ]) - - return ( - -
-
-
-

- 프로젝트 GTC 관리 -

- {/*

- 프로젝트별 GTC(General Terms and Conditions) 파일을 관리할 수 있습니다. - 각 프로젝트마다 하나의 GTC 파일을 업로드할 수 있으며, 파일 업로드 시 기존 파일은 자동으로 교체됩니다. -

*/} -
-
-
- - }> - {/* 추가 기능이 필요하면 여기에 추가 */} - - - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/project-vendors/page.tsx b/app/[lng]/sales/(sales)/project-vendors/page.tsx deleted file mode 100644 index 525cff07..00000000 --- a/app/[lng]/sales/(sales)/project-vendors/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { ProjectAVLTable } from "@/lib/project-avl/table/proejctAVL-table" -import { getProjecTAVL } from "@/lib/project-avl/service" -import { searchProjectAVLParamsCache } from "@/lib/project-avl/validations" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchProjectAVLParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getProjecTAVL({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- 프로젝트 AVL 리스트 -

- {/*

- 프로젝트 PQ를 통과한 벤더의 리스트를 보여줍니다.{" "} - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/sales/(sales)/projects/page.tsx b/app/[lng]/sales/(sales)/projects/page.tsx deleted file mode 100644 index 649dd56f..00000000 --- a/app/[lng]/sales/(sales)/projects/page.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { ItemsTable } from "@/lib/items/table/items-table" -import { getProjectLists } from "@/lib/projects/service" -import { ProjectsTable } from "@/lib/projects/table/projects-table" -import { searchParamsProjectsCache } from "@/lib/projects/validation" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsProjectsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getProjectLists({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- 수행 프로젝트 리스트 from S-EDP -

- {/*

- S-EDP로부터 수신하는 프로젝트 리스트입니다. 향후 MDG로 전환됩니다.{" "} - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. -

*/} -
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/sales/(sales)/report/page.tsx b/app/[lng]/sales/(sales)/report/page.tsx deleted file mode 100644 index 152721cf..00000000 --- a/app/[lng]/sales/(sales)/report/page.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import * as React from "react"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Shell } from "@/components/shell"; -import { ErrorBoundary } from "@/components/error-boundary"; -import { getDashboardData } from "@/lib/dashboard/service"; -import { DashboardClient } from "@/lib/dashboard/dashboard-client"; - -// 데이터 fetch 시 비동기 함수 호출 후 await 하므로 static-pre-render 과정에서 dynamic-server-error 발생. -// 따라서, dynamic 속성을 force-dynamic 으로 설정하여 동적 렌더링 처리 -// getDashboardData 함수에 대한 Promise를 넘기는 식으로 수정하게 되면 force-dynamic 선언을 제거해도 됨. -export const dynamic = 'force-dynamic' - -export default async function IndexPage() { - // domain을 명시적으로 전달 - const domain = "sales"; - - try { - // 서버에서 직접 데이터 fetch - const dashboardData = await getDashboardData(domain); - - return ( - - - - ); - } catch (error) { - console.error("Dashboard data fetch error:", error); - return ( - -
-
-

데이터를 불러오는데 실패했습니다.

-

{error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다."}

-
-
-
- ); - } -} - -function DashboardSkeleton() { - return ( -
- {/* 헤더 스켈레톤 */} -
-
- - -
- -
- - {/* 요약 카드 스켈레톤 */} -
- {[...Array(4)].map((_, i) => ( -
-
- - -
- - -
- ))} -
- - {/* 차트 스켈레톤 */} -
- {[...Array(2)].map((_, i) => ( -
-
- - -
- -
- ))} -
- - {/* 탭 스켈레톤 */} -
- -
- {[...Array(6)].map((_, i) => ( -
- -
-
- - -
-
- - - -
- -
-
- ))} -
-
-
- ); -} diff --git a/app/[lng]/sales/(sales)/settings/layout.tsx b/app/[lng]/sales/(sales)/settings/layout.tsx deleted file mode 100644 index 6c380919..00000000 --- a/app/[lng]/sales/(sales)/settings/layout.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" - -export const metadata: Metadata = { - title: "Settings", - // description: "Advanced form example using react-hook-form and Zod.", -} - - -interface SettingsLayoutProps { - children: React.ReactNode - params: { lng: string } -} - -export default async function SettingsLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string } -}) { - const resolvedParams = await params - const lng = resolvedParams.lng - - - const sidebarNavItems = [ - - { - title: "Account", - href: `/${lng}/evcp/settings`, - }, - { - title: "Preferences", - href: `/${lng}/evcp/settings/preferences`, - } - - - ] - - - return ( - <> -
-
-
-
-

설정

- {/*

- Manage your account settings and preferences. -

*/} -
- -
- -
{children}
-
-
-
-
- - - - ) -} diff --git a/app/[lng]/sales/(sales)/settings/page.tsx b/app/[lng]/sales/(sales)/settings/page.tsx deleted file mode 100644 index eba5e948..00000000 --- a/app/[lng]/sales/(sales)/settings/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { AccountForm } from "@/components/settings/account-form" - -export default function SettingsAccountPage() { - return ( -
-
-

Account

- {/*

- Update your account settings. Set your preferred language and - timezone. -

*/} -
- - -
- ) -} diff --git a/app/[lng]/sales/(sales)/settings/preferences/page.tsx b/app/[lng]/sales/(sales)/settings/preferences/page.tsx deleted file mode 100644 index e2a88021..00000000 --- a/app/[lng]/sales/(sales)/settings/preferences/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { AppearanceForm } from "@/components/settings/appearance-form" - -export default function SettingsAppearancePage() { - return ( -
-
-

Preference

-

- Customize the preference of the app. -

-
- - -
- ) -} diff --git a/app/[lng]/sales/(sales)/system/admin-users/page.tsx b/app/[lng]/sales/(sales)/system/admin-users/page.tsx deleted file mode 100644 index 11a9e9fb..00000000 --- a/app/[lng]/sales/(sales)/system/admin-users/page.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { DateRangePicker } from "@/components/date-range-picker" -import { Separator } from "@/components/ui/separator" - -import { searchParamsCache } from "@/lib/admin-users/validations" -import { getAllCompanies, getAllRoles, getUserCountGroupByCompany, getUserCountGroupByRole, getUsers } from "@/lib/admin-users/service" -import { AdmUserTable } from "@/lib/admin-users/table/ausers-table" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function UserTable(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getUsers({ - ...search, - filters: validFilters, - }), - getUserCountGroupByCompany(), - getUserCountGroupByRole(), - getAllCompanies(), - getAllRoles() - ]) - - return ( - - } - > -
-
-

Vendor Admin User Management

-

- 협력업체의 유저 전체를 조회하고 어드민 유저를 생성할 수 있는 페이지입니다. 이곳에서 초기 유저를 생성시킬 수 있습니다.
생성 후에는 생성된 사용자의 이메일로 생성 통보 이메일이 발송되며 사용자는 이메일과 OTP로 로그인이 가능합니다. -

-
- - -
-
- - ) -} diff --git a/app/[lng]/sales/(sales)/system/layout.tsx b/app/[lng]/sales/(sales)/system/layout.tsx deleted file mode 100644 index 2776ed8b..00000000 --- a/app/[lng]/sales/(sales)/system/layout.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" - -export const metadata: Metadata = { - title: "System Setting", - // description: "Advanced form example using react-hook-form and Zod.", -} - - -interface SettingsLayoutProps { - children: React.ReactNode - params: { lng: string } -} - -export default async function SettingsLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string } -}) { - const resolvedParams = await params - const lng = resolvedParams.lng - - - const sidebarNavItems = [ - - { - title: "삼성중공업 사용자", - href: `/${lng}/evcp/system`, - }, - { - title: "Roles", - href: `/${lng}/evcp/system/roles`, - }, - { - title: "권한 통제", - href: `/${lng}/evcp/system/permissions`, - }, - { - title: "협력업체 사용자", - href: `/${lng}/evcp/system/admin-users`, - }, - - { - title: "비밀번호 정책", - href: `/${lng}/evcp/system/password-policy`, - }, - - ] - - - return ( - <> -
-
-
-
-

시스템 설정

- {/*

- 사용자, 롤, 접근 권한을 관리하세요. -

*/} -
- -
- -
{children}
-
-
-
-
- - - - ) -} diff --git a/app/[lng]/sales/(sales)/system/page.tsx b/app/[lng]/sales/(sales)/system/page.tsx deleted file mode 100644 index fe0a262c..00000000 --- a/app/[lng]/sales/(sales)/system/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import * as React from "react" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsCache } from "@/lib/admin-users/validations" -import { getAllRoles, getUsersEVCP } from "@/lib/users/service" -import { getUserCountGroupByRole } from "@/lib/admin-users/service" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { UserTable } from "@/lib/users/table/users-table" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function SystemUserPage(props: IndexPageProps) { - - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getUsersEVCP({ - ...search, - filters: validFilters, - }), - getUserCountGroupByRole(), - getAllRoles() - ]) - - return ( - - } - > -
-
-

SHI Users

-

- 시스템 전체 사용자들을 조회하고 관리할 수 있는 페이지입니다. 사용자에게 롤을 할당하는 것으로 메뉴별 권한을 관리할 수 있습니다. -

-
- - -
-
- - ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/system/password-policy/page.tsx b/app/[lng]/sales/(sales)/system/password-policy/page.tsx deleted file mode 100644 index 0f14fefe..00000000 --- a/app/[lng]/sales/(sales)/system/password-policy/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -// app/admin/password-policy/page.tsx - -import * as React from "react" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Separator } from "@/components/ui/separator" -import { Alert, AlertDescription } from "@/components/ui/alert" -import { AlertTriangle } from "lucide-react" -import SecuritySettingsTable from "@/components/system/passwordPolicy" -import { getSecuritySettings } from "@/lib/password-policy/service" - - -export default async function PasswordPolicyPage() { - try { - // 보안 설정 데이터 로드 - const securitySettings = await getSecuritySettings() - - return ( - - } - > -
-
-

협력업체 사용자 비밀번호 정책 설정

-

- 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다. -

-
- - -
-
- ) - } catch (error) { - console.error('Failed to load security settings:', error) - - return ( -
-
-

협력업체 사용자 비밀번호 정책 설정

-

- 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다. -

-
- - - - - 보안 설정을 불러오는 중 오류가 발생했습니다. 페이지를 새로고침하거나 관리자에게 문의하세요. - - -
- ) - } -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/system/permissions/page.tsx b/app/[lng]/sales/(sales)/system/permissions/page.tsx deleted file mode 100644 index 6aa2b693..00000000 --- a/app/[lng]/sales/(sales)/system/permissions/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import PermissionsTree from "@/components/system/permissionsTree" -import { Separator } from "@/components/ui/separator" - -export default function PermissionsPage() { - return ( -
-
-

Permissions

-

- Set permissions to the menu by Role -

-
- - -
- ) -} diff --git a/app/[lng]/sales/(sales)/system/roles/page.tsx b/app/[lng]/sales/(sales)/system/roles/page.tsx deleted file mode 100644 index fe074600..00000000 --- a/app/[lng]/sales/(sales)/system/roles/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Separator } from "@/components/ui/separator" - -import { searchParamsCache } from "@/lib/roles/validations" -import { searchParamsCache as searchParamsCache2 } from "@/lib/admin-users/validations" -import { RolesTable } from "@/lib/roles/table/roles-table" -import { getRolesWithCount } from "@/lib/roles/services" -import { getUsersAll } from "@/lib/users/service" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function UserTable(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - const search2 = searchParamsCache2.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRolesWithCount({ - ...search, - filters: validFilters, - }), - - - ]) - - - const promises2 = Promise.all([ - getUsersAll({ - ...search2, - filters: validFilters, - }, "evcp"), - ]) - - - return ( - - } - > -
-
-

Role Management

-

- 역할을 생성하고 역할에 유저를 할당할 수 있는 페이지입니다. 역할에 메뉴의 접근 권한 역시 할당할 수 있습니다. -

-
- - -
-
- - ) -} diff --git a/app/[lng]/sales/(sales)/tbe/page.tsx b/app/[lng]/sales/(sales)/tbe/page.tsx deleted file mode 100644 index 211cf376..00000000 --- a/app/[lng]/sales/(sales)/tbe/page.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getAllTBE } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { AllTbeTable } from "@/lib/tbe/table/tbe-table" -import { RfqType } from "@/lib/rfqs/validations" -import * as React from "react" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" - -interface IndexPageProps { - params: { - lng: string - } - searchParams: Promise -} - -// 타입별 페이지 설명 구성 (Budgetary 제외) -const typeConfig: Record = { - "purchase": { - title: "Purchase RFQ Technical Bid Evaluation", - description: "실제 구매 발주 전 가격 요청을 위한 TBE입니다.", - rfqType: RfqType.PURCHASE - }, - "purchase-budgetary": { - title: "Purchase Budgetary RFQ Technical Bid Evaluation", - description: "프로젝트 수주 후, 공식 입찰 전 예산 책정을 위한 TBE입니다.", - rfqType: RfqType.PURCHASE_BUDGETARY - } -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - - // URL 쿼리 파라미터에서 타입 추출 - const searchParams = await props.searchParams - // 기본값으로 'purchase' 사용 - const typeParam = searchParams?.type as string || 'purchase' - - // 유효한 타입인지 확인하고 기본값 설정 - const validType = Object.keys(typeConfig).includes(typeParam) ? typeParam : 'purchase' - const rfqType = typeConfig[validType].rfqType - - // SearchParams 파싱 (Zod) - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // 현재 선택된 타입의 데이터 로드 - const promises = Promise.all([ - getAllTBE({ - ...search, - filters: validFilters, - rfqType - }) - ]) - - // 페이지 경로 생성 함수 - 단순화 - const getTabUrl = (type: string) => { - return `/${lng}/evcp/tbe?type=${type}`; - } - - return ( - -
-
-
-

- TBE 관리 -

- {/*

- 초대된 협력업체에게 TBE를 보낼 수 있습니다.
- 체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. -

*/} -
-
-
- - {/* 타입 선택 탭 (Budgetary 제외) */} - - - - Purchase - - - Purchase Budgetary - - - -
-

- {typeConfig[validType].description} -

-
-
- - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-contact-possible-items/page.tsx b/app/[lng]/sales/(sales)/tech-contact-possible-items/page.tsx deleted file mode 100644 index 5bc36790..00000000 --- a/app/[lng]/sales/(sales)/tech-contact-possible-items/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Suspense } from "react" -import { SearchParams } from "@/types/table" -import { Shell } from "@/components/shell" -import { ContactPossibleItemsTable } from "@/lib/contact-possible-items/table/contact-possible-items-table" -import { getContactPossibleItems } from "@/lib/contact-possible-items/service" -import { searchParamsCache } from "@/lib/contact-possible-items/validations" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" - - -interface ContactPossibleItemsPageProps { - searchParams: Promise -} - -export default async function ContactPossibleItemsPage({ - searchParams, -}: ContactPossibleItemsPageProps) { - const resolvedSearchParams = await searchParams - const search = searchParamsCache.parse(resolvedSearchParams) - - const contactPossibleItemsPromise = getContactPossibleItems(search) - - return ( - -
-
-
-

- 담당자별 자재 관리 -

- {/*

- 기술영업 담당자별 자재를 관리합니다. -

*/} -
-
-
- - - - } - > - - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-project-avl/page.tsx b/app/[lng]/sales/(sales)/tech-project-avl/page.tsx deleted file mode 100644 index 4ce018cd..00000000 --- a/app/[lng]/sales/(sales)/tech-project-avl/page.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import * as React from "react" -import { redirect } from "next/navigation" -import { getServerSession } from "next-auth/next" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import { SearchParams } from "@/types/table" -import { searchParamsCache } from "@/lib/tech-project-avl/validations" -import { Skeleton } from "@/components/ui/skeleton" -import { Shell } from "@/components/shell" -import { AcceptedQuotationsTable } from "@/lib/tech-project-avl/table/accepted-quotations-table" -import { getAcceptedTechSalesVendorQuotations } from "@/lib/techsales-rfq/service" -import { getValidFilters } from "@/lib/data-table" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Ellipsis } from "lucide-react" -import { InformationButton } from "@/components/information/information-button" -export interface PageProps { - params: Promise<{ lng: string }> - searchParams: Promise -} - -export default async function AcceptedQuotationsPage({ - params, - searchParams, -}: PageProps) { - const { lng } = await params - - const session = await getServerSession(authOptions) - if (!session) { - redirect(`/${lng}/auth/signin`) - } - - const search = await searchParams - const { page, perPage, sort, filters, search: searchText } = searchParamsCache.parse(search) - const validFilters = getValidFilters(filters ?? []) - - const { data, pageCount } = await getAcceptedTechSalesVendorQuotations({ - page, - perPage: perPage ?? 10, - sort, - search: searchText, - filters: validFilters, - }) - - return ( - -
-
-
-
-

- 견적 Result 전송 -

- -
- {/*

- 기술영업 승인 견적서에 대한 요약 정보를 확인하고{" "} - - - 버튼 - - 을 통해 RFQ 코드, 설명, 업체명, 업체 코드 등의 상세 정보를 확인할 수 있습니다. -

*/} -
-
-
- - }> - {/* Date range picker can be added here if needed */} - - - - } - > - - -
- ) -} diff --git a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/layout.tsx b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/layout.tsx deleted file mode 100644 index 291cd630..00000000 --- a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/layout.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { findTechVendorById } from "@/lib/tech-vendors/service" -import { TechVendor } from "@/db/schema/techVendors" -import { Button } from "@/components/ui/button" -import { ArrowLeft } from "lucide-react" -import Link from "next/link" -export const metadata: Metadata = { - title: "Tech Vendor Detail", -} - -export default async function SettingsLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string , id: string} -}) { - - // 1) URL 파라미터에서 id 추출, Number로 변환 - const resolvedParams = await params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - // 2) DB에서 해당 협력업체 정보 조회 - const vendor: TechVendor | null = await findTechVendorById(idAsNumber) - - // 3) 사이드바 메뉴 - const sidebarNavItems = [ - { - title: "연락처", - href: `/${lng}/evcp/tech-vendors/${id}/info`, - }, - { - title: "RFQ 히스토리", - href: `/${lng}/evcp/tech-vendors/${id}/info/rfq-history`, - }, - { - title: "자재 리스트", - href: `/${lng}/evcp/tech-vendors/${id}/info/possible-items`, - }, - ] - - return ( - <> -
-
-
- {/* RFQ 목록으로 돌아가는 링크 추가 */} -
- - - -
-
- {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} -

- {vendor - ? `${vendor.vendorCode ?? ""} - ${vendor.vendorName} 상세 정보` - : "Loading Vendor..."} -

-

기술영업 벤더 관련 상세사항을 확인하세요.

-
- -
- -
{children}
-
-
-
-
- - ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/page.tsx b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/page.tsx deleted file mode 100644 index 9969a801..00000000 --- a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { getTechVendorContacts } from "@/lib/tech-vendors/service" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsContactCache } from "@/lib/tech-vendors/validations" -import { TechVendorContactsTable } from "@/lib/tech-vendors/contacts-table/contact-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function SettingsAccountPage(props: IndexPageProps) { - const resolvedParams = await props.params - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsContactCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - - - const promises = Promise.all([ - getTechVendorContacts({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - // 4) 렌더링 - return ( -
-
-

- Contacts -

-

- 업무별 담당자 정보를 확인하세요. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/possible-items/page.tsx b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/possible-items/page.tsx deleted file mode 100644 index 642c6e32..00000000 --- a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/possible-items/page.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { getTechVendorPossibleItems } from "@/lib/tech-vendors/service" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsPossibleItemsCache } from "@/lib/tech-vendors/validations" -import { TechVendorPossibleItemsTable } from "@/lib/tech-vendors/possible-items/possible-items-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: Promise<{ - lng: string - id: string - }> - searchParams: Promise -} - -export default async function TechVendorPossibleItemsPage(props: IndexPageProps) { - const resolvedParams = await props.params - const id = resolvedParams.id - - const idAsNumber = Number(id) - console.log(idAsNumber) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 possible items 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsPossibleItemsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTechVendorPossibleItems({ - ...search, - filters: validFilters, - }, idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- 공급가능 아이템 목록 -

-

- 해당 벤더가 공급 가능한 아이템 목록을 확인할 수 있습니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/rfq-history/page.tsx b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/rfq-history/page.tsx deleted file mode 100644 index 9122d524..00000000 --- a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/rfq-history/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { TechVendorRfqHistoryTable } from "@/lib/tech-vendors/rfq-history-table/tech-vendor-rfq-history-table" -import { getTechVendorRfqHistory } from "@/lib/tech-vendors/service" -import { searchParamsRfqHistoryCache } from "@/lib/tech-vendors/validations" -import { Separator } from "@/components/ui/separator" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function SettingsAccountPage(props: IndexPageProps) { - const resolvedParams = await props.params - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsRfqHistoryCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - - - const promises = Promise.all([ - getTechVendorRfqHistory({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - return ( -
-
-

- RFQ 히스토리 -

-

- 벤더가 참여한 기술영업 RFQ 목록입니다. -

-
- -
- -
-
- - ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-vendors/page.tsx b/app/[lng]/sales/(sales)/tech-vendors/page.tsx deleted file mode 100644 index e49ba79e..00000000 --- a/app/[lng]/sales/(sales)/tech-vendors/page.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { searchParamsCache } from "@/lib/tech-vendors/validations" -import { getTechVendors, getTechVendorStatusCounts } from "@/lib/tech-vendors/service" -import { TechVendorsTable } from "@/lib/tech-vendors/table/tech-vendors-table" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTechVendors({ - ...search, - filters: validFilters, - }), - getTechVendorStatusCounts(), - ]) - - return ( - -
- {/* 왼쪽: 타이틀 & 설명 */} -
-
-

기술영업 협력업체 관리

- {/* InformationButton은 필요시 추가 */} - {/* */} -
- {/*

- 기술영업 벤더에 대한 요약 정보를 확인하고 관리할 수 있습니다. -

*/} -
-
- - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/vendor-candidates/page.tsx b/app/[lng]/sales/(sales)/vendor-candidates/page.tsx deleted file mode 100644 index f4bee95b..00000000 --- a/app/[lng]/sales/(sales)/vendor-candidates/page.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { getVendorCandidateCounts, getVendorCandidates } from "@/lib/vendor-candidates/service" -import { searchParamsCandidateCache } from "@/lib/vendor-candidates/validations" -import { VendorCandidateTable } from "@/lib/vendor-candidates/table/candidates-table" -import { DateRangePicker } from "@/components/date-range-picker" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCandidateCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getVendorCandidates({ - ...search, - filters: validFilters, - }), - getVendorCandidateCounts() - ]) - - return ( - - -
-
-
-

- 발굴업체 등록 관리 -

- {/*

- 수집한 협력업체 후보를 등록하고 초대 메일을 송부할 수 있습니다. -

*/} -
-
-
- - {/* 수집일 라벨과 DateRangePicker를 함께 배치 */} -
- {/* 수집일 기간 설정: */} - }> - - -
- - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/sales/page.tsx b/app/[lng]/sales/page.tsx deleted file mode 100644 index f9662cb7..00000000 --- a/app/[lng]/sales/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Metadata } from "next" -import { Suspense } from "react" -import { LoginFormSkeleton } from "@/components/login/login-form-skeleton" -import { LoginFormSHI } from "@/components/login/login-form-shi" - -export const metadata: Metadata = { - title: "eVCP Portal", - description: "", -} - -export default function AuthenticationPage() { - - - return ( - <> - }> - - - - ) -} diff --git a/app/api/procurement-rfqs/[rfqId]/vendors/[vendorId]/comments/route.ts b/app/api/procurement-rfqs/[rfqId]/vendors/[vendorId]/comments/route.ts deleted file mode 100644 index 51430118..00000000 --- a/app/api/procurement-rfqs/[rfqId]/vendors/[vendorId]/comments/route.ts +++ /dev/null @@ -1,145 +0,0 @@ -// app/api/procurement-rfqs/[rfqId]/vendors/[vendorId]/comments/route.ts -import { NextRequest, NextResponse } from "next/server" - -import db from '@/db/db'; -import { getServerSession } from "next-auth/next" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" - -import { procurementRfqComments, procurementRfqAttachments } from "@/db/schema" -import { revalidateTag } from "next/cache" - -// 파일 저장을 위한 유틸리티 -import { writeFile, mkdir } from 'fs/promises' -import { join } from 'path' -import crypto from 'crypto' - -/** - * 코멘트 생성 API 엔드포인트 - */ -export async function POST( - request: NextRequest, - { params }: { params: { rfqId: string; vendorId: string } } -) { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - if (!session?.user) { - return NextResponse.json( - { success: false, message: "인증이 필요합니다" }, - { status: 401 } - ) - } - - const rfqId = parseInt(params.rfqId) - const vendorId = parseInt(params.vendorId) - - // 유효성 검사 - if (isNaN(rfqId) || isNaN(vendorId)) { - return NextResponse.json( - { success: false, message: "유효하지 않은 매개변수입니다" }, - { status: 400 } - ) - } - - // FormData 파싱 - const formData = await request.formData() - const content = formData.get("content") as string - const isVendorComment = formData.get("isVendorComment") === "true" - const files = formData.getAll("attachments") as File[] - - if (!content && files.length === 0) { - return NextResponse.json( - { success: false, message: "내용이나 첨부파일이 필요합니다" }, - { status: 400 } - ) - } - - // 코멘트 생성 - const [comment] = await db - .insert(procurementRfqComments) - .values({ - rfqId, - vendorId, - userId: parseInt(session.user.id), - content, - isVendorComment, - isRead: !isVendorComment, // 본인 메시지는 읽음 처리 - createdAt: new Date(), - updatedAt: new Date(), - }) - .returning() - - // 첨부파일 처리 - const attachments = [] - if (files.length > 0) { - // 디렉토리 생성 - const uploadDir = join(process.cwd(), "public", `rfq-${rfqId}`, `vendor-${vendorId}`, `comment-${comment.id}`) - await mkdir(uploadDir, { recursive: true }) - - // 각 파일 저장 - for (const file of files) { - const buffer = Buffer.from(await file.arrayBuffer()) - const filename = `${Date.now()}-${crypto.randomBytes(8).toString("hex")}-${file.name.replace(/[^a-zA-Z0-9.-]/g, "_")}` - const filePath = join(uploadDir, filename) - - // 파일 쓰기 - await writeFile(filePath, buffer) - - // DB에 첨부파일 정보 저장 - const [attachment] = await db - .insert(procurementRfqAttachments) - .values({ - rfqId, - commentId: comment.id, - fileName: file.name, - fileSize: file.size, - fileType: file.type, - filePath: `/rfq-${rfqId}/vendor-${vendorId}/comment-${comment.id}/${filename}`, - isVendorUpload: isVendorComment, - uploadedBy: parseInt(session.user.id), - vendorId, - uploadedAt: new Date(), - }) - .returning() - - attachments.push({ - id: attachment.id, - fileName: attachment.fileName, - fileSize: attachment.fileSize, - fileType: attachment.fileType, - filePath: attachment.filePath, - uploadedAt: attachment.uploadedAt - }) - } - } - - // 캐시 무효화 - revalidateTag(`rfq-${rfqId}-comments`) - - // 응답 데이터 구성 - const responseData = { - id: comment.id, - rfqId: comment.rfqId, - vendorId: comment.vendorId, - userId: comment.userId, - content: comment.content, - isVendorComment: comment.isVendorComment, - createdAt: comment.createdAt, - updatedAt: comment.updatedAt, - userName: session.user.name, - attachments, - isRead: comment.isRead - } - - return NextResponse.json({ - success: true, - data: { comment: responseData } - }) - } catch (error) { - console.error("코멘트 생성 오류:", error) - return NextResponse.json( - { success: false, message: "코멘트 생성 중 오류가 발생했습니다" }, - { status: 500 } - ) - } -} \ No newline at end of file diff --git a/app/api/rfq-attachments/download/route.ts b/app/api/rfq-attachments/download/route.ts deleted file mode 100644 index 5a07bc0b..00000000 --- a/app/api/rfq-attachments/download/route.ts +++ /dev/null @@ -1,474 +0,0 @@ -// app/api/rfq-attachments/download/route.ts -import { NextRequest, NextResponse } from 'next/server'; -import { readFile, access, constants, stat } from 'fs/promises'; -import { join, normalize, resolve } from 'path'; -import db from '@/db/db'; -import { bRfqAttachmentRevisions, vendorResponseAttachmentsB } from '@/db/schema'; -import { eq } from 'drizzle-orm'; -import { getServerSession } from 'next-auth'; -import { authOptions } from '@/app/api/auth/[...nextauth]/route'; -import { createFileDownloadLog } from '@/lib/file-download-log/service'; -import rateLimit from '@/lib/rate-limit'; -import { z } from 'zod'; -import { getRequestInfo } from '@/lib/network/get-client-ip'; - -// 허용된 파일 확장자 -const ALLOWED_EXTENSIONS = new Set([ - 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', - 'txt', 'csv', 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', - 'dwg', 'dxf', 'zip', 'rar', '7z' -]); - -// 최대 파일 크기 (50MB) -const MAX_FILE_SIZE = 50 * 1024 * 1024; - -// 다운로드 요청 검증 스키마 -const downloadRequestSchema = z.object({ - path: z.string().min(1, 'File path is required'), - type: z.enum(['client', 'vendor']).optional(), - revisionId: z.string().optional(), - responseFileId: z.string().optional(), -}); - -// 파일 정보 타입 -interface FileRecord { - id: number; - fileName: string; - originalFileName?: string; - filePath: string; - fileSize: number; - fileType?: string; -} - -// 강화된 파일 경로 검증 함수 -function validateFilePath(filePath: string): boolean { - // null, undefined, 빈 문자열 체크 - if (!filePath || typeof filePath !== 'string') { - return false; - } - - // 위험한 패턴 체크 - const dangerousPatterns = [ - /\.\./, // 상위 디렉토리 접근 - /\/\//, // 이중 슬래시 - /[<>:"'|?*]/, // 특수문자 - /[\x00-\x1f]/, // 제어문자 - /\\+/ // 백슬래시 - ]; - - if (dangerousPatterns.some(pattern => pattern.test(filePath))) { - return false; - } - - // 시스템 파일 접근 방지 - const dangerousPaths = ['/etc', '/proc', '/sys', '/var', '/usr', '/root', '/home']; - for (const dangerousPath of dangerousPaths) { - if (filePath.toLowerCase().startsWith(dangerousPath)) { - return false; - } - } - - return true; -} - -// 파일 확장자 검증 -function validateFileExtension(fileName: string): boolean { - const extension = fileName.split('.').pop()?.toLowerCase() || ''; - return ALLOWED_EXTENSIONS.has(extension); -} - -// 안전한 파일명 생성 -function sanitizeFileName(fileName: string): string { - return fileName - .replace(/[^\w\s.-]/g, '_') // 안전하지 않은 문자 제거 - .replace(/\s+/g, '_') // 공백을 언더스코어로 - .substring(0, 255); // 파일명 길이 제한 -} - -export async function GET(request: NextRequest) { - const startTime = Date.now(); - const requestInfo = getRequestInfo(request); - let fileRecord: FileRecord | null = null; - - try { - // Rate limiting 체크 - const limiterResult = await rateLimit(request); - if (!limiterResult.success) { - console.warn('🚨 Rate limit 초과:', { - ip: requestInfo.ip, - userAgent: requestInfo.userAgent - }); - - return NextResponse.json( - { error: "Too many requests" }, - { status: 429 } - ); - } - - // 세션 확인 - const session = await getServerSession(authOptions); - if (!session?.user) { - console.warn('🚨 인증되지 않은 다운로드 시도:', { - ip: requestInfo.ip, - userAgent: requestInfo.userAgent, - path: request.nextUrl.searchParams.get("path") - }); - - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); - } - - // 파라미터 검증 - const searchParams = { - path: request.nextUrl.searchParams.get("path"), - type: request.nextUrl.searchParams.get("type"), - revisionId: request.nextUrl.searchParams.get("revisionId"), - responseFileId: request.nextUrl.searchParams.get("responseFileId"), - }; - - const validatedParams = downloadRequestSchema.parse(searchParams); - const { path, type, revisionId, responseFileId } = validatedParams; - - // 파일 경로 보안 검증 - if (!validateFilePath(path)) { - console.warn(`🚨 의심스러운 파일 경로 접근 시도: ${path}`, { - userId: session.user.id, - ip: requestInfo.ip, - userAgent: requestInfo.userAgent - }); - - return NextResponse.json( - { error: "Invalid file path" }, - { status: 400 } - ); - } - - // 경로 정규화 - const normalizedPath = normalize(path.replace(/^\/+/, "")); - - // DB에서 파일 정보 조회 - let dbRecord: FileRecord | null = null; - - if (type === "client" && revisionId) { - // 발주처 첨부파일 리비전 - const [record] = await db - .select({ - id: bRfqAttachmentRevisions.id, - fileName: bRfqAttachmentRevisions.fileName, - originalFileName: bRfqAttachmentRevisions.originalFileName, - filePath: bRfqAttachmentRevisions.filePath, - fileSize: bRfqAttachmentRevisions.fileSize, - fileType: bRfqAttachmentRevisions.fileType, - }) - .from(bRfqAttachmentRevisions) - .where(eq(bRfqAttachmentRevisions.id, Number(revisionId))); - - dbRecord = record; - - } else if (type === "vendor" && responseFileId) { - // 벤더 응답 파일 - const [record] = await db - .select({ - id: vendorResponseAttachmentsB.id, - fileName: vendorResponseAttachmentsB.fileName, - originalFileName: vendorResponseAttachmentsB.originalFileName, - filePath: vendorResponseAttachmentsB.filePath, - fileSize: vendorResponseAttachmentsB.fileSize, - fileType: vendorResponseAttachmentsB.fileType, - }) - .from(vendorResponseAttachmentsB) - .where(eq(vendorResponseAttachmentsB.id, Number(responseFileId))); - - dbRecord = record; - - } else { - // filePath로 직접 검색 (fallback) - 정규화된 경로로 검색 - const [clientRecord] = await db - .select({ - id: bRfqAttachmentRevisions.id, - fileName: bRfqAttachmentRevisions.fileName, - originalFileName: bRfqAttachmentRevisions.originalFileName, - filePath: bRfqAttachmentRevisions.filePath, - fileSize: bRfqAttachmentRevisions.fileSize, - fileType: bRfqAttachmentRevisions.fileType, - }) - .from(bRfqAttachmentRevisions) - .where(eq(bRfqAttachmentRevisions.filePath, normalizedPath)); - - if (clientRecord) { - dbRecord = clientRecord; - } else { - // 벤더 파일에서도 검색 - const [vendorRecord] = await db - .select({ - id: vendorResponseAttachmentsB.id, - fileName: vendorResponseAttachmentsB.fileName, - originalFileName: vendorResponseAttachmentsB.originalFileName, - filePath: vendorResponseAttachmentsB.filePath, - fileSize: vendorResponseAttachmentsB.fileSize, - fileType: vendorResponseAttachmentsB.fileType, - }) - .from(vendorResponseAttachmentsB) - .where(eq(vendorResponseAttachmentsB.filePath, normalizedPath)); - - dbRecord = vendorRecord; - } - } - - // DB에서 파일 정보를 찾지 못한 경우 - if (!dbRecord) { - console.warn("⚠️ DB에서 파일 정보를 찾지 못함:", { - path, - normalizedPath, - userId: session.user.id, - ip: requestInfo.ip - }); - - return NextResponse.json( - { error: "File not found in database" }, - { status: 404 } - ); - } - - fileRecord = dbRecord; - - // 파일명 설정 - const fileName = dbRecord.originalFileName || dbRecord.fileName; - - // 파일 확장자 검증 - if (!validateFileExtension(fileName)) { - console.warn(`🚨 허용되지 않은 파일 타입 다운로드 시도: ${fileName}`, { - userId: session.user.id, - ip: requestInfo.ip - }); - - // 실패 로그 기록 - await createFileDownloadLog({ - fileId: dbRecord.id, - success: false, - errorMessage: 'File type not allowed', - requestId: requestInfo.requestId, - fileInfo: { - fileName, - filePath: path, - fileSize: 0, - } - }); - - return NextResponse.json( - { error: "File type not allowed" }, - { status: 403 } - ); - } - - // 안전한 파일 경로 구성 - const allowedDirs = ["public", "uploads", "storage"]; - let actualPath: string | null = null; - let baseDir: string | null = null; - - // 각 허용된 디렉터리에서 파일 찾기 - for (const dir of allowedDirs) { - baseDir = resolve(process.cwd(), dir); - const testPath = resolve(baseDir, normalizedPath); - - // 경로 탐색 공격 방지 - 허용된 디렉터리 외부 접근 차단 - if (!testPath.startsWith(baseDir)) { - continue; - } - - try { - await access(testPath, constants.R_OK); - actualPath = testPath; - console.log("✅ 파일 발견:", testPath); - break; - } catch (err) { - // 조용히 다음 디렉터리 시도 - } - } - - if (!actualPath || !baseDir) { - console.error("❌ 모든 경로에서 파일을 찾을 수 없음:", { - normalizedPath, - userId: session.user.id, - requestedPath: path - }); - - // 실패 로그 기록 - await createFileDownloadLog({ - fileId: dbRecord.id, - success: false, - errorMessage: 'File not found on server', - requestId: requestInfo.requestId, - fileInfo: { - fileName, - filePath: path, - fileSize: dbRecord.fileSize || 0, - } - }); - - return NextResponse.json( - { error: "File not found on server" }, - { status: 404 } - ); - } - - // 파일 크기 확인 - const stats = await stat(actualPath); - if (stats.size > MAX_FILE_SIZE) { - console.warn(`🚨 파일 크기 초과: ${fileName} (${stats.size} bytes)`, { - userId: session.user.id, - ip: requestInfo.ip - }); - - // 실패 로그 기록 - await createFileDownloadLog({ - fileId: dbRecord.id, - success: false, - errorMessage: 'File too large', - requestId: requestInfo.requestId, - fileInfo: { - fileName, - filePath: path, - fileSize: stats.size, - } - }); - - return NextResponse.json( - { error: "File too large" }, - { status: 413 } - ); - } - - // 파일 읽기 - const fileBuffer = await readFile(actualPath); - - // MIME 타입 결정 - const fileExtension = fileName.split('.').pop()?.toLowerCase() || ''; - let contentType = dbRecord.fileType || 'application/octet-stream'; - - // 확장자에 따른 MIME 타입 매핑 (fallback) - if (!contentType || contentType === 'application/octet-stream') { - const mimeTypes: Record = { - 'pdf': 'application/pdf', - 'doc': 'application/msword', - 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'xls': 'application/vnd.ms-excel', - 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'ppt': 'application/vnd.ms-powerpoint', - 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'txt': 'text/plain; charset=utf-8', - 'csv': 'text/csv; charset=utf-8', - 'png': 'image/png', - 'jpg': 'image/jpeg', - 'jpeg': 'image/jpeg', - 'gif': 'image/gif', - 'bmp': 'image/bmp', - 'svg': 'image/svg+xml', - 'dwg': 'application/acad', - 'dxf': 'application/dxf', - 'zip': 'application/zip', - 'rar': 'application/x-rar-compressed', - '7z': 'application/x-7z-compressed', - }; - - contentType = mimeTypes[fileExtension] || 'application/octet-stream'; - } - - // 안전한 파일명 생성 - const safeFileName = sanitizeFileName(fileName); - - // 보안 헤더와 다운로드용 헤더 설정 - const headers = new Headers(); - headers.set('Content-Type', contentType); - headers.set('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(safeFileName)}`); - headers.set('Content-Length', fileBuffer.length.toString()); - - // 보안 헤더 - headers.set('Cache-Control', 'no-cache, no-store, must-revalidate'); - headers.set('Pragma', 'no-cache'); - headers.set('Expires', '0'); - headers.set('X-Content-Type-Options', 'nosniff'); - headers.set('X-Frame-Options', 'DENY'); - headers.set('X-XSS-Protection', '1; mode=block'); - headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); - - // 성공 로그 기록 - await createFileDownloadLog({ - fileId: dbRecord.id, - success: true, - requestId: requestInfo.requestId, - fileInfo: { - fileName: safeFileName, - filePath: path, - fileSize: fileBuffer.length, - } - }); - - console.log("✅ 파일 다운로드 성공:", { - fileName: safeFileName, - contentType, - size: fileBuffer.length, - actualPath, - userId: session.user.id, - ip: requestInfo.ip, - downloadDurationMs: Date.now() - startTime - }); - - return new NextResponse(fileBuffer, { - status: 200, - headers, - }); - - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - - console.error('❌ RFQ 첨부파일 다운로드 오류:', { - error: errorMessage, - userId: (await getServerSession(authOptions))?.user?.id, - ip: requestInfo.ip, - path: request.nextUrl.searchParams.get("path"), - downloadDurationMs: Date.now() - startTime - }); - - // 에러 로그 기록 - if (fileRecord?.id) { - try { - await createFileDownloadLog({ - fileId: fileRecord.id, - success: false, - errorMessage, - requestId: requestInfo.requestId, - fileInfo: { - fileName: fileRecord.fileName || 'unknown', - filePath: request.nextUrl.searchParams.get("path") || '', - fileSize: fileRecord.fileSize || 0, - } - }); - } catch (logError) { - console.error('로그 기록 실패:', logError); - } - } - - // Zod 검증 에러 처리 - if (error instanceof z.ZodError) { - return NextResponse.json( - { - error: 'Invalid request parameters', - details: error.errors.map(e => e.message).join(', ') - }, - { status: 400 } - ); - } - - // 에러 정보 최소화 (정보 노출 방지) - return NextResponse.json( - { - error: 'Internal server error', - details: process.env.NODE_ENV === 'development' ? errorMessage : undefined - }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/tbe-download/route.ts b/app/api/tbe-download/route.ts deleted file mode 100644 index 93eb62db..00000000 --- a/app/api/tbe-download/route.ts +++ /dev/null @@ -1,417 +0,0 @@ -// app/api/tbe-download/route.ts -import { NextRequest, NextResponse } from 'next/server'; -import { readFile, access, constants, stat } from 'fs/promises'; -import { join, normalize, resolve } from 'path'; -import db from '@/db/db'; -import { rfqAttachments, vendorResponseAttachments } from '@/db/schema/rfq'; -import { eq } from 'drizzle-orm'; -import { getServerSession } from 'next-auth'; -import { authOptions } from '@/app/api/auth/[...nextauth]/route'; -import { createFileDownloadLog } from '@/lib/file-download-log/service'; -import rateLimit from '@/lib/rate-limit'; -import { z } from 'zod'; -import { getRequestInfo } from '@/lib/network/get-client-ip'; - -// 허용된 파일 확장자 -const ALLOWED_EXTENSIONS = new Set([ - 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', - 'txt', 'csv', 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', - 'dwg', 'dxf', 'zip', 'rar', '7z' -]); - -// 최대 파일 크기 (50MB) -const MAX_FILE_SIZE = 50 * 1024 * 1024; - -// 다운로드 요청 검증 스키마 -const downloadRequestSchema = z.object({ - path: z.string().min(1, 'File path is required'), -}); - -// 파일 정보 타입 -interface FileRecord { - id: number; - fileName: string; - filePath: string; - fileSize?: number; - fileType?: string; -} - - -// 강화된 파일 경로 검증 함수 -function validateFilePath(filePath: string): boolean { - // null, undefined, 빈 문자열 체크 - if (!filePath || typeof filePath !== 'string') { - return false; - } - - // 위험한 패턴 체크 - const dangerousPatterns = [ - /\.\./, // 상위 디렉토리 접근 - /\/\//, // 이중 슬래시 - /[<>:"'|?*]/, // 특수문자 - /[\x00-\x1f]/, // 제어문자 - /\\+/ // 백슬래시 - ]; - - if (dangerousPatterns.some(pattern => pattern.test(filePath))) { - return false; - } - - // 시스템 파일 접근 방지 - const dangerousPaths = ['/etc', '/proc', '/sys', '/var', '/usr', '/root', '/home']; - for (const dangerousPath of dangerousPaths) { - if (filePath.toLowerCase().startsWith(dangerousPath)) { - return false; - } - } - - return true; -} - -// 파일 확장자 검증 -function validateFileExtension(fileName: string): boolean { - const extension = fileName.split('.').pop()?.toLowerCase() || ''; - return ALLOWED_EXTENSIONS.has(extension); -} - -// 안전한 파일명 생성 -function sanitizeFileName(fileName: string): string { - return fileName - .replace(/[^\w\s.-]/g, '_') // 안전하지 않은 문자 제거 - .replace(/\s+/g, '_') // 공백을 언더스코어로 - .substring(0, 255); // 파일명 길이 제한 -} - -export async function GET(request: NextRequest) { - const startTime = Date.now(); - const requestInfo = getRequestInfo(request); - let fileRecord: FileRecord | null = null; - - try { - // Rate limiting 체크 - const limiterResult = await rateLimit(request); - if (!limiterResult.success) { - console.warn('🚨 Rate limit 초과:', { - ip: requestInfo.ip, - userAgent: requestInfo.userAgent - }); - - return NextResponse.json( - { error: "Too many requests" }, - { status: 429 } - ); - } - - // 세션 확인 - const session = await getServerSession(authOptions); - if (!session?.user) { - console.warn('🚨 인증되지 않은 다운로드 시도:', { - ip: requestInfo.ip, - userAgent: requestInfo.userAgent, - path: request.nextUrl.searchParams.get("path") - }); - - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); - } - - // 파라미터 검증 - const searchParams = { - path: request.nextUrl.searchParams.get("path"), - }; - - const validatedParams = downloadRequestSchema.parse(searchParams); - const { path } = validatedParams; - - // 파일 경로 보안 검증 - if (!validateFilePath(path)) { - console.warn(`🚨 의심스러운 파일 경로 접근 시도: ${path}`, { - userId: session.user.id, - ip: requestInfo.ip, - userAgent: requestInfo.userAgent - }); - - return NextResponse.json( - { error: "Invalid file path" }, - { status: 400 } - ); - } - - // 경로 정규화 - const normalizedPath = normalize(path.replace(/^\/+/, "")); - - // DB에서 파일 정보 조회 (정확히 일치하는 filePath로 검색) - const [dbRecord] = await db - .select({ - id: vendorResponseAttachments.id, - fileName: vendorResponseAttachments.fileName, - filePath: vendorResponseAttachments.filePath, - fileType: vendorResponseAttachments.fileType, - }) - .from(vendorResponseAttachments) - .where(eq(vendorResponseAttachments.filePath, normalizedPath)); - - // DB에서 파일 정보를 찾지 못한 경우 - if (!dbRecord) { - console.warn("⚠️ DB에서 파일 정보를 찾지 못함:", { - path, - normalizedPath, - userId: session.user.id, - ip: requestInfo.ip - }); - - return NextResponse.json( - { error: "File not found in database" }, - { status: 404 } - ); - } - - fileRecord = dbRecord; - - // 파일명 설정 - const fileName = dbRecord.fileName; - - // 파일 확장자 검증 - if (!validateFileExtension(fileName)) { - console.warn(`🚨 허용되지 않은 파일 타입 다운로드 시도: ${fileName}`, { - userId: session.user.id, - ip: requestInfo.ip - }); - - // 실패 로그 기록 - await createFileDownloadLog({ - fileId: dbRecord.id, - success: false, - errorMessage: 'File type not allowed', - requestId: requestInfo.requestId, - fileInfo: { - fileName, - filePath: path, - fileSize: 0, - } - }); - - return NextResponse.json( - { error: "File type not allowed" }, - { status: 403 } - ); - } - - // 안전한 파일 경로 구성 - const allowedDirs = ["public", "uploads", "storage"]; - let actualPath: string | null = null; - let baseDir: string | null = null; - - // 각 허용된 디렉터리에서 파일 찾기 - for (const dir of allowedDirs) { - baseDir = resolve(process.cwd(), dir); - const testPath = resolve(baseDir, normalizedPath); - - // 경로 탐색 공격 방지 - 허용된 디렉터리 외부 접근 차단 - if (!testPath.startsWith(baseDir)) { - continue; - } - - try { - await access(testPath, constants.R_OK); - actualPath = testPath; - console.log("✅ 파일 발견:", testPath); - break; - } catch (err) { - console.log("❌ 경로에 파일 없음:", testPath); - } - } - - if (!actualPath || !baseDir) { - console.error("❌ 모든 경로에서 파일을 찾을 수 없음:", { - normalizedPath, - userId: session.user.id, - requestedPath: path, - triedDirs: allowedDirs - }); - - // 실패 로그 기록 - await createFileDownloadLog({ - fileId: dbRecord.id, - success: false, - errorMessage: 'File not found on server', - requestId: requestInfo.requestId, - fileInfo: { - fileName, - filePath: path, - fileSize: dbRecord.fileSize || 0, - } - }); - - return NextResponse.json( - { - error: "File not found on server", - details: { - path: path, - fileName: fileName, - } - }, - { status: 404 } - ); - } - - // 파일 크기 확인 - const stats = await stat(actualPath); - if (stats.size > MAX_FILE_SIZE) { - console.warn(`🚨 파일 크기 초과: ${fileName} (${stats.size} bytes)`, { - userId: session.user.id, - ip: requestInfo.ip - }); - - // 실패 로그 기록 - await createFileDownloadLog({ - fileId: dbRecord.id, - success: false, - errorMessage: 'File too large', - requestId: requestInfo.requestId, - fileInfo: { - fileName, - filePath: path, - fileSize: stats.size, - } - }); - - return NextResponse.json( - { error: "File too large" }, - { status: 413 } - ); - } - - // 파일 읽기 - const fileBuffer = await readFile(actualPath); - - // MIME 타입 결정 - const fileExtension = fileName.split('.').pop()?.toLowerCase() || ''; - let contentType = dbRecord.fileType || 'application/octet-stream'; - - // 확장자에 따른 MIME 타입 매핑 (fallback) - if (!contentType || contentType === 'application/octet-stream') { - const mimeTypes: Record = { - 'pdf': 'application/pdf', - 'doc': 'application/msword', - 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'xls': 'application/vnd.ms-excel', - 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'ppt': 'application/vnd.ms-powerpoint', - 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'txt': 'text/plain; charset=utf-8', - 'csv': 'text/csv; charset=utf-8', - 'png': 'image/png', - 'jpg': 'image/jpeg', - 'jpeg': 'image/jpeg', - 'gif': 'image/gif', - 'bmp': 'image/bmp', - 'svg': 'image/svg+xml', - 'dwg': 'application/acad', - 'dxf': 'application/dxf', - 'zip': 'application/zip', - 'rar': 'application/x-rar-compressed', - '7z': 'application/x-7z-compressed', - }; - - contentType = mimeTypes[fileExtension] || 'application/octet-stream'; - } - - // 안전한 파일명 생성 - const safeFileName = sanitizeFileName(fileName); - - // 보안 헤더와 다운로드용 헤더 설정 - const headers = new Headers(); - headers.set('Content-Type', contentType); - headers.set('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(safeFileName)}`); - headers.set('Content-Length', fileBuffer.length.toString()); - - // 보안 헤더 - headers.set('Cache-Control', 'no-cache, no-store, must-revalidate'); - headers.set('Pragma', 'no-cache'); - headers.set('Expires', '0'); - headers.set('X-Content-Type-Options', 'nosniff'); - headers.set('X-Frame-Options', 'DENY'); - headers.set('X-XSS-Protection', '1; mode=block'); - headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); - - // 성공 로그 기록 - await createFileDownloadLog({ - fileId: dbRecord.id, - success: true, - requestId: requestInfo.requestId, - fileInfo: { - fileName: safeFileName, - filePath: path, - fileSize: fileBuffer.length, - } - }); - - console.log("✅ TBE 파일 다운로드 성공:", { - fileName: safeFileName, - contentType, - size: fileBuffer.length, - actualPath, - userId: session.user.id, - ip: requestInfo.ip, - downloadDurationMs: Date.now() - startTime - }); - - return new NextResponse(fileBuffer, { - status: 200, - headers, - }); - - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - - console.error('❌ TBE 파일 다운로드 오류:', { - error: errorMessage, - userId: (await getServerSession(authOptions))?.user?.id, - ip: requestInfo.ip, - path: request.nextUrl.searchParams.get("path"), - downloadDurationMs: Date.now() - startTime - }); - - // 에러 로그 기록 - if (fileRecord?.id) { - try { - await createFileDownloadLog({ - fileId: fileRecord.id, - success: false, - errorMessage, - requestId: requestInfo.requestId, - fileInfo: { - fileName: fileRecord.fileName || 'unknown', - filePath: request.nextUrl.searchParams.get("path") || '', - fileSize: fileRecord.fileSize || 0, - } - }); - } catch (logError) { - console.error('로그 기록 실패:', logError); - } - } - - // Zod 검증 에러 처리 - if (error instanceof z.ZodError) { - return NextResponse.json( - { - error: 'Invalid request parameters', - details: error.errors.map(e => e.message).join(', ') - }, - { status: 400 } - ); - } - - // 에러 정보 최소화 (정보 노출 방지) - return NextResponse.json( - { - error: 'Internal server error', - details: process.env.NODE_ENV === 'development' ? errorMessage : undefined - }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/vendor-responses/update-comment/route.ts b/app/api/vendor-responses/update-comment/route.ts deleted file mode 100644 index f1e4c487..00000000 --- a/app/api/vendor-responses/update-comment/route.ts +++ /dev/null @@ -1,62 +0,0 @@ -// app/api/vendor-responses/update-comment/route.ts -import { NextRequest, NextResponse } from "next/server"; -import db from "@/db/db"; -import { vendorAttachmentResponses } from "@/db/schema"; - -import { getServerSession } from "next-auth/next" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import { eq } from "drizzle-orm"; - -export async function POST(request: NextRequest) { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json( - { message: "인증이 필요합니다." }, - { status: 401 } - ); - } - - const body = await request.json(); - const { responseId, responseComment, vendorComment } = body; - - if (!responseId) { - return NextResponse.json( - { message: "응답 ID가 필요합니다." }, - { status: 400 } - ); - } - - // 코멘트만 업데이트 - const [updatedResponse] = await db - .update(vendorAttachmentResponses) - .set({ - responseComment, - vendorComment, - updatedAt: new Date(), - updatedBy:Number(session?.user.id) - }) - .where(eq(vendorAttachmentResponses.id, parseInt(responseId))) - .returning(); - - if (!updatedResponse) { - return NextResponse.json( - { message: "응답을 찾을 수 없습니다." }, - { status: 404 } - ); - } - - return NextResponse.json({ - message: "코멘트가 성공적으로 업데이트되었습니다.", - response: updatedResponse, - }); - - } catch (error) { - console.error("Comment update error:", error); - return NextResponse.json( - { message: "코멘트 업데이트 중 오류가 발생했습니다." }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/vendor-responses/update/route.ts b/app/api/vendor-responses/update/route.ts deleted file mode 100644 index cf7e551c..00000000 --- a/app/api/vendor-responses/update/route.ts +++ /dev/null @@ -1,118 +0,0 @@ -// app/api/vendor-responses/update/route.ts -import { NextRequest, NextResponse } from "next/server"; -import db from "@/db/db"; -import { vendorAttachmentResponses } from "@/db/schema"; -import { eq } from "drizzle-orm"; -import { getServerSession } from "next-auth/next" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" - -// 리비전 번호를 증가시키는 헬퍼 함수 -function getNextRevision(currentRevision?: string): string { - if (!currentRevision) { - return "Rev.0"; // 첫 번째 응답 - } - - // "Rev.1" -> 1, "Rev.2" -> 2 형태로 숫자 추출 - const match = currentRevision.match(/Rev\.(\d+)/); - if (match) { - const currentNumber = parseInt(match[1]); - return `Rev.${currentNumber + 1}`; - } - - // 형식이 다르면 기본값 반환 - return "Rev.0"; -} - -export async function POST(request: NextRequest) { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json( - { message: "인증이 필요합니다." }, - { status: 401 } - ); - } - - const body = await request.json(); - const { - responseId, - responseStatus, - responseComment, - vendorComment, - respondedAt, - } = body; - - if (!responseId) { - return NextResponse.json( - { message: "응답 ID가 필요합니다." }, - { status: 400 } - ); - } - - // 1. 기존 응답 정보 조회 (현재 respondedRevision 확인) - const existingResponse = await db - .select() - .from(vendorAttachmentResponses) - .where(eq(vendorAttachmentResponses.id, parseInt(responseId))) - .limit(1); - - if (!existingResponse || existingResponse.length === 0) { - return NextResponse.json( - { message: "응답을 찾을 수 없습니다." }, - { status: 404 } - ); - } - - const currentResponse = existingResponse[0]; - - // 2. 벤더 응답 리비전 결정 - let nextRespondedRevision: string; - - - if (responseStatus === "RESPONDED") { - - // 첫 응답이거나 수정 요청 후 재응답인 경우 리비전 증가 - nextRespondedRevision = getNextRevision(currentResponse.respondedRevision); - - } else { - // WAIVED 등 다른 상태는 기존 리비전 유지 - nextRespondedRevision = currentResponse.respondedRevision || ""; - } - - // 3. vendor response 업데이트 - const [updatedResponse] = await db - .update(vendorAttachmentResponses) - .set({ - responseStatus, - respondedRevision: nextRespondedRevision, - responseComment, - vendorComment, - respondedAt: respondedAt ? new Date(respondedAt) : null, - updatedAt: new Date(), - updatedBy:Number(session?.user.id) - }) - .where(eq(vendorAttachmentResponses.id, parseInt(responseId))) - .returning(); - - if (!updatedResponse) { - return NextResponse.json( - { message: "응답 업데이트에 실패했습니다." }, - { status: 500 } - ); - } - - return NextResponse.json({ - message: "응답이 성공적으로 업데이트되었습니다.", - response: updatedResponse, - newRevision: nextRespondedRevision, // 새로운 리비전 정보 반환 - }); - - } catch (error) { - console.error("Response update error:", error); - return NextResponse.json( - { message: "응답 업데이트 중 오류가 발생했습니다." }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/vendor-responses/upload/route.ts b/app/api/vendor-responses/upload/route.ts deleted file mode 100644 index 111e4bd4..00000000 --- a/app/api/vendor-responses/upload/route.ts +++ /dev/null @@ -1,105 +0,0 @@ -// app/api/vendor-response-attachments/upload/route.ts -import { NextRequest, NextResponse } from "next/server"; -import { writeFile, mkdir } from "fs/promises"; -import { existsSync } from "fs"; -import path from "path"; -import db from "@/db/db"; -import { vendorResponseAttachmentsB } from "@/db/schema"; -import { getServerSession } from "next-auth/next" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" - -export async function POST(request: NextRequest) { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json( - { message: "인증이 필요합니다." }, - { status: 401 } - ); - } - - const formData = await request.formData(); - const responseId = formData.get("responseId") as string; - const file = formData.get("file") as File; - const description = formData.get("description") as string; - - if (!responseId) { - return NextResponse.json( - { message: "응답 ID가 필요합니다." }, - { status: 400 } - ); - } - - if (!file) { - return NextResponse.json( - { message: "파일이 선택되지 않았습니다." }, - { status: 400 } - ); - } - - // 파일 크기 검증 (10MB) - if (file.size > 10 * 1024 * 1024) { - return NextResponse.json( - { message: "파일이 너무 큽니다. (최대 10MB)" }, - { status: 400 } - ); - } - - // 업로드 디렉토리 생성 - const uploadDir = path.join( - process.cwd(), - "public", - "uploads", - "vendor-responses", - responseId - ); - - if (!existsSync(uploadDir)) { - await mkdir(uploadDir, { recursive: true }); - } - - // 고유한 파일명 생성 - const timestamp = Date.now(); - const sanitizedName = file.name.replace(/[^a-zA-Z0-9.-]/g, "_"); - const fileName = `${timestamp}_${sanitizedName}`; - const filePath = `/uploads/vendor-responses/${responseId}/${fileName}`; - const fullPath = path.join(uploadDir, fileName); - - // 파일 저장 - const buffer = Buffer.from(await file.arrayBuffer()); - await writeFile(fullPath, buffer); - - // DB에 파일 정보 저장 - const [insertedFile] = await db - .insert(vendorResponseAttachmentsB) - .values({ - vendorResponseId: parseInt(responseId), - fileName, - originalFileName: file.name, - filePath, - fileSize: file.size, - fileType: file.type || path.extname(file.name).slice(1), - description: description || null, - uploadedBy: parseInt(session.user.id), - }) - .returning(); - - return NextResponse.json({ - id: insertedFile.id, - fileName, - originalFileName: file.name, - filePath, - fileSize: file.size, - fileType: file.type || path.extname(file.name).slice(1), - message: "파일이 성공적으로 업로드되었습니다.", - }); - - } catch (error) { - console.error("File upload error:", error); - return NextResponse.json( - { message: "파일 업로드 중 오류가 발생했습니다." }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/vendor-responses/waive/route.ts b/app/api/vendor-responses/waive/route.ts deleted file mode 100644 index e732e8d2..00000000 --- a/app/api/vendor-responses/waive/route.ts +++ /dev/null @@ -1,69 +0,0 @@ -// app/api/vendor-responses/waive/route.ts -import { NextRequest, NextResponse } from "next/server"; -import db from "@/db/db"; -import { getServerSession } from "next-auth/next" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import { eq } from "drizzle-orm"; -import { vendorAttachmentResponses } from "@/db/schema"; - -export async function POST(request: NextRequest) { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json( - { message: "인증이 필요합니다." }, - { status: 401 } - ); - } - - const body = await request.json(); - const { responseId, responseComment, vendorComment } = body; - - if (!responseId) { - return NextResponse.json( - { message: "응답 ID가 필요합니다." }, - { status: 400 } - ); - } - - if (!responseComment) { - return NextResponse.json( - { message: "포기 사유를 입력해주세요." }, - { status: 400 } - ); - } - - // vendor response를 WAIVED 상태로 업데이트 - const [updatedResponse] = await db - .update(vendorAttachmentResponses) - .set({ - responseStatus: "WAIVED", - responseComment, - vendorComment, - respondedAt: new Date(), - updatedAt: new Date(), - }) - .where(eq(vendorAttachmentResponses.id, parseInt(responseId))) - .returning(); - - if (!updatedResponse) { - return NextResponse.json( - { message: "응답을 찾을 수 없습니다." }, - { status: 404 } - ); - } - - return NextResponse.json({ - message: "응답이 성공적으로 포기 처리되었습니다.", - response: updatedResponse, - }); - - } catch (error) { - console.error("Waive response error:", error); - return NextResponse.json( - { message: "응답 포기 처리 중 오류가 발생했습니다." }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/components/ProjectSelector.tsx b/components/ProjectSelector.tsx index 58fa2c23..45963d88 100644 --- a/components/ProjectSelector.tsx +++ b/components/ProjectSelector.tsx @@ -6,7 +6,14 @@ import { Button } from "@/components/ui/button" import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover" import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command" import { cn } from "@/lib/utils" -import { getProjects, type Project } from "@/lib/rfqs/service" +import { getProjects } from "@/lib/projects/service" + +export type Project = { + id: number; + projectCode: string; + projectName: string; + type: string; +} interface ProjectSelectorProps { selectedProjectId?: number | null; diff --git a/components/bidding/ProjectSelectorBid.tsx b/components/bidding/ProjectSelectorBid.tsx index a87c8dce..8a4b85af 100644 --- a/components/bidding/ProjectSelectorBid.tsx +++ b/components/bidding/ProjectSelectorBid.tsx @@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button" import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover" import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command" import { cn } from "@/lib/utils" -import { getProjects, type Project } from "@/lib/rfqs/service" +import { getProjects } from "@/lib/projects/service" interface ProjectSelectorProps { selectedProjectId?: number | null; @@ -16,6 +16,13 @@ interface ProjectSelectorProps { disabled?: boolean; } +export type Project = { + id: number; + projectCode: string; + projectName: string; + type: string; +} + export function ProjectSelector({ selectedProjectId, onProjectSelect, diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx index 0c83e858..2752948a 100644 --- a/components/layout/Header.tsx +++ b/components/layout/Header.tsx @@ -123,7 +123,60 @@ export function Header() { }, [pathname]); // 도메인별 메뉴 및 브랜딩 정보 가져오기 - const getDomainConfig = (pathname: string | null) => { + // session.user.domain이 있으면 그것을 우선적으로 따르고, 없으면 pathname을 따릅니다. + const userDomain = (session?.user as { domain?: string } | undefined)?.domain; + + const getDomainConfig = (pathname: string | null, domain?: string) => { + // 1. 세션 도메인 기반 설정 + if (domain) { + if (domain === "partners") { + return { + main: mainNavVendor, + additional: additionalNavVendor, + logoHref: `/${lng}/partners`, + brandNameKey: domainBrandingKeys.partners, + basePath: `/${lng}/partners` + }; + } + if (domain === "procurement") { + return { + main: procurementNav, + additional: additional2Nav, + logoHref: `/${lng}/procurement`, + brandNameKey: domainBrandingKeys.procurement, + basePath: `/${lng}/procurement` + }; + } + if (domain === "sales") { + return { + main: salesNav, + additional: additional2Nav, + logoHref: `/${lng}/sales`, + brandNameKey: domainBrandingKeys.sales, + basePath: `/${lng}/sales` + }; + } + if (domain === "engineering") { + return { + main: engineeringNav, + additional: additional2Nav, + logoHref: `/${lng}/engineering`, + brandNameKey: domainBrandingKeys.engineering, + basePath: `/${lng}/engineering` + }; + } + if (domain === "evcp") { + return { + main: mainNav, + additional: additionalNav, + logoHref: `/${lng}/evcp`, + brandNameKey: domainBrandingKeys.evcp, + basePath: `/${lng}/evcp` + }; + } + } + + // 2. 경로 기반 설정 (Fallback) if (pathname?.includes("/partners")) { return { main: mainNavVendor, @@ -174,7 +227,7 @@ export function Header() { }; }; - const { main: originalMain, additional: originalAdditional, logoHref, brandNameKey, basePath } = getDomainConfig(pathname); + const { main: originalMain, additional: originalAdditional, logoHref, brandNameKey, basePath } = getDomainConfig(pathname, userDomain); // partners 도메인 여부 확인 const isPartners = pathname?.includes("/partners") ?? false; diff --git a/config/menuConfig.ts b/config/menuConfig.ts index 860c2a88..76f1302e 100644 --- a/config/menuConfig.ts +++ b/config/menuConfig.ts @@ -221,24 +221,6 @@ export const mainNav: MenuSection[] = [ href: '/evcp/avl', descriptionKey: 'menu.vendor_management.avl_management_desc', }, - // 기존 project avl - // { - // titleKey: "menu.vendor_management.project_avl", - // href: "/evcp/project-vendors", - // descriptionKey: "menu.vendor_management.project_avl_desc", - // }, - { - titleKey: 'menu.vendor_management.legalReview', - href: '/evcp/legal-review', - // descriptionKey: "menu.vendor_management.legalReview_desc", - groupKey: 'groups.legal', - }, - { - titleKey: 'menu.vendor_management.legalResponse', - href: '/evcp/legal-response', - // descriptionKey: "menu.vendor_management.legalResponse_desc", - groupKey: 'groups.legal', - }, { titleKey: 'menu.vendor_management.risk_by_agency', href: '/evcp/risk-management', diff --git a/lib/b-rfq/attachment/add-attachment-dialog.tsx b/lib/b-rfq/attachment/add-attachment-dialog.tsx deleted file mode 100644 index 665e0f88..00000000 --- a/lib/b-rfq/attachment/add-attachment-dialog.tsx +++ /dev/null @@ -1,355 +0,0 @@ -"use client" - -import * as React from "react" -import { useForm } from "react-hook-form" -import { zodResolver } from "@hookform/resolvers/zod" -import { z } from "zod" -import { Plus ,X} from "lucide-react" -import { toast } from "sonner" - -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" -import { - Dropzone, - DropzoneDescription, - DropzoneInput, - DropzoneTitle, - DropzoneUploadIcon, - DropzoneZone, -} from "@/components/ui/dropzone" -import { - FileList, - FileListAction, - FileListDescription, - FileListHeader, - FileListIcon, - FileListInfo, - FileListItem, - FileListName, - FileListSize, -} from "@/components/ui/file-list" -import { Button } from "@/components/ui/button" -import { Textarea } from "@/components/ui/textarea" -import { addRfqAttachmentRecord } from "../service" - -// 첨부파일 추가 폼 스키마 (단일 파일) -const addAttachmentSchema = z.object({ - attachmentType: z.enum(["구매", "설계"], { - required_error: "문서 타입을 선택해주세요.", - }), - description: z.string().optional(), - file: z.instanceof(File, { - message: "파일을 선택해주세요.", - }), -}) - -type AddAttachmentFormData = z.infer - -interface AddAttachmentDialogProps { - rfqId: number -} - -export function AddAttachmentDialog({ rfqId }: AddAttachmentDialogProps) { - const [open, setOpen] = React.useState(false) - const [isSubmitting, setIsSubmitting] = React.useState(false) - const [uploadProgress, setUploadProgress] = React.useState(0) - - const form = useForm({ - resolver: zodResolver(addAttachmentSchema), - defaultValues: { - attachmentType: undefined, - description: "", - file: undefined, - }, - }) - - const selectedFile = form.watch("file") - - // 다이얼로그 닫기 핸들러 - const handleOpenChange = (newOpen: boolean) => { - if (!newOpen && !isSubmitting) { - form.reset() - } - setOpen(newOpen) - } - - // 파일 선택 처리 - const handleFileChange = (files: File[]) => { - if (files.length === 0) return - - const file = files[0] // 첫 번째 파일만 사용 - - // 파일 크기 검증 - const maxFileSize = 10 * 1024 * 1024 // 10MB - if (file.size > maxFileSize) { - toast.error(`파일이 너무 큽니다. (최대 10MB)`) - return - } - - form.setValue("file", file) - form.clearErrors("file") - } - - // 파일 제거 - const removeFile = () => { - form.resetField("file") - } - - // 파일 업로드 API 호출 - const uploadFile = async (file: File): Promise<{ - fileName: string - originalFileName: string - filePath: string - fileSize: number - fileType: string - }> => { - const formData = new FormData() - formData.append("rfqId", rfqId.toString()) - formData.append("file", file) - - const response = await fetch("/api/upload/rfq-attachment", { - method: "POST", - body: formData, - }) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.message || "파일 업로드 실패") - } - - return response.json() - } - - // 폼 제출 - const onSubmit = async (data: AddAttachmentFormData) => { - setIsSubmitting(true) - setUploadProgress(0) - - try { - // 1단계: 파일 업로드 - setUploadProgress(30) - const uploadedFile = await uploadFile(data.file) - - // 2단계: DB 레코드 생성 (시리얼 번호 자동 생성) - setUploadProgress(70) - const attachmentRecord = { - rfqId, - attachmentType: data.attachmentType, - description: data.description, - fileName: uploadedFile.fileName, - originalFileName: uploadedFile.originalFileName, - filePath: uploadedFile.filePath, - fileSize: uploadedFile.fileSize, - fileType: uploadedFile.fileType, - } - - const result = await addRfqAttachmentRecord(attachmentRecord) - - setUploadProgress(100) - - if (result.success) { - toast.success(result.message) - form.reset() - handleOpenChange(false) - } else { - toast.error(result.message) - } - - } catch (error) { - console.error("Upload error:", error) - toast.error(error instanceof Error ? error.message : "파일 업로드 중 오류가 발생했습니다.") - } finally { - setIsSubmitting(false) - setUploadProgress(0) - } - } - - return ( - - - - - - - - 새 첨부파일 추가 - - RFQ에 첨부할 문서를 업로드합니다. 시리얼 번호는 자동으로 부여됩니다. - - - -
- - {/* 문서 타입 선택 */} - ( - - 문서 타입 - - - - )} - /> - - {/* 설명 */} - ( - - 설명 (선택) - -