From 837ed2757ff9b4ea8d66ddfd839478defbb61888 Mon Sep 17 00:00:00 2001 From: whistleJs Date: Sun, 11 Feb 2024 01:08:34 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feature-058:=20API=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=8B=9C=20credentials=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20prox?= =?UTF-8?q?y=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/apis/config/instance.ts | 7 ++++++- vite.config.ts | 9 +++++++++ yarn.lock | 9 ++++++++- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 7d60688..3ae2b35 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@types/jest": "^29.5.11", - "@types/node": "^20.11.9", + "@types/node": "^20.11.17", "@types/react-modal": "^3.16.3", "axios": "^1.6.4", "react": "^18.2.0", diff --git a/src/apis/config/instance.ts b/src/apis/config/instance.ts index 794cb5f..7cdcce7 100644 --- a/src/apis/config/instance.ts +++ b/src/apis/config/instance.ts @@ -1,10 +1,15 @@ import axios from 'axios'; -const baseURL = 'https://backend.vi-no.site'; +const baseURL = + process.env.NODE_ENV === 'development' + ? '/api' + : 'https://backend.vi-no.site'; const axiosInstance = axios.create({ baseURL }); axiosInstance.interceptors.request.use((config) => { + config.withCredentials = true; + if (localStorage.vino) { const storage = JSON.parse(localStorage.vino); diff --git a/vite.config.ts b/vite.config.ts index e98bcec..8d16013 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,4 +6,13 @@ import svgr from 'vite-plugin-svgr'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), paths(), svgr()], + server: { + proxy: { + '/api': { + target: 'https://backend.vi-no.site/', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ''), + }, + }, + }, }); diff --git a/yarn.lock b/yarn.lock index 420cd38..732c10e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -730,13 +730,20 @@ resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/node@*", "@types/node@^20.11.9": +"@types/node@*": version "20.11.9" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.9.tgz#959d436f20ce2ee3df897c3eaa0617c98fa70efb" integrity sha512-CQXNuMoS/VcoAMISe5pm4JnEd1Br5jildbQEToEMQvutmv+EaQr90ry9raiudgpyDuqFiV9e4rnjSfLNq12M5w== dependencies: undici-types "~5.26.4" +"@types/node@^20.11.17": + version "20.11.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.17.tgz#cdd642d0e62ef3a861f88ddbc2b61e32578a9292" + integrity sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw== + dependencies: + undici-types "~5.26.4" + "@types/prop-types@*": version "15.7.11" resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz" From 825a44b78320d9fdccf15775281e02d503c7e67b Mon Sep 17 00:00:00 2001 From: whistleJs Date: Sun, 11 Feb 2024 01:08:57 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feature-058:=20Tooltip=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/Tooltip/Tooltip.tsx | 10 ++- src/components/common/Tooltip/style.ts | 79 +++++++++++++++++++++-- 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/src/components/common/Tooltip/Tooltip.tsx b/src/components/common/Tooltip/Tooltip.tsx index d6600e7..661129b 100644 --- a/src/components/common/Tooltip/Tooltip.tsx +++ b/src/components/common/Tooltip/Tooltip.tsx @@ -1,12 +1,18 @@ +import { CSSProperties } from 'react'; import { TooltipBox, TooltipBoxDirection } from './style'; type Props = { direction: TooltipBoxDirection; children: JSX.Element | JSX.Element[] | string; + style?: CSSProperties; }; -const Tooltip = ({ direction, children }: Props) => { - return {children}; +const Tooltip = ({ direction, children, style }: Props) => { + return ( + + {children} + + ); }; export default Tooltip; diff --git a/src/components/common/Tooltip/style.ts b/src/components/common/Tooltip/style.ts index c5d4450..8eeddfa 100644 --- a/src/components/common/Tooltip/style.ts +++ b/src/components/common/Tooltip/style.ts @@ -1,20 +1,91 @@ -import styled from 'styled-components'; +import styled, { css, keyframes } from 'styled-components'; export type TooltipBoxDirection = 'up' | 'right' | 'down' | 'left'; +const upKeyframe = keyframes` + from { + opacity: 0; + transform: translateY(-15px); + } + + to { + opacity: 1; + transform: translateY(0px); + } +`; + +const rightKeyframe = keyframes` + from { + opacity: 0; + transform: translateX(20px); + } + + to { + opacity: 1; + transform: translateX(0px); + } +`; + +const downKeyframe = keyframes` + from { + opacity: 0; + transform: translateY(15px); + } + + to { + opacity: 1; + transform: translateY(0px); + } +`; + +const leftKeyframe = keyframes` + from { + opacity: 0; + transform: translateX(-20px); + } + + to { + opacity: 1; + transform: translateX(0px); + } +`; + export const TooltipBox = styled.div<{ direction: TooltipBoxDirection }>` position: relative; padding: 12px 28px; border-radius: 4px; - background-color: rgba(30, 30, 30, 0.9); + background-color: ${(props) => props.theme.color.green300}; + box-shadow: 4px 4px 30px 0 rgba(0, 0, 0, 0.16); text-align: center; - color: ${(props) => props.theme.color.green400}; + white-space: nowrap; + color: ${(props) => props.theme.color.gray500}; ${(props) => props.theme.typography.Caption2}; + animation: ${(props) => { + switch (props.direction) { + case 'up': + return css` + ${upKeyframe} 1s forwards + `; + case 'right': + return css` + ${rightKeyframe} 1s forwards + `; + case 'down': + return css` + ${downKeyframe} 1s forwards + `; + case 'left': + return css` + ${leftKeyframe} 1s forwards + `; + } + }}; + &::before { content: ''; position: absolute; - background-color: rgba(30, 30, 30, 0.9); + background-color: ${(props) => props.theme.color.green300}; ${(props) => { switch (props.direction) { From 935da909ebb0b188dbc4b9eb930195b748c92734 Mon Sep 17 00:00:00 2001 From: whistleJs Date: Sun, 11 Feb 2024 01:09:42 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feature-058:=20=EB=82=B4=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=8E=98=EC=9D=B4=EC=A7=80=20API=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/user.ts | 12 +- .../ProfilePage/Account/Account.tsx | 200 +++++++++++++++--- .../Account/ChangePassword/ChangePassword.tsx | 26 +++ .../ChangePassword/ChangePasswordModal.tsx | 9 + .../Account/ChangePassword/index.ts | 1 + .../layout/header/profile/ProfileDetail.tsx | 11 +- .../layout/header/profile/index.tsx | 22 +- src/components/modals/RecommendationModal.tsx | 73 +++---- src/constants/user.ts | 5 + src/hooks/useFocus.ts | 31 +++ src/models/user.ts | 24 ++- src/pages/ProfilePage.tsx | 14 +- src/stores/user.ts | 7 + src/styles/ProfilePage.ts | 20 +- src/utils/validation.ts | 2 + 15 files changed, 369 insertions(+), 88 deletions(-) create mode 100644 src/components/ProfilePage/Account/ChangePassword/ChangePassword.tsx create mode 100644 src/components/ProfilePage/Account/ChangePassword/ChangePasswordModal.tsx create mode 100644 src/components/ProfilePage/Account/ChangePassword/index.ts create mode 100644 src/constants/user.ts create mode 100644 src/hooks/useFocus.ts create mode 100644 src/utils/validation.ts diff --git a/src/apis/user.ts b/src/apis/user.ts index 9d02156..677fdf2 100644 --- a/src/apis/user.ts +++ b/src/apis/user.ts @@ -1,4 +1,4 @@ -import { APIResponse } from '@/models/config/axios'; +import { APIBaseResponse, APIResponse } from '@/models/config/axios'; import { CheckEmailRequest, CheckEmailResponse, @@ -6,6 +6,8 @@ import { JoinResponse, LoginRequest, LoginResponse, + MyInfoResponse, + UpdateMyInfoRequest, } from '@/models/user'; import { AlarmResponse, @@ -13,7 +15,6 @@ import { DeleteAlarmRequest, DeleteAlarmResponse, } from '@/models/alarm'; -import { getNicknameResponse } from '@/models/user'; import axios from './config/instance'; const PREFIX = '/user'; @@ -54,7 +55,10 @@ export const socialAccountAPI = (code: string) => { return axios.get(`/sign-up/success?code=${code}`); }; -export const getNicknameAPI = () => { - return axios.get>(PREFIX + '/myPage/myInfo'); +export const getMyInfoAPI = () => { + return axios.get>(PREFIX + '/myPage/myInfo'); }; +export const updateMyInfoAPI = (data: UpdateMyInfoRequest) => { + return axios.put(PREFIX + '/myPage/setInfo', data); +}; diff --git a/src/components/ProfilePage/Account/Account.tsx b/src/components/ProfilePage/Account/Account.tsx index 61419ec..78a091d 100644 --- a/src/components/ProfilePage/Account/Account.tsx +++ b/src/components/ProfilePage/Account/Account.tsx @@ -1,22 +1,140 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useRecoilState } from 'recoil'; + +import { getMyInfoAPI, updateMyInfoAPI } from '@/apis/user'; + import NaverIconImage from '@/assets/naver-icon.png'; +import { Tooltip } from '@/components/common'; + +import { GENDER_TYPE_LIST } from '@/constants/user'; + +import useFocus from '@/hooks/useFocus'; + +import { userInfoState } from '@/stores/user'; + +import theme from '@/styles/theme'; import { Box } from '@/styles/ProfilePage'; +import { validateNickname } from '@/utils/validation'; + +import { ChangePassword } from './ChangePassword'; + const Account = () => { - const isSocialAccount = true; + const timerRef = useRef(); + const [userInfo, setUserInfo] = useRecoilState(userInfoState); + const [nickname, setNickname] = useState(userInfo?.nick_name || ''); + const [gender, setGender] = useState(userInfo?.gender || ''); + + const [isDuplicateNickname, setIsDuplicateNickname] = useState(false); + const [isErrorNickname, setIsErrorNickname] = useState( + validateNickname(nickname), + ); + + const [isShowTooltip, setIsShowTooltip] = useState(false); + const [nicknameInputRef, focusNicknameInput, isNicknameFocus] = + useFocus(); + + const isSocialAccount = !!userInfo?.platform; + const birthDate = new Date(userInfo?.birth_date || ''); + + const isDisabled = useMemo(() => { + if (isErrorNickname) return true; + + return !(nickname !== userInfo?.nick_name || gender !== userInfo?.gender); + }, [userInfo, nickname, gender, isErrorNickname]); + + const nicknameInputStyle = { + border: `1.5px solid ${ + isErrorNickname + ? theme.color.red + : isNicknameFocus + ? theme.color.gray500 + : theme.color.gray200 + }`, + }; + + const clearTooltipTimer = () => { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }; + + const showTooltip = () => { + setIsShowTooltip(true); + + clearTooltipTimer(); + + timerRef.current = setTimeout(() => { + setIsShowTooltip(false); + }, 1000 * 5); + }; + + const refreshMyInfo = async () => { + try { + const { result } = (await getMyInfoAPI()).data; + + setUserInfo(result); + } catch (e) { + console.error(e); + } + }; + + const handleChangeNickname: React.ChangeEventHandler = ({ + target: { value }, + }) => { + setIsDuplicateNickname(false); + setNickname(value); + setIsErrorNickname(validateNickname(value)); + }; + + const handleClickSubmitButton = async () => { + try { + const { success } = ( + await updateMyInfoAPI({ nick_name: nickname, gender }) + ).data; + + if (success) { + showTooltip(); + refreshMyInfo(); + } + } catch (e) { + console.error(e); + } + }; + + useEffect(() => { + return () => { + clearTooltipTimer(); + }; + }, []); return (

계정

- +
+ {isShowTooltip && ( +
+ 수정이 완료되었어요! +
+ )} + + +
@@ -30,13 +148,35 @@ const Account = () => { > 닉네임 -
+
- +
- 3/7 (공백포함) + + {nickname.length}/7 (공백포함) +
+ + {isErrorNickname && ( + + 닉네임 형식이 올바르지 않아요! + + )} + {isDuplicateNickname && ( + + 이미 존재하는 닉네임이에요! + + )}
@@ -44,9 +184,13 @@ const Account = () => { 이름
-
서예진
+
+ {userInfo?.name} +
- 3/7 (공백포함) + + {userInfo?.name.length}/7 (공백포함) +
@@ -54,9 +198,15 @@ const Account = () => { 성별
- - - + {GENDER_TYPE_LIST.map((genderType) => ( + + ))}
@@ -64,16 +214,20 @@ const Account = () => { 생년월일
-
2001
-
07
-
01
+
{birthDate.getFullYear()}
+
+ {String(birthDate.getMonth() + 1).padStart(2, '0')} +
+
+ {String(birthDate.getDate()).padStart(2, '0')} +
전화번호 -
010901716171
+
{userInfo?.phone_number}
{isSocialAccount && ( @@ -86,7 +240,7 @@ const Account = () => { > - yejin2174@naver.com + {userInfo?.email} )} @@ -97,20 +251,10 @@ const Account = () => {
계정 -
yejin2174@naver.com
+
{userInfo?.email}
-
- 비밀번호 - -
-
- -
- - -
-
+ )} diff --git a/src/components/ProfilePage/Account/ChangePassword/ChangePassword.tsx b/src/components/ProfilePage/Account/ChangePassword/ChangePassword.tsx new file mode 100644 index 0000000..96697ca --- /dev/null +++ b/src/components/ProfilePage/Account/ChangePassword/ChangePassword.tsx @@ -0,0 +1,26 @@ +import useBoolean from '@/hooks/useBoolean'; + +import ChangePasswordModal from './ChangePasswordModal'; + +const ChangePassword = () => { + const [isOpen, , open, close] = useBoolean(false); + + return ( + <> +
+ 비밀번호 + + +
+ + {isOpen && } + + ); +}; + +export default ChangePassword; diff --git a/src/components/ProfilePage/Account/ChangePassword/ChangePasswordModal.tsx b/src/components/ProfilePage/Account/ChangePassword/ChangePasswordModal.tsx new file mode 100644 index 0000000..141169b --- /dev/null +++ b/src/components/ProfilePage/Account/ChangePassword/ChangePasswordModal.tsx @@ -0,0 +1,9 @@ +type Props = { + onClose: () => void; +}; + +const ChangePasswordModal = ({ onClose }: Props) => { + return
; +}; + +export default ChangePasswordModal; diff --git a/src/components/ProfilePage/Account/ChangePassword/index.ts b/src/components/ProfilePage/Account/ChangePassword/index.ts new file mode 100644 index 0000000..ce02d00 --- /dev/null +++ b/src/components/ProfilePage/Account/ChangePassword/index.ts @@ -0,0 +1 @@ +export { default as ChangePassword } from './ChangePassword'; diff --git a/src/components/layout/header/profile/ProfileDetail.tsx b/src/components/layout/header/profile/ProfileDetail.tsx index d3880c5..13b5c6f 100644 --- a/src/components/layout/header/profile/ProfileDetail.tsx +++ b/src/components/layout/header/profile/ProfileDetail.tsx @@ -1,11 +1,13 @@ import { useNavigate } from 'react-router-dom'; -import { useSetRecoilState } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import ProfileImage from '@/assets/default-profile-rect.png'; import InformationImage from '@/assets/information.png'; import KeyImage from '@/assets/key.png'; -import { userTokenState } from '@/stores/user'; +import { MyInfoResponse } from '@/models/user'; + +import { userInfoState, userTokenState } from '@/stores/user'; import * as ProfileDetailStyle from '@/styles/layout/header/profile/ProfileDetailstyle'; @@ -16,6 +18,7 @@ type Props = { const ProfileDetail = ({ onClose }: Props) => { const navigate = useNavigate(); const setUserToken = useSetRecoilState(userTokenState); + const userInfo = useRecoilValue(userInfoState) as MyInfoResponse; const handleClickProfileButton = () => { navigate('/profile'); @@ -34,10 +37,10 @@ const ProfileDetail = ({ onClose }: Props) => { 사각 프로필 이미지 - 여울 + {userInfo.name} - abcd1234@naver.com + {userInfo.email} diff --git a/src/components/layout/header/profile/index.tsx b/src/components/layout/header/profile/index.tsx index 589ccc7..0fafbf3 100644 --- a/src/components/layout/header/profile/index.tsx +++ b/src/components/layout/header/profile/index.tsx @@ -1,19 +1,39 @@ -import { useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; +import { useSetRecoilState } from 'recoil'; + +import { getMyInfoAPI } from '@/apis/user'; import ProfileImage from '@/assets/default-profile-circle.png'; import useOutsideClick from '@/hooks/useOutsideClick'; +import { userInfoState } from '@/stores/user'; + import * as HeaderStyle from '@/styles/layout/header'; import { BlurBackground } from '@/styles/modals/common.style'; import ProfileDetail from './ProfileDetail'; const Profile = () => { + const setUserInfo = useSetRecoilState(userInfoState); const [isOpen, setIsOpen] = useState(false); const [profileRef] = useOutsideClick(() => setIsOpen(false)); + const callAPI = useCallback(async () => { + try { + const { result } = (await getMyInfoAPI()).data; + + setUserInfo(result); + } catch (e) { + console.error(e); + } + }, [setUserInfo]); + + useEffect(() => { + callAPI(); + }, [callAPI]); + return ( <>
diff --git a/src/components/modals/RecommendationModal.tsx b/src/components/modals/RecommendationModal.tsx index 066aedf..6233b53 100644 --- a/src/components/modals/RecommendationModal.tsx +++ b/src/components/modals/RecommendationModal.tsx @@ -8,7 +8,7 @@ import CloseIcon from '@/assets/icons/close.svg?react'; import cardimageImg from '@/assets/card-image.png'; import { RecommendationModalContainer } from '@/styles/modals/RecommendationModal.style'; -import { getNicknameAPI } from '@/apis/user'; +import { getMyInfoAPI } from '@/apis/user'; const RecommendationModal: React.FC = () => { const [nickname, setNickname] = useState(''); @@ -16,59 +16,56 @@ const RecommendationModal: React.FC = () => { const closeModal = () => { setModalOpen(false); - } + }; const [modalRef] = useOutsideClick(closeModal); useEffect(() => { async function fetchNickname() { - const response = await getNicknameAPI(); + const response = await getMyInfoAPI(); setNickname(response.data.result.nickname); } fetchNickname(); }, []); - return ( - -
-
-
- -
-
-
- emptyvideoImg -
- 기다리는 동안 이런 영상은 어때요? -
-
- {nickname}님을 위해 미리 정리 된 영상을 소개해드릴게요 -
-
-
+ +
+
+
+ +
+
+
+ emptyvideoImg +
+ 기다리는 동안 이런 영상은 어때요? +
+
+ {nickname}님을 위해 미리 정리 된 영상을 소개해드릴게요 +
-
- card-image -
-

- 우리는 카카오워크로 일해요 -

-
- - # 디자인 - - - # 진로 - -
-
+
+
+
+ card-image +
+

우리는 카카오워크로 일해요

+
+ # 디자인 + # 진로
+
+
); }; -export default RecommendationModal; \ No newline at end of file +export default RecommendationModal; diff --git a/src/constants/user.ts b/src/constants/user.ts new file mode 100644 index 0000000..0049627 --- /dev/null +++ b/src/constants/user.ts @@ -0,0 +1,5 @@ +export const GENDER_TYPE_LIST = [ + { id: '', name: '미표기' }, + { id: 'male', name: '남자' }, + { id: 'female', name: '여자' }, +]; diff --git a/src/hooks/useFocus.ts b/src/hooks/useFocus.ts new file mode 100644 index 0000000..0c54434 --- /dev/null +++ b/src/hooks/useFocus.ts @@ -0,0 +1,31 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +const useFocus = () => { + const ref = useRef(null); + const [isFocus, setIsFocus] = useState(false); + + const focus = useCallback(() => { + if (ref.current) { + ref.current.focus(); + } + }, [ref]); + + const handleFocus = () => setIsFocus(true); + const handleBlur = () => setIsFocus(false); + + useEffect(() => { + const element = ref.current; + + element?.addEventListener('focus', handleFocus); + element?.addEventListener('blur', handleBlur); + + return () => { + element?.removeEventListener('focus', handleFocus); + element?.removeEventListener('blur', handleBlur); + }; + }, [ref]); + + return [ref, focus, isFocus] as const; +}; + +export default useFocus; diff --git a/src/models/user.ts b/src/models/user.ts index 49a1cac..29f0670 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -11,23 +11,31 @@ export interface CheckEmailRequest { email: string; } -export interface CheckEmailResponse { -} +export interface CheckEmailResponse {} export interface JoinRequest { name: string; email: string; password: string; - check_password: string; + check_password: string; birth_date: string; gender: string; phone_number: string; } -export interface JoinResponse { -} +export interface JoinResponse {} +export interface MyInfoResponse { + birth_date: string; + email: string; + gender: string; + name: string; + nick_name: string; + phone_number: string; + platform?: 'kakao' | 'naver'; +} -export interface getNicknameResponse { - nickname: string; -} \ No newline at end of file +export interface UpdateMyInfoRequest { + nick_name: string; + gender: string; +} diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index e2fe02b..2137f13 100644 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -1,8 +1,14 @@ +import { useRecoilValue } from 'recoil'; + import { Account, ServiceSetting } from '@/components/ProfilePage'; +import { userInfoState } from '@/stores/user'; + import { Wrapper } from '@/styles/ProfilePage'; const ProfilePage = () => { + const userInfo = useRecoilValue(userInfoState); + return (
@@ -12,9 +18,13 @@ const ProfilePage = () => { 여기서 계정 정보를 관리하세요
- + {userInfo && ( + <> + - + + + )}
diff --git a/src/stores/user.ts b/src/stores/user.ts index d48fce4..ebe3381 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -1,5 +1,7 @@ import { atom } from 'recoil'; +import { MyInfoResponse } from '@/models/user'; + import localStorageEffect from './effects/localStorageEffect'; export const userState = atom({ @@ -7,6 +9,11 @@ export const userState = atom({ default: true, }); +export const userInfoState = atom({ + key: 'user-info', + default: null, +}); + export const userTokenState = atom({ key: 'user-token', default: null, diff --git a/src/styles/ProfilePage.ts b/src/styles/ProfilePage.ts index bfc4b09..6f5a5ac 100644 --- a/src/styles/ProfilePage.ts +++ b/src/styles/ProfilePage.ts @@ -41,6 +41,13 @@ export const Wrapper = styled.div` } } + & .submit-tooltip { + position: absolute; + top: 0; + left: 50%; + transform: translate(-50%, calc(-200% + 25px)); + } + & button.submit { display: flex; align-items: center; @@ -55,7 +62,7 @@ export const Wrapper = styled.div` cursor: pointer; ${(props) => props.theme.typography.Body1}; - &.disabled { + &:disabled { background-color: ${(props) => props.theme.color.gray100}; color: ${(props) => props.theme.color.gray300}; cursor: not-allowed; @@ -99,6 +106,7 @@ export const Box = styled.div` min-height: 50px; border-radius: 12px; border: solid 1.5px ${(props) => props.theme.color.gray200}; + transition: 0.2s; ${(props) => props.theme.typography.Body1}; &.disabled { @@ -122,6 +130,12 @@ export const Box = styled.div` ${(props) => props.theme.typography.Caption1}; } + & .input-error-text { + padding-left: 16px; + color: ${(props) => props.theme.color.red}; + ${(props) => props.theme.typography.Body3}; + } + & button.option { width: 100%; height: 50px; @@ -129,12 +143,12 @@ export const Box = styled.div` border: solid 1.5px ${(props) => props.theme.color.gray200}; background-color: white; color: ${(props) => props.theme.color.gray400}; - transition: 0.1s; + transition: 0.15s; cursor: pointer; ${(props) => props.theme.typography.Body1}; &.selected { - border: none; + border: solid 1.5px ${(props) => props.theme.color.gray100}; background-color: ${(props) => props.theme.color.gray100}; color: ${(props) => props.theme.color.gray500}; } diff --git a/src/utils/validation.ts b/src/utils/validation.ts new file mode 100644 index 0000000..f59dd6c --- /dev/null +++ b/src/utils/validation.ts @@ -0,0 +1,2 @@ +export const validateNickname = (nickname: string) => + !/^[a-zA-Z가-힣\s]{1,7}$/g.test(nickname); From e0ff4e97e2c48648cce47b5d49c009ff44af098d Mon Sep 17 00:00:00 2001 From: whistleJs Date: Sun, 11 Feb 2024 01:44:07 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feature-058:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EB=AA=A8=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChangePassword/ChangePasswordModal.tsx | 4 +- .../ProfilePage/LogoutModal/LogoutModal.tsx | 50 +++++++++++++ .../ProfilePage/LogoutModal/index.ts | 1 + src/components/ProfilePage/index.ts | 1 + src/components/common/Modal/Modal.tsx | 30 ++++++++ src/components/common/Modal/index.ts | 1 + src/components/common/index.ts | 1 + src/pages/ProfilePage.tsx | 50 +++++++------ src/styles/ProfilePage.ts | 71 +++++++++++++++++++ 9 files changed, 188 insertions(+), 21 deletions(-) create mode 100644 src/components/ProfilePage/LogoutModal/LogoutModal.tsx create mode 100644 src/components/ProfilePage/LogoutModal/index.ts create mode 100644 src/components/common/Modal/Modal.tsx create mode 100644 src/components/common/Modal/index.ts diff --git a/src/components/ProfilePage/Account/ChangePassword/ChangePasswordModal.tsx b/src/components/ProfilePage/Account/ChangePassword/ChangePasswordModal.tsx index 141169b..9d09526 100644 --- a/src/components/ProfilePage/Account/ChangePassword/ChangePasswordModal.tsx +++ b/src/components/ProfilePage/Account/ChangePassword/ChangePasswordModal.tsx @@ -1,9 +1,11 @@ +import { Modal } from '@/components/common'; + type Props = { onClose: () => void; }; const ChangePasswordModal = ({ onClose }: Props) => { - return
; + return asdf; }; export default ChangePasswordModal; diff --git a/src/components/ProfilePage/LogoutModal/LogoutModal.tsx b/src/components/ProfilePage/LogoutModal/LogoutModal.tsx new file mode 100644 index 0000000..e3ff822 --- /dev/null +++ b/src/components/ProfilePage/LogoutModal/LogoutModal.tsx @@ -0,0 +1,50 @@ +import { useNavigate } from 'react-router-dom'; +import { useSetRecoilState } from 'recoil'; + +import CloseIcon from '@/assets/icons/close.svg?react'; +import AfterLoginImage from '@/assets/after-login.png'; + +import { Modal } from '@/components/common'; + +import { ModalBox } from '@/styles/ProfilePage'; +import { userInfoState, userTokenState } from '@/stores/user'; + +type Props = { + onClose: () => void; +}; + +const LogoutModal = ({ onClose }: Props) => { + const navigate = useNavigate(); + const setUserInfo = useSetRecoilState(userInfoState); + const setUserToken = useSetRecoilState(userTokenState); + + const handleClickLogoutButton = () => { + setUserInfo(null); + setUserToken(null); + navigate('/'); + }; + + return ( + + +
+
+ +
+ +
+ image + +

로그아웃 하시겠어요?

+ + 다시 돌아오길 기다릴게요 +
+
+ + +
+
+ ); +}; + +export default LogoutModal; diff --git a/src/components/ProfilePage/LogoutModal/index.ts b/src/components/ProfilePage/LogoutModal/index.ts new file mode 100644 index 0000000..fe477cc --- /dev/null +++ b/src/components/ProfilePage/LogoutModal/index.ts @@ -0,0 +1 @@ +export { default as LogoutModal } from './LogoutModal'; diff --git a/src/components/ProfilePage/index.ts b/src/components/ProfilePage/index.ts index c6d5328..6e4a135 100644 --- a/src/components/ProfilePage/index.ts +++ b/src/components/ProfilePage/index.ts @@ -1,2 +1,3 @@ export { Account } from './Account'; +export { LogoutModal } from './LogoutModal'; export { ServiceSetting } from './ServiceSetting'; diff --git a/src/components/common/Modal/Modal.tsx b/src/components/common/Modal/Modal.tsx new file mode 100644 index 0000000..852d4b9 --- /dev/null +++ b/src/components/common/Modal/Modal.tsx @@ -0,0 +1,30 @@ +import { BlurBackground } from '@/styles/modals/common.style'; +import { useCallback, useEffect } from 'react'; + +type Props = { + children?: JSX.Element | JSX.Element[] | string; + onClose: () => void; +}; + +const Modal = ({ children, onClose }: Props) => { + const handleKeydown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + } + }, + [onClose], + ); + + useEffect(() => { + window.addEventListener('keydown', handleKeydown); + + return () => { + window.removeEventListener('keydown', handleKeydown); + }; + }, [handleKeydown]); + + return {children}; +}; + +export default Modal; diff --git a/src/components/common/Modal/index.ts b/src/components/common/Modal/index.ts new file mode 100644 index 0000000..c6b3568 --- /dev/null +++ b/src/components/common/Modal/index.ts @@ -0,0 +1 @@ +export { default as Modal } from './Modal'; diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 1f9ca7c..b14b0f9 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -1,2 +1,3 @@ +export { Modal } from './Modal'; export { Tooltip } from './Tooltip'; export { ToastList } from './ToastList'; diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index 2137f13..25e39f8 100644 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -1,6 +1,8 @@ import { useRecoilValue } from 'recoil'; -import { Account, ServiceSetting } from '@/components/ProfilePage'; +import { Account, LogoutModal, ServiceSetting } from '@/components/ProfilePage'; + +import useBoolean from '@/hooks/useBoolean'; import { userInfoState } from '@/stores/user'; @@ -8,31 +10,39 @@ import { Wrapper } from '@/styles/ProfilePage'; const ProfilePage = () => { const userInfo = useRecoilValue(userInfoState); + const [isShowLogoutModal, , openLogoutModal, closeLogoutModal] = + useBoolean(false); return ( - -
-
-
-

내 정보

- 여기서 계정 정보를 관리하세요 + <> + +
+
+
+

내 정보

+ 여기서 계정 정보를 관리하세요 +
+ + {userInfo && ( + <> + + + + + )}
- {userInfo && ( - <> - - - - - )} +
+ + +
+
-
- - -
-
- + {isShowLogoutModal && } + ); }; diff --git a/src/styles/ProfilePage.ts b/src/styles/ProfilePage.ts index 6f5a5ac..4fce258 100644 --- a/src/styles/ProfilePage.ts +++ b/src/styles/ProfilePage.ts @@ -37,7 +37,17 @@ export const Wrapper = styled.div` background-color: white; color: ${(props) => props.theme.color.gray400}; cursor: pointer; + outline: none; + transition: 0.1s; ${(props) => props.theme.typography.Body1}; + + &:hover { + border: 1.5px solid ${(props) => props.theme.color.gray400}; + } + + &:active { + border: 1.5px solid ${(props) => props.theme.color.gray300}; + } } } @@ -60,6 +70,7 @@ export const Wrapper = styled.div` color: white; transition: 0.1s; cursor: pointer; + outline: none; ${(props) => props.theme.typography.Body1}; &:disabled { @@ -145,6 +156,7 @@ export const Box = styled.div` color: ${(props) => props.theme.color.gray400}; transition: 0.15s; cursor: pointer; + outline: none; ${(props) => props.theme.typography.Body1}; &.selected { @@ -180,6 +192,7 @@ export const Box = styled.div` color: ${(props) => props.theme.color.gray300}; transition: 0.1s; cursor: pointer; + outline: none; ${(props) => props.theme.typography.Body2}; &.selected { @@ -193,3 +206,61 @@ export const Box = styled.div` } } `; + +export const ModalBox = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 48px; + padding: 40px 50px; + width: 700px; + background-color: white; + box-shadow: 0 4px 40px 0 rgba(0, 0, 0, 0.1); + border-radius: 40px; + + & > .box { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + + & > .close { + align-self: flex-end; + cursor: pointer; + } + + & > .content { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + + & img { + width: 56px; + height: auto; + } + + & h1.title { + color: ${(props) => props.theme.color.gray500}; + ${(props) => props.theme.typography.Header6}; + } + + & span.description { + color: ${(props) => props.theme.color.gray300}; + ${(props) => props.theme.typography.Body1}; + } + } + } + + & > button { + width: 100%; + height: 58px; + background-color: ${(props) => props.theme.color.gray500}; + border: none; + border-radius: 12px; + color: white; + cursor: pointer; + outline: none; + ${(props) => props.theme.typography.Body1}; + } +`; From 0357614df42f146bd99b9158af721c3be27ffbd4 Mon Sep 17 00:00:00 2001 From: whistleJs Date: Sun, 11 Feb 2024 09:22:31 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feature-058:=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/modals/RecommendationModal.tsx | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/components/modals/RecommendationModal.tsx b/src/components/modals/RecommendationModal.tsx index 6233b53..d684f7b 100644 --- a/src/components/modals/RecommendationModal.tsx +++ b/src/components/modals/RecommendationModal.tsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect } from 'react'; -import { useRecoilState } from 'recoil'; +import React from 'react'; +import { useRecoilState, useRecoilValue } from 'recoil'; import { recommendationModalState } from '@/stores/modal'; import useOutsideClick from '@/hooks/useOutsideClick'; @@ -7,11 +7,12 @@ import emptyvideoImg from '@/assets/empty-video.png'; import CloseIcon from '@/assets/icons/close.svg?react'; import cardimageImg from '@/assets/card-image.png'; +import { userInfoState } from '@/stores/user'; + import { RecommendationModalContainer } from '@/styles/modals/RecommendationModal.style'; -import { getMyInfoAPI } from '@/apis/user'; const RecommendationModal: React.FC = () => { - const [nickname, setNickname] = useState(''); + const userInfo = useRecoilValue(userInfoState); const [modalOpen, setModalOpen] = useRecoilState(recommendationModalState); const closeModal = () => { @@ -20,15 +21,6 @@ const RecommendationModal: React.FC = () => { const [modalRef] = useOutsideClick(closeModal); - useEffect(() => { - async function fetchNickname() { - const response = await getMyInfoAPI(); - setNickname(response.data.result.nickname); - } - - fetchNickname(); - }, []); - return (
@@ -48,7 +40,8 @@ const RecommendationModal: React.FC = () => { 기다리는 동안 이런 영상은 어때요?
- {nickname}님을 위해 미리 정리 된 영상을 소개해드릴게요 + {userInfo?.nick_name}님을 위해 미리 정리 된 영상을 + 소개해드릴게요