-
Notifications
You must be signed in to change notification settings - Fork 1
Feat: 온보딩 UI #10
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
base: develop
Are you sure you want to change the base?
Feat: 온보딩 UI #10
Changes from 15 commits
d9d9659
81222b9
f4a9e4a
4f7e2c5
de71818
b2bcc6d
74cb410
9243742
2499166
f522b7d
d80c9e7
8c7048d
3b7dc44
a4d14e0
1483d0c
3e4517f
f0e0b67
2b1e02d
059fed1
6317409
14cfb75
f1559ac
142e82d
58563bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| export default function Home() { | ||
| return <div>홈화면</div>; | ||
| export default function HomePage() { | ||
| return <main>홈 화면</main>; | ||
idid10 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,36 +9,14 @@ import Checkbox from "@/components/common/Checkbox"; | |
| import FullButton from "@/components/common/FullButton"; | ||
| import PageTitle from "@/components/common/PageTitle"; | ||
|
|
||
| type AgreementKey = | ||
| | "service" // 심톡 이용약관 동의 (필수) | ||
| | "finance" // 전자금융거래 이용약관 동의 (필수) | ||
| | "personalReq" // 개인정보 수집 이용 동의 (필수) | ||
| | "personalOpt1" // 개인정보 수집 이용 동의 (선택) | ||
| | "personalOpt2" // 개인정보 수집 이용 동의 (선택) | ||
| | "marketing"; // 마케팅 정보 수신 동의 (선택) | ||
| import { AGREEMENTS, INITIAL_AGREEMENTS } from "@/constants/agreement"; | ||
|
|
||
| const AGREEMENTS: { key: AgreementKey; label: string }[] = [ | ||
| { key: "service", label: "심톡 이용약관 동의 (필수)" }, | ||
| { key: "finance", label: "전자금융거래 이용약관 동의 (필수)" }, | ||
| { key: "personalReq", label: "개인정보 수집 이용 동의 (필수)" }, | ||
| { key: "personalOpt1", label: "개인정보 수집 이용 동의 (선택)" }, | ||
| { key: "personalOpt2", label: "개인정보 수집 이용 동의 (선택)" }, | ||
| { key: "marketing", label: "마케팅 정보 메일, SNS수신동의 (선택)" }, | ||
| ]; | ||
|
|
||
| const initialAgreements: Record<AgreementKey, boolean> = { | ||
| service: false, | ||
| finance: false, | ||
| personalReq: false, | ||
| personalOpt1: false, | ||
| personalOpt2: false, | ||
| marketing: false, | ||
| }; | ||
| import type { AgreementKey } from "@/types/agreement.type"; | ||
|
|
||
| const AgreePage = () => { | ||
| const router = useRouter(); | ||
| const [agreements, setAgreements] = | ||
| useState<Record<AgreementKey, boolean>>(initialAgreements); | ||
| useState<Record<AgreementKey, boolean>>(INITIAL_AGREEMENTS); | ||
|
|
||
| const isConfirmActive = | ||
| agreements.service && agreements.finance && agreements.personalReq; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| "use client"; | ||
|
|
||
| import { useEffect, useRef, useState } from "react"; | ||
|
|
||
| interface NameInputProps { | ||
| value: string; | ||
| onChange: (value: string) => void; | ||
| } | ||
|
|
||
| const PLACEHOLDER = "이름을 입력해주세요"; | ||
|
|
||
| const INITIAL_WIDTH = 204; | ||
| const NameInput = ({ value, onChange }: NameInputProps) => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 컴포넌트에서는 export default형식이 아니라 export const 형식으로 사용해주세요 |
||
| const spanRef = useRef<HTMLSpanElement>(null); | ||
| const [inputWidth, setInputWidth] = useState<number>(INITIAL_WIDTH); | ||
|
|
||
| useEffect(() => { | ||
| if (!spanRef.current) return; | ||
|
|
||
| const spanWidth = spanRef.current.offsetWidth; | ||
| setInputWidth(spanWidth + 36); | ||
| }, [value]); | ||
|
|
||
| return ( | ||
| <div className="flex justify-center"> | ||
| <span ref={spanRef} className="text-d3 invisible absolute whitespace-pre"> | ||
| {value || PLACEHOLDER} | ||
| </span> | ||
|
|
||
| <input | ||
| type="text" | ||
| value={value} | ||
| onChange={e => onChange(e.target.value)} | ||
| placeholder={PLACEHOLDER} | ||
| style={{ width: inputWidth }} | ||
| className="border-mint-01 text-d3 text-neutral-01 placeholder:text-neutral-07 rounded-2xl border px-4 py-2 text-center transition-[width] duration-150 ease-out outline-none" | ||
| /> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default NameInput; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| "use client"; | ||
|
|
||
| import { useRouter } from "next/router"; | ||
|
|
||
| import { useState } from "react"; | ||
|
|
||
| import FullButton from "@/components/common/FullButton"; | ||
| import LoadingModal from "@/components/common/LoadingModal"; | ||
| import NameInput from "@/components/common/NameInput"; | ||
| import ProfileImagePicker from "@/components/onboarding/ProfileImagePicker"; | ||
| import UploadButton from "@/components/onboarding/UploadButton"; | ||
|
|
||
| import { useProfileImageUpload } from "@/hooks/useProfileImageUpload"; | ||
|
|
||
| const OnboardingProfileClient = () => { | ||
| const router = useRouter(); | ||
|
|
||
| const [name, setName] = useState(""); | ||
| const [isUploadOpen, setIsUploadOpen] = useState(false); | ||
|
|
||
| const isNameValid = name.trim().length > 0; | ||
|
|
||
| const { profileImage, isLoading, uploadImage, resetImage } = | ||
| useProfileImageUpload(); | ||
idid10 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| const handleCreateProfile = () => { | ||
| if (!isNameValid) return; | ||
| router.replace("/login"); | ||
| }; | ||
idid10 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return ( | ||
| <> | ||
| <main className="flex min-h-dvh justify-center"> | ||
| <div className="relative mt-[13px] flex h-full w-[440px] flex-col"> | ||
| <section className="mt-[123px] px-4 py-2.5"> | ||
| <p className="text-d2 text-neutral-02 whitespace-pre-line"> | ||
| 가족들에게 보여줄{"\n"}내 프로필을 만들어주세요 | ||
| </p> | ||
| </section> | ||
| <section className="mt-[86px] flex flex-col items-center"> | ||
| <ProfileImagePicker | ||
| imageUrl={profileImage} | ||
| onClick={() => setIsUploadOpen(true)} | ||
| /> | ||
| <div className="mt-4 w-full px-[118px]"> | ||
| <NameInput value={name} onChange={setName} /> | ||
| </div> | ||
| </section> | ||
|
Comment on lines
+56
to
+64
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기는 flex구조라서 mt-4로 간격 넣는 것보다는 gap-4로 간격 넣는게 더 좋을 것 같습니다 |
||
|
|
||
| <section className="absolute right-0 bottom-[42px] left-0 px-4 py-2.5"> | ||
| <FullButton isActive={isNameValid} onClick={handleCreateProfile}> | ||
| 프로필생성하기 | ||
| </FullButton> | ||
|
Comment on lines
+67
to
+69
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| </section> | ||
| </div> | ||
| </main> | ||
| <UploadButton | ||
| isOpen={isUploadOpen} | ||
| onClose={() => setIsUploadOpen(false)} | ||
| onSelectAlbum={file => { | ||
| uploadImage(file); | ||
| setIsUploadOpen(false); | ||
| }} | ||
| onSelectDefault={() => { | ||
| resetImage(); | ||
| setIsUploadOpen(false); | ||
| }} | ||
| /> | ||
idid10 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| <LoadingModal | ||
| isOpen={isLoading} | ||
| title="로딩중" | ||
| confirmLabel="취소하기" | ||
| isLoading | ||
| backdrop="blur" | ||
| onClose={() => {}} | ||
| /> | ||
idid10 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| export default OnboardingProfileClient; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| "use client"; | ||
|
|
||
| import Image from "next/image"; | ||
|
|
||
| import FullButton from "@/components/common/FullButton"; | ||
|
|
||
| import type { OnboardingStepName } from "@/constants/onboardingSteps"; | ||
| import { ONBOARDING_CONTENTS } from "@/constants/onboardingSteps"; | ||
|
|
||
| interface Props { | ||
| stepName: OnboardingStepName; | ||
| isLastStep: boolean; | ||
| onNext: () => void; | ||
| } | ||
|
|
||
| const OnboardingStep = ({ stepName, isLastStep, onNext }: Props) => { | ||
| const { title, background } = ONBOARDING_CONTENTS[stepName]; | ||
|
|
||
| return ( | ||
| <div className="flex min-h-dvh justify-center bg-white"> | ||
| <div className="relative mt-[13px] flex h-full w-[440px] flex-col pt-[558px]"> | ||
| {background.type === "image" && ( | ||
| <Image | ||
| src={background.src} | ||
| alt="" | ||
| fill | ||
| priority | ||
| className="pointer-events-none -translate-y-[110px] object-cover object-top" | ||
| /> | ||
| )} | ||
|
|
||
| {background.type === "class" && ( | ||
| <div | ||
| className={`pointer-events-none absolute -translate-y-[110px] ${background.className}`} | ||
| aria-hidden | ||
| /> | ||
| )} | ||
|
|
||
| <section className="relative z-10 px-4 py-2.5"> | ||
| <p className="text-d2 text-neutral-02 whitespace-pre-line">{title}</p> | ||
| </section> | ||
|
Comment on lines
+39
to
+41
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저였다면 이거를 하단바에서부터 fixed로 해서 배치시켰을 것 같습니다 |
||
|
|
||
| <section className="relative z-10 mt-[33px] px-4"> | ||
| <FullButton type="button" onClick={onNext} isActive> | ||
| {isLastStep ? "시작하기" : "다음"} | ||
| </FullButton> | ||
| </section> | ||
|
Comment on lines
+43
to
+47
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 하단버튼의 경우 네비바와 같이 화면에서 고정되어있는 영역이라서 relative가 아니라 fixed bottom-0과 같이 사용해주셔야합니다
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default OnboardingStep; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| "use client"; | ||
|
|
||
| import ProfileIcon from "@/assets/onboarding_profile.svg"; | ||
| import PhotoIcon from "@/assets/photo.svg"; | ||
|
|
||
| type ProfileImagePickerProps = { | ||
| imageUrl: string | null; | ||
| onClick: () => void; | ||
| }; | ||
|
|
||
| const ProfileImagePicker = ({ imageUrl, onClick }: ProfileImagePickerProps) => { | ||
| return ( | ||
| <div | ||
| className="relative flex h-[160px] w-[160px] cursor-pointer items-center justify-center" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기부분에서 h-40 w-40이렇게 사용해줘도 좋을 것 같아요 |
||
| onClick={onClick} | ||
| > | ||
| {imageUrl ? ( | ||
| <img | ||
| src={imageUrl} | ||
| alt="프로필 이미지" | ||
| className="h-full w-full rounded-[36px] object-cover" | ||
| /> | ||
| ) : ( | ||
| <ProfileIcon /> | ||
| )} | ||
|
|
||
| <button | ||
| type="button" | ||
| className="absolute right-[16px] bottom-[16px] h-[32px] w-[32px] cursor-pointer" | ||
| > | ||
| <PhotoIcon /> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. button에 아이콘 크기를 지정하는게 아니라 아이콘 내부에 h-8 w-8을 적용해야할 것 같습니다. |
||
| </button> | ||
|
Comment on lines
+27
to
+32
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default ProfileImagePicker; | ||





There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
browser라서 일단 funnel/next 코드 지워주세요오...!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
내가 지움Vv