From 742290e829749b69ac4e5c2ff27c3d19c5097861 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 1 Jul 2025 11:43:28 +0000 Subject: (최겸) tiptap 의존성 추가 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 907 +++++++++++++++++++++++++++++++++++++++++++++++++++++- package.json | 19 ++ 2 files changed, 925 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index bbcd1ff6..7580549a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,6 +74,25 @@ "@radix-ui/react-tooltip": "^1.1.6", "@t3-oss/env-nextjs": "^0.11.1", "@tanstack/react-table": "^8.20.6", + "@tiptap/extension-blockquote": "^2.23.1", + "@tiptap/extension-bullet-list": "^2.23.1", + "@tiptap/extension-highlight": "^2.23.1", + "@tiptap/extension-image": "^2.23.1", + "@tiptap/extension-link": "^2.23.1", + "@tiptap/extension-list-item": "^2.23.1", + "@tiptap/extension-ordered-list": "^2.23.1", + "@tiptap/extension-subscript": "^2.23.1", + "@tiptap/extension-superscript": "^2.23.1", + "@tiptap/extension-table": "^2.23.1", + "@tiptap/extension-table-cell": "^2.23.1", + "@tiptap/extension-table-header": "^2.23.1", + "@tiptap/extension-table-row": "^2.23.1", + "@tiptap/extension-task-item": "^2.23.1", + "@tiptap/extension-task-list": "^2.23.1", + "@tiptap/extension-text-align": "^2.23.1", + "@tiptap/extension-underline": "^2.23.1", + "@tiptap/react": "^2.23.1", + "@tiptap/starter-kit": "^2.23.1", "@types/docusign-esign": "^5.19.8", "@types/formidable": "^3.4.5", "accept-language": "^3.0.20", @@ -4516,6 +4535,12 @@ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", "license": "MIT" }, + "node_modules/@remirror/core-constants": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", + "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -4611,6 +4636,566 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tiptap/core": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.23.1.tgz", + "integrity": "sha512-EURGKGsEPrwxvOPi9gA+BsczvsECJNV+xgTAGWHmEtU4YJ0AulYrCX3b7FK+aiduVhThIHDoG/Mmvmb/HPLRhQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.23.1.tgz", + "integrity": "sha512-GI3s+uFU88LWRaDG20Z9yIu2av3Usn8kw2lkm2ntwX1K6/mQBS/zkGhWr/FSwWOlMtTzYFxF4Ttb0e+hn67A/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.23.1.tgz", + "integrity": "sha512-OM4RxuZeOqpYRN1G/YpXSE8tZ3sVtT2XlO3qKa74qf+htWz8W3x4X0oQCrHrRTDSAA1wbmeZU3QghAIHnbvP/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.23.1.tgz", + "integrity": "sha512-tupuvrlZMTziVZXJuCVjLwllUnux/an9BtTYHpoRyLX9Hg0v7Kh39k9x58zJaoW8Q/Oc/qxPhbJpyOqhE1rLeg==", + "license": "MIT", + "dependencies": { + "tippy.js": "^6.3.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.23.1.tgz", + "integrity": "sha512-0g9U42m+boLJZP3x9KoJHDCp9WD5abaVdqNbTg9sFPDNsepb7Zaeu8AEB+yZLP/fuTI1I4ko6qkdr3UaaIYcmA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.23.1.tgz", + "integrity": "sha512-3IOdE40m0UTR2+UXui69o/apLtutAbtzfgmMxD6q0qlRvVqz99QEfk9RPHDNlUqJtYCL4TD+sj7UclBsDdgVXA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.23.1.tgz", + "integrity": "sha512-eYzJVUR13BhSE/TYAMZihGBId+XiwhnTPqGcSFo+zx89It/vxwDLvAUn0PReMNI7ULKPTw8orUt2fVKSarb2DQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.23.1.tgz", + "integrity": "sha512-2nkIkGVsaMJkpd024E6vXK+5XNz8VOVWp/pM6bbXpuv0HnGPrfLdh4ruuFc+xTQ3WPOmpSu8ygtujt4I1o9/6g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.23.1.tgz", + "integrity": "sha512-GyVp+o/RVrKlLdrQvtIpJGphFGogiPjcPCkAFcrfY1vDY1EYxfVZELC96gG1mUT1BO8FUD3hmbpkWi9l8/6O4A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.23.1.tgz", + "integrity": "sha512-GMWkpH+p/OUOk1Y5UGOnKuHSDEVBN7DhYIJiWt5g9LK/mpPeuqoCmQg3RQDgjtZXb74SlxLK2pS/3YcAnemdfQ==", + "license": "MIT", + "dependencies": { + "tippy.js": "^6.3.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.23.1.tgz", + "integrity": "sha512-iP+TiFIGZEbOvYAs04pI14mLI4xqbt64Da91TgMF1FNZUrG+9eWKjqbcHLQREuK3Qnjn5f0DI4nOBv61FlnPmA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.23.1.tgz", + "integrity": "sha512-YF66EVxnBxt1bHPx6fUUSSXK1Vg+/9baJ0AfJ12hCSPCgSjUclRuNmWIH5ikVfByOmPV1xlrN9wryLoSEBcNRQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.23.1.tgz", + "integrity": "sha512-5BPoli9wudiAOgSyK8309jyRhFyu5vd02lNChfpHwxUudzIJ/L+0E6FcwrDcw+yXh23cx7F5SSjtFQ7AobxlDQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-highlight": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-2.23.1.tgz", + "integrity": "sha512-cvfQ6Ise2aZp0eABRbKNfyfU1Fd304q8nAtAKDCRQKzGbSPTEM4zhCp1RcEmt/93I1cLvKaciQFBoldVl1xaGw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-history": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.23.1.tgz", + "integrity": "sha512-1rp2CRjM+P58oGEgeUUDSk0ch67ngIGbGJOOjiBGKU9GIVhI2j4uSwsYTAa9qYMjMUI6IyH1xJpsY2hLKcBOtg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.23.1.tgz", + "integrity": "sha512-uHEF0jpmhtgAxjKw8/s5ipEeTnu99f9RVMGAlmcthJ5Fx9TzH0MvtH4dtBNEu5MXC7+0bNsnncWo125AAbCohg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-image": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.23.1.tgz", + "integrity": "sha512-8j2FLBWKq6j27aQcOAlmyKixxHflW8j5FhxLgPPS6hithgFQVET4OYH+1c6r7Qd/T4YoAqt/0PmNZ/gYWI9gzg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.23.1.tgz", + "integrity": "sha512-a+cPzffaC/1AKMmZ1Ka6l81xmTgcalf8NXfBuFCUTf5r7uI9NIgXnLo9hg+jR9F4K+bwhC4/UbMvQQzAjh0c0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.23.1.tgz", + "integrity": "sha512-zMD0V8djkvwRYACzd8EvFXXNLQH5poJt6aHC9/8uest6njRhlrRjSjwG5oa+xHW4A76XfAH0A5BPj6ZxZnAUQg==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.23.1.tgz", + "integrity": "sha512-wVrRp6KAiyjFVFGmn+ojisP64Bsd+ZPdqQBYVbebBx1skZeW0uhG60d7vUkWHi0gCgxHZDfvDbXpfnOD0INRWw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.23.1.tgz", + "integrity": "sha512-Zp+qognyNgoaJ9bxkBwIuWJEnQ67RdsHXzv3YOdeGRbkUhd8LT6OL7P0mAuNbMBU8MwHxyJ7C7NsyzwzuVbFzA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.23.1.tgz", + "integrity": "sha512-LLEPizt1ALE7Ek6prlJ1uhoUCT8C/a3PdZpCh3DshM1L3Kv9TENlaJL2GhFl8SVUCwHmWHvXg30+4tIRFBedaQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.23.1.tgz", + "integrity": "sha512-hAT9peYkKezRGp/EcPQKtyYQT+2XGUbb26toTr9XIBQIeQCuCpT+FirPrDMrMVWPwcJt7Rv+AzoVjDuBs9wE0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-subscript": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-subscript/-/extension-subscript-2.23.1.tgz", + "integrity": "sha512-LFUwe90E90f38aES6ka0Jg7kUG3WTMq3M+7qRu6skEx4+izVB6ub5RTvA56trQlWefWiYeJZptf8xfIKdcwGSw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-superscript": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-superscript/-/extension-superscript-2.23.1.tgz", + "integrity": "sha512-us1p+AZD6B3+eWuhO83WP3Kd9MBSApOh5c4S9MEavmmQxHAvFk7sv/tuyQPgBQNxvNnpV5z0mfFEtqiMrCo22A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-table": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-2.23.1.tgz", + "integrity": "sha512-v8qRKIM74U51KOFK90tpN1Yy1wj7wceifbXXnwySN4H7s0jmOdN//u73QPjqkynWw9xKo/dRjSWqiEoSRhno1w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-table-cell": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-2.23.1.tgz", + "integrity": "sha512-znCUmoZAZvXEw9nqZCzfAC+m++t5PaEh0//cmCqSuvB43c6s2oEFg06JiJnrxWBe8kMZUyzyCHMMoPMLE2vaDQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-table-header": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-2.23.1.tgz", + "integrity": "sha512-LIdSdgLPvQ5fGZA3PtV1l6l3dAkgJ5OWsxM0srXBwteHDf9SHMlJ2VJ/X6YtYk3itLkLWeMpqDkPHw9Sq0lbVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-table-row": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-2.23.1.tgz", + "integrity": "sha512-zREF2+tXI8LMapTJVQtbj+SIdzi3hwFte9JugzTDym+16kAPU7Smjsi5wLa7Y1AD8M882OwRWZ9qkHL7L99eZQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-task-item": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-2.23.1.tgz", + "integrity": "sha512-v6lNiuKYlEUmoQq2ISzX3dH60ThuALXgKIZclrvVuQsYohHQ2A+5joKEzkdNoWmairNxouAfSa+1p+HEwDaORg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-task-list": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-task-list/-/extension-task-list-2.23.1.tgz", + "integrity": "sha512-JN5Fai/H4PoYaUoqbxoPzUwW9PmN+qmBvbzt3ciCJEG2lXlmF0Z5vGYU/bJ5013C7jTyPJlp0qE7sZAPdF3WdQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.23.1.tgz", + "integrity": "sha512-XK0D/eyS1Vm5yUrCtkS0AfgyKLJqpi8nJivCOux/JLhhC4x87R1+mI8NoFDYZJ5ic/afREPSBB8jORqOi0qIHg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-text-align": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-2.23.1.tgz", + "integrity": "sha512-AugfX5iJXM7RJfv/AoXfEmXw70ZFlAIRm0tdSxYwkHvt1f0fqdtdTsHth7OGPnudH91h4FoVgHBktcHcPpEUfg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-text-style": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.23.1.tgz", + "integrity": "sha512-fZn1GePlL27pUFfKXKoRZo4L4pZP9dUjNNiS/eltLpbi/SenJ15UKhAoHtN1KQvNGJsWkYN49FjnnltU8qvQ+Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.23.1.tgz", + "integrity": "sha512-MTG+VlGStXD3uj7iPzZU8aJrqxoQyX45WX6xTouezaZzh/NQtTyVWwJqNGE7fsMhxirpJ+at0IZmqlDTjAhEDQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/pm": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.23.1.tgz", + "integrity": "sha512-iAx4rP0k4Xd9Ywh+Gpaz5IWfY2CYRpiwVXWekTHLlNRFtrVIWVpMxaQr2mvRU2g0Ca6rz5w3KzkHBMqrI3dIBA==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.3.0", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.6.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.13.1", + "prosemirror-menu": "^1.2.4", + "prosemirror-model": "^1.23.0", + "prosemirror-schema-basic": "^1.2.3", + "prosemirror-schema-list": "^1.4.1", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.6.4", + "prosemirror-trailing-node": "^3.0.0", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.37.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.23.1.tgz", + "integrity": "sha512-eP8jksq9rY1PBIYKNUgG5c92gCTNK40bmzno+cEAu8RMYs5M6BSXwMaZSjjqHM53Juvj6ake90+7kLOM8XlXfA==", + "license": "MIT", + "dependencies": { + "@tiptap/extension-bubble-menu": "^2.23.1", + "@tiptap/extension-floating-menu": "^2.23.1", + "@types/use-sync-external-store": "^0.0.6", + "fast-deep-equal": "^3", + "use-sync-external-store": "^1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.23.1.tgz", + "integrity": "sha512-rrImwzJbKSHoFa+WdNU4I0evXcMiQ4yRm737sxvNJwYItT6fXIxrbRT7nJDmtYu2TflcfT1KklEnSrzz1hhYRw==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^2.23.1", + "@tiptap/extension-blockquote": "^2.23.1", + "@tiptap/extension-bold": "^2.23.1", + "@tiptap/extension-bullet-list": "^2.23.1", + "@tiptap/extension-code": "^2.23.1", + "@tiptap/extension-code-block": "^2.23.1", + "@tiptap/extension-document": "^2.23.1", + "@tiptap/extension-dropcursor": "^2.23.1", + "@tiptap/extension-gapcursor": "^2.23.1", + "@tiptap/extension-hard-break": "^2.23.1", + "@tiptap/extension-heading": "^2.23.1", + "@tiptap/extension-history": "^2.23.1", + "@tiptap/extension-horizontal-rule": "^2.23.1", + "@tiptap/extension-italic": "^2.23.1", + "@tiptap/extension-list-item": "^2.23.1", + "@tiptap/extension-ordered-list": "^2.23.1", + "@tiptap/extension-paragraph": "^2.23.1", + "@tiptap/extension-strike": "^2.23.1", + "@tiptap/extension-text": "^2.23.1", + "@tiptap/extension-text-style": "^2.23.1", + "@tiptap/pm": "^2.23.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -4788,6 +5373,28 @@ "@types/node": "*" } }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -4943,6 +5550,12 @@ "@types/node": "*" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/xml-encryption": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/xml-encryption/-/xml-encryption-1.2.4.tgz", @@ -5490,7 +6103,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-hidden": { @@ -6573,6 +7185,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-fetch": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", @@ -7437,6 +8055,18 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -10369,6 +10999,21 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/linkifyjs": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.1.tgz", + "integrity": "sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==", + "license": "MIT" + }, "node_modules/listenercount": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", @@ -10575,6 +11220,23 @@ "devOptional": true, "license": "ISC" }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, "node_modules/math-intrinsics": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz", @@ -10584,6 +11246,12 @@ "node": ">= 0.4" } }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/media-typer": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.2.0.tgz", @@ -11805,6 +12473,12 @@ "node": ">=14.6" } }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -12487,6 +13161,201 @@ "react-is": "^16.13.1" } }, + "node_modules/prosemirror-changeset": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz", + "integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-collab": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz", + "integrity": "sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.4.1.tgz", + "integrity": "sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.0.tgz", + "integrity": "sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-markdown": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz", + "integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==", + "license": "MIT", + "dependencies": { + "@types/markdown-it": "^14.0.0", + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-menu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz", + "integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==", + "license": "MIT", + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.1.tgz", + "integrity": "sha512-AUvbm7qqmpZa5d9fPKMvH1Q5bqYQvAZWOGRvxsB6iFLyycvC9MwNemNVjHVrWgjaoxAfY8XVg7DbvQ/qxvI9Eg==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz", + "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz", + "integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.7.1.tgz", + "integrity": "sha512-eRQ97Bf+i9Eby99QbyAiyov43iOKgWa7QCGly+lrDt7efZ1v8NWolhXiB43hSDGIXT1UXgbs4KJN3a06FGpr1Q==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.2", + "prosemirror-model": "^1.25.0", + "prosemirror-state": "^1.4.3", + "prosemirror-transform": "^1.10.3", + "prosemirror-view": "^1.39.1" + } + }, + "node_modules/prosemirror-trailing-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", + "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", + "license": "MIT", + "dependencies": { + "@remirror/core-constants": "3.0.0", + "escape-string-regexp": "^4.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1.22.1", + "prosemirror-state": "^1.4.2", + "prosemirror-view": "^1.33.8" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.4.tgz", + "integrity": "sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.40.0.tgz", + "integrity": "sha512-2G3svX0Cr1sJjkD/DYWSe3cfV5VPVTBOxI9XQEGWJDFEpsZb/gh4MV29ctv+OJx2RFX4BLt09i+6zaGM/ldkCw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.20.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "node_modules/proxy-addr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.1.tgz", @@ -12533,6 +13402,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -13171,6 +14049,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -14320,6 +15204,15 @@ "node": ">=0.4.0" } }, + "node_modules/tippy.js": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", + "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.9.0" + } + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -15062,6 +15955,12 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -15329,6 +16228,12 @@ "node": ">=0.10.0" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 4f957f3e..308f8259 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,25 @@ "@radix-ui/react-tooltip": "^1.1.6", "@t3-oss/env-nextjs": "^0.11.1", "@tanstack/react-table": "^8.20.6", + "@tiptap/extension-blockquote": "^2.23.1", + "@tiptap/extension-bullet-list": "^2.23.1", + "@tiptap/extension-highlight": "^2.23.1", + "@tiptap/extension-image": "^2.23.1", + "@tiptap/extension-link": "^2.23.1", + "@tiptap/extension-list-item": "^2.23.1", + "@tiptap/extension-ordered-list": "^2.23.1", + "@tiptap/extension-subscript": "^2.23.1", + "@tiptap/extension-superscript": "^2.23.1", + "@tiptap/extension-table": "^2.23.1", + "@tiptap/extension-table-cell": "^2.23.1", + "@tiptap/extension-table-header": "^2.23.1", + "@tiptap/extension-table-row": "^2.23.1", + "@tiptap/extension-task-item": "^2.23.1", + "@tiptap/extension-task-list": "^2.23.1", + "@tiptap/extension-text-align": "^2.23.1", + "@tiptap/extension-underline": "^2.23.1", + "@tiptap/react": "^2.23.1", + "@tiptap/starter-kit": "^2.23.1", "@types/docusign-esign": "^5.19.8", "@types/formidable": "^3.4.5", "accept-language": "^3.0.20", -- cgit v1.2.3 From a382208003044caa45bb1cecb67dade544d44ada Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 1 Jul 2025 11:44:40 +0000 Subject: (최겸) 인포메이션 컴포넌트 테이블 별 prop 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[lng]/evcp/(evcp)/b-rfq/page.tsx | 2 +- .../evcp/(evcp)/basic-contract-template/page.tsx | 2 +- app/[lng]/evcp/(evcp)/basic-contract/page.tsx | 2 +- app/[lng]/evcp/(evcp)/bid-projects/page.tsx | 2 +- app/[lng]/evcp/(evcp)/budgetary-rfq/page.tsx | 2 +- .../evcp/(evcp)/budgetary-tech-sales-hull/page.tsx | 2 +- .../evcp/(evcp)/budgetary-tech-sales-ship/page.tsx | 2 +- .../evcp/(evcp)/budgetary-tech-sales-top/page.tsx | 2 +- app/[lng]/evcp/(evcp)/email-template/page.tsx | 2 +- app/[lng]/evcp/(evcp)/equip-class/page.tsx | 2 +- app/[lng]/evcp/(evcp)/esg-check-list/page.tsx | 2 +- .../evcp/(evcp)/evaluation-check-list/page.tsx | 2 +- .../evcp/(evcp)/evaluation-target-list/page.tsx | 2 +- app/[lng]/evcp/(evcp)/evaluation/page.tsx | 2 +- app/[lng]/evcp/(evcp)/faq/page.tsx | 2 +- app/[lng]/evcp/(evcp)/form-list/page.tsx | 2 +- app/[lng]/evcp/(evcp)/incoterms/page.tsx | 2 +- app/[lng]/evcp/(evcp)/information/page.tsx | 35 +++-- app/[lng]/evcp/(evcp)/items/page.tsx | 2 +- app/[lng]/evcp/(evcp)/menu-access/page.tsx | 2 +- app/[lng]/evcp/(evcp)/menu-list/page.tsx | 2 +- app/[lng]/evcp/(evcp)/notice/page.tsx | 60 ++++++++ app/[lng]/evcp/(evcp)/payment-conditions/page.tsx | 2 +- app/[lng]/evcp/(evcp)/po-rfq/page.tsx | 2 +- app/[lng]/evcp/(evcp)/po/page.tsx | 2 +- app/[lng]/evcp/(evcp)/poa/page.tsx | 2 +- app/[lng]/evcp/(evcp)/pq-criteria/page.tsx | 2 +- app/[lng]/evcp/(evcp)/pq/page.tsx | 2 +- app/[lng]/evcp/(evcp)/pq_new/page.tsx | 2 +- app/[lng]/evcp/(evcp)/project-gtc/page.tsx | 2 +- app/[lng]/evcp/(evcp)/project-vendors/page.tsx | 2 +- app/[lng]/evcp/(evcp)/projects/page.tsx | 2 +- app/[lng]/evcp/(evcp)/qna/[id]/page.tsx | 13 ++ app/[lng]/evcp/(evcp)/qna/page.tsx | 66 +++++++++ app/[lng]/evcp/(evcp)/tag-numbering/page.tsx | 2 +- app/[lng]/evcp/(evcp)/tech-project-avl/page.tsx | 2 +- .../evcp/(evcp)/tech-vendor-candidates/page.tsx | 2 +- app/[lng]/evcp/(evcp)/vendor-candidates/page.tsx | 2 +- app/[lng]/evcp/(evcp)/vendor-check-list/page.tsx | 2 +- .../evcp/(evcp)/vendor-investigation/page.tsx | 2 +- app/[lng]/evcp/(evcp)/vendor-type/page.tsx | 2 +- app/[lng]/evcp/(evcp)/vendors/page.tsx | 2 +- .../partners/(partners)/basic-contract/page.tsx | 2 +- app/[lng]/partners/(partners)/cbe-tech/page.tsx | 2 +- app/[lng]/partners/(partners)/cbe/page.tsx | 2 +- app/[lng]/partners/(partners)/dashboard/page.tsx | 152 +++++++++++++++------ .../(partners)/document-list-ship/page.tsx | 2 +- app/[lng]/partners/(partners)/evaluation/page.tsx | 2 +- app/[lng]/partners/(partners)/qna/page.tsx | 66 +++++++++ app/[lng]/partners/(partners)/rfq-answer/page.tsx | 2 +- app/[lng]/partners/(partners)/rfq-ship/page.tsx | 2 +- app/[lng]/partners/(partners)/rfq-tech/page.tsx | 2 +- app/[lng]/partners/(partners)/rfq/page.tsx | 2 +- app/[lng]/partners/(partners)/tbe-tech/page.tsx | 2 +- app/[lng]/partners/(partners)/tbe/page.tsx | 2 +- .../techsales/rfq-offshore-hull/page.tsx | 2 +- .../(partners)/techsales/rfq-offshore-top/page.tsx | 2 +- .../(partners)/techsales/rfq-ship/page.tsx | 2 +- .../partners/(partners)/vendor-data/layout.tsx | 2 +- app/[lng]/partners/pq_new/page.tsx | 2 +- 60 files changed, 397 insertions(+), 103 deletions(-) create mode 100644 app/[lng]/evcp/(evcp)/notice/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/qna/[id]/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/qna/page.tsx create mode 100644 app/[lng]/partners/(partners)/qna/page.tsx diff --git a/app/[lng]/evcp/(evcp)/b-rfq/page.tsx b/app/[lng]/evcp/(evcp)/b-rfq/page.tsx index 4f80211b..6dc0fb44 100644 --- a/app/[lng]/evcp/(evcp)/b-rfq/page.tsx +++ b/app/[lng]/evcp/(evcp)/b-rfq/page.tsx @@ -58,7 +58,7 @@ export default async function PQReviewPage(props: PQReviewPageProps) {

견적 RFQ

- + diff --git a/app/[lng]/evcp/(evcp)/basic-contract-template/page.tsx b/app/[lng]/evcp/(evcp)/basic-contract-template/page.tsx index d98bd4c7..1345667d 100644 --- a/app/[lng]/evcp/(evcp)/basic-contract-template/page.tsx +++ b/app/[lng]/evcp/(evcp)/basic-contract-template/page.tsx @@ -38,7 +38,7 @@ export default async function IndexPage(props: IndexPageProps) {

기본계약서 템플릿 관리

- +

기본계약서를 비롯하여 초기 서명이 필요한 문서를 등록하고 편집할 수 있습니다. 활성화된 템플릿이 서명 요청의 리스트에 나타나게 됩니다..{" "} diff --git a/app/[lng]/evcp/(evcp)/basic-contract/page.tsx b/app/[lng]/evcp/(evcp)/basic-contract/page.tsx index b0b46197..9bc66b49 100644 --- a/app/[lng]/evcp/(evcp)/basic-contract/page.tsx +++ b/app/[lng]/evcp/(evcp)/basic-contract/page.tsx @@ -38,7 +38,7 @@ export default async function IndexPage(props: IndexPageProps) {

기본계약서 서명 현황

- +

기본계약서를 비롯하여 초기 서명이 필요한 문서의 서명 현황을 확인할 수 있고 서명된 문서들을 다운로드할 수 있습니다. {" "} diff --git a/app/[lng]/evcp/(evcp)/bid-projects/page.tsx b/app/[lng]/evcp/(evcp)/bid-projects/page.tsx index e000cd6f..55f90dbb 100644 --- a/app/[lng]/evcp/(evcp)/bid-projects/page.tsx +++ b/app/[lng]/evcp/(evcp)/bid-projects/page.tsx @@ -37,7 +37,7 @@ export default async function IndexPage(props: IndexPageProps) {

견적 프로젝트 리스트

- +

SAP(S-ERP)로부터 수신한 견적 프로젝트 데이터입니다. 기술영업의 Budgetary RFQ에서 사용됩니다. diff --git a/app/[lng]/evcp/(evcp)/budgetary-rfq/page.tsx b/app/[lng]/evcp/(evcp)/budgetary-rfq/page.tsx index 42683006..ae5f2b48 100644 --- a/app/[lng]/evcp/(evcp)/budgetary-rfq/page.tsx +++ b/app/[lng]/evcp/(evcp)/budgetary-rfq/page.tsx @@ -49,7 +49,7 @@ export default async function RfqPage({

{title}

- +

{description} diff --git a/app/[lng]/evcp/(evcp)/budgetary-tech-sales-hull/page.tsx b/app/[lng]/evcp/(evcp)/budgetary-tech-sales-hull/page.tsx index c2d25016..ce7bac9a 100644 --- a/app/[lng]/evcp/(evcp)/budgetary-tech-sales-hull/page.tsx +++ b/app/[lng]/evcp/(evcp)/budgetary-tech-sales-hull/page.tsx @@ -37,7 +37,7 @@ export default async function HullRfqPage(props: HullRfqPageProps) {

기술영업-해양 Hull RFQ

- + diff --git a/app/[lng]/evcp/(evcp)/budgetary-tech-sales-ship/page.tsx b/app/[lng]/evcp/(evcp)/budgetary-tech-sales-ship/page.tsx index ef43cf0d..b2132cac 100644 --- a/app/[lng]/evcp/(evcp)/budgetary-tech-sales-ship/page.tsx +++ b/app/[lng]/evcp/(evcp)/budgetary-tech-sales-ship/page.tsx @@ -37,7 +37,7 @@ export default async function RfqPage(props: RfqPageProps) {

기술영업-조선 RFQ

- + diff --git a/app/[lng]/evcp/(evcp)/budgetary-tech-sales-top/page.tsx b/app/[lng]/evcp/(evcp)/budgetary-tech-sales-top/page.tsx index 0a135add..37b75d22 100644 --- a/app/[lng]/evcp/(evcp)/budgetary-tech-sales-top/page.tsx +++ b/app/[lng]/evcp/(evcp)/budgetary-tech-sales-top/page.tsx @@ -37,7 +37,7 @@ export default async function HullRfqPage(props: HullRfqPageProps) {

기술영업-해양 TOP RFQ

- + diff --git a/app/[lng]/evcp/(evcp)/email-template/page.tsx b/app/[lng]/evcp/(evcp)/email-template/page.tsx index 7219d523..520fd8d5 100644 --- a/app/[lng]/evcp/(evcp)/email-template/page.tsx +++ b/app/[lng]/evcp/(evcp)/email-template/page.tsx @@ -11,7 +11,7 @@ export default async function MailTemplatesPage() {

메일 템플릿 관리

- +

이메일 템플릿을 관리할 수 있습니다.

diff --git a/app/[lng]/evcp/(evcp)/equip-class/page.tsx b/app/[lng]/evcp/(evcp)/equip-class/page.tsx index 268b09b6..15f4f333 100644 --- a/app/[lng]/evcp/(evcp)/equip-class/page.tsx +++ b/app/[lng]/evcp/(evcp)/equip-class/page.tsx @@ -38,7 +38,7 @@ export default async function IndexPage(props: IndexPageProps) {

객체 클래스 목록 from S-EDP

- +

객체 클래스 목록을 확인할 수 있습니다.{" "} diff --git a/app/[lng]/evcp/(evcp)/esg-check-list/page.tsx b/app/[lng]/evcp/(evcp)/esg-check-list/page.tsx index 08292e56..7bbd058f 100644 --- a/app/[lng]/evcp/(evcp)/esg-check-list/page.tsx +++ b/app/[lng]/evcp/(evcp)/esg-check-list/page.tsx @@ -37,7 +37,7 @@ export default async function IndexPage(props: IndexPageProps) {

ESG 자가진단표

- +

협력업체 평가에 사용되는 ESG 자가진단표를 관리{" "} diff --git a/app/[lng]/evcp/(evcp)/evaluation-check-list/page.tsx b/app/[lng]/evcp/(evcp)/evaluation-check-list/page.tsx index e0375f44..56f18e92 100644 --- a/app/[lng]/evcp/(evcp)/evaluation-check-list/page.tsx +++ b/app/[lng]/evcp/(evcp)/evaluation-check-list/page.tsx @@ -39,7 +39,7 @@ async function EvaluationCriteriaPage(props: EvaluationCriteriaPageProps) {

협력업체 평가기준표

- +

협력업체 평가에 사용되는 평가기준표를 관리{" "} diff --git a/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx b/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx index 577a1932..9ec30b66 100644 --- a/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx +++ b/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx @@ -70,7 +70,7 @@ export default async function EvaluationTargetsPage(props: EvaluationTargetsPage

협력업체 평가 대상 확정

- + {currentEvaluationYear}년도 diff --git a/app/[lng]/evcp/(evcp)/evaluation/page.tsx b/app/[lng]/evcp/(evcp)/evaluation/page.tsx index d2b8c67e..4c498104 100644 --- a/app/[lng]/evcp/(evcp)/evaluation/page.tsx +++ b/app/[lng]/evcp/(evcp)/evaluation/page.tsx @@ -136,7 +136,7 @@ export default async function PeriodicEvaluationsPage(props: PeriodicEvaluations

협력업체 정기평가

- + {currentEvaluationYear}년도 diff --git a/app/[lng]/evcp/(evcp)/faq/page.tsx b/app/[lng]/evcp/(evcp)/faq/page.tsx index 80fdc5d1..558d140b 100644 --- a/app/[lng]/evcp/(evcp)/faq/page.tsx +++ b/app/[lng]/evcp/(evcp)/faq/page.tsx @@ -27,7 +27,7 @@ export default async function FaqPage(props: Props) {

Frequently Asked Questions

- +

Find answers to common questions about using the EVCP system. diff --git a/app/[lng]/evcp/(evcp)/form-list/page.tsx b/app/[lng]/evcp/(evcp)/form-list/page.tsx index 186976f4..b9bdb8e9 100644 --- a/app/[lng]/evcp/(evcp)/form-list/page.tsx +++ b/app/[lng]/evcp/(evcp)/form-list/page.tsx @@ -39,7 +39,7 @@ export default async function IndexPage(props: IndexPageProps) {

레지스터 목록 from S-EDP

- +

협력업체 데이터 입력을 위한 레지스터 목록 리스트입니다.{" "} diff --git a/app/[lng]/evcp/(evcp)/incoterms/page.tsx b/app/[lng]/evcp/(evcp)/incoterms/page.tsx index 4da1b4e2..dcfca922 100644 --- a/app/[lng]/evcp/(evcp)/incoterms/page.tsx +++ b/app/[lng]/evcp/(evcp)/incoterms/page.tsx @@ -30,7 +30,7 @@ export default async function IndexPage(props: IndexPageProps) {

인코텀즈 관리

- +

인코텀즈(Incoterms)를 등록, 수정, 삭제할 수 있습니다. diff --git a/app/[lng]/evcp/(evcp)/information/page.tsx b/app/[lng]/evcp/(evcp)/information/page.tsx index 4027ab8a..db383c32 100644 --- a/app/[lng]/evcp/(evcp)/information/page.tsx +++ b/app/[lng]/evcp/(evcp)/information/page.tsx @@ -4,9 +4,7 @@ import { unstable_noStore as noStore } from "next/cache" import { Shell } from "@/components/shell" import { getInformationLists } from "@/lib/information/service" -import { InformationTable } from "@/lib/information/table/information-table" -import { searchParamsInformationCache } from "@/lib/information/validations" -import type { SearchParams } from "@/types/table" +import { InformationClient } from "@/components/information/information-client" import { InformationButton } from "@/components/information/information-button" export const metadata: Metadata = { @@ -15,15 +13,30 @@ export const metadata: Metadata = { } interface InformationPageProps { - searchParams: Promise + params: Promise<{ lng: string }> } -export default async function InformationPage({ searchParams }: InformationPageProps) { +export default async function InformationPage({ params }: InformationPageProps) { noStore() + + const { lng } = await params - const search = await searchParamsInformationCache.parse(await searchParams) - - const informationPromise = getInformationLists(search) + // 초기 데이터 로딩 + const initialData = await getInformationLists({ + page: 1, + perPage: 500, + search: "", + sort: [{ id: "createdAt", desc: true }], + flags: [], + filters: [], + joinOperator: "and", + pagePath: "", + pageName: "", + informationContent: "", + isActive: null, + from: "", + to: "", + }) return ( @@ -32,14 +45,14 @@ export default async function InformationPage({ searchParams }: InformationPageP

- 도움말 관리 + 인포메이션 관리

- +
- + ) } \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/items/page.tsx b/app/[lng]/evcp/(evcp)/items/page.tsx index c4e00557..fb0734a9 100644 --- a/app/[lng]/evcp/(evcp)/items/page.tsx +++ b/app/[lng]/evcp/(evcp)/items/page.tsx @@ -38,7 +38,7 @@ export default async function IndexPage(props: IndexPageProps) {

패키지 정보

- +

S-EDP로부터 수신된 패키지 정보이며 PR 전 입찰, 견적에 사용되며 벤더 데이터, 문서와 연결됩니다. diff --git a/app/[lng]/evcp/(evcp)/menu-access/page.tsx b/app/[lng]/evcp/(evcp)/menu-access/page.tsx index f632bc9e..b99a3342 100644 --- a/app/[lng]/evcp/(evcp)/menu-access/page.tsx +++ b/app/[lng]/evcp/(evcp)/menu-access/page.tsx @@ -30,7 +30,7 @@ export default async function IndexPage(props: IndexPageProps) {

메뉴 접근제어 관리

- +

화면, 메뉴별로 접근 통제를 할 수 있습니다. 도메인을 설정하면 해당 도메인에 대한 접근만 가능합니다. diff --git a/app/[lng]/evcp/(evcp)/menu-list/page.tsx b/app/[lng]/evcp/(evcp)/menu-list/page.tsx index 6f56a2de..1632eeee 100644 --- a/app/[lng]/evcp/(evcp)/menu-list/page.tsx +++ b/app/[lng]/evcp/(evcp)/menu-list/page.tsx @@ -26,7 +26,7 @@ export default async function MenuListPage() {

메뉴 관리

- +

각 메뉴별로 담당자를 지정하고 관리할 수 있습니다. diff --git a/app/[lng]/evcp/(evcp)/notice/page.tsx b/app/[lng]/evcp/(evcp)/notice/page.tsx new file mode 100644 index 00000000..a4157d1b --- /dev/null +++ b/app/[lng]/evcp/(evcp)/notice/page.tsx @@ -0,0 +1,60 @@ +import * as React from "react" +import type { Metadata } from "next" +import { unstable_noStore as noStore } from "next/cache" +import { getServerSession } from "next-auth" +import { Shell } from "@/components/shell" +import { NoticeClient } from "@/components/notice/notice-client" +import { InformationButton } from "@/components/information/information-button" +import { getNoticeLists } from "@/lib/notice/service" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" + +export const metadata: Metadata = { + title: "공지사항 관리", + description: "페이지별 공지사항을 관리합니다.", +} + +export default async function NoticePage() { + noStore() + + // 세션에서 사용자 ID 가져오기 + const session = await getServerSession(authOptions) + const currentUserId = session?.user?.id ? parseInt(session.user.id) : undefined + + + // 간단한 초기 데이터 로딩 + const initialData = await getNoticeLists({ + page: 1, + perPage: 50, + search: "", + sort: [{ id: "createdAt", desc: true }], + flags: [], + filters: [], + joinOperator: "and", + pagePath: "", + title: "", + content: "", + authorId: currentUserId || null, + isActive: true, + from: "", + to: "", + }) + + + return ( + +

+
+
+
+

+ 공지사항 관리 +

+ +
+
+
+
+ + + ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/payment-conditions/page.tsx b/app/[lng]/evcp/(evcp)/payment-conditions/page.tsx index fc22745c..40d0cf54 100644 --- a/app/[lng]/evcp/(evcp)/payment-conditions/page.tsx +++ b/app/[lng]/evcp/(evcp)/payment-conditions/page.tsx @@ -30,7 +30,7 @@ export default async function IndexPage(props: IndexPageProps) {

결제 조건 관리

- +

결제 조건(Payment Terms)을 등록, 수정, 삭제할 수 있습니다. diff --git a/app/[lng]/evcp/(evcp)/po-rfq/page.tsx b/app/[lng]/evcp/(evcp)/po-rfq/page.tsx index 99eab9f3..5a7c7ae0 100644 --- a/app/[lng]/evcp/(evcp)/po-rfq/page.tsx +++ b/app/[lng]/evcp/(evcp)/po-rfq/page.tsx @@ -37,7 +37,7 @@ export default async function RfqPage(props: RfqPageProps) {

발주용 견적

- +
diff --git a/app/[lng]/evcp/(evcp)/po/page.tsx b/app/[lng]/evcp/(evcp)/po/page.tsx index cd23ff0e..8edb1207 100644 --- a/app/[lng]/evcp/(evcp)/po/page.tsx +++ b/app/[lng]/evcp/(evcp)/po/page.tsx @@ -37,7 +37,7 @@ export default async function IndexPage(props: IndexPageProps) {

PO 확인 및 전자서명

- +

기간계 시스템으로부터 PO를 확인하고 협력업체에게 전자서명을 요청할 수 있습니다. 요쳥된 전자서명의 이력 또한 확인할 수 있습니다. diff --git a/app/[lng]/evcp/(evcp)/poa/page.tsx b/app/[lng]/evcp/(evcp)/poa/page.tsx index e0172af3..55b5240d 100644 --- a/app/[lng]/evcp/(evcp)/poa/page.tsx +++ b/app/[lng]/evcp/(evcp)/poa/page.tsx @@ -35,7 +35,7 @@ export default async function IndexPage(props: IndexPageProps) {

변경 PO 확인 및 전자서명

- +

발행된 PO의 변경 내역을 확인하고 관리할 수 있습니다. diff --git a/app/[lng]/evcp/(evcp)/pq-criteria/page.tsx b/app/[lng]/evcp/(evcp)/pq-criteria/page.tsx index ce92e039..34908028 100644 --- a/app/[lng]/evcp/(evcp)/pq-criteria/page.tsx +++ b/app/[lng]/evcp/(evcp)/pq-criteria/page.tsx @@ -37,7 +37,7 @@ export default async function IndexPage(props: IndexPageProps) {

Pre-Qualification Check Sheet

- +

협력업체 등록을 위한, 협력업체가 제출할 PQ 항목을 관리할 수 있습니다. diff --git a/app/[lng]/evcp/(evcp)/pq/page.tsx b/app/[lng]/evcp/(evcp)/pq/page.tsx index 02cb621e..da123ce2 100644 --- a/app/[lng]/evcp/(evcp)/pq/page.tsx +++ b/app/[lng]/evcp/(evcp)/pq/page.tsx @@ -37,7 +37,7 @@ export default async function IndexPage(props: IndexPageProps) {

Pre-Qualification Review

- +

벤더가 제출한 PQ를 확인하고 수정 요청 등을 할 수 있으며 PQ 종료 후에는 통과 여부를 결정할 수 있습니다. diff --git a/app/[lng]/evcp/(evcp)/pq_new/page.tsx b/app/[lng]/evcp/(evcp)/pq_new/page.tsx index abe134bd..2e5e3e01 100644 --- a/app/[lng]/evcp/(evcp)/pq_new/page.tsx +++ b/app/[lng]/evcp/(evcp)/pq_new/page.tsx @@ -73,7 +73,7 @@ export default async function PQReviewPage(props: PQReviewPageProps) {

PQ 검토/실사 의뢰

- + diff --git a/app/[lng]/evcp/(evcp)/project-gtc/page.tsx b/app/[lng]/evcp/(evcp)/project-gtc/page.tsx index cebfb1a9..05a8388c 100644 --- a/app/[lng]/evcp/(evcp)/project-gtc/page.tsx +++ b/app/[lng]/evcp/(evcp)/project-gtc/page.tsx @@ -34,7 +34,7 @@ export default async function IndexPage(props: IndexPageProps) {

Project GTC

- +

프로젝트별 GTC(General Terms and Conditions) 파일을 관리할 수 있습니다. diff --git a/app/[lng]/evcp/(evcp)/project-vendors/page.tsx b/app/[lng]/evcp/(evcp)/project-vendors/page.tsx index 5841b2c5..1bf316b6 100644 --- a/app/[lng]/evcp/(evcp)/project-vendors/page.tsx +++ b/app/[lng]/evcp/(evcp)/project-vendors/page.tsx @@ -37,7 +37,7 @@ export default async function IndexPage(props: IndexPageProps) {

프로젝트 AVL 리스트

- +

프로젝트 PQ를 통과한 벤더의 리스트를 보여줍니다.{" "} diff --git a/app/[lng]/evcp/(evcp)/projects/page.tsx b/app/[lng]/evcp/(evcp)/projects/page.tsx index 09cf6541..401de7e0 100644 --- a/app/[lng]/evcp/(evcp)/projects/page.tsx +++ b/app/[lng]/evcp/(evcp)/projects/page.tsx @@ -38,7 +38,7 @@ export default async function IndexPage(props: IndexPageProps) {

Project List from S-EDP

- +

S-EDP로부터 수신하는 프로젝트 리스트입니다. 향후 MDG로 전환됩니다.{" "} diff --git a/app/[lng]/evcp/(evcp)/qna/[id]/page.tsx b/app/[lng]/evcp/(evcp)/qna/[id]/page.tsx new file mode 100644 index 00000000..93b948c6 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/qna/[id]/page.tsx @@ -0,0 +1,13 @@ +import { getQnaById } from "@/lib/qna/service"; +import QnaDetail from "@/lib/qna/table/qna-detail"; +import { notFound } from "next/navigation"; + +export default async function QnaDetailPage({ params }: { params: { id: string } }) { + const question = await getQnaById(params.id); + + if (!question) { + notFound(); + } + + return ; +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/qna/page.tsx b/app/[lng]/evcp/(evcp)/qna/page.tsx new file mode 100644 index 00000000..f5d86ad4 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/qna/page.tsx @@ -0,0 +1,66 @@ +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 { QnaTable } from "@/lib/qna/table/qna-table" +import { getQnaList } from "@/lib/qna/service" +import { searchParamsQnaCache } from "@/lib/qna/validation" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsQnaCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getQnaList({ + ...search, + filters: validFilters, + }), + ]) + + return ( + +

+
+
+
+

+ Q&A +

+
+

+ 협력업체로부터 수집된 질문에 대해서 댓글을 달거나 응답할 수 있습니다. +

+
+
+ +
+ }> + + + } + > + + + + ) +} diff --git a/app/[lng]/evcp/(evcp)/tag-numbering/page.tsx b/app/[lng]/evcp/(evcp)/tag-numbering/page.tsx index b4a90703..c211bad7 100644 --- a/app/[lng]/evcp/(evcp)/tag-numbering/page.tsx +++ b/app/[lng]/evcp/(evcp)/tag-numbering/page.tsx @@ -37,7 +37,7 @@ export default async function IndexPage(props: IndexPageProps) {

태그 타입 목록 from S-EDP

- +

태그 넘버링을 위한 룰셋을 S-EDP로부터 가져오고 확인할 수 있습니다{" "} diff --git a/app/[lng]/evcp/(evcp)/tech-project-avl/page.tsx b/app/[lng]/evcp/(evcp)/tech-project-avl/page.tsx index f8662ce0..f62b5314 100644 --- a/app/[lng]/evcp/(evcp)/tech-project-avl/page.tsx +++ b/app/[lng]/evcp/(evcp)/tech-project-avl/page.tsx @@ -49,7 +49,7 @@ export default async function AcceptedQuotationsPage({

승인된 견적서(해양TOP,HULL)

- +

기술영업 승인 견적서에 대한 요약 정보를 확인하고{" "} diff --git a/app/[lng]/evcp/(evcp)/tech-vendor-candidates/page.tsx b/app/[lng]/evcp/(evcp)/tech-vendor-candidates/page.tsx index a3bee46a..7e2ec9ff 100644 --- a/app/[lng]/evcp/(evcp)/tech-vendor-candidates/page.tsx +++ b/app/[lng]/evcp/(evcp)/tech-vendor-candidates/page.tsx @@ -39,7 +39,7 @@ export default async function IndexPage(props: IndexPageProps) {

Vendor Candidates Management

- +

수집한 협력업체 후보를 등록하고 초대 메일을 송부할 수 있습니다. diff --git a/app/[lng]/evcp/(evcp)/vendor-candidates/page.tsx b/app/[lng]/evcp/(evcp)/vendor-candidates/page.tsx index 53b0ece5..6f8d5880 100644 --- a/app/[lng]/evcp/(evcp)/vendor-candidates/page.tsx +++ b/app/[lng]/evcp/(evcp)/vendor-candidates/page.tsx @@ -39,7 +39,7 @@ export default async function IndexPage(props: IndexPageProps) {

Vendor Candidates Management

- +

수집한 협력업체 후보를 등록하고 초대 메일을 송부할 수 있습니다. diff --git a/app/[lng]/evcp/(evcp)/vendor-check-list/page.tsx b/app/[lng]/evcp/(evcp)/vendor-check-list/page.tsx index 42f74578..30021e2c 100644 --- a/app/[lng]/evcp/(evcp)/vendor-check-list/page.tsx +++ b/app/[lng]/evcp/(evcp)/vendor-check-list/page.tsx @@ -37,7 +37,7 @@ export default async function IndexPage(props: IndexPageProps) {

협력업체 정기평가 체크리스트

- +

협력업체 평가에 사용되는 정기평가 체크리스트를 관리{" "} diff --git a/app/[lng]/evcp/(evcp)/vendor-investigation/page.tsx b/app/[lng]/evcp/(evcp)/vendor-investigation/page.tsx index df3567b4..9b9a5ac5 100644 --- a/app/[lng]/evcp/(evcp)/vendor-investigation/page.tsx +++ b/app/[lng]/evcp/(evcp)/vendor-investigation/page.tsx @@ -37,7 +37,7 @@ export default async function IndexPage(props: IndexPageProps) {

Vendor Investigation Management

- +

요청된 Vendor 실사에 대한 스케줄 정보를 관리하고 결과를 입력할 수 있습니다. diff --git a/app/[lng]/evcp/(evcp)/vendor-type/page.tsx b/app/[lng]/evcp/(evcp)/vendor-type/page.tsx index d81e351d..82324dbf 100644 --- a/app/[lng]/evcp/(evcp)/vendor-type/page.tsx +++ b/app/[lng]/evcp/(evcp)/vendor-type/page.tsx @@ -37,7 +37,7 @@ export default async function IndexPage(props: IndexPageProps) {

업체 유형

- +

업체 유형을 등록하고 관리할 수 있습니다.{" "} diff --git a/app/[lng]/evcp/(evcp)/vendors/page.tsx b/app/[lng]/evcp/(evcp)/vendors/page.tsx index d5434188..4dbd642c 100644 --- a/app/[lng]/evcp/(evcp)/vendors/page.tsx +++ b/app/[lng]/evcp/(evcp)/vendors/page.tsx @@ -40,7 +40,7 @@ export default async function IndexPage(props: IndexPageProps) {

협력업체 리스트

- +

협력업체에 대한 요약 정보를 확인하고{" "} diff --git a/app/[lng]/partners/(partners)/basic-contract/page.tsx b/app/[lng]/partners/(partners)/basic-contract/page.tsx index 5316c357..37b7d1a6 100644 --- a/app/[lng]/partners/(partners)/basic-contract/page.tsx +++ b/app/[lng]/partners/(partners)/basic-contract/page.tsx @@ -45,7 +45,7 @@ export default async function IndexPage(props: IndexPageProps) {

기본계약서 서명 요청현황

- +

기본계약서를 비롯하여 초기 서명이 필요한 문서의 서명 현황을 확인할 수 있고 서명을 진행할 수 있습니다. {" "} diff --git a/app/[lng]/partners/(partners)/cbe-tech/page.tsx b/app/[lng]/partners/(partners)/cbe-tech/page.tsx index 9aeb4e66..35d7bba4 100644 --- a/app/[lng]/partners/(partners)/cbe-tech/page.tsx +++ b/app/[lng]/partners/(partners)/cbe-tech/page.tsx @@ -54,7 +54,7 @@ export default async function CBEPage(props: IndexPageProps) {

Commercial Bid Evaluation

- +

CBE에 응답하고 커뮤니케이션을 할 수 있습니다.{" "} diff --git a/app/[lng]/partners/(partners)/cbe/page.tsx b/app/[lng]/partners/(partners)/cbe/page.tsx index 235426a4..01393551 100644 --- a/app/[lng]/partners/(partners)/cbe/page.tsx +++ b/app/[lng]/partners/(partners)/cbe/page.tsx @@ -54,7 +54,7 @@ export default async function CBEPage(props: IndexPageProps) {

Commercial Bid Evaluation

- +

CBE에 응답하고 커뮤니케이션을 할 수 있습니다.{" "} diff --git a/app/[lng]/partners/(partners)/dashboard/page.tsx b/app/[lng]/partners/(partners)/dashboard/page.tsx index 01d3c2be..71b70abc 100644 --- a/app/[lng]/partners/(partners)/dashboard/page.tsx +++ b/app/[lng]/partners/(partners)/dashboard/page.tsx @@ -1,50 +1,126 @@ -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" +// app/procurement/dashboard/page.tsx +import * as React from "react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Shell } from "@/components/shell"; +import { DashboardClient } from "@/lib/dashboard/dashboard-client"; +import { getPartnersDashboardData } from "@/lib/dashboard/partners-service"; -export default async function IndexPage() { +// 대시보드 데이터 로딩 컴포넌트 +async function DashboardContent() { + try { + const data = await getPartnersDashboardData("partners"); + + const handleRefresh = async () => { + "use server"; + return await getPartnersDashboardData("partners"); + }; + return ( + + ); + } catch (error) { + console.error("Dashboard data loading error:", error); + throw error; + } +} +// 대시보드 로딩 스켈레톤 +function DashboardSkeleton() { return ( - +

+ {/* 헤더 스켈레톤 */}
-
-
-

- Dashboard -

- +
+ + +
+ +
+ + {/* 요약 카드 스켈레톤 */} +
+ {[...Array(4)].map((_, i) => ( +
+
+ + +
+ + +
+ ))} +
+ + {/* 차트 스켈레톤 */} +
+ {[...Array(2)].map((_, i) => ( +
+
+ + +
+
-

- 각종 지표 등을 대시보드로 표현하거나 리포트를 출력할 수 있습니다. -

+ ))} +
+ + {/* 탭 스켈레톤 */} +
+ +
+ {[...Array(6)].map((_, i) => ( +
+ +
+
+ + +
+
+ + + +
+ +
+
+ ))}
+
+ ); +} - }> - {/* */} - - - - } +// 에러 표시 컴포넌트 +function DashboardError({ error, reset }: { error: Error; reset: () => void }) { + return ( +
+
+

대시보드를 불러올 수 없습니다

+

+ {error.message || "알 수 없는 오류가 발생했습니다."} +

+
+ +
+ ); +} + +export default async function DashboardPage() { + return ( + + }> + + - ) -} \ No newline at end of file + ); +} diff --git a/app/[lng]/partners/(partners)/document-list-ship/page.tsx b/app/[lng]/partners/(partners)/document-list-ship/page.tsx index da4d9e90..f6ceb264 100644 --- a/app/[lng]/partners/(partners)/document-list-ship/page.tsx +++ b/app/[lng]/partners/(partners)/document-list-ship/page.tsx @@ -36,7 +36,7 @@ export default async function IndexPage(props: IndexPageProps) {

Document Management

- +

소속 회사의 모든 도서/도면을 확인하고 관리합니다. diff --git a/app/[lng]/partners/(partners)/evaluation/page.tsx b/app/[lng]/partners/(partners)/evaluation/page.tsx index 2ddaf365..085b3d65 100644 --- a/app/[lng]/partners/(partners)/evaluation/page.tsx +++ b/app/[lng]/partners/(partners)/evaluation/page.tsx @@ -36,7 +36,7 @@ export default async function IndexPage(props: IndexPageProps) {

정기평가

- +

요청된 정기평가를 입력하고 제출할 수 있습니다. diff --git a/app/[lng]/partners/(partners)/qna/page.tsx b/app/[lng]/partners/(partners)/qna/page.tsx new file mode 100644 index 00000000..bdd1372d --- /dev/null +++ b/app/[lng]/partners/(partners)/qna/page.tsx @@ -0,0 +1,66 @@ +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 { QnaTable } from "@/lib/qna/table/qna-table" +import { getQnaList } from "@/lib/qna/service" +import { searchParamsQnaCache } from "@/lib/qna/validation" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsQnaCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getQnaList({ + ...search, + filters: validFilters, + }), + ]) + + return ( + +

+
+
+
+

+ Q&A +

+
+

+ 협력업체로부터 수집된 질문에 대해서 댓글을 달거나 응답할 수 있습니다. +

+
+
+ +
+ }> + + + } + > + + + + ) +} diff --git a/app/[lng]/partners/(partners)/rfq-answer/page.tsx b/app/[lng]/partners/(partners)/rfq-answer/page.tsx index 7a5dabd9..9037062f 100644 --- a/app/[lng]/partners/(partners)/rfq-answer/page.tsx +++ b/app/[lng]/partners/(partners)/rfq-answer/page.tsx @@ -42,7 +42,7 @@ export default async function IndexPage(props: IndexPageProps) {

응답 관리

- +

RFQ 첨부파일 응답 현황을 확인하고 관리합니다. diff --git a/app/[lng]/partners/(partners)/rfq-ship/page.tsx b/app/[lng]/partners/(partners)/rfq-ship/page.tsx index 1ad7cfe8..fbad280a 100644 --- a/app/[lng]/partners/(partners)/rfq-ship/page.tsx +++ b/app/[lng]/partners/(partners)/rfq-ship/page.tsx @@ -42,7 +42,7 @@ export default async function IndexPage(props: IndexPageProps) {

견적 목록

- +

진행 중인 견적서 목록을 확인하고 관리합니다. diff --git a/app/[lng]/partners/(partners)/rfq-tech/page.tsx b/app/[lng]/partners/(partners)/rfq-tech/page.tsx index a196cf9e..154247fe 100644 --- a/app/[lng]/partners/(partners)/rfq-tech/page.tsx +++ b/app/[lng]/partners/(partners)/rfq-tech/page.tsx @@ -36,7 +36,7 @@ export default async function IndexPage(props: IndexPageProps) {

RFQ

- +

RFQ를 응답하고 커뮤니케이션을 할 수 있습니다. diff --git a/app/[lng]/partners/(partners)/rfq/page.tsx b/app/[lng]/partners/(partners)/rfq/page.tsx index 612d48f5..87202155 100644 --- a/app/[lng]/partners/(partners)/rfq/page.tsx +++ b/app/[lng]/partners/(partners)/rfq/page.tsx @@ -105,7 +105,7 @@ export default async function IndexPage(props: IndexPageProps) {

RFQ

- +

RFQ를 응답하고 커뮤니케이션을 할 수 있습니다. diff --git a/app/[lng]/partners/(partners)/tbe-tech/page.tsx b/app/[lng]/partners/(partners)/tbe-tech/page.tsx index 463a8dc9..2085ca36 100644 --- a/app/[lng]/partners/(partners)/tbe-tech/page.tsx +++ b/app/[lng]/partners/(partners)/tbe-tech/page.tsx @@ -53,7 +53,7 @@ export default async function RfqTBEPage(props: IndexPageProps) {

Technical Bid Evaluation

- +

TBE에 응답하고 커뮤니케이션을 할 수 있습니다.{" "} diff --git a/app/[lng]/partners/(partners)/tbe/page.tsx b/app/[lng]/partners/(partners)/tbe/page.tsx index b85ebf71..96f42e09 100644 --- a/app/[lng]/partners/(partners)/tbe/page.tsx +++ b/app/[lng]/partners/(partners)/tbe/page.tsx @@ -53,7 +53,7 @@ export default async function RfqTBEPage(props: IndexPageProps) {

Technical Bid Evaluation

- +

TBE에 응답하고 커뮤니케이션을 할 수 있습니다.{" "} diff --git a/app/[lng]/partners/(partners)/techsales/rfq-offshore-hull/page.tsx b/app/[lng]/partners/(partners)/techsales/rfq-offshore-hull/page.tsx index 0504b51b..0325130e 100644 --- a/app/[lng]/partners/(partners)/techsales/rfq-offshore-hull/page.tsx +++ b/app/[lng]/partners/(partners)/techsales/rfq-offshore-hull/page.tsx @@ -63,7 +63,7 @@ export default async function VendorQuotationsHullPage() {

기술영업 해양HULL 견적서

- +

할당받은 해양HULL RFQ에 대한 견적서를 작성하고 관리합니다. diff --git a/app/[lng]/partners/(partners)/techsales/rfq-offshore-top/page.tsx b/app/[lng]/partners/(partners)/techsales/rfq-offshore-top/page.tsx index b872058f..6c3eaf56 100644 --- a/app/[lng]/partners/(partners)/techsales/rfq-offshore-top/page.tsx +++ b/app/[lng]/partners/(partners)/techsales/rfq-offshore-top/page.tsx @@ -65,7 +65,7 @@ export default async function VendorQuotationsTopPage() {

기술영업 해양TOP 견적서

- +

할당받은 해양TOP RFQ에 대한 견적서를 작성하고 관리합니다. diff --git a/app/[lng]/partners/(partners)/techsales/rfq-ship/page.tsx b/app/[lng]/partners/(partners)/techsales/rfq-ship/page.tsx index ad2ab07b..68830184 100644 --- a/app/[lng]/partners/(partners)/techsales/rfq-ship/page.tsx +++ b/app/[lng]/partners/(partners)/techsales/rfq-ship/page.tsx @@ -68,7 +68,7 @@ export default async function VendorQuotationsPage() {

기술영업 조선 견적서

- +

할당받은 조선 RFQ에 대한 견적서를 작성하고 관리합니다. diff --git a/app/[lng]/partners/(partners)/vendor-data/layout.tsx b/app/[lng]/partners/(partners)/vendor-data/layout.tsx index bdf352c7..cf658e80 100644 --- a/app/[lng]/partners/(partners)/vendor-data/layout.tsx +++ b/app/[lng]/partners/(partners)/vendor-data/layout.tsx @@ -41,7 +41,7 @@ export default async function VendorDataLayout({

Vendor Data

- +

각종 Data 입력할 수 있습니다 diff --git a/app/[lng]/partners/pq_new/page.tsx b/app/[lng]/partners/pq_new/page.tsx index 24051f34..f822eacc 100644 --- a/app/[lng]/partners/pq_new/page.tsx +++ b/app/[lng]/partners/pq_new/page.tsx @@ -133,7 +133,7 @@ export default async function PQListPage() {

사전 평가 (PQ) 목록

- +

요청된 사전 평가 목록을 확인하고 작성합니다. -- cgit v1.2.3 From 795b4915069c44f500a91638e16ded67b9e16618 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 1 Jul 2025 11:46:33 +0000 Subject: (최겸) 정보시스템 공지사항 개발 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/additional-info/join-form.tsx | 5 +- .../additional-info/tech-vendor-info-form.tsx | 7 +- .../document-lists/vendor-doc-list-client.tsx | 7 +- components/documents/vendor-docs.client.tsx | 7 +- components/information/information-button.tsx | 306 +++++++------- components/information/information-client.tsx | 340 ++++++++++++++++ components/items-tech/item-tech-container.tsx | 7 +- components/notice/notice-client.tsx | 438 +++++++++++++++++++++ components/notice/notice-create-dialog.tsx | 216 ++++++++++ components/notice/notice-edit-sheet.tsx | 246 ++++++++++++ components/notice/notice-view-dialog.tsx | 56 +++ lib/notice/repository.ts | 244 ++++++++++++ lib/notice/service.ts | 324 +++++++++++++++ lib/notice/validations.ts | 80 ++++ 14 files changed, 2121 insertions(+), 162 deletions(-) create mode 100644 components/information/information-client.tsx create mode 100644 components/notice/notice-client.tsx create mode 100644 components/notice/notice-create-dialog.tsx create mode 100644 components/notice/notice-edit-sheet.tsx create mode 100644 components/notice/notice-view-dialog.tsx create mode 100644 lib/notice/repository.ts create mode 100644 lib/notice/service.ts create mode 100644 lib/notice/validations.ts diff --git a/components/additional-info/join-form.tsx b/components/additional-info/join-form.tsx index 4a9a3379..b6cb0d9c 100644 --- a/components/additional-info/join-form.tsx +++ b/components/additional-info/join-form.tsx @@ -79,7 +79,7 @@ import { CardHeader, CardTitle, } from "@/components/ui/card" - +import { InformationButton } from "@/components/information/information-button" i18nIsoCountries.registerLocale(enLocale) i18nIsoCountries.registerLocale(koLocale) @@ -535,11 +535,14 @@ const handleDownloadAllFiles = async () => {

+

{t("infoForm.title", { defaultValue: "Update Vendor Information", })}

+ +

{t("infoForm.description", { defaultValue: diff --git a/components/additional-info/tech-vendor-info-form.tsx b/components/additional-info/tech-vendor-info-form.tsx index 8e6f7eaf..55d01d21 100644 --- a/components/additional-info/tech-vendor-info-form.tsx +++ b/components/additional-info/tech-vendor-info-form.tsx @@ -30,7 +30,7 @@ import { CardHeader, CardTitle, } from "@/components/ui/card" - +import { InformationButton } from "@/components/information/information-button" // 타입 정의 interface TechVendorContact { id: number @@ -251,7 +251,10 @@ export function TechVendorInfoForm() {

-

기술영업 벤더 정보

+
+

기술영업 벤더 정보

+ +

기술영업 벤더 정보를 확인하고 업데이트할 수 있습니다.

{attachments.length > 0 && ( diff --git a/components/document-lists/vendor-doc-list-client.tsx b/components/document-lists/vendor-doc-list-client.tsx index d914b6f0..2bd7d996 100644 --- a/components/document-lists/vendor-doc-list-client.tsx +++ b/components/document-lists/vendor-doc-list-client.tsx @@ -4,7 +4,7 @@ import { useRouter, useParams } from "next/navigation" import DocumentContainer from "@/components/documents/document-container" import { ProjectInfo, ProjectSwitcher } from "@/components/documents/project-swicher" - +import { InformationButton } from "@/components/information/information-button" interface VendorDocumentsClientProps { projects: ProjectInfo[] children: React.ReactNode @@ -60,7 +60,10 @@ export default function VendorDocumentListClient({
{/* 왼쪽: 타이틀 & 설명 */}
-

Vendor Document List

+
+

Vendor Document List

+ +

{projectType === "ship" ? "삼성중공업 문서시스템으로부터 목록을 가져오고 문서 파일을 등록하여 삼성중공업으로 전달할 수 있습니다." diff --git a/components/documents/vendor-docs.client.tsx b/components/documents/vendor-docs.client.tsx index 9bb7988c..ebc30b83 100644 --- a/components/documents/vendor-docs.client.tsx +++ b/components/documents/vendor-docs.client.tsx @@ -5,7 +5,7 @@ import { useRouter, useParams } from "next/navigation" import DocumentContainer from "@/components/documents/document-container" import { ProjectInfo, ProjectSwitcher } from "./project-swicher" - +import { InformationButton } from "@/components/information/information-button" interface VendorDocumentsClientProps { projects: ProjectInfo[] children: React.ReactNode @@ -55,7 +55,10 @@ export default function VendorDocumentsClient({

{/* 왼쪽: 타이틀 & 설명 */}
-

Vendor Documents

+
+

Vendor Documents

+ +

문서리스트를 확인하고 리스트에 맞게 문서를 업로드하고 관리할 수 있으며 삼성중공업으로 전달할 수 있습니다. diff --git a/components/information/information-button.tsx b/components/information/information-button.tsx index da0de548..38e8cb12 100644 --- a/components/information/information-button.tsx +++ b/components/information/information-button.tsx @@ -1,137 +1,129 @@ "use client" -import React, { useState, useEffect } from "react" -import { Info, Download, Edit } from "lucide-react" +import * as React from "react" +import { useState } from "react" import { Button } from "@/components/ui/button" + import { Dialog, DialogContent, - DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" +import { Info, Download, Edit } from "lucide-react" import { getCachedPageInformation, getCachedEditPermission } from "@/lib/information/service" +import { getCachedPageNotices } from "@/lib/notice/service" import { UpdateInformationDialog } from "@/lib/information/table/update-information-dialog" +import { NoticeViewDialog } from "@/components/notice/notice-view-dialog" import type { PageInformation } from "@/db/schema/information" +import type { Notice } from "@/db/schema/notice" import { useSession } from "next-auth/react" +import { formatDate } from "@/lib/utils" interface InformationButtonProps { - pageCode: string + pagePath: string className?: string variant?: "default" | "outline" | "ghost" | "secondary" size?: "default" | "sm" | "lg" | "icon" } +type NoticeWithAuthor = Notice & { + authorName: string | null + authorEmail: string | null +} + export function InformationButton({ - pageCode, + pagePath, className, variant = "ghost", size = "icon" }: InformationButtonProps) { const { data: session } = useSession() - const [information, setInformation] = useState(null) - const [isLoading, setIsLoading] = useState(false) const [isOpen, setIsOpen] = useState(false) + const [information, setInformation] = useState(null) + const [notices, setNotices] = useState([]) const [hasEditPermission, setHasEditPermission] = useState(false) const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) + const [selectedNotice, setSelectedNotice] = useState(null) + const [isNoticeViewDialogOpen, setIsNoticeViewDialogOpen] = useState(false) + const [dataLoaded, setDataLoaded] = useState(false) - useEffect(() => { - if (isOpen && !information) { - loadInformation() - } - }, [isOpen, information]) - - // 편집 권한 확인 - useEffect(() => { - const checkEditPermission = async () => { - if (session?.user?.id) { - try { - const permission = await getCachedEditPermission(pageCode, session.user.id) - setHasEditPermission(permission) - } catch (error) { - console.error("Failed to check edit permission:", error) - setHasEditPermission(false) - } - } - } + // 데이터 로드 함수 (단순화) + const loadData = React.useCallback(async () => { + if (dataLoaded) return // 이미 로드되었으면 중복 방지 - checkEditPermission() - }, [pageCode, session?.user?.id]) - - const loadInformation = async () => { - setIsLoading(true) try { - const data = await getCachedPageInformation(pageCode) - setInformation(data) + // pagePath 정규화 (앞의 / 제거) + const normalizedPath = pagePath.startsWith('/') ? pagePath.slice(1) : pagePath + + // 병렬로 데이터 조회 + const [infoResult, noticesResult] = await Promise.all([ + getCachedPageInformation(normalizedPath), + getCachedPageNotices(normalizedPath) + ]) + + setInformation(infoResult) + setNotices(noticesResult) + setDataLoaded(true) + + // 권한 확인 + if (session?.user?.id) { + const hasPermission = await getCachedEditPermission(normalizedPath, session.user.id) + setHasEditPermission(hasPermission) + } } catch (error) { - console.error("Failed to load information:", error) - } finally { - setIsLoading(false) + console.error("데이터 로딩 중 오류:", error) } - } + }, [pagePath, session?.user?.id, dataLoaded]) - const handleDownload = () => { - if (information?.attachmentFilePath && information?.attachmentFileName) { - const link = document.createElement('a') - link.href = information.attachmentFilePath - link.download = information.attachmentFileName - document.body.appendChild(link) - link.click() - document.body.removeChild(link) + // 다이얼로그 열기 + const handleDialogOpen = (open: boolean) => { + setIsOpen(open) + + if (open && !dataLoaded) { + loadData() } } + // 편집 관련 핸들러 const handleEditClick = () => { setIsEditDialogOpen(true) } - const handleEditClose = () => { + const handleEditSuccess = () => { setIsEditDialogOpen(false) - refreshInformation() + // 편집 후 데이터 다시 로드 + setDataLoaded(false) + loadData() + } + + // 공지사항 클릭 핸들러 + const handleNoticeClick = (notice: NoticeWithAuthor) => { + setSelectedNotice(notice) + setIsNoticeViewDialogOpen(true) } - const refreshInformation = () => { - // 편집 후 정보 다시 로드 - setInformation(null) - if (isOpen) { - loadInformation() + // 파일 다운로드 핸들러 + const handleDownload = () => { + if (information?.attachmentFilePath) { + window.open(information.attachmentFilePath, '_blank') } - // 캐시 무효화를 위해 다시 확인 - setTimeout(() => { - loadInformation() - }, 500) } - // 인포메이션이 없으면 버튼을 숨김 - const [hasInformation, setHasInformation] = useState(null) - useEffect(() => { - const checkInformation = async () => { - try { - const data = await getCachedPageInformation(pageCode) - setHasInformation(!!data) - } catch { - setHasInformation(false) - } - } - checkInformation() - }, [pageCode]) - // 인포메이션이 없으면 버튼을 숨김 - if (hasInformation === false) { - return null - } + return ( <> -

+
- - -
- {isLoading ? ( -
-
- ) : information ? ( - <> - {/* 공지사항 */} - {(information.noticeTitle || information.noticeContent) && ( -
-
-

공지사항

-
-
- {information.noticeTitle && ( -
- 제목: {information.noticeTitle} -
- )} - {information.noticeContent && ( -
-
- {information.noticeContent} -
-
- )} -
-
- )} - - {/* 페이지 정보 */} -
-

도움말

-
-
- {information.description || "페이지 설명이 없습니다."} -
+
+
+ {information.informationContent}
+
+ )} - {/* 첨부파일 */} -
-

첨부파일

- {information.attachmentFileName ? ( -
-
-
-
- {information.attachmentFileName} -
- {information.attachmentFileSize && ( -
- {information.attachmentFileSize} -
- )} -
- + {/* 첨부파일 */} + {information?.attachmentFileName && ( +
+

첨부파일

+
+
+
+
+ {information.attachmentFileName}
+ {information.attachmentFileSize && ( +
+ {information.attachmentFileSize} +
+ )}
- ) : ( -
- -

첨부된 파일이 없습니다.

-
- )} + +
- - ) : ( +
+ )} + + {!information && notices.length === 0 && (
- 이 페이지에 대한 정보가 없습니다. +

이 페이지에 대한 정보가 없습니다.

)}
+ {/* 공지사항 보기 다이얼로그 */} + + {/* 편집 다이얼로그 */} {information && ( )} diff --git a/components/information/information-client.tsx b/components/information/information-client.tsx new file mode 100644 index 00000000..513b8f20 --- /dev/null +++ b/components/information/information-client.tsx @@ -0,0 +1,340 @@ +"use client" + +import { useState, useEffect, useTransition } from "react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" +import { + Search, + Edit, + FileText, + ChevronUp, + ChevronDown, + Download +} from "lucide-react" +import { toast } from "sonner" +import { formatDate } from "@/lib/utils" +import { getInformationLists } from "@/lib/information/service" +import type { PageInformation } from "@/db/schema/information" +import { UpdateInformationDialog } from "@/lib/information/table/update-information-dialog" + +interface InformationClientProps { + initialData?: PageInformation[] +} + +type SortField = "pageName" | "pagePath" | "createdAt" +type SortDirection = "asc" | "desc" + +export function InformationClient({ initialData = [] }: InformationClientProps) { + const [informations, setInformations] = useState(initialData) + const [loading, setLoading] = useState(false) + const [searchQuery, setSearchQuery] = useState("") + const [sortField, setSortField] = useState("createdAt") + const [sortDirection, setSortDirection] = useState("desc") + const [editingInformation, setEditingInformation] = useState(null) + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) + const [, startTransition] = useTransition() + + // 정보 목록 조회 + const fetchInformations = async () => { + try { + setLoading(true) + const search = searchQuery || undefined + + startTransition(async () => { + const result = await getInformationLists({ + page: 1, + perPage: 50, + search: search, + sort: [{ id: sortField, desc: sortDirection === "desc" }], + flags: [], + filters: [], + joinOperator: "and", + pagePath: "", + pageName: "", + informationContent: "", + isActive: null, + }) + + if (result?.data) { + setInformations(result.data) + } else { + toast.error("정보 목록을 가져오는데 실패했습니다.") + } + setLoading(false) + }) + } catch (error) { + console.error("Error fetching informations:", error) + toast.error("정보 목록을 가져오는데 실패했습니다.") + setLoading(false) + } + } + + // 검색 핸들러 + const handleSearch = () => { + fetchInformations() + } + + // 정렬 함수 + const sortInformations = (informations: PageInformation[]) => { + return [...informations].sort((a, b) => { + let aValue: string | Date + let bValue: string | Date + + if (sortField === "pageName") { + aValue = a.pageName + bValue = b.pageName + } else if (sortField === "pagePath") { + aValue = a.pagePath + bValue = b.pagePath + } else { + aValue = new Date(a.createdAt) + bValue = new Date(b.createdAt) + } + + if (aValue < bValue) { + return sortDirection === "asc" ? -1 : 1 + } + if (aValue > bValue) { + return sortDirection === "asc" ? 1 : -1 + } + return 0 + }) + } + + // 정렬 핸들러 + const handleSort = (field: SortField) => { + if (sortField === field) { + setSortDirection(sortDirection === "asc" ? "desc" : "asc") + } else { + setSortField(field) + setSortDirection("asc") + } + } + + // 편집 핸들러 + const handleEdit = (information: PageInformation) => { + setEditingInformation(information) + setIsEditDialogOpen(true) + } + + // 편집 완료 핸들러 + const handleEditClose = () => { + setIsEditDialogOpen(false) + setEditingInformation(null) + // 데이터 새로고침 + fetchInformations() + } + + // 다운로드 핸들러 + const handleDownload = (information: PageInformation) => { + if (information.attachmentFilePath && information.attachmentFileName) { + const link = document.createElement('a') + link.href = information.attachmentFilePath + link.download = information.attachmentFileName + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + } + } + + // 정렬된 정보 목록 + const sortedInformations = sortInformations(informations) + + useEffect(() => { + if (initialData.length > 0) { + setInformations(initialData) + } else { + fetchInformations() + } + }, []) + + useEffect(() => { + if (searchQuery !== "") { + fetchInformations() + } else if (initialData.length > 0) { + setInformations(initialData) + } + }, [searchQuery]) + + return ( +
+ {/* 검색 */} +
+
+
+ + setSearchQuery(e.target.value)} + className="pl-10" + onKeyPress={(e) => e.key === "Enter" && handleSearch()} + /> +
+ + +
+
+ + {/* 정보 테이블 */} +
+ + + + + + + + + + 정보 내용 + 첨부파일 + 상태 + + + + 작업 + + + + {loading ? ( + + + 로딩 중... + + + ) : informations.length === 0 ? ( + + + 정보가 없습니다. + + + ) : ( + sortedInformations.map((information) => ( + + +
+ + + {information.pageName} + +
+
+ + + {information.pagePath} + + + +
+ + + {information.attachmentFileName ? ( + + ) : ( + 없음 + )} + + + + {information.isActive ? "활성" : "비활성"} + + + + {formatDate(information.createdAt)} + + + + + + )) + )} + +
+
+ + {/* 편집 다이얼로그 */} + {editingInformation && ( + + )} +
+ ) +} \ No newline at end of file diff --git a/components/items-tech/item-tech-container.tsx b/components/items-tech/item-tech-container.tsx index c09f684a..260f3e64 100644 --- a/components/items-tech/item-tech-container.tsx +++ b/components/items-tech/item-tech-container.tsx @@ -11,7 +11,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" - +import { InformationButton } from "@/components/information/information-button" interface ItemType { id: string name: string @@ -56,7 +56,10 @@ export function ItemTechContainer({
{/* 왼쪽: 타이틀 & 설명 */}
-

자재 관리

+
+

자재 관리

+ +

조선 및 해양 자재를 등록하고 관리할 수 있습니다.

diff --git a/components/notice/notice-client.tsx b/components/notice/notice-client.tsx new file mode 100644 index 00000000..fab0d758 --- /dev/null +++ b/components/notice/notice-client.tsx @@ -0,0 +1,438 @@ +"use client" + +import { useState, useEffect, useTransition } from "react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" +import { + Search, + Edit, + FileText, + ChevronUp, + ChevronDown, + Plus, + Eye, + Trash2 +} from "lucide-react" +import { toast } from "sonner" +import { formatDate } from "@/lib/utils" +import { getNoticeLists, deleteNotice, getPagePathList } from "@/lib/notice/service" +import type { Notice } from "@/db/schema/notice" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { UpdateNoticeSheet } from "./notice-edit-sheet" +import { NoticeCreateDialog } from "./notice-create-dialog" +import { NoticeViewDialog } from "./notice-view-dialog" + +type NoticeWithAuthor = Notice & { + authorName: string | null + authorEmail: string | null +} + +interface NoticeClientProps { + initialData?: NoticeWithAuthor[] + currentUserId?: number +} + +type SortField = "title" | "pagePath" | "createdAt" +type SortDirection = "asc" | "desc" + +export function NoticeClient({ initialData = [], currentUserId }: NoticeClientProps) { + const [notices, setNotices] = useState(initialData) + const [loading, setLoading] = useState(false) + const [searchQuery, setSearchQuery] = useState("") + const [sortField, setSortField] = useState("createdAt") + const [sortDirection, setSortDirection] = useState("desc") + const [, startTransition] = useTransition() + const [isEditSheetOpen, setIsEditSheetOpen] = useState(false) + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) + const [isViewDialogOpen, setIsViewDialogOpen] = useState(false) + const [selectedNotice, setSelectedNotice] = useState(null) + const [pagePathOptions, setPagePathOptions] = useState>([]) + // 공지사항 목록 조회 + const fetchNotices = async () => { + try { + setLoading(true) + const search = searchQuery || undefined + + startTransition(async () => { + const result = await getNoticeLists({ + page: 1, + perPage: 50, + search: search, + sort: [{ id: sortField, desc: sortDirection === "desc" }], + flags: [], + filters: [], + joinOperator: "and", + pagePath: "", + title: "", + content: "", + authorId: null, + isActive: null, + from: "", + to: "", + }) + + if (result?.data) { + setNotices(result.data) + } else { + toast.error("공지사항 목록을 가져오는데 실패했습니다.") + } + setLoading(false) + }) + } catch (error) { + console.error("Error fetching notices:", error) + toast.error("공지사항 목록을 가져오는데 실패했습니다.") + setLoading(false) + } + } + + // 검색 핸들러 + const handleSearch = () => { + fetchNotices() + } + + // 정렬 함수 + const sortNotices = (notices: NoticeWithAuthor[]) => { + return [...notices].sort((a, b) => { + let aValue: string | Date + let bValue: string | Date + + if (sortField === "title") { + aValue = a.title + bValue = b.title + } else if (sortField === "pagePath") { + aValue = a.pagePath + bValue = b.pagePath + } else { + aValue = new Date(a.createdAt) + bValue = new Date(b.createdAt) + } + + if (aValue < bValue) { + return sortDirection === "asc" ? -1 : 1 + } + if (aValue > bValue) { + return sortDirection === "asc" ? 1 : -1 + } + return 0 + }) + } + + // 정렬 핸들러 + const handleSort = (field: SortField) => { + if (sortField === field) { + setSortDirection(sortDirection === "asc" ? "desc" : "asc") + } else { + setSortField(field) + setSortDirection("asc") + } + } + + // 삭제 핸들러 + const handleDelete = async (notice: NoticeWithAuthor) => { + try { + const result = await deleteNotice(notice.id) + + if (result.success) { + toast.success(result.message) + setNotices(notices.filter(n => n.id !== notice.id)) + } else { + toast.error(result.message) + } + } catch (error) { + console.error("Error deleting notice:", error) + toast.error("공지사항 삭제에 실패했습니다.") + } + } + + // 정렬된 공지사항 목록 + const sortedNotices = sortNotices(notices) + + // 페이지 경로 옵션 로딩 + const loadPagePathOptions = async () => { + try { + const paths = await getPagePathList() + const options = paths.map(path => ({ + value: path.pagePath, + label: `${path.pageName} (${path.pagePath})` + })) + setPagePathOptions(options) + } catch (error) { + console.error("페이지 경로 로딩 실패:", error) + } + } + + // View 다이얼로그 열기 + const handleViewNotice = (notice: NoticeWithAuthor) => { + setSelectedNotice(notice) + setIsViewDialogOpen(true) + } + + // Edit Sheet 열기 + const handleEditNotice = (notice: NoticeWithAuthor) => { + setSelectedNotice(notice) + setIsEditSheetOpen(true) + } + + // Create Dialog 열기 + const handleCreateNotice = () => { + setIsCreateDialogOpen(true) + } + + useEffect(() => { + if (initialData.length > 0) { + setNotices(initialData) + } else { + fetchNotices() + } + loadPagePathOptions() + }, []) + + useEffect(() => { + if (searchQuery !== "") { + fetchNotices() + } else if (initialData.length > 0) { + setNotices(initialData) + } + }, [searchQuery]) + + return ( +
+ {/* 검색 및 추가 버튼 */} +
+
+
+ + setSearchQuery(e.target.value)} + className="pl-10" + onKeyPress={(e) => e.key === "Enter" && handleSearch()} + /> +
+ + +
+ +
+ + {/* 공지사항 테이블 */} +
+ + + + + + + + + + 작성자 + 상태 + + + + 작업 + + + + {loading ? ( + + + 로딩 중... + + + ) : notices.length === 0 ? ( + + + 공지사항이 없습니다. + + + ) : ( + sortedNotices.map((notice) => ( + + +
+ + + {notice.title} + +
+
+ + + {notice.pagePath} + + + +
+ + {notice.authorName || "알 수 없음"} + + {notice.authorEmail && ( + + {notice.authorEmail} + + )} +
+
+ + + {notice.isActive ? "활성" : "비활성"} + + + + {formatDate(notice.createdAt)} + + +
+ {/* View 버튼 - 다이얼로그 방식 */} + + + {/* Edit 버튼 - 다이얼로그 방식 */} + + + {/* 기존 페이지 방식 (비교용) + + + */} + + + + + + + + 공지사항 삭제 + + 이 공지사항을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. + + + + 취소 + handleDelete(notice)} + className="bg-red-600 hover:bg-red-700" + > + 삭제 + + + + +
+
+
+ )) + )} +
+
+
+ + {/* 다이얼로그들과 시트 - 테이블 밖에서 단일 렌더링 */} + + + + + +
+ ) +} \ No newline at end of file diff --git a/components/notice/notice-create-dialog.tsx b/components/notice/notice-create-dialog.tsx new file mode 100644 index 00000000..e3ce16a1 --- /dev/null +++ b/components/notice/notice-create-dialog.tsx @@ -0,0 +1,216 @@ +"use client" + +import * as React from "react" +import { useState } from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { toast } from "sonner" +import { Loader } from "lucide-react" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Switch } from "@/components/ui/switch" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import TiptapEditor from "@/components/qna/tiptap-editor" +import { createNotice } from "@/lib/notice/service" +import { createNoticeSchema, type CreateNoticeSchema } from "@/lib/notice/validations" + +interface NoticeCreateDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + pagePathOptions: Array<{ value: string; label: string }> + currentUserId?: number + onSuccess?: () => void +} + +export function NoticeCreateDialog({ + open, + onOpenChange, + pagePathOptions, + currentUserId, + onSuccess, +}: NoticeCreateDialogProps) { + const [isLoading, setIsLoading] = useState(false) + + const form = useForm({ + resolver: zodResolver(createNoticeSchema), + defaultValues: { + pagePath: "", + title: "", + content: "", + authorId: currentUserId, + isActive: true, + }, + }) + + React.useEffect(() => { + if (open) { + // 다이얼로그가 열릴 때마다 폼 초기화 + form.reset({ + pagePath: "", + title: "", + content: "", + authorId: currentUserId, + isActive: true, + }) + } + }, [open, currentUserId, form]) + + const onSubmit = async (values: CreateNoticeSchema) => { + setIsLoading(true) + console.log("Form values:", values) // 디버깅용 + try { + const result = await createNotice(values) + console.log("Create result:", result) // 디버깅용 + + if (result.success) { + toast.success(result.message || "공지사항이 성공적으로 생성되었습니다.") + if (onSuccess) onSuccess() + onOpenChange(false) + } else { + toast.error(result.message || "공지사항 생성에 실패했습니다.") + console.error("Create failed:", result.message) + } + } catch (error) { + toast.error("공지사항 생성에 실패했습니다.") + console.error("Create error:", error) + } finally { + setIsLoading(false) + } + } + + + + return ( + + + + + 새 공지사항 작성 + + + +
+ +
+ ( + + 페이지 경로 * + + + + )} + /> + + ( + +
+ 활성 상태 +
+ 활성화하면 해당 페이지에서 공지사항이 표시됩니다. +
+
+ + + +
+ )} + /> +
+ + ( + + 제목 * + + + + + + )} + /> + + ( + + 내용 * + +
+ +
+
+ +
+ )} + /> + +
+ + +
+ + +
+
+ ) +} \ No newline at end of file diff --git a/components/notice/notice-edit-sheet.tsx b/components/notice/notice-edit-sheet.tsx new file mode 100644 index 00000000..91bcae3b --- /dev/null +++ b/components/notice/notice-edit-sheet.tsx @@ -0,0 +1,246 @@ +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { toast } from "sonner" + +import { Button } from "@/components/ui/button" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Switch } from "@/components/ui/switch" + +import { updateNoticeSchema, type UpdateNoticeSchema } from "@/lib/notice/validations" +import type { Notice } from "@/db/schema/notice" +import { updateNoticeData } from "@/lib/notice/service" +import TiptapEditor from "@/components/qna/tiptap-editor" + +type NoticeWithAuthor = Notice & { + authorName: string | null + authorEmail: string | null +} + +interface UpdateNoticeSheetProps { + open: boolean + onOpenChange: (open: boolean) => void + notice: NoticeWithAuthor | null + pagePathOptions: Array<{ value: string; label: string }> + onSuccess?: () => void +} + +export function UpdateNoticeSheet({ + open, + onOpenChange, + notice, + pagePathOptions, + onSuccess +}: UpdateNoticeSheetProps) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + + const form = useForm({ + resolver: zodResolver(updateNoticeSchema), + defaultValues: { + id: 0, + pagePath: "", + title: "", + content: "", + isActive: true, + }, + }) + + // notice 데이터가 변경될 때 폼 초기화 + React.useEffect(() => { + if (notice) { + form.reset({ + id: notice.id, + pagePath: notice.pagePath, + title: notice.title, + content: notice.content, + isActive: notice.isActive, + }) + } + }, [notice, form]) + + function onSubmit(input: UpdateNoticeSchema) { + if (!notice) return + + startUpdateTransition(async () => { + try { + const result = await updateNoticeData(input) + + if (result.success) { + toast.success(result.message || "공지사항이 성공적으로 수정되었습니다.") + if (onSuccess) onSuccess() + onOpenChange(false) + } else { + toast.error(result.message || "공지사항 수정에 실패했습니다.") + } + } catch (error) { + toast.error("예기치 못한 오류가 발생했습니다.") + console.error("공지사항 수정 오류:", error) + } + }) + } + + return ( + + + + 공지사항 수정 + + 공지사항의 제목과 내용을 수정할 수 있습니다. 수정된 내용은 즉시 반영됩니다. + + + +
+ + {/* 공지사항 정보 표시 */} + {notice && ( +
+
공지사항 정보
+
+
작성자: {notice.authorName} ({notice.authorEmail})
+
작성일: {new Date(notice.createdAt).toLocaleDateString("ko-KR")}
+
수정일: {new Date(notice.updatedAt).toLocaleDateString("ko-KR")}
+
상태: {notice.isActive ? "활성" : "비활성"}
+
+
+ )} + + {/* 페이지 경로 선택 */} + ( + + 페이지 경로 * + + + + )} + /> + + {/* 제목 입력 */} + ( + + 제목 * + + + + + + )} + /> + + {/* 활성 상태 */} + ( + +
+ 활성 상태 +
+ 활성화하면 해당 페이지에서 공지사항이 표시됩니다. +
+
+ + + +
+ )} + /> + + {/* 내용 입력 (리치텍스트 에디터) */} + ( + + 내용 * + + + + + + )} + /> + + + + + + + +
+
+ ) +} \ No newline at end of file diff --git a/components/notice/notice-view-dialog.tsx b/components/notice/notice-view-dialog.tsx new file mode 100644 index 00000000..9b42311a --- /dev/null +++ b/components/notice/notice-view-dialog.tsx @@ -0,0 +1,56 @@ +"use client" + +import * as React from "react" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import type { Notice } from "@/db/schema/notice" + +type NoticeWithAuthor = Notice & { + authorName: string | null + authorEmail: string | null +} + +interface NoticeViewDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + notice: NoticeWithAuthor | null +} + +export function NoticeViewDialog({ + open, + onOpenChange, + notice, +}: NoticeViewDialogProps) { + + if (!notice) return null + + return ( + + + +
+
+
+ + 제목: {notice.title} + +
+
+
+
+ +
+
+
+ + +
+ ) +} \ No newline at end of file diff --git a/lib/notice/repository.ts b/lib/notice/repository.ts new file mode 100644 index 00000000..84e64f00 --- /dev/null +++ b/lib/notice/repository.ts @@ -0,0 +1,244 @@ +import { asc, desc, eq, ilike, and, count, sql } from "drizzle-orm" +import db from "@/db/db" +import { notice, users, type Notice, type NewNotice } from "@/db/schema" + +// 최신 패턴: 트랜잭션을 지원하는 공지사항 조회 +export async function selectNoticeLists( + tx: typeof db, + params: { + where?: ReturnType + orderBy?: (ReturnType | ReturnType)[] + offset?: number + limit?: number + } +) { + const { where, orderBy, offset = 0, limit = 10 } = params + + return tx + .select({ + id: notice.id, + pagePath: notice.pagePath, + title: notice.title, + content: notice.content, + authorId: notice.authorId, + isActive: notice.isActive, + createdAt: notice.createdAt, + updatedAt: notice.updatedAt, + authorName: users.name, + authorEmail: users.email, + }) + .from(notice) + .leftJoin(users, eq(notice.authorId, users.id)) + .where(where) + .orderBy(...(orderBy ?? [desc(notice.createdAt)])) + .offset(offset) + .limit(limit) +} + +// 최신 패턴: 트랜잭션을 지원하는 카운트 조회 +export async function countNoticeLists( + tx: typeof db, + where?: ReturnType +) { + const res = await tx + .select({ count: count() }) + .from(notice) + .where(where) + + return res[0]?.count ?? 0 +} + +// 기존 패턴 (하위 호환성을 위해 유지) +export async function selectNotice(input: { page: number; per_page: number; sort?: string; pagePath?: string; title?: string; authorId?: number; isActive?: boolean; from?: string; to?: string }) { + const { page, per_page = 50, sort, pagePath, title, authorId, isActive, from, to } = input + + const conditions = [] + + if (pagePath) { + conditions.push(ilike(notice.pagePath, `%${pagePath}%`)) + } + + if (title) { + conditions.push(ilike(notice.title, `%${title}%`)) + } + + if (authorId) { + conditions.push(eq(notice.authorId, authorId)) + } + + if (isActive !== null && isActive !== undefined) { + conditions.push(eq(notice.isActive, isActive)) + } + + if (from) { + conditions.push(sql`${notice.createdAt} >= ${from}`) + } + + if (to) { + conditions.push(sql`${notice.createdAt} <= ${to}`) + } + + const offset = (page - 1) * per_page + + // 정렬 설정 + let orderBy = desc(notice.createdAt); + + if (sort && Array.isArray(sort) && sort.length > 0) { + const sortItem = sort[0]; + if (sortItem.id === "createdAt") { + orderBy = sortItem.desc ? desc(notice.createdAt) : asc(notice.createdAt); + } + } + + const whereClause = conditions.length > 0 ? and(...conditions) : undefined + + const data = await db + .select({ + id: notice.id, + pagePath: notice.pagePath, + title: notice.title, + content: notice.content, + authorId: notice.authorId, + isActive: notice.isActive, + createdAt: notice.createdAt, + updatedAt: notice.updatedAt, + authorName: users.name, + authorEmail: users.email, + }) + .from(notice) + .leftJoin(users, eq(notice.authorId, users.id)) + .where(whereClause) + .orderBy(orderBy) + .limit(per_page) + .offset(offset) + + return data +} + +// 기존 패턴: 공지사항 총 개수 조회 +export async function countNotice(input: { pagePath?: string; title?: string; authorId?: number; isActive?: boolean; from?: string; to?: string }) { + const { pagePath, title, authorId, isActive, from, to } = input + + const conditions = [] + + if (pagePath) { + conditions.push(ilike(notice.pagePath, `%${pagePath}%`)) + } + + if (title) { + conditions.push(ilike(notice.title, `%${title}%`)) + } + + if (authorId) { + conditions.push(eq(notice.authorId, authorId)) + } + + if (isActive !== null && isActive !== undefined) { + conditions.push(eq(notice.isActive, isActive)) + } + + if (from) { + conditions.push(sql`${notice.createdAt} >= ${from}`) + } + + if (to) { + conditions.push(sql`${notice.createdAt} <= ${to}`) + } + + const whereClause = conditions.length > 0 ? and(...conditions) : undefined + + const result = await db + .select({ count: count() }) + .from(notice) + .where(whereClause) + + return result[0]?.count ?? 0 +} + +// 페이지 경로별 공지사항 조회 (활성화된 것만, 작성자 정보 포함) +export async function getNoticesByPagePath(pagePath: string): Promise> { + const result = await db + .select({ + id: notice.id, + pagePath: notice.pagePath, + title: notice.title, + content: notice.content, + authorId: notice.authorId, + isActive: notice.isActive, + createdAt: notice.createdAt, + updatedAt: notice.updatedAt, + authorName: users.name, + authorEmail: users.email, + }) + .from(notice) + .leftJoin(users, eq(notice.authorId, users.id)) + .where(and( + eq(notice.pagePath, pagePath), + eq(notice.isActive, true) + )) + .orderBy(desc(notice.createdAt)) + + return result +} + +// 공지사항 생성 +export async function insertNotice(data: NewNotice): Promise { + const result = await db + .insert(notice) + .values(data) + .returning() + + return result[0] +} + +// 공지사항 수정 +export async function updateNotice(id: number, data: Partial): Promise { + const result = await db + .update(notice) + .set({ ...data, updatedAt: new Date() }) + .where(eq(notice.id, id)) + .returning() + + return result[0] || null +} + +// 공지사항 삭제 +export async function deleteNoticeById(id: number): Promise { + const result = await db + .delete(notice) + .where(eq(notice.id, id)) + + return (result.rowCount ?? 0) > 0 +} + +// 공지사항 다중 삭제 +export async function deleteNoticeByIds(ids: number[]): Promise { + const result = await db + .delete(notice) + .where(sql`${notice.id} = ANY(${ids})`) + + return result.rowCount ?? 0 +} + +// ID로 공지사항 조회 (작성자 정보 포함) +export async function getNoticeById(id: number): Promise<(Notice & { authorName: string | null; authorEmail: string | null }) | null> { + const result = await db + .select({ + id: notice.id, + pagePath: notice.pagePath, + title: notice.title, + content: notice.content, + authorId: notice.authorId, + isActive: notice.isActive, + createdAt: notice.createdAt, + updatedAt: notice.updatedAt, + authorName: users.name, + authorEmail: users.email, + }) + .from(notice) + .leftJoin(users, eq(notice.authorId, users.id)) + .where(eq(notice.id, id)) + .limit(1) + + return result[0] || null +} \ No newline at end of file diff --git a/lib/notice/service.ts b/lib/notice/service.ts new file mode 100644 index 00000000..24b03fe9 --- /dev/null +++ b/lib/notice/service.ts @@ -0,0 +1,324 @@ +"use server" + +import { revalidateTag, unstable_noStore } from "next/cache" +import { getErrorMessage } from "@/lib/handle-error" +import { unstable_cache } from "@/lib/unstable-cache" +import { filterColumns } from "@/lib/filter-columns" +import { asc, desc, ilike, and, or, eq } from "drizzle-orm" +import db from "@/db/db" +import { notice, pageInformation } from "@/db/schema" + +import type { + CreateNoticeSchema, + UpdateNoticeSchema, + GetNoticeSchema +} from "./validations" + +import { + selectNotice, + countNotice, + getNoticesByPagePath, + insertNotice, + updateNotice, + deleteNoticeById, + deleteNoticeByIds, + getNoticeById, + selectNoticeLists, + countNoticeLists +} from "./repository" + +import type { Notice } from "@/db/schema/notice" + +export async function getNoticeLists(input: GetNoticeSchema) { + return unstable_cache( + async () => { + try { + // 고급 검색 로직 + const { page, perPage, search, filters, joinOperator, pagePath, title, content, authorId, isActive } = input + + // 기본 검색 조건들 + const conditions = [] + + // 검색어가 있으면 여러 필드에서 검색 + if (search && search.trim()) { + const searchConditions = [ + ilike(notice.pagePath, `%${search}%`), + ilike(notice.title, `%${search}%`), + ilike(notice.content, `%${search}%`) + ] + conditions.push(or(...searchConditions)) + } + + // 개별 필드 조건들 + if (pagePath && pagePath.trim()) { + conditions.push(ilike(notice.pagePath, `%${pagePath}%`)) + } + + if (title && title.trim()) { + conditions.push(ilike(notice.title, `%${title}%`)) + } + + if (content && content.trim()) { + conditions.push(ilike(notice.content, `%${content}%`)) + } + + if (authorId !== null && authorId !== undefined) { + conditions.push(eq(notice.authorId, authorId)) + } + + if (isActive !== null && isActive !== undefined) { + conditions.push(eq(notice.isActive, isActive)) + } + // 고급 필터 처리 + if (filters && filters.length > 0) { + const advancedConditions = filters.map(() => + filterColumns({ + table: notice, + filters: filters, + joinOperator: joinOperator, + }) + ) + + if (advancedConditions.length > 0) { + if (joinOperator === "or") { + conditions.push(or(...advancedConditions)) + } else { + conditions.push(and(...advancedConditions)) + } + } + } + + // 전체 WHERE 조건 조합 + const finalWhere = conditions.length > 0 + ? (joinOperator === "or" ? or(...conditions) : and(...conditions)) + : undefined + + // 페이지네이션 + const offset = (page - 1) * perPage + + // 정렬 처리 + const orderBy = input.sort.length > 0 + ? input.sort.map((item) => { + if (item.id === "createdAt") { + return item.desc ? desc(notice.createdAt) : asc(notice.createdAt) + } else if (item.id === "updatedAt") { + return item.desc ? desc(notice.updatedAt) : asc(notice.updatedAt) + } else if (item.id === "pagePath") { + return item.desc ? desc(notice.pagePath) : asc(notice.pagePath) + } else if (item.id === "title") { + return item.desc ? desc(notice.title) : asc(notice.title) + } else if (item.id === "authorId") { + return item.desc ? desc(notice.authorId) : asc(notice.authorId) + } else if (item.id === "isActive") { + return item.desc ? desc(notice.isActive) : asc(notice.isActive) + } else { + return desc(notice.createdAt) // 기본값 + } + }) + : [desc(notice.createdAt)] + + // 트랜잭션 내부에서 Repository 호출 + const { data, total } = await db.transaction(async (tx) => { + const data = await selectNoticeLists(tx, { + where: finalWhere, + orderBy, + offset, + limit: input.perPage, + }) + + const total = await countNoticeLists(tx, finalWhere) + return { data, total } + }) + + const pageCount = Math.ceil(total / input.perPage) + + return { data, pageCount, total } + } catch (err) { + console.error("Failed to get notice lists:", err) + // 에러 발생 시 기본값 반환 + return { data: [], pageCount: 0, total: 0 } + } + }, + [JSON.stringify(input)], + { + revalidate: 3600, + tags: ["notice-lists"], + } + )() +} + +// 기존 패턴 (하위 호환성을 위해 유지) +export async function getNoticeList(input: Partial<{ page: number; per_page: number; sort?: string; pagePath?: string; title?: string; authorId?: number; isActive?: boolean; from?: string; to?: string }> & { page: number; per_page: number }) { + unstable_noStore() + + try { + const [data, total] = await Promise.all([ + selectNotice(input), + countNotice(input) + ]) + + const pageCount = Math.ceil(total / input.per_page) + + return { + data, + pageCount, + total + } + } catch (error) { + console.error("Failed to get notice list:", error) + throw new Error(getErrorMessage(error)) + } +} + +// 페이지별 공지사항 조회 (일반 사용자용) +export async function getPageNotices(pagePath: string): Promise> { + try { + return await getNoticesByPagePath(pagePath) + } catch (error) { + console.error(`Failed to get notices for page ${pagePath}:`, error) + return [] + } +} + +// 캐시된 페이지별 공지사항 조회 +export const getCachedPageNotices = unstable_cache( + async (pagePath: string) => getPageNotices(pagePath), + ["page-notices"], + { + tags: ["page-notices"], + revalidate: 3600, // 1시간 캐시 + } +) + +// 공지사항 생성 +export async function createNotice(input: CreateNoticeSchema) { + try { + const result = await insertNotice(input) + + revalidateTag("page-notices") + revalidateTag("notice-lists") + + return { + success: true, + data: result, + message: "공지사항이 성공적으로 생성되었습니다." + } + } catch (error) { + console.error("Failed to create notice:", error) + return { + success: false, + message: getErrorMessage(error) + } + } +} + +// 공지사항 수정 +export async function updateNoticeData(input: UpdateNoticeSchema) { + try { + const { id, ...updateData } = input + const result = await updateNotice(id, updateData) + + if (!result) { + return { + success: false, + message: "공지사항을 찾을 수 없거나 수정에 실패했습니다." + } + } + + revalidateTag("page-notices") + revalidateTag("notice-lists") + + return { + success: true, + message: "공지사항이 성공적으로 수정되었습니다." + } + } catch (error) { + console.error("Failed to update notice:", error) + return { + success: false, + message: getErrorMessage(error) + } + } +} + +// 공지사항 삭제 +export async function deleteNotice(id: number) { + try { + const success = await deleteNoticeById(id) + + if (!success) { + return { + success: false, + message: "공지사항을 찾을 수 없거나 삭제에 실패했습니다." + } + } + + revalidateTag("page-notices") + revalidateTag("notice-lists") + + return { + success: true, + message: "공지사항이 성공적으로 삭제되었습니다." + } + } catch (error) { + console.error("Failed to delete notice:", error) + return { + success: false, + message: getErrorMessage(error) + } + } +} + +// 공지사항 다중 삭제 +export async function deleteMultipleNotices(ids: number[]) { + try { + const deletedCount = await deleteNoticeByIds(ids) + + revalidateTag("page-notices") + revalidateTag("notice-lists") + + return { + success: true, + deletedCount, + message: `${deletedCount}개의 공지사항이 성공적으로 삭제되었습니다.` + } + } catch (error) { + console.error("Failed to delete multiple notices:", error) + return { + success: false, + message: getErrorMessage(error) + } + } +} + +// ID로 공지사항 조회 +export async function getNoticeDetail(id: number): Promise<(Notice & { authorName: string | null; authorEmail: string | null }) | null> { + try { + return await getNoticeById(id) + } catch (error) { + console.error(`Failed to get notice detail for id ${id}:`, error) + return null + } +} + +// pagePath 목록 조회 (정보 시스템에서 사용) +export async function getPagePathList(): Promise> { + try { + const result = await db + .selectDistinct({ + pagePath: pageInformation.pagePath, + pageName: pageInformation.pageName + }) + .from(pageInformation) + .where(eq(pageInformation.isActive, true)) + .orderBy(asc(pageInformation.pagePath)) + + return result.map(item => ({ + pagePath: item.pagePath, + pageName: item.pageName || item.pagePath + })) + } catch (error) { + console.error("Failed to get page path list:", error) + return [] + } +} \ No newline at end of file diff --git a/lib/notice/validations.ts b/lib/notice/validations.ts new file mode 100644 index 00000000..05e84af9 --- /dev/null +++ b/lib/notice/validations.ts @@ -0,0 +1,80 @@ +import { z } from "zod" +import { + createSearchParamsCache, + parseAsArrayOf, + parseAsInteger, + parseAsString, + parseAsStringEnum, + parseAsBoolean, +} from "nuqs/server" +import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers" +import { Notice } from "@/db/schema/notice" + +// 공지사항 생성 스키마 +export const createNoticeSchema = z.object({ + pagePath: z.string().min(1, "페이지 경로를 입력해주세요"), + title: z.string().min(1, "제목을 입력해주세요"), + content: z.string().min(1, "내용을 입력해주세요"), + authorId: z.number().min(1, "작성자를 선택해주세요"), + isActive: z.boolean().default(true), +}) + +// 공지사항 수정 스키마 +export const updateNoticeSchema = z.object({ + id: z.number(), + pagePath: z.string().min(1, "페이지 경로를 입력해주세요"), + title: z.string().min(1, "제목을 입력해주세요"), + content: z.string().min(1, "내용을 입력해주세요"), + isActive: z.boolean().default(true), +}) + +// 현대적인 검색 파라미터 캐시 +export const searchParamsNoticeCache = createSearchParamsCache({ + flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]), + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(10), + sort: getSortingStateParser().withDefault([ + { id: "createdAt", desc: true }, + ]), + + // 기본 검색 필드들 + pagePath: parseAsString.withDefault(""), + title: parseAsString.withDefault(""), + content: parseAsString.withDefault(""), + authorId: parseAsInteger, + isActive: parseAsBoolean, + + // 고급 필터 + filters: getFiltersStateParser().withDefault([]), + joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), + search: parseAsString.withDefault(""), + + // 날짜 범위 + from: parseAsString.withDefault(""), + to: parseAsString.withDefault(""), +}) + +// 타입 추출 +export type CreateNoticeSchema = z.infer +export type UpdateNoticeSchema = z.infer +export type GetNoticeSchema = Awaited> + +// 기존 스키마 (하위 호환성을 위해 유지) +export const getNoticeSchema = z.object({ + page: z.coerce.number().default(1), + per_page: z.coerce.number().default(10), + sort: z.string().optional(), + pagePath: z.string().optional(), + title: z.string().optional(), + authorId: z.coerce.number().optional(), + isActive: z.coerce.boolean().optional(), + from: z.string().optional(), + to: z.string().optional(), +}) + +// 페이지 경로별 공지사항 조회 스키마 +export const getPageNoticeSchema = z.object({ + pagePath: z.string().min(1, "페이지 경로를 입력해주세요"), +}) + +export type GetPageNoticeSchema = z.infer \ No newline at end of file -- cgit v1.2.3 From 4c15b99d9586aa48693213c78c02fba4639ebb85 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 1 Jul 2025 11:47:47 +0000 Subject: (최겸) 인포메이션 기능 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/informationColumnsConfig.ts | 27 +- config/noticeColumnsConfig.ts | 54 ++++ lib/information/repository.ts | 58 +--- lib/information/service.ts | 224 +++++--------- lib/information/table/add-information-dialog.tsx | 329 --------------------- .../table/delete-information-dialog.tsx | 125 -------- .../table/information-table-columns.tsx | 248 ---------------- .../table/information-table-toolbar-actions.tsx | 25 -- lib/information/table/information-table.tsx | 148 --------- .../table/update-information-dialog.tsx | 124 ++------ lib/information/validations.ts | 32 +- 11 files changed, 184 insertions(+), 1210 deletions(-) create mode 100644 config/noticeColumnsConfig.ts delete mode 100644 lib/information/table/add-information-dialog.tsx delete mode 100644 lib/information/table/delete-information-dialog.tsx delete mode 100644 lib/information/table/information-table-columns.tsx delete mode 100644 lib/information/table/information-table-toolbar-actions.tsx delete mode 100644 lib/information/table/information-table.tsx diff --git a/config/informationColumnsConfig.ts b/config/informationColumnsConfig.ts index 6357cfa3..508cb846 100644 --- a/config/informationColumnsConfig.ts +++ b/config/informationColumnsConfig.ts @@ -10,9 +10,9 @@ export interface InformationColumnConfig { export const informationColumnsConfig: InformationColumnConfig[] = [ { - id: "pageCode", - label: "페이지 코드", - excelHeader: "페이지 코드", + id: "pagePath", + label: "페이지 경로", + excelHeader: "페이지 경로", }, { id: "pageName", @@ -20,24 +20,9 @@ export const informationColumnsConfig: InformationColumnConfig[] = [ excelHeader: "페이지명", }, { - id: "title", - label: "제목", - excelHeader: "제목", - }, - { - id: "description", - label: "설명", - excelHeader: "설명", - }, - { - id: "noticeTitle", - label: "공지사항 제목", - excelHeader: "공지사항 제목", - }, - { - id: "noticeContent", - label: "공지사항 내용", - excelHeader: "공지사항 내용", + id: "informationContent", + label: "내용", + excelHeader: "내용", }, { id: "attachmentFileName", diff --git a/config/noticeColumnsConfig.ts b/config/noticeColumnsConfig.ts new file mode 100644 index 00000000..9e9565fb --- /dev/null +++ b/config/noticeColumnsConfig.ts @@ -0,0 +1,54 @@ +import { Notice } from "@/db/schema/notice" + +export interface NoticeColumnConfig { + id: keyof Notice + label: string + group?: string + excelHeader?: string + type?: string +} + +export const noticeColumnsConfig: NoticeColumnConfig[] = [ + { + id: "id", + label: "ID", + excelHeader: "ID", + }, + { + id: "pagePath", + label: "페이지 경로", + excelHeader: "페이지 경로", + }, + { + id: "title", + label: "제목", + excelHeader: "제목", + }, + { + id: "content", + label: "내용", + excelHeader: "내용", + }, + { + id: "authorId", + label: "작성자 ID", + excelHeader: "작성자 ID", + }, + { + id: "isActive", + label: "활성 상태", + excelHeader: "활성 상태", + }, + { + id: "createdAt", + label: "생성일", + excelHeader: "생성일", + type: "date", + }, + { + id: "updatedAt", + label: "수정일", + excelHeader: "수정일", + type: "date", + }, +] \ No newline at end of file diff --git a/lib/information/repository.ts b/lib/information/repository.ts index 2a3bc1c0..f640a4c6 100644 --- a/lib/information/repository.ts +++ b/lib/information/repository.ts @@ -40,19 +40,15 @@ export async function countInformationLists( // 기존 패턴 (하위 호환성을 위해 유지) export async function selectInformation(input: GetInformationSchema) { - const { page, per_page = 50, sort, pageCode, pageName, isActive, from, to } = input + const { page, per_page = 50, sort, pagePath, isActive, from, to } = input const conditions = [] - if (pageCode) { - conditions.push(ilike(pageInformation.pageCode, `%${pageCode}%`)) + if (pagePath) { + conditions.push(ilike(pageInformation.pagePath, `%${pagePath}%`)) } - if (pageName) { - conditions.push(ilike(pageInformation.pageName, `%${pageName}%`)) - } - - if (isActive !== null) { + if (isActive !== null && isActive !== undefined) { conditions.push(eq(pageInformation.isActive, isActive)) } @@ -91,19 +87,15 @@ export async function selectInformation(input: GetInformationSchema) { // 기존 패턴: 인포메이션 총 개수 조회 export async function countInformation(input: GetInformationSchema) { - const { pageCode, pageName, isActive, from, to } = input + const { pagePath, isActive, from, to } = input const conditions = [] - if (pageCode) { - conditions.push(ilike(pageInformation.pageCode, `%${pageCode}%`)) + if (pagePath) { + conditions.push(ilike(pageInformation.pagePath, `%${pagePath}%`)) } - if (pageName) { - conditions.push(ilike(pageInformation.pageName, `%${pageName}%`)) - } - - if (isActive !== null) { + if (isActive !== null && isActive !== undefined) { conditions.push(eq(pageInformation.isActive, isActive)) } @@ -125,13 +117,13 @@ export async function countInformation(input: GetInformationSchema) { return result[0]?.count ?? 0 } -// 페이지 코드별 인포메이션 조회 (활성화된 것만) -export async function getInformationByPageCode(pageCode: string): Promise { +// 페이지 경로별 인포메이션 조회 (활성화된 것만) +export async function getInformationByPagePath(pagePath: string): Promise { const result = await db .select() .from(pageInformation) .where(and( - eq(pageInformation.pageCode, pageCode), + eq(pageInformation.pagePath, pagePath), eq(pageInformation.isActive, true) )) .limit(1) @@ -139,16 +131,6 @@ export async function getInformationByPageCode(pageCode: string): Promise { - const result = await db - .insert(pageInformation) - .values(data) - .returning() - - return result[0] -} - // 인포메이션 수정 export async function updateInformation(id: number, data: Partial): Promise { const result = await db @@ -160,24 +142,6 @@ export async function updateInformation(id: number, data: Partial { - const result = await db - .delete(pageInformation) - .where(eq(pageInformation.id, id)) - - return (result.rowCount ?? 0) > 0 -} - -// 인포메이션 다중 삭제 -export async function deleteInformationByIds(ids: number[]): Promise { - const result = await db - .delete(pageInformation) - .where(sql`${pageInformation.id} = ANY(${ids})`) - - return result.rowCount ?? 0 -} - // ID로 인포메이션 조회 export async function getInformationById(id: number): Promise { const result = await db diff --git a/lib/information/service.ts b/lib/information/service.ts index 8f1e5679..30a651f1 100644 --- a/lib/information/service.ts +++ b/lib/information/service.ts @@ -9,7 +9,6 @@ import db from "@/db/db" import { pageInformation, menuAssignments } from "@/db/schema" import type { - CreateInformationSchema, UpdateInformationSchema, GetInformationSchema } from "./validations" @@ -17,11 +16,8 @@ import type { import { selectInformation, countInformation, - getInformationByPageCode, - insertInformation, + getInformationByPagePath, updateInformation, - deleteInformationById, - deleteInformationByIds, getInformationById, selectInformationLists, countInformationLists @@ -34,57 +30,65 @@ export async function getInformationLists(input: GetInformationSchema) { return unstable_cache( async () => { try { - const offset = (input.page - 1) * input.perPage + // 고급 검색 로직 + const { page, perPage, search, filters, joinOperator, pagePath, pageName, informationContent, isActive } = input - // 고급 필터링 - const advancedWhere = filterColumns({ - table: pageInformation, - filters: input.filters, - joinOperator: input.joinOperator, - }) + // 기본 검색 조건들 + const conditions = [] - // 전역 검색 - let globalWhere - if (input.search) { - const s = `%${input.search}%` - globalWhere = or( - ilike(pageInformation.pageCode, s), - ilike(pageInformation.pageName, s), - ilike(pageInformation.title, s), - ilike(pageInformation.description, s) - ) + // 검색어가 있으면 여러 필드에서 검색 + if (search && search.trim()) { + const searchConditions = [ + ilike(pageInformation.pagePath, `%${search}%`), + ilike(pageInformation.pageName, `%${search}%`), + ilike(pageInformation.informationContent, `%${search}%`) + ] + conditions.push(or(...searchConditions)) } - // 기본 필터들 - let basicWhere - const basicConditions = [] - - if (input.pageCode) { - basicConditions.push(ilike(pageInformation.pageCode, `%${input.pageCode}%`)) + // 개별 필드 조건들 + if (pagePath && pagePath.trim()) { + conditions.push(ilike(pageInformation.pagePath, `%${pagePath}%`)) } - - if (input.pageName) { - basicConditions.push(ilike(pageInformation.pageName, `%${input.pageName}%`)) + + if (pageName && pageName.trim()) { + conditions.push(ilike(pageInformation.pageName, `%${pageName}%`)) } - - if (input.title) { - basicConditions.push(ilike(pageInformation.title, `%${input.title}%`)) + + if (informationContent && informationContent.trim()) { + conditions.push(ilike(pageInformation.informationContent, `%${informationContent}%`)) } - - if (input.isActive !== undefined && input.isActive !== null) { - basicConditions.push(eq(pageInformation.isActive, input.isActive)) - } - - if (basicConditions.length > 0) { - basicWhere = and(...basicConditions) - } - - // 최종 where 조건 - const finalWhere = and( - advancedWhere, - globalWhere, - basicWhere - ) + + if (isActive !== null && isActive !== undefined) { + conditions.push(eq(pageInformation.isActive, isActive)) + } + + // 고급 필터 처리 + if (filters && filters.length > 0) { + const advancedConditions = filters.map(() => + filterColumns({ + table: pageInformation, + filters: filters, + joinOperator: joinOperator, + }) + ) + + if (advancedConditions.length > 0) { + if (joinOperator === "or") { + conditions.push(or(...advancedConditions)) + } else { + conditions.push(and(...advancedConditions)) + } + } + } + + // 전체 WHERE 조건 조합 + const finalWhere = conditions.length > 0 + ? (joinOperator === "or" ? or(...conditions) : and(...conditions)) + : undefined + + // 페이지네이션 + const offset = (page - 1) * perPage // 정렬 처리 const orderBy = input.sort.length > 0 @@ -93,12 +97,12 @@ export async function getInformationLists(input: GetInformationSchema) { return item.desc ? desc(pageInformation.createdAt) : asc(pageInformation.createdAt) } else if (item.id === "updatedAt") { return item.desc ? desc(pageInformation.updatedAt) : asc(pageInformation.updatedAt) - } else if (item.id === "pageCode") { - return item.desc ? desc(pageInformation.pageCode) : asc(pageInformation.pageCode) + } else if (item.id === "pagePath") { + return item.desc ? desc(pageInformation.pagePath) : asc(pageInformation.pagePath) } else if (item.id === "pageName") { return item.desc ? desc(pageInformation.pageName) : asc(pageInformation.pageName) - } else if (item.id === "title") { - return item.desc ? desc(pageInformation.title) : asc(pageInformation.title) + } else if (item.id === "informationContent") { + return item.desc ? desc(pageInformation.informationContent) : asc(pageInformation.informationContent) } else if (item.id === "isActive") { return item.desc ? desc(pageInformation.isActive) : asc(pageInformation.isActive) } else { @@ -129,7 +133,7 @@ export async function getInformationLists(input: GetInformationSchema) { return { data: [], pageCount: 0, total: 0 } } }, - [JSON.stringify(input)], // 캐싱 키 + [JSON.stringify(input)], { revalidate: 3600, tags: ["information-lists"], @@ -161,18 +165,18 @@ export async function getInformationList(input: Partial & } // 페이지별 인포메이션 조회 (일반 사용자용) -export async function getPageInformation(pageCode: string): Promise { +export async function getPageInformation(pagePath: string): Promise { try { - return await getInformationByPageCode(pageCode) + return await getInformationByPagePath(pagePath) } catch (error) { - console.error(`Failed to get information for page ${pageCode}:`, error) + console.error(`Failed to get information for page ${pagePath}:`, error) return null } } // 캐시된 페이지별 인포메이션 조회 export const getCachedPageInformation = unstable_cache( - async (pageCode: string) => getPageInformation(pageCode), + async (pagePath: string) => getPageInformation(pagePath), ["page-information"], { tags: ["page-information"], @@ -180,34 +184,20 @@ export const getCachedPageInformation = unstable_cache( } ) -// 인포메이션 생성 -export async function createInformation(input: CreateInformationSchema) { - try { - const result = await insertInformation(input) - - revalidateTag("page-information") - revalidateTag("information-lists") - revalidateTag("information-edit-permission") - - return { - success: true, - data: result, - message: "인포메이션이 성공적으로 생성되었습니다." - } - } catch (error) { - console.error("Failed to create information:", error) - return { - success: false, - message: getErrorMessage(error) - } - } -} - -// 인포메이션 수정 +// 인포메이션 수정 (내용과 첨부파일만) export async function updateInformationData(input: UpdateInformationSchema) { try { const { id, ...updateData } = input - const result = await updateInformation(id, updateData) + + // 수정 가능한 필드만 허용 + const allowedFields = { + informationContent: updateData.informationContent, + attachmentFilePath: updateData.attachmentFilePath, + attachmentFileName: updateData.attachmentFileName, + updatedAt: new Date() + } + + const result = await updateInformation(id, allowedFields) if (!result) { return { @@ -233,56 +223,6 @@ export async function updateInformationData(input: UpdateInformationSchema) { } } -// 인포메이션 삭제 -export async function deleteInformation(id: number) { - try { - const success = await deleteInformationById(id) - - if (!success) { - return { - success: false, - message: "인포메이션을 찾을 수 없거나 삭제에 실패했습니다." - } - } - - revalidateTag("page-information") - revalidateTag("information-lists") - - return { - success: true, - message: "인포메이션이 성공적으로 삭제되었습니다." - } - } catch (error) { - console.error("Failed to delete information:", error) - return { - success: false, - message: getErrorMessage(error) - } - } -} - -// 인포메이션 다중 삭제 -export async function deleteMultipleInformation(ids: number[]) { - try { - const deletedCount = await deleteInformationByIds(ids) - - revalidateTag("page-information") - revalidateTag("information-lists") - - return { - success: true, - deletedCount, - message: `${deletedCount}개의 인포메이션이 성공적으로 삭제되었습니다.` - } - } catch (error) { - console.error("Failed to delete multiple information:", error) - return { - success: false, - message: getErrorMessage(error) - } - } -} - // ID로 인포메이션 조회 export async function getInformationDetail(id: number): Promise { try { @@ -294,18 +234,18 @@ export async function getInformationDetail(id: number): Promise { +export async function checkInformationEditPermission(pagePath: string, userId: string): Promise { try { - // pageCode를 menuPath로 변환 (pageCode가 menuPath의 마지막 부분이라고 가정) - // 예: pageCode "vendor-list" -> menuPath "/evcp/vendor-list" 또는 "/partners/vendor-list" + // pagePath를 menuPath로 변환 (pagePath가 menuPath의 마지막 부분이라고 가정) + // 예: pagePath "vendor-list" -> menuPath "/evcp/vendor-list" 또는 "/partners/vendor-list" const menuPathQueries = [ - `/evcp/${pageCode}`, - `/partners/${pageCode}`, - `/${pageCode}`, // 루트 경로 - pageCode // 정확한 매칭 + `/evcp/${pagePath}`, + `/partners/${pagePath}`, + `/${pagePath}`, // 루트 경로 + pagePath // 정확한 매칭 ] - // menu_assignments에서 해당 pageCode와 매칭되는 메뉴 찾기 + // menu_assignments에서 해당 pagePath와 매칭되는 메뉴 찾기 const menuAssignment = await db .select() .from(menuAssignments) @@ -334,7 +274,7 @@ export async function checkInformationEditPermission(pageCode: string, userId: s // 캐시된 권한 확인 export const getCachedEditPermission = unstable_cache( - async (pageCode: string, userId: string) => checkInformationEditPermission(pageCode, userId), + async (pagePath: string, userId: string) => checkInformationEditPermission(pagePath, userId), ["information-edit-permission"], { tags: ["information-edit-permission"], diff --git a/lib/information/table/add-information-dialog.tsx b/lib/information/table/add-information-dialog.tsx deleted file mode 100644 index a879fbfe..00000000 --- a/lib/information/table/add-information-dialog.tsx +++ /dev/null @@ -1,329 +0,0 @@ -"use client" - -import * as React from "react" -import { zodResolver } from "@hookform/resolvers/zod" -import { useForm } from "react-hook-form" -import { toast } from "sonner" -import { Loader, Upload, X } from "lucide-react" -import { useRouter } from "next/navigation" - -import { Button } from "@/components/ui/button" -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form" -import { Input } from "@/components/ui/input" -import { Textarea } from "@/components/ui/textarea" -import { Switch } from "@/components/ui/switch" -import { createInformation } from "@/lib/information/service" -import { createInformationSchema, type CreateInformationSchema } from "@/lib/information/validations" - -interface AddInformationDialogProps { - open: boolean - onOpenChange: (open: boolean) => void -} - -export function AddInformationDialog({ - open, - onOpenChange, -}: AddInformationDialogProps) { - const router = useRouter() - const [isLoading, setIsLoading] = React.useState(false) - const [uploadedFile, setUploadedFile] = React.useState(null) - - const form = useForm({ - resolver: zodResolver(createInformationSchema), - defaultValues: { - pageCode: "", - pageName: "", - title: "", - description: "", - noticeTitle: "", - noticeContent: "", - attachmentFileName: "", - attachmentFilePath: "", - attachmentFileSize: "", - isActive: true, - }, - }) - - const handleFileSelect = (event: React.ChangeEvent) => { - const file = event.target.files?.[0] - if (file) { - setUploadedFile(file) - // 파일 크기를 MB 단위로 변환 - const sizeInMB = (file.size / (1024 * 1024)).toFixed(2) - form.setValue("attachmentFileName", file.name) - form.setValue("attachmentFileSize", `${sizeInMB} MB`) - } - } - - const removeFile = () => { - setUploadedFile(null) - form.setValue("attachmentFileName", "") - form.setValue("attachmentFilePath", "") - form.setValue("attachmentFileSize", "") - } - - const uploadFile = async (file: File): Promise => { - const formData = new FormData() - formData.append("file", file) - - const response = await fetch("/api/upload", { - method: "POST", - body: formData, - }) - - if (!response.ok) { - throw new Error("파일 업로드에 실패했습니다.") - } - - const result = await response.json() - return result.url - } - - const onSubmit = async (values: CreateInformationSchema) => { - setIsLoading(true) - try { - const finalValues = { ...values } - - // 파일이 있으면 업로드 - if (uploadedFile) { - const filePath = await uploadFile(uploadedFile) - finalValues.attachmentFilePath = filePath - } - - const result = await createInformation(finalValues) - - if (result.success) { - toast.success(result.message) - form.reset() - setUploadedFile(null) - onOpenChange(false) - router.refresh() - } else { - toast.error(result.message) - } - } catch (error) { - toast.error("인포메이션 생성에 실패했습니다.") - console.error(error) - } finally { - setIsLoading(false) - } - } - - // 다이얼로그가 닫힐 때 폼 초기화 - React.useEffect(() => { - if (!open) { - form.reset() - setUploadedFile(null) - } - }, [open, form]) - - return ( - - - - 인포메이션 추가 - - 새로운 페이지 인포메이션을 추가합니다. - - - -
- -
- ( - - 페이지 코드 - - - - - - )} - /> - - ( - - 페이지명 - - - - - - )} - /> -
- - ( - - 제목 - - - - - - )} - /> - - ( - - 설명 - -