editor 추가 및 권한관리 작업 완료 [컨텐츠 관리], [권한관리]
[컨텐츠 관리], [권한관리]
@c4884bf80208d032f1bdac5771aacdd2629c4a1f
--- package-lock.json
+++ package-lock.json
... | ... | @@ -9,7 +9,19 @@ |
| 9 | 9 |
"version": "0.0.0", |
| 10 | 10 |
"dependencies": {
|
| 11 | 11 |
"@tanstack/react-query": "^5.99.0", |
| 12 |
+ "@tiptap/extension-image": "^3.23.5", |
|
| 13 |
+ "@tiptap/extension-link": "^3.23.5", |
|
| 14 |
+ "@tiptap/extension-placeholder": "^3.23.5", |
|
| 15 |
+ "@tiptap/extension-table": "^3.23.5", |
|
| 16 |
+ "@tiptap/extension-table-cell": "^3.23.5", |
|
| 17 |
+ "@tiptap/extension-table-header": "^3.23.5", |
|
| 18 |
+ "@tiptap/extension-table-row": "^3.23.5", |
|
| 19 |
+ "@tiptap/extension-text-align": "^3.23.5", |
|
| 20 |
+ "@tiptap/extension-underline": "^3.23.5", |
|
| 21 |
+ "@tiptap/react": "^3.23.5", |
|
| 22 |
+ "@tiptap/starter-kit": "^3.23.5", |
|
| 12 | 23 |
"axios": "^1.16.0", |
| 24 |
+ "dompurify": "^3.4.5", |
|
| 13 | 25 |
"react": "^19.2.4", |
| 14 | 26 |
"react-dom": "^19.2.4", |
| 15 | 27 |
"react-router-dom": "^7.14.1", |
... | ... | @@ -461,6 +473,34 @@ |
| 461 | 473 |
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" |
| 462 | 474 |
} |
| 463 | 475 |
}, |
| 476 |
+ "node_modules/@floating-ui/core": {
|
|
| 477 |
+ "version": "1.7.5", |
|
| 478 |
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", |
|
| 479 |
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", |
|
| 480 |
+ "license": "MIT", |
|
| 481 |
+ "optional": true, |
|
| 482 |
+ "dependencies": {
|
|
| 483 |
+ "@floating-ui/utils": "^0.2.11" |
|
| 484 |
+ } |
|
| 485 |
+ }, |
|
| 486 |
+ "node_modules/@floating-ui/dom": {
|
|
| 487 |
+ "version": "1.7.6", |
|
| 488 |
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", |
|
| 489 |
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", |
|
| 490 |
+ "license": "MIT", |
|
| 491 |
+ "optional": true, |
|
| 492 |
+ "dependencies": {
|
|
| 493 |
+ "@floating-ui/core": "^1.7.5", |
|
| 494 |
+ "@floating-ui/utils": "^0.2.11" |
|
| 495 |
+ } |
|
| 496 |
+ }, |
|
| 497 |
+ "node_modules/@floating-ui/utils": {
|
|
| 498 |
+ "version": "0.2.11", |
|
| 499 |
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", |
|
| 500 |
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", |
|
| 501 |
+ "license": "MIT", |
|
| 502 |
+ "optional": true |
|
| 503 |
+ }, |
|
| 464 | 504 |
"node_modules/@humanfs/core": {
|
| 465 | 505 |
"version": "0.19.1", |
| 466 | 506 |
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", |
... | ... | @@ -900,6 +940,526 @@ |
| 900 | 940 |
"react": "^18 || ^19" |
| 901 | 941 |
} |
| 902 | 942 |
}, |
| 943 |
+ "node_modules/@tiptap/core": {
|
|
| 944 |
+ "version": "3.23.5", |
|
| 945 |
+ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.23.5.tgz", |
|
| 946 |
+ "integrity": "sha512-657Xqcgf1IYWLkAmRDJKNSGdoS1AHJEgK6zHWHFJERQGIqHnwC7Fz7nvWs/NQhQVBkclQd0ERRdTCZ3XwRc1+g==", |
|
| 947 |
+ "license": "MIT", |
|
| 948 |
+ "funding": {
|
|
| 949 |
+ "type": "github", |
|
| 950 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 951 |
+ }, |
|
| 952 |
+ "peerDependencies": {
|
|
| 953 |
+ "@tiptap/pm": "3.23.5" |
|
| 954 |
+ } |
|
| 955 |
+ }, |
|
| 956 |
+ "node_modules/@tiptap/extension-blockquote": {
|
|
| 957 |
+ "version": "3.23.5", |
|
| 958 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.23.5.tgz", |
|
| 959 |
+ "integrity": "sha512-PBQRoGfSWfIY7HmGbS5PTHEBQl5nKbild5J5phPLFF+O3aOBQ0d49AC9cxbaou/6FRCtq6g4Uqse9rRTKJRM0w==", |
|
| 960 |
+ "license": "MIT", |
|
| 961 |
+ "funding": {
|
|
| 962 |
+ "type": "github", |
|
| 963 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 964 |
+ }, |
|
| 965 |
+ "peerDependencies": {
|
|
| 966 |
+ "@tiptap/core": "3.23.5" |
|
| 967 |
+ } |
|
| 968 |
+ }, |
|
| 969 |
+ "node_modules/@tiptap/extension-bold": {
|
|
| 970 |
+ "version": "3.23.5", |
|
| 971 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.23.5.tgz", |
|
| 972 |
+ "integrity": "sha512-DZsDCCf53fA9HmsFzfUHl5jLOwDYf+XzfP+QJjJ4cK23SsxDirameTjgnwi4l1EgEPLWunMZQjU+wHmh7vvX6Q==", |
|
| 973 |
+ "license": "MIT", |
|
| 974 |
+ "funding": {
|
|
| 975 |
+ "type": "github", |
|
| 976 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 977 |
+ }, |
|
| 978 |
+ "peerDependencies": {
|
|
| 979 |
+ "@tiptap/core": "3.23.5" |
|
| 980 |
+ } |
|
| 981 |
+ }, |
|
| 982 |
+ "node_modules/@tiptap/extension-bubble-menu": {
|
|
| 983 |
+ "version": "3.23.5", |
|
| 984 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.23.5.tgz", |
|
| 985 |
+ "integrity": "sha512-otcGwyVO6OfxdDPnbooZxYGrb+6q5WYmS+g2V+XGGNRn5oJgyY5pW0dqELIUJ66dosIIXXPyw2XqBDpMMY2kyQ==", |
|
| 986 |
+ "license": "MIT", |
|
| 987 |
+ "optional": true, |
|
| 988 |
+ "dependencies": {
|
|
| 989 |
+ "@floating-ui/dom": "^1.0.0" |
|
| 990 |
+ }, |
|
| 991 |
+ "funding": {
|
|
| 992 |
+ "type": "github", |
|
| 993 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 994 |
+ }, |
|
| 995 |
+ "peerDependencies": {
|
|
| 996 |
+ "@tiptap/core": "3.23.5", |
|
| 997 |
+ "@tiptap/pm": "3.23.5" |
|
| 998 |
+ } |
|
| 999 |
+ }, |
|
| 1000 |
+ "node_modules/@tiptap/extension-bullet-list": {
|
|
| 1001 |
+ "version": "3.23.5", |
|
| 1002 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.23.5.tgz", |
|
| 1003 |
+ "integrity": "sha512-o0bzZbFvOPhPX6+RAhIFPKMIN3jIenY6Ib3FJ6ZqxTdVcjuV2mIXUmJU0uV2BwKtz73GmKSRKRKia6KJ0ml8qA==", |
|
| 1004 |
+ "license": "MIT", |
|
| 1005 |
+ "funding": {
|
|
| 1006 |
+ "type": "github", |
|
| 1007 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1008 |
+ }, |
|
| 1009 |
+ "peerDependencies": {
|
|
| 1010 |
+ "@tiptap/extension-list": "3.23.5" |
|
| 1011 |
+ } |
|
| 1012 |
+ }, |
|
| 1013 |
+ "node_modules/@tiptap/extension-code": {
|
|
| 1014 |
+ "version": "3.23.5", |
|
| 1015 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.23.5.tgz", |
|
| 1016 |
+ "integrity": "sha512-NOJUD2Z0hrtBWnovXiiH1XtOjEQePOfIG3bNJgXSs1bWxPVhqp6KjVd8mUJNra974hxbml3tC97sL9QqjpAWFg==", |
|
| 1017 |
+ "license": "MIT", |
|
| 1018 |
+ "funding": {
|
|
| 1019 |
+ "type": "github", |
|
| 1020 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1021 |
+ }, |
|
| 1022 |
+ "peerDependencies": {
|
|
| 1023 |
+ "@tiptap/core": "3.23.5" |
|
| 1024 |
+ } |
|
| 1025 |
+ }, |
|
| 1026 |
+ "node_modules/@tiptap/extension-code-block": {
|
|
| 1027 |
+ "version": "3.23.5", |
|
| 1028 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.23.5.tgz", |
|
| 1029 |
+ "integrity": "sha512-P2XH8WPM4UahavcWoQgAwNAKQCbF/JWi6ZqgsQmVBfAqQ3mf8gMxB7HnciMq1DlyI9EfjXoJH11yUqldF/6AaQ==", |
|
| 1030 |
+ "license": "MIT", |
|
| 1031 |
+ "funding": {
|
|
| 1032 |
+ "type": "github", |
|
| 1033 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1034 |
+ }, |
|
| 1035 |
+ "peerDependencies": {
|
|
| 1036 |
+ "@tiptap/core": "3.23.5", |
|
| 1037 |
+ "@tiptap/pm": "3.23.5" |
|
| 1038 |
+ } |
|
| 1039 |
+ }, |
|
| 1040 |
+ "node_modules/@tiptap/extension-document": {
|
|
| 1041 |
+ "version": "3.23.5", |
|
| 1042 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.23.5.tgz", |
|
| 1043 |
+ "integrity": "sha512-Y7uPjEM1xIK4Spcdk/kp/vZ/Az3cEaglTCk6uHrWvNFVglEoGehNb6IQbQFZW0wjE19YoMIiLBLtG6V9dqrpBw==", |
|
| 1044 |
+ "license": "MIT", |
|
| 1045 |
+ "funding": {
|
|
| 1046 |
+ "type": "github", |
|
| 1047 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1048 |
+ }, |
|
| 1049 |
+ "peerDependencies": {
|
|
| 1050 |
+ "@tiptap/core": "3.23.5" |
|
| 1051 |
+ } |
|
| 1052 |
+ }, |
|
| 1053 |
+ "node_modules/@tiptap/extension-dropcursor": {
|
|
| 1054 |
+ "version": "3.23.5", |
|
| 1055 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.23.5.tgz", |
|
| 1056 |
+ "integrity": "sha512-l72R798Q69D6f89Vp9xreoRnPcpK0LHPKLZIc6pvqBC2iOjx5wLKtW0uP1uqVWdQtvF5AUYBRNIGAQ5Gel9XEg==", |
|
| 1057 |
+ "license": "MIT", |
|
| 1058 |
+ "funding": {
|
|
| 1059 |
+ "type": "github", |
|
| 1060 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1061 |
+ }, |
|
| 1062 |
+ "peerDependencies": {
|
|
| 1063 |
+ "@tiptap/extensions": "3.23.5" |
|
| 1064 |
+ } |
|
| 1065 |
+ }, |
|
| 1066 |
+ "node_modules/@tiptap/extension-floating-menu": {
|
|
| 1067 |
+ "version": "3.23.5", |
|
| 1068 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.23.5.tgz", |
|
| 1069 |
+ "integrity": "sha512-kP0bZKH/lxNogfvoIy/YJZ5gkty0OwqFVtQUwoc85vXYUfvy5Jh1VdO053tCE1iDzmvOITUpcb+MdWryP8dBxA==", |
|
| 1070 |
+ "license": "MIT", |
|
| 1071 |
+ "optional": true, |
|
| 1072 |
+ "funding": {
|
|
| 1073 |
+ "type": "github", |
|
| 1074 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1075 |
+ }, |
|
| 1076 |
+ "peerDependencies": {
|
|
| 1077 |
+ "@floating-ui/dom": "^1.0.0", |
|
| 1078 |
+ "@tiptap/core": "3.23.5", |
|
| 1079 |
+ "@tiptap/pm": "3.23.5" |
|
| 1080 |
+ } |
|
| 1081 |
+ }, |
|
| 1082 |
+ "node_modules/@tiptap/extension-gapcursor": {
|
|
| 1083 |
+ "version": "3.23.5", |
|
| 1084 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.23.5.tgz", |
|
| 1085 |
+ "integrity": "sha512-x9XlYG26TowX0Ly1w0ZV2D8qliyQy9fTmMY4suI6B/6o6m/sXHGTAJMmJqwP66sZKF6cMLU3HECumhtyQxPT2g==", |
|
| 1086 |
+ "license": "MIT", |
|
| 1087 |
+ "funding": {
|
|
| 1088 |
+ "type": "github", |
|
| 1089 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1090 |
+ }, |
|
| 1091 |
+ "peerDependencies": {
|
|
| 1092 |
+ "@tiptap/extensions": "3.23.5" |
|
| 1093 |
+ } |
|
| 1094 |
+ }, |
|
| 1095 |
+ "node_modules/@tiptap/extension-hard-break": {
|
|
| 1096 |
+ "version": "3.23.5", |
|
| 1097 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.23.5.tgz", |
|
| 1098 |
+ "integrity": "sha512-j/BDBMOA1mA+RhCx622SRPBhpp2XWNFYz9asbg8T3yk8v9WI3Vjo6IDlfTp6fwsR2LGE7Pek3R0xDAjW6yVG3g==", |
|
| 1099 |
+ "license": "MIT", |
|
| 1100 |
+ "funding": {
|
|
| 1101 |
+ "type": "github", |
|
| 1102 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1103 |
+ }, |
|
| 1104 |
+ "peerDependencies": {
|
|
| 1105 |
+ "@tiptap/core": "3.23.5" |
|
| 1106 |
+ } |
|
| 1107 |
+ }, |
|
| 1108 |
+ "node_modules/@tiptap/extension-heading": {
|
|
| 1109 |
+ "version": "3.23.5", |
|
| 1110 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.23.5.tgz", |
|
| 1111 |
+ "integrity": "sha512-tFI+iYk34geacVOGqYgyoC8siQjdGn605XaYSZcGRFF8NY+HrGlLkQi2QRRCeLaUhxoctONmWc8USn3H5U7wLQ==", |
|
| 1112 |
+ "license": "MIT", |
|
| 1113 |
+ "funding": {
|
|
| 1114 |
+ "type": "github", |
|
| 1115 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1116 |
+ }, |
|
| 1117 |
+ "peerDependencies": {
|
|
| 1118 |
+ "@tiptap/core": "3.23.5" |
|
| 1119 |
+ } |
|
| 1120 |
+ }, |
|
| 1121 |
+ "node_modules/@tiptap/extension-horizontal-rule": {
|
|
| 1122 |
+ "version": "3.23.5", |
|
| 1123 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.23.5.tgz", |
|
| 1124 |
+ "integrity": "sha512-9XkRYc4XE0stERZB3y8bsJd32Jw9UZfMwZXo1GLNYRHFr7dmhSGUj0IzgofqOVmLDcOMW6XcCk54TBYw6BCrWA==", |
|
| 1125 |
+ "license": "MIT", |
|
| 1126 |
+ "funding": {
|
|
| 1127 |
+ "type": "github", |
|
| 1128 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1129 |
+ }, |
|
| 1130 |
+ "peerDependencies": {
|
|
| 1131 |
+ "@tiptap/core": "3.23.5", |
|
| 1132 |
+ "@tiptap/pm": "3.23.5" |
|
| 1133 |
+ } |
|
| 1134 |
+ }, |
|
| 1135 |
+ "node_modules/@tiptap/extension-image": {
|
|
| 1136 |
+ "version": "3.23.5", |
|
| 1137 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.23.5.tgz", |
|
| 1138 |
+ "integrity": "sha512-v6u9zbJSKLjml6DDn1/1WOOIzVxz3K5Idl1EgUl+IpJH7kR1HLRJ3TaSgF7z2RRQmqyHlmtdCzdaKoe0jCIyqQ==", |
|
| 1139 |
+ "license": "MIT", |
|
| 1140 |
+ "funding": {
|
|
| 1141 |
+ "type": "github", |
|
| 1142 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1143 |
+ }, |
|
| 1144 |
+ "peerDependencies": {
|
|
| 1145 |
+ "@tiptap/core": "3.23.5" |
|
| 1146 |
+ } |
|
| 1147 |
+ }, |
|
| 1148 |
+ "node_modules/@tiptap/extension-italic": {
|
|
| 1149 |
+ "version": "3.23.5", |
|
| 1150 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.23.5.tgz", |
|
| 1151 |
+ "integrity": "sha512-XjRSPr6j4mz+8O5j5KNfxVb+1fGNt0wr+js6MLxxGdU7M+PoDPdVY6fARbmBazv4ERlZ5PNS9m35Vo5xDjDfrg==", |
|
| 1152 |
+ "license": "MIT", |
|
| 1153 |
+ "funding": {
|
|
| 1154 |
+ "type": "github", |
|
| 1155 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1156 |
+ }, |
|
| 1157 |
+ "peerDependencies": {
|
|
| 1158 |
+ "@tiptap/core": "3.23.5" |
|
| 1159 |
+ } |
|
| 1160 |
+ }, |
|
| 1161 |
+ "node_modules/@tiptap/extension-link": {
|
|
| 1162 |
+ "version": "3.23.5", |
|
| 1163 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.23.5.tgz", |
|
| 1164 |
+ "integrity": "sha512-FEI58NAPnauBbs4nw1dkgRyEhcWnure0vIlStfQoQGXxj3xSRvxKH2lOkz54fGzuzRJAoudyLU65HW6D7kc+8Q==", |
|
| 1165 |
+ "license": "MIT", |
|
| 1166 |
+ "dependencies": {
|
|
| 1167 |
+ "linkifyjs": "^4.3.3" |
|
| 1168 |
+ }, |
|
| 1169 |
+ "funding": {
|
|
| 1170 |
+ "type": "github", |
|
| 1171 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1172 |
+ }, |
|
| 1173 |
+ "peerDependencies": {
|
|
| 1174 |
+ "@tiptap/core": "3.23.5", |
|
| 1175 |
+ "@tiptap/pm": "3.23.5" |
|
| 1176 |
+ } |
|
| 1177 |
+ }, |
|
| 1178 |
+ "node_modules/@tiptap/extension-list": {
|
|
| 1179 |
+ "version": "3.23.5", |
|
| 1180 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.23.5.tgz", |
|
| 1181 |
+ "integrity": "sha512-nzZXpVwnyKwTj4TVyPyu1bCUFjJCsaXnhAthmvJDnX3RBtemNG9Ka07xGR2NIspzumSbQSMFtDxjmxv3W5dEtg==", |
|
| 1182 |
+ "license": "MIT", |
|
| 1183 |
+ "funding": {
|
|
| 1184 |
+ "type": "github", |
|
| 1185 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1186 |
+ }, |
|
| 1187 |
+ "peerDependencies": {
|
|
| 1188 |
+ "@tiptap/core": "3.23.5", |
|
| 1189 |
+ "@tiptap/pm": "3.23.5" |
|
| 1190 |
+ } |
|
| 1191 |
+ }, |
|
| 1192 |
+ "node_modules/@tiptap/extension-list-item": {
|
|
| 1193 |
+ "version": "3.23.5", |
|
| 1194 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.23.5.tgz", |
|
| 1195 |
+ "integrity": "sha512-l7Hb4rfNIkO6JrNJYkdXap6QYXCz4XeeFmI1bfQgEiwPGs+RAn/+0cOdg7q+6MmtZFac5uSXV0PftPk6A0GsEA==", |
|
| 1196 |
+ "license": "MIT", |
|
| 1197 |
+ "funding": {
|
|
| 1198 |
+ "type": "github", |
|
| 1199 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1200 |
+ }, |
|
| 1201 |
+ "peerDependencies": {
|
|
| 1202 |
+ "@tiptap/extension-list": "3.23.5" |
|
| 1203 |
+ } |
|
| 1204 |
+ }, |
|
| 1205 |
+ "node_modules/@tiptap/extension-list-keymap": {
|
|
| 1206 |
+ "version": "3.23.5", |
|
| 1207 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.23.5.tgz", |
|
| 1208 |
+ "integrity": "sha512-Hz8jRA51VSiHezEkwqwaMYbTEYcR/5Aq3UgCgDlNPlE6k1OZrvRtV/4s3AOO0RRgzyVLKv7yv7KuOJN/OLGErw==", |
|
| 1209 |
+ "license": "MIT", |
|
| 1210 |
+ "funding": {
|
|
| 1211 |
+ "type": "github", |
|
| 1212 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1213 |
+ }, |
|
| 1214 |
+ "peerDependencies": {
|
|
| 1215 |
+ "@tiptap/extension-list": "3.23.5" |
|
| 1216 |
+ } |
|
| 1217 |
+ }, |
|
| 1218 |
+ "node_modules/@tiptap/extension-ordered-list": {
|
|
| 1219 |
+ "version": "3.23.5", |
|
| 1220 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.23.5.tgz", |
|
| 1221 |
+ "integrity": "sha512-qQeU71ij0cAAD9bbGqot5T5bpR3dysgQ+W67quRs6VDyusU89EYaJHKn/qWU6a1XOEQ4sL+5GNw52FYQVHUxbA==", |
|
| 1222 |
+ "license": "MIT", |
|
| 1223 |
+ "funding": {
|
|
| 1224 |
+ "type": "github", |
|
| 1225 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1226 |
+ }, |
|
| 1227 |
+ "peerDependencies": {
|
|
| 1228 |
+ "@tiptap/extension-list": "3.23.5" |
|
| 1229 |
+ } |
|
| 1230 |
+ }, |
|
| 1231 |
+ "node_modules/@tiptap/extension-paragraph": {
|
|
| 1232 |
+ "version": "3.23.5", |
|
| 1233 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.23.5.tgz", |
|
| 1234 |
+ "integrity": "sha512-LtgMcR1rvWnZDtphFJ/LBltlC0+6HGA07k7vhy+U7P/zIg/V3Fb4RD6YDuAo0cPfBsLm8p1WYJV92WpAsGgtlg==", |
|
| 1235 |
+ "license": "MIT", |
|
| 1236 |
+ "funding": {
|
|
| 1237 |
+ "type": "github", |
|
| 1238 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1239 |
+ }, |
|
| 1240 |
+ "peerDependencies": {
|
|
| 1241 |
+ "@tiptap/core": "3.23.5" |
|
| 1242 |
+ } |
|
| 1243 |
+ }, |
|
| 1244 |
+ "node_modules/@tiptap/extension-placeholder": {
|
|
| 1245 |
+ "version": "3.23.5", |
|
| 1246 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.23.5.tgz", |
|
| 1247 |
+ "integrity": "sha512-B2snUujc6fb/16p8jSQCN4+mto7RlHKLm8quBTUWXksY8D82u/cxjUdmRQ7ueq7vsbRsA+WoJTrKEjJ8RQOpjw==", |
|
| 1248 |
+ "license": "MIT", |
|
| 1249 |
+ "funding": {
|
|
| 1250 |
+ "type": "github", |
|
| 1251 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1252 |
+ }, |
|
| 1253 |
+ "peerDependencies": {
|
|
| 1254 |
+ "@tiptap/extensions": "3.23.5" |
|
| 1255 |
+ } |
|
| 1256 |
+ }, |
|
| 1257 |
+ "node_modules/@tiptap/extension-strike": {
|
|
| 1258 |
+ "version": "3.23.5", |
|
| 1259 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.23.5.tgz", |
|
| 1260 |
+ "integrity": "sha512-PMB9lpQGOJGuRTIS9rBw8UZtHQwmsiJbWKjcBr5z20MluaJQ3ZCHFhDYG6ncIDRz+0ny4ZvoJ7cKGpI+NTvXMA==", |
|
| 1261 |
+ "license": "MIT", |
|
| 1262 |
+ "funding": {
|
|
| 1263 |
+ "type": "github", |
|
| 1264 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1265 |
+ }, |
|
| 1266 |
+ "peerDependencies": {
|
|
| 1267 |
+ "@tiptap/core": "3.23.5" |
|
| 1268 |
+ } |
|
| 1269 |
+ }, |
|
| 1270 |
+ "node_modules/@tiptap/extension-table": {
|
|
| 1271 |
+ "version": "3.23.5", |
|
| 1272 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.23.5.tgz", |
|
| 1273 |
+ "integrity": "sha512-3uTaC+LsilQHaMGTW6vK4fXHsTYL/TPGM0mxoBz8UvMl+G/uzL149RcMC0d0qKvYPxInFQ2rFzxPTpnY3Rg3UA==", |
|
| 1274 |
+ "license": "MIT", |
|
| 1275 |
+ "funding": {
|
|
| 1276 |
+ "type": "github", |
|
| 1277 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1278 |
+ }, |
|
| 1279 |
+ "peerDependencies": {
|
|
| 1280 |
+ "@tiptap/core": "3.23.5", |
|
| 1281 |
+ "@tiptap/pm": "3.23.5" |
|
| 1282 |
+ } |
|
| 1283 |
+ }, |
|
| 1284 |
+ "node_modules/@tiptap/extension-table-cell": {
|
|
| 1285 |
+ "version": "3.23.5", |
|
| 1286 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-3.23.5.tgz", |
|
| 1287 |
+ "integrity": "sha512-P8agH3q1EDEGTcs9+BXqIv/2weUkJfy63SIQetU28egtNRJt1L+rhnrkwzd95f01TNN/TRSyW01OAZ3tj8tHKA==", |
|
| 1288 |
+ "license": "MIT", |
|
| 1289 |
+ "funding": {
|
|
| 1290 |
+ "type": "github", |
|
| 1291 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1292 |
+ }, |
|
| 1293 |
+ "peerDependencies": {
|
|
| 1294 |
+ "@tiptap/extension-table": "3.23.5" |
|
| 1295 |
+ } |
|
| 1296 |
+ }, |
|
| 1297 |
+ "node_modules/@tiptap/extension-table-header": {
|
|
| 1298 |
+ "version": "3.23.5", |
|
| 1299 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-3.23.5.tgz", |
|
| 1300 |
+ "integrity": "sha512-CeEiMwEg4L38e8c4qJovasKaaSkcAUrMNNfvF/qE8WogSYfke2/OAy4cZ47bhzd5oauSUjiIUnmsY11Q0jdX/Q==", |
|
| 1301 |
+ "license": "MIT", |
|
| 1302 |
+ "funding": {
|
|
| 1303 |
+ "type": "github", |
|
| 1304 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1305 |
+ }, |
|
| 1306 |
+ "peerDependencies": {
|
|
| 1307 |
+ "@tiptap/extension-table": "3.23.5" |
|
| 1308 |
+ } |
|
| 1309 |
+ }, |
|
| 1310 |
+ "node_modules/@tiptap/extension-table-row": {
|
|
| 1311 |
+ "version": "3.23.5", |
|
| 1312 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-3.23.5.tgz", |
|
| 1313 |
+ "integrity": "sha512-tbpl+kSHeuPIqwgPw3SuQXS/rzdzRYigxXY1fCeWea20wONMQhLHyaL710vFcugIW6d9aSgmvdiwcGS/MneL0A==", |
|
| 1314 |
+ "license": "MIT", |
|
| 1315 |
+ "funding": {
|
|
| 1316 |
+ "type": "github", |
|
| 1317 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1318 |
+ }, |
|
| 1319 |
+ "peerDependencies": {
|
|
| 1320 |
+ "@tiptap/extension-table": "3.23.5" |
|
| 1321 |
+ } |
|
| 1322 |
+ }, |
|
| 1323 |
+ "node_modules/@tiptap/extension-text": {
|
|
| 1324 |
+ "version": "3.23.5", |
|
| 1325 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.23.5.tgz", |
|
| 1326 |
+ "integrity": "sha512-GLa+AaA2NC5XYRZad/Qq/oH5Pa95s+uA17J7+RCkF8j1RNREUBkYQ5CD5MT8kT+D3DHgU8MRyYdTd28I46HBDQ==", |
|
| 1327 |
+ "license": "MIT", |
|
| 1328 |
+ "funding": {
|
|
| 1329 |
+ "type": "github", |
|
| 1330 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1331 |
+ }, |
|
| 1332 |
+ "peerDependencies": {
|
|
| 1333 |
+ "@tiptap/core": "3.23.5" |
|
| 1334 |
+ } |
|
| 1335 |
+ }, |
|
| 1336 |
+ "node_modules/@tiptap/extension-text-align": {
|
|
| 1337 |
+ "version": "3.23.5", |
|
| 1338 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.23.5.tgz", |
|
| 1339 |
+ "integrity": "sha512-eOeXrbpPWc6gfXli2aXYg9t61HhkvEkdxQgpEpZPFhrT4pPQcIqTlihswByC+cPb8B5ynrc/iamiY9cRSU1qvw==", |
|
| 1340 |
+ "license": "MIT", |
|
| 1341 |
+ "funding": {
|
|
| 1342 |
+ "type": "github", |
|
| 1343 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1344 |
+ }, |
|
| 1345 |
+ "peerDependencies": {
|
|
| 1346 |
+ "@tiptap/core": "3.23.5" |
|
| 1347 |
+ } |
|
| 1348 |
+ }, |
|
| 1349 |
+ "node_modules/@tiptap/extension-underline": {
|
|
| 1350 |
+ "version": "3.23.5", |
|
| 1351 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.23.5.tgz", |
|
| 1352 |
+ "integrity": "sha512-fyxthzE6CNCi9a9OVAwXs1sSyJ7jlrzT3aP2KhYLQCsJABHaPJgJA7k52/CRuKqCW3WbxU1ULH9LGuGtBbhEyw==", |
|
| 1353 |
+ "license": "MIT", |
|
| 1354 |
+ "funding": {
|
|
| 1355 |
+ "type": "github", |
|
| 1356 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1357 |
+ }, |
|
| 1358 |
+ "peerDependencies": {
|
|
| 1359 |
+ "@tiptap/core": "3.23.5" |
|
| 1360 |
+ } |
|
| 1361 |
+ }, |
|
| 1362 |
+ "node_modules/@tiptap/extensions": {
|
|
| 1363 |
+ "version": "3.23.5", |
|
| 1364 |
+ "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.23.5.tgz", |
|
| 1365 |
+ "integrity": "sha512-ROcdNPV+buzldEFKvD3o29P7H7zpAf2lnLfndO2LHSToWyHw4hlzVPCeAU8uAvhl/jyfeUoFLrBwxphMX/KG6A==", |
|
| 1366 |
+ "license": "MIT", |
|
| 1367 |
+ "funding": {
|
|
| 1368 |
+ "type": "github", |
|
| 1369 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1370 |
+ }, |
|
| 1371 |
+ "peerDependencies": {
|
|
| 1372 |
+ "@tiptap/core": "3.23.5", |
|
| 1373 |
+ "@tiptap/pm": "3.23.5" |
|
| 1374 |
+ } |
|
| 1375 |
+ }, |
|
| 1376 |
+ "node_modules/@tiptap/pm": {
|
|
| 1377 |
+ "version": "3.23.5", |
|
| 1378 |
+ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.23.5.tgz", |
|
| 1379 |
+ "integrity": "sha512-9tgLdpTvNN0/fLP4RcNzbyQ0qjg9J2ahaFbQzgV5uvd+QMy8Xkg2IqKKnOoJJUAV3FDjGq3Yx0WrV2BGro9pfw==", |
|
| 1380 |
+ "license": "MIT", |
|
| 1381 |
+ "dependencies": {
|
|
| 1382 |
+ "prosemirror-changeset": "^2.3.0", |
|
| 1383 |
+ "prosemirror-commands": "^1.6.2", |
|
| 1384 |
+ "prosemirror-dropcursor": "^1.8.1", |
|
| 1385 |
+ "prosemirror-gapcursor": "^1.3.2", |
|
| 1386 |
+ "prosemirror-history": "^1.4.1", |
|
| 1387 |
+ "prosemirror-keymap": "^1.2.2", |
|
| 1388 |
+ "prosemirror-model": "^1.24.1", |
|
| 1389 |
+ "prosemirror-schema-list": "^1.5.0", |
|
| 1390 |
+ "prosemirror-state": "^1.4.3", |
|
| 1391 |
+ "prosemirror-tables": "^1.6.4", |
|
| 1392 |
+ "prosemirror-transform": "^1.10.2", |
|
| 1393 |
+ "prosemirror-view": "^1.38.1" |
|
| 1394 |
+ }, |
|
| 1395 |
+ "funding": {
|
|
| 1396 |
+ "type": "github", |
|
| 1397 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1398 |
+ } |
|
| 1399 |
+ }, |
|
| 1400 |
+ "node_modules/@tiptap/react": {
|
|
| 1401 |
+ "version": "3.23.5", |
|
| 1402 |
+ "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.23.5.tgz", |
|
| 1403 |
+ "integrity": "sha512-aEdKfJxoa6tCEV4FrnBqMQoUPwGcTWLaDzmP4fL1gR7E40rYDTiYNKoF1Ob+UimUpguAP6Emv1WlJa5oyI8FSw==", |
|
| 1404 |
+ "license": "MIT", |
|
| 1405 |
+ "dependencies": {
|
|
| 1406 |
+ "@types/use-sync-external-store": "^0.0.6", |
|
| 1407 |
+ "fast-equals": "^5.3.3", |
|
| 1408 |
+ "use-sync-external-store": "^1.4.0" |
|
| 1409 |
+ }, |
|
| 1410 |
+ "funding": {
|
|
| 1411 |
+ "type": "github", |
|
| 1412 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1413 |
+ }, |
|
| 1414 |
+ "optionalDependencies": {
|
|
| 1415 |
+ "@tiptap/extension-bubble-menu": "^3.23.5", |
|
| 1416 |
+ "@tiptap/extension-floating-menu": "^3.23.5" |
|
| 1417 |
+ }, |
|
| 1418 |
+ "peerDependencies": {
|
|
| 1419 |
+ "@tiptap/core": "3.23.5", |
|
| 1420 |
+ "@tiptap/pm": "3.23.5", |
|
| 1421 |
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", |
|
| 1422 |
+ "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", |
|
| 1423 |
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0", |
|
| 1424 |
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" |
|
| 1425 |
+ } |
|
| 1426 |
+ }, |
|
| 1427 |
+ "node_modules/@tiptap/starter-kit": {
|
|
| 1428 |
+ "version": "3.23.5", |
|
| 1429 |
+ "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.23.5.tgz", |
|
| 1430 |
+ "integrity": "sha512-ac0edQ1a1nYkNAzOgdqIBKGdrOlNQpPP9wGAG3Q9EgTq4+C4/EftJZZJmUn3KzaSOUv4cLEDo0z0jurJvZPkaw==", |
|
| 1431 |
+ "license": "MIT", |
|
| 1432 |
+ "dependencies": {
|
|
| 1433 |
+ "@tiptap/core": "^3.23.5", |
|
| 1434 |
+ "@tiptap/extension-blockquote": "^3.23.5", |
|
| 1435 |
+ "@tiptap/extension-bold": "^3.23.5", |
|
| 1436 |
+ "@tiptap/extension-bullet-list": "^3.23.5", |
|
| 1437 |
+ "@tiptap/extension-code": "^3.23.5", |
|
| 1438 |
+ "@tiptap/extension-code-block": "^3.23.5", |
|
| 1439 |
+ "@tiptap/extension-document": "^3.23.5", |
|
| 1440 |
+ "@tiptap/extension-dropcursor": "^3.23.5", |
|
| 1441 |
+ "@tiptap/extension-gapcursor": "^3.23.5", |
|
| 1442 |
+ "@tiptap/extension-hard-break": "^3.23.5", |
|
| 1443 |
+ "@tiptap/extension-heading": "^3.23.5", |
|
| 1444 |
+ "@tiptap/extension-horizontal-rule": "^3.23.5", |
|
| 1445 |
+ "@tiptap/extension-italic": "^3.23.5", |
|
| 1446 |
+ "@tiptap/extension-link": "^3.23.5", |
|
| 1447 |
+ "@tiptap/extension-list": "^3.23.5", |
|
| 1448 |
+ "@tiptap/extension-list-item": "^3.23.5", |
|
| 1449 |
+ "@tiptap/extension-list-keymap": "^3.23.5", |
|
| 1450 |
+ "@tiptap/extension-ordered-list": "^3.23.5", |
|
| 1451 |
+ "@tiptap/extension-paragraph": "^3.23.5", |
|
| 1452 |
+ "@tiptap/extension-strike": "^3.23.5", |
|
| 1453 |
+ "@tiptap/extension-text": "^3.23.5", |
|
| 1454 |
+ "@tiptap/extension-underline": "^3.23.5", |
|
| 1455 |
+ "@tiptap/extensions": "^3.23.5", |
|
| 1456 |
+ "@tiptap/pm": "^3.23.5" |
|
| 1457 |
+ }, |
|
| 1458 |
+ "funding": {
|
|
| 1459 |
+ "type": "github", |
|
| 1460 |
+ "url": "https://github.com/sponsors/ueberdosis" |
|
| 1461 |
+ } |
|
| 1462 |
+ }, |
|
| 903 | 1463 |
"node_modules/@tybys/wasm-util": {
|
| 904 | 1464 |
"version": "0.10.1", |
| 905 | 1465 |
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", |
... | ... | @@ -939,7 +1499,6 @@ |
| 939 | 1499 |
"version": "19.2.14", |
| 940 | 1500 |
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", |
| 941 | 1501 |
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", |
| 942 |
- "dev": true, |
|
| 943 | 1502 |
"license": "MIT", |
| 944 | 1503 |
"dependencies": {
|
| 945 | 1504 |
"csstype": "^3.2.2" |
... | ... | @@ -949,11 +1508,23 @@ |
| 949 | 1508 |
"version": "19.2.3", |
| 950 | 1509 |
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", |
| 951 | 1510 |
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", |
| 952 |
- "dev": true, |
|
| 953 | 1511 |
"license": "MIT", |
| 954 | 1512 |
"peerDependencies": {
|
| 955 | 1513 |
"@types/react": "^19.2.0" |
| 956 | 1514 |
} |
| 1515 |
+ }, |
|
| 1516 |
+ "node_modules/@types/trusted-types": {
|
|
| 1517 |
+ "version": "2.0.7", |
|
| 1518 |
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", |
|
| 1519 |
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", |
|
| 1520 |
+ "license": "MIT", |
|
| 1521 |
+ "optional": true |
|
| 1522 |
+ }, |
|
| 1523 |
+ "node_modules/@types/use-sync-external-store": {
|
|
| 1524 |
+ "version": "0.0.6", |
|
| 1525 |
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", |
|
| 1526 |
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", |
|
| 1527 |
+ "license": "MIT" |
|
| 957 | 1528 |
}, |
| 958 | 1529 |
"node_modules/@typescript-eslint/eslint-plugin": {
|
| 959 | 1530 |
"version": "8.58.2", |
... | ... | @@ -1569,7 +2140,6 @@ |
| 1569 | 2140 |
"version": "3.2.3", |
| 1570 | 2141 |
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", |
| 1571 | 2142 |
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", |
| 1572 |
- "dev": true, |
|
| 1573 | 2143 |
"license": "MIT" |
| 1574 | 2144 |
}, |
| 1575 | 2145 |
"node_modules/debug": {
|
... | ... | @@ -1614,6 +2184,15 @@ |
| 1614 | 2184 |
"license": "Apache-2.0", |
| 1615 | 2185 |
"engines": {
|
| 1616 | 2186 |
"node": ">=8" |
| 2187 |
+ } |
|
| 2188 |
+ }, |
|
| 2189 |
+ "node_modules/dompurify": {
|
|
| 2190 |
+ "version": "3.4.5", |
|
| 2191 |
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.5.tgz", |
|
| 2192 |
+ "integrity": "sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==", |
|
| 2193 |
+ "license": "(MPL-2.0 OR Apache-2.0)", |
|
| 2194 |
+ "optionalDependencies": {
|
|
| 2195 |
+ "@types/trusted-types": "^2.0.7" |
|
| 1617 | 2196 |
} |
| 1618 | 2197 |
}, |
| 1619 | 2198 |
"node_modules/dunder-proto": {
|
... | ... | @@ -1895,6 +2474,15 @@ |
| 1895 | 2474 |
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", |
| 1896 | 2475 |
"dev": true, |
| 1897 | 2476 |
"license": "MIT" |
| 2477 |
+ }, |
|
| 2478 |
+ "node_modules/fast-equals": {
|
|
| 2479 |
+ "version": "5.4.0", |
|
| 2480 |
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", |
|
| 2481 |
+ "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", |
|
| 2482 |
+ "license": "MIT", |
|
| 2483 |
+ "engines": {
|
|
| 2484 |
+ "node": ">=6.0.0" |
|
| 2485 |
+ } |
|
| 1898 | 2486 |
}, |
| 1899 | 2487 |
"node_modules/fast-json-stable-stringify": {
|
| 1900 | 2488 |
"version": "2.1.0", |
... | ... | @@ -2621,6 +3209,12 @@ |
| 2621 | 3209 |
"url": "https://opencollective.com/parcel" |
| 2622 | 3210 |
} |
| 2623 | 3211 |
}, |
| 3212 |
+ "node_modules/linkifyjs": {
|
|
| 3213 |
+ "version": "4.3.3", |
|
| 3214 |
+ "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz", |
|
| 3215 |
+ "integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==", |
|
| 3216 |
+ "license": "MIT" |
|
| 3217 |
+ }, |
|
| 2624 | 3218 |
"node_modules/locate-path": {
|
| 2625 | 3219 |
"version": "6.0.0", |
| 2626 | 3220 |
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", |
... | ... | @@ -2755,6 +3349,12 @@ |
| 2755 | 3349 |
"node": ">= 0.8.0" |
| 2756 | 3350 |
} |
| 2757 | 3351 |
}, |
| 3352 |
+ "node_modules/orderedmap": {
|
|
| 3353 |
+ "version": "2.1.1", |
|
| 3354 |
+ "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", |
|
| 3355 |
+ "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", |
|
| 3356 |
+ "license": "MIT" |
|
| 3357 |
+ }, |
|
| 2758 | 3358 |
"node_modules/p-limit": {
|
| 2759 | 3359 |
"version": "3.1.0", |
| 2760 | 3360 |
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", |
... | ... | @@ -2877,6 +3477,135 @@ |
| 2877 | 3477 |
"license": "MIT", |
| 2878 | 3478 |
"engines": {
|
| 2879 | 3479 |
"node": ">= 0.8.0" |
| 3480 |
+ } |
|
| 3481 |
+ }, |
|
| 3482 |
+ "node_modules/prosemirror-changeset": {
|
|
| 3483 |
+ "version": "2.4.1", |
|
| 3484 |
+ "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz", |
|
| 3485 |
+ "integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==", |
|
| 3486 |
+ "license": "MIT", |
|
| 3487 |
+ "dependencies": {
|
|
| 3488 |
+ "prosemirror-transform": "^1.0.0" |
|
| 3489 |
+ } |
|
| 3490 |
+ }, |
|
| 3491 |
+ "node_modules/prosemirror-commands": {
|
|
| 3492 |
+ "version": "1.7.1", |
|
| 3493 |
+ "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", |
|
| 3494 |
+ "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", |
|
| 3495 |
+ "license": "MIT", |
|
| 3496 |
+ "dependencies": {
|
|
| 3497 |
+ "prosemirror-model": "^1.0.0", |
|
| 3498 |
+ "prosemirror-state": "^1.0.0", |
|
| 3499 |
+ "prosemirror-transform": "^1.10.2" |
|
| 3500 |
+ } |
|
| 3501 |
+ }, |
|
| 3502 |
+ "node_modules/prosemirror-dropcursor": {
|
|
| 3503 |
+ "version": "1.8.2", |
|
| 3504 |
+ "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", |
|
| 3505 |
+ "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", |
|
| 3506 |
+ "license": "MIT", |
|
| 3507 |
+ "dependencies": {
|
|
| 3508 |
+ "prosemirror-state": "^1.0.0", |
|
| 3509 |
+ "prosemirror-transform": "^1.1.0", |
|
| 3510 |
+ "prosemirror-view": "^1.1.0" |
|
| 3511 |
+ } |
|
| 3512 |
+ }, |
|
| 3513 |
+ "node_modules/prosemirror-gapcursor": {
|
|
| 3514 |
+ "version": "1.4.1", |
|
| 3515 |
+ "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", |
|
| 3516 |
+ "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", |
|
| 3517 |
+ "license": "MIT", |
|
| 3518 |
+ "dependencies": {
|
|
| 3519 |
+ "prosemirror-keymap": "^1.0.0", |
|
| 3520 |
+ "prosemirror-model": "^1.0.0", |
|
| 3521 |
+ "prosemirror-state": "^1.0.0", |
|
| 3522 |
+ "prosemirror-view": "^1.0.0" |
|
| 3523 |
+ } |
|
| 3524 |
+ }, |
|
| 3525 |
+ "node_modules/prosemirror-history": {
|
|
| 3526 |
+ "version": "1.5.0", |
|
| 3527 |
+ "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", |
|
| 3528 |
+ "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", |
|
| 3529 |
+ "license": "MIT", |
|
| 3530 |
+ "dependencies": {
|
|
| 3531 |
+ "prosemirror-state": "^1.2.2", |
|
| 3532 |
+ "prosemirror-transform": "^1.0.0", |
|
| 3533 |
+ "prosemirror-view": "^1.31.0", |
|
| 3534 |
+ "rope-sequence": "^1.3.0" |
|
| 3535 |
+ } |
|
| 3536 |
+ }, |
|
| 3537 |
+ "node_modules/prosemirror-keymap": {
|
|
| 3538 |
+ "version": "1.2.3", |
|
| 3539 |
+ "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", |
|
| 3540 |
+ "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", |
|
| 3541 |
+ "license": "MIT", |
|
| 3542 |
+ "dependencies": {
|
|
| 3543 |
+ "prosemirror-state": "^1.0.0", |
|
| 3544 |
+ "w3c-keyname": "^2.2.0" |
|
| 3545 |
+ } |
|
| 3546 |
+ }, |
|
| 3547 |
+ "node_modules/prosemirror-model": {
|
|
| 3548 |
+ "version": "1.25.7", |
|
| 3549 |
+ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.7.tgz", |
|
| 3550 |
+ "integrity": "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug==", |
|
| 3551 |
+ "license": "MIT", |
|
| 3552 |
+ "dependencies": {
|
|
| 3553 |
+ "orderedmap": "^2.0.0" |
|
| 3554 |
+ } |
|
| 3555 |
+ }, |
|
| 3556 |
+ "node_modules/prosemirror-schema-list": {
|
|
| 3557 |
+ "version": "1.5.1", |
|
| 3558 |
+ "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", |
|
| 3559 |
+ "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", |
|
| 3560 |
+ "license": "MIT", |
|
| 3561 |
+ "dependencies": {
|
|
| 3562 |
+ "prosemirror-model": "^1.0.0", |
|
| 3563 |
+ "prosemirror-state": "^1.0.0", |
|
| 3564 |
+ "prosemirror-transform": "^1.7.3" |
|
| 3565 |
+ } |
|
| 3566 |
+ }, |
|
| 3567 |
+ "node_modules/prosemirror-state": {
|
|
| 3568 |
+ "version": "1.4.4", |
|
| 3569 |
+ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", |
|
| 3570 |
+ "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", |
|
| 3571 |
+ "license": "MIT", |
|
| 3572 |
+ "dependencies": {
|
|
| 3573 |
+ "prosemirror-model": "^1.0.0", |
|
| 3574 |
+ "prosemirror-transform": "^1.0.0", |
|
| 3575 |
+ "prosemirror-view": "^1.27.0" |
|
| 3576 |
+ } |
|
| 3577 |
+ }, |
|
| 3578 |
+ "node_modules/prosemirror-tables": {
|
|
| 3579 |
+ "version": "1.8.5", |
|
| 3580 |
+ "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", |
|
| 3581 |
+ "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", |
|
| 3582 |
+ "license": "MIT", |
|
| 3583 |
+ "dependencies": {
|
|
| 3584 |
+ "prosemirror-keymap": "^1.2.3", |
|
| 3585 |
+ "prosemirror-model": "^1.25.4", |
|
| 3586 |
+ "prosemirror-state": "^1.4.4", |
|
| 3587 |
+ "prosemirror-transform": "^1.10.5", |
|
| 3588 |
+ "prosemirror-view": "^1.41.4" |
|
| 3589 |
+ } |
|
| 3590 |
+ }, |
|
| 3591 |
+ "node_modules/prosemirror-transform": {
|
|
| 3592 |
+ "version": "1.12.0", |
|
| 3593 |
+ "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz", |
|
| 3594 |
+ "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==", |
|
| 3595 |
+ "license": "MIT", |
|
| 3596 |
+ "dependencies": {
|
|
| 3597 |
+ "prosemirror-model": "^1.21.0" |
|
| 3598 |
+ } |
|
| 3599 |
+ }, |
|
| 3600 |
+ "node_modules/prosemirror-view": {
|
|
| 3601 |
+ "version": "1.41.8", |
|
| 3602 |
+ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.8.tgz", |
|
| 3603 |
+ "integrity": "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==", |
|
| 3604 |
+ "license": "MIT", |
|
| 3605 |
+ "dependencies": {
|
|
| 3606 |
+ "prosemirror-model": "^1.20.0", |
|
| 3607 |
+ "prosemirror-state": "^1.0.0", |
|
| 3608 |
+ "prosemirror-transform": "^1.1.0" |
|
| 2880 | 3609 |
} |
| 2881 | 3610 |
}, |
| 2882 | 3611 |
"node_modules/proxy-from-env": {
|
... | ... | @@ -3019,6 +3748,12 @@ |
| 3019 | 3748 |
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", |
| 3020 | 3749 |
"integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", |
| 3021 | 3750 |
"dev": true, |
| 3751 |
+ "license": "MIT" |
|
| 3752 |
+ }, |
|
| 3753 |
+ "node_modules/rope-sequence": {
|
|
| 3754 |
+ "version": "1.3.4", |
|
| 3755 |
+ "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", |
|
| 3756 |
+ "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", |
|
| 3022 | 3757 |
"license": "MIT" |
| 3023 | 3758 |
}, |
| 3024 | 3759 |
"node_modules/scheduler": {
|
... | ... | @@ -3239,6 +3974,15 @@ |
| 3239 | 3974 |
"punycode": "^2.1.0" |
| 3240 | 3975 |
} |
| 3241 | 3976 |
}, |
| 3977 |
+ "node_modules/use-sync-external-store": {
|
|
| 3978 |
+ "version": "1.6.0", |
|
| 3979 |
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", |
|
| 3980 |
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", |
|
| 3981 |
+ "license": "MIT", |
|
| 3982 |
+ "peerDependencies": {
|
|
| 3983 |
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" |
|
| 3984 |
+ } |
|
| 3985 |
+ }, |
|
| 3242 | 3986 |
"node_modules/vite": {
|
| 3243 | 3987 |
"version": "8.0.8", |
| 3244 | 3988 |
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", |
... | ... | @@ -3317,6 +4061,12 @@ |
| 3317 | 4061 |
} |
| 3318 | 4062 |
} |
| 3319 | 4063 |
}, |
| 4064 |
+ "node_modules/w3c-keyname": {
|
|
| 4065 |
+ "version": "2.2.8", |
|
| 4066 |
+ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", |
|
| 4067 |
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", |
|
| 4068 |
+ "license": "MIT" |
|
| 4069 |
+ }, |
|
| 3320 | 4070 |
"node_modules/which": {
|
| 3321 | 4071 |
"version": "2.0.2", |
| 3322 | 4072 |
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", |
--- package.json
+++ package.json
... | ... | @@ -12,7 +12,19 @@ |
| 12 | 12 |
}, |
| 13 | 13 |
"dependencies": {
|
| 14 | 14 |
"@tanstack/react-query": "^5.99.0", |
| 15 |
+ "@tiptap/extension-image": "^3.23.5", |
|
| 16 |
+ "@tiptap/extension-link": "^3.23.5", |
|
| 17 |
+ "@tiptap/extension-placeholder": "^3.23.5", |
|
| 18 |
+ "@tiptap/extension-table": "^3.23.5", |
|
| 19 |
+ "@tiptap/extension-table-cell": "^3.23.5", |
|
| 20 |
+ "@tiptap/extension-table-header": "^3.23.5", |
|
| 21 |
+ "@tiptap/extension-table-row": "^3.23.5", |
|
| 22 |
+ "@tiptap/extension-text-align": "^3.23.5", |
|
| 23 |
+ "@tiptap/extension-underline": "^3.23.5", |
|
| 24 |
+ "@tiptap/react": "^3.23.5", |
|
| 25 |
+ "@tiptap/starter-kit": "^3.23.5", |
|
| 15 | 26 |
"axios": "^1.16.0", |
| 27 |
+ "dompurify": "^3.4.5", |
|
| 16 | 28 |
"react": "^19.2.4", |
| 17 | 29 |
"react-dom": "^19.2.4", |
| 18 | 30 |
"react-router-dom": "^7.14.1", |
--- src/App.tsx
+++ src/App.tsx
... | ... | @@ -1,8 +1,10 @@ |
| 1 | 1 |
import { useEffect, useState } from 'react';
|
| 2 |
+import {Navigate, Route, Routes} from 'react-router-dom';
|
|
| 2 | 3 |
import { UserLayout } from './user/UserLayout';
|
| 3 | 4 |
import { UserListPage } from './user/UserListPage';
|
| 4 | 5 |
import {AdminRoute} from "./admin/route/AdminRoute.tsx";
|
| 5 | 6 |
import {ToastContainer} from "react-toastify";
|
| 7 |
+import {UserContentPreviewPage} from "./user/UserContentPreviewPage.tsx";
|
|
| 6 | 8 |
|
| 7 | 9 |
type Skin = 'admin' | 'user'; |
| 8 | 10 |
|
... | ... | @@ -56,9 +58,25 @@ |
| 56 | 58 |
{skin === 'admin' ? (
|
| 57 | 59 |
<AdminRoute /> |
| 58 | 60 |
) : ( |
| 59 |
- <UserLayout> |
|
| 60 |
- <UserListPage /> |
|
| 61 |
- </UserLayout> |
|
| 61 |
+ <Routes> |
|
| 62 |
+ <Route |
|
| 63 |
+ path="/preview/content" |
|
| 64 |
+ element={(
|
|
| 65 |
+ <UserLayout title="콘텐츠 미리보기"> |
|
| 66 |
+ <UserContentPreviewPage /> |
|
| 67 |
+ </UserLayout> |
|
| 68 |
+ )} |
|
| 69 |
+ /> |
|
| 70 |
+ <Route |
|
| 71 |
+ path="/" |
|
| 72 |
+ element={(
|
|
| 73 |
+ <UserLayout> |
|
| 74 |
+ <UserListPage /> |
|
| 75 |
+ </UserLayout> |
|
| 76 |
+ )} |
|
| 77 |
+ /> |
|
| 78 |
+ <Route path="*" element={<Navigate to="/?skin=user" replace />} />
|
|
| 79 |
+ </Routes> |
|
| 62 | 80 |
)} |
| 63 | 81 |
<ToastContainer position="bottom-right" autoClose={3000} />
|
| 64 | 82 |
</> |
+++ src/admin/component/editor/RichTextEditor.tsx
... | ... | @@ -0,0 +1,239 @@ |
| 1 | +import {EditorContent, useEditor} from '@tiptap/react'; | |
| 2 | +import StarterKit from '@tiptap/starter-kit'; | |
| 3 | +import Link from '@tiptap/extension-link'; | |
| 4 | +import Underline from '@tiptap/extension-underline'; | |
| 5 | +import TextAlign from '@tiptap/extension-text-align'; | |
| 6 | +import Placeholder from '@tiptap/extension-placeholder'; | |
| 7 | +import Image from '@tiptap/extension-image'; | |
| 8 | +import {Table} from '@tiptap/extension-table'; | |
| 9 | +import {TableCell} from '@tiptap/extension-table-cell'; | |
| 10 | +import {TableHeader} from '@tiptap/extension-table-header'; | |
| 11 | +import {TableRow} from '@tiptap/extension-table-row'; | |
| 12 | +import {useEffect} from 'react'; | |
| 13 | + | |
| 14 | +type RichTextEditorProps = { | |
| 15 | + value: string; | |
| 16 | + onChange: (value: string) => void; | |
| 17 | + placeholder?: string; | |
| 18 | + disabled?: boolean; | |
| 19 | +}; | |
| 20 | + | |
| 21 | +type ToolbarButtonProps = { | |
| 22 | + label: string; | |
| 23 | + title: string; | |
| 24 | + active?: boolean; | |
| 25 | + disabled?: boolean; | |
| 26 | + onClick: () => void; | |
| 27 | +}; | |
| 28 | + | |
| 29 | +const ToolbarButton = ({label, title, active = false, disabled = false, onClick}: ToolbarButtonProps) => ( | |
| 30 | + <button | |
| 31 | + type="button" | |
| 32 | + className={active ? 'active' : ''} | |
| 33 | + title={title} | |
| 34 | + aria-label={title} | |
| 35 | + disabled={disabled} | |
| 36 | + onClick={onClick} | |
| 37 | + > | |
| 38 | + {label} | |
| 39 | + </button> | |
| 40 | +); | |
| 41 | + | |
| 42 | +export const RichTextEditor = ({ | |
| 43 | + value, | |
| 44 | + onChange, | |
| 45 | + placeholder = '내용을 입력하세요.', | |
| 46 | + disabled = false, | |
| 47 | + }: RichTextEditorProps) => { | |
| 48 | + const editor = useEditor({ | |
| 49 | + editable: !disabled, | |
| 50 | + extensions: [ | |
| 51 | + StarterKit, | |
| 52 | + Underline, | |
| 53 | + Link.configure({ | |
| 54 | + openOnClick: false, | |
| 55 | + autolink: true, | |
| 56 | + defaultProtocol: 'https', | |
| 57 | + }), | |
| 58 | + TextAlign.configure({ | |
| 59 | + types: ['heading', 'paragraph'], | |
| 60 | + }), | |
| 61 | + Placeholder.configure({ | |
| 62 | + placeholder, | |
| 63 | + }), | |
| 64 | + Image.configure({ | |
| 65 | + allowBase64: true, | |
| 66 | + }), | |
| 67 | + Table.configure({ | |
| 68 | + resizable: true, | |
| 69 | + }), | |
| 70 | + TableRow, | |
| 71 | + TableHeader, | |
| 72 | + TableCell, | |
| 73 | + ], | |
| 74 | + content: value || '', | |
| 75 | + editorProps: { | |
| 76 | + attributes: { | |
| 77 | + class: 'rich_text_editor_body', | |
| 78 | + }, | |
| 79 | + }, | |
| 80 | + onUpdate: ({editor}) => { | |
| 81 | + onChange(editor.getHTML()); | |
| 82 | + }, | |
| 83 | + }); | |
| 84 | + | |
| 85 | + useEffect(() => { | |
| 86 | + if (!editor) { | |
| 87 | + return; | |
| 88 | + } | |
| 89 | + | |
| 90 | + editor.setEditable(!disabled); | |
| 91 | + }, [disabled, editor]); | |
| 92 | + | |
| 93 | + useEffect(() => { | |
| 94 | + if (!editor || value === editor.getHTML()) { | |
| 95 | + return; | |
| 96 | + } | |
| 97 | + | |
| 98 | + editor.commands.setContent(value || '', {emitUpdate: false}); | |
| 99 | + }, [editor, value]); | |
| 100 | + | |
| 101 | + if (!editor) { | |
| 102 | + return null; | |
| 103 | + } | |
| 104 | + | |
| 105 | + const setLink = () => { | |
| 106 | + const previousUrl = editor.getAttributes('link').href as string | undefined; | |
| 107 | + const url = window.prompt('링크 URL을 입력하세요.', previousUrl ?? ''); | |
| 108 | + | |
| 109 | + if (url === null) { | |
| 110 | + return; | |
| 111 | + } | |
| 112 | + | |
| 113 | + if (!url.trim()) { | |
| 114 | + editor.chain().focus().extendMarkRange('link').unsetLink().run(); | |
| 115 | + return; | |
| 116 | + } | |
| 117 | + | |
| 118 | + editor.chain().focus().extendMarkRange('link').setLink({href: url.trim()}).run(); | |
| 119 | + }; | |
| 120 | + | |
| 121 | + const addImage = () => { | |
| 122 | + const url = window.prompt('이미지 URL을 입력하세요.'); | |
| 123 | + | |
| 124 | + if (!url?.trim()) { | |
| 125 | + return; | |
| 126 | + } | |
| 127 | + | |
| 128 | + editor.chain().focus().setImage({src: url.trim()}).run(); | |
| 129 | + }; | |
| 130 | + | |
| 131 | + return ( | |
| 132 | + <div className="rich_text_editor"> | |
| 133 | + <div className="rich_text_editor_toolbar"> | |
| 134 | + <ToolbarButton | |
| 135 | + label="H2" | |
| 136 | + title="제목" | |
| 137 | + active={editor.isActive('heading', {level: 2})} | |
| 138 | + disabled={disabled} | |
| 139 | + onClick={() => editor.chain().focus().toggleHeading({level: 2}).run()} | |
| 140 | + /> | |
| 141 | + <ToolbarButton | |
| 142 | + label="B" | |
| 143 | + title="굵게" | |
| 144 | + active={editor.isActive('bold')} | |
| 145 | + disabled={disabled} | |
| 146 | + onClick={() => editor.chain().focus().toggleBold().run()} | |
| 147 | + /> | |
| 148 | + <ToolbarButton | |
| 149 | + label="I" | |
| 150 | + title="기울임" | |
| 151 | + active={editor.isActive('italic')} | |
| 152 | + disabled={disabled} | |
| 153 | + onClick={() => editor.chain().focus().toggleItalic().run()} | |
| 154 | + /> | |
| 155 | + <ToolbarButton | |
| 156 | + label="U" | |
| 157 | + title="밑줄" | |
| 158 | + active={editor.isActive('underline')} | |
| 159 | + disabled={disabled} | |
| 160 | + onClick={() => editor.chain().focus().toggleUnderline().run()} | |
| 161 | + /> | |
| 162 | + <ToolbarButton | |
| 163 | + label="S" | |
| 164 | + title="취소선" | |
| 165 | + active={editor.isActive('strike')} | |
| 166 | + disabled={disabled} | |
| 167 | + onClick={() => editor.chain().focus().toggleStrike().run()} | |
| 168 | + /> | |
| 169 | + <ToolbarButton | |
| 170 | + label="•" | |
| 171 | + title="글머리 기호" | |
| 172 | + active={editor.isActive('bulletList')} | |
| 173 | + disabled={disabled} | |
| 174 | + onClick={() => editor.chain().focus().toggleBulletList().run()} | |
| 175 | + /> | |
| 176 | + <ToolbarButton | |
| 177 | + label="1." | |
| 178 | + title="번호 목록" | |
| 179 | + active={editor.isActive('orderedList')} | |
| 180 | + disabled={disabled} | |
| 181 | + onClick={() => editor.chain().focus().toggleOrderedList().run()} | |
| 182 | + /> | |
| 183 | + <ToolbarButton | |
| 184 | + label="L" | |
| 185 | + title="왼쪽 정렬" | |
| 186 | + active={editor.isActive({textAlign: 'left'})} | |
| 187 | + disabled={disabled} | |
| 188 | + onClick={() => editor.chain().focus().setTextAlign('left').run()} | |
| 189 | + /> | |
| 190 | + <ToolbarButton | |
| 191 | + label="C" | |
| 192 | + title="가운데 정렬" | |
| 193 | + active={editor.isActive({textAlign: 'center'})} | |
| 194 | + disabled={disabled} | |
| 195 | + onClick={() => editor.chain().focus().setTextAlign('center').run()} | |
| 196 | + /> | |
| 197 | + <ToolbarButton | |
| 198 | + label="R" | |
| 199 | + title="오른쪽 정렬" | |
| 200 | + active={editor.isActive({textAlign: 'right'})} | |
| 201 | + disabled={disabled} | |
| 202 | + onClick={() => editor.chain().focus().setTextAlign('right').run()} | |
| 203 | + /> | |
| 204 | + <ToolbarButton | |
| 205 | + label="Link" | |
| 206 | + title="링크" | |
| 207 | + active={editor.isActive('link')} | |
| 208 | + disabled={disabled} | |
| 209 | + onClick={setLink} | |
| 210 | + /> | |
| 211 | + <ToolbarButton | |
| 212 | + label="Img" | |
| 213 | + title="이미지" | |
| 214 | + disabled={disabled} | |
| 215 | + onClick={addImage} | |
| 216 | + /> | |
| 217 | + <ToolbarButton | |
| 218 | + label="Tbl" | |
| 219 | + title="표 삽입" | |
| 220 | + disabled={disabled} | |
| 221 | + onClick={() => editor.chain().focus().insertTable({rows: 3, cols: 3, withHeaderRow: true}).run()} | |
| 222 | + /> | |
| 223 | + <ToolbarButton | |
| 224 | + label="Undo" | |
| 225 | + title="실행 취소" | |
| 226 | + disabled={disabled || !editor.can().undo()} | |
| 227 | + onClick={() => editor.chain().focus().undo().run()} | |
| 228 | + /> | |
| 229 | + <ToolbarButton | |
| 230 | + label="Redo" | |
| 231 | + title="다시 실행" | |
| 232 | + disabled={disabled || !editor.can().redo()} | |
| 233 | + onClick={() => editor.chain().focus().redo().run()} | |
| 234 | + /> | |
| 235 | + </div> | |
| 236 | + <EditorContent editor={editor}/> | |
| 237 | + </div> | |
| 238 | + ); | |
| 239 | +}; |
+++ src/admin/feature/content/api/contentApi.ts
... | ... | @@ -0,0 +1,34 @@ |
| 1 | +import type { | |
| 2 | + ContentDetailParams, | |
| 3 | + ContentFormItem, | |
| 4 | + ContentListItem, | |
| 5 | + ContentSearchParams, | |
| 6 | + DeleteBatchContentRequest | |
| 7 | +} from "../type/content.types.ts"; | |
| 8 | +import {apiClient} from "../../../../api/apiClient.ts"; | |
| 9 | +import type {PageResponse} from "../../../../type/pageResponse.ts"; | |
| 10 | + | |
| 11 | +export async function fetchContentList(params: ContentSearchParams) { | |
| 12 | + return apiClient.get<PageResponse<ContentListItem>>('/uss/ion/cnt/list.do', params); | |
| 13 | +} | |
| 14 | + | |
| 15 | +export async function fetchContentDetail(params: ContentDetailParams) { | |
| 16 | + return apiClient.get<ContentFormItem>('/uss/ion/cnt/detail.do', params); | |
| 17 | +} | |
| 18 | + | |
| 19 | +export async function fetchContentCreate(params: ContentFormItem) { | |
| 20 | + return apiClient.post('/uss/ion/cnt/egovCntManageInsert.do', params); | |
| 21 | +} | |
| 22 | + | |
| 23 | +export async function fetchContentUpdate(params: ContentFormItem) { | |
| 24 | + return apiClient.post('/uss/ion/cnt/egovCntManageUpdate.do', params); | |
| 25 | +} | |
| 26 | + | |
| 27 | +export async function fetchContentDelete(cntId: string) { | |
| 28 | + return apiClient.post(`/uss/ion/cnt/egovCntManageDelete.do?cntId=${cntId}`); | |
| 29 | +} | |
| 30 | + | |
| 31 | +export async function fetchContentDeleteBatch(params: DeleteBatchContentRequest[]) { | |
| 32 | + return apiClient.post('/uss/ion/cnt/egovCntManageDeleteBatch.do', params); | |
| 33 | +} | |
| 34 | + |
+++ src/admin/feature/content/component/ContentFormTable.tsx
... | ... | @@ -0,0 +1,91 @@ |
| 1 | +import type {ChangeEvent} from 'react'; | |
| 2 | +import {RichTextEditor} from '../../../component/editor/RichTextEditor.tsx'; | |
| 3 | +import type {ContentFormItem} from '../type/content.types.ts'; | |
| 4 | + | |
| 5 | +type ContentFormTableProps = { | |
| 6 | + form: ContentFormItem; | |
| 7 | + onChange: (event: ChangeEvent<HTMLInputElement>) => void; | |
| 8 | + onContentChange: (value: string) => void; | |
| 9 | + disabled?: boolean; | |
| 10 | +}; | |
| 11 | + | |
| 12 | +export const ContentFormTable = ({ | |
| 13 | + form, | |
| 14 | + onChange, | |
| 15 | + onContentChange, | |
| 16 | + disabled = false, | |
| 17 | + }: ContentFormTableProps) => { | |
| 18 | + return ( | |
| 19 | + <div className="table table_type_rows"> | |
| 20 | + <table> | |
| 21 | + <colgroup> | |
| 22 | + <col style={{width: '200px'}}/> | |
| 23 | + <col style={{width: 'auto'}}/> | |
| 24 | + </colgroup> | |
| 25 | + | |
| 26 | + <tbody> | |
| 27 | + <tr> | |
| 28 | + <th> | |
| 29 | + <span className="required">*</span> | |
| 30 | + 콘텐츠 이름 | |
| 31 | + </th> | |
| 32 | + <td> | |
| 33 | + <input | |
| 34 | + type="text" | |
| 35 | + className="input" | |
| 36 | + id="cntName" | |
| 37 | + name="cntName" | |
| 38 | + value={form.cntName} | |
| 39 | + onChange={onChange} | |
| 40 | + disabled={disabled} | |
| 41 | + /> | |
| 42 | + </td> | |
| 43 | + </tr> | |
| 44 | + | |
| 45 | + <tr> | |
| 46 | + <th> | |
| 47 | + <span className="required">*</span> | |
| 48 | + 내용 | |
| 49 | + </th> | |
| 50 | + <td> | |
| 51 | + <RichTextEditor | |
| 52 | + value={form.cntCn} | |
| 53 | + onChange={onContentChange} | |
| 54 | + disabled={disabled} | |
| 55 | + /> | |
| 56 | + </td> | |
| 57 | + </tr> | |
| 58 | + | |
| 59 | + {form.registPnttm ? ( | |
| 60 | + <tr> | |
| 61 | + <th>최종수정일</th> | |
| 62 | + <td> | |
| 63 | + <input | |
| 64 | + type="text" | |
| 65 | + className="input" | |
| 66 | + value={form.registPnttm} | |
| 67 | + readOnly | |
| 68 | + /> | |
| 69 | + </td> | |
| 70 | + </tr> | |
| 71 | + ) : null} | |
| 72 | + | |
| 73 | + {form.registerId ? ( | |
| 74 | + <tr> | |
| 75 | + <th>작성자</th> | |
| 76 | + <td> | |
| 77 | + <input | |
| 78 | + type="text" | |
| 79 | + className="input" | |
| 80 | + value={form.registerId} | |
| 81 | + readOnly | |
| 82 | + /> | |
| 83 | + </td> | |
| 84 | + </tr> | |
| 85 | + ) : null} | |
| 86 | + | |
| 87 | + </tbody> | |
| 88 | + </table> | |
| 89 | + </div> | |
| 90 | + ); | |
| 91 | +}; |
+++ src/admin/feature/content/component/ContentListTable.tsx
... | ... | @@ -0,0 +1,55 @@ |
| 1 | +import type {CheckableTableModel, RowActionsModel} from "../../../../type/viewModel.ts"; | |
| 2 | +import type {ContentListItem} from "../type/content.types.ts"; | |
| 3 | +import type {SearchParams} from "../../../../type/searchParams.ts"; | |
| 4 | +import {ContentListTableHeader} from "./ContentListTableHeader.tsx"; | |
| 5 | +import {EmptyRow} from "../../../component/EmptyRow.tsx"; | |
| 6 | +import {ContentListTableRow} from "./ContentListTableRow.tsx"; | |
| 7 | + | |
| 8 | +type ContentListTableProps = CheckableTableModel<ContentListItem, SearchParams> & RowActionsModel<{ | |
| 9 | + onDetail: (cntId: string, cntDtId: string) => void | |
| 10 | + onPreview: (cntId: string, cntDtId: string) => void | |
| 11 | +}>; | |
| 12 | + | |
| 13 | + | |
| 14 | +export const ContentListTable = ({ | |
| 15 | + items, | |
| 16 | + params, | |
| 17 | + onChange, | |
| 18 | + pagination, | |
| 19 | + check, | |
| 20 | + rowActions | |
| 21 | + }: ContentListTableProps) => { | |
| 22 | + return ( | |
| 23 | + <div className={"table table_type_cols"}> | |
| 24 | + <table> | |
| 25 | + <ContentListTableHeader | |
| 26 | + params={params} | |
| 27 | + onChange={onChange} | |
| 28 | + checked={check.isAllChecked} | |
| 29 | + indeterminate={check.isPartiallyChecked} | |
| 30 | + onCheckAll={check.onCheckAll} | |
| 31 | + /> | |
| 32 | + | |
| 33 | + <tbody> | |
| 34 | + {items.length > 0 ? | |
| 35 | + items.map((item, index) => ( | |
| 36 | + <ContentListTableRow | |
| 37 | + key={index} | |
| 38 | + item={item} | |
| 39 | + index={index} | |
| 40 | + searchParams={params} | |
| 41 | + {...pagination} | |
| 42 | + checked={check.isChecked(item.cntId)} | |
| 43 | + onCheck={check.onCheck} | |
| 44 | + {...rowActions} | |
| 45 | + /> | |
| 46 | + ) | |
| 47 | + ) : ( | |
| 48 | + <EmptyRow colSpan={7}/> | |
| 49 | + ) | |
| 50 | + } | |
| 51 | + </tbody> | |
| 52 | + </table> | |
| 53 | + </div> | |
| 54 | + ) | |
| 55 | +} |
+++ src/admin/feature/content/component/ContentListTableHeader.tsx
... | ... | @@ -0,0 +1,53 @@ |
| 1 | +import {useTableSort} from "../../../hook/useTableSort.ts"; | |
| 2 | +import type {SearchParams} from "../../../../type/searchParams.ts"; | |
| 3 | +import {CheckBox} from "../../../component/checkbox/CheckBox.tsx"; | |
| 4 | +import {SortableHeaderCell} from "../../../component/table/SortableHeaderCell.tsx"; | |
| 5 | + | |
| 6 | +interface ContentListTableHeaderProps { | |
| 7 | + params: SearchParams; | |
| 8 | + onChange: (params: SearchParams) => void | |
| 9 | + checked: boolean | |
| 10 | + indeterminate: boolean | |
| 11 | + onCheckAll: (checked: boolean) => void | |
| 12 | +} | |
| 13 | + | |
| 14 | +export const ContentListTableHeader = ({ | |
| 15 | + params, | |
| 16 | + onChange, | |
| 17 | + checked, | |
| 18 | + indeterminate, | |
| 19 | + onCheckAll | |
| 20 | + }: ContentListTableHeaderProps) => { | |
| 21 | + const {handleSort, getSortIcon, isSorted} = useTableSort(params, onChange); | |
| 22 | + | |
| 23 | + return ( | |
| 24 | + <> | |
| 25 | + <colgroup> | |
| 26 | + <col style={{width: "40px"}}/> | |
| 27 | + <col style={{width: "6%"}}/> | |
| 28 | + <col style={{width: "calc((54%/2) - 40px)"}}/> | |
| 29 | + <col style={{width: "calc((54%/2) - 40px)"}}/> | |
| 30 | + <col style={{width: "10%"}}/> | |
| 31 | + <col style={{width: "20%"}}/> | |
| 32 | + <col style={{width: "10%"}}/> | |
| 33 | + </colgroup> | |
| 34 | + <thead> | |
| 35 | + <tr> | |
| 36 | + <th> | |
| 37 | + <CheckBox id="contentCheckAll" | |
| 38 | + name="checkAll" | |
| 39 | + checked={checked} | |
| 40 | + onChange={onCheckAll} | |
| 41 | + indeterminate={indeterminate}/> | |
| 42 | + </th> | |
| 43 | + <SortableHeaderCell field={"cntId"} active={isSorted("cntId")} icon={getSortIcon("cntId")} onSort={handleSort}>번호</SortableHeaderCell> | |
| 44 | + <SortableHeaderCell field={"cntName"} active={isSorted("cntName")} icon={getSortIcon("cntName")} onSort={handleSort}>콘텐츠이름</SortableHeaderCell> | |
| 45 | + <SortableHeaderCell field={"menuNm"} active={isSorted("menuNm")} icon={getSortIcon("menuNm")} onSort={handleSort}>연결메뉴</SortableHeaderCell> | |
| 46 | + <SortableHeaderCell field={"registerId"} active={isSorted("registerId")} icon={getSortIcon("registerId")} onSort={handleSort}>등록자</SortableHeaderCell> | |
| 47 | + <SortableHeaderCell field={"registPnttm"} active={isSorted("registPnttm")} icon={getSortIcon("registPnttm")} onSort={handleSort}>등록일자</SortableHeaderCell> | |
| 48 | + <th scope={"col"}>미리보기</th> | |
| 49 | + </tr> | |
| 50 | + </thead> | |
| 51 | + </> | |
| 52 | + ) | |
| 53 | +}(No newline at end of file) |
+++ src/admin/feature/content/component/ContentListTableRow.tsx
... | ... | @@ -0,0 +1,76 @@ |
| 1 | +import type {ContentListItem} from "../type/content.types.ts"; | |
| 2 | +import type {SearchParams} from "../../../../type/searchParams.ts"; | |
| 3 | +import {CheckBox} from "../../../component/checkbox/CheckBox.tsx"; | |
| 4 | +import {getTableRowNumber} from "../../../component/table/getTableRowNumber.ts"; | |
| 5 | + | |
| 6 | +interface ContentListTableRowProps { | |
| 7 | + item: ContentListItem | |
| 8 | + index: number; | |
| 9 | + searchParams: SearchParams; | |
| 10 | + totalItems: number | |
| 11 | + currentPage: number | |
| 12 | + checked: boolean | |
| 13 | + onCheck: (id: string, checked: boolean) => void | |
| 14 | + onDetail: (cntId: string, cntDtId: string) => void | |
| 15 | + onPreview: (cntId: string, cntDtId: string) => void | |
| 16 | +} | |
| 17 | + | |
| 18 | +export const ContentListTableRow = ( | |
| 19 | + { | |
| 20 | + item, | |
| 21 | + index, | |
| 22 | + searchParams, | |
| 23 | + totalItems, | |
| 24 | + currentPage, | |
| 25 | + checked, | |
| 26 | + onCheck, | |
| 27 | + onDetail, | |
| 28 | + onPreview | |
| 29 | + }: ContentListTableRowProps | |
| 30 | +) => { | |
| 31 | + | |
| 32 | + const rowNumber = getTableRowNumber({ | |
| 33 | + searchParams, | |
| 34 | + totalItems, | |
| 35 | + currentPage, | |
| 36 | + index | |
| 37 | + }) | |
| 38 | + | |
| 39 | + const cntId = item.cntId; | |
| 40 | + const cntDtId = item.cntDtId; | |
| 41 | + | |
| 42 | + return ( | |
| 43 | + <tr> | |
| 44 | + <td> | |
| 45 | + <CheckBox id={`contentCheckList_${cntId}`} | |
| 46 | + name={'checkList'} | |
| 47 | + value={cntId} | |
| 48 | + checked={checked} | |
| 49 | + onChange={(nextChecked => onCheck(cntId, nextChecked))}/> | |
| 50 | + </td> | |
| 51 | + <td> | |
| 52 | + {rowNumber} | |
| 53 | + </td> | |
| 54 | + | |
| 55 | + <td> | |
| 56 | + <button onClick={() => onDetail(cntId, cntDtId)}> | |
| 57 | + {item.cntName} | |
| 58 | + </button> | |
| 59 | + </td> | |
| 60 | + <td> | |
| 61 | + {item.menuNm} | |
| 62 | + </td> | |
| 63 | + <td> | |
| 64 | + {item.registerId} | |
| 65 | + </td> | |
| 66 | + <td> | |
| 67 | + {item.registPnttm} | |
| 68 | + </td> | |
| 69 | + <td> | |
| 70 | + <button className={"btn line primary small"} onClick={() => onPreview(cntId, cntDtId)}> | |
| 71 | + 미리보기 | |
| 72 | + </button> | |
| 73 | + </td> | |
| 74 | + </tr> | |
| 75 | + ) | |
| 76 | +} |
+++ src/admin/feature/content/hook/mutation/useCreateContent.ts
... | ... | @@ -0,0 +1,15 @@ |
| 1 | +import {useMutation, useQueryClient} from "@tanstack/react-query"; | |
| 2 | +import {fetchContentCreate} from "../../api/contentApi.ts"; | |
| 3 | + | |
| 4 | +export const useCreateContent = () => { | |
| 5 | + const queryClient = useQueryClient(); | |
| 6 | + | |
| 7 | + return useMutation({ | |
| 8 | + mutationFn: fetchContentCreate, | |
| 9 | + onSuccess: () => { | |
| 10 | + queryClient.invalidateQueries({ | |
| 11 | + queryKey: ['contentList'], | |
| 12 | + }); | |
| 13 | + }, | |
| 14 | + }); | |
| 15 | +}; |
+++ src/admin/feature/content/hook/mutation/useDeleteBatchContent.ts
... | ... | @@ -0,0 +1,15 @@ |
| 1 | +import {useMutation, useQueryClient} from "@tanstack/react-query"; | |
| 2 | +import {fetchContentDeleteBatch} from "../../api/contentApi.ts"; | |
| 3 | + | |
| 4 | +export const useDeleteBatchContent = () => { | |
| 5 | + const queryClient = useQueryClient(); | |
| 6 | + | |
| 7 | + return useMutation({ | |
| 8 | + mutationFn: fetchContentDeleteBatch, | |
| 9 | + onSuccess: () => { | |
| 10 | + queryClient.invalidateQueries({ | |
| 11 | + queryKey: ['contentList'], | |
| 12 | + }) | |
| 13 | + } | |
| 14 | + }) | |
| 15 | +}(No newline at end of file) |
+++ src/admin/feature/content/hook/mutation/useDeleteContent.ts
... | ... | @@ -0,0 +1,15 @@ |
| 1 | +import {useMutation, useQueryClient} from "@tanstack/react-query"; | |
| 2 | +import {fetchContentDelete} from "../../api/contentApi.ts"; | |
| 3 | + | |
| 4 | +export const useDeleteContent = () => { | |
| 5 | + const queryClient = useQueryClient(); | |
| 6 | + | |
| 7 | + return useMutation({ | |
| 8 | + mutationFn: fetchContentDelete, | |
| 9 | + onSuccess: () => { | |
| 10 | + queryClient.invalidateQueries({ | |
| 11 | + queryKey: ['contentList'], | |
| 12 | + }); | |
| 13 | + }, | |
| 14 | + }); | |
| 15 | +}; |
+++ src/admin/feature/content/hook/mutation/useUpdateContent.ts
... | ... | @@ -0,0 +1,18 @@ |
| 1 | +import {useMutation, useQueryClient} from "@tanstack/react-query"; | |
| 2 | +import {fetchContentUpdate} from "../../api/contentApi.ts"; | |
| 3 | + | |
| 4 | +export const useUpdateContent = () => { | |
| 5 | + const queryClient = useQueryClient(); | |
| 6 | + | |
| 7 | + return useMutation({ | |
| 8 | + mutationFn: fetchContentUpdate, | |
| 9 | + onSuccess: () => { | |
| 10 | + queryClient.invalidateQueries({ | |
| 11 | + queryKey: ['contentList'], | |
| 12 | + }); | |
| 13 | + queryClient.invalidateQueries({ | |
| 14 | + queryKey: ['contentDetail'], | |
| 15 | + }); | |
| 16 | + }, | |
| 17 | + }); | |
| 18 | +}; |
+++ src/admin/feature/content/hook/page/useContentFormPage.ts
... | ... | @@ -0,0 +1,184 @@ |
| 1 | +import {type ChangeEvent, useMemo, useState} from "react"; | |
| 2 | +import {useNavigate} from "react-router-dom"; | |
| 3 | +import {toast} from "react-toastify"; | |
| 4 | +import type {FormActionsModel, FormMode, HeaderModel, StatusModel} from "../../../../../type/viewModel.ts"; | |
| 5 | +import {ADMIN_CONTENT_LIST_ROUTE} from "../../../../route/adminRouteMap.ts"; | |
| 6 | +import type {ContentFormItem} from "../../type/content.types.ts"; | |
| 7 | +import {useContentDetail} from "../query/useContentDetail.ts"; | |
| 8 | +import {useCreateContent} from "../mutation/useCreateContent.ts"; | |
| 9 | +import {useUpdateContent} from "../mutation/useUpdateContent.ts"; | |
| 10 | +import {useDeleteContent} from "../mutation/useDeleteContent.ts"; | |
| 11 | + | |
| 12 | +type ContentFormPageModel = { | |
| 13 | + header: HeaderModel; | |
| 14 | + status: StatusModel; | |
| 15 | + form: { | |
| 16 | + form: ContentFormItem; | |
| 17 | + onChange: (event: ChangeEvent<HTMLInputElement>) => void; | |
| 18 | + onContentChange: (value: string) => void; | |
| 19 | + disabled: boolean; | |
| 20 | + }; | |
| 21 | + actions: FormActionsModel<FormMode>; | |
| 22 | +}; | |
| 23 | + | |
| 24 | +const initContentFormData: ContentFormItem = { | |
| 25 | + cntId: '', | |
| 26 | + cntDtId: '', | |
| 27 | + cntName: '', | |
| 28 | + cntCn: '', | |
| 29 | + registerId: '', | |
| 30 | + registPnttm: '', | |
| 31 | + updusrId: '', | |
| 32 | + updtPnttm: '', | |
| 33 | +}; | |
| 34 | + | |
| 35 | +const createInitialForm = ( | |
| 36 | + item?: ContentFormItem, | |
| 37 | + cntId = '', | |
| 38 | + cntDtId = '', | |
| 39 | +) => ({ | |
| 40 | + ...initContentFormData, | |
| 41 | + cntId, | |
| 42 | + cntDtId, | |
| 43 | + ...item, | |
| 44 | +}); | |
| 45 | + | |
| 46 | +export const useContentFormPage = (cntId: string, cntDtId: string): ContentFormPageModel => { | |
| 47 | + const navigate = useNavigate(); | |
| 48 | + const mode: FormMode = cntId ? 'update' : 'create'; | |
| 49 | + const [formDraft, setFormDraft] = useState<Partial<ContentFormItem>>({}); | |
| 50 | + | |
| 51 | + const {data, isLoading, error} = useContentDetail(cntId, cntDtId, {enabled: !!cntId}); | |
| 52 | + console.log(data); | |
| 53 | + const {mutateAsync: createContent, isPending: isCreating} = useCreateContent(); | |
| 54 | + const {mutateAsync: updateContent, isPending: isUpdating} = useUpdateContent(); | |
| 55 | + const {mutateAsync: deleteContent, isPending: isDeleting} = useDeleteContent(); | |
| 56 | + const isPending = isCreating || isUpdating || isDeleting; | |
| 57 | + | |
| 58 | + const title = `콘텐츠 ${mode === 'create' ? '등록' : '수정'}`; | |
| 59 | + const breadcrumb = [ | |
| 60 | + {label: '콘텐츠 관리', url: ADMIN_CONTENT_LIST_ROUTE}, | |
| 61 | + {label: title}, | |
| 62 | + ]; | |
| 63 | + | |
| 64 | + const baseForm = useMemo( | |
| 65 | + () => createInitialForm(data, cntId, cntDtId), | |
| 66 | + [cntDtId, cntId, data], | |
| 67 | + ); | |
| 68 | + const form = { | |
| 69 | + ...baseForm, | |
| 70 | + ...formDraft, | |
| 71 | + }; | |
| 72 | + | |
| 73 | + const handleChange = (event: ChangeEvent<HTMLInputElement>) => { | |
| 74 | + const {name, value} = event.target; | |
| 75 | + | |
| 76 | + setFormDraft((prev) => ({ | |
| 77 | + ...prev, | |
| 78 | + [name]: value, | |
| 79 | + })); | |
| 80 | + }; | |
| 81 | + | |
| 82 | + const handleContentChange = (value: string) => { | |
| 83 | + setFormDraft((prev) => ({ | |
| 84 | + ...prev, | |
| 85 | + cntCn: value, | |
| 86 | + })); | |
| 87 | + }; | |
| 88 | + | |
| 89 | + const validateForm = () => { | |
| 90 | + if (!form.cntName.trim()) { | |
| 91 | + toast.warning('콘텐츠 이름을 입력해주세요.'); | |
| 92 | + return false; | |
| 93 | + } | |
| 94 | + | |
| 95 | + if (!form.cntCn.trim() || form.cntCn === '<p></p>') { | |
| 96 | + toast.warning('내용을 입력해주세요.'); | |
| 97 | + return false; | |
| 98 | + } | |
| 99 | + | |
| 100 | + return true; | |
| 101 | + }; | |
| 102 | + | |
| 103 | + const handleCreate = async () => { | |
| 104 | + if (!validateForm()) { | |
| 105 | + return; | |
| 106 | + } | |
| 107 | + | |
| 108 | + await toast.promise( | |
| 109 | + createContent(form), | |
| 110 | + { | |
| 111 | + pending: '등록 중...', | |
| 112 | + success: '등록 완료', | |
| 113 | + error: '등록 실패', | |
| 114 | + }, | |
| 115 | + ); | |
| 116 | + | |
| 117 | + handleList(); | |
| 118 | + }; | |
| 119 | + | |
| 120 | + const handleUpdate = async () => { | |
| 121 | + if (!validateForm()) { | |
| 122 | + return; | |
| 123 | + } | |
| 124 | + | |
| 125 | + await toast.promise( | |
| 126 | + updateContent(form), | |
| 127 | + { | |
| 128 | + pending: '수정 중...', | |
| 129 | + success: '수정 완료', | |
| 130 | + error: '수정 실패', | |
| 131 | + }, | |
| 132 | + ); | |
| 133 | + | |
| 134 | + handleList(); | |
| 135 | + }; | |
| 136 | + | |
| 137 | + const handleDelete = async () => { | |
| 138 | + if (!cntId || !window.confirm('콘텐츠를 삭제하시겠습니까?')) { | |
| 139 | + return; | |
| 140 | + } | |
| 141 | + | |
| 142 | + await toast.promise( | |
| 143 | + deleteContent(cntId), | |
| 144 | + { | |
| 145 | + pending: '삭제 중...', | |
| 146 | + success: '삭제 완료', | |
| 147 | + error: '삭제 실패', | |
| 148 | + }, | |
| 149 | + ); | |
| 150 | + | |
| 151 | + handleList(); | |
| 152 | + }; | |
| 153 | + | |
| 154 | + const handleList = () => { | |
| 155 | + navigate(ADMIN_CONTENT_LIST_ROUTE); | |
| 156 | + }; | |
| 157 | + | |
| 158 | + return { | |
| 159 | + header: { | |
| 160 | + title, | |
| 161 | + breadcrumb, | |
| 162 | + homeUrl: '#', | |
| 163 | + }, | |
| 164 | + status: { | |
| 165 | + isLoading, | |
| 166 | + error, | |
| 167 | + successMessage: '콘텐츠 조회가 완료되었습니다.', | |
| 168 | + }, | |
| 169 | + form: { | |
| 170 | + form, | |
| 171 | + onChange: handleChange, | |
| 172 | + onContentChange: handleContentChange, | |
| 173 | + disabled: isPending, | |
| 174 | + }, | |
| 175 | + actions: { | |
| 176 | + mode, | |
| 177 | + disabled: isPending, | |
| 178 | + onCreate: handleCreate, | |
| 179 | + onUpdate: handleUpdate, | |
| 180 | + onDelete: handleDelete, | |
| 181 | + onList: handleList, | |
| 182 | + }, | |
| 183 | + }; | |
| 184 | +}; |
+++ src/admin/feature/content/hook/page/useContentListPage.ts
... | ... | @@ -0,0 +1,176 @@ |
| 1 | +import type {ContentListItem, ContentSearchParams} from "../../type/content.types.ts"; | |
| 2 | +import type { | |
| 3 | + CheckableTableModel, | |
| 4 | + HeaderModel, ListActionsModel, PaginationModel, | |
| 5 | + RowActionsModel, | |
| 6 | + SearchModel, | |
| 7 | + StatusModel | |
| 8 | +} from "../../../../../type/viewModel.ts"; | |
| 9 | +import {useMemo, useState} from "react"; | |
| 10 | +import {useContentList} from "../query/useContentList.ts"; | |
| 11 | +import {useCheckedList} from "../../../../hook/useCheckedList.ts"; | |
| 12 | +import {toast} from "react-toastify"; | |
| 13 | +import {useNavigate} from "react-router-dom"; | |
| 14 | +import {useDeleteBatchContent} from "../mutation/useDeleteBatchContent.ts"; | |
| 15 | +import {ADMIN_CONTENT_FORM_ROUTE} from "../../../../route/adminRouteMap.ts"; | |
| 16 | + | |
| 17 | +type ContentListRowActions = { | |
| 18 | + onDetail: (cntId: string, cntDtId: string) => void, | |
| 19 | + onPreview: (cntId: string, cntDtId: string) => void, | |
| 20 | +} | |
| 21 | + | |
| 22 | +type ContentListPageModel = { | |
| 23 | + header: HeaderModel; | |
| 24 | + status: StatusModel; | |
| 25 | + search: SearchModel<ContentSearchParams> | |
| 26 | + table: CheckableTableModel<ContentListItem, ContentSearchParams> & RowActionsModel<ContentListRowActions> | |
| 27 | + actions: ListActionsModel; | |
| 28 | + pagination: PaginationModel; | |
| 29 | +} | |
| 30 | + | |
| 31 | +const initSearchParams: ContentSearchParams = { | |
| 32 | + pageIndex: 1, | |
| 33 | + pageUnit: 10, | |
| 34 | + searchCnd: "0", | |
| 35 | + searchKeyword: "", | |
| 36 | + searchSortCnd: "", | |
| 37 | + searchSortOrd: "" | |
| 38 | +} | |
| 39 | + | |
| 40 | +const pageSizeOptions = [ | |
| 41 | + {value: '10', label: '10줄'}, | |
| 42 | + {value: '20', label: '20줄'}, | |
| 43 | + {value: '30', label: '30줄'}, | |
| 44 | +] | |
| 45 | + | |
| 46 | +const title = "콘텐츠관리" | |
| 47 | +const breadcrumb = [ | |
| 48 | + {label: "콘텐츠관리"} | |
| 49 | +] | |
| 50 | + | |
| 51 | +export const useContentListPage = (): ContentListPageModel => { | |
| 52 | + const navigate = useNavigate(); | |
| 53 | + const [searchParams, setSearchParams] = useState(initSearchParams); | |
| 54 | + const { | |
| 55 | + list, | |
| 56 | + totalItems, | |
| 57 | + totalPages, | |
| 58 | + currentPage, | |
| 59 | + size, | |
| 60 | + isLoading, | |
| 61 | + error | |
| 62 | + } = useContentList(searchParams); | |
| 63 | + const {mutateAsync: deleteBatchContent} = useDeleteBatchContent(); | |
| 64 | + | |
| 65 | + const contentIds = useMemo(() => list.map((item) => item.cntId), [list]); | |
| 66 | + | |
| 67 | + const { | |
| 68 | + checkedIds, | |
| 69 | + isAllChecked, | |
| 70 | + isPartiallyChecked, | |
| 71 | + isChecked, | |
| 72 | + handleCheck, | |
| 73 | + handleCheckAll | |
| 74 | + } = useCheckedList(contentIds); | |
| 75 | + | |
| 76 | + const handleDetail = (cntId:string, cntDtId: string) => { | |
| 77 | + navigate(`${ADMIN_CONTENT_FORM_ROUTE}?cntId=${cntId}&cntDtId=${cntDtId}`); | |
| 78 | + } | |
| 79 | + | |
| 80 | + const handlePreview = (cntId: string, cntDtId: string) => { | |
| 81 | + const params = new URLSearchParams({ | |
| 82 | + skin: 'user', | |
| 83 | + cntId, | |
| 84 | + cntDtId, | |
| 85 | + }); | |
| 86 | + const previewUrl = `/preview/content?${params.toString()}`; | |
| 87 | + | |
| 88 | + window.open( | |
| 89 | + previewUrl, | |
| 90 | + 'contentPreview', | |
| 91 | + 'width=1200,height=900,top=80,left=120,scrollbars=yes,resizable=yes', | |
| 92 | + ); | |
| 93 | + } | |
| 94 | + | |
| 95 | + const handleDeleteBatch = async () => { | |
| 96 | + if(checkedIds.length === 0) { | |
| 97 | + toast.warning('삭제할 컨텐츠를 선택해주세요'); | |
| 98 | + return; | |
| 99 | + } | |
| 100 | + | |
| 101 | + const contentList = checkedIds.map((cntId) => ({ | |
| 102 | + cntId, | |
| 103 | + })); | |
| 104 | + | |
| 105 | + await toast.promise( | |
| 106 | + deleteBatchContent(contentList), | |
| 107 | + { | |
| 108 | + pending: '삭제 처리중...', | |
| 109 | + success: '삭제 완료', | |
| 110 | + error: '삭제 실패' | |
| 111 | + } | |
| 112 | + ) | |
| 113 | + } | |
| 114 | + | |
| 115 | + const handleCreate = () => { | |
| 116 | + navigate(`${ADMIN_CONTENT_FORM_ROUTE}`); | |
| 117 | + } | |
| 118 | + | |
| 119 | + const handlePageChange = (pageIndex: number) => { | |
| 120 | + setSearchParams((prev) =>({ | |
| 121 | + ...prev, | |
| 122 | + pageIndex | |
| 123 | + })); | |
| 124 | + } | |
| 125 | + | |
| 126 | + return { | |
| 127 | + header: { | |
| 128 | + title, | |
| 129 | + breadcrumb, | |
| 130 | + homeUrl: '#', | |
| 131 | + }, | |
| 132 | + status : { | |
| 133 | + isLoading, | |
| 134 | + error, | |
| 135 | + successMessage: '콘텐츠의 조회가 완료되었습니다.' | |
| 136 | + }, | |
| 137 | + search: { | |
| 138 | + totalItems, | |
| 139 | + searchParams, | |
| 140 | + onChange: setSearchParams, | |
| 141 | + pageSizeOptions, | |
| 142 | + }, | |
| 143 | + table: { | |
| 144 | + items: list, | |
| 145 | + params: searchParams, | |
| 146 | + onChange: setSearchParams, | |
| 147 | + pagination: { | |
| 148 | + totalItems, | |
| 149 | + currentPage, | |
| 150 | + totalPages, | |
| 151 | + }, | |
| 152 | + check: { | |
| 153 | + isAllChecked, | |
| 154 | + isPartiallyChecked, | |
| 155 | + isChecked, | |
| 156 | + onCheck: handleCheck, | |
| 157 | + onCheckAll: handleCheckAll, | |
| 158 | + }, | |
| 159 | + rowActions: { | |
| 160 | + onDetail: handleDetail, | |
| 161 | + onPreview: handlePreview, | |
| 162 | + } | |
| 163 | + }, | |
| 164 | + actions: { | |
| 165 | + onDelete: handleDeleteBatch, | |
| 166 | + onCreate: handleCreate | |
| 167 | + }, | |
| 168 | + pagination: { | |
| 169 | + totalItems, | |
| 170 | + totalPages, | |
| 171 | + currentPage, | |
| 172 | + size, | |
| 173 | + onPageChange: handlePageChange | |
| 174 | + } | |
| 175 | + } | |
| 176 | +} |
+++ src/admin/feature/content/hook/query/useContentDetail.ts
... | ... | @@ -0,0 +1,19 @@ |
| 1 | +import {keepPreviousData, useQuery} from '@tanstack/react-query'; | |
| 2 | +import {fetchContentDetail} from '../../api/contentApi.ts'; | |
| 3 | + | |
| 4 | +type UseContentDetailOptions = { | |
| 5 | + enabled: boolean; | |
| 6 | +}; | |
| 7 | + | |
| 8 | +export const useContentDetail = ( | |
| 9 | + cntId: string, | |
| 10 | + cntDtId: string, | |
| 11 | + options?: UseContentDetailOptions, | |
| 12 | +) => { | |
| 13 | + return useQuery({ | |
| 14 | + queryKey: ['contentDetail', cntId, cntDtId], | |
| 15 | + queryFn: () => fetchContentDetail({cntId, cntDtId}), | |
| 16 | + placeholderData: keepPreviousData, | |
| 17 | + enabled: options?.enabled ?? true, | |
| 18 | + }); | |
| 19 | +}; |
+++ src/admin/feature/content/hook/query/useContentList.ts
... | ... | @@ -0,0 +1,14 @@ |
| 1 | +import {keepPreviousData, useQuery} from "@tanstack/react-query"; | |
| 2 | +import {fetchContentList} from "../../api/contentApi.ts"; | |
| 3 | +import type {SearchParams} from "../../../../../type/searchParams.ts"; | |
| 4 | +import {createPageQueryResult} from "../../../../../type/pageResponse.ts"; | |
| 5 | + | |
| 6 | +export const useContentList = (searchParams: SearchParams) => { | |
| 7 | + const query = useQuery({ | |
| 8 | + queryKey: ['contentList', searchParams], | |
| 9 | + queryFn: () => fetchContentList(searchParams), | |
| 10 | + placeholderData: keepPreviousData | |
| 11 | + }); | |
| 12 | + | |
| 13 | + return createPageQueryResult(query); | |
| 14 | +}(No newline at end of file) |
+++ src/admin/feature/content/page/ContentFormPage.tsx
... | ... | @@ -0,0 +1,28 @@ |
| 1 | +import {useSearchParams} from "react-router-dom"; | |
| 2 | +import {PageHeader} from "../../../component/PageHeader.tsx"; | |
| 3 | +import {useLoadingToast} from "../../../hook/useLoadingToast.ts"; | |
| 4 | +import {ActionButtonFormGroup} from "../../../component/button/ActionButtonFormGroup.tsx"; | |
| 5 | +import {useContentFormPage} from "../hook/page/useContentFormPage.ts"; | |
| 6 | +import {ContentFormTable} from "../component/ContentFormTable.tsx"; | |
| 7 | + | |
| 8 | +export const ContentFormPage = () => { | |
| 9 | + const [urlParams] = useSearchParams(); | |
| 10 | + const cntId = urlParams.get("cntId") || ""; | |
| 11 | + const cntDtId = urlParams.get("cntDtId") || ""; | |
| 12 | + const { | |
| 13 | + header, | |
| 14 | + status, | |
| 15 | + form, | |
| 16 | + actions, | |
| 17 | + } = useContentFormPage(cntId, cntDtId); | |
| 18 | + | |
| 19 | + useLoadingToast(status); | |
| 20 | + | |
| 21 | + return ( | |
| 22 | + <> | |
| 23 | + <PageHeader {...header}/> | |
| 24 | + <ContentFormTable {...form}/> | |
| 25 | + <ActionButtonFormGroup {...actions}/> | |
| 26 | + </> | |
| 27 | + ); | |
| 28 | +} |
+++ src/admin/feature/content/page/ContentListPage.tsx
... | ... | @@ -0,0 +1,24 @@ |
| 1 | +import {useContentListPage} from "../hook/page/useContentListPage.ts"; | |
| 2 | +import {PageHeader} from "../../../component/PageHeader.tsx"; | |
| 3 | +import {ListSearchForm} from "../../../component/ListSearchForm.tsx"; | |
| 4 | +import {Pagination} from "../../../component/pagination/Pagination.tsx"; | |
| 5 | +import {ActionButtonListGroup} from "../../../component/button/ActionButtonListGroup.tsx"; | |
| 6 | +import {useLoadingToast} from "../../../hook/useLoadingToast.ts"; | |
| 7 | +import {ContentListTable} from "../component/ContentListTable.tsx"; | |
| 8 | + | |
| 9 | +export const ContentListPage = () => { | |
| 10 | + const {header, search, status, actions, pagination, table} = useContentListPage(); | |
| 11 | + | |
| 12 | + useLoadingToast(status); | |
| 13 | + return ( | |
| 14 | + <> | |
| 15 | + <PageHeader {...header} /> | |
| 16 | + <ListSearchForm {...search} | |
| 17 | + totalLabel="총 게시물" | |
| 18 | + /> | |
| 19 | + <ContentListTable {...table} /> | |
| 20 | + <ActionButtonListGroup {...actions} /> | |
| 21 | + <Pagination {...pagination} /> | |
| 22 | + </> | |
| 23 | + ) | |
| 24 | +}(No newline at end of file) |
+++ src/admin/feature/content/type/content.types.ts
... | ... | @@ -0,0 +1,29 @@ |
| 1 | +import type {SearchParams} from "../../../../type/searchParams.ts"; | |
| 2 | + | |
| 3 | +export type ContentSearchParams = SearchParams; | |
| 4 | + | |
| 5 | +export interface ContentListItem { | |
| 6 | + cntId: string; | |
| 7 | + cntDtId: string; | |
| 8 | + cntName: string; | |
| 9 | + menuNm: string; | |
| 10 | + registerId: string; | |
| 11 | + registPnttm: string; | |
| 12 | +} | |
| 13 | + | |
| 14 | +export interface ContentFormItem { | |
| 15 | + cntId: string; | |
| 16 | + cntDtId: string; | |
| 17 | + cntName: string; | |
| 18 | + cntCn: string; | |
| 19 | + registerId: string; | |
| 20 | + registPnttm: string; | |
| 21 | + updusrId: string; | |
| 22 | + updtPnttm: string; | |
| 23 | +} | |
| 24 | + | |
| 25 | +export interface DeleteBatchContentRequest { | |
| 26 | + cntId: string; | |
| 27 | +} | |
| 28 | + | |
| 29 | +export type ContentDetailParams = Pick<ContentFormItem, 'cntId' | 'cntDtId'>; |
--- src/admin/route/AdminRoute.tsx
+++ src/admin/route/AdminRoute.tsx
... | ... | @@ -4,7 +4,9 @@ |
| 4 | 4 |
ADMIN_AUTHOR_DETAIL_ROUTE, ADMIN_AUTHOR_GROUP_LIST_ROUTE, |
| 5 | 5 |
ADMIN_AUTHOR_LIST_ROUTE, |
| 6 | 6 |
ADMIN_AUTHOR_ROLE_LIST_ROUTE, ADMIN_BBS_ARTICLE_FORM_ROUTE, |
| 7 |
- ADMIN_BBS_MASTER_ROUTE, ADMIN_MENU_CREATE_MANAGE_ROUTE, ADMIN_MENU_POPUP_ROUTE, ADMIN_ROLE_FORM_ROUTE, |
|
| 7 |
+ ADMIN_BBS_MASTER_ROUTE, ADMIN_CONTENT_FORM_ROUTE, |
|
| 8 |
+ ADMIN_CONTENT_LIST_ROUTE, ADMIN_MENU_CREATE_MANAGE_ROUTE, |
|
| 9 |
+ ADMIN_MENU_CREATE_TREE_ROUTE, ADMIN_MENU_POPUP_ROUTE, ADMIN_ROLE_FORM_ROUTE, |
|
| 8 | 10 |
ADMIN_ROLE_LIST_ROUTE |
| 9 | 11 |
} from "./adminRouteMap.ts"; |
| 10 | 12 |
import {BoardArticleListPage} from "../feature/board/article/page/BoardArticleListPage.tsx";
|
... | ... | @@ -17,7 +19,9 @@ |
| 17 | 19 |
import {AdminLayout} from "../layout/AdminLayout.tsx";
|
| 18 | 20 |
import {AuthorGroupListPage} from "../feature/role/authorGroup/page/AuthorGroupListPage.tsx";
|
| 19 | 21 |
import {RoleListPage} from "../feature/role/role/page/RoleListPage.tsx";
|
| 20 |
- |
|
| 22 |
+import {AuthorFormPage} from "../feature/role/author/page/AuthorFormPage.tsx";
|
|
| 23 |
+import {ContentListPage} from "../feature/content/page/ContentListPage.tsx";
|
|
| 24 |
+import {ContentFormPage} from "../feature/content/page/ContentFormPage.tsx";
|
|
| 21 | 25 |
const ReadyPage = () => {
|
| 22 | 26 |
return <div>Preparing menu.</div>; |
| 23 | 27 |
}; |
... | ... | @@ -28,20 +32,31 @@ |
| 28 | 32 |
<Route path={`${ADMIN_MENU_POPUP_ROUTE}/:authorCode`} element={<AuthorRoleMenuPopupPage/>}/>
|
| 29 | 33 |
|
| 30 | 34 |
<Route element={<AdminLayout/>}>
|
| 35 |
+ {/* bbs */}
|
|
| 31 | 36 |
<Route path="/" element={<Navigate to={ADMIN_BBS_MASTER_ROUTE} replace/>}/>
|
| 32 | 37 |
<Route path={ADMIN_BBS_MASTER_ROUTE} element={<BoardListPage/>}/>
|
| 33 | 38 |
<Route path={`/admin/cop/bbs/article/:bbsId`} element={<BoardArticleListPage/>}/>
|
| 34 | 39 |
<Route path={`${ADMIN_BBS_ARTICLE_FORM_ROUTE}:bbsId`} element={<BoardFormPage/>}/>
|
| 35 | 40 |
<Route path={ADMIN_BBS_ARTICLE_FORM_ROUTE} element={<BoardFormPage/>}/>
|
| 36 | 41 |
|
| 42 |
+ {/* author */}
|
|
| 37 | 43 |
<Route path={ADMIN_AUTHOR_LIST_ROUTE} element={<AuthorListPage/>}/>
|
| 38 |
- <Route path={ADMIN_AUTHOR_DETAIL_ROUTE} element={<ReadyPage/>}/>
|
|
| 44 |
+ <Route path={ADMIN_AUTHOR_DETAIL_ROUTE} element={<AuthorFormPage/>}/>
|
|
| 45 |
+ <Route path={`${ADMIN_AUTHOR_DETAIL_ROUTE}/:authorCode`} element={<AuthorFormPage/>}/>
|
|
| 39 | 46 |
<Route path={ADMIN_AUTHOR_ROLE_LIST_ROUTE} element={<AuthorRoleListPage/>}/>
|
| 40 | 47 |
<Route path={ADMIN_MENU_CREATE_MANAGE_ROUTE} element={<AuthorRoleMenuListPage/>}/>
|
| 41 | 48 |
<Route path={ADMIN_AUTHOR_GROUP_LIST_ROUTE} element={<AuthorGroupListPage/>}/>
|
| 42 | 49 |
<Route path={ADMIN_ROLE_LIST_ROUTE} element={<RoleListPage/>}/>
|
| 43 | 50 |
<Route path={ADMIN_ROLE_FORM_ROUTE} element={<RoleFormPage/>}/>
|
| 44 | 51 |
<Route path={`${ADMIN_ROLE_FORM_ROUTE}/:roleCode`} element={<RoleFormPage/>}/>
|
| 52 |
+ |
|
| 53 |
+ {/* content */}
|
|
| 54 |
+ <Route path={ADMIN_CONTENT_LIST_ROUTE} element={<ContentListPage />} />
|
|
| 55 |
+ <Route path={ADMIN_CONTENT_FORM_ROUTE} element={<ContentFormPage />} />
|
|
| 56 |
+ |
|
| 57 |
+ {/* menu */}
|
|
| 58 |
+ <Route path={ADMIN_MENU_CREATE_TREE_ROUTE} element={<></>} />
|
|
| 59 |
+ |
|
| 45 | 60 |
<Route path="*" element={<ReadyPage/>}/>
|
| 46 | 61 |
</Route> |
| 47 | 62 |
</Routes> |
--- src/admin/route/adminRouteMap.ts
+++ src/admin/route/adminRouteMap.ts
... | ... | @@ -13,7 +13,11 @@ |
| 13 | 13 |
export const ADMIN_ROLE_FORM_ROUTE = `${ADMIN_ROUTE_PREFIX}/sec/rmt/detail.do`;
|
| 14 | 14 |
|
| 15 | 15 |
export const ADMIN_MAIN_ZONE_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/uss/ion/pwm/mainZoneList.do`;
|
| 16 |
+ |
|
| 16 | 17 |
export const ADMIN_CONTENT_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/uss/ion/cnt/contentList.do`;
|
| 18 |
+export const ADMIN_CONTENT_FORM_ROUTE = `${ADMIN_ROUTE_PREFIX}/uss/ion/cnt/contentForm.do`;
|
|
| 19 |
+ |
|
| 20 |
+ |
|
| 17 | 21 |
export const ADMIN_MAIN_PAGE_ROUTE = `${ADMIN_ROUTE_PREFIX}/cmm/main/mainPage.do`;
|
| 18 | 22 |
export const ADMIN_LOGIN_GROUP_POLICY_ROUTE = `${ADMIN_ROUTE_PREFIX}/uat/uap/selectLoginGroupPolicyList.do`;
|
| 19 | 23 |
export const ADMIN_MEMBER_MANAGE_ROUTE = `${ADMIN_ROUTE_PREFIX}/uss/umt/EgovMberManage.do`;
|
--- src/styles/adm/style.css
+++ src/styles/adm/style.css
... | ... | @@ -156,3 +156,20 @@ |
| 156 | 156 |
.menu_list{width:100%;max-height:calc(100vh - 300px);margin:12px 0 0 0;border-radius:4px;}
|
| 157 | 157 |
.menu_detail{width:80%;}
|
| 158 | 158 |
|
| 159 |
+/* rich text editor */ |
|
| 160 |
+.rich_text_editor{width:100%;border:1px solid var(--default-line-color);border-radius:5px;background:#fff;overflow:hidden;}
|
|
| 161 |
+.rich_text_editor_toolbar{display:flex;flex-wrap:wrap;gap:4px;padding:8px;border-bottom:1px solid var(--default-line-color);background:#f5f7f9;}
|
|
| 162 |
+.rich_text_editor_toolbar button{min-width:32px;height:30px;padding:0 8px;border:1px solid #cfd6e2;border-radius:4px;background:#fff;color:#222;font-size:13px;font-weight:700;line-height:1;cursor:pointer;}
|
|
| 163 |
+.rich_text_editor_toolbar button.active{border-color:var(--primary-color);background:#e9eef8;color:var(--primary-color);}
|
|
| 164 |
+.rich_text_editor_toolbar button:disabled{opacity:0.45;cursor:not-allowed;}
|
|
| 165 |
+.rich_text_editor_body{min-height:320px;padding:18px;outline:none;font-size:16px;line-height:1.7;color:var(--body-text-color);}
|
|
| 166 |
+.rich_text_editor_body p{margin:0 0 10px 0;}
|
|
| 167 |
+.rich_text_editor_body h2{margin:0 0 14px 0;font-size:24px;font-weight:700;color:var(--primary-title-color);}
|
|
| 168 |
+.rich_text_editor_body ul,.rich_text_editor_body ol{margin:0 0 12px 22px;}
|
|
| 169 |
+.rich_text_editor_body a{color:var(--primary-color);text-decoration:underline;}
|
|
| 170 |
+.rich_text_editor_body img{max-width:100%;height:auto;}
|
|
| 171 |
+.rich_text_editor_body table{width:100%;border-collapse:collapse;margin:12px 0;}
|
|
| 172 |
+.rich_text_editor_body th,.rich_text_editor_body td{min-width:80px;border:1px solid var(--default-line-color);padding:8px;text-align:left;vertical-align:top;}
|
|
| 173 |
+.rich_text_editor_body th{background:#f2f4f6;font-weight:700;}
|
|
| 174 |
+.rich_text_editor_body .is-empty::before{content:attr(data-placeholder);float:left;height:0;color:#9aa3b2;pointer-events:none;}
|
|
| 175 |
+ |
--- src/styles/usr/style.css
+++ src/styles/usr/style.css
... | ... | @@ -139,3 +139,19 @@ |
| 139 | 139 |
.cmmt_input textarea{width:calc(100% - 95px);height:120px;margin:0 10px 0 0;}
|
| 140 | 140 |
.cmmt_input button.btn.xlarge{height:120px;}
|
| 141 | 141 |
|
| 142 |
+/* content preview */ |
|
| 143 |
+.content_preview{border-top:2px solid var(--primary-color);}
|
|
| 144 |
+.content_preview_header{padding:24px 0;border-bottom:1px solid var(--default-line-color);}
|
|
| 145 |
+.content_preview_header h3{font-size:28px;font-weight:700;color:var(--primary-title-color);}
|
|
| 146 |
+.content_preview_header p{margin:8px 0 0 0;font-size:15px;color:#666;}
|
|
| 147 |
+.content_preview_body{min-height:360px;padding:28px 0;font-size:17px;line-height:1.8;color:var(--body-text-color);}
|
|
| 148 |
+.content_preview_body h2{margin:0 0 18px 0;font-size:26px;font-weight:700;color:var(--primary-title-color);}
|
|
| 149 |
+.content_preview_body p{margin:0 0 12px 0;}
|
|
| 150 |
+.content_preview_body ul,.content_preview_body ol{margin:0 0 14px 24px;}
|
|
| 151 |
+.content_preview_body a{color:var(--primary-color);text-decoration:underline;}
|
|
| 152 |
+.content_preview_body img{max-width:100%;height:auto;}
|
|
| 153 |
+.content_preview_body table{width:100%;border-collapse:collapse;margin:16px 0;}
|
|
| 154 |
+.content_preview_body th,.content_preview_body td{border:1px solid var(--default-line-color);padding:10px;text-align:left;vertical-align:top;}
|
|
| 155 |
+.content_preview_body th{background:#f2f4f6;font-weight:700;}
|
|
| 156 |
+.content_preview_empty{padding:60px 0;text-align:center;color:#666;}
|
|
| 157 |
+ |
+++ src/user/UserContentPreviewPage.tsx
... | ... | @@ -0,0 +1,43 @@ |
| 1 | +import DOMPurify from 'dompurify'; | |
| 2 | +import {useMemo} from 'react'; | |
| 3 | +import {useSearchParams} from 'react-router-dom'; | |
| 4 | +import {useContentDetail} from '../admin/feature/content/hook/query/useContentDetail.ts'; | |
| 5 | + | |
| 6 | +export function UserContentPreviewPage() { | |
| 7 | + const [params] = useSearchParams(); | |
| 8 | + const cntId = params.get('cntId') ?? ''; | |
| 9 | + const cntDtId = params.get('cntDtId') ?? ''; | |
| 10 | + const {data, isLoading, error} = useContentDetail(cntId, cntDtId, {enabled: !!cntId}); | |
| 11 | + | |
| 12 | + const contentHtml = useMemo( | |
| 13 | + () => DOMPurify.sanitize(data?.cntCn ?? ''), | |
| 14 | + [data?.cntCn], | |
| 15 | + ); | |
| 16 | + | |
| 17 | + if (!cntId) { | |
| 18 | + return <div className="content_preview_empty">미리보기 콘텐츠 정보가 없습니다.</div>; | |
| 19 | + } | |
| 20 | + | |
| 21 | + if (isLoading) { | |
| 22 | + return <div className="content_preview_empty">미리보기를 불러오는 중입니다.</div>; | |
| 23 | + } | |
| 24 | + | |
| 25 | + if (error) { | |
| 26 | + return <div className="content_preview_empty">미리보기를 불러오지 못했습니다.</div>; | |
| 27 | + } | |
| 28 | + | |
| 29 | + return ( | |
| 30 | + <article className="content_preview"> | |
| 31 | + <header className="content_preview_header"> | |
| 32 | + <h3>{data?.cntName}</h3> | |
| 33 | + {data?.updtPnttm || data?.registPnttm ? ( | |
| 34 | + <p>{data.updtPnttm || data.registPnttm}</p> | |
| 35 | + ) : null} | |
| 36 | + </header> | |
| 37 | + <div | |
| 38 | + className="content_preview_body" | |
| 39 | + dangerouslySetInnerHTML={{__html: contentHtml}} | |
| 40 | + /> | |
| 41 | + </article> | |
| 42 | + ); | |
| 43 | +} |
--- src/user/UserLayout.tsx
+++ src/user/UserLayout.tsx
... | ... | @@ -109,7 +109,12 @@ |
| 109 | 109 |
); |
| 110 | 110 |
} |
| 111 | 111 |
|
| 112 |
-export function UserLayout({ children }: { children: ReactNode }) {
|
|
| 112 |
+type UserLayoutProps = {
|
|
| 113 |
+ children: ReactNode; |
|
| 114 |
+ title?: string; |
|
| 115 |
+}; |
|
| 116 |
+ |
|
| 117 |
+export function UserLayout({ children, title = '공지사항' }: UserLayoutProps) {
|
|
| 113 | 118 |
return ( |
| 114 | 119 |
<div className="wrap"> |
| 115 | 120 |
<UserHeader /> |
... | ... | @@ -118,7 +123,7 @@ |
| 118 | 123 |
<UserSideMenu /> |
| 119 | 124 |
<div className="content_wrap"> |
| 120 | 125 |
<div className="content_title"> |
| 121 |
- <h2>공지사항</h2> |
|
| 126 |
+ <h2>{title}</h2>
|
|
| 122 | 127 |
</div> |
| 123 | 128 |
<div className="contents">{children}</div>
|
| 124 | 129 |
</div> |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?