-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Feat: #69 유저 개인정보 설정 페이지 구현 * Refactor: #69 UserSettingPage에서 이미지, 링크 관련 코드를 컴포넌트로 분리 * Refactor: #69 SignUpPage에서 이미지, 링크 관련 코드를 컴포넌트로 분리 * Refactor: #69 이미지 전송을 위해 폼에서 이미지를 처리할 수 있도록 코드 리팩토링 * Refactor: #69 UserAuthenticatePage의 이메일 인증 로직을 훅으로 분리 및 인증 submit 버튼을 컴포넌트로 분리 * Refactor: #69 SignUpPage의 이메일 인증 로직, 인증 submit 버튼 분리 * Refactor: #69 인증코드 확인 로직 분리 * Rename: #69 유저설정 페이지 관련 컴포넌트 이름 수정 및 auth-form 폴더 구조 수정 * Feat: #69 인증버튼 라벨 props 추가 * Fix: #69 conflict 에러 해결 * UI: #69 전체적인 코드 태그 수정 및 디자인 코드 수정, 로고 수정 * Refactor: #69 인증번호관리 훅에서 타이머 visible 코드 삭제 및 타이머 visible 로직 수정 * UI: #69 일부 코드 태그 수정 및 디자인 코드 수정 * UI: #69 링크 컴포넌트 버튼 내부 아이콘 크기 조절 * Comment: #69 타이머 만료 코드 주석 수정 * UI: #69 label 태그 중첩 해결 및 input 태그 height 수정
- Loading branch information
Showing
18 changed files
with
530 additions
and
439 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { ChangeEvent, useState } from 'react'; | ||
import { FaPlus, FaMinus } from 'react-icons/fa6'; | ||
import { useFormContext } from 'react-hook-form'; | ||
import { USER_SETTINGS } from '@constants/userSettings'; | ||
import useToast from '@hooks/useToast'; | ||
|
||
type LinkContainerProps = { | ||
initialLinks: string[]; | ||
}; | ||
|
||
export default function LinkContainer({ initialLinks }: LinkContainerProps) { | ||
const { setValue } = useFormContext(); | ||
const [link, setLink] = useState<string>(''); | ||
const [links, setLinks] = useState<string[]>(initialLinks); | ||
const [isFocused, setIsFocused] = useState(false); | ||
const { toastWarn } = useToast(); | ||
|
||
const handleFocus = () => setIsFocused(true); | ||
|
||
const handleBlur = () => setIsFocused(false); | ||
|
||
const handleLinkChange = (e: ChangeEvent<HTMLInputElement>) => setLink(e.target.value); | ||
|
||
const handleAddLink = (newLink: string) => { | ||
if (newLink.trim() === '') return; | ||
if (links.length === USER_SETTINGS.MAX_LINK_COUNT) { | ||
setLink(''); | ||
return toastWarn(`링크는 최대 ${USER_SETTINGS.MAX_LINK_COUNT}개까지 등록할 수 있습니다.`); | ||
} | ||
|
||
const isIncludedLink = links.includes(newLink); | ||
if (isIncludedLink) return toastWarn('이미 등록된 링크입니다.'); | ||
|
||
const updatedLinks = [...links, newLink.trim()]; | ||
setLinks(updatedLinks); | ||
setValue('links', updatedLinks); | ||
setLink(''); | ||
}; | ||
|
||
const handleRemoveLink = (removeLink: string) => { | ||
const filteredData = links.filter((linkItem) => linkItem !== removeLink); | ||
setLinks(filteredData); | ||
setValue('links', filteredData); | ||
}; | ||
|
||
return ( | ||
<section> | ||
<label className="font-bold" htmlFor="link"> | ||
링크 | ||
</label> | ||
<div className="space-y-4"> | ||
{links.map((linkItem) => ( | ||
<div key={linkItem} className="flex h-25 items-center space-x-8 rounded-lg border border-input px-6 text-sm"> | ||
<div className="grow overflow-hidden"> | ||
<a href={`https://${linkItem}`} target="_blank" rel="noopener noreferrer"> | ||
{linkItem} | ||
</a> | ||
</div> | ||
<button | ||
type="button" | ||
onClick={() => handleRemoveLink(linkItem)} | ||
className="flex size-18 items-center justify-center rounded-lg bg-sub" | ||
aria-label="삭제" | ||
> | ||
<FaMinus className="size-8" /> | ||
</button> | ||
</div> | ||
))} | ||
<div | ||
className={`flex h-25 items-center space-x-8 rounded-lg border border-input px-6 text-sm ${isFocused ? 'bg-white' : 'bg-disable'}`} | ||
> | ||
<input | ||
id="link" | ||
placeholder="ex) www.github.com" | ||
value={link} | ||
onFocus={handleFocus} | ||
onBlur={handleBlur} | ||
onChange={handleLinkChange} | ||
type="text" | ||
// TODO: 전체적으로 인풋 관련 스타일링 수정 필요, div 전체를 input이 덮을 수 있도록 수정... | ||
className="h-full grow bg-inherit outline-none placeholder:text-emphasis" | ||
/> | ||
<button | ||
type="button" | ||
onClick={() => handleAddLink(link)} | ||
className="flex size-18 items-center justify-center rounded-lg bg-sub" | ||
aria-label="추가" | ||
> | ||
<FaPlus className="size-8" /> | ||
</button> | ||
</div> | ||
</div> | ||
</section> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { GoPlusCircle } from 'react-icons/go'; | ||
import { FaRegTrashCan } from 'react-icons/fa6'; | ||
import { useFormContext } from 'react-hook-form'; | ||
import { convertBytesToString } from '@utils/converter'; | ||
import { USER_SETTINGS } from '@constants/userSettings'; | ||
import useToast from '@hooks/useToast'; | ||
|
||
type ProfileImageContainerProps = { | ||
imageUrl: string; | ||
setImageUrl: React.Dispatch<React.SetStateAction<string>>; | ||
}; | ||
|
||
export default function ProfileImageContainer({ imageUrl, setImageUrl }: ProfileImageContainerProps) { | ||
const { setValue } = useFormContext(); | ||
const { toastWarn } = useToast(); | ||
|
||
// 이미지 관련 코드 | ||
const handleChangeImg = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
const file = e.target.files?.[0]; | ||
|
||
if (!file) return; | ||
if (file.size > USER_SETTINGS.MAX_IMAGE_SIZE) { | ||
e.target.value = ''; | ||
return toastWarn( | ||
`최대 ${convertBytesToString(USER_SETTINGS.MAX_IMAGE_SIZE)} 이하의 이미지 파일만 업로드 가능합니다.`, | ||
); | ||
} | ||
|
||
const image = URL.createObjectURL(file); | ||
setImageUrl(image); | ||
setValue('profileUrl', image); | ||
}; | ||
|
||
const handleRemoveImg = () => { | ||
setImageUrl(''); | ||
setValue('profileUrl', ''); | ||
}; | ||
|
||
return ( | ||
<section className="flex flex-col items-center"> | ||
<div className="group relative flex size-100 items-center justify-center overflow-hidden rounded-full border border-input"> | ||
{imageUrl ? ( | ||
<> | ||
<img src={imageUrl} alt="profileImage" className="size-full object-cover" /> | ||
<div className="absolute inset-0 hidden items-center justify-center bg-black bg-opacity-50 group-hover:flex"> | ||
<p role="presentation" className="cursor-pointer" onClick={handleRemoveImg} onKeyDown={handleRemoveImg}> | ||
<FaRegTrashCan className="size-15 text-white" /> | ||
</p> | ||
</div> | ||
</> | ||
) : ( | ||
<label htmlFor="image" className="cursor-pointer"> | ||
<input id="image" type="file" className="hidden" onChange={handleChangeImg} /> | ||
<GoPlusCircle className="size-15 text-[#5E5E5E]" /> | ||
</label> | ||
)} | ||
</div> | ||
</section> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import Timer from '@components/common/Timer'; | ||
|
||
type VerificationButtonProps = { | ||
isVerificationRequested: boolean; | ||
isSubmitting: boolean; | ||
requestCode: () => void; | ||
expireVerificationCode: () => void; | ||
buttonLabel: string; | ||
}; | ||
|
||
export default function VerificationButton({ | ||
isVerificationRequested, | ||
isSubmitting, | ||
requestCode, | ||
expireVerificationCode, | ||
buttonLabel, | ||
}: VerificationButtonProps) { | ||
return ( | ||
<div> | ||
{!isVerificationRequested ? ( | ||
<button type="submit" className="h-25 w-full rounded-lg bg-sub px-8 font-bold" onClick={requestCode}> | ||
<span>인증요청</span> | ||
</button> | ||
) : ( | ||
<button type="submit" className="relative h-25 w-full rounded-lg bg-sub px-8 font-bold" disabled={isSubmitting}> | ||
{isVerificationRequested && ( | ||
<div className="absolute left-10"> | ||
<Timer time={180} onTimeout={expireVerificationCode} /> | ||
</div> | ||
)} | ||
<span>{buttonLabel}</span> | ||
</button> | ||
)} | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { useState } from 'react'; | ||
import { UseFormSetError } from 'react-hook-form'; | ||
import useToast from '@hooks/useToast'; | ||
import type { EmailVerificationForm, UserSignUpForm } from '@/types/UserType'; | ||
|
||
export default function useEmailVerification() { | ||
const [isVerificationRequested, setIsVerificationRequested] = useState(false); | ||
const { toastSuccess, toastError } = useToast(); | ||
|
||
// 이메일 인증번호 요청 함수 | ||
const requestVerificationCode = () => { | ||
if (!isVerificationRequested) { | ||
setIsVerificationRequested(true); | ||
toastSuccess('인증번호가 발송되었습니다. 이메일을 확인해 주세요.'); | ||
} | ||
}; | ||
|
||
// 인증번호 확인 함수 | ||
const verifyCode = (verificationCode: string, setError: UseFormSetError<UserSignUpForm | EmailVerificationForm>) => { | ||
if (verificationCode === '1234') return true; | ||
|
||
// 인증번호 불일치 | ||
setError('code', { | ||
type: 'manual', | ||
message: '인증번호가 일치하지 않습니다.', | ||
}); | ||
toastError('인증번호가 유효하지 않습니다. 다시 시도해 주세요.'); | ||
return false; | ||
}; | ||
|
||
// 인증 코드 만료 | ||
const expireVerificationCode = () => { | ||
setIsVerificationRequested(false); | ||
toastError('인증 시간이 만료되었습니다. 다시 시도해 주세요.'); | ||
}; | ||
|
||
return { | ||
isVerificationRequested, | ||
requestVerificationCode, | ||
verifyCode, | ||
expireVerificationCode, | ||
}; | ||
} |
6 changes: 3 additions & 3 deletions
6
src/components/user/authForm/AuthForm.tsx → src/layouts/AuthFormLayout.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.