Skip to content
Open
Show file tree
Hide file tree
Changes from 15 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
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

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.
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
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 <main>홈 화면</main>;
}
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;
Copy link
Member

Choose a reason for hiding this comment

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

image

변수명에 축약어 Req라던가 이러거에 있어서 쓰는 것에 대한 단점에 대한 얘기입니다....보시고 수정한번....부탁드립니다

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,35 @@
"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;
loadingImageSrc?: string;
}

const AlertModal = ({
const LoadingModal = ({
isOpen,
title,
message,
confirmLabel = "확인",
onClose,
}: AlertModalProps) => {
backdrop = "default",
isLoading = false,
loadingImageSrc,
}: 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 +41,15 @@ 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 @@ -48,4 +66,4 @@ const AlertModal = ({
);
};

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

const handleCreateProfile = () => {
if (!isNameValid) return;
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
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="absolute right-0 bottom-[42px] left-0 px-4 py-2.5">
<FullButton isActive={isNameValid} onClick={handleCreateProfile}>
프로필생성하기
</FullButton>
Comment on lines +67 to +69
Copy link
Member

Choose a reason for hiding this comment

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

{!isUploadOpen && <FullButton></FullButton>} 과 같이 모달이 열려있을 때 뒤쪽 버튼이 안보였으면 좋을 것 같아요

Copy link
Member

Choose a reason for hiding this comment

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

image

이렇게 뒤에 미묘하게 선이 보여서 ㅠ 안예쁨..

</section>
</div>
</main>
<UploadButton
isOpen={isUploadOpen}
onClose={() => setIsUploadOpen(false)}
onSelectAlbum={file => {
uploadImage(file);
setIsUploadOpen(false);
}}
onSelectDefault={() => {
resetImage();
setIsUploadOpen(false);
}}
/>

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

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

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

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

이 하단버튼의 경우 네비바와 같이 화면에서 고정되어있는 영역이라서 relative가 아니라 fixed bottom-0과 같이 사용해주셔야합니다

Copy link
Member

Choose a reason for hiding this comment

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

image 여기 마진 고려해서 만들어주세용

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

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] cursor-pointer"
>
<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>
Comment on lines +27 to +32
Copy link
Member

Choose a reason for hiding this comment

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

image image

이거 지금 버튼 위치 다른데
left-[129.5px] top-[120px]
이렇게 해야할 것 같
습니다

</div>
);
};

export default ProfileImagePicker;
Loading