Skip to content

Commit

Permalink
Merge pull request #53 from teamViNO/feature-058
Browse files Browse the repository at this point in the history
Feature 058
  • Loading branch information
whistleJs authored Feb 11, 2024
2 parents 1758620 + 0357614 commit 2c70b56
Show file tree
Hide file tree
Showing 27 changed files with 666 additions and 122 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 6 additions & 1 deletion src/apis/config/instance.ts
Original file line number Diff line number Diff line change
@@ -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);

Expand Down
17 changes: 13 additions & 4 deletions src/apis/user.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { APIResponse } from '@/models/config/axios';
import { APIBaseResponse, APIResponse } from '@/models/config/axios';
import {
CheckEmailRequest,
CheckEmailResponse,
JoinRequest,
JoinResponse,
LoginRequest,
LoginResponse,
MyInfoResponse,
UpdateMyInfoRequest,
} from '@/models/user';
import {
AlarmResponse,
ConfirmAlarmRequest,
DeleteAlarmRequest,
DeleteAlarmResponse,
} from '@/models/alarm';
import { getNicknameResponse } from '@/models/user';
import axios from './config/instance';

const PREFIX = '/user';
Expand Down Expand Up @@ -50,6 +51,14 @@ export const joinAPI = (data: JoinRequest) => {
return axios.post<APIResponse<JoinResponse>>(PREFIX + '/join', data);
};

export const getNicknameAPI = () => {
return axios.get<APIResponse<getNicknameResponse>>(PREFIX + '/myPage/myInfo');
export const socialAccountAPI = (code: string) => {
return axios.get(`/sign-up/success?code=${code}`);
};

export const getMyInfoAPI = () => {
return axios.get<APIResponse<MyInfoResponse>>(PREFIX + '/myPage/myInfo');
};

export const updateMyInfoAPI = (data: UpdateMyInfoRequest) => {
return axios.put<APIBaseResponse>(PREFIX + '/myPage/setInfo', data);
};
200 changes: 172 additions & 28 deletions src/components/ProfilePage/Account/Account.tsx
Original file line number Diff line number Diff line change
@@ -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<NodeJS.Timeout>();
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<HTMLInputElement>();

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<HTMLInputElement> = ({
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 (
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<h2 className="title">계정</h2>

<button className="submit disabled">변경하기</button>
<div style={{ position: 'relative' }}>
{isShowTooltip && (
<div className="submit-tooltip">
<Tooltip direction="down">수정이 완료되었어요!</Tooltip>
</div>
)}

<button
className="submit"
disabled={isDisabled}
onClick={handleClickSubmitButton}
>
변경하기
</button>
</div>
</div>

<Box>
<div
className="account-group"
style={{ flexDirection: 'row', alignItems: 'center', gap: 20 }}
style={{ flexDirection: 'row', gap: 20 }}
>
<div className="avatar"></div>

Expand All @@ -30,50 +148,86 @@ const Account = () => {
>
<span className="group-title">닉네임</span>

<div className="input-box">
<div
className="input-box"
style={nicknameInputStyle}
onClick={focusNicknameInput}
>
<div style={{ display: 'flex', flex: '1 1 auto' }}>
<input type="text" value="여울" />
<input
ref={nicknameInputRef}
type="text"
value={nickname}
onChange={handleChangeNickname}
/>
</div>

<span className="input-guide">3/7 (공백포함)</span>
<span className="input-guide">
{nickname.length}/7 (공백포함)
</span>
</div>

{isErrorNickname && (
<span className="input-error-text">
닉네임 형식이 올바르지 않아요!
</span>
)}
{isDuplicateNickname && (
<span className="input-error-text">
이미 존재하는 닉네임이에요!
</span>
)}
</div>
</div>

<div className="account-group">
<span className="group-title">이름</span>

<div className="input-box disabled">
<div style={{ display: 'flex', flex: '1 1 auto' }}>서예진</div>
<div style={{ display: 'flex', flex: '1 1 auto' }}>
{userInfo?.name}
</div>

<span className="input-guide">3/7 (공백포함)</span>
<span className="input-guide">
{userInfo?.name.length}/7 (공백포함)
</span>
</div>
</div>

<div className="account-group">
<span className="group-title">성별</span>

<div style={{ display: 'flex', gap: 12 }}>
<button className="option">미표기</button>
<button className="option">남자</button>
<button className="option selected">여자</button>
{GENDER_TYPE_LIST.map((genderType) => (
<button
key={'gender-' + genderType.id}
className={`option ${genderType.id === gender && 'selected'}`}
onClick={() => setGender(genderType.id)}
>
{genderType.name}
</button>
))}
</div>
</div>

<div className="account-group">
<span className="group-title">생년월일</span>

<div style={{ display: 'flex', gap: 12 }}>
<div className="input-box disabled">2001</div>
<div className="input-box disabled">07</div>
<div className="input-box disabled">01</div>
<div className="input-box disabled">{birthDate.getFullYear()}</div>
<div className="input-box disabled">
{String(birthDate.getMonth() + 1).padStart(2, '0')}
</div>
<div className="input-box disabled">
{String(birthDate.getDate()).padStart(2, '0')}
</div>
</div>
</div>

<div className="account-group">
<span className="group-title">전화번호</span>

<div className="input-box disabled">010901716171</div>
<div className="input-box disabled">{userInfo?.phone_number}</div>
</div>

{isSocialAccount && (
Expand All @@ -86,7 +240,7 @@ const Account = () => {
>
<img src={NaverIconImage} width={40} />

<span>[email protected]</span>
<span>{userInfo?.email}</span>
</div>
</div>
)}
Expand All @@ -97,20 +251,10 @@ const Account = () => {
<div className="account-group">
<span className="group-title">계정</span>

<div className="input-box disabled">[email protected]</div>
<div className="input-box disabled">{userInfo?.email}</div>
</div>

<div className="account-group">
<span className="group-title">비밀번호</span>

<div className="input-box" style={{ padding: '5px 4px 5px 16px' }}>
<div style={{ display: 'flex', flex: '1 1 auto' }}>
<input type="password" value="123456" />
</div>

<button className="submit disabled">변경하기</button>
</div>
</div>
<ChangePassword />
</Box>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import useBoolean from '@/hooks/useBoolean';

import ChangePasswordModal from './ChangePasswordModal';

const ChangePassword = () => {
const [isOpen, , open, close] = useBoolean(false);

return (
<>
<div
className="account-group"
style={{ flexDirection: 'row', justifyContent: 'space-between' }}
>
<span className="group-title">비밀번호</span>

<button className="submit" onClick={open}>
변경하기
</button>
</div>

{isOpen && <ChangePasswordModal onClose={close} />}
</>
);
};

export default ChangePassword;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Modal } from '@/components/common';

type Props = {
onClose: () => void;
};

const ChangePasswordModal = ({ onClose }: Props) => {
return <Modal onClose={onClose}>asdf</Modal>;
};

export default ChangePasswordModal;
1 change: 1 addition & 0 deletions src/components/ProfilePage/Account/ChangePassword/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as ChangePassword } from './ChangePassword';
Loading

0 comments on commit 2c70b56

Please sign in to comment.