Skip to content

Commit

Permalink
FE-34 ✨ 마이페이지 프로필 수정 반응형 구현 및 토스트 메세지 출력 (#33)
Browse files Browse the repository at this point in the history
* .nvmrc 버전 수정

* 폰트 및 공용컬러 추가 (#6)

* font-family 추가

* tailwind common color 추가

* color 명 변경

* lang 수정

---------

Co-authored-by: 전유민 <[email protected]>

* 💄 공용 컴포넌트 shadcn ui 추가 (#7)

* 💄 Feat: shadcn-ui init

* 💄 Feat: add toast ui

* Feat: add textarea ui

* Feat: add switch ui

* Feat: add radio-group ui

* Feat: add label ui

* Feat: add input ui

* Feat: add form ui

* Feat: add button ui

* Feat: add dropdown-menu ui

* Feat: add card ui

* Feat: add badge ui

* Feat: add avatar ui

* Feat: add alert dialog ui

* Chore: add eslint rules

* Chore: add shadcn ui

* FE-48 📰 공용 컴포넌트 face emoji svg 파일 생성

* FE-48 🎨 감정 이모티콘 폴더 구조 변경

* FE-48 ✨ 감정 이모티콘 카드 컴포넌트 ui 생성

* FE-48 ✨ 감정 이모티콘 상태에 따른 클래스 설정

* FE-48 💄 감정 이모티콘 카드 컴포넌트 ui 수정

* FE-48 ✨ 감정 이모티콘 카드 클릭 이벤트 구현

- EmotionIconCardContainer를 사용해 상태관리와 이벤트 처리 (Clicked<->UnClicked)

* FE-48 📝 컴포넌트 이름 변경

명확한 의미 전달을 위해 컴포넌트 이름 변경

* FE-48 ✨ 감정 이모티콘 상태 변화 동기화 구

감정 카드를 클릭할 때 상태가 올바르게 전환되고, 다른 카드의 상태도 동기화되는 기능 구현

* FE-48 ✨ EmotionSelector 컴포넌트 동적 크기 변경 구현

useMediaQuery 훅 생성: 화면의 크기가 변경될 때마다 리스너 추가 및 제거

* FE-48 🔥 출력 확인을 위한 테스트 컴포넌트 삭제

* FE-48 🔨 EmotionTypes 인터페이스 정의

emotion 관련 컴포넌트에서 해당 인터페이스를 import하여 사용하게 구현

* FE-59 ✨ 에피그램 카드 ui 구현

tailwind css를 확장해 줄무늬 배경 이미지 구현

* FE-59 ✨ 에피그램 카드 반응현 디자인 구현

* FE-59 💄 에피그램 카드 글씨체 적용

* FE-59 🔥 에피그램 카드 테스트 코드 삭제

* FE-59 🔥 테스트 흔적 삭제

* FE-58 ✨ 공용 컴포넌트 댓글 카드 기본 ui 구현

* FE-34 ✨ 유저 프로필 컴포넌트 분리
- Profile.tsx 파일로 유저 프로필 부분 분리
- 파일(이미지) 선택 기능 구현(api 연동x)
- 등록 된 이미지가 없다면 샘플이미지 출력

* FE-34 ✨ 이미지 업로드 presignedUrl 생성 api 연동
- 현재 로그인 인증 토큰이 없어 401 에러가 뜸

* FE-58 💄 공용 컴포넌트 반응형 디자인 적용

* FE-58 🔥 댓글 카드 테스트 코드 삭제

* FE-58 👄 댓글 카드 관련 인터페이스, 스타일 분리

* FE-50 ✨공용컴포넌트 헤더 구현 (#19)

* FE-5050✨ feat:  헤더 부분 기능 초안

* FE-50 ✨styles: 주석 추가

* FE-50 ✨styles: 주석 추추가

* FE-5050 ✨test: 테스트 코드

* FE-50 ✨fix: 테스트 코드 삭제

* FE-50 ✨feat: 공유 이미지 추가 및 현재 URL 복사 기능 추가

* FE-50 ✨styles: U셋 중 하나가 빠지더라도 안무너지게 UI 수정

* FE-50 ✨comment:  주석 수정 및 추가

* FE-50 ✨fix: 테스트 코드 삭제

* FE-50 ✨fix:  함수명 컨벤션에 맞게 변경

* FE-50 ✨fix: types 폴더에 interface 정의

* FE-50 fix: build 오류 수정

* FE-34 ✨ 프로필 수정 모달 ui

* FE-34 🔀 main branch merge

* FE-34✨ 이미지 파일 미리보기 기능 구현

* FE-34✨ 텍스트 입력 함수 추가

* FE-34✨ formik으로 회원정보 변경 로직 수정

* FE-34✨ presigned url 생성 api 연동

* FE-34 🔨 next.config 파일 s3  url 추가

* FE-34 ✨ 프로필 수정 유효성 검사 추가
- 파일 이름 'profile' + 파일업로드 날짜로 변경
- 닉네임 1~30자 일때만 입력 가능하도록 설정

* FE-34 ✨ 프로필 수정 api 완료

* FE-34 🎨 pr 리뷰 수정

* FE-34 🔥 confilct 삭제

* FE-34 💄 shadcn/ui dialog 추

* FE-34 🎨 shadcn/ui dialog로 프로필 수정 기능 변경

* FE-34 ✨ 프로필 수정 완료 시 toast 메세지 구현

* FE-34 🐛 build test 오류 수정

---------

Co-authored-by: 전유민 <[email protected]>
Co-authored-by: MOON <[email protected]>
Co-authored-by: NEWJIN <[email protected]>
Co-authored-by: NEWJIN <[email protected]>
Co-authored-by: imsoohyeok <[email protected]>
  • Loading branch information
6 people authored Jul 16, 2024
1 parent 8ceec83 commit 555cc5f
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 51 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@lukemorales/query-key-factory": "^1.3.4",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-radio-group": "^1.2.0",
Expand Down
65 changes: 65 additions & 0 deletions src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';

import cn from '@/lib/utils';

const Dialog = DialogPrimitive.Root;

const DialogTrigger = DialogPrimitive.Trigger;

const DialogPortal = DialogPrimitive.Portal;

const DialogClose = DialogPrimitive.Close;

const DialogOverlay = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Overlay>, React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn('fixed inset-0 z-50 bg-background-100 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', className)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;

const DialogContent = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Content>, React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className='absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground'>
<X className='h-4 w-4' />
<span className='sr-only'>Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;

function DialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />;
}
DialogHeader.displayName = 'DialogHeader';

function DialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...props} />;
}
DialogFooter.displayName = 'DialogFooter';

const DialogTitle = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Title>, React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>>(({ className, ...props }, ref) => (
<DialogPrimitive.Title ref={ref} className={cn('text-lg font-semibold leading-none tracking-tight', className)} {...props} />
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;

const DialogDescription = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Description>, React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>>(({ className, ...props }, ref) => (
<DialogPrimitive.Description ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;

export { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription };
2 changes: 2 additions & 0 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ 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());
return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={pageProps.dehydratedState}>
<Component {...pageProps} />
<Toaster />
</HydrationBoundary>
<ReactQueryDevtools />
</QueryClientProvider>
Expand Down
46 changes: 23 additions & 23 deletions src/user/ui-profile/Profile.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
import Image from 'next/image';
import { UserProfileProps } from '@/types/user';
import { useState } from 'react';

import { Button } from '@/components/ui/button';
import { Dialog, DialogTrigger, DialogContent } from '@/components/ui/dialog';
import { sampleImage } from '../util/constants';
import ProfileEdit from './ProfileEdit';

export default function Profile({ image, nickname }: UserProfileProps) {
const [isModalOpen, setIsModalOpen] = useState(false);

// TODO: 여러개의 샘플 이미지 랜덤하게 뜨도록 추가 할 예정
const profileImage = image || sampleImage[1];

const handleProfileEditOpen = () => {
setIsModalOpen(true);
};

const handleProfileEditClose = () => {
setIsModalOpen(false);
};

// TODO: 여러개의 샘플 이미지 랜덤하게 뜨도록 추가 할 예정
const profileImage = image || sampleImage[1];

return (
<>
<div className='w-[130px] h-[240px] flex flex-col justify-center items-center absolute top-[-50px]'>
<div>
<div role='button' tabIndex={0} className='w-[120px] h-[120px] rounded-full overflow-hidden cursor-pointer'>
<Image src={profileImage} alt='유저 프로필' className='w-full h-full object-cover' width={120} height={120} priority />
</div>
</div>
<p className='mt-4 mb-6'>{nickname}</p>
<div className='w-[130px] h-12 pl-4 pr-3.5 py-1.5 bg-zinc-100 rounded-[100px] justify-center items-center gap-1.5 inline-flex'>
<button type='button' className="text-neutral-400 text-xl font-medium font-['Pretendard'] leading-loose" onClick={handleProfileEditOpen}>
프로필 수정
</button>
<div className='w-[130px] h-[240px] flex flex-col justify-center items-center absolute top-[-50px]'>
<div>
<div role='button' tabIndex={0} className='w-[120px] h-[120px] rounded-full overflow-hidden cursor-pointer'>
<Image src={profileImage} alt='유저 프로필' className='w-full h-full object-cover' width={120} height={120} priority />
</div>
</div>

{isModalOpen && <ProfileEdit initialValues={{ image: profileImage, nickname }} onModalClose={handleProfileEditClose} />}
</>
<p className='mt-4 mb-6'>{nickname}</p>
<div className='w-[130px] h-12 pl-4 pr-3.5 py-1.5 bg-zinc-100 rounded-[100px] justify-center items-center gap-1.5 inline-flex'>
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogTrigger asChild>
<Button variant='outline' className='border-none'>
프로필 수정
</Button>
</DialogTrigger>
<DialogContent className='sm:max-w-[425px] md:max-w-[1200px] bg-white' aria-describedby={undefined}>
<ProfileEdit initialValues={{ image: profileImage, nickname }} onModalClose={handleProfileEditClose} />
</DialogContent>
</Dialog>
</div>
</div>
);
}
64 changes: 36 additions & 28 deletions src/user/ui-profile/ProfileEdit.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import Image from 'next/image';
import { UserProfileProps } from '@/types/user';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import Label from '@/components/ui/label';
import { DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { useToast } from '@/components/ui/use-toast';
import { useEffect, useRef } from 'react';
import { useFormik } from 'formik';
import { Form, Formik, useFormik } from 'formik';
import { useCreatePresignedUrl, useUpdateMe } from '@/hooks/userQueryHooks';
import * as Yup from 'yup';
import X_ICON from '../../../public/icon/x-icon_md.svg';
import fileNameChange from '../util/fileNameChange';

interface UserProfileEditProps {
Expand All @@ -24,10 +27,18 @@ export default function ProfileEdit({ initialValues, onModalClose }: UserProfile
const createPresignedUrl = useCreatePresignedUrl();
const fileInputRef = useRef<HTMLInputElement | null>(null);

const { toast } = useToast();

const handleSubmit = async () => {
await formik.submitForm(); // Formik의 submitForm 함수 호출
};

const { mutate: updateMe } = useUpdateMe({
onSuccess: () => {
// 모달창닫기
onModalClose();
toast({
description: '프로필 수정이 완료되었습니다.',
});
},
});

Expand Down Expand Up @@ -89,32 +100,29 @@ export default function ProfileEdit({ initialValues, onModalClose }: UserProfile
}, [initialValues]);

return (
<div className='w-full h-full fixed top-0 flex flex-col justify-center items-center bg-background-100'>
<form onSubmit={formik.handleSubmit}>
<div className='w-[1200px] relative rounded-sm bg-white'>
<button className='absolute top-4 right-4 w-5 h-5 lg:w-9 lg:h-9' type='button' aria-label='닫기 버튼' onClick={onModalClose}>
<Image src={X_ICON} alt='뒤로가기 버튼 이미지' />
</button>
<div className='w-full h-[700px] py-[60px] px-[100px] flex justify-center gap-[60px] shadow-3xl'>
<div className='w-[400px] flex flex-col gap-8 justify-center items-start'>
<button type='button' className='rounded-xl bg-blue-400 text-white shadow-sm text-lg p-3' onClick={handleImageEditClick} onKeyDown={handleImageEditClick}>
프로필 사진 변경
</button>
<Input type='file' accept='image/*' name='image' onChange={(e) => handleImageChange(e)} className='hidden' ref={fileInputRef} />
<Input type='text' name='nickname' value={formik.values.nickname} className='text-lg p-3' onChange={formik.handleChange} />
</div>
<div className='w-[500px] flex flex-col gap-8 justify-center items-center border border-blue-300 rounded-lg bg-background-100'>
<div className='w-[200px] h-[200px] rounded-full overflow-hidden cursor-pointer'>
<Image src={formik.values.image || initialValues.image} alt='유저 프로필' className='w-full h-full object-cover' width={200} height={200} priority />
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
{({ isSubmitting }) => (
<Form>
<DialogHeader>
<DialogTitle>프로필 수정</DialogTitle>
<div className='flex flex-col justify-center items-center pt-8'>
<div className='w-[200px] h-[200px] rounded-full overflow-hidden cursor-pointer border border-gray-300 shadow-sm'>
<Image src={formik.values.image || initialValues.image} alt='유저 프로필' className='w-full h-full object-cover' width={200} height={200} priority onClick={handleImageEditClick} />
<Input type='file' accept='image/*' name='image' onChange={(e) => handleImageChange(e)} className='hidden' ref={fileInputRef} />
</div>
<div className='mt-10 flex flex-col items-start gap-4'>
<Label htmlFor='name'>닉네임</Label>
<Input type='text' name='nickname' value={formik.values.nickname} className='text-lg p-3' onChange={formik.handleChange} />
</div>
<p className='text-3xl'>{formik.values.nickname}</p>
<button type='submit' disabled={!formik.isValid || formik.isSubmitting} className='rounded-xl bg-black-600 text-white shadow-sm text-lg p-3 w-[100px]'>
저장
</button>
</div>
</div>
</div>
</form>
</div>
<DialogFooter>
<Button type='submit' className='bg-slate-600 text-white' disabled={!formik.isValid || isSubmitting}>
수정하기
</Button>
</DialogFooter>
</DialogHeader>
</Form>
)}
</Formik>
);
}

0 comments on commit 555cc5f

Please sign in to comment.