From ec0a464f5b14a1c6a2d880332366958669e88221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=ED=86=A0?= <59536977+seongjin2427@users.noreply.github.com> Date: Thu, 7 Nov 2024 13:33:22 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[chore]=20Tanstack-query=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=20=EC=84=A4=EC=A0=95=20(#29)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [feat] initial setting up for Tanstack-query * [style] added fill props "gray" of ProfileEditButton Component * [feat] added useTimer hook --- package-lock.json | 18 ++++---- package.json | 2 +- src/app/layout.tsx | 3 +- .../ProfileEditButton/ProfileEditButton.tsx | 2 +- src/hooks/useTimer.ts | 41 +++++++++++++++++++ src/provider/QueryClientProvider.tsx | 22 ++++++++++ 6 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 src/hooks/useTimer.ts create mode 100644 src/provider/QueryClientProvider.tsx diff --git a/package-lock.json b/package-lock.json index 437722d..6e1250c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "prostargram-frontend-next", "version": "0.1.0", "dependencies": { - "@tanstack/react-query": "^5.51.23", + "@tanstack/react-query": "^5.59.15", "clsx": "^2.1.1", "next": "14.2.3", "react": "^18", @@ -2998,27 +2998,27 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.51.21", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.21.tgz", - "integrity": "sha512-POQxm42IUp6n89kKWF4IZi18v3fxQWFRolvBA6phNVmA8psdfB1MvDnGacCJdS+EOX12w/CyHM62z//rHmYmvw==", + "version": "5.59.13", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.13.tgz", + "integrity": "sha512-Oou0bBu/P8+oYjXsJQ11j+gcpLAMpqW42UlokQYEz4dE7+hOtVO9rVuolJKgEccqzvyFzqX4/zZWY+R/v1wVsQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/react-query": { - "version": "5.51.23", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.23.tgz", - "integrity": "sha512-CfJCfX45nnVIZjQBRYYtvVMIsGgWLKLYC4xcUiYEey671n1alvTZoCBaU9B85O8mF/tx9LPyrI04A6Bs2THv4A==", + "version": "5.59.15", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.15.tgz", + "integrity": "sha512-QbVlAkTI78wB4Mqgf2RDmgC0AOiJqer2c5k9STOOSXGv1S6ZkY37r/6UpE8DbQ2Du0ohsdoXgFNEyv+4eDoPEw==", "dependencies": { - "@tanstack/query-core": "5.51.21" + "@tanstack/query-core": "5.59.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "react": "^18.0.0" + "react": "^18 || ^19" } }, "node_modules/@testing-library/dom": { diff --git a/package.json b/package.json index 94c154e..095e765 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ ] }, "dependencies": { - "@tanstack/react-query": "^5.51.23", + "@tanstack/react-query": "^5.59.15", "clsx": "^2.1.1", "next": "14.2.3", "react": "^18", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 69589de..3359e98 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next'; import localFont from 'next/font/local'; +import QueryClientProvider from '@/provider/QueryClientProvider'; import '@/styles/global.scss'; const pretendard = localFont({ @@ -24,8 +25,8 @@ const RootLayout = ({ return ( + {children}
- {children} ); diff --git a/src/app/my/components/Profile/ProfileEditButton/ProfileEditButton.tsx b/src/app/my/components/Profile/ProfileEditButton/ProfileEditButton.tsx index d4f86df..78e878e 100644 --- a/src/app/my/components/Profile/ProfileEditButton/ProfileEditButton.tsx +++ b/src/app/my/components/Profile/ProfileEditButton/ProfileEditButton.tsx @@ -20,7 +20,7 @@ const ProfileEditButton = ({ - diff --git a/src/hooks/useTimer.ts b/src/hooks/useTimer.ts new file mode 100644 index 0000000..2b49098 --- /dev/null +++ b/src/hooks/useTimer.ts @@ -0,0 +1,41 @@ +import { useEffect, useRef, useState } from 'react'; + +interface UseTimerParams { + waitTime: number; +} + +const useTimer = ({ waitTime }: UseTimerParams) => { + const [time, setTime] = useState(waitTime); + const timerId = useRef(null); + + const clearTimer = () => { + if (timerId.current) { + clearInterval(timerId.current); + timerId.current = null; + } + }; + + const startTimer = () => { + clearTimer(); + + if (time > 0) { + timerId.current = setInterval(() => { + setTime((prev) => { + if (prev <= 1000) { + clearTimer(); + return 0; + } + return prev - 1000; + }); + }, 1000); + } + }; + + useEffect(() => { + return () => clearTimer(); + }, []); + + return { time, startTimer, clearTimer }; +}; + +export default useTimer; diff --git a/src/provider/QueryClientProvider.tsx b/src/provider/QueryClientProvider.tsx new file mode 100644 index 0000000..4562536 --- /dev/null +++ b/src/provider/QueryClientProvider.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { useState, PropsWithChildren } from 'react'; +import { + QueryClient, + QueryClientProvider as Provider, +} from '@tanstack/react-query'; + +const QueryClientProvider = ({ children }: PropsWithChildren) => { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { refetchOnWindowFocus: false }, + }, + }), + ); + + return {children}; +}; + +export default QueryClientProvider; From e2cd566ce79dc8e9b869158c5d27a1eaeac5cdca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=ED=86=A0?= <59536977+seongjin2427@users.noreply.github.com> Date: Thu, 7 Nov 2024 17:49:57 +0900 Subject: [PATCH 2/3] [feat] added axios package and common httpRequest logic (#32) --- next.config.mjs | 8 +++ package-lock.json | 54 ++++++++++++++++--- package.json | 2 + src/api/httpRequest.ts | 54 +++++++++++++++++++ .../common/ErrorBoundary/ErrorBoundary.tsx | 52 ++++++++++++++++++ src/components/common/ErrorBoundary/index.ts | 1 + 6 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 src/api/httpRequest.ts create mode 100644 src/components/common/ErrorBoundary/ErrorBoundary.tsx create mode 100644 src/components/common/ErrorBoundary/index.ts diff --git a/next.config.mjs b/next.config.mjs index c2dd1da..c61fb1d 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,5 +1,13 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + async rewrites() { + return [ + { + source: '/api/:path*', + destination: `${process.env.NEXT_PUBLIC_BASE_URL}/:path*`, + }, + ]; + }, images: { remotePatterns: [ { diff --git a/package-lock.json b/package-lock.json index 6e1250c..84d7f0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "prostargram-frontend-next", "version": "0.1.0", "dependencies": { + "axios": "^1.7.7", "@tanstack/react-query": "^5.59.15", "clsx": "^2.1.1", "next": "14.2.3", @@ -20,6 +21,7 @@ "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^15.0.5", "@testing-library/user-event": "^14.5.2", + "@types/axios": "^0.14.4", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", @@ -3150,6 +3152,16 @@ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true }, + "node_modules/@types/axios": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.4.tgz", + "integrity": "sha512-9JgOaunvQdsQ/qW2OPmE5+hCeUB52lQSolecrFrthct55QekhmXEwT203s20RL+UHtCQc15y3VXpby9E7Kkh/g==", + "deprecated": "This is a stub types definition. axios provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "axios": "*" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4214,8 +4226,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/available-typed-arrays": { "version": "1.0.7", @@ -4241,6 +4252,16 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -4667,7 +4688,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -5041,7 +5061,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -6086,6 +6105,25 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -6115,7 +6153,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -7747,7 +7784,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -7756,7 +7792,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -11121,6 +11156,11 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", diff --git a/package.json b/package.json index 095e765..bbd8cdf 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ ] }, "dependencies": { + "axios": "^1.7.7", "@tanstack/react-query": "^5.59.15", "clsx": "^2.1.1", "next": "14.2.3", @@ -31,6 +32,7 @@ "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^15.0.5", "@testing-library/user-event": "^14.5.2", + "@types/axios": "^0.14.4", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/src/api/httpRequest.ts b/src/api/httpRequest.ts new file mode 100644 index 0000000..1a559ec --- /dev/null +++ b/src/api/httpRequest.ts @@ -0,0 +1,54 @@ +import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios'; + +type HttpResponseType = { isSuccess: boolean; code: number; message: string }; +type HttpSuccessType = HttpResponseType & { result?: T }; + +const BASE_URL = '/api'; + +const defaultAxios = axios.create({ baseURL: BASE_URL }); +const authAxios = axios.create({ baseURL: BASE_URL }); + +authAxios.interceptors.request.use((config) => { + return config; +}); + +authAxios.interceptors.response.use((config) => { + return config; +}); + +class ResponseError extends Error { + isSuccess: boolean; + + code: number; + + constructor(response: HttpResponseType) { + super(response?.message); + + this.isSuccess = response?.isSuccess; + this.code = response?.code; + } +} + +const makeInstance = + (instance: AxiosInstance) => + async (options: AxiosRequestConfig) => { + try { + const result = await instance>(options); + + return result.data; + } catch (e) { + const err = e as AxiosError; + + console.error('error', err); + if (err.response) { + throw new ResponseError(err.response.data); + } + + throw err; + } + }; + +const defaultInstance = makeInstance(defaultAxios); +const authInstance = makeInstance(authAxios); + +export { defaultInstance, authInstance }; diff --git a/src/components/common/ErrorBoundary/ErrorBoundary.tsx b/src/components/common/ErrorBoundary/ErrorBoundary.tsx new file mode 100644 index 0000000..d1ac946 --- /dev/null +++ b/src/components/common/ErrorBoundary/ErrorBoundary.tsx @@ -0,0 +1,52 @@ +import React from 'react'; + +interface ErrorBoundaryProps { + fallback: React.ReactNode; + children: React.ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; +} + +class ErrorBoundary extends React.Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError() { + console.log('a'); + return { hasError: true }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { + console.log('a'); + console.error('Error caught by componentDidCatch', error, errorInfo); + } + + resetError = () => { + this.setState({ hasError: false }); + }; + + render() { + const { hasError } = this.state; + const { fallback, children } = this.props; + + if (hasError) { + return ( +
+ {fallback} + +
+ ); + } + + return children; + } +} + +export default ErrorBoundary; diff --git a/src/components/common/ErrorBoundary/index.ts b/src/components/common/ErrorBoundary/index.ts new file mode 100644 index 0000000..257f6a2 --- /dev/null +++ b/src/components/common/ErrorBoundary/index.ts @@ -0,0 +1 @@ +export { default } from './ErrorBoundary'; From 533aa16caae2a2f6ba4745fa3b83aa0c17837a1e Mon Sep 17 00:00:00 2001 From: lee1nna Date: Tue, 19 Nov 2024 14:52:07 +0900 Subject: [PATCH 3/3] [feat] add following list component in main page - add following list component - link route user profile page --- .../FollowingList/FollowingList.module.scss | 18 ++++++ .../FollowingList/FollowingList.tsx | 61 +++++++++++++++++++ src/app/(MainLayout)/page.tsx | 59 ++++++++++++++++++ src/app/my/types/my.ts | 2 + 4 files changed, 140 insertions(+) create mode 100644 src/app/(MainLayout)/components/FollowingList/FollowingList.module.scss create mode 100644 src/app/(MainLayout)/components/FollowingList/FollowingList.tsx diff --git a/src/app/(MainLayout)/components/FollowingList/FollowingList.module.scss b/src/app/(MainLayout)/components/FollowingList/FollowingList.module.scss new file mode 100644 index 0000000..99986d5 --- /dev/null +++ b/src/app/(MainLayout)/components/FollowingList/FollowingList.module.scss @@ -0,0 +1,18 @@ +@use '../../../../styles/helpers/index' as *; + +.following_ul { + @include flex-start-center; + width: 100%; + gap: 30px; + overflow-x: auto; + + .following_li { + @include flex-column; + justify-content: center; + cursor: pointer; + + .user_img { + border-radius: 50%; + } + } +} diff --git a/src/app/(MainLayout)/components/FollowingList/FollowingList.tsx b/src/app/(MainLayout)/components/FollowingList/FollowingList.tsx new file mode 100644 index 0000000..5d39916 --- /dev/null +++ b/src/app/(MainLayout)/components/FollowingList/FollowingList.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import Image from 'next/image'; +import Typo from '@/components/common/Typo'; +import { UserType } from '@/app/my/types/my'; +import styles from './FollowingList.module.scss'; + +type FollowingListProps = { + followingUsers: Partial[]; +}; + +const FollowingList = ({ followingUsers }: FollowingListProps) => { + const router = useRouter(); + + const moveUserProfilePage = (userId: number) => { + router.push(`/profile/${userId}`); + }; + + return ( +
    + {followingUsers.length > 0 && + followingUsers.map((user) => { + return ( +
  • moveUserProfilePage(user.userId!)} + aria-hidden="true" + > + {user.profileUrl === '' && ( + + )} + + + {user.userName} + +
  • + ); + })} +
+ ); +}; + +export default FollowingList; diff --git a/src/app/(MainLayout)/page.tsx b/src/app/(MainLayout)/page.tsx index d67702d..0faca0f 100644 --- a/src/app/(MainLayout)/page.tsx +++ b/src/app/(MainLayout)/page.tsx @@ -3,6 +3,7 @@ import ReadOnlyDebateFeed from './components/ReadOnlyDebateFeed'; import styles from './page.module.scss'; import Feed from './components/Feed/Feed'; +import FollowingList from './components/FollowingList/FollowingList'; const MOCK_BASIC_FEED_DATA = { post: { @@ -118,9 +119,67 @@ const MOCK_DATA_OF_DEBATE_FEED = { }, }; +const MOCK_DATA_OF_FOLLOWING_DATA = [ + { + userId: 1, + userName: '정민욱', + profileImgUrl: 'https://profileImg1.url', + departmentName: '네이버', + }, + { + userId: 2, + userName: '제이슨', + profileImgUrl: 'https://profileImg2.url', + departmentName: '카카오', + }, + { + userId: 3, + userName: '진성진', + profileImgUrl: 'https://profileImg3.url', + departmentName: '우아한형제들', + }, + { + userId: 1, + userName: '정민욱', + profileImgUrl: 'https://profileImg1.url', + departmentName: '네이버', + }, + { + userId: 2, + userName: '제이슨', + profileImgUrl: 'https://profileImg2.url', + departmentName: '카카오', + }, + { + userId: 3, + userName: '진성진', + profileImgUrl: 'https://profileImg3.url', + departmentName: '우아한형제들', + }, + { + userId: 1, + userName: '정민욱', + profileImgUrl: 'https://profileImg1.url', + departmentName: '네이버', + }, + { + userId: 2, + userName: '제이슨', + profileImgUrl: 'https://profileImg2.url', + departmentName: '카카오', + }, + { + userId: 3, + userName: '진성진', + profileImgUrl: 'https://profileImg3.url', + departmentName: '우아한형제들', + }, +]; + const MainPage = () => { return (
+ diff --git a/src/app/my/types/my.ts b/src/app/my/types/my.ts index a92a151..10c64cf 100644 --- a/src/app/my/types/my.ts +++ b/src/app/my/types/my.ts @@ -1,6 +1,8 @@ interface UserType { + userId: number; profileUrl: string; nickname: string; + userName: string; currentState: string; description: string; followers: number;