Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/마이페이지 #35

Merged
merged 2 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/app/mypage/api/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { http } from '@/api'
import { MyPageResponse, PatchMyPageRequest } from './types'

export const getMypage = () => {
return http.get<MyPageResponse>({
url: '/profile',
})
}

export const patchAlarm = () => {
return http.patch({
url: '/members/email-notification',
})
}

export const patchMyPage = (data: PatchMyPageRequest) => {
return http.patch({
url: '/member/profile',
data,
})
}
24 changes: 24 additions & 0 deletions src/app/mypage/api/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useMutation, useSuspenseQuery } from '@tanstack/react-query'
import { getMypage, patchAlarm, patchMyPage } from './api'
import { PatchMyPageRequest } from './types'

export const useGetMyPage = () =>
useSuspenseQuery({
queryKey: ['mypage'],
queryFn: () => getMypage(),
select: (data) => data.data,
})

export const usePatchAlarm = () => {
return useMutation({
mutationKey: ['alarm'],
mutationFn: () => patchAlarm(),
})
}

export const usePatchMyPage = () => {
return useMutation({
mutationKey: ['mypage'],
mutationFn: (data: PatchMyPageRequest) => patchMyPage(data),
})
}
9 changes: 9 additions & 0 deletions src/app/mypage/api/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface MyPageResponse {
email: string
isEmailNotificationEnabled: boolean
}

export interface PatchMyPageRequest {
nickname: string
profileImage: string
}
17 changes: 17 additions & 0 deletions src/app/mypage/components/fetcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client'

import { generateContext } from '@/react-utils'
import { StrictPropsWithChildren } from '@/types'
import { MyPageResponse } from '../api/types'
import { useGetMyPage } from '../api/queries'

export const [MyPageProvider, useMyPageContext] =
generateContext<MyPageResponse>({
name: 'mypage-context',
})

export function MyPageFetcher({ children }: StrictPropsWithChildren) {
const { data } = useGetMyPage()

return <MyPageProvider {...data}>{children}</MyPageProvider>
}
107 changes: 107 additions & 0 deletions src/app/mypage/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
'use client'

import { useNicknameValidator, useProfileSelector } from '@/app/start/hooks'
import { Button, HeaderWithBack, Input, Left } from '@/components'
import useUserInfo from '@/store/useUserInfo'
import { cn } from '@/util'
import Image from 'next/image'
import { useRouter } from 'next/navigation'
import { ChangeEvent, useState } from 'react'
import { usePatchMyPage } from '../api/queries'

export default function MyPageEdit() {
const { push } = useRouter()
const { userInfo, setUserInfo } = useUserInfo()
const { mutate } = usePatchMyPage()
const [error, setError] = useState<boolean>(true)
const { nickname, errorMessage, handleNicknameChange } = useNicknameValidator(
{
initialNickname: userInfo.nickname,
setError,
},
)
const { profiles, profileUrl, selectedProfileID, handleProfileSelect } =
useProfileSelector()

const handleSaveClick = () => {
mutate(
{
nickname,
profileImage: selectedProfileID,
},
{
onSuccess: () => {
setUserInfo({
...userInfo,
nickname,
profileImage: profileUrl,
})
push('/mypage')
},
},
)
}

return (
<HeaderWithBack title="프로필 수정하기" onBack={() => push('/mypage')}>
<div className="p-24">
<h2 className="text-lg font-semibold leading-relaxed">
닉네임 수정하기
</h2>
<Input
success="멋진 이름이에요!"
label="6자 이내의 한글/영문/숫자를 입력해주세요."
value={nickname}
placeholder="닉네임을 적어주세요."
error={errorMessage}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleNicknameChange(e.target.value)
}
/>

<h2 className="text-lg font-semibold leading-relaxed mt-44">
프로필 이미지 수정하기
</h2>

<div className="grid grid-cols-4 gap-y-22 gap-x-10 w-full justify-items-stretch mt-16">
{profiles.map((id) => (
<button
type="button"
key={id}
onClick={() => handleProfileSelect(id)}
className={cn(
'relative w-full aspect-square border rounded-full overflow-hidden p-20',
selectedProfileID === id && 'border-accent-50 bg-accent-10',
)}
tabIndex={0}
>
{selectedProfileID === id && (
<div className="absolute -top-18">
<Left />
</div>
)}

<Image
alt="profile image"
src={`${process.env.NEXT_PUBLIC_IMAGE_URL}/image/profile/profile${id}.svg`}
layout="fill"
objectFit="cover"
className="p-5"
/>
</button>
))}
</div>
</div>
<div className="absolute bottom-50 w-full py-4 flex justify-center">
<Button
className="w-[90%] mx-auto font-semibold"
disabled={error}
onClick={handleSaveClick}
type="button"
>
변경사항 저장하기
</Button>
</div>
</HeaderWithBack>
)
}
20 changes: 20 additions & 0 deletions src/app/mypage/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { AsyncBoundaryWithQuery } from '@/react-utils'
import { StrictPropsWithChildren } from '@/types'
import type { Metadata } from 'next'
import { MyPageFetcher } from './components/fetcher'

export const metadata: Metadata = {
title: '나의 시간조각을 모아, 조각조각',
description: '자투리 시간 앱',
}

export default function MyPageLayout({ children }: StrictPropsWithChildren) {
return (
<AsyncBoundaryWithQuery
pendingFallback={<>Loading...</>}
errorFallback={<>error..</>}
>
<MyPageFetcher>{children}</MyPageFetcher>
</AsyncBoundaryWithQuery>
)
}
103 changes: 103 additions & 0 deletions src/app/mypage/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
'use client'

import { Button, HeaderWithBack, IconRight, Pencil, Switch } from '@/components'
import useUserInfo from '@/store/useUserInfo'
import { useRouter } from 'next/navigation'
import Image from 'next/image'
import { useState } from 'react'
import { useMyPageContext } from './components/fetcher'
import { usePatchAlarm } from './api/queries'

export default function MyPage() {
const { nickname, profileImage } = useUserInfo().userInfo
const { push } = useRouter()

const { isEmailNotificationEnabled, email } = useMyPageContext()
const { mutate } = usePatchAlarm()

const [isEmailAlert, setIsEmailAlert] = useState(isEmailNotificationEnabled)

const handleAlarmSwitch = () => {
setIsEmailAlert(!isEmailAlert)
mutate(undefined, {
onError: () => {
setIsEmailAlert(isEmailAlert)
alert('알림 설정을 변경하는 데 실패했습니다. 다시 시도해 주세요.')
},
})
}

return (
<HeaderWithBack onBack={() => push('/home')} title="마이페이지">
<div className="min-h-screen">
<div className="flex flex-col items-center bg-white py-20 border-b-6 border-primary_foundation-5 px-24">
<div className="w-full mb-16">
<div className="flex justify-between w-full">
<div className="text-primary_foundation-60 flex items-center">
<strong className="text-24 text-primary_foundation-100 font-[500]">
{nickname}
</strong>
<span className="ml-2">님의 프로필</span>
</div>
<Button
leftIcon={<Pencil />}
onClick={() => push('/mypage/edit')}
className="bg-primary_foundation-5 text-12 p-8 h-fit"
>
프로필 수정하기
</Button>
</div>
<span className="text-primary_foundation-40 text-14">{email}</span>
</div>

<div className="relative w-140 h-140 rounded-full overflow-hidden border-2 bg-accent-10 border-accent-30">
<Image
src={profileImage}
alt="프로필 이미지"
width={100}
height={100}
/>
</div>
</div>
<div className="bg-primary_foundation-5 h-6 w-ful px-24" />

<div className="bg-white mt-8 rounded-lg">
<h2 className="text-16 weight-[400] px-20 py-16 text-primary_foundation-50">
환경 설정
</h2>
<div className="text-16">
<div className="flex justify-between items-center px-20 py-16 text-16">
<span className="text-16 text-primary_foundation-100">
이메일 알림
</span>
<Switch
isOn={isEmailAlert}
onSwitch={() => handleAlarmSwitch()}
/>
</div>

<button
type="button"
className="flex w-full justify-between items-center px-20 py-16"
onClick={() => push('/mypage')}
>
<span className="text-primary_foundation-100">공지사항</span>
<IconRight />
</button>

<button
type="submit"
className="px-20 py-16"
onClick={() => {
// TODO: 로그아웃 처리
push('/login')
}}
>
<span>로그아웃</span>
</button>
</div>
</div>
</div>
</HeaderWithBack>
)
}
Loading