Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 0 additions & 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,6 @@
]
},
"dependencies": {
"@toss/use-funnel": "^1.4.2",
"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">
Copy link
Member

Choose a reason for hiding this comment

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

아이폰 노치부분은 와이어프레임에 해당하는 부분이라서 해당 부분 빼고 마진계산해주셔야합니다
image
이렇게 73px 가져오면 안돼요

<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;
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.
4 changes: 2 additions & 2 deletions src/components/common/FullButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ export const FullButton = ({
disabled={!isActive}
{...props}
>
<div className="flex items-center px-20.5">
<span className="text-h2 flex h-7.5 items-center">{children}</span>
<div className="flex h-[58px] items-center justify-center">
<span className="text-button-sb">{children}</span>
</div>
</button>
);
Expand Down
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) => {
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;
4 changes: 2 additions & 2 deletions src/components/dailyRecord/MissionFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@ export const RecordMissionFeed = () => {
<section className="flex gap-4">
{otherRecords.map(item => (
<Link key={item.id} href={`/day-story/${item.id}`}>
<div className="flex w-[88px] shrink-0 cursor-pointer flex-col items-center gap-2">
<div className="flex w-[88px] shrink-0 cursor-pointer flex-col items-center gap-2 py-1">
<Image
src={item.image}
alt={item.userName}
width={88}
height={88}
className={`${item.isRead ? "" : "border-green-03 border border-[4px] shadow-[0_0_12px_-1px_rgba(0,168,85,0.39)]"} h-[88px] w-[88px] rounded-full object-cover`}
className={`${item.isRead ? "" : "border-green-03 border border-[4px] shadow-[0_0_6px_-1px_rgba(0,168,85,0.50)]"} h-[88px] w-[88px] rounded-full object-cover`}
/>
<p className="text-neutral-03 text-d3">{item.userName}</p>
</div>
Expand Down
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();
Comment on lines +23 to +24
Copy link

@coderabbitai coderabbitai bot Jan 8, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find the useProfileImageUpload hook implementation
fd -e ts -e tsx | xargs grep -l "useProfileImageUpload" | grep -v node_modules

Repository: IT-Cotato/12th-SimTok-FE

Length of output: 150


🏁 Script executed:

# Search for revokeObjectURL usage in the codebase
rg -n "revokeObjectURL" --type ts --type tsx

Repository: IT-Cotato/12th-SimTok-FE

Length of output: 95


🏁 Script executed:

# Search for createObjectURL usage
rg -n "createObjectURL" --type ts --type tsx

Repository: IT-Cotato/12th-SimTok-FE

Length of output: 95


🏁 Script executed:

# Read the useProfileImageUpload hook implementation
cat -n src/hooks/useProfileImageUpload.ts

Repository: IT-Cotato/12th-SimTok-FE

Length of output: 1528


🏁 Script executed:

# Search for revokeObjectURL and createObjectURL without file type restrictions
rg "revokeObjectURL|createObjectURL" -n

Repository: IT-Cotato/12th-SimTok-FE

Length of output: 153


useProfileImageUpload 훅에서 Object URL 메모리 누수 문제 해결 필요.

URL.createObjectURL으로 생성된 blob URL이 URL.revokeObjectURL로 정리되지 않고 있습니다.

구체적으로:

  • 새 이미지 업로드 시 이전 blob URL이 해제되지 않음
  • resetImage() 호출 시에도 blob URL이 해제되지 않음
  • 컴포넌트 언마운트 시 cleanup이 없음
  • 사용자가 반복적으로 이미지를 업로드하면 메모리에 blob URL이 계속 누적됨

uploadImageresetImage 함수에서 상태 변경 전에 기존 blob URL을 URL.revokeObjectURL로 해제하고, useEffect cleanup에서도 언마운트 시 blob URL을 정리해야 합니다.

🤖 Prompt for AI Agents
In @src/components/onboarding/OnboardingProfile.tsx around lines 23 - 24, The
useProfileImageUpload hook leaks blob URLs; ensure any existing object URL
stored in profileImage (or equivalent state) is revoked with URL.revokeObjectURL
before replacing it in uploadImage and before clearing it in resetImage, and add
a useEffect cleanup in useProfileImageUpload that revokes the current blob URL
on unmount; update uploadImage to revoke the previous URL before setting a new
one, update resetImage to revoke then clear the state, and add a cleanup
callback that revokes profileImage when the hook/component unmounts.

Copy link
Member

Choose a reason for hiding this comment

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

이거 관련해서 공유일기 업로드pr에서 구현해 두었으니 보고 참고하면 좋을 것 같습니다

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for PRs related to shared diary upload
gh pr list --repo IT-Cotato/12th-SimTok-FE --limit 30 --state all --json number,title,state,url | jq -r '.[] | select(.title | test("공유|일기|업로드|upload|Upload"; "i")) | "\(.number): \(.title) (\(.state)) - \(.url)"'

Length of output: 262


🏁 Script executed:

#!/bin/bash
# Check PR #14 for Object URL cleanup implementation
gh pr view 14 --repo IT-Cotato/12th-SimTok-FE --json files,body | jq -r '.files[] | .path' | head -20

Length of output: 723


🏁 Script executed:

#!/bin/bash
# Check upload page files for Object URL cleanup implementation
gh pr diff 14 --repo IT-Cotato/12th-SimTok-FE -- src/app/day-story/upload/page.tsx src/app/shared-diary/upload/page.tsx | head -200

Length of output: 36


🏁 Script executed:

#!/bin/bash
# Check shared diary upload page for Object URL cleanup
gh pr diff 14 --repo IT-Cotato/12th-SimTok-FE | grep -A 30 -B 5 "revokeObjectURL\|createObjectURL\|useEffect.*cleanup\|return () =>"

Length of output: 1392


🏁 Script executed:

#!/bin/bash
# Search for image upload or file handling hooks in PR #14
gh pr diff 14 --repo IT-Cotato/12th-SimTok-FE | grep -B 10 -A 30 "useState.*image\|useState.*file\|handleImage\|handleFile\|File\[\]"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get the full diff of shared diary upload page
gh pr diff 14 --repo IT-Cotato/12th-SimTok-FE | grep -A 200 "src/app/shared-diary/upload/page.tsx"

Length of output: 10175


🏁 Script executed:

#!/bin/bash
# Search for hooks directory changes in PR #14
gh pr diff 14 --repo IT-Cotato/12th-SimTok-FE | grep -B 5 -A 100 "src/hooks\|useImage\|useUpload"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check day-story upload page in PR #14 for image handling
gh pr diff 14 --repo IT-Cotato/12th-SimTok-FE | grep -B 10 -A 100 "src/app/day-story/upload"

Length of output: 6727


🏁 Script executed:

#!/bin/bash
# List all changed files in PR #14 to see if there are any hooks or image handling files
gh pr view 14 --repo IT-Cotato/12th-SimTok-FE --json files | jq -r '.files[].path' | sort

Length of output: 894


@lemoncurdyogurt 좋은 정보 감사합니다! PR #14 ("[FEAT] 하루한컷 업로드")를 확인해 보았는데, 아직 Object URL cleanup 관련 구현이 포함되어 있지 않은 것 같습니다. 현재 PR #14에서는 업로드 페이지 UI가 주로 추가되었고, src/app/shared-diary/upload/page.tsx 파일은 아직 플레이스홀더 상태입니다.

혹시 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
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={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