Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
27 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
63e143b
feat: w-full 적용
idid10 Jan 10, 2026
14e162c
refactor: 반응형 화면 수정
idid10 Jan 10, 2026
8a4aa56
refactor: 새 이미지 업로드시 이전 blob URL 해제
idid10 Jan 12, 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
2 changes: 1 addition & 1 deletion package.json
Copy link
Member

Choose a reason for hiding this comment

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

browser라서 일단 funnel/next 코드 지워주세요오...!

Copy link
Member

Choose a reason for hiding this comment

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

내가 지움Vv

Copy link
Contributor Author

Choose a reason for hiding this comment

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

감사합니다

Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
]
},
"dependencies": {
"@toss/use-funnel": "^1.4.2",
"@use-funnel/next": "^0.0.22",
"axios": "^1.13.2",
"next": "16.0.10",
"react": "19.2.1",
Expand Down
9 changes: 9 additions & 0 deletions public/images/onboarding-step-1.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions public/images/onboarding-step-2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions public/images/onboarding-step-3.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
58 changes: 58 additions & 0 deletions src/app/onboarding/page.tsx
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">
<div className="z-10">
<ProgressDots
total={ONBOARDING_STEPS.length}
current={currentStepIndex}
/>
</div>

<OnboardingStep
stepName={currentStepName}
isLastStep={isLastStep}
onNext={goNext}
/>
</div>
</section>
);
};

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;
4 changes: 2 additions & 2 deletions src/app/page.tsx
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>;
}
28 changes: 3 additions & 25 deletions src/app/signup/agree/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/app/signup/password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ const SettingPage = () => {
<div className="mt-[320px] flex w-full justify-center">
<FullButton
isActive={isPasswordConfirmValid}
onClick={() => router.push("/login")}
onClick={() => router.push("/onboarding")}
>
완료
</FullButton>
Expand Down
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.
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
"use client";

type AlertModalProps = {
import { ALERT_BACKDROP_CLASS, AlertBackdrop } from "@/constants/alert";

interface LoadingModalProps {
isOpen: boolean;
title: string;
message: string;
message?: string;
confirmLabel?: string;
onClose: () => void;
};
backdrop?: AlertBackdrop;
isLoading?: boolean;
}

export const AlertModal = ({
const LoadingModal = ({
isOpen,
title,
message,
confirmLabel = "확인",
onClose,
}: AlertModalProps) => {
backdrop = "default",
isLoading = false,
}: LoadingModalProps) => {
if (!isOpen) return null;

return (
<div
className="bg-dim-02 fixed inset-0 z-50 flex items-center justify-center"
className={`fixed inset-0 z-50 flex items-center justify-center ${
ALERT_BACKDROP_CLASS[backdrop]
}`}
onClick={onClose}
>
<div
Expand All @@ -31,7 +39,15 @@ export 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 ? (
<span className="loader" />
) : (
message && (
<p className="text-sub2-r text-neutral-01 text-center">
{message}
</p>
)
)}
</div>

<div className="flex w-full">
Expand All @@ -47,3 +63,5 @@ export const AlertModal = ({
</div>
);
};

export default LoadingModal;
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) => {
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;
102 changes: 102 additions & 0 deletions src/components/onboarding/OnboardingProfile.tsx
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();

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>

<section className="absolute right-0 bottom-[42px] left-0 px-4 py-2.5">
<FullButton isActive={isNameValid} onClick={handleCreateProfile}>
프로필생성하기
</FullButton>
</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;
Loading