diff --git a/fe/package-lock.json b/fe/package-lock.json index 5d6491b86..37c96821a 100644 --- a/fe/package-lock.json +++ b/fe/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "dependencies": { "@hookform/resolvers": "^3.3.2", + "@loadable/component": "^5.16.3", "@tanstack/react-query": "^4.36.1", "@tanstack/react-query-devtools": "^4.36.1", "@types/navermaps": "^3.7.4", @@ -34,6 +35,7 @@ "@tanstack/eslint-plugin-query": "^4.36.1", "@trivago/prettier-plugin-sort-imports": "^1.4.4", "@types/event-source-polyfill": "^1.0.5", + "@types/loadable__component": "^5.13.9", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@types/react-slick": "^0.23.11", @@ -45,6 +47,7 @@ "eslint-plugin-prettier": "^5.0.1", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", + "rollup-plugin-visualizer": "^5.12.0", "typescript": "^5.0.2", "vite": "^4.4.5", "vite-tsconfig-paths": "^4.2.1" @@ -321,6 +324,17 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", @@ -870,6 +884,26 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@loadable/component": { + "version": "5.16.3", + "resolved": "https://registry.npmjs.org/@loadable/component/-/component-5.16.3.tgz", + "integrity": "sha512-2mVvHs2988oVX2/zM0y6nYhJ4rTVHhkhRnpupBA0Rjl5tS8op9uSR4u5SLVfMLxzpspr2UiIBQD+wEuMsuq4Dg==", + "dependencies": { + "@babel/runtime": "^7.7.7", + "hoist-non-react-statics": "^3.3.1", + "react-is": "^16.12.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1515,6 +1549,15 @@ "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", "dev": true }, + "node_modules/@types/loadable__component": { + "version": "5.13.9", + "resolved": "https://registry.npmjs.org/@types/loadable__component/-/loadable__component-5.13.9.tgz", + "integrity": "sha512-QWOtIkwZqHNdQj3nixQ8oyihQiTMKZLk/DNuvNxMSbTfxf47w+kqcbnxlUeBgAxdOtW0Dh48dTAIp83iJKtnrQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/lodash": { "version": "4.14.200", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.200.tgz", @@ -2053,6 +2096,20 @@ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -2289,6 +2346,12 @@ "react": ">=16" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "node_modules/enquire.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz", @@ -2878,6 +2941,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -3070,6 +3142,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3881,11 +3962,25 @@ } } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/remove-accents": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", @@ -3939,6 +4034,73 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-visualizer": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz", + "integrity": "sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==", + "dev": true, + "dependencies": { + "open": "^8.4.0", + "picomatch": "^2.3.1", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "rollup": "2.x || 3.x || 4.x" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-applescript": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", @@ -4147,6 +4309,15 @@ "tslib": "^2.0.3" } }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -4160,6 +4331,20 @@ "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==" }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -4539,18 +4724,104 @@ "node": ">= 8" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/fe/package.json b/fe/package.json index a1ca0680a..5c36f02a8 100644 --- a/fe/package.json +++ b/fe/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@hookform/resolvers": "^3.3.2", + "@loadable/component": "^5.16.3", "@tanstack/react-query": "^4.36.1", "@tanstack/react-query-devtools": "^4.36.1", "@types/navermaps": "^3.7.4", @@ -36,6 +37,7 @@ "@tanstack/eslint-plugin-query": "^4.36.1", "@trivago/prettier-plugin-sort-imports": "^1.4.4", "@types/event-source-polyfill": "^1.0.5", + "@types/loadable__component": "^5.13.9", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@types/react-slick": "^0.23.11", @@ -47,6 +49,7 @@ "eslint-plugin-prettier": "^5.0.1", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", + "rollup-plugin-visualizer": "^5.12.0", "typescript": "^5.0.2", "vite": "^4.4.5", "vite-tsconfig-paths": "^4.2.1" diff --git a/fe/src/assets/bin.svg b/fe/src/assets/bin.svg new file mode 100644 index 000000000..eb6fe4dad --- /dev/null +++ b/fe/src/assets/bin.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/fe/src/assets/googleIcon.svg b/fe/src/assets/googleIcon.svg new file mode 100644 index 000000000..27fe9c7b7 --- /dev/null +++ b/fe/src/assets/googleIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fe/src/components/collection/CollectionContainer.tsx b/fe/src/components/collection/CollectionContainer.tsx index beebc6e1c..b3a3a86bf 100644 --- a/fe/src/components/collection/CollectionContainer.tsx +++ b/fe/src/components/collection/CollectionContainer.tsx @@ -8,7 +8,8 @@ import { ListLayout } from './ListLayout'; export const CollectionContainer = () => { // TODO 프로필에 쓰이는거는 위로 올려야함 const { sortBy } = useSort('collection'); - const { collections, hasNextPage, fetchNextPage } = useGetCollection(sortBy); + const SORT = sortBy === 'likeCount' ? 'likeCount,desc' : 'createdAt'; + const { collections, hasNextPage, fetchNextPage } = useGetCollection(SORT); console.log(collections); const grid = useToggle('grid'); diff --git a/fe/src/components/common/commentItem/ReplyItem.tsx b/fe/src/components/common/commentItem/ReplyItem.tsx index d1ea433fe..c9d059e0d 100644 --- a/fe/src/components/common/commentItem/ReplyItem.tsx +++ b/fe/src/components/common/commentItem/ReplyItem.tsx @@ -67,7 +67,6 @@ export const ReplyItem: React.FC = ({ const handleEdit = () => { setIsEdit(true); - console.log(inputRef?.current, ' now inputRef'); inputRef?.current?.focus(); }; @@ -111,8 +110,6 @@ export const ReplyItem: React.FC = ({ }; const handleSubmitLike = () => { - console.log(reply.id, ' now reply ID', commentId, 'commentId'); - if (isLogin) { reply.liked ? unLikeMutate({ commentId: commentId, replyId: reply.id }) diff --git a/fe/src/components/common/feedUserInfo/FeedUserInfo.tsx b/fe/src/components/common/feedUserInfo/FeedUserInfo.tsx index 094e759e2..70ba1326d 100644 --- a/fe/src/components/common/feedUserInfo/FeedUserInfo.tsx +++ b/fe/src/components/common/feedUserInfo/FeedUserInfo.tsx @@ -1,6 +1,7 @@ import { useNavigate } from 'react-router-dom'; import { useDeleteFeed } from 'service/queries/feed'; import { styled } from 'styled-components'; +import { useModal } from 'components/common/modal/useModal'; import { useAuthState } from 'hooks/auth/useAuth'; import { formatTimeStamp } from 'utils/formatTimeStamp'; import { generateDefaultUserImage } from 'utils/generateDefaultUserImage'; @@ -16,7 +17,7 @@ type Props = { feedId?: string; member: FeedMemberInfo; createdAt: string; - isUpdated: boolean; + isUpdated: string | null; store: Store; thumbnail?: string; }; @@ -34,6 +35,7 @@ export const FeedUserInfo: React.FC = ({ const { isLogin, userInfo } = useAuthState(); const formattedTimeStamp = formatTimeStamp(createdAt); const isAuthor = userInfo?.id === member.id; + const { openModal } = useModal<'collection'>(); const handleNavigateProfile = () => { isAuthor @@ -54,11 +56,6 @@ export const FeedUserInfo: React.FC = ({ }, { id: 2, - content: '컬렉션 추가하기', - onClick: () => {}, - }, - { - id: 3, content: '팔로우', onClick: () => { navigate(`${PATH.PROFILE}/${member.id}`); @@ -70,7 +67,9 @@ export const FeedUserInfo: React.FC = ({ { id: 1, content: '컬렉션 추가하기', - onClick: () => {}, + onClick: () => { + openModal('collection', { type: 'addFeed', feedId: feedId }); + }, }, { id: 2, diff --git a/fe/src/components/common/icon/NotiIcon.tsx b/fe/src/components/common/icon/NotiIcon.tsx index 6dba69c2d..c8f40e529 100644 --- a/fe/src/components/common/icon/NotiIcon.tsx +++ b/fe/src/components/common/icon/NotiIcon.tsx @@ -18,6 +18,15 @@ export const NotiIcon: React.FC = ({ onClick }) => { useEffect(() => { if (isLogin) { + const updateNotiCount = (newCount: number) => { + setNotiCount((currentCount) => { + if (currentCount !== newCount) { + return newCount; + } + return currentCount; + }); + }; + const eventSource = new EventSourcePolyfill(`${BASE_API_URL}/sse`, { headers: { 'Content-Type': 'text/event-stream', @@ -28,17 +37,14 @@ export const NotiIcon: React.FC = ({ onClick }) => { eventSource.addEventListener('notification', (event) => { const messageEvent = event as MessageEvent; const { count } = JSON.parse(messageEvent.data); - - if (notiCount !== count) { - setNotiCount(count); - } + updateNotiCount(count); }); eventSource.onerror = (err) => { if (eventSource.readyState === EventSource.CLOSED) { console.log(err, 'SSE closed'); - // 오류 날릴지 여부 } + return; // 그 전의 에러가 너무 시끄러워서 넣었는데 다른 방법 있으면 추천 plz }; return () => { diff --git a/fe/src/components/common/icon/icons.ts b/fe/src/components/common/icon/icons.ts index 874a1884f..da0333deb 100644 --- a/fe/src/components/common/icon/icons.ts +++ b/fe/src/components/common/icon/icons.ts @@ -49,3 +49,5 @@ export { default as ShareIcon } from 'assets/share.svg?react'; export { default as StoreIcon } from 'assets/store.svg?react'; export { default as CopySmallIcon } from 'assets/copyS.svg?react'; export { default as PhoneIcon } from 'assets/phone.svg?react'; +export { default as GoogleIcon } from 'assets/googleIcon.svg?react'; +export { default as TrashIcon } from 'assets/bin.svg?react'; diff --git a/fe/src/components/common/modal/CollectionAlert.tsx b/fe/src/components/common/modal/CollectionAlert.tsx new file mode 100644 index 000000000..a007e6f4b --- /dev/null +++ b/fe/src/components/common/modal/CollectionAlert.tsx @@ -0,0 +1,63 @@ +import { styled } from 'styled-components'; +import { Button } from '../button/Button'; +import { Dim } from '../dim/Dim'; +import { useModal } from './useModal'; + +export const CollectionAlert: React.FC = ({ + title, + onConfirm, + deleteText = '삭제', + closeText = '취소', +}) => { + const { closeModal } = useModal<'collectionAlert'>(); + return ( + <> + { + closeModal('collectionAlert'); + }} + /> + + {title} + + + + + {onConfirm && ( + + )} + + + + ); +}; + +const Wrapper = styled.div` + z-index: 100; + width: 504px; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + margin: 0 auto; + background-color: #fff; + padding: 24px; + border: 1px solid black; +`; +const ActionBox = styled.div` + display: flex; + gap: 16px; +`; +const Title = styled.p` + font: ${({ theme: { fonts } }) => fonts.displayB16}; + margin-bottom: 30px; +`; diff --git a/fe/src/components/common/modal/CollectionModal.tsx b/fe/src/components/common/modal/CollectionModal.tsx index c3246e1cb..fa5510668 100644 --- a/fe/src/components/common/modal/CollectionModal.tsx +++ b/fe/src/components/common/modal/CollectionModal.tsx @@ -1,10 +1,15 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; +import Skeleton from 'react-loading-skeleton'; +import 'react-loading-skeleton/dist/skeleton.css'; import { useToast } from 'recoil/toast/useToast'; import { + useAddFeedToCollection, useAddUserCollection, - useUserCollectionTitle, + useDeleteFeedFromCollection, + useGetCollectionsWithFeedStatus, + useGetUserCollectionTitle, } from 'service/queries/collection'; import styled from 'styled-components'; import { z } from 'zod'; @@ -18,39 +23,6 @@ import { Input } from '../input/Input'; import { InputField } from '../input/InputField'; import { useModal } from './useModal'; -const DATA = [ - { - id: '1', - title: '컬렉션 제목1', - checked: false, - }, - { - id: '2', - title: '컬렉션 제목2', - checked: true, - }, - { - id: '3', - title: '컬렉션 제목2', - checked: true, - }, - { - id: '4', - title: '컬렉션 제목2', - checked: true, - }, - { - id: '5', - title: '컬렉션 제목2', - checked: true, - }, - { - id: '6', - title: '컬렉션 제목2', - checked: true, - }, -]; - const formSchema = z.object({ title: z.string().min(3, { message: '제목을 3자 이상 입력해주세요', @@ -67,10 +39,11 @@ type MyCollection = { title: string; }; -// type MyCollectionWithChecked = MyCollection & { checked: boolean }; +type MyCollectionWithFeedStatus = MyCollection & { containsFeed: boolean }; export const CollectionModal: React.FC = ({ type = 'default', // default, addFeed + feedId = null, }) => { const { // openModal, @@ -79,27 +52,27 @@ export const CollectionModal: React.FC = ({ const toast = useToast(); const [selectedBadgeList, setSelectedBadgeList] = useState([]); const [isFormToggle, setIsFormToggle] = useState(false); - const [checkedIds, setCheckedIds] = useState( - DATA ? DATA.filter((item) => item.checked).map((item) => item.id) : [] - ); const [isPrivate, setIsPrivate] = useState(false); - const { data: myCollectionTitle } = useUserCollectionTitle(); - console.log(myCollectionTitle); + const { data: myCollectionTitle } = useGetUserCollectionTitle(type); + const { data: myCollectionTitleWithFeedStatus, isLoading } = + useGetCollectionsWithFeedStatus(feedId); + + const { mutate: addFeed } = useAddFeedToCollection(); + const { mutate: deleteFeed } = useDeleteFeedFromCollection(); const handleSelectBadgeList = (badges: Badge[]) => { setSelectedBadgeList(badges); }; - const handleCheckChange = (id: string, isChecked: boolean) => { - setCheckedIds((prevId) => { - if (isChecked) { - return [...prevId, id]; - } else { - return prevId.filter((checkedId) => checkedId !== id); - } - }); - console.log(checkedIds); + const handleCheckChange = (collectionId: string, isChecked: boolean) => { + if (!feedId) return; + + if (isChecked) { + addFeed({ collectionId, feedId }); + } else { + deleteFeed({ collectionId, feedId }); + } }; const handleOpenForm = () => { @@ -144,32 +117,46 @@ export const CollectionModal: React.FC = ({ closeModal('collection')} /> -

나만의 컬렉션

- - {DATA.length === 0 && 아직 회원님의 컬렉션이 없어요} - +

내 컬렉션 목록

- {type === 'default' && - myCollectionTitle?.map((collection: MyCollection) => ( - - {collection.title} - - ))} - - {type === 'addFeed' && - DATA.map((collection) => ( - - {!isFormToggle && ( - - )} - {collection.title} - - ))} + {type === 'default' && ( + <> + {myCollectionTitle && myCollectionTitle.length > 0 ? ( + myCollectionTitle.map((collection: MyCollection) => ( + + {collection.title} + + )) + ) : ( + 아직 회원님의 컬렉션이 없어요 + )} + + )} + + {type === 'addFeed' && isLoading && } + {type === 'addFeed' && !isLoading && ( + <> + {myCollectionTitleWithFeedStatus && + myCollectionTitleWithFeedStatus.length > 0 ? ( + myCollectionTitleWithFeedStatus.map( + (collection: MyCollectionWithFeedStatus) => ( + + {!isFormToggle && ( + + )} + {collection.title} + + ) + ) + ) : ( + 아직 회원님의 컬렉션이 없어요 + )} + + )}
diff --git a/fe/src/components/common/modal/Modal.tsx b/fe/src/components/common/modal/Modal.tsx index a29fcad69..4a112f54c 100644 --- a/fe/src/components/common/modal/Modal.tsx +++ b/fe/src/components/common/modal/Modal.tsx @@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil'; import { modalListState } from 'recoil/modal/atom'; import { modalSelector } from 'recoil/modal/selector'; import { AccountAlert } from './AccountAlert'; +import { CollectionAlert } from './CollectionAlert'; import { CollectionModal } from './CollectionModal'; import { CommentAlert } from './CommentAlert'; import { ProfileImageAlert } from './ProfileImageAlert'; @@ -14,6 +15,7 @@ const MODAL_COMPONENTS: { commentAlert: CommentAlert, accountAlert: AccountAlert, collection: CollectionModal, + collectionAlert: CollectionAlert, profileImageAlert: ProfileImageAlert, test: TestModal, test2: Test2Modal, diff --git a/fe/src/components/common/oauthButton/OAuthButton.tsx b/fe/src/components/common/oauthButton/OAuthButton.tsx new file mode 100644 index 000000000..c411322d4 --- /dev/null +++ b/fe/src/components/common/oauthButton/OAuthButton.tsx @@ -0,0 +1,50 @@ +import { styled } from 'styled-components'; +import { GoogleIcon } from '../icon/icons'; +import { PATH } from 'constants/path'; + +const { MODE, VITE_GOOGLE_CLIENT_ID, VITE_API_URL } = import.meta.env; + +export const OAuthButton = () => { + const isDev = MODE === 'development'; + const LOCAL_URL = 'http://localhost:5173'; + const GOOGLE_URL = `https://accounts.google.com/o/oauth2/v2/auth? + client_id=${VITE_GOOGLE_CLIENT_ID} + &redirect_uri=${isDev ? LOCAL_URL + PATH.GOOGLE : VITE_API_URL + PATH.GOOGLE} + &response_type=code + &scope=email profile`; + + const handleOauthLogin = () => { + // location.replace(GOOGLE_URL); // 이동 + window.open(GOOGLE_URL, '_blank', 'width=500,height=600,left=50,top=10'); // 새창 + }; + + return ( + + + Google로 계속하기 + + ); +}; + +const Wrapper = styled.button` + width: 100%; + display: flex; + align-items: center; + height: 48px; + padding: 16px; + border-radius: 4px; + + cursor: pointer; + border: 1px solid ${({ theme: { colors } }) => colors.textTertiary}; + background-color: ${({ theme: { colors } }) => colors.white}; + transition: all 0.2s ease-in-out; + &:hover { + background-color: ${({ theme: { colors } }) => colors.bgGray50}; + } +`; + +const Text = styled.p` + font: ${({ theme: { fonts } }) => fonts.displayB14}; + color: ${({ theme: { colors } }) => colors.textSecondary}; + flex: 1; +`; diff --git a/fe/src/components/feedMain/FeedMainItem.tsx b/fe/src/components/feedMain/FeedMainItem.tsx index 3093c0a88..fd160e50a 100644 --- a/fe/src/components/feedMain/FeedMainItem.tsx +++ b/fe/src/components/feedMain/FeedMainItem.tsx @@ -25,16 +25,14 @@ export const MainFeedItem = forwardRef( }); }; - const isUpdated = feed.createdAt !== feed.updatedAt; - return ( diff --git a/fe/src/components/notification/NotiList.tsx b/fe/src/components/notification/NotiList.tsx index 9132f9fab..bbb0e4e81 100644 --- a/fe/src/components/notification/NotiList.tsx +++ b/fe/src/components/notification/NotiList.tsx @@ -15,6 +15,8 @@ export const NotiList = () => { // error, } = useAllNotifications(); + console.log(notifications); + const { mutate: readNotiMutate } = useReadNotification(); const { observeTarget } = useIntersectionObserver({ @@ -29,18 +31,22 @@ export const NotiList = () => { return ( - {notifications?.map((notification: NotificationItem, index) => { - const isLastItem = index === notifications.length - 1; - - return ( - - ); - })} + {notifications.length > 0 ? ( + notifications?.map((notification: NotificationItem, index) => { + const isLastItem = index === notifications.length - 1; + + return ( + + ); + }) + ) : ( + 모든 알림을 확인하셨어요 + )} ); }; @@ -58,3 +64,9 @@ const Wrapper = styled.ul` padding-bottom: 59px; } */ `; + +const TextBox = styled.p` + font: ${({ theme: { fonts } }) => fonts.displayM14}; + color: ${({ theme: { colors } }) => colors.textSecondary}; + padding-top: 20px; +`; diff --git a/fe/src/components/sort/SelectSort.tsx b/fe/src/components/sort/SelectSort.tsx index 072a5b7e4..5946e8fcb 100644 --- a/fe/src/components/sort/SelectSort.tsx +++ b/fe/src/components/sort/SelectSort.tsx @@ -11,7 +11,7 @@ export const SelectSort = ({ sortId }: Props) => { const OPTIONS = [ { id: '1', name: '최신순', value: 'createdAt' }, - { id: '2', name: '좋아요순', value: 'heartCount' }, + { id: '2', name: '좋아요순', value: 'likeCount' }, ]; return ( diff --git a/fe/src/constants/path.ts b/fe/src/constants/path.ts index d2d7c9d85..1c630f5a5 100644 --- a/fe/src/constants/path.ts +++ b/fe/src/constants/path.ts @@ -18,4 +18,5 @@ export const PATH = { FOLLOWER: '/followers', FOLLOWING: '/followings', STORE: '/store', + GOOGLE: '/oauth/google', }; diff --git a/fe/src/pages/CollectionDetailPage.tsx b/fe/src/pages/CollectionDetailPage.tsx index bbf9ce729..da6030080 100644 --- a/fe/src/pages/CollectionDetailPage.tsx +++ b/fe/src/pages/CollectionDetailPage.tsx @@ -1,6 +1,13 @@ +import { useParams } from 'react-router-dom'; +import { + useDeleteFeedFromCollection, + useGetCollectionDetail, +} from 'service/queries/collection'; import { styled } from 'styled-components'; import { media } from 'styles/mediaQuery'; import { StoreMoodBadge } from 'components/common/badge/StoreMoodBadge'; +import { Dropdown } from 'components/common/dropdown/Dropdown'; +import { DropdownRow } from 'components/common/dropdown/DropdownRow'; import { DotGhostIcon, HeartSmallEmpty, @@ -8,192 +15,69 @@ import { StoreIcon, HeartBgIcon, ChatDotsIcon, + TrashIcon, } from 'components/common/icon/icons'; +import { useModal } from 'components/common/modal/useModal'; import { UserImage } from 'components/common/userImage/UserImage'; import { FollowListButton } from 'components/follow/followButton/FollowListButton'; +import { useAuthState } from 'hooks/auth/useAuth'; +import { formatTimeStamp } from 'utils/formatTimeStamp'; + +/** TODO + * 1. 기본 정보 수정 기능 + * 2. 글 삭제 + * 3. 좋아요 기능 + * 4. 공유하기 + * 5. 댓글 (?) + */ export const CollectionDetailPage = () => { - const collection = { - id: 'fd9ecac46496ef07ec38ccbb', - author: { - id: 'fd9ec99e6496ef07ec38cc96', - name: '아티', - mood: 'https://foodymoody-test.s3.ap-northeast-2.amazonaws.com/foodymoody_logo.png1', - profileImageUrl: - 'https://cdn.pixabay.com/photo/2020/05/17/20/21/cat-5183427_1280.jpg', - }, - thumnailImgUrl: - 'https://d2v80xjmx68n4w.cloudfront.net/gigs/fPoZ31584321311.jpg', - title: '테스트 컬렉션', - description: - '이보다 맛있는 맛집 컬렉션 이세상에 없습니다.이보다 맛있는 맛집 컬렉션 이세상에 없습니다.이보다 맛있는 맛집 컬렉션 이세상에 없습니다.이보다 맛있는 맛집 컬렉션 이세상에 없습니다.이보다 맛있는 맛집 컬렉션 이세상에 없습니다.이보다 맛있는 맛집 컬렉션 이세상에 없습니다.', - likeCount: 0, - followerCount: 0, - viewCount: 0, - feedCount: 5, - commentCount: 3, - moods: [ - { - id: 'fd9eca8d6496ef07ec38ccb8', - name: '행복', - }, - { - id: 'fd9eca9e6496ef07ec38ccb9', - name: '행복', - }, - { - id: 'fd9ecaaf6496ef07ec38ccba', - name: '행복', - }, - ], - createdAt: '2024-01-12T12:21:31.460596', - updatedAt: '2024-01-12T12:21:31.498458', - liked: false, + // TODO. private 글이면 접근 못하게 해야함 + const { id } = useParams() as { id: string }; + const { openModal, closeModal } = useModal<'collectionAlert'>(); - private: false, + const { userInfo } = useAuthState(); - feeds: [ - { - id: '20adfddfb74c89083d7d454c', - thumbnailUrl: - 'https://d2v80xjmx68n4w.cloudfront.net/gigs/fPoZ31584321311.jpg', - content: '맛있어요!', - storeMood: [ - { - id: '1', - name: '가족과 함께', - }, - { - id: '3', - name: '감성', - }, - { - id: '4', - name: '데이트', - }, - ], - likeCount: 0, - commentCount: 0, - createdAt: '2024-01-19T07:44:50.143663', - updatedAt: null, - liked: false, - }, - { - id: '20adfe00b74c89083d7d4552', - thumbnailUrl: - 'https://d2v80xjmx68n4w.cloudfront.net/gigs/fPoZ31584321311.jpg', - content: '맛있어요!', - storeMood: [ - { - id: '1', - name: '가족과 함께', - }, - { - id: '3', - name: '감성', - }, - { - id: '4', - name: '데이트', - }, - ], - likeCount: 0, - commentCount: 0, - createdAt: '2024-01-19T07:44:50.17695', - updatedAt: null, - liked: false, - }, - { - id: '20adfe22b74c89083d7d4558', - thumbnailUrl: - 'https://d2v80xjmx68n4w.cloudfront.net/gigs/fPoZ31584321311.jpg', - content: '맛있어요!', - storeMood: [ - { - id: '1', - name: '가족과 함께', - }, - { - id: '3', - name: '감성', - }, - { - id: '4', - name: '데이트', - }, - ], - likeCount: 0, - commentCount: 0, - createdAt: '2024-01-19T07:44:50.21004', - updatedAt: null, - liked: false, - }, - { - id: '20adfe3eb74c89083d7d455e', - thumbnailUrl: - 'https://d2v80xjmx68n4w.cloudfront.net/gigs/fPoZ31584321311.jpg', - content: '맛있어요!', - storeMood: [ - { - id: '1', - name: '가족과 함께', - }, - { - id: '3', - name: '감성', - }, - { - id: '4', - name: '데이트', - }, - ], - likeCount: 0, - commentCount: 0, - createdAt: '2024-01-19T07:44:50.238608', - updatedAt: null, - liked: false, - }, + const { data: collection, isLoading } = useGetCollectionDetail(id); + const { mutate: deleteFeed } = useDeleteFeedFromCollection(); + console.log(collection); + + if (isLoading) return

로딩중

; // 임시 로딩중 + + const isMe = userInfo?.id === collection.author.id; + + const handleDeleteFeed = (feedId: string) => { + deleteFeed( + { collectionId: id, feedId }, { - id: '20adfe5db74c89083d7d4564', - thumbnailUrl: - 'https://d2v80xjmx68n4w.cloudfront.net/gigs/fPoZ31584321311.jpg', - content: '맛있어요!', - storeMood: [ - { - id: '1', - name: '가족과 함께', - }, - { - id: '3', - name: '감성', - }, - { - id: '4', - name: '데이트', - }, - ], - likeCount: 0, - commentCount: 0, - createdAt: '2024-01-19T07:44:50.269128', - updatedAt: null, - liked: false, - }, - ], + onSuccess: () => { + closeModal('collectionAlert'); + }, + } + ); }; + console.log(userInfo, 'userInfo'); return ( - + {collection.author.name} - + {!isMe && ( + + )} @@ -202,18 +86,32 @@ export const CollectionDetailPage = () => {
<h1>{collection.title}</h1> - <span>날짜 및 업데이트 관련</span> + <span>{formatTimeStamp(collection.createdAt)}</span>

{collection.description}

- {/* TODO. 이거 나중에 수정 */} + {isMe && ( + }> + 수정하기 + + openModal('collectionAlert', { + title: '현재 컬렉션을 삭제하시겠습니까?', + onConfirm: () => {}, + }) + } + > + 삭제하기 + + + )}
- {collection.moods.map((mood) => ( - + {collection.moods.map((mood: { id: string; name: string }) => ( + ))} @@ -232,12 +130,14 @@ export const CollectionDetailPage = () => {

- 피드들 15 + 피드들 {collection.feeds.length}

    - {collection.feeds.map((feed) => ( - + {/* 수정되면 type 박아용 */} + {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + {collection.feeds.map((feed: any) => ( + @@ -245,14 +145,23 @@ export const CollectionDetailPage = () => {

    가게 이름이 올거에요~~

    - {/* TODO. 이거 나중에 수정 */} + {isMe && ( + + openModal('collectionAlert', { + title: '해당 피드를 컬렉션에서 삭제하시겠습니까?', + onConfirm: () => handleDeleteFeed(feed.id), + }) + } + /> + )}

    {feed.content}

    - {feed.storeMood.map((mood) => ( - + {feed.storeMood.map((mood: Badge) => ( + ))} @@ -272,7 +181,7 @@ export const CollectionDetailPage = () => {
-
위로 올라가기
+ {/*
위로 올라가기
*/}
); }; @@ -367,6 +276,7 @@ const Moods = styled.div` display: flex; gap: 8px; margin-left: auto; + flex-wrap: wrap; `; const ActionBar = styled.div` diff --git a/fe/src/pages/DetailFeedPage.tsx b/fe/src/pages/DetailFeedPage.tsx index f9754cc24..5762ae720 100644 --- a/fe/src/pages/DetailFeedPage.tsx +++ b/fe/src/pages/DetailFeedPage.tsx @@ -68,8 +68,6 @@ export const DetailFeedModalPage = () => { } }; - const isUpdated = feed?.createdAt !== feed?.updatedAt; - return ( <> { diff --git a/fe/src/pages/HomePage.tsx b/fe/src/pages/HomePage.tsx index 18b43b9c2..adc2e8285 100644 --- a/fe/src/pages/HomePage.tsx +++ b/fe/src/pages/HomePage.tsx @@ -1,12 +1,29 @@ +import loadable from '@loadable/component'; import { Suspense } from 'react'; import { useLocation } from 'react-router-dom'; import { styled } from 'styled-components'; +import { Spinner } from 'components/common/loading/spinner'; import { DeferredComponent } from 'components/common/skeleton/DeferredComponent'; import { FeedSkeleton } from 'components/common/skeleton/FeedSkeleton'; import { FeedMainList } from 'components/feedMain/FeedMainList'; -import { DetailFeedModalPage } from './DetailFeedPage'; +// import { DetailFeedModalPage } from './DetailFeedPage'; import { NewFeedModalPage } from './NewFeedPage'; +const SpinnerPage = () => { + return ( +
+ +
+ ); +}; + +const DetailFeedModalPage = loadable( + () => import('./DetailFeedPage').then((module) => module.DetailFeedModalPage), + { + fallback: , + } +); + export const HomePage = () => { const location = useLocation(); const background = location.state && location.state.background; diff --git a/fe/src/pages/LoginPage.tsx b/fe/src/pages/LoginPage.tsx index 48d4741c0..94d851fa4 100644 --- a/fe/src/pages/LoginPage.tsx +++ b/fe/src/pages/LoginPage.tsx @@ -1,6 +1,7 @@ import { styled } from 'styled-components'; import { TextButton } from 'components/common/button/TextButton'; import { LogoXLarge } from 'components/common/icon/icons'; +import { OAuthButton } from 'components/common/oauthButton/OAuthButton'; import { LoginForm } from 'components/login/LoginForm'; import { usePageNavigator } from 'hooks/usePageNavigator'; @@ -14,6 +15,14 @@ export const LoginPage = () => { + + + + SNS계정으로 시작하기 + + + + 계정이 없으신가요? @@ -59,3 +68,33 @@ const ButtonWrapper = styled.div` font: ${({ theme: { fonts } }) => fonts.displayM14}; } `; + +const OAuthBox = styled.div` + margin-top: -16px; + display: flex; + flex-direction: column; + align-items: center; + + gap: 16px; +`; + +const Divider = styled.div` + width: 100%; + display: flex; + align-items: center; + gap: 16px; +`; + +const Line = styled.div` + flex: 1; + height: 0.4px; + background-color: ${({ theme: { colors } }) => colors.textTertiary}; +`; + +const OAuthHelperText = styled.p` + flex: 1; + text-align: center; + white-space: nowrap; + color: ${({ theme: { colors } }) => colors.textSecondary}; + font: ${({ theme: { fonts } }) => fonts.displayM14}; +`; diff --git a/fe/src/pages/NotiPage.tsx b/fe/src/pages/NotiPage.tsx index ff17d40ee..22d02583a 100644 --- a/fe/src/pages/NotiPage.tsx +++ b/fe/src/pages/NotiPage.tsx @@ -1,3 +1,5 @@ +// import { DetailFeedModalPage } from './DetailFeedPage'; +import loadable from '@loadable/component'; import { Suspense } from 'react'; import { useLocation } from 'react-router-dom'; import { @@ -7,11 +9,25 @@ import { import { styled } from 'styled-components'; import { media } from 'styles/mediaQuery'; import { Button } from 'components/common/button/Button'; +import { Spinner } from 'components/common/loading/spinner'; import { DeferredComponent } from 'components/common/skeleton/DeferredComponent'; import { NotiSkeleton } from 'components/common/skeleton/NotiSkeleton'; import { NotiList } from 'components/notification/NotiList'; -import { DetailFeedModalPage } from './DetailFeedPage'; +const SpinnerPage = () => { + return ( +
+ +
+ ); +}; + +const DetailFeedModalPage = loadable( + () => import('./DetailFeedPage').then((module) => module.DetailFeedModalPage), + { + fallback: , + } +); export const NotiPage = () => { const location = useLocation(); const background = location.state && location.state.background; diff --git a/fe/src/pages/OAuthRedirectPage.tsx b/fe/src/pages/OAuthRedirectPage.tsx new file mode 100644 index 000000000..a92244a9e --- /dev/null +++ b/fe/src/pages/OAuthRedirectPage.tsx @@ -0,0 +1,24 @@ +import { useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +export const OAuthRedirectPage = () => { + const [params] = useSearchParams(); + const code = params.get('code'); + console.log('code', code); + + //mutate작성 + + useEffect(() => { + if (code) { + //mutate실행 + } + }, [code]); + + // const handleOauthLogin = () => {}; + + // if (isLoading) { + // return ; + // } + + return <>; +}; diff --git a/fe/src/pages/ProfilePage.tsx b/fe/src/pages/ProfilePage.tsx index 5ab4493ae..be32495f6 100644 --- a/fe/src/pages/ProfilePage.tsx +++ b/fe/src/pages/ProfilePage.tsx @@ -1,17 +1,34 @@ +import loadable from '@loadable/component'; import { Suspense, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { styled } from 'styled-components'; import { media } from 'styles/mediaQuery'; import { Collection } from 'components/collection/profile/Collection'; +import { Spinner } from 'components/common/loading/spinner'; import { DeferredComponent } from 'components/common/skeleton/DeferredComponent'; import { ProfileUserInfoSkeleton } from 'components/common/skeleton/ProfileUserInfoSkeleton'; import { UserFeedTabs } from 'components/common/userFeedTabs/UserFeedTabs'; import { FeedLikeList } from 'components/feedLike/FeedLikeList'; import { FeedProfileList } from 'components/feedProfile/FeedPofileList'; import { ProfileUserInfo } from 'components/profileUserInfo/ProfileUserInfo'; -import { DetailFeedModalPage } from './DetailFeedPage'; +// import { DetailFeedModalPage } from './DetailFeedPage'; import { FollowModalPage } from './FollowPage'; +const SpinnerPage = () => { + return ( +
+ +
+ ); +}; + +const DetailFeedModalPage = loadable( + () => import('./DetailFeedPage').then((module) => module.DetailFeedModalPage), + { + fallback: , + } +); + export const ProfilePage = () => { /* TODO. data.myFeed 데이터 생기면 추가하기 */ const location = useLocation(); diff --git a/fe/src/routes/router.tsx b/fe/src/routes/router.tsx index eebb20795..2b86ad222 100644 --- a/fe/src/routes/router.tsx +++ b/fe/src/routes/router.tsx @@ -1,8 +1,9 @@ +import loadable from '@loadable/component'; import { createBrowserRouter } from 'react-router-dom'; import { AccountSettingPage } from 'pages/AccountSettingPage'; import { CollectionDetailPage } from 'pages/CollectionDetailPage'; import { CollectionPage } from 'pages/CollectionPage'; -import { DetailFeedModalPage } from 'pages/DetailFeedPage'; +// import { DetailFeedModalPage } from 'pages/DetailFeedPage'; import { ErrorPage } from 'pages/ErrorPage'; import { FollowModalPage } from 'pages/FollowPage'; import { HomePage } from 'pages/HomePage'; @@ -11,6 +12,7 @@ import { LoginPage } from 'pages/LoginPage'; import { NewFeedModalPage } from 'pages/NewFeedPage'; import { NotiPage } from 'pages/NotiPage'; import { NotiSettingPage } from 'pages/NotiSettingPage'; +import { OAuthRedirectPage } from 'pages/OAuthRedirectPage'; import { PasswordPage } from 'pages/PasswordPage'; import { ProfileEditPage } from 'pages/ProfileEditPage'; import { ProfilePage } from 'pages/ProfilePage'; @@ -19,8 +21,25 @@ import { RegisterPage } from 'pages/RegisterPage'; import { SearchPage } from 'pages/SearchPage'; import { SettingPage } from 'pages/SettingPage'; import { StorePage } from 'pages/StorePage'; +import { Spinner } from 'components/common/loading/spinner'; import { PATH } from 'constants/path'; +const SpinnerPage = () => { + return ( +
+ +
+ ); +}; + +const DetailFeedModalPage = loadable( + () => + import('pages/DetailFeedPage').then((module) => module.DetailFeedModalPage), + { + fallback: , + } +); + const router = createBrowserRouter([ { path: PATH.HOME, @@ -133,6 +152,10 @@ const router = createBrowserRouter([ path: PATH.REGISTER, element: , }, + { + path: PATH.GOOGLE, + element: , + }, ]); export default router; diff --git a/fe/src/service/axios/collection/collection.ts b/fe/src/service/axios/collection/collection.ts index ddb60f440..015f23420 100644 --- a/fe/src/service/axios/collection/collection.ts +++ b/fe/src/service/axios/collection/collection.ts @@ -28,16 +28,52 @@ export const getProfileCollections = async ( }; export const getDetailCollection = async (id: string) => { - const { data } = await publicApi.get(END_POINT.collection(id)); + const { data } = await publicApi.get(`/feed_collections/${id}`); return data; }; +export const addUserCollection = async (collectionForm: CollectionForm) => { + const { data } = await privateApi.post('/feed_collections', collectionForm); + + console.log(data); + + return data; +}; + +// TODO. 타이틀 관련 요청 하나 더 추가되면 수정 예정 export const getUserCollectionTitle = async () => { const { data } = await privateApi.get('/members/me/collections/titles'); return data; }; -export const addUserCollection = async (collectionForm: CollectionForm) => { - const { data } = await privateApi.post('/feed_collections', collectionForm); +export const getCollectionsWithFeedStatus = async (feedId: string | null) => { + if (!feedId) { + throw new Error('feedId is required'); + } + + const { data } = await privateApi.get( + `/members/me/collections/with-feed-inclusion-status/${feedId}` + ); + return data; +}; + +export const addFeedToCollection = async ( + collectionId: string, + feedId: string +) => { + const { data } = await privateApi.post( + `/feed_collections/${collectionId}/feeds`, + { feedId: feedId } + ); + return data; +}; + +export const deleteFeedToCollection = async ( + collectionId: string, + feedId: string +) => { + const { data } = await privateApi.delete( + `/feed_collections/${collectionId}/feeds/${feedId}` + ); return data; }; diff --git a/fe/src/service/constants/queryKey.ts b/fe/src/service/constants/queryKey.ts index 3330c97f0..b75f618d8 100644 --- a/fe/src/service/constants/queryKey.ts +++ b/fe/src/service/constants/queryKey.ts @@ -15,6 +15,8 @@ export const QUERY_KEY = { followers: 'followers', collections: 'collections', myCollections: 'myCollections', + myCollectionsContainFeed: 'myCollectionsContainFeed', + collectionDetail: 'collectionDetail', notificationSetting: 'notificationSettings', refresh: 'refresh', }; diff --git a/fe/src/service/queries/auth.ts b/fe/src/service/queries/auth.ts index f20014b29..49f4226cd 100644 --- a/fe/src/service/queries/auth.ts +++ b/fe/src/service/queries/auth.ts @@ -26,13 +26,13 @@ export const useLogin = () => { const location = useLocation(); const from = location.state?.redirectedFrom?.pathname || PATH.HOME; // 사용자가 로그인 전에 접근하려고 했던 경로 - + // 오어스에서 충돌되는 부분 없나? 이거때매 안되면 그냥 오어스용 쿼리 따로 파기 const setAccessToken = useSetRecoilState(accessTokenState); const setRefreshToken = useSetRecoilState(refreshTokenState); const setUserInfo = useSetRecoilState(userInfoState); return useMutation({ - mutationFn: (body: LoginBody) => fetchLogin(body), + mutationFn: (body: LoginBody) => fetchLogin(body), // oauth 로그 바디도 추가 onSuccess: (data) => { const { accessToken, refreshToken } = data; const payload = jwtDecode(accessToken); @@ -101,6 +101,7 @@ export const useRefreshToken = () => { const refreshToken = useRecoilValue(refreshTokenState); const userInfo = useRecoilValue(userInfoState); const clearLoginInfo = useClearLoginInfo(); + console.log('refreshToken in auth query', refreshToken); const refreshTokenMutation = useMutation( () => { diff --git a/fe/src/service/queries/collection.ts b/fe/src/service/queries/collection.ts index 76349b8a0..ebb520a5d 100644 --- a/fe/src/service/queries/collection.ts +++ b/fe/src/service/queries/collection.ts @@ -1,12 +1,21 @@ -import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query'; +import { + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query'; import { AxiosError } from 'axios'; import { useMemo } from 'react'; import { useToast } from 'recoil/toast/useToast'; import { addUserCollection, getAllCollections, + getDetailCollection, getUserCollectionTitle, getProfileCollections, + getCollectionsWithFeedStatus, + addFeedToCollection, + deleteFeedToCollection, } from 'service/axios/collection/collection'; import { QUERY_KEY } from 'service/constants/queryKey'; @@ -35,11 +44,65 @@ export const useGetCollection = (sortBy?: string) => { }; }; -export const useUserCollectionTitle = () => - useQuery({ +export const useGetUserCollectionTitle = (type: string) => { + return useQuery({ queryKey: [QUERY_KEY.myCollections], queryFn: () => getUserCollectionTitle(), + enabled: type === 'default', + }); +}; + +export const useGetCollectionsWithFeedStatus = (feedId: string | null) => { + return useQuery({ + queryKey: [QUERY_KEY.myCollectionsContainFeed], + queryFn: () => getCollectionsWithFeedStatus(feedId), + enabled: !!feedId, }); +}; + +type AddAndDeleteFeedToCollectionArgs = { + collectionId: string; + feedId: string; +}; + +export const useAddFeedToCollection = () => { + const queryClient = useQueryClient(); + const toast = useToast(); + + return useMutation({ + mutationFn: ({ collectionId, feedId }: AddAndDeleteFeedToCollectionArgs) => + addFeedToCollection(collectionId, feedId), + onSuccess: () => { + queryClient.invalidateQueries([QUERY_KEY.myCollectionsContainFeed]); + queryClient.invalidateQueries([QUERY_KEY.collectionDetail]); + // queryClient.invalidateQueries([QUERY_KEY.myCollectionsContainFeed]); + toast.success('해당 피드를 컬렉션에 추가했어요'); + }, + onError: (error) => { + console.error('Error adding feed to collection:', error); + toast.error('오류가 발생해 피드를 추가할 수 없어요'); + }, + }); +}; + +export const useDeleteFeedFromCollection = () => { + const queryClient = useQueryClient(); + const toast = useToast(); + + return useMutation({ + mutationFn: ({ collectionId, feedId }: AddAndDeleteFeedToCollectionArgs) => + deleteFeedToCollection(collectionId, feedId), + onSuccess: () => { + queryClient.invalidateQueries([QUERY_KEY.myCollectionsContainFeed]); + queryClient.invalidateQueries([QUERY_KEY.collectionDetail]); + toast.success('해당 피드를 컬렉션에서 제거했어요'); + }, + onError: (error) => { + console.error('Error deleting feed from collection:', error); + toast.error('오류가 발생해 피드를 제거할 수 없어요'); + }, + }); +}; export const useAddUserCollection = () => { const toast = useToast(); @@ -82,4 +145,12 @@ export const useGetProfileCollection = (memberId: string, sortBy?: string) => { isLoading, fetchNextPage, }; -} +}; + +export const useGetCollectionDetail = (id: string) => { + return useQuery({ + queryKey: [QUERY_KEY.collectionDetail], + queryFn: () => getDetailCollection(id), + enabled: !!id, + }); +}; diff --git a/fe/src/service/queries/follow.ts b/fe/src/service/queries/follow.ts index 8382fe10f..6de6e0631 100644 --- a/fe/src/service/queries/follow.ts +++ b/fe/src/service/queries/follow.ts @@ -62,6 +62,7 @@ export const usePostFollow = (memberId?: string, queryKey?: string) => { : queryClient.invalidateQueries([QUERY_KEY.profile, memberId]); queryClient.invalidateQueries([QUERY_KEY.followings]); queryClient.invalidateQueries([QUERY_KEY.followers]); + queryClient.invalidateQueries([QUERY_KEY.notifications]); }, onError: (error: AxiosError) => { const errorData = error?.response?.data; diff --git a/fe/src/service/queries/notification.ts b/fe/src/service/queries/notification.ts index ca9e3899a..27e2b695c 100644 --- a/fe/src/service/queries/notification.ts +++ b/fe/src/service/queries/notification.ts @@ -56,9 +56,21 @@ export const useReadAllNotification = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: () => readAllNotifications(), + mutationFn: async () => { + const data = queryClient.getQueryData([QUERY_KEY.notifications]) as { + pages: { content: NotificationItem[] }[]; + }; + if (data.pages[0].content.length !== 0) { + return readAllNotifications(); + } + }, onSuccess: () => { - queryClient.invalidateQueries([QUERY_KEY.notifications]); + const data = queryClient.getQueryData([QUERY_KEY.notifications]) as { + pages: { content: NotificationItem[] }[]; + }; + if (data.pages[0].content.length !== 0) { + queryClient.invalidateQueries([QUERY_KEY.notifications]); + } }, onError: (error: AxiosError) => { const errorData = error?.response?.data; @@ -71,9 +83,21 @@ export const useDeleteReadNotification = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: () => deleteReadNotifications(), + mutationFn: async () => { + const data = queryClient.getQueryData([QUERY_KEY.notifications]) as { + pages: { content: NotificationItem[] }[]; + }; + if (data.pages[0].content.length !== 0) { + return deleteReadNotifications(); + } + }, onSuccess: () => { - queryClient.invalidateQueries([QUERY_KEY.notifications]); + const data = queryClient.getQueryData([QUERY_KEY.notifications]) as { + pages: { content: NotificationItem[] }[]; + }; + if (data.pages[0].content.length !== 0) { + queryClient.invalidateQueries([QUERY_KEY.notifications]); + } }, onError: (error: AxiosError) => { const errorData = error?.response?.data; diff --git a/fe/src/service/queries/reply.ts b/fe/src/service/queries/reply.ts index cc1d854cd..a97f261bb 100644 --- a/fe/src/service/queries/reply.ts +++ b/fe/src/service/queries/reply.ts @@ -17,6 +17,7 @@ import { QUERY_KEY } from 'service/constants/queryKey'; export const useGetReplies = (commentId: string, feedId?: string) => { console.log('replies commentId', commentId); + console.log('replies feedId', feedId); const { data, hasNextPage, isFetching, fetchNextPage, refetch } = useInfiniteQuery({ diff --git a/fe/src/types/modal.d.ts b/fe/src/types/modal.d.ts index ec0d15db4..b0ef0db6c 100644 --- a/fe/src/types/modal.d.ts +++ b/fe/src/types/modal.d.ts @@ -7,6 +7,7 @@ type ModalPropsMap = { accountAlert: AccountAlertProps; profileImageAlert: ProfileImageAlertProps; collection: CollectionModalProps; + collectionAlert: CollectionAlertProps; }; type Modal = { @@ -27,6 +28,7 @@ type Test2ModalProps = { type CollectionModalProps = { type: 'default' | 'addFeed'; + feedId?: string; }; type CommentAlertProps = { @@ -46,3 +48,10 @@ type ProfileImageAlertProps = { onDelete?(): void; onClose?(): void; }; + +type CollectionAlertProps = { + title: string; + onConfirm: () => void; + deleteText?: string; + closeText?: string; +}; diff --git a/fe/vite.config.ts b/fe/vite.config.ts index a0c8d01a8..2af1c7042 100644 --- a/fe/vite.config.ts +++ b/fe/vite.config.ts @@ -1,10 +1,20 @@ import react from '@vitejs/plugin-react-swc'; -import { defineConfig } from 'vite'; +import { visualizer } from 'rollup-plugin-visualizer'; +import { defineConfig, type PluginOption } from 'vite'; import svgr from 'vite-plugin-svgr'; import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ - plugins: [react(), svgr(), tsconfigPaths()], + plugins: [ + react(), + svgr(), + tsconfigPaths(), + visualizer({ + filename: './dist/report.html', + open: true, + brotliSize: true, + }) as PluginOption, + ], optimizeDeps: { include: ['emoji-picker-react'], },