diff --git a/package-lock.json b/package-lock.json index 7f00162..fcdd50b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,11 +25,14 @@ "react": "^17.0.2", "react-circular-progressbar": "^2.0.4", "react-dom": "^17.0.2", + "react-error-boundary": "^3.1.4", "react-intersection-observer": "^8.33.1", + "react-query": "^3.34.16", "react-router-dom": "^6.2.1", "react-scripts": "5.0.0", "react-textarea-autosize": "^8.3.3", "react-transition-group": "^4.4.2", + "remove-markdown": "^0.3.0", "styled-components": "^5.3.3", "web-vitals": "^2.1.3" }, @@ -5427,6 +5430,14 @@ "node": ">= 8.0.0" } }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -5549,6 +5560,21 @@ "node": ">=8" } }, + "node_modules/broadcast-channel": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", + "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", + "dependencies": { + "@babel/runtime": "^7.7.2", + "detect-node": "^2.1.0", + "js-sha3": "0.8.0", + "microseconds": "0.2.0", + "nano-time": "1.0.0", + "oblivious-set": "1.0.0", + "rimraf": "3.0.2", + "unload": "2.2.0" + } + }, "node_modules/browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -11668,6 +11694,11 @@ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==" }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -12088,6 +12119,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/match-sorter": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz", + "integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "remove-accents": "0.4.2" + } + }, "node_modules/mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -12186,6 +12226,11 @@ "node": ">=8.6" } }, + "node_modules/microseconds": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", + "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==" + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -12462,6 +12507,14 @@ "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" }, + "node_modules/nano-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", + "integrity": "sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=", + "dependencies": { + "big-integer": "^1.6.16" + } + }, "node_modules/nanoid": { "version": "3.1.30", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", @@ -12905,6 +12958,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oblivious-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", + "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==" + }, "node_modules/obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -14863,6 +14921,21 @@ "react": "17.0.2" } }, + "node_modules/react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-error-overlay": { "version": "6.0.10", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz", @@ -14886,6 +14959,31 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-query": { + "version": "3.34.16", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.34.16.tgz", + "integrity": "sha512-7FvBvjgEM4YQ8nPfmAr+lJfbW95uyW/TVjFoi2GwCkF33/S8ajx45tuPHPFGWs4qYwPy1mzwxD4IQfpUDrefNQ==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "broadcast-channel": "^3.4.1", + "match-sorter": "^6.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -15294,6 +15392,16 @@ "node": ">= 0.10" } }, + "node_modules/remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=" + }, + "node_modules/remove-markdown": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/remove-markdown/-/remove-markdown-0.3.0.tgz", + "integrity": "sha1-XktmdJOpNXlyjz1S7MHbnKUF3Jg=" + }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -17282,6 +17390,15 @@ "node": ">= 10.0.0" } }, + "node_modules/unload": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", + "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", + "dependencies": { + "@babel/runtime": "^7.6.2", + "detect-node": "^2.0.4" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -22179,6 +22296,11 @@ "tryer": "^1.0.1" } }, + "big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==" + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -22279,6 +22401,21 @@ "fill-range": "^7.0.1" } }, + "broadcast-channel": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", + "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", + "requires": { + "@babel/runtime": "^7.7.2", + "detect-node": "^2.1.0", + "js-sha3": "0.8.0", + "microseconds": "0.2.0", + "nano-time": "1.0.0", + "oblivious-set": "1.0.0", + "rimraf": "3.0.2", + "unload": "2.2.0" + } + }, "browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -26730,6 +26867,11 @@ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==" }, + "js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -27058,6 +27200,15 @@ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==" }, + "match-sorter": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz", + "integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==", + "requires": { + "@babel/runtime": "^7.12.5", + "remove-accents": "0.4.2" + } + }, "mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -27131,6 +27282,11 @@ "picomatch": "^2.2.3" } }, + "microseconds": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", + "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==" + }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -27331,6 +27487,14 @@ "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" }, + "nano-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", + "integrity": "sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=", + "requires": { + "big-integer": "^1.6.16" + } + }, "nanoid": { "version": "3.1.30", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", @@ -27648,6 +27812,11 @@ "es-abstract": "^1.19.1" } }, + "oblivious-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", + "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==" + }, "obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -28974,6 +29143,14 @@ "scheduler": "^0.20.2" } }, + "react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "requires": { + "@babel/runtime": "^7.12.5" + } + }, "react-error-overlay": { "version": "6.0.10", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz", @@ -28995,6 +29172,16 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "react-query": { + "version": "3.34.16", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.34.16.tgz", + "integrity": "sha512-7FvBvjgEM4YQ8nPfmAr+lJfbW95uyW/TVjFoi2GwCkF33/S8ajx45tuPHPFGWs4qYwPy1mzwxD4IQfpUDrefNQ==", + "requires": { + "@babel/runtime": "^7.5.5", + "broadcast-channel": "^3.4.1", + "match-sorter": "^6.0.2" + } + }, "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -29300,6 +29487,16 @@ "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" }, + "remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=" + }, + "remove-markdown": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/remove-markdown/-/remove-markdown-0.3.0.tgz", + "integrity": "sha1-XktmdJOpNXlyjz1S7MHbnKUF3Jg=" + }, "renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -30797,6 +30994,15 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" }, + "unload": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", + "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", + "requires": { + "@babel/runtime": "^7.6.2", + "detect-node": "^2.0.4" + } + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 09ed767..cebe668 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,14 @@ "react": "^17.0.2", "react-circular-progressbar": "^2.0.4", "react-dom": "^17.0.2", + "react-error-boundary": "^3.1.4", "react-intersection-observer": "^8.33.1", + "react-query": "^3.34.16", "react-router-dom": "^6.2.1", "react-scripts": "5.0.0", "react-textarea-autosize": "^8.3.3", "react-transition-group": "^4.4.2", + "remove-markdown": "^0.3.0", "styled-components": "^5.3.3", "web-vitals": "^2.1.3" }, diff --git a/public/assets/42mainlogo3.png b/public/assets/42mainlogo3.png new file mode 100644 index 0000000..9d56ba3 Binary files /dev/null and b/public/assets/42mainlogo3.png differ diff --git a/public/assets/CharacterWhiteBG/bbo.png b/public/assets/CharacterWhiteBG/bbo.png index ccbfd62..e3cf036 100644 Binary files a/public/assets/CharacterWhiteBG/bbo.png and b/public/assets/CharacterWhiteBG/bbo.png differ diff --git a/public/assets/CharacterWhiteBG/bora.png b/public/assets/CharacterWhiteBG/bora.png index 63130e8..3a1cc47 100644 Binary files a/public/assets/CharacterWhiteBG/bora.png and b/public/assets/CharacterWhiteBG/bora.png differ diff --git a/public/assets/CharacterWhiteBG/ddub.png b/public/assets/CharacterWhiteBG/ddub.png index c99b12a..bf5213c 100644 Binary files a/public/assets/CharacterWhiteBG/ddub.png and b/public/assets/CharacterWhiteBG/ddub.png differ diff --git a/public/assets/CharacterWhiteBG/nana.png b/public/assets/CharacterWhiteBG/nana.png index f0460e1..109b1d2 100644 Binary files a/public/assets/CharacterWhiteBG/nana.png and b/public/assets/CharacterWhiteBG/nana.png differ diff --git a/src/App.jsx b/src/App.jsx index 9bd67e1..6289c2b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -6,6 +6,7 @@ import { Route, Navigate, } from 'react-router-dom'; +import { QueryClient, QueryClientProvider, QueryCache } from 'react-query'; import { UserService } from 'Network'; @@ -75,142 +76,156 @@ const PrivateRouteCheckFtAuth = ({ children }) => { // return children; }; +const queryClient = new QueryClient({ + defaultOptions: { + onError: () => { + console.log('시발'); + }, + queries: { retry: false, suspense: true }, + }, + // queryCache: new QueryCache({ + // onError: () => alert('시발'), + // }), +}); + // 글 보기 : 모드view?글id=12 or view/글id // 글 작성 : free?mode=write const App = () => { return ( - - - - } /> - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - - } - /> - - - - - - } - /> - - - - - - - } - /> - - - - - - - } - /> - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - } - /> - } /> - - - + + + + + } /> + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + + } + /> + + + + + + } + /> + + + + + + + } + /> + + + + + + + } + /> + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + } + /> + } /> + + + + ); }; diff --git a/src/Components/PreviewArticle.jsx b/src/Components/PreviewArticle.jsx index ff47b36..25931db 100644 --- a/src/Components/PreviewArticle.jsx +++ b/src/Components/PreviewArticle.jsx @@ -1,4 +1,5 @@ -import dayjs from 'dayjs'; +import { getArticleTime, isNewArticle } from 'Utils/dayjsUtils'; +import removeMarkdown from 'remove-markdown'; import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; import SmsOutlined from '@mui/icons-material/SmsOutlined'; @@ -6,12 +7,7 @@ import SmsOutlined from '@mui/icons-material/SmsOutlined'; import Styled from './PreviewArticle.styled'; const PreviewArticle = ({ article, isBestArticle, onClickArticle }) => { - const getArticleTime = time => - dayjs(time).isSame(dayjs(), 'day') - ? dayjs(time).format('HH:mm') - : dayjs(time).format('MM/DD'); - const isNewArticle = time => dayjs().isBefore(dayjs(time).add(12, "hour")); - isNewArticle(article.createdAt); + const getPlainText = text => removeMarkdown(text).replaceAll('\\', ''); return ( { {isNewArticle(article.createdAt) && } {article.title} -
{article.content}
+
{getPlainText(article.content)}
{article.writer &&

{article.writer.nickname}

}

{getArticleTime(article.createdAt)}

diff --git a/src/Components/PreviewArticleNoti.jsx b/src/Components/PreviewArticleNoti.jsx index 3f8ab80..e5cfb01 100644 --- a/src/Components/PreviewArticleNoti.jsx +++ b/src/Components/PreviewArticleNoti.jsx @@ -1,13 +1,8 @@ -import dayjs from 'dayjs'; +import { getArticleTime } from 'Utils/dayjsUtils'; import Styled from './PreviewArticle.styled'; const PreviewArticleNoti = ({ article, onClickArticle }) => { - const getArticleTime = time => - dayjs(time).isSame(dayjs(), 'day') - ? dayjs(time).format('HH:mm') - : dayjs(time).format('MM/DD'); - return ( { return `${API.url('/articles')}${path}`; @@ -91,6 +92,25 @@ const ArticleService = { * `200` : success \ * `401` : fail */ + getAllArticles: async (categoryId, page) => { + const method = 'GET'; + const url = articleUrl(''); + const take = 1000; + // const take = 3; + const params = { categoryId, page, take }; + + let response; + try { + response = await API.AXIOS({ + params, + method, + url, + }); + } catch (error) { + alert(error); + } + return response.data; + }, getArticles: async (categoryId, page) => { const method = 'GET'; const url = articleUrl(''); @@ -145,21 +165,23 @@ const ArticleService = { * `200` : success * `401` : fail */ - getArticlesById: async articlesId => { + getArticleById: async articleId => { const method = 'GET'; - const url = articleUrl(`/${articlesId}`); + const url = articleUrl(`/${articleId}`); - let response; - try { - response = await API.AXIOS({ - method, - url, - }); - } catch (error) { - alert(error); - } + const response = await API.AXIOS({ + method, + url, + }); return response.data; }, + // useArticle(articleId) { + // return useQuery( + // ['getArticleById', articleId], + // async () => await this.getArticleById(articleId), + // { suspense: true }, + // ); + // }, /** * **UPDATE** One Articles By Articles ID * @param {string} articlesId @@ -196,15 +218,10 @@ const ArticleService = { const method = 'DELETE'; const url = articleUrl(`/${articlesId}`); - let response; - try { - response = await API.AXIOS({ - method, - url, - }); - } catch (error) { - alert(error); - } + const response = await API.AXIOS({ + method, + url, + }); return response.data; }, /** @@ -238,18 +255,25 @@ const ArticleService = { const url = articleUrl(`/${articlesId}/comments`); const params = { order, page, take }; - let response; - try { - response = await API.AXIOS({ - method, - url, - params, - }); - } catch (error) { - alert(error); - } + const response = await API.AXIOS({ + method, + url, + params, + }); return response.data; }, + // useComments(articleId, order, page, take) { + // return useQuery( + // ['getCommentsById'], + // async () => + // await ArticleService.getArticlesCommentsById( + // articleId, + // order, + // page, + // take, + // ), + // ); + // }, editArticles: async (articlesId, articles) => { const method = 'PUT'; const url = articleUrl(`/${articlesId}`); diff --git a/src/Network/ArticleService_old.js b/src/Network/ArticleService_old.js deleted file mode 100644 index 8a162af..0000000 --- a/src/Network/ArticleService_old.js +++ /dev/null @@ -1,93 +0,0 @@ -// # 게시글 /articles -import { Article, Comment } from '../Entities'; -import * as API from './APIType'; - -const authUrl = path => { - return `${API.url('/artiles')}path`; -}; - -// - 가져오기 -// - 카테고리별 게시글 목록 GET /articles?category=”anonymous” -// - 게시글 상세 GET /articles/:id -// - 조회수 갱신까지 같이 한다. <❗️추후 수정 여지 있음❗️> -// - 댓글 가져오기 GET /articles/:id/comments -// - 추가하기 POST /articles -// - 수정하기 -// - 본문 / 제목 수정하기 PUT /articles/:id -// - 지우기 DELETE /articles/:id - -const generateRandomArticle = () => { - const id = 1; - const category_id = 1; - const writer_id = 1; - const title = 'this is title'; - const content = 'this is content'; - const view_count = 1; - const comment_count = 2; - const liked_count = 3; - - return new Article( - id, - category_id, - writer_id, - title, - content, - view_count, - comment_count, - liked_count, - ); -}; - -const generateRandomComment = () => { - const id = 1; - const article_id = 1; - const writer_id = 1; - const content = 'this is comment'; - - return new Comment(id, article_id, writer_id, content); -}; - -const ArticleService = { - fetchAllArticle: category_id => { - return [ - generateRandomArticle(), - generateRandomArticle(), - generateRandomArticle(), - generateRandomArticle(), - generateRandomArticle(), - generateRandomArticle(), - generateRandomArticle(), - generateRandomArticle(), - generateRandomArticle(), - generateRandomArticle(), - generateRandomArticle(), - generateRandomArticle(), - generateRandomArticle(), - generateRandomArticle(), - generateRandomArticle(), - generateRandomArticle(), - generateRandomArticle(), - ]; - }, - fetchArticle: id => { - return generateRandomArticle(); - }, - fetchArticleComments: id => { - return [ - generateRandomComment(), - generateRandomComment(), - generateRandomComment(), - ]; - }, - createArticle: (title, content) => { - return true; - }, - updateArticle: (id, title, content) => { - return true; - }, - deleteArticle: id => { - return true; - }, -}; - -export default ArticleService; diff --git a/src/Network/CommentService_old.js b/src/Network/CommentService_old.js deleted file mode 100644 index 89fd53a..0000000 --- a/src/Network/CommentService_old.js +++ /dev/null @@ -1,28 +0,0 @@ -// # 댓글 /comments - -// - 쓰기 POST /comments/:id -// - 수정하기 PUT /comments/:id -// - 삭제하기 DELETE /comments/:id - -const generateRandomComment = () => { - const id = 1; - const article_id = 1; - const writer_id = 1; - const content = "this is comment"; - - return new Comment(id, article_id, writer_id, content); -}; - -const CommentService = { - createComment: (article_id, writer_id, content) => { - return generateRandomComment(); - }, - updateComment: (id) => { - return true; - }, - deleteComment: (id) => { - return true; - }, -}; - -export default CommentService; diff --git a/src/Network/ReactionService.js b/src/Network/ReactionService.js index 27f22be..9d4264a 100644 --- a/src/Network/ReactionService.js +++ b/src/Network/ReactionService.js @@ -13,15 +13,10 @@ const ReactionService = { createArticleReactionHeart: async id => { const method = 'POST'; const url = reactionUrl(`/${id}`); - let response; - try { - response = await API.AXIOS({ - method, - url, - }); - } catch (error) { - console.log('error'); - } + const response = await API.AXIOS({ + method, + url, + }); return response.data; }, @@ -34,15 +29,10 @@ const ReactionService = { createCommentReactionHeart: async (articleId, commentId) => { const method = 'POST'; const url = reactionUrl(`/${articleId}/comments/${commentId}`); - let response; - try { - response = await API.AXIOS({ - method, - url, - }); - } catch (error) { - console.log('error'); - } + const response = await API.AXIOS({ + method, + url, + }); return response.data; }, }; diff --git a/src/Network/UserService_old.js b/src/Network/UserService_old.js deleted file mode 100644 index bc482d5..0000000 --- a/src/Network/UserService_old.js +++ /dev/null @@ -1,94 +0,0 @@ -import { User, Notification } from '../Entities'; -import * as API from './APIType'; -// # 유저 /users - -// - 인증 -// - 로그아웃 DELETE /users/logout -// - 깃헙 로그인 인증 POST /users/github -// - 유저 42 인증 요청 -// - 42 로그인으로 인증 요청 POST /users/42auth/login -// - 42 이메일로 인증 요청 POST /users/42auth/email -// - 회원 탈퇴 DELETE /users -// - 정보 -// - 로그인 및 내 정보 가져오기 GET /users -// - 프로필 정보 수정 PUT /users/profile -// - 닉네임 중복확인 GET /users/nickname -// - 알람 -// - 가져오기 GET /users/alarm -// - 읽음 PUT /users/alarm/readall <❗️추후 수정 여지 있음❗️> - -const generateRandomUser = () => { - const id = 1; - const nickname = 'ycha'; - const is_authenticated = true; - const role = 'CADET'; - const charactor = 'https://picsum.photos/200/200'; - - return new User(id, nickname, is_authenticated, role, charactor); -}; - -const generateRandomNotification = () => { - const id = 1; - const user_id = 'ycha'; - const type = 'NEW_COMMENT'; - const content = 'asdf'; - const time = new Date(); - const is_read = true; - - return new Notification(id, user_id, type, content, time, is_read); -}; - -const UserService = { - Auth: { - signOut: () => {}, - validateGithub: token => { - return true; - }, - validate42Login: intra_token => { - return true; - }, - validate42Email: intra_id => { - return true; - }, - deleteUser: () => {}, - }, - Info: { - fetchUser: async () => { - try { - const method = 'GET'; - const headers = {}; - const body = {}; - const url = API.url('path'); - const response = await API.AXIOS({ - method, - headers, - body, - url, - }); - } catch (error) { - console.log('error'); - } - return generateRandomUser(); - }, - updateUserProfile: (character, nickname) => { - return true; - }, - checkDuplicateNickname: nickname => { - return true; - }, - }, - Noti: { - fetchNotification: () => { - return [ - generateRandomNotification(), - generateRandomNotification(), - generateRandomNotification(), - ]; - }, - updateNotification: () => { - return true; - }, - }, -}; - -export default UserService; diff --git a/src/Pages/AlarmPage/Components/AlarmBody.jsx b/src/Pages/AlarmPage/Components/AlarmBody.jsx index a25ddb4..1774a34 100644 --- a/src/Pages/AlarmPage/Components/AlarmBody.jsx +++ b/src/Pages/AlarmPage/Components/AlarmBody.jsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import NotificationService from 'Network/NotificationService'; -import dayjs from 'dayjs'; +import { getArticleTime } from 'Utils/dayjsUtils'; import Styled from './AlarmArticle.styled.js'; @@ -26,8 +26,7 @@ const AlarmBody = () => { const mainTextLen = 10; const moveArticles = articleId => { - alert('구현 중입니다!'); - // navi(`/article/${articleId}`); + navi(`/article/${articleId}`); }; const previewMainText = article => { @@ -38,8 +37,6 @@ const AlarmBody = () => { return alarmType(context, article.type); }; - const getArticleTime = time => dayjs(time).format('MM/DD HH:mm'); - const readAlarm = async () => { const read = await NotificationService.readAllNotifications(); }; @@ -68,7 +65,7 @@ const AlarmBody = () => { className="article" isRead={article.isRead} isNotice={false} - onClick={() => moveArticles(article.userId)} + onClick={() => moveArticles(article.articleId)} >
새 댓글
{previewMainText(article)}
diff --git a/src/Pages/ArticlePage/ArticlePage.jsx b/src/Pages/ArticlePage/ArticlePage.jsx index 95760d9..02b5d15 100644 --- a/src/Pages/ArticlePage/ArticlePage.jsx +++ b/src/Pages/ArticlePage/ArticlePage.jsx @@ -1,20 +1,32 @@ +import { Suspense, useContext } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; import { useParams } from 'react-router-dom'; import { Header } from 'Components'; -import { Body } from './Components'; +import { Article } from './Components'; +import { Comments } from './Components'; +import { Loading } from 'Components'; import Styled from './ArticlePage.styled'; +import { ErrorPage } from 'Pages'; +import { AuthContext } from 'App'; +import CreateComment from './Components/CreateComment'; const ArticlePage = () => { + const { curUser } = useContext(AuthContext); const { id } = useParams(); return ( - <> + }>
- - - - + }> + +
+ + + + + ); }; diff --git a/src/Pages/ArticlePage/ArticlePage.styled.js b/src/Pages/ArticlePage/ArticlePage.styled.js index 25b68c0..e98c652 100644 --- a/src/Pages/ArticlePage/ArticlePage.styled.js +++ b/src/Pages/ArticlePage/ArticlePage.styled.js @@ -1,13 +1,176 @@ import styled, { css } from 'styled-components'; import GlobalStyled from 'Styled/Global.styled'; -const ProfileImage = styled.img` - ${props => (props.width ? `width: ${props.width};` : 'width: 2.5rem;')} - ${props => - props.width ? `min-width: ${props.width};` : 'min-width: 2.5rem;'} - ${props => (props.width ? `height: ${props.width};` : 'height: 2.5rem;')} - border-radius: 10%; - border: 0px; +const ArticlePageDiv = styled.div` + display: flex; + flex-direction: column; + align-items: center; + + .comment_list_div { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + } + } +`; + +/// Article +const ArticleViewDiv = styled.div` + display: flex; + flex-direction: column; + width: 100%; + + .content_top { + display: flex; + flex-direction: row; + align-items: flex-end; + padding: 0.7rem; + width: 100%; + border-bottom: 1px solid ${GlobalStyled.theme.borderColor}; + + .title { + display: flex; + flex-direction: column; + width: 100%; + + h1 { + font-size: 1rem; + font-weight: 600; + color: ${GlobalStyled.theme.textColor}; + margin-bottom: 0.2rem; + padding-right: 0.5rem; + } + + .info { + display: flex; + flex-direction: row; + align-items: center; + + h2 { + color: ${GlobalStyled.theme.textColorGray}; + font-size: 0.7rem; + font-weight: 300; + margin-right: 0.8rem; + } + + .edit_article { + display: flex; + flex-direction: row; + align-items: flex-end; + margin: 0 0.5rem; + margin-left: auto; + + button { + padding: 0; + border: none; + width: max-content; + background: none; + cursor: pointer; + margin: 0 0.3rem; + font-size: 0.7rem; + font-weight: 400; + } + } + } + } + } + + .toastui-editor-contents { + padding: 0.7rem; + } +`; + +const ArticleReactionDiv = styled.div` + display: flex; + align-items: center; + justify-content: center; + flex-direction: row; + margin-top: 1em; + margin-bottom: 0.2rem; + font-size: 0.95rem; + color: ${GlobalStyled.theme.likedCountColor}; + + svg { + width: 2rem; + height: 2rem; + margin-right: 0.2rem; + cursor: pointer; + } + + &::after { + content: '${props => { + if (props.likedCount > 0) return props.likedCount; + else return ''; + }}'; + } +`; +/// + +/// Comments +const CommentsCountDiv = styled.div` + display: flex; + flex-direction: row; + width: 100%; + align-items: center; + justify-content: flex-start; + color: ${GlobalStyled.theme.commentIconColor}; + font-size: 0.85rem; + + margin-top: 0.5rem; + padding: 0.3rem 0.8rem 0.5rem 0.8rem; + border-top: 1.5px solid ${GlobalStyled.theme.borderColor}; + + svg { + width: 1.3rem; + height: 1.3rem; + margin-top: 0.2rem; + margin-right: 0.2rem; + } + + &::after { + content: '${props => { + if (props.commentCount > 0) return props.commentCount; + else return ''; + }}'; + } +`; + +const CommentViewDiv = styled.div` + border-top: 1px solid #e6e6e6; + padding: 0.5rem 0.8rem; + width: 100%; + display: flex; + flex-direction: column; + + .info { + margin-bottom: 0.4rem; + display: flex; + flex-direction: row; + align-items: center; + ${props => props.isMine && `flex-direction: row-reverse;`} + + .text { + margin: 0rem 0.7rem; + ${props => props.isMine && `text-align: right;`} + h1 { + color: ${GlobalStyled.theme.textColor}; + font-size: 0.9rem; + font-weight: 600; + line-height: 1.1; + } + h2 { + color: ${GlobalStyled.theme.textColorGray}; + font-size: 0.4rem; + font-weight: 400; + } + } + } + + .text { + margin-left: 0.2rem; + word-break: break-all; + } `; const CommentContent = styled.div` @@ -75,8 +238,10 @@ const CommentContent = styled.div` } } `; +/// -const CreateCommentDiv = styled.div` +/// CreateComment +const CreateCommentViewDiv = styled.div` position: sticky; bottom: 1rem; display: flex; @@ -135,183 +300,24 @@ const CreateCommentDiv = styled.div` } `; -const ArticleLikedDiv = styled.div` - display: flex; - align-items: center; - justify-content: center; - flex-direction: row; - margin-top: 1em; - margin-bottom: 0.2rem; - font-size: 0.95rem; - color: ${GlobalStyled.theme.buttonRed1}; - - svg { - width: 2rem; - height: 2rem; - margin-right: 0.2rem; - cursor: pointer; - } - - &::after { - content: '${props => { - if (props.likedCount > 0) return props.likedCount; - else return ''; - }}'; - } -`; - -const ArticleCommentDiv = styled.div` - display: flex; - flex-direction: row; - width: 100%; - align-items: center; - justify-content: flex-start; - color: ${GlobalStyled.theme.textBlue}; - font-size: 0.85rem; - - margin-top: 0.5rem; - padding: 0.3rem 0.8rem 0.5rem 0.8rem; - border-top: 1.5px solid ${GlobalStyled.theme.lineGray2}; - - svg { - width: 1.3rem; - height: 1.3rem; - margin-top: 0.2rem; - margin-right: 0.2rem; - } - - &::after { - content: '${props => { - if (props.commentCount > 0) return props.commentCount; - else return ''; - }}'; - } -`; - -const ArticlePageDiv = styled.div` - display: flex; - flex-direction: column; - align-items: center; - - .comment_list_div { - display: flex; - flex-direction: column; - align-items: center; - width: 100%; - } - } - - .content_div { - display: flex; - flex-direction: column; - width: 100%; - - .content_top { - display: flex; - flex-direction: row; - align-items: flex-end; - padding: 0.7rem; - width: 100%; - border-bottom: 1px solid ${GlobalStyled.theme.lineGray2}; - - .title { - display: flex; - flex-direction: column; - width: 100%; - - h1 { - font-size: 1rem; - font-weight: 600; - color: ${GlobalStyled.theme.textBlack}; - margin-bottom: 0.2rem; - padding-right: 0.5rem; - } - - .info { - display: flex; - flex-direction: row; - align-items: center; - - h2 { - color: ${GlobalStyled.theme.textGray3}; - font-size: 0.7rem; - font-weight: 300; - margin-right: 0.8rem; - } - - .edit_article { - display: flex; - flex-direction: row; - align-items: flex-end; - margin: 0 0.5rem; - margin-left: auto; - - button { - padding: 0; - border: none; - width: max-content; - background: none; - cursor: pointer; - margin: 0 0.3rem; - font-size: 0.7rem; - font-weight: 400; - } - } - } - } - } - - .toastui-editor-contents { - padding: 0.7rem; - } - } -`; - -const CommentDiv = styled.div` - border-top: 1px solid #e6e6e6; - padding: 0.5rem 0.8rem; - width: 100%; - display: flex; - flex-direction: column; - - .info { - margin-bottom: 0.4rem; - display: flex; - flex-direction: row; - align-items: center; - ${props => props.isMine && `flex-direction: row-reverse;`} - - .text { - margin: 0rem 0.7rem; - ${props => props.isMine && `text-align: right;`} - h1 { - color: ${GlobalStyled.theme.textBlack}; - font-size: 0.9rem; - font-weight: 600; - line-height: 1.1; - } - h2 { - color: ${GlobalStyled.theme.textGray3}; - font-size: 0.4rem; - font-weight: 400; - } - } - } - - .text { - margin-left: 0.2rem; - word-break: break-all; - } +const ProfileImage = styled.img` + ${props => (props.width ? `width: ${props.width};` : 'width: 2.5rem;')} + ${props => + props.width ? `min-width: ${props.width};` : 'min-width: 2.5rem;'} + ${props => (props.width ? `height: ${props.width};` : 'height: 2.5rem;')} + border-radius: 10%; + border: 0px; `; const Styled = { ArticlePageDiv, - ProfileImage, + ArticleViewDiv, + ArticleReactionDiv, + CommentsCountDiv, + CommentViewDiv, CommentContent, - ArticleLikedDiv, - ArticleCommentDiv, - CreateCommentDiv, - CommentDiv, + CreateCommentViewDiv, + ProfileImage, }; export default Styled; diff --git a/src/Pages/ArticlePage/Components/Article.jsx b/src/Pages/ArticlePage/Components/Article.jsx new file mode 100644 index 0000000..fa278de --- /dev/null +++ b/src/Pages/ArticlePage/Components/Article.jsx @@ -0,0 +1,44 @@ +import { useNavigate } from 'react-router-dom'; + +import '@toast-ui/editor/dist/toastui-editor.css'; + +import { getCategoryById, getProfile } from 'Utils'; +import { useArticle, useLikeArticle, useDeleteArticle } from './hooks'; +import { getArticleTime } from 'Utils/dayjsUtils'; +import ArticleView from './ArticleView'; + +const Article = ({ articleId, currentUserId }) => { + const navi = useNavigate(); + const { data } = useArticle(articleId); + const likeArticle = useLikeArticle(articleId); + const deleteArticle = useDeleteArticle(articleId); + + const handleClickEdit = () => { + navi(`/article/${articleId}/edit`, { state: { article: data } }); + }; + + const handleClickCategory = () => { + navi(`/category/${data.categoryId}`); + }; + + const props = { + handleClickCategory, + category: getCategoryById(data.categoryId), + title: data.title, + writer: data.writer.nickname, + time: getArticleTime(data.createdAt), + viewCount: data.viewCount, + isModifiable: data.writer.id === currentUserId, + handleClickEdit, + handleClickDelete: deleteArticle.mutate, + profileSrc: getProfile.findProfileById(data.writer.character), + content: data.content, + isReactionPossible: data.categoryId !== 3, + likeCount: data.likeCount, + handleClickLike: likeArticle.mutate, + isLike: data.isLike, + }; + return ; +}; + +export default Article; diff --git a/src/Pages/ArticlePage/Components/ArticleView.jsx b/src/Pages/ArticlePage/Components/ArticleView.jsx new file mode 100644 index 0000000..7b0bfae --- /dev/null +++ b/src/Pages/ArticlePage/Components/ArticleView.jsx @@ -0,0 +1,63 @@ +import { Viewer } from '@toast-ui/react-editor'; + +import { FavoriteBorder } from '@mui/icons-material'; +import FavoriteIcon from '@mui/icons-material/Favorite'; + +import GlobalStyled from 'Styled/Global.styled'; +import Styled from '../ArticlePage.styled'; + +const ArticleView = ({ + handleClickCategory, + category, + title, + writer, + time, + viewCount, + isModifiable, + handleClickEdit, + handleClickDelete, + profileSrc, + content, + isReactionPossible, + likeCount, + handleClickLike, + isLike, +}) => { + return ( + + +
{category}
+
+
+
+

{title}

+
+

{writer}

+

{time}

+

조회수 {viewCount}

+ {isModifiable && ( +
+ + +
+ )} +
+
+ +
+ + {isReactionPossible && ( + + + {isLike ? : } + + + )} +
+ ); +}; + +export default ArticleView; diff --git a/src/Pages/ArticlePage/Components/Body.jsx b/src/Pages/ArticlePage/Components/Body.jsx deleted file mode 100644 index c0df907..0000000 --- a/src/Pages/ArticlePage/Components/Body.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import { useState, useEffect, useContext } from 'react'; -import { useNavigate } from 'react-router-dom'; - -import '@toast-ui/editor/dist/toastui-editor.css'; -import { Viewer } from '@toast-ui/react-editor'; - -import { AuthContext } from 'App'; -import { getCategoryById, getProfile } from 'Utils'; -import { ArticleService, ReactionService } from 'Network'; -import dayjs from 'dayjs'; - -import { CommentContainer } from '.'; -import { FavoriteBorder } from '@mui/icons-material'; -import FavoriteIcon from '@mui/icons-material/Favorite'; - -import GlobalStyled from 'Styled/Global.styled'; -import Styled from '../ArticlePage.styled'; - -const Body = ({ articleId, categoryId }) => { - const [article, setArticle] = useState(); - const [isLike, setIsLike] = useState(false); - const [likeCount, setLikeCount] = useState(0); - const navi = useNavigate(); - const handleClickEdit = () => { - navi(`/article/${articleId}/edit`, { state: { article } }); - }; - const handleClickDelete = async () => { - await ArticleService.deleteArticles(articleId); - navi(-1); - }; - const { curUser } = useContext(AuthContext); - - useEffect(() => { - const fetch = async () => { - const res = await ArticleService.getArticlesById(articleId); - setArticle(res); - setIsLike(res.isLike); - setLikeCount(res.likeCount); - }; - - fetch(); - }, []); - - const getArticleTime = time => - dayjs(time).isSame(dayjs(), 'day') - ? dayjs(time).format('HH:mm') - : dayjs(time).format('MM/DD'); - - const handleClickLike = async () => { - const data = await ReactionService.createArticleReactionHeart(articleId); - setIsLike(data.isLike); - setLikeCount(data.likeCount); - }; - - if (!article) return <>; - return ( - <> -
- { - navi(`/category/${article.categoryId}`); - }} - > -
- {getCategoryById(article.categoryId)} -
-
-
-
-

{article.title}

-
-

{article.writer.nickname}

-

{getArticleTime(article.createdAt)}

-

조회수 {article.viewCount}

- {article.writer.id === curUser.id && ( -
- - -
- )} -
-
- -
- {/*
{article.content}
*/} - -
- {categoryId !== 3 && ( - - - {isLike ? : } - - - )} -
-
- {article && article.categoryId !== 3 && ( - - )} - - ); -}; - -export default Body; diff --git a/src/Pages/ArticlePage/Components/Comment.jsx b/src/Pages/ArticlePage/Components/Comment.jsx index d7aad4c..d6a1e79 100644 --- a/src/Pages/ArticlePage/Components/Comment.jsx +++ b/src/Pages/ArticlePage/Components/Comment.jsx @@ -1,76 +1,27 @@ -import { useState } from 'react'; +import React from 'react'; import { getProfile } from 'Utils'; -import { ReactionService, CommentService } from 'Network'; -import dayjs from 'dayjs'; - -import { FavoriteBorder } from '@mui/icons-material'; -import FavoriteIcon from '@mui/icons-material/Favorite'; - -import Styled from '../ArticlePage.styled'; -const Comment = ({ - curUser, - articleId, - comment, - isLikeInitial, - likeCountInitial, - onDeleteComment, -}) => { - const [isLike, setIsLike] = useState(isLikeInitial); - const [likeCount, setLikeCount] = useState(likeCountInitial); - - const getArticleTime = time => - dayjs(time).isSame(dayjs(), 'day') - ? dayjs(time).format('HH:mm') - : dayjs(time).format('MM/DD'); - - const handleClickLike = async id => { - const res = await ReactionService.createCommentReactionHeart(articleId, id); - - setIsLike(res.isLike); - setLikeCount(res.likeCount); - }; - - const handleClickDelete = async commentId => { - await CommentService.deleteComments(commentId); - onDeleteComment(commentId); +import { getArticleTime } from 'Utils/dayjsUtils'; +import CommentView from './CommentView'; + +import { useDeleteComment, useLikeComment } from './hooks'; + +const Comment = ({ currentUserId, articleId, comment }) => { + const likeComment = useLikeComment(comment.id, articleId); + const deleteComment = useDeleteComment(comment.id, articleId); + + const props = { + isMine: currentUserId === comment.writer.id, + profileSrc: getProfile.findProfileById(comment.writer.character), + writer: comment.writer.nickname, + time: getArticleTime(comment.createdAt), + likeCount: comment.likeCount, + content: comment.content, + handleClickDelete: deleteComment.mutate, + handleClickLike: likeComment.mutate, + isLike: comment.isLike, }; - return ( - -
- -
-
-

{comment?.writer?.nickname}

-

{getArticleTime(comment?.updatedAt)}

-
-
- -
{comment.content}
- {curUser.id === comment.writer.id ? ( - - ) : ( - handleClickLike(comment?.id)} - > - {isLike ? : } - - )} -
-
- ); + return ; }; -export default Comment; +export default React.memo(Comment); diff --git a/src/Pages/ArticlePage/Components/CommentContainer.jsx b/src/Pages/ArticlePage/Components/CommentContainer.jsx deleted file mode 100644 index f37237f..0000000 --- a/src/Pages/ArticlePage/Components/CommentContainer.jsx +++ /dev/null @@ -1,111 +0,0 @@ -import { useContext, useEffect, useRef, useState } from 'react'; - -import { AuthContext } from 'App'; -import { ArticleService, CommentService } from 'Network'; - -import Comment from './Comment'; -import Fab from '@mui/material/Fab'; -import ArrowUpwardRoundedIcon from '@mui/icons-material/ArrowUpwardRounded'; -import { SmsOutlined } from '@mui/icons-material'; - -import Styled from '../ArticlePage.styled'; - -const CommentContainer = ({ articleId }) => { - const lastComment = useRef(); - const [comments, setComments] = useState([]); - const [totalCount, setTotalCount] = useState(); - const auth = useContext(AuthContext); - - const handleCreateComment = newComment => { - setComments(prev => prev.concat(newComment)); - lastComment.current.scrollIntoView(); - fetchComment(); - }; - - const handleDeleteComment = commentId => { - setComments(prev => prev.filter(comment => comment.id !== commentId)); - fetchComment(); - }; - const fetchComment = async () => { - const res = await ArticleService.getArticlesCommentsById( - articleId, - 'ASC', - 1, - 1000, // 한번에 1000개 긁어옴. 어떻게 할 지 결정 후 바꿔야 함. - ); - setComments(res?.data || []); - setTotalCount(res?.meta.totalCount); - }; - - useEffect(() => { - fetchComment(); - }, []); - - return ( -
- - - - {comments && - comments.map(comment => ( - - ))} - - - -
-
- ); -}; - -const CreateComment = ({ articleId, handleCreateComment }) => { - const [input, setInput] = useState(''); - const handleChange = e => { - if (e.target.value.length < 420) { - setInput(e.target.value); - } - }; - const handleClickSubmit = async e => { - e.preventDefault(); - if (input === '') { - return; - } - - const res = await CommentService.createComments({ - content: input, - articleId: +articleId, - }); - if (res) { - handleCreateComment(res); - setInput(''); - } - }; - return ( -
- - - - -
- ); -}; - -export default CommentContainer; diff --git a/src/Pages/ArticlePage/Components/CommentView.jsx b/src/Pages/ArticlePage/Components/CommentView.jsx new file mode 100644 index 0000000..69ffa15 --- /dev/null +++ b/src/Pages/ArticlePage/Components/CommentView.jsx @@ -0,0 +1,45 @@ +import React from 'react'; + +import { FavoriteBorder } from '@mui/icons-material'; +import FavoriteIcon from '@mui/icons-material/Favorite'; + +import Styled from '../ArticlePage.styled'; + +const CommentView = ({ + isMine, + profileSrc, + writer, + time, + likeCount, + content, + handleClickDelete, + handleClickLike, + isLike, +}) => { + return ( + +
+ +
+
+

{writer}

+

{time}

+
+
+ +
{content}
+ {isMine ? ( + + ) : ( + + {isLike ? : } + + )} +
+
+ ); +}; + +export default React.memo(CommentView); diff --git a/src/Pages/ArticlePage/Components/Comments.jsx b/src/Pages/ArticlePage/Components/Comments.jsx new file mode 100644 index 0000000..604d8f8 --- /dev/null +++ b/src/Pages/ArticlePage/Components/Comments.jsx @@ -0,0 +1,38 @@ +import React from 'react'; + +import { useArticle, useComments } from './hooks'; +import Comment from './Comment'; +import { SmsOutlined } from '@mui/icons-material'; + +import Styled from '../ArticlePage.styled'; + +const Comments = ({ articleId, currentUserId }) => { + const { + data: { categoryId }, + } = useArticle(articleId); + const { + data: { meta, data }, + } = useComments(articleId, 'ASC', 1, 1000); + + return ( +
+ + + + {categoryId !== 3 && + data.map(comment => ( + + ))} +
+ ); +}; + +export default React.memo(Comments); diff --git a/src/Pages/ArticlePage/Components/CreateComment.jsx b/src/Pages/ArticlePage/Components/CreateComment.jsx new file mode 100644 index 0000000..1390bb3 --- /dev/null +++ b/src/Pages/ArticlePage/Components/CreateComment.jsx @@ -0,0 +1,28 @@ +import { useRef } from 'react'; + +import CreateCommentView from './CreateCommentView'; +import useInput from './useInput'; +import { useArticle, useComments, useCreateComment } from './hooks'; + +const CreateComment = ({ articleId }) => { + const [input, handleChange, reset] = useInput(); + const lastComment = useRef(); + const createComment = useCreateComment(input, articleId, lastComment); + + const handleClickSubmit = e => { + e.preventDefault(); + createComment.mutate(); + reset(); + }; + + const props = { + handleClickSubmit, + input, + handleChange, + placeholder: '댓글을 입력하세요', + lastComment, + }; + return ; +}; + +export default CreateComment; diff --git a/src/Pages/ArticlePage/Components/CreateCommentView.jsx b/src/Pages/ArticlePage/Components/CreateCommentView.jsx new file mode 100644 index 0000000..1e7f2a5 --- /dev/null +++ b/src/Pages/ArticlePage/Components/CreateCommentView.jsx @@ -0,0 +1,33 @@ +import Fab from '@mui/material/Fab'; +import ArrowUpwardRoundedIcon from '@mui/icons-material/ArrowUpwardRounded'; + +import Styled from '../ArticlePage.styled'; + +const CreateCommentView = ({ + handleClickSubmit, + input, + handleChange, + placeholder, + lastComment, +}) => { + return ( + <> + +
+ + + + +
+
+
+ + ); +}; + +export default CreateCommentView; diff --git a/src/Pages/ArticlePage/Components/hooks.js b/src/Pages/ArticlePage/Components/hooks.js new file mode 100644 index 0000000..a7b96f9 --- /dev/null +++ b/src/Pages/ArticlePage/Components/hooks.js @@ -0,0 +1,128 @@ +import { ArticleService, ReactionService, CommentService } from 'Network'; +import { useQuery, useMutation, useQueryClient } from 'react-query'; +import { useNavigate } from 'react-router'; + +export function useArticle(articleId) { + return useQuery( + ['getArticleById', articleId], + async () => await ArticleService.getArticleById(articleId), + { suspense: true }, + ); +} + +export function useComments(articleId, order, page, take) { + return useQuery(['getCommentsByArticleId', articleId], async () => { + return await ArticleService.getArticlesCommentsById( + articleId, + order, + page, + take, + ); + }); +} + +export function useLikeArticle(articleId) { + const queryClient = useQueryClient(); + return useMutation( + async () => await ReactionService.createArticleReactionHeart(articleId), + { + onSuccess: result => { + queryClient.setQueryData(['getArticleById', articleId], data => { + return { + ...data, + isLike: result.isLike, + likeCount: result.likeCount, + }; + }); + }, + }, + ); +} + +export function useDeleteArticle(articleId) { + const navigate = useNavigate(); + return useMutation( + async () => await ArticleService.deleteArticles(articleId), + { + onSuccess: () => { + navigate(-1); + }, + }, + ); +} + +// 좀 보기 싫긴 함.. +export function useCreateComment(input, articleId, lastComment) { + const queryClient = useQueryClient(); + return useMutation( + async () => { + return await CommentService.createComments({ + content: input, + articleId: +articleId, + }); + }, + { + onSuccess: async () => { + await queryClient.invalidateQueries([ + 'getCommentsByArticleId', + articleId, + ]); + if (lastComment.current) lastComment.current.scrollIntoView(); + }, + }, + ); +} + +export function useLikeComment(commentId, articleId) { + const queryClient = useQueryClient(); + return useMutation( + async () => + await ReactionService.createCommentReactionHeart(articleId, commentId), + { + // onMutate: () => { + // return { id: commentId }; + // }, + // onSuccess: (result, _, { id }) => { + // queryClient.setQueryData( + // ['getCommentsByArticleId', articleId], + // data => { + // console.log(data.data.find(comment => comment.id === id)['isLike']); + // return { ...data }; + // }, + // ); + // }, + onSuccess: () => { + // 새로 fetch + queryClient.invalidateQueries(['getCommentsByArticleId', articleId]); + }, + }, + ); +} + +export function useDeleteComment(commentId, articleId) { + const queryClient = useQueryClient(); + return useMutation( + async () => await CommentService.deleteComments(commentId), + { + onMutate: () => { + return { id: commentId }; + }, + onSuccess: (_, __, { id }) => { + // 새로 fetch + queryClient.invalidateQueries(['getCommentsByArticleId', articleId]); + + // 얘는 캐시된 데이터만 프론트 자체적으로 업데이트 + // meta 정보가 새로 업데이트 돼야 해서 아마 새로 fetch해와야할듯..? + // queryClient.setQueryData( + // ['getCommentsByArticleId', articleId], + // data => { + // const newData = data.data.filter(comment => comment.id !== id); + // console.log(data.meta); + // data.meta.totalCount -= 1; + // return { data: newData, meta: data.meta }; + // }, + // ); + }, + }, + ); +} diff --git a/src/Pages/ArticlePage/Components/index.jsx b/src/Pages/ArticlePage/Components/index.jsx index 2acb63a..2d4ac59 100644 --- a/src/Pages/ArticlePage/Components/index.jsx +++ b/src/Pages/ArticlePage/Components/index.jsx @@ -1,4 +1,4 @@ -import Body from './Body'; -import CommentContainer from './CommentContainer'; +import Article from './Article'; +import Comments from './Comments'; -export { Body, CommentContainer }; +export { Article, Comments }; diff --git a/src/Pages/ArticlePage/Components/useInput.js b/src/Pages/ArticlePage/Components/useInput.js new file mode 100644 index 0000000..c1ae89f --- /dev/null +++ b/src/Pages/ArticlePage/Components/useInput.js @@ -0,0 +1,15 @@ +import { useCallback, useState } from 'react'; + +function useInput() { + const [input, setInput] = useState(''); + + const handleChange = useCallback(e => { + if (e.target.value.length < 420) setInput(e.target.value); + }, []); + + const reset = useCallback(() => setInput(''), []); + + return [input, handleChange, reset]; +} + +export default useInput; diff --git a/src/Pages/CategoryPage/Components/CategoryBody.jsx b/src/Pages/CategoryPage/Components/CategoryBody.jsx index 10a24cd..6c486ac 100644 --- a/src/Pages/CategoryPage/Components/CategoryBody.jsx +++ b/src/Pages/CategoryPage/Components/CategoryBody.jsx @@ -22,7 +22,6 @@ const CategoryBody = () => { // const [hasNextPage, setHasNextPage] = useState(true); let hasNextPage = true; const [target, setTarget] = useState(null); - const [curCate, setCurCate] = useState(''); const cateList = ['자유 게시판', '익명 게시판', '공지 게시판']; const loca = useLocation(); const navi = useNavigate(); @@ -36,37 +35,13 @@ const CategoryBody = () => { navi(`/article/${id}`); }; - // const setInitalArticles = async () => { - // setIsLoaded(true); - // const result = await ArticleService.getArticles(categoryId); - // console.log('result ,', result); - // const meta = result.meta; - // setArticles(result.data); - // setIsLoaded(false); - // setHasNextPage(meta.hasNextPage); - // hasNextPage = meta.hasNextPage; - // }; - const handleChangeCate = id => { navi(`/category/${parseInt(id) + 1}`); }; - - useEffect(() => { - if (categoryId > 3) { - alert('준비 중입니다!'); - navi('/'); - } - setCurCate(getCategoryByUrl(loca)); - }, [categoryId]); - // 동기적으로 sleep하는 함수 - // const sleep = delay => { - // let start = new Date().getTime(); - // while (new Date().getTime() < start + delay); - // }; - - const getMoreItem = async () => { + const getMoreArticles = async () => { if (!hasNextPage) return; + setIsLoaded(true); const result = await ArticleService.getArticles(categoryId, page); const newData = result.data; @@ -81,19 +56,23 @@ const CategoryBody = () => { const onIntersect = async ([entry], observer) => { if (entry.isIntersecting && !isLoaded) { + // if (page === 1 && hasNextPage) console.log("리렌더링 확인") observer.unobserve(entry.target); - await getMoreItem(); + await getMoreArticles(); observer.observe(entry.target); } }; + // 존재하지 않는 categortId 일 경우의 예외 처리, 하드 코딩. useEffect(() => { - if (categoryId > 3) { - alert('준비 중입니다!'); - navi('/'); + if (categoryId > 3 || categoryId == 2) { + navi('/error'); } - setCurCate(getCategoryByUrl(loca)); - }, []); + setArticles([]); + // 리렌더링 되면서 let으로 선언한 page, hasNextPage가 자동으로 초기화 되므로 state만 초기화 주면 된다. + // page = 1; + // hasNextPage = true; + }, [categoryId]); useEffect(() => { let observer; @@ -105,7 +84,7 @@ const CategoryBody = () => { observer.observe(target); // observer가 해당 객체를 감시하여 변경된다면 onIntersect 콜백 함수를 실행할 것이다. } return () => observer && observer.disconnect(); // 주석 씌워도 잘 돌아가네? - }, [target]); + }, [target, categoryId]); return ( <> @@ -119,6 +98,7 @@ const CategoryBody = () => { }} > {cateList.map((cate, idx) => { + if (idx === 1) return; return ; })} diff --git a/src/Pages/CreateArticlePage/Components/CreateArticleBody.jsx b/src/Pages/CreateArticlePage/Components/CreateArticleBody.jsx index 9691728..8b9c04d 100644 --- a/src/Pages/CreateArticlePage/Components/CreateArticleBody.jsx +++ b/src/Pages/CreateArticlePage/Components/CreateArticleBody.jsx @@ -18,7 +18,7 @@ const CreateArticleBody = () => { const [title, setTitle] = useState(''); const [content, setContent] = useState(''); const [curCate, setCurCate] = useState(0); - const cateList = ['자유 게시판', '익명 게시판']; + const cateList = ['자유 게시판']; const [isSending, setIsSending] = useState(false); const [anchorEl, setAnchorEl] = useState(null); diff --git a/src/Pages/MainPage/Components/Community.jsx b/src/Pages/MainPage/Components/Community.jsx index 6120a7c..560ace0 100644 --- a/src/Pages/MainPage/Components/Community.jsx +++ b/src/Pages/MainPage/Components/Community.jsx @@ -14,11 +14,7 @@ const Community = ({ }) => { return ( <> - + {bestArticles && bestArticles.map(article => { return ( @@ -61,8 +57,8 @@ const Community = ({ ); })} - {/* margin="0.4rem" */} - + {/* ); })} - + */} ); }; diff --git a/src/Pages/MainPage/Components/MainBody.jsx b/src/Pages/MainPage/Components/MainBody.jsx index 6b5fad2..296cdc2 100644 --- a/src/Pages/MainPage/Components/MainBody.jsx +++ b/src/Pages/MainPage/Components/MainBody.jsx @@ -41,17 +41,17 @@ const MainBody = () => { useEffect(() => { const getFreeArticles = async () => { - const response = await ArticleService.getArticles(1); + const response = await ArticleService.getAllArticles(1); setFreeArticles(response.data); }; const getAnonyArticles = async () => { - const response = await ArticleService.getArticles(2); + const response = await ArticleService.getAllArticles(2); setAnonyArticles(response.data); }; const getNotiArticles = async () => { - const response = await ArticleService.getArticles(3); + const response = await ArticleService.getAllArticles(3); setNotiArticles(response.data); }; @@ -61,7 +61,7 @@ const MainBody = () => { }; getBestArticles(); getFreeArticles(); - getAnonyArticles(); + // getAnonyArticles(); getNotiArticles(); }, []); diff --git a/src/Styled/Global.styled.js b/src/Styled/Global.styled.js index c551e69..bd309b4 100644 --- a/src/Styled/Global.styled.js +++ b/src/Styled/Global.styled.js @@ -55,7 +55,7 @@ const theme = { }; const assets = { - headerLogo: '/assets/42mainlogo2.svg', + headerLogo: '/assets/42mainlogo3.png', sidebar: { '80000co': '/assets/sidebar/80000co.png', humansof42: '/assets/sidebar/humansof42.png', diff --git a/src/Utils/dayjsUtils.js b/src/Utils/dayjsUtils.js new file mode 100644 index 0000000..454a52a --- /dev/null +++ b/src/Utils/dayjsUtils.js @@ -0,0 +1,13 @@ +import dayjs from 'dayjs'; + +export const getArticleTime = time => + dayjs(time).isSame(dayjs(), 'day') + ? dayjs(time).format('HH:mm') + : dayjs(time).format('MM/DD'); + +export const isNewArticle = time => + dayjs().isBefore(dayjs(time).add(12, 'hour')); + +const dayjsUtils = { getArticleTime, isNewArticle }; + +export default dayjsUtils;