Skip to content

Commit

Permalink
Merge pull request #672 from woowacourse-teams/feature/#664
Browse files Browse the repository at this point in the history
QA에서 나왔던 닉네임 validate 추가
  • Loading branch information
jaeml06 authored Oct 17, 2024
2 parents 3774daf + 061f8f8 commit b3b6002
Show file tree
Hide file tree
Showing 15 changed files with 218 additions and 122 deletions.
2 changes: 1 addition & 1 deletion frontend/src/apis/deletes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ export const deleteCancelChamyo = async (moimId: number) => {
};

export const deleteMyInfo = async () => {
await ApiClient.deleteWithLastDarakbangId(`/member/delete`);
await ApiClient.deleteWithLastDarakbangId(`/auth`);
};
9 changes: 9 additions & 0 deletions frontend/src/common/assets/default_profile.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 0 additions & 4 deletions frontend/src/common/assets/empty_profile.svg

This file was deleted.

12 changes: 7 additions & 5 deletions frontend/src/components/Input/MoimInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useTheme } from '@emotion/react';

export interface LabeledInputProps<T extends string | number>
extends HTMLProps<HTMLInputElement> {
title: string;
title?: string;
validateFun?: (value: T) => boolean;
}

Expand Down Expand Up @@ -49,10 +49,12 @@ export default function LabeledInput<T extends string | number>(

return (
<label htmlFor={title} css={S.labelWrapper}>
<h3 css={S.title({ theme })}>
{title}
<span css={S.required({ theme })}>{required ? '*' : ''}</span>
</h3>
{title && (
<h3 css={S.title({ theme })}>
{title}
<span css={S.required({ theme })}>{required ? '*' : ''}</span>
</h3>
)}

<input
name={name}
Expand Down
19 changes: 15 additions & 4 deletions frontend/src/components/ProfileFrame/ProfileFrame.style.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { css } from '@emotion/react';
import EmptyProfile from '@_common/assets/empty_profile.svg?url';
import { css, Theme } from '@emotion/react';
import EmptyProfile from '@_common/assets/default_profile.svg?url';
type Size = number;

export const profileBox = () => {
Expand All @@ -8,7 +8,17 @@ export const profileBox = () => {
`;
};

export const profileFrame = (width: Size, height: Size, borderWidth: Size) => {
export const profileFrame = ({
width,
height,
borderWidth,
theme,
}: {
width: Size;
height: Size;
borderWidth: Size;
theme: Theme;
}) => {
return css`
overflow: hidden;
display: flex;
Expand All @@ -17,8 +27,9 @@ export const profileFrame = (width: Size, height: Size, borderWidth: Size) => {
width: ${width}rem;
height: ${height}rem;
background: ${theme.colorPalette.white[100]};
border: ${borderWidth}rem solid orange;
border: ${borderWidth}rem solid ${theme.colorPalette.orange[200]};
border-radius: 300rem;
`;
};
Expand Down
12 changes: 7 additions & 5 deletions frontend/src/components/ProfileFrame/ProfileFrame.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as S from './ProfileFrame.style';

import Crown from '@_common/assets/crown.svg?url';
import EmptyProfile from '@_common/assets/empty_profile.svg?url';
import DefaultProfile from '@_common/assets/default_profile.svg?url';
import { ImgHTMLAttributes, useState } from 'react';
import { Role } from '@_types/index';
import { useTheme } from '@emotion/react';

interface ProfileFrameProps extends ImgHTMLAttributes<HTMLImageElement> {
role?: Role;
Expand All @@ -22,26 +23,27 @@ export default function ProfileFrame(props: ProfileFrameProps) {
src,
...args
} = props;
const theme = useTheme();
const [isLoaded, setIsLoaded] = useState(false);
const handleError = (
event: React.SyntheticEvent<HTMLImageElement, Event>,
) => {
if (onError) {
onError(event);
}
event.currentTarget.src = EmptyProfile;
event.currentTarget.src = DefaultProfile;
};

return (
<div css={S.profileBox()}>
{role === 'MOIMER' ? <img src={Crown} css={S.profileCrown(width)} /> : ''}
<div css={S.profileFrame(width, height, borderWidth)}>
<div css={S.profileFrame({ width, height, borderWidth, theme })}>
{!isLoaded && (
<img src={EmptyProfile} css={S.profileImage()} alt="Placeholder" />
<img src={DefaultProfile} css={S.profileImage()} alt="Placeholder" />
)}
<img
css={S.profileImage({ isLoaded })}
src={src || EmptyProfile}
src={src || DefaultProfile}
onLoad={() => setIsLoaded(true)}
{...args}
onError={handleError}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/UserPreview/UserPreview.style.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { css } from '@emotion/react';
import defaultProfile from '@_common/assets/empty_profile.svg?url';
import defaultProfile from '@_common/assets/default_profile.svg?url';

export const preview = ({
imageUrl,
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/constants/poclies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const POLICES = {

maxDarakbangName: 40,

maxNicknameLength: 10,
minNicknameLength: 1,
maxNicknameLength: 12,

entranceCodeLength: 7,
};
Expand Down
19 changes: 13 additions & 6 deletions frontend/src/pages/Mypage/MyPage.style.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { css, Theme } from '@emotion/react';

export const AccountButton = (props: { theme: Theme }) => css`
${props.theme.typography.b2}
export const AccountButton = ({
theme,
isValidMyInfo = true,
}: {
theme: Theme;
isValidMyInfo?: boolean;
}) => css`
${theme.typography.b2}
color: ${isValidMyInfo ? '' : theme.colorPalette.grey[200]};
background: none;
border: none;
`;
Expand All @@ -13,7 +20,7 @@ export const mainContainer = () => css`
align-items: center;
`;

export const editButton = (props: { theme: Theme }) => css`
export const editButton = ({ theme }: { theme: Theme }) => css`
display: flex;
gap: 1rem;
align-items: center;
Expand All @@ -22,13 +29,13 @@ export const editButton = (props: { theme: Theme }) => css`
width: 25rem;
padding: 1.6rem 5.9rem;
color: ${props.theme.colorPalette.white[100]};
color: ${theme.colorPalette.white[100]};
background-color: ${props.theme.semantic.primary};
background-color: ${theme.semantic.primary};
border: none;
border-radius: 3rem;
&:active {
background-color: ${props.theme.colorPalette.orange[900]};
background-color: ${theme.colorPalette.orange[900]};
}
`;
120 changes: 29 additions & 91 deletions frontend/src/pages/Mypage/MyPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// import { useRef } from 'react';
import { css, useTheme } from '@emotion/react';
import InformationLayout from '@_layouts/InformationLayout/InformationLayout';
import MineInfoCard from './components/MineInfoCard/MineInfoCard';
Expand All @@ -10,99 +9,33 @@ import * as S from './MyPage.style';
import MyInfoTabBar from './components/MyInfoTabBar/MyInfoTabBar';
import Setting from '@_common/assets/setting.svg';
import Edit from '@_common/assets/edit.svg';
import { ChangeEvent, Fragment, useEffect, useRef, useState } from 'react'; // ChangeEvent를 가져옴
import useEditMyInfo from '@_hooks/mutaions/useEditMyInfo';
import { Fragment } from 'react';
import { useNavigate } from 'react-router-dom';
import GET_ROUTES from '@_common/getRoutes';
import useMyPage from './hook/useMyPage';

export default function MyPage() {
const navigate = useNavigate();
const { myInfo } = useMyInfo();
const fileInput = useRef<HTMLInputElement | null>(null); // 타입을 명시적으로 지정
const [profile, setProfile] = useState(myInfo?.profile || '');
const [nickname, setNickname] = useState(myInfo?.nickname || '');
const [description, setDescription] = useState(myInfo?.description || '');
const [selectedFile, setSelectedFile] = useState<File | string>('');
const [isEditing, setIsEditing] = useState(false); // 편집 모드 상태
const [isReset, setIsReset] = useState('false');
const [isShownRest, setIsShownRest] = useState(false);

const theme = useTheme();
const { mutate } = useEditMyInfo();

useEffect(() => {
if (myInfo) {
setNickname(myInfo.nickname || '');
setDescription(myInfo.description || '');
setProfile(myInfo.profile || '');
setSelectedFile(myInfo.profile || '');
myInfo.profile && setIsShownRest(true);
}
}, [myInfo]); // myInfo가 업데이트될 때마다 상태 업데이트

const handleEditClick = () => {
setIsEditing((prev) => !prev); // 편집 모드 활성화
setSelectedFile('');
};

const onChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setSelectedFile(e.target.files[0]); // 선택한 파일을 상태에 저장
setIsShownRest(true);
setIsReset('false');
} else {
setProfile(myInfo?.profile ?? '');
return;
}

const reader = new FileReader();
reader.onload = () => {
if (reader.readyState === 2 && typeof reader.result === 'string') {
setProfile(reader.result);
}
};
reader.readAsDataURL(e.target.files[0]);
};

const onUpload = async () => {
const formData = new FormData();

// 파일 추가

formData.append('file', selectedFile);
// 문자열 데이터 추가
formData.append('nickname', nickname ?? '');
formData.append('description', description ?? '');
formData.append('isReset', isReset);

try {
// 서버로 파일 및 데이터 전송
mutate(formData); // FormData 객체 자체를 전달
handleEditClick(); // 편집 모드 비활성화
} catch (error) {
console.error('파일 업로드 실패', error);
}
};

const handleProfileClick = () => {
fileInput.current?.click(); // ProfileFrame 클릭 시 파일 선택창 열기
};

const handleCancel = () => {
// 편집 취소시 기존 데이터 복구
setProfile(myInfo?.profile || '');
setNickname(myInfo?.nickname || '');
setDescription(myInfo?.description || '');
setIsEditing(false);
setIsReset('false');
myInfo?.profile && setIsShownRest(false);
};
const handleDefaultProfile = () => {
setProfile('');
setSelectedFile('');
setIsReset('true');
setIsShownRest(false);
};
const {
myInfo,
fileInput,
profile,
nickname,
description,
isEditing,
isShownRest,
isValidMyInfo,
setNickname,
setDescription,
handleEditClick,
onChange,
onUpload,
handleProfileClick,
handleCancel,
handleDefaultProfile,
} = useMyPage();

return (
<Fragment>
Expand Down Expand Up @@ -139,9 +72,14 @@ export default function MyPage() {
기본이미지로 변경
</button>
)}
<button css={S.AccountButton({ theme })} onClick={onUpload}>
저장
</button>
{isValidMyInfo && (
<button
css={S.AccountButton({ theme })}
onClick={() => onUpload()}
>
저장
</button>
)}
<button css={S.AccountButton({ theme })} onClick={handleCancel}>
취소
</button>
Expand All @@ -155,7 +93,7 @@ export default function MyPage() {
<MineInfoCard
myInfo={{
nickname,
profile: profile,
profile,
name: myInfo.name,
}}
onProfileClick={handleProfileClick}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,4 @@ export const editSVG = () => css`

export const input = (props: { theme: Theme }) => css`
${props.theme.typography.h5}
max-width: 60%;
`;
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import * as S from './MineInfoCard.style';
import ProfileFrame from '@_components/ProfileFrame/ProfileFrame';
import { useTheme } from '@emotion/react';
import Edit from '@_common/assets/edit.svg?url';
import LabeledInput from '@_components/Input/MoimInput';
import { ChangeEvent } from 'react';
import { validateNickName } from '@_pages/Mypage/validate';

interface MineInfoCardProps {
myInfo: {
Expand Down Expand Up @@ -33,10 +36,14 @@ export default function MineInfoCard({
{isEditing && <img src={Edit} alt="Edit" css={S.editSVG} />}
</div>
{isEditing ? (
<input
<LabeledInput
css={S.input({ theme })}
value={nickname}
onChange={(e) => setNickname(e.target.value)}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setNickname(e.target.value)
}
placeholder="닉네임을 1자에서 12자이하로 지어주세요"
validateFun={validateNickName}
/>
) : (
<span css={theme.typography.h5}>{nickname}</span>
Expand Down
Loading

0 comments on commit b3b6002

Please sign in to comment.