-
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 all 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 |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| "use client"; | ||
|
|
||
| import { useRouter } from "next/navigation"; | ||
|
|
||
| import { useState } from "react"; | ||
|
|
||
| import OnboardingStep from "@/components/onboarding/OnboardingStep"; | ||
| import ProgressDots from "@/components/onboarding/ProgressDots"; | ||
|
|
||
| import { | ||
| ONBOARDING_STEPS, | ||
| type OnboardingStepName, | ||
| } from "@/constants/onboardingSteps"; | ||
|
|
||
| const OnboardingPage = () => { | ||
| const router = useRouter(); | ||
| const [currentStepIndex, setCurrentStepIndex] = useState(0); | ||
|
|
||
| const currentStepName = ONBOARDING_STEPS[ | ||
| currentStepIndex | ||
| ] as OnboardingStepName; | ||
| const isLastStep = currentStepIndex === ONBOARDING_STEPS.length - 1; | ||
|
|
||
| const goNext = () => { | ||
| const nextIndex = currentStepIndex + 1; | ||
|
|
||
| if (nextIndex >= ONBOARDING_STEPS.length) { | ||
| if (typeof window !== "undefined") { | ||
| localStorage.setItem("onboardingDone", "true"); | ||
| } | ||
| router.push("/onboarding/profile"); | ||
| return; | ||
| } | ||
|
|
||
| setCurrentStepIndex(nextIndex); | ||
| }; | ||
|
|
||
| return ( | ||
| <section className="flex min-h-dvh justify-center"> | ||
| <div className="relative mt-[73px] flex h-full flex-col"> | ||
|
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 className="z-10"> | ||
| <ProgressDots | ||
| total={ONBOARDING_STEPS.length} | ||
| current={currentStepIndex} | ||
| /> | ||
| </div> | ||
|
|
||
| <OnboardingStep | ||
| stepName={currentStepName} | ||
| isLastStep={isLastStep} | ||
| onNext={goNext} | ||
| /> | ||
| </div> | ||
| </section> | ||
| ); | ||
| }; | ||
|
|
||
| export default OnboardingPage; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import OnboardingProfileClient from "@/components/onboarding/OnboardingProfile"; | ||
|
|
||
| const OnboardingProfilePage = () => { | ||
| return <OnboardingProfileClient />; | ||
| }; | ||
| export default OnboardingProfilePage; |
| 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 <div>홈 화면</div>; | ||
| } |
| 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,102 @@ | ||
| "use client"; | ||
|
|
||
| import { useRouter } from "next/navigation"; | ||
|
|
||
| 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, cancelUpload } = | ||
| useProfileImageUpload(); | ||
|
Comment on lines
+23
to
+24
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. 🧩 Analysis chain🏁 Script executed: # Find the useProfileImageUpload hook implementation
fd -e ts -e tsx | xargs grep -l "useProfileImageUpload" | grep -v node_modulesRepository: IT-Cotato/12th-SimTok-FE Length of output: 150 🏁 Script executed: # Search for revokeObjectURL usage in the codebase
rg -n "revokeObjectURL" --type ts --type tsxRepository: IT-Cotato/12th-SimTok-FE Length of output: 95 🏁 Script executed: # Search for createObjectURL usage
rg -n "createObjectURL" --type ts --type tsxRepository: IT-Cotato/12th-SimTok-FE Length of output: 95 🏁 Script executed: # Read the useProfileImageUpload hook implementation
cat -n src/hooks/useProfileImageUpload.tsRepository: IT-Cotato/12th-SimTok-FE Length of output: 1528 🏁 Script executed: # Search for revokeObjectURL and createObjectURL without file type restrictions
rg "revokeObjectURL|createObjectURL" -nRepository: IT-Cotato/12th-SimTok-FE Length of output: 153
구체적으로:
🤖 Prompt for AI Agents
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. 이거 관련해서 공유일기 업로드pr에서 구현해 두었으니 보고 참고하면 좋을 것 같습니다 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. 🧩 Analysis chain🏁 Script executed: Length of output: 262 🏁 Script executed: Length of output: 723 🏁 Script executed: Length of output: 36 🏁 Script executed: Length of output: 1392 🏁 Script executed: Length of output: 0 🏁 Script executed: Length of output: 10175 🏁 Script executed: Length of output: 0 🏁 Script executed: Length of output: 6727 🏁 Script executed: Length of output: 894 @lemoncurdyogurt 좋은 정보 감사합니다! PR #14 ("[FEAT] 하루한컷 업로드")를 확인해 보았는데, 아직 Object URL cleanup 관련 구현이 포함되어 있지 않은 것 같습니다. 현재 PR #14에서는 업로드 페이지 UI가 주로 추가되었고, 혹시 Object URL cleanup 구현이 다른 커밋이나 파일에 있나요? 구체적인 파일명이나 위치를 알려주시면 참고하여 이 온보딩 프로필 이미지 업로드 기능에도 동일한 패턴을 적용할 수 있도록 안내해 드리겠습니다. 아니면 해당 구현이 아직 진행 중이시라면, 완료되었을 때 다시 확인할 수 있도록 알려주시면 감사하겠습니다! 🙂 |
||
|
|
||
| const handleCreateProfile = async () => { | ||
| if (!isNameValid) return; | ||
|
|
||
| // 1) 프로필 데이터 저장 (지금은 임시로 localStorage 예시) | ||
| const profile = { | ||
| name: name.trim(), | ||
| imageUrl: profileImage ?? null, | ||
| }; | ||
| try { | ||
| // TODO: 실제 API로 교체 | ||
| // await fetch("/api/profile", { method: "POST", body: JSON.stringify(profile) }); | ||
| localStorage.setItem("onboardingProfile", JSON.stringify(profile)); | ||
| } catch (e) { | ||
| // 에러 처리 | ||
| console.error(e); | ||
| return; | ||
| } | ||
| // 2) 저장이 끝난 뒤에만 페이지 이동 | ||
| router.replace("/login"); | ||
| }; | ||
|
|
||
| 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={async file => { | ||
| try { | ||
| await uploadImage(file); | ||
| setIsUploadOpen(false); | ||
| } catch (error) { | ||
| console.error("이미지 업로드 실패:", error); | ||
| } | ||
| }} | ||
| onSelectDefault={() => { | ||
| resetImage(); | ||
| setIsUploadOpen(false); | ||
| }} | ||
| /> | ||
|
|
||
| <LoadingModal | ||
| isOpen={isLoading} | ||
| title="로딩중" | ||
| confirmLabel="취소하기" | ||
| isLoading | ||
| backdrop="blur" | ||
| onClose={cancelUpload} | ||
| /> | ||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| export default OnboardingProfileClient; | ||



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