✨ frontend add
This commit is contained in:
268
frontend/portal/src/pages/user/info/index.tsx
Normal file
268
frontend/portal/src/pages/user/info/index.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
import {
|
||||
GoogleLoginButton,
|
||||
KakaoLoginButton,
|
||||
NaverLoginButton,
|
||||
} from '@components/Buttons'
|
||||
import CustomAlert, { CustomAlertPrpps } from '@components/CustomAlert'
|
||||
import { PasswordConfirm } from '@components/Password'
|
||||
import { IUserForm, UserInfoDone, UserInfoModified } from '@components/UserInfo'
|
||||
import { userService } from '@service'
|
||||
import { errorStateSelector, userAtom } from '@stores'
|
||||
import produce from 'immer'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
|
||||
interface AlertProps extends CustomAlertPrpps {
|
||||
message: string
|
||||
}
|
||||
|
||||
const UserInfo = () => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const user = useRecoilValue(userAtom)
|
||||
const setUser = useSetRecoilState(userAtom)
|
||||
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
const [checkedEmail, setCheckedEmail] = useState<boolean>(false)
|
||||
const [modified, setModified] = useState<boolean>(false)
|
||||
|
||||
const [customAlert, setCustomAlert] = useState<AlertProps | undefined>({
|
||||
open: false,
|
||||
message: '',
|
||||
handleAlert: () => {},
|
||||
})
|
||||
|
||||
// form hook
|
||||
const methods = useForm<IUserForm>({
|
||||
defaultValues: {
|
||||
password: '',
|
||||
email: user?.email || '',
|
||||
userName: user?.userName || '',
|
||||
},
|
||||
})
|
||||
const { control, handleSubmit, formState, getValues, setFocus } = methods
|
||||
|
||||
useEffect(() => {
|
||||
if (user && user.verification) {
|
||||
setUser({
|
||||
...user,
|
||||
verification: null,
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 비밀번호 확인
|
||||
const handleCheckPassword = async (data: IUserForm) => {
|
||||
data = produce(data, draft => {
|
||||
draft.password = data.currentPassword
|
||||
})
|
||||
try {
|
||||
const result = await userService.matchPassword(data.currentPassword)
|
||||
if (result === true) {
|
||||
setUser({
|
||||
...user,
|
||||
verification: {
|
||||
provider: 'password',
|
||||
password: data.currentPassword,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
throw new Error(t('err.user.password.notmatch'))
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
const showMessage = (message: string, callback?: () => void) => {
|
||||
setCustomAlert({
|
||||
open: true,
|
||||
message,
|
||||
handleAlert: () => {
|
||||
setCustomAlert({
|
||||
...customAlert,
|
||||
open: false,
|
||||
})
|
||||
if (callback) callback()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 변경
|
||||
const handleUpdate = async (data: IUserForm) => {
|
||||
if (user.email === data.email && user.userName === data.userName) {
|
||||
showMessage(t('msg.notmodified'))
|
||||
return
|
||||
}
|
||||
|
||||
if (user.email !== data.email && !checkedEmail) {
|
||||
showMessage(t('msg.user.email.check'), () => {
|
||||
setFocus('email')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await userService.updateInfo(user.userId, {
|
||||
...data,
|
||||
...user.verification,
|
||||
})
|
||||
if (result) {
|
||||
setUser({
|
||||
...user,
|
||||
...data,
|
||||
})
|
||||
|
||||
setModified(true)
|
||||
} else {
|
||||
throw new Error(t('err.internal.server'))
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
// 메인 이동
|
||||
const handleFirst = () => {
|
||||
setUser({
|
||||
...user,
|
||||
verification: null,
|
||||
})
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// 카카오 로그인
|
||||
const handleKakaoLogin = async response => {
|
||||
if (response.profile?.id?.toString() === user.kakaoId) {
|
||||
// setVerification({
|
||||
// provider: 'kakao',
|
||||
// token: response.response.access_token,
|
||||
// })
|
||||
setUser({
|
||||
...user,
|
||||
verification: {
|
||||
provider: 'kakao',
|
||||
token: response.response.access_token,
|
||||
},
|
||||
})
|
||||
setErrorState(null)
|
||||
} else {
|
||||
setErrorState(t('err.user.login.social'))
|
||||
}
|
||||
}
|
||||
|
||||
// 네이버 로그인
|
||||
const handleNaverLogin = async response => {
|
||||
if (response.user?.id === user.naverId) {
|
||||
// Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
|
||||
// 로그인 페이지에서는 잘되는데 이유를 모르겠다. 구글/카카오는 잘된다. 편법으로 일단 진행
|
||||
/* setVerification({
|
||||
provider: 'naver',
|
||||
token: response.accessToken,
|
||||
}) */
|
||||
|
||||
setUser({
|
||||
...user,
|
||||
verification: {
|
||||
provider: 'naver',
|
||||
token: response.accessToken,
|
||||
},
|
||||
})
|
||||
setErrorState(null)
|
||||
} else {
|
||||
setErrorState(t('err.user.login.social'))
|
||||
}
|
||||
}
|
||||
|
||||
// 구글 로그인
|
||||
const handleGoogleLogin = async response => {
|
||||
if (response.googleId === user.googleId) {
|
||||
// setVerification({
|
||||
// provider: 'google',
|
||||
// token: response.tokenId,
|
||||
// })
|
||||
setUser({
|
||||
...user,
|
||||
verification: {
|
||||
provider: 'google',
|
||||
token: response.tokenId,
|
||||
},
|
||||
})
|
||||
setErrorState(null)
|
||||
} else {
|
||||
setErrorState(t('err.user.login.social'))
|
||||
}
|
||||
}
|
||||
|
||||
// 비밀번호 랜더링
|
||||
const renderPasswordForm = () => {
|
||||
return (
|
||||
<article className="mypage">
|
||||
<div className="message small">
|
||||
<span className="">{t('label.text.required.login')}</span>
|
||||
</div>
|
||||
{user?.hasPassword === true && (
|
||||
<PasswordConfirm
|
||||
control={control}
|
||||
formState={formState}
|
||||
handleCheckPassword={handleSubmit(handleCheckPassword)}
|
||||
handleList={handleFirst}
|
||||
/>
|
||||
)}
|
||||
{user?.isSocialUser === true && (
|
||||
<>
|
||||
<h3>
|
||||
<span>{t('label.title.oauth')}</span>
|
||||
</h3>
|
||||
<div className="btn_social">
|
||||
{user?.kakaoId && (
|
||||
<KakaoLoginButton handleClick={handleKakaoLogin} />
|
||||
)}
|
||||
{user?.naverId && (
|
||||
<NaverLoginButton handleClick={handleNaverLogin} />
|
||||
)}
|
||||
{user?.googleId && (
|
||||
<GoogleLoginButton handleClick={handleGoogleLogin} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{modified === false && (
|
||||
<form>
|
||||
{!user?.verification && renderPasswordForm()}
|
||||
{user?.verification && (
|
||||
<UserInfoModified
|
||||
control={control}
|
||||
formState={formState}
|
||||
handleUpdate={handleSubmit(handleUpdate)}
|
||||
handleList={handleFirst}
|
||||
getValues={getValues}
|
||||
setFocus={setFocus}
|
||||
showMessage={showMessage}
|
||||
setCheckedEmail={setCheckedEmail}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
{modified === true && <UserInfoDone handleList={handleFirst} />}
|
||||
<CustomAlert
|
||||
contentText={customAlert.message}
|
||||
open={customAlert.open}
|
||||
handleAlert={customAlert.handleAlert}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserInfo
|
||||
50
frontend/portal/src/pages/user/leave/bye.tsx
Normal file
50
frontend/portal/src/pages/user/leave/bye.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { makeStyles, Theme } from '@material-ui/core/styles'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
mg0: {
|
||||
margin: '0 auto',
|
||||
},
|
||||
}))
|
||||
|
||||
const Bye = () => {
|
||||
const router = useRouter()
|
||||
const classes = useStyles()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleMain = event => {
|
||||
event.preventDefault()
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={classes.mg0}>
|
||||
<article className="rocation">
|
||||
<h2>{t('label.title.mypage')}</h2>
|
||||
<ul>
|
||||
<li>{t('label.title.home')}</li>
|
||||
<li>{t('label.title.mypage')}</li>
|
||||
<li>{t('label.title.leave')}</li>
|
||||
</ul>
|
||||
</article>
|
||||
<article className="mypage">
|
||||
<div className="message">
|
||||
<span className="end">
|
||||
{t('label.text.leave.complete1')}
|
||||
<br />
|
||||
{t('label.text.leave.complete2')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="btn_center">
|
||||
<a href="#" onClick={handleMain}>
|
||||
{t('label.button.first')}
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Bye
|
||||
202
frontend/portal/src/pages/user/leave/index.tsx
Normal file
202
frontend/portal/src/pages/user/leave/index.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import {
|
||||
GoogleLoginButton,
|
||||
KakaoLoginButton,
|
||||
NaverLoginButton,
|
||||
} from '@components/Buttons'
|
||||
import CustomConfirm, { CustomConfirmPrpps } from '@components/CustomConfirm'
|
||||
import { PasswordConfirm } from '@components/Password'
|
||||
import { IVerification, userService } from '@service'
|
||||
import { errorStateSelector, userAtom } from '@stores'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
|
||||
interface IUserForm {
|
||||
currentPassword: string
|
||||
}
|
||||
|
||||
interface AlertProps extends CustomConfirmPrpps {
|
||||
message: string
|
||||
}
|
||||
|
||||
const UserLeave = () => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const user = useRecoilValue(userAtom)
|
||||
|
||||
const [customConfirm, setCustomConfirm] = useState<AlertProps | null>(null)
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
// form hook
|
||||
const methods = useForm<IUserForm>({
|
||||
defaultValues: {
|
||||
currentPassword: '',
|
||||
},
|
||||
})
|
||||
const { control, handleSubmit, formState } = methods
|
||||
|
||||
// 탈퇴 처리
|
||||
const leave = async (data: IVerification) => {
|
||||
console.log('leave', data)
|
||||
try {
|
||||
const result = await userService.leave(data)
|
||||
if (result === true) {
|
||||
router.push('/auth/logout?redirect=/user/leave/bye')
|
||||
} else {
|
||||
throw new Error(t('err.internal.server'))
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
const leaveConfirm = (data: IVerification) => {
|
||||
setCustomConfirm({
|
||||
open: true,
|
||||
message: t('msg.confirm.leave'),
|
||||
handleConfirm: () => {
|
||||
setCustomConfirm({
|
||||
...customConfirm,
|
||||
open: false,
|
||||
})
|
||||
|
||||
leave(data)
|
||||
},
|
||||
handleCancel: () => {
|
||||
setCustomConfirm({ ...customConfirm, open: false })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 탈퇴 클릭
|
||||
const handleLeave = async (data: IUserForm) => {
|
||||
const { currentPassword } = data
|
||||
|
||||
try {
|
||||
const result = await userService.matchPassword(currentPassword)
|
||||
if (result === true) {
|
||||
leaveConfirm({
|
||||
provider: 'password',
|
||||
password: currentPassword,
|
||||
})
|
||||
} else {
|
||||
throw new Error(t('err.user.password.notmatch'))
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
// 카카오 로그인
|
||||
const handleKakaoLogin = async response => {
|
||||
if (response.response?.access_token) {
|
||||
await leave({
|
||||
provider: 'kakao',
|
||||
token: response.response.access_token,
|
||||
})
|
||||
} else {
|
||||
setErrorState({ message: t('err.user.login.social') })
|
||||
}
|
||||
}
|
||||
|
||||
// 네이버 로그인
|
||||
const handleNaverLogin = async response => {
|
||||
if (response.accessToken) {
|
||||
await leave({
|
||||
provider: 'naver',
|
||||
token: response.accessToken,
|
||||
})
|
||||
} else {
|
||||
setErrorState({ message: t('err.user.login.social') })
|
||||
}
|
||||
}
|
||||
|
||||
// 구글 로그인
|
||||
const handleGoogleLogin = async response => {
|
||||
if (response.tokenId) {
|
||||
await leave({
|
||||
provider: 'google',
|
||||
token: response.tokenId,
|
||||
})
|
||||
} else {
|
||||
setErrorState({ message: t('err.user.login.social') })
|
||||
}
|
||||
}
|
||||
|
||||
const handleList = () => {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form>
|
||||
<article className="mypage">
|
||||
<p>
|
||||
{t('label.text.user.leave1')}
|
||||
<br />
|
||||
{t('label.text.user.leave2')}
|
||||
</p>
|
||||
<div className="guide">
|
||||
<h4>{t('label.title.guide')}</h4>
|
||||
<ul>
|
||||
<li>{t('label.text.user.leave.guide1')}</li>
|
||||
<li>{t('label.text.user.leave.guide2')}</li>
|
||||
<li>{t('label.text.user.leave.guide3')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
{user?.hasPassword === true && (
|
||||
<>
|
||||
<p>{t('label.text.user.leave.password')}</p>
|
||||
<PasswordConfirm
|
||||
control={control}
|
||||
formState={formState}
|
||||
handleCheckPassword={handleSubmit(handleLeave)}
|
||||
handleList={handleList}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{user?.isSocialUser === true && (
|
||||
<>
|
||||
<h3>
|
||||
<span>{t('label.title.oauth')}</span>
|
||||
</h3>
|
||||
<div className="btn_social">
|
||||
{user?.kakaoId && (
|
||||
<KakaoLoginButton
|
||||
handleClick={handleKakaoLogin}
|
||||
confirmMessage={t('msg.confirm.leave')}
|
||||
/>
|
||||
)}
|
||||
{user?.naverId && (
|
||||
<NaverLoginButton
|
||||
handleClick={handleNaverLogin}
|
||||
confirmMessage={t('msg.confirm.leave')}
|
||||
/>
|
||||
)}
|
||||
{user?.googleId && (
|
||||
<GoogleLoginButton
|
||||
handleClick={handleGoogleLogin}
|
||||
confirmMessage={t('msg.confirm.leave')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
{customConfirm && (
|
||||
<CustomConfirm
|
||||
handleConfirm={customConfirm.handleConfirm}
|
||||
handleCancel={customConfirm.handleCancel}
|
||||
contentText={customConfirm.message}
|
||||
open={customConfirm.open}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserLeave
|
||||
216
frontend/portal/src/pages/user/password/index.tsx
Normal file
216
frontend/portal/src/pages/user/password/index.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import {
|
||||
GoogleLoginButton,
|
||||
KakaoLoginButton,
|
||||
NaverLoginButton,
|
||||
} from '@components/Buttons'
|
||||
import {
|
||||
IUserPasswordForm,
|
||||
PasswordChange,
|
||||
PasswordConfirm,
|
||||
PasswordDone,
|
||||
} from '@components/Password'
|
||||
import { userService } from '@service'
|
||||
import { errorStateSelector, userAtom } from '@stores'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
|
||||
const EditPassword = () => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const user = useRecoilValue(userAtom)
|
||||
const setUser = useSetRecoilState(userAtom)
|
||||
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
const [modified, setModified] = useState<boolean>(false)
|
||||
|
||||
// form hook
|
||||
const methods = useForm<IUserPasswordForm>({
|
||||
defaultValues: {
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
newPasswordConfirm: '',
|
||||
},
|
||||
})
|
||||
const { control, handleSubmit, formState, setFocus } = methods
|
||||
|
||||
useEffect(() => {
|
||||
if (user && user.verification) {
|
||||
setUser({
|
||||
...user,
|
||||
verification: null,
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 메인 이동
|
||||
const handleFirst = () => {
|
||||
setUser({
|
||||
...user,
|
||||
verification: null,
|
||||
})
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// 비밀번호 확인
|
||||
const handleCheckPassword = async (data: IUserPasswordForm) => {
|
||||
try {
|
||||
const result = await userService.matchPassword(data.currentPassword)
|
||||
if (result === true) {
|
||||
setUser({
|
||||
...user,
|
||||
verification: {
|
||||
provider: 'password',
|
||||
password: data.currentPassword,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
throw new Error(t('err.user.password.notmatch'))
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
// 비밀번호 변경
|
||||
const handleChangePassword = async (data: IUserPasswordForm) => {
|
||||
try {
|
||||
const result = await userService.updatePassword({
|
||||
...user.verification,
|
||||
newPassword: data.newPassword,
|
||||
})
|
||||
|
||||
if (result === true) {
|
||||
setModified(true)
|
||||
} else {
|
||||
throw new Error(t('err.internal.server'))
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
// 카카오 로그인
|
||||
const handleKakaoLogin = response => {
|
||||
if (response.profile?.id?.toString() === user.kakaoId) {
|
||||
// setVerification({
|
||||
// provider: 'kakao',
|
||||
// token: response.response.access_token,
|
||||
// })
|
||||
setUser({
|
||||
...user,
|
||||
verification: {
|
||||
provider: 'kakao',
|
||||
token: response.response.access_token,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
setErrorState({ message: t('err.user.social.notmatch') })
|
||||
}
|
||||
}
|
||||
|
||||
// 네이버 로그인
|
||||
const handleNaverLogin = async response => {
|
||||
if (response.user?.id === user.naverId) {
|
||||
// Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
|
||||
// 로그인 페이지에서는 잘되는데 이유를 모르겠다. 구글/카카오는 잘된다. 편법으로 일단 진행
|
||||
/* setVerification({
|
||||
provider: 'naver',
|
||||
token: response.accessToken,
|
||||
}) */
|
||||
|
||||
setUser({
|
||||
...user,
|
||||
verification: {
|
||||
provider: 'naver',
|
||||
token: response.accessToken,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
setErrorState({ message: t('err.user.social.notmatch') })
|
||||
}
|
||||
}
|
||||
|
||||
// 구글 로그인
|
||||
const handleGoogleLogin = response => {
|
||||
if (response.googleId === user.googleId) {
|
||||
// setVerification({
|
||||
// provider: 'google',
|
||||
// token: response.tokenId,
|
||||
// })
|
||||
setUser({
|
||||
...user,
|
||||
verification: {
|
||||
provider: 'google',
|
||||
token: response.tokenId,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
setErrorState({ message: t('err.user.social.notmatch') })
|
||||
}
|
||||
}
|
||||
|
||||
// 비밀번호 랜더링
|
||||
const renderPasswordForm = () => {
|
||||
return (
|
||||
<article className="mypage">
|
||||
<div className="message small">
|
||||
<span className="">{t('label.text.required.login')}</span>
|
||||
</div>
|
||||
{user?.hasPassword === true && (
|
||||
<PasswordConfirm
|
||||
control={control}
|
||||
formState={formState}
|
||||
handleCheckPassword={handleSubmit(handleCheckPassword)}
|
||||
handleList={handleFirst}
|
||||
/>
|
||||
)}
|
||||
{user?.isSocialUser === true && (
|
||||
<>
|
||||
<h3>
|
||||
<span>{t('label.title.oauth')}</span>
|
||||
</h3>
|
||||
<div className="btn_social">
|
||||
{user?.kakaoId && (
|
||||
<KakaoLoginButton handleClick={handleKakaoLogin} />
|
||||
)}
|
||||
{user?.naverId && (
|
||||
<NaverLoginButton handleClick={handleNaverLogin} />
|
||||
)}
|
||||
{user?.googleId && (
|
||||
<GoogleLoginButton handleClick={handleGoogleLogin} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{modified === false && (
|
||||
<form>
|
||||
{!user?.verification && renderPasswordForm()}
|
||||
{user?.verification && (
|
||||
<PasswordChange
|
||||
control={control}
|
||||
formState={formState}
|
||||
handleChangePassword={handleSubmit(handleChangePassword)}
|
||||
setFocus={setFocus}
|
||||
currentPassword={user.verification.password}
|
||||
handleList={handleFirst}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
{modified === true && <PasswordDone handleList={handleFirst} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditPassword
|
||||
98
frontend/portal/src/pages/user/reserve/[id].tsx
Normal file
98
frontend/portal/src/pages/user/reserve/[id].tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { BottomButtons, IButtons } from '@components/Buttons'
|
||||
import { ReserveInfo, ReserveItemInfo } from '@components/Reserve'
|
||||
import { ICode, IReserve, reserveService } from '@service'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface UserReserveDetailProps {
|
||||
initData: IReserve
|
||||
status?: ICode
|
||||
}
|
||||
|
||||
const UserReserveDetail = ({ initData, status }: UserReserveDetailProps) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 버튼
|
||||
const bottomButtons = useMemo((): IButtons[] => {
|
||||
const buttons: IButtons[] = [
|
||||
{
|
||||
id: 'item-list-button',
|
||||
title: t('label.button.list'),
|
||||
href: `/user/reserve`,
|
||||
},
|
||||
]
|
||||
|
||||
if (
|
||||
initData?.reserveStatusId === 'approve' ||
|
||||
initData?.reserveStatusId === 'request'
|
||||
) {
|
||||
buttons.push({
|
||||
id: 'item-cancel-button',
|
||||
title: `${t('reserve')} ${t('label.button.cancel')}`,
|
||||
href: `/user/reserve/cancel/${initData.reserveId}`,
|
||||
className: 'blue',
|
||||
})
|
||||
return buttons.reverse()
|
||||
}
|
||||
|
||||
return buttons
|
||||
}, [t, router.query, initData])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="table_view02">
|
||||
{initData && (
|
||||
<>
|
||||
<ReserveItemInfo
|
||||
data={initData.reserveItem}
|
||||
reserveStatus={status}
|
||||
/>
|
||||
<ReserveInfo data={initData} />
|
||||
</>
|
||||
)}
|
||||
<BottomButtons handleButtons={bottomButtons} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
const id = String(context.query.id)
|
||||
|
||||
let initData: IReserve = null
|
||||
let status: ICode = null
|
||||
|
||||
try {
|
||||
const result = await reserveService.getReserve(id)
|
||||
if (result) {
|
||||
initData = result.data
|
||||
}
|
||||
|
||||
const codeResult = await reserveService.getCode('reserve-status')
|
||||
|
||||
if (codeResult) {
|
||||
status = codeResult.data.find(
|
||||
item => item.codeId === initData?.reserveStatusId,
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`reserve detail item query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
initData,
|
||||
status,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default UserReserveDetail
|
||||
102
frontend/portal/src/pages/user/reserve/cancel/[id].tsx
Normal file
102
frontend/portal/src/pages/user/reserve/cancel/[id].tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { BottomButtons, IButtons } from '@components/Buttons'
|
||||
import ValidationAlert from '@components/ValidationAlert'
|
||||
import useInputs from '@hooks/useInputs'
|
||||
import { reserveService } from '@service'
|
||||
import { errorStateSelector } from '@stores'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useSnackbar } from 'notistack'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
|
||||
const ReserveCancel = () => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar()
|
||||
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
const searchText = useInputs('')
|
||||
const [error, setError] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (searchText.value !== '') {
|
||||
setError(false)
|
||||
}
|
||||
}, [searchText])
|
||||
|
||||
const handleCancelClick = async () => {
|
||||
if (searchText.value === '') {
|
||||
setError(true)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await reserveService.cancel(
|
||||
String(router.query?.id),
|
||||
searchText.value,
|
||||
)
|
||||
if (result) {
|
||||
enqueueSnackbar(
|
||||
`${t('reserve')} ${t('common.cancel')}${t('common.msg.done.format')}`,
|
||||
{
|
||||
variant: 'success',
|
||||
},
|
||||
)
|
||||
router.push('/user/reserve')
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
// 버튼
|
||||
const bottomButtons = useMemo(
|
||||
(): IButtons[] => [
|
||||
{
|
||||
id: 'item-confirm-button',
|
||||
title: t('label.button.confirm'),
|
||||
href: ``,
|
||||
handleClick: handleCancelClick,
|
||||
className: 'blue',
|
||||
},
|
||||
{
|
||||
id: 'item-list-button',
|
||||
title: t('label.button.cancel'),
|
||||
href: ``,
|
||||
handleClick: () => {
|
||||
router.back()
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
[t, router, searchText],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="mypage">
|
||||
<p>{t('reserve.msg.calcel_reason')}</p>
|
||||
<div className="table_write01">
|
||||
<span>{t('common.required_fields')}</span>
|
||||
<div className="write">
|
||||
<dl>
|
||||
<dt className="import">{t('reserve.cancel_reason')}</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('reserve.cancel_reason')}
|
||||
{...searchText}
|
||||
/>
|
||||
{error && (
|
||||
<ValidationAlert message={t('reserve.msg.calcel_reason')} />
|
||||
)}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<BottomButtons handleButtons={bottomButtons} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReserveCancel
|
||||
419
frontend/portal/src/pages/user/reserve/index.tsx
Normal file
419
frontend/portal/src/pages/user/reserve/index.tsx
Normal file
@@ -0,0 +1,419 @@
|
||||
import {
|
||||
OptionsType,
|
||||
SelectBox,
|
||||
SelectType,
|
||||
} from '@components/Inputs/SelectBox'
|
||||
import Search from '@components/Search'
|
||||
import DataGridTable from '@components/TableList/DataGridTable'
|
||||
import { DEFUALT_GRID_PAGE_SIZE, GRID_ROWS_PER_PAGE_OPTION } from '@constants'
|
||||
import usePage from '@hooks/usePage'
|
||||
import useSearchTypes from '@hooks/useSearchTypes'
|
||||
import { convertStringToDateFormat } from '@libs/date'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import {
|
||||
GridCellParams,
|
||||
GridColDef,
|
||||
GridValueFormatterParams,
|
||||
GridValueGetterParams,
|
||||
MuiEvent,
|
||||
} from '@material-ui/data-grid'
|
||||
import { ICode, ILocation, Page, reserveService } from '@service'
|
||||
import { conditionAtom, conditionValue, userAtom } from '@stores'
|
||||
import { rownum } from '@utils'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { createRef, useEffect, useMemo, useState } from 'react'
|
||||
import { TFunction, useTranslation } from 'react-i18next'
|
||||
import { useRecoilValue } from 'recoil'
|
||||
|
||||
type ColorType =
|
||||
| 'inherit'
|
||||
| 'initial'
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'textPrimary'
|
||||
| 'textSecondary'
|
||||
| 'error'
|
||||
|
||||
type ColumnsType = (
|
||||
data: Page,
|
||||
locations: OptionsType[],
|
||||
categories: OptionsType[],
|
||||
status: ICode[],
|
||||
t?: TFunction,
|
||||
) => GridColDef[]
|
||||
|
||||
const getColumns: ColumnsType = (data, locations, categories, status, t) => {
|
||||
return [
|
||||
{
|
||||
field: 'rownum',
|
||||
headerName: t('common.no'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
rownum(data, params.api.getRowIndex(params.id), 'desc'),
|
||||
},
|
||||
{
|
||||
field: 'locationId',
|
||||
headerName: t('location'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<>{locations.find(item => item.value === params.value)?.label}</>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'categoryId',
|
||||
headerName: t('reserve_item.type'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<>{categories.find(item => item.value === params.value)?.label}</>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'reserveItemName',
|
||||
headerName: t('reserve_item.name'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
cellClassName: 'title',
|
||||
},
|
||||
{
|
||||
field: 'reserveQty',
|
||||
headerName: `${t('reserve.count')}/${t('reserve.number_of_people')} (${t(
|
||||
'reserve_item.inventory',
|
||||
)})`,
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => {
|
||||
const category = params.row.categoryId
|
||||
if (category === 'education') {
|
||||
return `${params.value}(${params.row.totalQty})`
|
||||
} else {
|
||||
return `${params.row.totalQty}`
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'reserveStatusId',
|
||||
headerName: t('reserve.status'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => {
|
||||
let color: ColorType = 'inherit'
|
||||
if (params.value === 'request' || params.value === 'cancel') {
|
||||
color = 'error'
|
||||
}
|
||||
return (
|
||||
<Typography color={color}>
|
||||
{status.find(item => item.codeId === params.value)?.codeName}
|
||||
</Typography>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createDate',
|
||||
headerName: t('common.created_date'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
minWidth: 140,
|
||||
sortable: false,
|
||||
valueFormatter: (params: GridValueFormatterParams) =>
|
||||
params.value
|
||||
? convertStringToDateFormat(String(params.value), 'yyyy-MM-dd HH:mm')
|
||||
: null,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const getXsColumns: ColumnsType = (data, locations, categories, status, t) => {
|
||||
return [
|
||||
{
|
||||
field: 'reserveItemName',
|
||||
headerName: t('reserve_item.name'),
|
||||
headerAlign: 'center',
|
||||
sortable: false,
|
||||
renderCell,
|
||||
},
|
||||
]
|
||||
|
||||
function renderCell(params: GridCellParams) {
|
||||
return (
|
||||
<div>
|
||||
<div className="title">{params.value}</div>
|
||||
<div className="sub">
|
||||
<p>
|
||||
{
|
||||
locations.find(item => item.value === params.row.locationId)
|
||||
?.label
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
{
|
||||
categories.find(item => item.value === params.row.categoryId)
|
||||
?.label
|
||||
}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{params.row.reserveStatusId === 'request' ||
|
||||
params.row.reserveStatusId === 'cancel' ? (
|
||||
<Typography component="span" color="error">
|
||||
{
|
||||
status.find(
|
||||
item => item.codeId === params.row.reserveStatusId,
|
||||
).codeName
|
||||
}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography component="span">
|
||||
{
|
||||
status.find(
|
||||
item => item.codeId === params.row.reserveStatusId,
|
||||
).codeName
|
||||
}
|
||||
</Typography>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const conditionKey = 'user-reserve'
|
||||
|
||||
interface UserReserveProps {
|
||||
locations: OptionsType[]
|
||||
categories: OptionsType[]
|
||||
status: ICode[]
|
||||
}
|
||||
|
||||
const UserReserve = (props: UserReserveProps) => {
|
||||
const { locations, categories, status } = props
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
const pageSizeRef = createRef<SelectType>()
|
||||
|
||||
const user = useRecoilValue(userAtom)
|
||||
const { page, setPageValue } = usePage(conditionKey, 0)
|
||||
const [pageSize, setPageSize] = useState<number>(DEFUALT_GRID_PAGE_SIZE)
|
||||
|
||||
const [customKeyword, setCustomKeyword] = useState<conditionValue | null>({
|
||||
locationId: keywordState?.locationId || '0',
|
||||
categoryId: keywordState?.categoryId || 'all',
|
||||
})
|
||||
|
||||
// 조회조건 select items
|
||||
const searchTypes = useSearchTypes([
|
||||
{
|
||||
value: 'item',
|
||||
label: t('reserve_item.name'),
|
||||
},
|
||||
])
|
||||
|
||||
const { data, mutate } = reserveService.searchUserReserve({
|
||||
userId: user?.userId,
|
||||
keywordType: keywordState?.keywordType || 'item',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: pageSize,
|
||||
page,
|
||||
locationId:
|
||||
keywordState?.locationId !== '0' ? keywordState?.locationId : null,
|
||||
categoryId:
|
||||
keywordState?.categoryId !== 'all' ? keywordState?.categoryId : null,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
console.log(data)
|
||||
}
|
||||
}, [data])
|
||||
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate(data, false)
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePageChange = (page: number, details?: any) => {
|
||||
setPageValue(page)
|
||||
}
|
||||
|
||||
const handlePageSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setPageSize(Number(e.target.value))
|
||||
}
|
||||
|
||||
const handleCellClick = (
|
||||
params: GridCellParams,
|
||||
event: MuiEvent<React.MouseEvent>,
|
||||
details?: any,
|
||||
) => {
|
||||
if (params.field === 'reserveItemName') {
|
||||
router.push(`${router.asPath}/${params.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLocationChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setCustomKeyword({
|
||||
...customKeyword,
|
||||
locationId: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
const handleCategoryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setCustomKeyword({
|
||||
...customKeyword,
|
||||
categoryId: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
const columns = useMemo(
|
||||
() => getColumns(data, locations, categories, status, t),
|
||||
[data, t],
|
||||
)
|
||||
|
||||
const xsColumns = useMemo(
|
||||
() => getXsColumns(data, locations, categories, status, t),
|
||||
[data, t],
|
||||
)
|
||||
|
||||
const rowsPerPageSizeOptinos = GRID_ROWS_PER_PAGE_OPTION.map(item => {
|
||||
return {
|
||||
value: item,
|
||||
label: `${item} 개`,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="mypage">
|
||||
<div className="table_list01">
|
||||
<fieldset>
|
||||
<div>
|
||||
<SelectBox
|
||||
ref={pageSizeRef}
|
||||
options={rowsPerPageSizeOptinos}
|
||||
onChange={handlePageSizeChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Search
|
||||
options={searchTypes}
|
||||
conditionKey={conditionKey}
|
||||
handleSearch={handleSearch}
|
||||
customKeyword={customKeyword}
|
||||
conditionNodes={
|
||||
<>
|
||||
{locations && (
|
||||
<SelectBox
|
||||
options={locations}
|
||||
value={customKeyword.locationId}
|
||||
onChange={handleLocationChange}
|
||||
style={{ marginRight: '2px' }}
|
||||
/>
|
||||
)}
|
||||
{categories && (
|
||||
<SelectBox
|
||||
options={categories}
|
||||
value={customKeyword.categoryId}
|
||||
onChange={handleCategoryChange}
|
||||
style={{ marginRight: '2px' }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<DataGridTable
|
||||
columns={columns}
|
||||
rows={data?.content}
|
||||
xsColumns={xsColumns}
|
||||
getRowId={r => r.reserveId}
|
||||
pageSize={pageSize}
|
||||
rowCount={data?.totalElements}
|
||||
page={page}
|
||||
onPageChange={handlePageChange}
|
||||
paginationMode="server"
|
||||
onCellClick={handleCellClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
const categoryId = String(context.query.category)
|
||||
|
||||
let locations: OptionsType[] = []
|
||||
let categories: OptionsType[] = []
|
||||
let status: ICode[] = []
|
||||
|
||||
try {
|
||||
const location = (await (
|
||||
await reserveService.getLocation()
|
||||
).data) as ILocation[]
|
||||
if (location) {
|
||||
locations = location.map(item => {
|
||||
return {
|
||||
value: item.locationId,
|
||||
label: item.locationName,
|
||||
}
|
||||
})
|
||||
locations.unshift({
|
||||
value: '0',
|
||||
label: '전체',
|
||||
})
|
||||
}
|
||||
|
||||
const category = (await (
|
||||
await reserveService.getCode('reserve-category')
|
||||
).data) as ICode[]
|
||||
if (category) {
|
||||
categories = category.map(item => {
|
||||
return {
|
||||
value: item.codeId,
|
||||
label: item.codeName,
|
||||
}
|
||||
})
|
||||
categories.unshift({
|
||||
value: 'all',
|
||||
label: '전체',
|
||||
})
|
||||
}
|
||||
|
||||
const codeResult = await reserveService.getCode('reserve-status')
|
||||
if (codeResult) {
|
||||
status = codeResult.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`reserve detail item query error ${error.message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
locations,
|
||||
categories,
|
||||
status,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default UserReserve
|
||||
Reference in New Issue
Block a user