From 2c6445ae967da93496d03bf0eaca7a160bd5843e Mon Sep 17 00:00:00 2001 From: NEWJIN Date: Mon, 15 Jul 2024 10:21:22 +0900 Subject: [PATCH 01/11] =?UTF-8?q?=20FE-61=20=F0=9F=94=A8=20eslint=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index cb854af6..a373fb94 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,6 +30,8 @@ module.exports = { 'react/jsx-props-no-spreading': 'off', 'no-use-before-define': 'off', '@typescript-eslint/no-use-before-define': ['off'], + "react/require-default-props": 'off', + "react/self-closing-comp": 'off', }, settings: { react: { From 2ddc39819c975c0ede520a509f0b370d6e21e4f0 Mon Sep 17 00:00:00 2001 From: NEWJIN Date: Mon, 15 Jul 2024 10:22:24 +0900 Subject: [PATCH 02/11] =?UTF-8?q?FE-61=20=F0=9F=94=A5=20InteractiveEmotion?= =?UTF-8?q?IconCard=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 단순 래핑 기능밖에 없는 컴포넌트 삭제 / emotionselector에서 emotioniconcard를 직접 사용하도록 수정 --- src/components/Emotion/card/EmotionSelector.tsx | 4 ++-- .../Emotion/card/InteractiveEmotionIconCard.tsx | 12 ------------ 2 files changed, 2 insertions(+), 14 deletions(-) delete mode 100644 src/components/Emotion/card/InteractiveEmotionIconCard.tsx diff --git a/src/components/Emotion/card/EmotionSelector.tsx b/src/components/Emotion/card/EmotionSelector.tsx index 29b3f104..ace202d4 100644 --- a/src/components/Emotion/card/EmotionSelector.tsx +++ b/src/components/Emotion/card/EmotionSelector.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import InteractiveEmotionIconCard from '@/components/Emotion/card/InteractiveEmotionIconCard'; +import EmotionIconCard from '@/components/Emotion/card/EmotionIconCard'; import useMediaQuery from '@/hooks/useMediaQuery'; import { EmotionType, EmotionState } from '@/types/EmotionTypes'; @@ -52,7 +52,7 @@ function EmotionSelector() { return (
{(['감동', '기쁨', '고민', '슬픔', '분노'] as const).map((iconType) => ( - handleCardClick(iconType)} /> + handleCardClick(iconType)} /> ))}
); diff --git a/src/components/Emotion/card/InteractiveEmotionIconCard.tsx b/src/components/Emotion/card/InteractiveEmotionIconCard.tsx deleted file mode 100644 index f8448315..00000000 --- a/src/components/Emotion/card/InteractiveEmotionIconCard.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import EmotionIconCard from '@/components/Emotion/card/EmotionIconCard'; -import { InteractiveEmotionIconCardProps } from '@/types/EmotionTypes'; - -// InteractiveEmotionIconCard 컴포넌트 함수 선언 -function InteractiveEmotionIconCard(props: InteractiveEmotionIconCardProps) { - return ; -} - -InteractiveEmotionIconCard.displayName = 'InteractiveEmotionIconCard'; - -export default InteractiveEmotionIconCard; From 3a0e0a028b8c67229c34b71cc42dee2ce2a0eac3 Mon Sep 17 00:00:00 2001 From: NEWJIN Date: Mon, 15 Jul 2024 10:23:25 +0900 Subject: [PATCH 03/11] =?UTF-8?q?FE-61=20=F0=9F=94=A8=20EpigramCard=20?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=EC=A6=88=20=EC=9E=AC=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit base -> xs로 변경 --- src/components/Card/EpigramCard.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/Card/EpigramCard.tsx b/src/components/Card/EpigramCard.tsx index 6fb71f86..c0cd5bc0 100644 --- a/src/components/Card/EpigramCard.tsx +++ b/src/components/Card/EpigramCard.tsx @@ -1,9 +1,9 @@ import React from 'react'; // figma 상으로는 sm ~ 3xl 사이즈로 구현되어 있는데, tailwind 환경을 반영해 -// base ~ 2xl 으로 정의했습니다. +// xs ~ 2xl 으로 정의했습니다. const sizeStyles = { - base: 'w-[286px] max-h-[132px]', + xs: 'w-[286px] max-h-[132px]', sm: 'sm:w-[312px] sm:max-h-[152px]', md: 'md:w-[384px] md:max-h-[180px]', lg: 'lg:w-[540px] lg:max-h-[160px]', @@ -12,7 +12,7 @@ const sizeStyles = { }; const textSizeStyles = { - base: 'text-xs', + xs: 'text-xs', sm: 'sm:text-sm', md: 'md:text-base', lg: 'lg:text-xl', @@ -22,19 +22,19 @@ const textSizeStyles = { function EpigramCard() { return ( -
+
{/* eslint-disable-next-line */}
{/* 줄무늬를 만들려면 비어있는 div가 필요합니다. */}
오랫동안 꿈을 그리는 사람은 마침내 그 꿈을 닮아 간다.
- 앙드레 말로 -
@@ -43,12 +43,12 @@ function EpigramCard() {
#나아가야할때
#꿈을이루고싶을때
From 6dd4a488571ddde165024d3ebecd073f17df6ab4 Mon Sep 17 00:00:00 2001 From: NEWJIN Date: Mon, 15 Jul 2024 10:24:57 +0900 Subject: [PATCH 04/11] =?UTF-8?q?FE-61=20=F0=9F=93=9D=20=EA=B3=B5=EC=9A=A9?= =?UTF-8?q?=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Card/CommentCard.tsx | 11 +++++++++-- src/components/Card/EpigramCard.tsx | 5 ++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/Card/CommentCard.tsx b/src/components/Card/CommentCard.tsx index 3975f828..db867651 100644 --- a/src/components/Card/CommentCard.tsx +++ b/src/components/Card/CommentCard.tsx @@ -17,8 +17,14 @@ function CommentCard({ status }: CommentCardProps) {
-
지킬과 하이드
-
1시간 전
+
+ {/* 테스트 텍스트입니다. */} + 지킬과 하이드 +
+
+ {/* 테스트 텍스트입니다. */} + 1시간 전 +
{status === 'edit' && (
@@ -30,6 +36,7 @@ function CommentCard({ status }: CommentCardProps) {
+ {/* 테스트 텍스트입니다. */} 오늘 하루 우울했었는데 덕분에 많은 힘 얻고 갑니다. 연금술사 책 다시 사서 오랜만에 읽어봐야겠어요!
diff --git a/src/components/Card/EpigramCard.tsx b/src/components/Card/EpigramCard.tsx index c0cd5bc0..07ceb392 100644 --- a/src/components/Card/EpigramCard.tsx +++ b/src/components/Card/EpigramCard.tsx @@ -31,12 +31,13 @@ function EpigramCard() {
+ {/* 테스트 텍스트입니다. */} 오랫동안 꿈을 그리는 사람은 마침내 그 꿈을 닮아 간다.
- - 앙드레 말로 - + {/* 테스트 텍스트입니다. */}- 앙드레 말로 -
@@ -45,11 +46,13 @@ function EpigramCard() {
+ {/* 테스트 텍스트입니다. */} #나아가야할때
+ {/* 테스트 텍스트입니다. */} #꿈을이루고싶을때
From 78d6bd0673c556196433024c6c9affe028d9e6ac Mon Sep 17 00:00:00 2001 From: NEWJIN Date: Mon, 15 Jul 2024 10:25:35 +0900 Subject: [PATCH 05/11] =?UTF-8?q?FE-61=20=F0=9F=94=A8=20CommentCard=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EB=82=B4=EB=B6=80=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Card/CommentCard.tsx | 5 ++++- src/types/CommentCardTypes.ts | 3 --- 2 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 src/types/CommentCardTypes.ts diff --git a/src/components/Card/CommentCard.tsx b/src/components/Card/CommentCard.tsx index db867651..a58f0e6f 100644 --- a/src/components/Card/CommentCard.tsx +++ b/src/components/Card/CommentCard.tsx @@ -1,8 +1,11 @@ import React from 'react'; import Image from 'next/image'; -import { CommentCardProps } from '@/types/CommentCardTypes'; import { sizeStyles, textSizeStyles, gapStyles, paddingStyles, contentWidthStyles } from '@/styles/CommentCardStyles'; +export interface CommentCardProps { + status: 'edit' | 'complete'; +} + function CommentCard({ status }: CommentCardProps) { return (
Date: Mon, 15 Jul 2024 10:26:51 +0900 Subject: [PATCH 06/11] =?UTF-8?q?FE-61=20:truck:=20=EA=B0=90=EC=A0=95=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=ED=8F=B4=EB=8D=94=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Emotion/{card => }/EmotionIconCard.tsx | 0 src/components/Emotion/{card => }/EmotionSelector.tsx | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/components/Emotion/{card => }/EmotionIconCard.tsx (100%) rename src/components/Emotion/{card => }/EmotionSelector.tsx (100%) diff --git a/src/components/Emotion/card/EmotionIconCard.tsx b/src/components/Emotion/EmotionIconCard.tsx similarity index 100% rename from src/components/Emotion/card/EmotionIconCard.tsx rename to src/components/Emotion/EmotionIconCard.tsx diff --git a/src/components/Emotion/card/EmotionSelector.tsx b/src/components/Emotion/EmotionSelector.tsx similarity index 100% rename from src/components/Emotion/card/EmotionSelector.tsx rename to src/components/Emotion/EmotionSelector.tsx From 3732b6fb27ff91f237163c90f270a7f8ffcbd462 Mon Sep 17 00:00:00 2001 From: NEWJIN Date: Mon, 15 Jul 2024 10:27:58 +0900 Subject: [PATCH 07/11] =?UTF-8?q?=20FE-61=20=F0=9F=93=9D=20=EA=B0=90?= =?UTF-8?q?=EC=A0=95=20=EC=B9=B4=EB=93=9C,=20=EA=B0=90=EC=A0=95=20?= =?UTF-8?q?=EC=85=80=EB=A0=89=ED=84=B0=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Emotion/{EmotionIconCard.tsx => EmotionCard.tsx} | 6 ++++++ src/components/Emotion/EmotionSelector.tsx | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) rename src/components/Emotion/{EmotionIconCard.tsx => EmotionCard.tsx} (93%) diff --git a/src/components/Emotion/EmotionIconCard.tsx b/src/components/Emotion/EmotionCard.tsx similarity index 93% rename from src/components/Emotion/EmotionIconCard.tsx rename to src/components/Emotion/EmotionCard.tsx index a230deec..ce14ccd3 100644 --- a/src/components/Emotion/EmotionIconCard.tsx +++ b/src/components/Emotion/EmotionCard.tsx @@ -1,3 +1,9 @@ +/* + 1개의 감정 아이콘 카드를 랜더링 합니다. + 아이콘의 타입, 상태, 크기, 클릭 이벤트를 관리합니다. + 아이콘 타입과 상태에 따라 아이콘의 모양과 스타일을 조정합니다. + */ + import React from 'react'; import cn from '@/lib/utils'; import Image from 'next/image'; diff --git a/src/components/Emotion/EmotionSelector.tsx b/src/components/Emotion/EmotionSelector.tsx index ace202d4..5a73639e 100644 --- a/src/components/Emotion/EmotionSelector.tsx +++ b/src/components/Emotion/EmotionSelector.tsx @@ -1,5 +1,10 @@ +/* + 여러 개의 EmotionIconCard를 관리합니다. + 사용자 인터페이스에 필요한 상호 작용 로직을 포함합니다. + */ + import React, { useState } from 'react'; -import EmotionIconCard from '@/components/Emotion/card/EmotionIconCard'; +import EmotionIconCard from '@/components/Emotion/EmotionCard'; import useMediaQuery from '@/hooks/useMediaQuery'; import { EmotionType, EmotionState } from '@/types/EmotionTypes'; From a2ba4b3700c15754693ce3bb488edb7aa9b33eb5 Mon Sep 17 00:00:00 2001 From: NEWJIN Date: Mon, 15 Jul 2024 10:45:32 +0900 Subject: [PATCH 08/11] =?UTF-8?q?FE-61=20:fire:=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EB=94=94=ED=8F=B4=ED=8A=B8=20=ED=94=84?= =?UTF-8?q?=EB=A1=AD=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Emotion/EmotionCard.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/components/Emotion/EmotionCard.tsx b/src/components/Emotion/EmotionCard.tsx index ce14ccd3..1896f7cd 100644 --- a/src/components/Emotion/EmotionCard.tsx +++ b/src/components/Emotion/EmotionCard.tsx @@ -110,11 +110,4 @@ function EmotionIconCard({ iconType = '감동', state = 'Default', size = 'sm', ); } -EmotionIconCard.displayName = 'EmotionIconCard'; - -// 기본 props 설정 -EmotionIconCard.defaultProps = { - onClick: () => {}, -}; - export default EmotionIconCard; From 8dd99faa30a1456576747d11100332a007e08e19 Mon Sep 17 00:00:00 2001 From: imsoohyeok <160010477+imsoohyeok@users.noreply.github.com> Date: Mon, 15 Jul 2024 15:08:27 +0900 Subject: [PATCH 09/11] =?UTF-8?q?FE-62=20=E2=9C=A8fix:=20=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20(#27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Header/Header.tsx | 32 ++++++++++++++++++++++---------- src/types/Header.ts | 12 ------------ 2 files changed, 22 insertions(+), 22 deletions(-) delete mode 100644 src/types/Header.ts diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 5c893fed..d342347f 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { useRouter } from 'next/router'; import Image from 'next/image'; -import { HeaderProps } from '../../types/Header'; import { useToast } from '../ui/use-toast'; import LOGO_ICON from '../../../public/epigram-icon.png'; import ARROW_LEFT_ICON from '../../../public/icon/arrow-left-icon.svg'; @@ -9,15 +8,28 @@ import PROFILE_ICON from '../../../public/icon/profile-icon.svg'; import SEARCH_ICON from '../../../public/icon/search-icon.svg'; import SHARE_ICON from '../../../public/icon/share-icon.svg'; -// TODO 네비게이션 바를 나타내는 컴포넌트 입니다. -// TODO 상위 컴포넌트에서 Props를 받아 원하는 스타일을 보여줍니다. -// TODO 사용 예시 -// TODO
-// TODO
{}} />; -// TODO icon: 'back'을 사용할 경우 routerPage의 값을 무조건 지정해줘야 합니다. -// TODO isLogo={false}일 경우 insteadOfLogo의 값을 무조건 지정해줘야 합니다. -// TODO isButton 일 경우 textInButton의 값을 무조건 지정해줘야 합니다. -// TODO SHARE_ICON 추가 시 토스트 기능도 사용하려면 해당 컴포넌트 아래 를 추가해주세요. +// NOTE 네비게이션 바를 나타내는 컴포넌트 입니다. +// NOTE 상위 컴포넌트에서 Props를 받아 원하는 스타일을 보여줍니다. +// NOTE 사용 예시 +// NOTE
+// NOTE
{}} />; +// NOTE icon: 'back'을 사용할 경우 routerPage의 값을 무조건 지정해줘야 합니다. +// NOTE isLogo={false}일 경우 insteadOfLogo의 값을 무조건 지정해줘야 합니다. +// NOTE isButton 일 경우 textInButton의 값을 무조건 지정해줘야 합니다. +// NOTE SHARE_ICON 추가 시 토스트 기능도 사용하려면 해당 컴포넌트 아래 를 추가해주세요. + +export interface HeaderProps { + icon: 'back' | 'search' | ''; + routerPage: string; + isLogo: boolean; + insteadOfLogo: string; + isProfileIcon: boolean; + isShareIcon: boolean; + isButton: boolean; + textInButton: string; + disabled: boolean; + onClick: (e: React.MouseEvent) => void; +} function Header({ isLogo, icon, insteadOfLogo, isButton, isProfileIcon, isShareIcon, textInButton, routerPage, disabled, onClick }: HeaderProps) { const router = useRouter(); diff --git a/src/types/Header.ts b/src/types/Header.ts deleted file mode 100644 index a75d1fce..00000000 --- a/src/types/Header.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface HeaderProps { - icon: 'back' | 'search' | ''; - routerPage: string; - isLogo: boolean; - insteadOfLogo: string; - isProfileIcon: boolean; - isShareIcon: boolean; - isButton: boolean; - textInButton: string; - disabled: boolean; - onClick: (e: React.MouseEvent) => void; -} From 596369a0c8b03622335f6d1860a65a0f7b2ac3a0 Mon Sep 17 00:00:00 2001 From: MOON <50370479+jangmoonwon@users.noreply.github.com> Date: Fri, 26 Jul 2024 08:09:40 +0900 Subject: [PATCH 10/11] =?UTF-8?q?FE-29=20:twisted=5Frightwards=5Farrows:?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EB=A8=B8=EC=A7=80=20=EC=9A=94=EC=B2=AD=20(#39)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :heavy_plus_sign: 이미지 파일 추가 * :lipstick: 로그인 페이지 레이아웃 생성 * :lipstick: 로그인 페이지 UI 생성 및 반응형 디자인 구현 * FE-60 :sparkles: react hook form, zod 추가 * FE-60 :lipstick: 로그인 폼 스타일 수정 - 텍스트 인풋 테두리 - 로그인 버튼 * FE-60 :recycle: 로그인 스키마 분리 * :sparkles: 로그인 응답 데이터 스키마 정의 * :sparkles: 로그인 api 생성 * :sparkles: 요청과 응답에 관한 인터셉터 추가 * :sparkles: useSignin mutation hook 생성 * :zap: useSignin hook 로그인 폼에 적용 * :fire: AuthLayout 삭제 * :art: onSubmit 함수 인라인으로 정의 * :recycle: 응답 인터셉터의 에러 처리 및 토큰 갱신 로직 개선 * :recycle: postSignin api 에러처리 로직 삭제 * :fire: useSignin hook 삭제 * :truck: useSigninMutation hook으로 이름 변경 및 파일 이동 * :sparkles: Toaster 컴포넌트 추가 * :sparkles: toast로 에러메시지 띄우기 --- public/lg.svg | 5 ++ public/logo-google.svg | 15 +++++ public/logo-kakao.svg | 13 +++++ public/logo-naver.svg | 5 ++ src/apis/auth.ts | 9 +++ src/apis/index.ts | 42 ++++++++++++++ src/hooks/useSignInMutation.ts | 22 ++++++++ src/pages/_app.tsx | 2 + src/pages/auth/SignIn.tsx | 100 +++++++++++++++++++++++++++++++++ src/schema/auth.ts | 25 +++++++++ 10 files changed, 238 insertions(+) create mode 100644 public/lg.svg create mode 100644 public/logo-google.svg create mode 100644 public/logo-kakao.svg create mode 100644 public/logo-naver.svg create mode 100644 src/apis/auth.ts create mode 100644 src/hooks/useSignInMutation.ts create mode 100644 src/pages/auth/SignIn.tsx create mode 100644 src/schema/auth.ts diff --git a/public/lg.svg b/public/lg.svg new file mode 100644 index 00000000..a4d3364f --- /dev/null +++ b/public/lg.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/logo-google.svg b/public/logo-google.svg new file mode 100644 index 00000000..5b169484 --- /dev/null +++ b/public/logo-google.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/logo-kakao.svg b/public/logo-kakao.svg new file mode 100644 index 00000000..f546e64d --- /dev/null +++ b/public/logo-kakao.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/logo-naver.svg b/public/logo-naver.svg new file mode 100644 index 00000000..dbec93dd --- /dev/null +++ b/public/logo-naver.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/apis/auth.ts b/src/apis/auth.ts new file mode 100644 index 00000000..8244a9b4 --- /dev/null +++ b/src/apis/auth.ts @@ -0,0 +1,9 @@ +import type { PostSigninRequestType, PostSigninResponseType } from '@/schema/auth'; +import httpClient from '.'; + +const postSignin = async (request: PostSigninRequestType): Promise => { + const response = await httpClient.post('/auth/signIn', request); + return response.data; +}; + +export default postSignin; diff --git a/src/apis/index.ts b/src/apis/index.ts index 29949fc2..4167407d 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -8,3 +8,45 @@ const httpClient = axios.create({ }); export default httpClient; + +// NOTE: eslint-disable no-param-reassign 미해결로 인한 설정 +httpClient.interceptors.request.use((config) => { + const accessToken = localStorage.getItem('accessToken'); + /* eslint-disable no-param-reassign */ + if (accessToken) config.headers.Authorization = `Bearer ${accessToken}`; + /* eslint-enable no-param-reassign */ + return config; +}); + +httpClient.interceptors.response.use( + (response) => response, + + (error) => { + if (error.response && error.response.status === 401) { + const refreshToken = localStorage.getItem('refreshToken'); + + if (!refreshToken) { + window.location.href = '/auth/SignIn'; + return Promise.reject(error); + } + + return httpClient + .post('/auth/refresh-token', null, { + headers: { Authorization: `Bearer ${refreshToken}` }, + }) + .then((response) => { + const { accessToken, refreshToken: newRefreshToken } = response.data; + localStorage.setItem('accessToken', accessToken); + localStorage.setItem('refreshToken', newRefreshToken); + + const originalRequest = error.config; + return httpClient(originalRequest); + }) + .catch(() => { + window.location.href = '/auth/SignIn'; + return Promise.reject(error); + }); + } + return Promise.reject(error); + }, +); diff --git a/src/hooks/useSignInMutation.ts b/src/hooks/useSignInMutation.ts new file mode 100644 index 00000000..eaf9fd76 --- /dev/null +++ b/src/hooks/useSignInMutation.ts @@ -0,0 +1,22 @@ +import postSignin from '@/apis/auth'; +import { toast } from '@/components/ui/use-toast'; +import { useMutation } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; + +const useSigninMutation = () => { + const router = useRouter(); + + return useMutation({ + mutationFn: postSignin, + onSuccess: (data) => { + localStorage.setItem('accessToken', data.accessToken); + localStorage.setItem('refreshToken', data.refreshToken); + router.push('/'); + }, + onError: () => { + toast({ description: '이메일 혹은 비밀번호를 확인해주세요.', className: 'border-state-error text-state-error font-semibold' }); + }, + }); +}; + +export default useSigninMutation; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 37d2f8d3..107acf01 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -3,6 +3,7 @@ import '@/styles/globals.css'; import type { AppProps } from 'next/app'; import { HydrationBoundary, QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import Toaster from '@/components/ui/toaster'; export default function App({ Component, pageProps }: AppProps) { const [queryClient] = React.useState(() => new QueryClient()); @@ -10,6 +11,7 @@ export default function App({ Component, pageProps }: AppProps) { + diff --git a/src/pages/auth/SignIn.tsx b/src/pages/auth/SignIn.tsx new file mode 100644 index 00000000..400d0edd --- /dev/null +++ b/src/pages/auth/SignIn.tsx @@ -0,0 +1,100 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form'; +import { PostSigninRequest, PostSigninRequestType } from '@/schema/auth'; +import useSigninMutation from '@/hooks/useSignInMutation'; + +export default function SignIn() { + const mutationSignin = useSigninMutation(); + // 폼 정의 + const form = useForm({ + resolver: zodResolver(PostSigninRequest), + mode: 'onBlur', + defaultValues: { + email: '', + password: '', + }, + }); + + // TODO: 나중에 컴포넌트 분리하기 + return ( +
+
+ + logo + +
+ +
+ mutationSignin.mutate(values))} className='flex flex-col items-center lg:gap-6 gap-5 w-full px-6'> +
+ ( + + + + + + + )} + /> + ( + + + + + + + )} + /> +
+ +
+ +
+

회원이 아니신가요?

+ + + +
+
+ + + +
+
+ ); +} diff --git a/src/schema/auth.ts b/src/schema/auth.ts new file mode 100644 index 00000000..33466608 --- /dev/null +++ b/src/schema/auth.ts @@ -0,0 +1,25 @@ +import * as z from 'zod'; + +export const PostSigninRequest = z.object({ + email: z.string().min(1, { message: '이메일은 필수 입력입니다.' }).email({ message: '올바른 이메일 주소가 아닙니다.' }), + password: z.string().min(1, { message: '비밀번호는 필수 입력입니다.' }), +}); + +const User = z.object({ + id: z.number(), + email: z.string().email(), + nickname: z.string(), + teamId: z.string(), + updatedAt: z.coerce.date(), + createdAt: z.coerce.date(), + image: z.string(), +}); + +export const PostSigninResponse = z.object({ + accessToken: z.string(), + refreshToken: z.string(), + user: User, +}); + +export type PostSigninRequestType = z.infer; +export type PostSigninResponseType = z.infer; From 4ba94c8b13cabb933a3fba0a7d9eaa23f6e4ff32 Mon Sep 17 00:00:00 2001 From: Jiseok Woo <115205098+jisurk@users.noreply.github.com> Date: Sat, 27 Jul 2024 22:19:45 +0900 Subject: [PATCH 11/11] =?UTF-8?q?FE-71=20=F0=9F=94=80=20=EC=97=90=ED=94=BC?= =?UTF-8?q?=EA=B7=B8=EB=9E=A8=20=EC=9E=91=EC=84=B1=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20(#71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * FE-64💄 글작성 페이지 UI추가 (#44) * FE-72 ✨ 에피그램 등록 api연동 (#52) * FE-72✨ 글작성페이지 스키마 추가 * FE-72✨ form태그 Form컴포넌트로 변경 * FE-72✨ 태그 저장기능 추가 * FE-72✨ 에피그램 등록 api연동 * FE-72✨ 에피그램 등록시 해당 에피그램 페이지로 이동 기능 추가 * FE-72✨ 등록 중일때의 로직추가 * FE-72✨ toast-> alert-dailog로 변경 * FE-72📝 TODO주석 추가 --------- Co-authored-by: 우지석 * FE-73✨ 유효성검사 추가 (#66) * FE-73♻️ Tag관리 함수 훅으로 분리 * FE-73✨ RadioGroup 로직 수정 * FE-73✨ 유효성검사 추가 * FE-73♻️ 저자 본인 선택시의 로직 변경 * FE-73✨ 중복 태그 검사 로직 추가 * FE-73♻️ 출처 유효성(optional)검사 수정 * FE-73✨ 필수항목 입력했을때 버튼 활성화 * FE-73🐛 태그를 입력했다가 지웠을때 버튼 활성화되있는 버그 수정 * FE-73🐛 useEffect 의존성배열 lint problem 해결 * FE-73🐛 url유효성검사 에러 메세지 안뜨는 버그 수정 --------- Co-authored-by: 우지석 * FE-71♻️ epic브랜치 코드리뷰 반영 (#76) * FE-71♻️ token,interceptor 로직 수정 * FE-71♻️ AddEpigram 코드리뷰 반영 * FE-71🔥 테스트용 상세페이지 삭제 * FE-71♻️ onKeyDown -> onKeyUp 수정 --------- Co-authored-by: 우지석 --- src/apis/add.ts | 9 + src/apis/index.ts | 4 +- src/hooks/epigramQueryHook.ts | 24 ++ src/hooks/useTagManagementHook.ts | 47 ++++ src/pageLayout/Epigram/AddEpigram.tsx | 316 ++++++++++++++++++++++++++ src/pages/addEpigram.tsx | 7 + src/schema/addEpigram.ts | 44 ++++ 7 files changed, 449 insertions(+), 2 deletions(-) create mode 100644 src/apis/add.ts create mode 100644 src/hooks/epigramQueryHook.ts create mode 100644 src/hooks/useTagManagementHook.ts create mode 100644 src/pageLayout/Epigram/AddEpigram.tsx create mode 100644 src/pages/addEpigram.tsx create mode 100644 src/schema/addEpigram.ts diff --git a/src/apis/add.ts b/src/apis/add.ts new file mode 100644 index 00000000..66a6b010 --- /dev/null +++ b/src/apis/add.ts @@ -0,0 +1,9 @@ +import { AddEpigramRequestType, AddEpigramResponseType } from '@/schema/addEpigram'; +import httpClient from '.'; + +const postEpigram = async (request: AddEpigramRequestType): Promise => { + const response = await httpClient.post('/epigrams', request); + return response.data; +}; + +export default postEpigram; diff --git a/src/apis/index.ts b/src/apis/index.ts index 4167407d..e58d8047 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -7,8 +7,6 @@ const httpClient = axios.create({ paramsSerializer: (parameters) => qs.stringify(parameters, { arrayFormat: 'repeat', encode: false }), }); -export default httpClient; - // NOTE: eslint-disable no-param-reassign 미해결로 인한 설정 httpClient.interceptors.request.use((config) => { const accessToken = localStorage.getItem('accessToken'); @@ -50,3 +48,5 @@ httpClient.interceptors.response.use( return Promise.reject(error); }, ); + +export default httpClient; diff --git a/src/hooks/epigramQueryHook.ts b/src/hooks/epigramQueryHook.ts new file mode 100644 index 00000000..e2ca6679 --- /dev/null +++ b/src/hooks/epigramQueryHook.ts @@ -0,0 +1,24 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AddEpigramFormType, AddEpigramResponseType } from '@/schema/addEpigram'; +import { MutationOptions } from '@/types/query'; +import postEpigram from '@/apis/add'; +import { AxiosError } from 'axios'; + +// TODO: 에피그램 수정과 삭제에도 사용 가능하게 훅 수정 예정 + +const useAddEpigram = (options?: MutationOptions) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (newEpigram: AddEpigramFormType) => postEpigram(newEpigram), + ...options, + onSuccess: (...args) => { + queryClient.invalidateQueries({ queryKey: ['epigrams'] }); + if (options?.onSuccess) { + options.onSuccess(...args); + } + }, + }); +}; + +export default useAddEpigram; diff --git a/src/hooks/useTagManagementHook.ts b/src/hooks/useTagManagementHook.ts new file mode 100644 index 00000000..dd0082de --- /dev/null +++ b/src/hooks/useTagManagementHook.ts @@ -0,0 +1,47 @@ +import { useState } from 'react'; +import { UseFormSetValue, UseFormGetValues, UseFormSetError } from 'react-hook-form'; +import { AddEpigramFormType } from '@/schema/addEpigram'; + +// NOTE: setError메서드로 FormField에 에러 설정 가능 +const useTagManagement = ({ + setValue, + getValues, + setError, +}: { + setValue: UseFormSetValue; + getValues: UseFormGetValues; + setError: UseFormSetError; +}) => { + const [currentTag, setCurrentTag] = useState(''); + + const handleAddTag = () => { + if (!currentTag || currentTag.length > 10) { + return; + } + const currentTags = getValues('tags') || []; + + if (currentTags.length >= 3) { + return; + } + if (currentTags.includes(currentTag)) { + setError('tags', { type: 'manual', message: '이미 저장된 태그입니다.' }); + return; + } + + setValue('tags', [...currentTags, currentTag]); + setCurrentTag(''); + setError('tags', { type: 'manual', message: '' }); + }; + + const handleRemoveTag = (tagToRemove: string) => { + const currentTags = getValues('tags') || []; + setValue( + 'tags', + currentTags.filter((tag) => tag !== tagToRemove), + ); + }; + + return { currentTag, setCurrentTag, handleAddTag, handleRemoveTag }; +}; + +export default useTagManagement; diff --git a/src/pageLayout/Epigram/AddEpigram.tsx b/src/pageLayout/Epigram/AddEpigram.tsx new file mode 100644 index 00000000..f314c730 --- /dev/null +++ b/src/pageLayout/Epigram/AddEpigram.tsx @@ -0,0 +1,316 @@ +import React, { KeyboardEvent, useCallback, useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import Header from '@/components/Header/Header'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Textarea } from '@/components/ui/textarea'; +import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'; +import { AddEpigramFormSchema, AddEpigramFormType } from '@/schema/addEpigram'; +import useAddEpigram from '@/hooks/epigramQueryHook'; +import { useRouter } from 'next/router'; +import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'; +import useTagManagement from '@/hooks/useTagManagementHook'; +import { useMeQuery } from '@/hooks/userQueryHooks'; + +function AddEpigram() { + const router = useRouter(); + const { data: userData, isPending, isError } = useMeQuery(); + const [isAlertOpen, setIsAlertOpen] = useState(false); + const [alertContent, setAlertContent] = useState({ title: '', description: '' }); + const [selectedAuthorOption, setSelectedAuthorOption] = useState('directly'); // 기본값을 'directly'로 설정 + const [isFormValid, setIsFormValid] = useState(false); + + const form = useForm({ + resolver: zodResolver(AddEpigramFormSchema), + defaultValues: { + content: '', + author: '', + referenceTitle: '', + referenceUrl: '', + tags: [], + }, + }); + + // NOTE: 필수항목들에 값이 들어있는지 확인 함수 + const checkFormEmpty = useCallback(() => { + const { content, author, tags } = form.getValues(); + return content.trim() !== '' && author.trim() !== '' && tags.length > 0; + }, [form]); + + // NOTE: form값이 변경될때 필수항목들이 들어있는지 확인 + const watchForm = useCallback(() => { + setIsFormValid(checkFormEmpty()); + }, [checkFormEmpty]); + + useEffect(() => { + const subscription = form.watch(watchForm); + return () => subscription.unsubscribe(); + }, [form, watchForm]); + + const { currentTag, setCurrentTag, handleAddTag, handleRemoveTag } = useTagManagement({ + setValue: form.setValue, + getValues: form.getValues, + setError: form.setError, + }); + const addEpigramMutation = useAddEpigram({ + onSuccess: () => { + setAlertContent({ + title: '등록 완료', + description: '등록이 완료되었습니다.', + }); + setIsAlertOpen(true); + form.reset(); + }, + onError: () => { + setAlertContent({ + title: '등록 실패', + description: '다시 시도해주세요.', + }); + setIsAlertOpen(true); + }, + }); + + const handleAlertClose = () => { + setIsAlertOpen(false); + if (alertContent.title === '등록 완료') { + router.push(`/epigram/${addEpigramMutation.data?.id}`); + } + }; + + const AUTHOR_OPTIONS = [ + { value: 'directly', label: '직접 입력' }, + { value: 'unknown', label: '알 수 없음' }, + { value: 'me', label: '본인' }, + ]; + + // NOTE: default를 직접 입력으로 설정 + // NOTE: 본인을 선택 시 유저의 nickname이 들어감 + const handleAuthorChange = async (value: string) => { + setSelectedAuthorOption(value); + let authorValue: string; + + switch (value) { + case 'unknown': + authorValue = '알 수 없음'; + break; + case 'me': + if (isPending) { + authorValue = '로딩 중...'; + } else if (userData) { + authorValue = userData.nickname; + } else { + authorValue = '본인 (정보 없음)'; + } + break; + default: + authorValue = ''; + } + form.setValue('author', authorValue); + }; + + if (isPending) { + return
사용자 정보를 불러오는 중...
; + } + + if (isError) { + return
사용자 정보를 불러오는 데 실패했습니다. 페이지를 새로고침 해주세요.
; + } + + // NOTE: 태그를 저장하려고 할때 enter키를 누르면 폼제출이 되는걸 방지 + const handleKeyUp = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleAddTag(); + } + }; + + // NOTE: url와title은 필수 항목이 아니라서 빈칸으로 제출할 때 항목에서 제외 + const handleSubmit = (data: AddEpigramFormType) => { + const submitData = { ...data }; + + if (!submitData.referenceUrl) { + delete submitData.referenceUrl; + } + + if (!submitData.referenceTitle) { + delete submitData.referenceTitle; + } + + addEpigramMutation.mutate(submitData); + }; + + return ( + <> +
{}} /> +
+
+ + ( + + + 내용 + * + + +