Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d9d9659
feat:온보딩 step
idid10 Jan 3, 2026
81222b9
feat: 프로필 업로드 ui
idid10 Jan 3, 2026
f4a9e4a
feat: loading 모달 추가
idid10 Jan 4, 2026
4f7e2c5
style: 프로필 업로드 훅 분리
idid10 Jan 4, 2026
de71818
design: 수정사항 반영
idid10 Jan 4, 2026
b2bcc6d
design:프로필 이미지 영역을 ProfileImagePicker로 분리
idid10 Jan 4, 2026
74cb410
design:이름 입력 인풋 너비 자동 조정
idid10 Jan 4, 2026
9243742
Merge branch 'develop' of https://github.com/IT-Cotato/12th-SimTok-FE…
idid10 Jan 6, 2026
2499166
refactor: loading gif삭제하고 직접 구현
idid10 Jan 6, 2026
f522b7d
refactor: 회원가입 -> 온보딩 -> 홈
idid10 Jan 6, 2026
d80c9e7
refactor: 비밀번호 valid 로직 변경사항 반영
idid10 Jan 6, 2026
8c7048d
refactor: agree type, constants분리
idid10 Jan 6, 2026
3b7dc44
fix: onboardingfunnelsteps object
idid10 Jan 6, 2026
a4d14e0
fix: 온보딩 배경 width
idid10 Jan 6, 2026
1483d0c
refactor: redirect 홈화면으로 수정
idid10 Jan 6, 2026
3e4517f
refactor: handlefilechange 동일한 파일 재선택 가능하도록 수정
idid10 Jan 7, 2026
f0e0b67
refactor: 모달 접근성 개선
idid10 Jan 7, 2026
2b1e02d
refactor: 이미지 업로드 에러 처리
idid10 Jan 7, 2026
059fed1
refactor: 프로필 업로드 중단 로직 추가
idid10 Jan 7, 2026
6317409
refactor: useFunnel 삭제
idid10 Jan 7, 2026
14cfb75
Merge brahcn 'develop' into feat/6-onboarding'
idid10 Jan 8, 2026
f1559ac
refactor: FullButton import 변경
idid10 Jan 8, 2026
142e82d
refactor: full button 코드 수정
Jan 9, 2026
58563bc
design: 하루한컷 피드 box-shadow 수정
Jan 9, 2026
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
Binary file added public/images/onboarding-step-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/onboarding-step-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/onboarding-step-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/loading.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import { useRouter } from "next/navigation";

import KakaoIcon from "@/assets/kakao.svg";

const AuthStartPage = () => {
const router = useRouter();

Expand Down
52 changes: 52 additions & 0 deletions src/app/onboarding/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use client";

import { useRouter } from "next/navigation";

import React from "react";

import OnboardingStep from "../../components/onboarding/OnboardingStep";
import ProgressDots from "../../components/onboarding/ProgressDots";
import {
ONBOARDING_STEPS,
OnboardingStepName,
} from "../../config/onboardingSteps";
import { useFunnel } from "../../hooks/useFunnel";

const OnboardingPage = () => {
const router = useRouter();
const { Funnel, Step, setStep, currentStep } = useFunnel(ONBOARDING_STEPS[0]);

const currentIndex = ONBOARDING_STEPS.indexOf(
currentStep as OnboardingStepName,
);
const isLast = currentIndex === ONBOARDING_STEPS.length - 1;

const goNext = () => {
const next = ONBOARDING_STEPS[currentIndex + 1];

if (!next) {
localStorage.setItem("onboardingDone", "true");
router.push("/onboarding/profile");
return;
}
setStep(next);
};

return (
<main className="flex min-h-dvh justify-center">
<div className="mt-[13px] flex h-full w-[440px] flex-col">
<ProgressDots total={ONBOARDING_STEPS.length} current={currentIndex} />

<Funnel>
{ONBOARDING_STEPS.map(name => (
<Step key={name} name={name}>
<OnboardingStep stepName={name} isLast={isLast} onNext={goNext} />
</Step>
))}
</Funnel>
</div>
</main>
);
};

export default OnboardingPage;
6 changes: 6 additions & 0 deletions src/app/onboarding/profile/page.tsx
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;
6 changes: 4 additions & 2 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export default function Home() {
return <div>홈화면</div>;
import { redirect } from "next/navigation";

export default function RootPage() {
redirect("/onboarding");
}
11 changes: 11 additions & 0 deletions src/assets/onboarding_profile.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions src/assets/photo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 35 additions & 4 deletions src/components/common/AlertModal.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
"use client";

type AlertBackdrop = "default" | "light" | "blur";

type AlertModalProps = {
isOpen: boolean;
title: string;
message: string;

message?: string;

confirmLabel?: string;
onClose: () => void;

backdrop?: AlertBackdrop;

isLoading?: boolean;
loadingImageSrc?: string;
};

const BACKDROP_CLASS: Record<AlertBackdrop, string> = {
default: "bg-dim-02",
light: "bg-dim-01",
blur: "bg-dim-01 backdrop-blur-[7px]",
};

const AlertModal = ({
Expand All @@ -14,13 +29,17 @@ const AlertModal = ({
message,
confirmLabel = "확인",
onClose,
backdrop = "default",
isLoading = false,
loadingImageSrc,
}: AlertModalProps) => {
if (!isOpen) return null;

return (
<div
className="bg-dim-02 fixed inset-0 z-50 flex items-center justify-center"
onClick={onClose}
className={`fixed inset-0 z-50 flex items-center justify-center ${
BACKDROP_CLASS[backdrop]
}`}
>
<div
className="flex w-[316px] flex-col rounded-2xl bg-white"
Expand All @@ -31,7 +50,19 @@ const AlertModal = ({
</div>

<div className="flex items-center justify-center px-[10px] py-[7px]">
<p className="text-sub2-r text-neutral-01 text-center">{message}</p>
{isLoading && loadingImageSrc ? (
<img
src={loadingImageSrc}
alt="loading"
className="h-[48px] w-[48px]"
/>
) : (
message && (
<p className="text-sub2-r text-neutral-01 text-center">
{message}
</p>
)
)}
</div>

<div className="flex w-full">
Expand Down
42 changes: 42 additions & 0 deletions src/components/common/NameInput.tsx
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) => {
Copy link
Member

Choose a reason for hiding this comment

The 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;
72 changes: 72 additions & 0 deletions src/components/onboarding/OnboardingProfile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use client";

import { useState } from "react";

import AlertModal from "@/components/common/AlertModal";
import FullButton from "@/components/common/FullButton";
import NameInput from "@/components/common/NameInput";
import ProfileImagePicker from "@/components/onboarding/ProfileImagePicker";
import UploadAlert from "@/components/onboarding/UploadAlert";

import { useProfileImageUpload } from "@/hooks/useProfileImageUpload";

const OnboardingProfileClient = () => {
const [name, setName] = useState("");
const [isUploadOpen, setIsUploadOpen] = useState(false);

const isNameValid = name.trim().length > 0;

const { profileImage, isLoading, uploadImage, resetImage } =
useProfileImageUpload();

return (
<>
<main className="flex min-h-dvh justify-center">
<div className="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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기는 flex구조라서 mt-4로 간격 넣는 것보다는 gap-4로 간격 넣는게 더 좋을 것 같습니다


<section className="mt-[210px] mb-[42px] px-4 py-2.5">
<FullButton isActive={isNameValid}>프로필생성하기</FullButton>
</section>
</div>
</main>
<UploadAlert
isOpen={isUploadOpen}
onClose={() => setIsUploadOpen(false)}
onSelectAlbum={file => {
uploadImage(file);
setIsUploadOpen(false);
}}
onSelectDefault={() => {
resetImage();
setIsUploadOpen(false);
}}
/>

<AlertModal
isOpen={isLoading}
title="로딩중"
confirmLabel="취소하기"
isLoading
loadingImageSrc="/loading.gif"
backdrop="blur"
onClose={() => {}}
/>
</>
);
};

export default OnboardingProfileClient;
48 changes: 48 additions & 0 deletions src/components/onboarding/OnboardingStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Image from "next/image";

import FullButton from "@/components/common/FullButton";

import type { OnboardingStepName } from "../../config/onboardingSteps";
import { onboardingContents } from "../../config/onboardingSteps";

interface Props {
stepName: OnboardingStepName;
isLast: boolean;
onNext: () => void;
}

const OnboardingStep = ({ stepName, isLast, onNext }: Props) => {
const { title, background } = onboardingContents[stepName];

return (
<main className="relative flex h-screen flex-col bg-white 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>

<section className="relative z-10 px-4 pt-[23px] pb-[78px]">
<FullButton type="button" onClick={onNext} isActive>
{isLast ? "시작하기" : "다음"}
</FullButton>
</section>
</main>
);
};

export default OnboardingStep;
37 changes: 37 additions & 0 deletions src/components/onboarding/ProfileImagePicker.tsx
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"
Copy link
Member

Choose a reason for hiding this comment

The 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]"
>
<PhotoIcon />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

button에 아이콘 크기를 지정하는게 아니라 아이콘 내부에 h-8 w-8을 적용해야할 것 같습니다.
safari에서만의 문제이긴한데 asset들에 크기를 지정안해주면 크기가 자동으로 0으로 잡히는 오류가 있어요

safari svgr not showing error

</button>
</div>
);
};

export default ProfileImagePicker;
Loading