From 4414a0b61d2682d0694faae167a1dee3c75a875b Mon Sep 17 00:00:00 2001 From: KwonOjin Date: Thu, 18 Jun 2026 01:40:30 +0900 Subject: [PATCH 01/10] =?UTF-8?q?#33=20[REFACTOR]=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EA=B3=B5=ED=86=B5=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8D=BC=EB=84=90=20config=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/signup/config/signupFunnel.ts | 8 ++++++++ src/features/signup/index.ts | 2 ++ src/features/signup/model/signup.ts | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 src/features/signup/config/signupFunnel.ts create mode 100644 src/features/signup/model/signup.ts diff --git a/src/features/signup/config/signupFunnel.ts b/src/features/signup/config/signupFunnel.ts new file mode 100644 index 0000000..358a609 --- /dev/null +++ b/src/features/signup/config/signupFunnel.ts @@ -0,0 +1,8 @@ +import type { SignupFunnelStep, SignupRole, SignupRoleStep } from "../model/signup"; + +export const SIGNUP_INITIAL_STEP = "role" satisfies SignupFunnelStep; + +export const SIGNUP_STEPS_BY_ROLE = { + designer: ["termsProfile", "account", "designerAdditional"], + instructor: ["termsProfile", "account"], +} as const satisfies Record; diff --git a/src/features/signup/index.ts b/src/features/signup/index.ts index 760d709..c42e640 100644 --- a/src/features/signup/index.ts +++ b/src/features/signup/index.ts @@ -1,4 +1,6 @@ export * from "./config/signup"; +export * from "./config/signupFunnel"; +export * from "./model/signup"; export { useSignupStep2Form } from "./model/useSignupStep2Form"; export { default as BankDropdown } from "./ui/BankDropdown"; export { default as UserTypeBtn } from "./ui/UserTypeBtn"; diff --git a/src/features/signup/model/signup.ts b/src/features/signup/model/signup.ts new file mode 100644 index 0000000..297247f --- /dev/null +++ b/src/features/signup/model/signup.ts @@ -0,0 +1,5 @@ +export type SignupRole = "designer" | "instructor"; + +export type SignupFunnelStep = "role" | "termsProfile" | "account" | "designerAdditional"; + +export type SignupRoleStep = Exclude; From f6edc07ab8a895355e2134fb0141ba2f910f27c5 Mon Sep 17 00:00:00 2001 From: KwonOjin Date: Thu, 18 Jun 2026 01:45:02 +0900 Subject: [PATCH 02/10] =?UTF-8?q?#33=20[REFACTOR]=20step1=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20UI=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/signup/designer/step1/page.tsx | 194 +------------------- src/app/signup/instructor/step1/page.tsx | 194 +------------------- src/features/signup/index.ts | 1 + src/features/signup/ui/TermsProfileStep.tsx | 193 +++++++++++++++++++ 4 files changed, 210 insertions(+), 372 deletions(-) create mode 100644 src/features/signup/ui/TermsProfileStep.tsx diff --git a/src/app/signup/designer/step1/page.tsx b/src/app/signup/designer/step1/page.tsx index 2d43133..63c2acc 100644 --- a/src/app/signup/designer/step1/page.tsx +++ b/src/app/signup/designer/step1/page.tsx @@ -1,198 +1,20 @@ "use client"; import { useRouter } from "next/navigation"; -import { type ChangeEvent, useState } from "react"; -import { - DESIGNER_TERMS, - SIGNUP_MAX_NAME_LENGTH, - SIGNUP_MAX_PHONE_NUMBER_LENGTH, -} from "@/features/signup"; -import { - CheckboxFillIcon, - CheckboxGrayIcon, - CloseIcon, - StepOneDesignerIcon, -} from "@/shared/assets/icons"; -import Button from "@/shared/ui/Button"; -import InputField from "@/shared/ui/input/InputField"; - -type DesignerTermsId = (typeof DESIGNER_TERMS)[number]["id"]; - -const CheckIcon = ({ isChecked }: { isChecked: boolean }) => { - const Icon = isChecked ? CheckboxFillIcon : CheckboxGrayIcon; - - return ; -}; - -const formatPhoneNumber = (value: string) => { - const numbers = value.replace(/\D/g, "").slice(0, SIGNUP_MAX_PHONE_NUMBER_LENGTH); - - if (numbers.length <= 3) return numbers; - if (numbers.length <= 7) return `${numbers.slice(0, 3)}-${numbers.slice(3)}`; - - return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7)}`; -}; - -const createCheckedTerms = (value: boolean) => - Object.fromEntries(DESIGNER_TERMS.map(({ id }) => [id, value])) as Record< - DesignerTermsId, - boolean - >; +import { DESIGNER_TERMS, TermsProfileStep } from "@/features/signup"; +import { StepOneDesignerIcon } from "@/shared/assets/icons"; const Page = () => { const router = useRouter(); - const [name, setName] = useState(""); - const [phoneNumber, setPhoneNumber] = useState(""); - const [selectedTermId, setSelectedTermId] = useState(null); - const [checkedTerms, setCheckedTerms] = useState>(() => - createCheckedTerms(false), - ); - - const selectedTerm = DESIGNER_TERMS.find(({ id }) => id === selectedTermId); - const isAllAgreed = DESIGNER_TERMS.every(({ id }) => checkedTerms[id]); - const isNextEnabled = isAllAgreed && name.trim().length > 0 && phoneNumber.trim().length > 0; - - const toggleAllTerms = () => { - const nextValue = !isAllAgreed; - - setCheckedTerms(createCheckedTerms(nextValue)); - }; - - const toggleTerm = (termId: DesignerTermsId) => { - setCheckedTerms(prev => ({ ...prev, [termId]: !prev[termId] })); - }; - - const handleNameChange = (event: ChangeEvent) => { - setName(event.target.value.slice(0, SIGNUP_MAX_NAME_LENGTH)); - }; - - const handlePhoneNumberChange = (event: ChangeEvent) => { - setPhoneNumber(formatPhoneNumber(event.target.value)); - }; return ( -
-
-
-
-
-

회원가입

- -
- -
-
-

약관 동의

- -
- - -
- -
- {DESIGNER_TERMS.map(({ id, label }) => ( -
- - - -
- ))} -
-
-
- - setName("")} - onChange={handleNameChange} - /> - setPhoneNumber("")} - onChange={handlePhoneNumberChange} - /> -
-
- -
- - -
-
-
- - {selectedTerm != null && ( -
setSelectedTermId(null)} - > -
event.stopPropagation()} - > -
-

{selectedTerm.modalTitle}

- -
-
-

- {selectedTerm.content} -

-
-
- )} -
+ } + onPrev={() => router.push("/signup")} + onNext={() => router.push("/signup/designer/step2")} + /> ); }; diff --git a/src/app/signup/instructor/step1/page.tsx b/src/app/signup/instructor/step1/page.tsx index 9985ff3..01d0bd8 100644 --- a/src/app/signup/instructor/step1/page.tsx +++ b/src/app/signup/instructor/step1/page.tsx @@ -1,198 +1,20 @@ "use client"; import { useRouter } from "next/navigation"; -import { type ChangeEvent, useState } from "react"; -import { - INSTRUCTOR_TERMS, - SIGNUP_MAX_NAME_LENGTH, - SIGNUP_MAX_PHONE_NUMBER_LENGTH, -} from "@/features/signup"; -import { - CheckboxFillIcon, - CheckboxGrayIcon, - CloseIcon, - StepOneInstructorIcon, -} from "@/shared/assets/icons"; -import Button from "@/shared/ui/Button"; -import InputField from "@/shared/ui/input/InputField"; - -type InstructorTermsId = (typeof INSTRUCTOR_TERMS)[number]["id"]; - -const CheckIcon = ({ isChecked }: { isChecked: boolean }) => { - const Icon = isChecked ? CheckboxFillIcon : CheckboxGrayIcon; - - return ; -}; - -const formatPhoneNumber = (value: string) => { - const numbers = value.replace(/\D/g, "").slice(0, SIGNUP_MAX_PHONE_NUMBER_LENGTH); - - if (numbers.length <= 3) return numbers; - if (numbers.length <= 7) return `${numbers.slice(0, 3)}-${numbers.slice(3)}`; - - return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7)}`; -}; - -const createCheckedTerms = (value: boolean) => - Object.fromEntries(INSTRUCTOR_TERMS.map(({ id }) => [id, value])) as Record< - InstructorTermsId, - boolean - >; +import { INSTRUCTOR_TERMS, TermsProfileStep } from "@/features/signup"; +import { StepOneInstructorIcon } from "@/shared/assets/icons"; const Page = () => { const router = useRouter(); - const [name, setName] = useState(""); - const [phoneNumber, setPhoneNumber] = useState(""); - const [selectedTermId, setSelectedTermId] = useState(null); - const [checkedTerms, setCheckedTerms] = useState>(() => - createCheckedTerms(false), - ); - - const selectedTerm = INSTRUCTOR_TERMS.find(({ id }) => id === selectedTermId); - const isAllAgreed = INSTRUCTOR_TERMS.every(({ id }) => checkedTerms[id]); - const isNextEnabled = isAllAgreed && name.trim().length > 0 && phoneNumber.trim().length > 0; - - const toggleAllTerms = () => { - const nextValue = !isAllAgreed; - - setCheckedTerms(createCheckedTerms(nextValue)); - }; - - const toggleTerm = (termId: InstructorTermsId) => { - setCheckedTerms(prev => ({ ...prev, [termId]: !prev[termId] })); - }; - - const handleNameChange = (event: ChangeEvent) => { - setName(event.target.value.slice(0, SIGNUP_MAX_NAME_LENGTH)); - }; - - const handlePhoneNumberChange = (event: ChangeEvent) => { - setPhoneNumber(formatPhoneNumber(event.target.value)); - }; return ( -
-
-
-
-
-

회원가입

- -
- -
-
-

약관 동의

- -
- - -
- -
- {INSTRUCTOR_TERMS.map(({ id, label }) => ( -
- - - -
- ))} -
-
-
- - setName("")} - onChange={handleNameChange} - /> - setPhoneNumber("")} - onChange={handlePhoneNumberChange} - /> -
-
- -
- - -
-
-
- - {selectedTerm != null && ( -
setSelectedTermId(null)} - > -
event.stopPropagation()} - > -
-

{selectedTerm.modalTitle}

- -
-
-

- {selectedTerm.content} -

-
-
- )} -
+ } + onPrev={() => router.push("/signup")} + onNext={() => router.push("/signup/instructor/step2")} + /> ); }; diff --git a/src/features/signup/index.ts b/src/features/signup/index.ts index c42e640..613d5ee 100644 --- a/src/features/signup/index.ts +++ b/src/features/signup/index.ts @@ -3,4 +3,5 @@ export * from "./config/signupFunnel"; export * from "./model/signup"; export { useSignupStep2Form } from "./model/useSignupStep2Form"; export { default as BankDropdown } from "./ui/BankDropdown"; +export { default as TermsProfileStep } from "./ui/TermsProfileStep"; export { default as UserTypeBtn } from "./ui/UserTypeBtn"; diff --git a/src/features/signup/ui/TermsProfileStep.tsx b/src/features/signup/ui/TermsProfileStep.tsx new file mode 100644 index 0000000..b05ee7f --- /dev/null +++ b/src/features/signup/ui/TermsProfileStep.tsx @@ -0,0 +1,193 @@ +"use client"; + +import { type ChangeEvent, type ReactNode, useState } from "react"; + +import { CheckboxFillIcon, CheckboxGrayIcon, CloseIcon } from "@/shared/assets/icons"; +import Button from "@/shared/ui/Button"; +import InputField from "@/shared/ui/input/InputField"; + +import { SIGNUP_MAX_NAME_LENGTH, SIGNUP_MAX_PHONE_NUMBER_LENGTH } from "../config/signup"; + +type SignupTerm = { + id: string; + label: string; + modalTitle: string; + content: string; +}; + +type TermsProfileStepProps = { + terms: readonly SignupTerm[]; + progressIcon: ReactNode; + onPrev: () => void; + onNext: () => void; +}; + +const CheckIcon = ({ isChecked }: { isChecked: boolean }) => { + const Icon = isChecked ? CheckboxFillIcon : CheckboxGrayIcon; + + return ; +}; + +const formatPhoneNumber = (value: string) => { + const numbers = value.replace(/\D/g, "").slice(0, SIGNUP_MAX_PHONE_NUMBER_LENGTH); + + if (numbers.length <= 3) return numbers; + if (numbers.length <= 7) return `${numbers.slice(0, 3)}-${numbers.slice(3)}`; + + return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7)}`; +}; + +const createCheckedTerms = (terms: readonly SignupTerm[], value: boolean) => + Object.fromEntries(terms.map(({ id }) => [id, value])) as Record; + +const TermsProfileStep = ({ terms, progressIcon, onPrev, onNext }: TermsProfileStepProps) => { + const [name, setName] = useState(""); + const [phoneNumber, setPhoneNumber] = useState(""); + const [selectedTermId, setSelectedTermId] = useState(null); + const [checkedTerms, setCheckedTerms] = useState>(() => + createCheckedTerms(terms, false), + ); + + const selectedTerm = terms.find(({ id }) => id === selectedTermId); + const isAllAgreed = terms.every(({ id }) => checkedTerms[id]); + const isNextEnabled = isAllAgreed && name.trim().length > 0 && phoneNumber.trim().length > 0; + + const toggleAllTerms = () => { + const nextValue = !isAllAgreed; + + setCheckedTerms(createCheckedTerms(terms, nextValue)); + }; + + const toggleTerm = (termId: string) => { + setCheckedTerms(prev => ({ ...prev, [termId]: !prev[termId] })); + }; + + const handleNameChange = (event: ChangeEvent) => { + setName(event.target.value.slice(0, SIGNUP_MAX_NAME_LENGTH)); + }; + + const handlePhoneNumberChange = (event: ChangeEvent) => { + setPhoneNumber(formatPhoneNumber(event.target.value)); + }; + + return ( +
+
+
+
+
+

회원가입

+ {progressIcon} +
+ +
+
+

약관 동의

+ +
+ + +
+ +
+ {terms.map(({ id, label }) => ( +
+ + + +
+ ))} +
+
+
+ + setName("")} + onChange={handleNameChange} + /> + setPhoneNumber("")} + onChange={handlePhoneNumberChange} + /> +
+
+ +
+ + +
+
+
+ + {selectedTerm != null && ( +
setSelectedTermId(null)} + > +
event.stopPropagation()} + > +
+

{selectedTerm.modalTitle}

+ +
+
+

+ {selectedTerm.content} +

+
+
+ )} +
+ ); +}; + +export default TermsProfileStep; From 28423811cf49c58aee9455e0d76c82d4374b6d85 Mon Sep 17 00:00:00 2001 From: KwonOjin Date: Thu, 18 Jun 2026 01:48:53 +0900 Subject: [PATCH 03/10] =?UTF-8?q?#33=20[REFACTOR]=20step2=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20UI=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/signup/designer/step2/page.tsx | 158 +--------------------- src/app/signup/instructor/step2/page.tsx | 158 +--------------------- src/features/signup/index.ts | 1 + src/features/signup/ui/AccountStep.tsx | 163 +++++++++++++++++++++++ 4 files changed, 178 insertions(+), 302 deletions(-) create mode 100644 src/features/signup/ui/AccountStep.tsx diff --git a/src/app/signup/designer/step2/page.tsx b/src/app/signup/designer/step2/page.tsx index 5842e6a..f46b849 100644 --- a/src/app/signup/designer/step2/page.tsx +++ b/src/app/signup/designer/step2/page.tsx @@ -2,163 +2,19 @@ import { useRouter } from "next/navigation"; -import { - SIGNUP_MAX_ID_LENGTH, - SIGNUP_MAX_PASSWORD_LENGTH, - useSignupStep2Form, -} from "@/features/signup"; +import { AccountStep } from "@/features/signup"; import { StepTwoDesignerIcon } from "@/shared/assets/icons"; -import Button from "@/shared/ui/Button"; -import InputField from "@/shared/ui/input/InputField"; const Page = () => { const router = useRouter(); - const form = useSignupStep2Form(); return ( -
-
-
-
-
-

회원가입

- -
- -
-
-

아이디

-
-
- - -
- {form.userIdMessage != null && ( -

- {form.userIdMessage} -

- )} -
-
- - 0 && form.isPasswordValid} - label="비밀번호" - maxLength={SIGNUP_MAX_PASSWORD_LENGTH} - placeholder="비밀번호를 입력해주세요" - showPasswordToggle - type="password" - value={form.password} - onChange={form.handlePasswordChange} - /> - - -
-

이메일

-
- - -
- {form.isVerificationCodeVisible && ( - - {form.verificationTimerText} - - ) : undefined - } - value={form.verificationCode} - onChange={form.handleVerificationCodeChange} - /> - )} -
-
-
- -
- - -
-
-
-
+ } + nextButtonText="다음" + onPrev={() => router.push("/signup/designer/step1")} + onNext={() => router.push("/signup/designer/step3")} + /> ); }; diff --git a/src/app/signup/instructor/step2/page.tsx b/src/app/signup/instructor/step2/page.tsx index 37b0b56..48458c2 100644 --- a/src/app/signup/instructor/step2/page.tsx +++ b/src/app/signup/instructor/step2/page.tsx @@ -2,163 +2,19 @@ import { useRouter } from "next/navigation"; -import { - SIGNUP_MAX_ID_LENGTH, - SIGNUP_MAX_PASSWORD_LENGTH, - useSignupStep2Form, -} from "@/features/signup"; +import { AccountStep } from "@/features/signup"; import { StepTwoInstructorIcon } from "@/shared/assets/icons"; -import Button from "@/shared/ui/Button"; -import InputField from "@/shared/ui/input/InputField"; const Page = () => { const router = useRouter(); - const form = useSignupStep2Form(); return ( -
-
-
-
-
-

회원가입

- -
- -
-
-

아이디

-
-
- - -
- {form.userIdMessage != null && ( -

- {form.userIdMessage} -

- )} -
-
- - 0 && form.isPasswordValid} - label="비밀번호" - maxLength={SIGNUP_MAX_PASSWORD_LENGTH} - placeholder="비밀번호를 입력해주세요" - showPasswordToggle - type="password" - value={form.password} - onChange={form.handlePasswordChange} - /> - - -
-

이메일

-
- - -
- {form.isVerificationCodeVisible && ( - - {form.verificationTimerText} - - ) : undefined - } - value={form.verificationCode} - onChange={form.handleVerificationCodeChange} - /> - )} -
-
-
- -
- - -
-
-
-
+ } + nextButtonText="가입하기" + onPrev={() => router.push("/signup/instructor/step1")} + onNext={() => router.push("/login")} + /> ); }; diff --git a/src/features/signup/index.ts b/src/features/signup/index.ts index 613d5ee..38eb8d5 100644 --- a/src/features/signup/index.ts +++ b/src/features/signup/index.ts @@ -2,6 +2,7 @@ export * from "./config/signup"; export * from "./config/signupFunnel"; export * from "./model/signup"; export { useSignupStep2Form } from "./model/useSignupStep2Form"; +export { default as AccountStep } from "./ui/AccountStep"; export { default as BankDropdown } from "./ui/BankDropdown"; export { default as TermsProfileStep } from "./ui/TermsProfileStep"; export { default as UserTypeBtn } from "./ui/UserTypeBtn"; diff --git a/src/features/signup/ui/AccountStep.tsx b/src/features/signup/ui/AccountStep.tsx new file mode 100644 index 0000000..624d5f9 --- /dev/null +++ b/src/features/signup/ui/AccountStep.tsx @@ -0,0 +1,163 @@ +"use client"; + +import { type ReactNode } from "react"; + +import Button from "@/shared/ui/Button"; +import InputField from "@/shared/ui/input/InputField"; + +import { SIGNUP_MAX_ID_LENGTH, SIGNUP_MAX_PASSWORD_LENGTH } from "../config/signup"; +import { useSignupStep2Form } from "../model/useSignupStep2Form"; + +type AccountStepProps = { + progressIcon: ReactNode; + nextButtonText: string; + onPrev: () => void; + onNext: () => void; +}; + +const AccountStep = ({ progressIcon, nextButtonText, onPrev, onNext }: AccountStepProps) => { + const form = useSignupStep2Form(); + + return ( +
+
+
+
+
+

회원가입

+ {progressIcon} +
+ +
+
+

아이디

+
+
+ + +
+ {form.userIdMessage != null && ( +

+ {form.userIdMessage} +

+ )} +
+
+ + 0 && form.isPasswordValid} + label="비밀번호" + maxLength={SIGNUP_MAX_PASSWORD_LENGTH} + placeholder="비밀번호를 입력해주세요" + showPasswordToggle + type="password" + value={form.password} + onChange={form.handlePasswordChange} + /> + + +
+

이메일

+
+ + +
+ {form.isVerificationCodeVisible && ( + + {form.verificationTimerText} + + ) : undefined + } + value={form.verificationCode} + onChange={form.handleVerificationCodeChange} + /> + )} +
+
+
+ +
+ + +
+
+
+
+ ); +}; + +export default AccountStep; From 353263b43cb15eaed3d6ed6b8badb49b08263628 Mon Sep 17 00:00:00 2001 From: KwonOjin Date: Thu, 18 Jun 2026 01:51:07 +0900 Subject: [PATCH 04/10] =?UTF-8?q?#33=20[REFACTOR]=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EC=9C=A0=ED=98=95=20=EC=84=A0=ED=83=9D=20=EB=8B=A8=EA=B3=84=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/signup/page.tsx | 53 ++------------------ src/features/signup/index.ts | 1 + src/features/signup/ui/UserTypeStep.tsx | 66 +++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 50 deletions(-) create mode 100644 src/features/signup/ui/UserTypeStep.tsx diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index ea40e64..c3e3539 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -1,64 +1,17 @@ "use client"; import { useRouter } from "next/navigation"; -import { useState } from "react"; -import { UserTypeBtn } from "@/features/signup"; -import { UserTypeDesignerIcon, UserTypeInstructorIcon } from "@/shared/assets/icons"; -import Button from "@/shared/ui/Button"; - -type UserType = "designer" | "instructor"; +import { type SignupRole, UserTypeStep } from "@/features/signup"; const Page = () => { const router = useRouter(); - const [selectedType, setSelectedType] = useState(null); - - const handleNextClick = () => { - if (selectedType == null) return; + const handleNext = (selectedType: SignupRole) => { router.push(`/signup/${selectedType}/step1`); }; - return ( -
-
-
-
-
-

회원가입을 진행하기 전!

-

가입하려는 회원 유형을 선택해주세요

-
- -
- } - type="디자이너" - description="외주를 맡고 싶어요!" - isSelected={selectedType === "designer"} - onClick={() => setSelectedType("designer")} - /> - } - type="강사/교사" - description="외주를 맡기고 싶어요!" - isSelected={selectedType === "instructor"} - onClick={() => setSelectedType("instructor")} - /> -
-
- - -
-
-
- ); + return ; }; export default Page; diff --git a/src/features/signup/index.ts b/src/features/signup/index.ts index 38eb8d5..c997da3 100644 --- a/src/features/signup/index.ts +++ b/src/features/signup/index.ts @@ -6,3 +6,4 @@ export { default as AccountStep } from "./ui/AccountStep"; export { default as BankDropdown } from "./ui/BankDropdown"; export { default as TermsProfileStep } from "./ui/TermsProfileStep"; export { default as UserTypeBtn } from "./ui/UserTypeBtn"; +export { default as UserTypeStep } from "./ui/UserTypeStep"; diff --git a/src/features/signup/ui/UserTypeStep.tsx b/src/features/signup/ui/UserTypeStep.tsx new file mode 100644 index 0000000..634a51b --- /dev/null +++ b/src/features/signup/ui/UserTypeStep.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { useState } from "react"; + +import { UserTypeDesignerIcon, UserTypeInstructorIcon } from "@/shared/assets/icons"; +import Button from "@/shared/ui/Button"; + +import type { SignupRole } from "../model/signup"; +import UserTypeBtn from "./UserTypeBtn"; + +type UserTypeStepProps = { + onNext: (selectedType: SignupRole) => void; +}; + +const UserTypeStep = ({ onNext }: UserTypeStepProps) => { + const [selectedType, setSelectedType] = useState(null); + + const handleNextClick = () => { + if (selectedType == null) return; + + onNext(selectedType); + }; + + return ( +
+
+
+
+
+

회원가입을 진행하기 전!

+

가입하려는 회원 유형을 선택해주세요

+
+ +
+ } + type="디자이너" + description="외주를 맡고 싶어요!" + isSelected={selectedType === "designer"} + onClick={() => setSelectedType("designer")} + /> + } + type="강사/교사" + description="외주를 맡기고 싶어요!" + isSelected={selectedType === "instructor"} + onClick={() => setSelectedType("instructor")} + /> +
+
+ + +
+
+
+ ); +}; + +export default UserTypeStep; From 895c53aa4c5dd8fc93640a8885a90782592f6dbf Mon Sep 17 00:00:00 2001 From: KwonOjin Date: Thu, 18 Jun 2026 01:54:51 +0900 Subject: [PATCH 05/10] =?UTF-8?q?#33=20[REFACTOR]=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B4=EB=84=88=20step3=20UI=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/signup/designer/step3/page.tsx | 124 +--------------- src/features/signup/index.ts | 1 + .../signup/ui/DesignerAdditionalStep.tsx | 136 ++++++++++++++++++ 3 files changed, 143 insertions(+), 118 deletions(-) create mode 100644 src/features/signup/ui/DesignerAdditionalStep.tsx diff --git a/src/app/signup/designer/step3/page.tsx b/src/app/signup/designer/step3/page.tsx index 5005858..a59ac0e 100644 --- a/src/app/signup/designer/step3/page.tsx +++ b/src/app/signup/designer/step3/page.tsx @@ -1,131 +1,19 @@ "use client"; import { useRouter } from "next/navigation"; -import { type ChangeEvent, useState } from "react"; -import { type BankCode, BankDropdown, type BankOption } from "@/features/signup"; +import { DesignerAdditionalStep } from "@/features/signup"; import { StepThreeDesignerIcon } from "@/shared/assets/icons"; -import { useUploadedFiles } from "@/shared/lib/hooks/useUploadedFiles"; -import Button from "@/shared/ui/Button"; -import FileDragAndDrop from "@/shared/ui/FileDragAndDrop"; -import FileUpload from "@/shared/ui/FileUpload"; -import InputField from "@/shared/ui/input/InputField"; - -const PORTFOLIO_MAX_FILE_COUNT = 3; -const PORTFOLIO_ALLOWED_EXTENSIONS = [".pdf", ".png"]; - -const isPortfolioFile = (file: File) => { - const fileName = file.name.toLowerCase(); - - return PORTFOLIO_ALLOWED_EXTENSIONS.some(extension => fileName.endsWith(extension)); -}; const Page = () => { const router = useRouter(); - const [selectedBank, setSelectedBank] = useState(null); - const [accountNumber, setAccountNumber] = useState(""); - const [accountHolder, setAccountHolder] = useState(""); - const { uploadedFiles, handleFilesAdded, handleRemove } = useUploadedFiles(); - - const isSubmitEnabled = - selectedBank != null && accountNumber.trim().length > 0 && accountHolder.trim().length > 0; - - const handleBankChange = (bank: BankOption) => { - setSelectedBank(bank.code); - }; - - const handleAccountNumberChange = (event: ChangeEvent) => { - setAccountNumber(event.target.value.replace(/\D/g, "")); - }; - - const handlePortfolioFilesAdded = (files: File[]) => { - const remainingCount = PORTFOLIO_MAX_FILE_COUNT - uploadedFiles.length; - if (remainingCount <= 0) return; - - const portfolioFiles = files.filter(isPortfolioFile).slice(0, remainingCount); - if (portfolioFiles.length > 0) handleFilesAdded(portfolioFiles); - }; return ( -
-
-
-
-
-

회원가입

- -
- -
-
-

은행 선택

- -
- - setAccountNumber("")} - /> - - setAccountHolder(event.target.value)} - onClear={() => setAccountHolder("")} - /> - -
-
-

포트폴리오 제출(선택)

-

- 파일은 3개까지 업로드 가능합니다 -

-
- - {uploadedFiles.length > 0 && ( -
- {uploadedFiles.map(file => ( - handleRemove(file.id)} - /> - ))} -
- )} -
-
-
- -
- - -
-
-
-
+ } + onPrev={() => router.push("/signup/designer/step2")} + onSubmit={() => router.push("/login")} + /> ); }; diff --git a/src/features/signup/index.ts b/src/features/signup/index.ts index c997da3..28fe483 100644 --- a/src/features/signup/index.ts +++ b/src/features/signup/index.ts @@ -4,6 +4,7 @@ export * from "./model/signup"; export { useSignupStep2Form } from "./model/useSignupStep2Form"; export { default as AccountStep } from "./ui/AccountStep"; export { default as BankDropdown } from "./ui/BankDropdown"; +export { default as DesignerAdditionalStep } from "./ui/DesignerAdditionalStep"; export { default as TermsProfileStep } from "./ui/TermsProfileStep"; export { default as UserTypeBtn } from "./ui/UserTypeBtn"; export { default as UserTypeStep } from "./ui/UserTypeStep"; diff --git a/src/features/signup/ui/DesignerAdditionalStep.tsx b/src/features/signup/ui/DesignerAdditionalStep.tsx new file mode 100644 index 0000000..b902a60 --- /dev/null +++ b/src/features/signup/ui/DesignerAdditionalStep.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { type ChangeEvent, type ReactNode, useState } from "react"; + +import { useUploadedFiles } from "@/shared/lib/hooks/useUploadedFiles"; +import Button from "@/shared/ui/Button"; +import FileDragAndDrop from "@/shared/ui/FileDragAndDrop"; +import FileUpload from "@/shared/ui/FileUpload"; +import InputField from "@/shared/ui/input/InputField"; + +import type { BankCode, BankOption } from "../config/signup"; +import BankDropdown from "./BankDropdown"; + +type DesignerAdditionalStepProps = { + progressIcon: ReactNode; + onPrev: () => void; + onSubmit: () => void; +}; + +const PORTFOLIO_MAX_FILE_COUNT = 3; +const PORTFOLIO_ALLOWED_EXTENSIONS = [".pdf", ".png"]; + +const isPortfolioFile = (file: File) => { + const fileName = file.name.toLowerCase(); + + return PORTFOLIO_ALLOWED_EXTENSIONS.some(extension => fileName.endsWith(extension)); +}; + +const DesignerAdditionalStep = ({ + progressIcon, + onPrev, + onSubmit, +}: DesignerAdditionalStepProps) => { + const [selectedBank, setSelectedBank] = useState(null); + const [accountNumber, setAccountNumber] = useState(""); + const [accountHolder, setAccountHolder] = useState(""); + const { uploadedFiles, handleFilesAdded, handleRemove } = useUploadedFiles(); + + const isSubmitEnabled = + selectedBank != null && accountNumber.trim().length > 0 && accountHolder.trim().length > 0; + + const handleBankChange = (bank: BankOption) => { + setSelectedBank(bank.code); + }; + + const handleAccountNumberChange = (event: ChangeEvent) => { + setAccountNumber(event.target.value.replace(/\D/g, "")); + }; + + const handlePortfolioFilesAdded = (files: File[]) => { + const remainingCount = PORTFOLIO_MAX_FILE_COUNT - uploadedFiles.length; + if (remainingCount <= 0) return; + + const portfolioFiles = files.filter(isPortfolioFile).slice(0, remainingCount); + if (portfolioFiles.length > 0) handleFilesAdded(portfolioFiles); + }; + + return ( +
+
+
+
+
+

회원가입

+ {progressIcon} +
+ +
+
+

은행 선택

+ +
+ + setAccountNumber("")} + /> + + setAccountHolder(event.target.value)} + onClear={() => setAccountHolder("")} + /> + +
+
+

포트폴리오 제출(선택)

+

+ 파일은 3개까지 업로드 가능합니다 +

+
+ + {uploadedFiles.length > 0 && ( +
+ {uploadedFiles.map(file => ( + handleRemove(file.id)} + /> + ))} +
+ )} +
+
+
+ +
+ + +
+
+
+
+ ); +}; + +export default DesignerAdditionalStep; From 30d617fab4bb59a958be03255a299c0f1020d756 Mon Sep 17 00:00:00 2001 From: KwonOjin Date: Thu, 18 Jun 2026 01:58:40 +0900 Subject: [PATCH 06/10] =?UTF-8?q?#33=20[FEAT]=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=9C=84=EC=A0=AF=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EB=8B=A8=EC=9D=BC=20URL=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/signup/page.tsx | 14 +-- src/widgets/signup/index.ts | 1 + src/widgets/signup/ui/SignupFunnel.tsx | 122 +++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 12 deletions(-) create mode 100644 src/widgets/signup/index.ts create mode 100644 src/widgets/signup/ui/SignupFunnel.tsx diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index c3e3539..f9b2450 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -1,17 +1,7 @@ -"use client"; - -import { useRouter } from "next/navigation"; - -import { type SignupRole, UserTypeStep } from "@/features/signup"; +import { SignupFunnel } from "@/widgets/signup"; const Page = () => { - const router = useRouter(); - - const handleNext = (selectedType: SignupRole) => { - router.push(`/signup/${selectedType}/step1`); - }; - - return ; + return ; }; export default Page; diff --git a/src/widgets/signup/index.ts b/src/widgets/signup/index.ts new file mode 100644 index 0000000..c1997de --- /dev/null +++ b/src/widgets/signup/index.ts @@ -0,0 +1 @@ +export { default as SignupFunnel } from "./ui/SignupFunnel"; diff --git a/src/widgets/signup/ui/SignupFunnel.tsx b/src/widgets/signup/ui/SignupFunnel.tsx new file mode 100644 index 0000000..73b88ad --- /dev/null +++ b/src/widgets/signup/ui/SignupFunnel.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +import { + AccountStep, + DESIGNER_TERMS, + DesignerAdditionalStep, + INSTRUCTOR_TERMS, + SIGNUP_INITIAL_STEP, + SIGNUP_STEPS_BY_ROLE, + type SignupFunnelStep, + type SignupRole, + TermsProfileStep, + UserTypeStep, +} from "@/features/signup"; +import { + StepOneDesignerIcon, + StepOneInstructorIcon, + StepThreeDesignerIcon, + StepTwoDesignerIcon, + StepTwoInstructorIcon, +} from "@/shared/assets/icons"; + +const SignupFunnel = () => { + const router = useRouter(); + const [selectedRole, setSelectedRole] = useState(null); + const [currentStep, setCurrentStep] = useState(SIGNUP_INITIAL_STEP); + + const handleRoleNext = (role: SignupRole) => { + setSelectedRole(role); + setCurrentStep(SIGNUP_STEPS_BY_ROLE[role][0]); + }; + + const movePrev = () => { + if (selectedRole == null || currentStep === SIGNUP_INITIAL_STEP) return; + + const roleSteps = SIGNUP_STEPS_BY_ROLE[selectedRole]; + const currentStepIndex = roleSteps.findIndex(step => step === currentStep); + + if (currentStepIndex <= 0) { + setCurrentStep(SIGNUP_INITIAL_STEP); + return; + } + + setCurrentStep(roleSteps[currentStepIndex - 1]); + }; + + const moveNext = () => { + if (selectedRole == null || currentStep === SIGNUP_INITIAL_STEP) return; + + const roleSteps = SIGNUP_STEPS_BY_ROLE[selectedRole]; + const currentStepIndex = roleSteps.findIndex(step => step === currentStep); + const nextStep = roleSteps[currentStepIndex + 1]; + + if (nextStep == null) { + router.push("/login"); + return; + } + + setCurrentStep(nextStep); + }; + + if (selectedRole == null || currentStep === "role") { + return ; + } + + if (currentStep === "termsProfile") { + if (selectedRole === "designer") { + return ( + } + onPrev={movePrev} + onNext={moveNext} + /> + ); + } + + return ( + } + onPrev={movePrev} + onNext={moveNext} + /> + ); + } + + if (currentStep === "account") { + if (selectedRole === "designer") { + return ( + } + nextButtonText="다음" + onPrev={movePrev} + onNext={moveNext} + /> + ); + } + + return ( + } + nextButtonText="가입하기" + onPrev={movePrev} + onNext={moveNext} + /> + ); + } + + return ( + } + onPrev={movePrev} + onSubmit={moveNext} + /> + ); +}; + +export default SignupFunnel; From 21a2376adf0d6e3ca4a001daf1955e5539619b9b Mon Sep 17 00:00:00 2001 From: KwonOjin Date: Thu, 18 Jun 2026 02:23:15 +0900 Subject: [PATCH 07/10] =?UTF-8?q?#33=20[REFACTOR]=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20step=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20?= =?UTF-8?q?=EB=8B=A8=EC=9D=BC=20URL=20=ED=8D=BC=EB=84=90=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=A1=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/signup/designer/step1/page.tsx | 21 --------------------- src/app/signup/designer/step2/page.tsx | 21 --------------------- src/app/signup/designer/step3/page.tsx | 20 -------------------- src/app/signup/instructor/step1/page.tsx | 21 --------------------- src/app/signup/instructor/step2/page.tsx | 21 --------------------- 5 files changed, 104 deletions(-) delete mode 100644 src/app/signup/designer/step1/page.tsx delete mode 100644 src/app/signup/designer/step2/page.tsx delete mode 100644 src/app/signup/designer/step3/page.tsx delete mode 100644 src/app/signup/instructor/step1/page.tsx delete mode 100644 src/app/signup/instructor/step2/page.tsx diff --git a/src/app/signup/designer/step1/page.tsx b/src/app/signup/designer/step1/page.tsx deleted file mode 100644 index 63c2acc..0000000 --- a/src/app/signup/designer/step1/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; - -import { DESIGNER_TERMS, TermsProfileStep } from "@/features/signup"; -import { StepOneDesignerIcon } from "@/shared/assets/icons"; - -const Page = () => { - const router = useRouter(); - - return ( - } - onPrev={() => router.push("/signup")} - onNext={() => router.push("/signup/designer/step2")} - /> - ); -}; - -export default Page; diff --git a/src/app/signup/designer/step2/page.tsx b/src/app/signup/designer/step2/page.tsx deleted file mode 100644 index f46b849..0000000 --- a/src/app/signup/designer/step2/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; - -import { AccountStep } from "@/features/signup"; -import { StepTwoDesignerIcon } from "@/shared/assets/icons"; - -const Page = () => { - const router = useRouter(); - - return ( - } - nextButtonText="다음" - onPrev={() => router.push("/signup/designer/step1")} - onNext={() => router.push("/signup/designer/step3")} - /> - ); -}; - -export default Page; diff --git a/src/app/signup/designer/step3/page.tsx b/src/app/signup/designer/step3/page.tsx deleted file mode 100644 index a59ac0e..0000000 --- a/src/app/signup/designer/step3/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; - -import { DesignerAdditionalStep } from "@/features/signup"; -import { StepThreeDesignerIcon } from "@/shared/assets/icons"; - -const Page = () => { - const router = useRouter(); - - return ( - } - onPrev={() => router.push("/signup/designer/step2")} - onSubmit={() => router.push("/login")} - /> - ); -}; - -export default Page; diff --git a/src/app/signup/instructor/step1/page.tsx b/src/app/signup/instructor/step1/page.tsx deleted file mode 100644 index 01d0bd8..0000000 --- a/src/app/signup/instructor/step1/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; - -import { INSTRUCTOR_TERMS, TermsProfileStep } from "@/features/signup"; -import { StepOneInstructorIcon } from "@/shared/assets/icons"; - -const Page = () => { - const router = useRouter(); - - return ( - } - onPrev={() => router.push("/signup")} - onNext={() => router.push("/signup/instructor/step2")} - /> - ); -}; - -export default Page; diff --git a/src/app/signup/instructor/step2/page.tsx b/src/app/signup/instructor/step2/page.tsx deleted file mode 100644 index 48458c2..0000000 --- a/src/app/signup/instructor/step2/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; - -import { AccountStep } from "@/features/signup"; -import { StepTwoInstructorIcon } from "@/shared/assets/icons"; - -const Page = () => { - const router = useRouter(); - - return ( - } - nextButtonText="가입하기" - onPrev={() => router.push("/signup/instructor/step1")} - onNext={() => router.push("/login")} - /> - ); -}; - -export default Page; From 55d21a728ef283942e131ae6ba2a0e18e767098a Mon Sep 17 00:00:00 2001 From: KwonOjin Date: Fri, 19 Jun 2026 01:07:49 +0900 Subject: [PATCH 08/10] =?UTF-8?q?#33=20[FEAT]=20proxy.ts=EB=A5=BC=20?= =?UTF-8?q?=ED=99=9C=EC=9A=A9=ED=95=9C=20url=20=EC=A7=81=EC=A0=91=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=20=EC=A0=9C=ED=95=9C=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/proxy.ts | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/proxy.ts diff --git a/src/proxy.ts b/src/proxy.ts new file mode 100644 index 0000000..fb6a949 --- /dev/null +++ b/src/proxy.ts @@ -0,0 +1,92 @@ +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +type UserRole = "designer" | "instructor"; + +const ACCESS_TOKEN_COOKIE_NAME = "accessToken"; +const USER_ROLE_COOKIE_NAME = "userRole"; + +const ROLE_HOME_PATH: Record = { + designer: "/designer", + instructor: "/instructor", +}; + +const normalizeRole = (role?: string): UserRole | null => { + const normalizedRole = role?.toLowerCase(); + + if (normalizedRole === "designer" || normalizedRole === "instructor") { + return normalizedRole; + } + + return null; +}; + +const parseJwtPayload = (token: string): Record | null => { + const [, payload] = token.split("."); + + if (payload == null) return null; + + try { + const base64 = payload.replace(/-/g, "+").replace(/_/g, "/"); + const paddedBase64 = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "="); + const decodedPayload = atob(paddedBase64); + + return JSON.parse(decodedPayload) as Record; + } catch { + return null; + } +}; + +const getUserRole = (request: NextRequest, accessToken: string): UserRole | null => { + const roleCookie = request.cookies.get(USER_ROLE_COOKIE_NAME)?.value; + const roleFromCookie = normalizeRole(roleCookie); + + if (roleFromCookie != null) return roleFromCookie; + + const tokenPayload = parseJwtPayload(accessToken); + const roleFromToken = typeof tokenPayload?.role === "string" ? tokenPayload.role : undefined; + + return normalizeRole(roleFromToken); +}; + +const getRequiredRole = (pathname: string): UserRole | null => { + if (pathname.startsWith("/designer")) return "designer"; + if (pathname.startsWith("/instructor")) return "instructor"; + + return null; +}; + +const redirect = (path: string, request: NextRequest) => { + return NextResponse.redirect(new URL(path, request.url)); +}; + +export function proxy(request: NextRequest) { + const { pathname } = request.nextUrl; + const requiredRole = getRequiredRole(pathname); + + if (requiredRole == null) { + return NextResponse.next(); + } + + const accessToken = request.cookies.get(ACCESS_TOKEN_COOKIE_NAME)?.value; + + if (accessToken == null) { + return redirect("/login", request); + } + + const userRole = getUserRole(request, accessToken); + + if (userRole == null) { + return redirect("/login", request); + } + + if (userRole !== requiredRole) { + return redirect(ROLE_HOME_PATH[userRole], request); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ["/designer/:path*", "/instructor/:path*"], +}; From 26e717bd5b4b8ba878e68013543eff961c42b6c1 Mon Sep 17 00:00:00 2001 From: KwonOjin Date: Fri, 19 Jun 2026 01:15:14 +0900 Subject: [PATCH 09/10] =?UTF-8?q?#33=20[REFACTOR]=20=EA=B0=81=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=EC=97=90=20=EB=A7=9E=EB=8A=94=20=ED=97=A4=EB=8D=94=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/Header.tsx | 75 +++++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 9 deletions(-) diff --git a/src/shared/ui/Header.tsx b/src/shared/ui/Header.tsx index b0c2c5e..dbee2c1 100644 --- a/src/shared/ui/Header.tsx +++ b/src/shared/ui/Header.tsx @@ -1,13 +1,74 @@ "use client"; import Link from "next/link"; -import { useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { EnterIcon, ProfileCircleIcon } from "@/shared/assets/icons"; import { PurpleLogo } from "@/shared/assets/logos"; +type UserRole = "designer" | "instructor"; + +interface AuthState { + isLoggedIn: boolean; + role: UserRole | null; +} + +const ACCESS_TOKEN_COOKIE_NAME = "accessToken"; +const USER_ROLE_COOKIE_NAME = "userRole"; + +const ROLE_ACCOUNT_PATH: Record = { + designer: "/designer", + instructor: "/instructor/my", +}; + +const getCookieValue = (name: string) => { + if (typeof document === "undefined") return undefined; + + const cookie = document.cookie.split("; ").find(cookie => cookie.startsWith(`${name}=`)); + + if (cookie == null) return undefined; + + return decodeURIComponent(cookie.slice(name.length + 1)); +}; + +const normalizeRole = (role?: string): UserRole | null => { + const normalizedRole = role?.toLowerCase(); + + if (normalizedRole === "designer" || normalizedRole === "instructor") { + return normalizedRole; + } + + return null; +}; + const Header = () => { - const [isLoggedIn, setIsLoggedIn] = useState(true); + const [authState, setAuthState] = useState({ + isLoggedIn: false, + role: null, + }); + + useEffect(() => { + const syncAuthState = () => { + const accessToken = getCookieValue(ACCESS_TOKEN_COOKIE_NAME); + const role = normalizeRole(getCookieValue(USER_ROLE_COOKIE_NAME)); + + setAuthState({ + isLoggedIn: accessToken != null && role != null, + role, + }); + }; + + syncAuthState(); + window.addEventListener("focus", syncAuthState); + + return () => window.removeEventListener("focus", syncAuthState); + }, []); + + const accountHref = useMemo(() => { + if (authState.role == null) return "/login"; + + return ROLE_ACCOUNT_PATH[authState.role]; + }, [authState.role]); return (
@@ -17,15 +78,11 @@ const Header = () => {

1:1 문의하기

FAQ

- {isLoggedIn ? ( - + ) : (
From 8d351c6d49e4a469d15d16f498de95394e3e1e01 Mon Sep 17 00:00:00 2001 From: KwonOjin Date: Fri, 19 Jun 2026 01:57:04 +0900 Subject: [PATCH 10/10] =?UTF-8?q?#33=20[REFACTOR]=20=EB=8B=A8=EA=B3=84?= =?UTF-8?q?=EB=B3=84=20=EC=9D=B8=EB=94=94=EC=BC=80=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/signup/index.ts | 1 + src/features/signup/ui/SignupProgressIcon.tsx | 63 +++++++++++++++++++ .../assets/icons/icon_step_one_designer.svg | 10 --- .../assets/icons/icon_step_one_instructor.svg | 7 --- .../assets/icons/icon_step_three_designer.svg | 12 ---- .../assets/icons/icon_step_two_designer.svg | 11 ---- .../assets/icons/icon_step_two_instructor.svg | 8 --- src/shared/assets/icons/index.ts | 5 -- src/widgets/signup/ui/SignupFunnel.tsx | 18 ++---- 9 files changed, 70 insertions(+), 65 deletions(-) create mode 100644 src/features/signup/ui/SignupProgressIcon.tsx delete mode 100644 src/shared/assets/icons/icon_step_one_designer.svg delete mode 100644 src/shared/assets/icons/icon_step_one_instructor.svg delete mode 100644 src/shared/assets/icons/icon_step_three_designer.svg delete mode 100644 src/shared/assets/icons/icon_step_two_designer.svg delete mode 100644 src/shared/assets/icons/icon_step_two_instructor.svg diff --git a/src/features/signup/index.ts b/src/features/signup/index.ts index 28fe483..92a6301 100644 --- a/src/features/signup/index.ts +++ b/src/features/signup/index.ts @@ -5,6 +5,7 @@ export { useSignupStep2Form } from "./model/useSignupStep2Form"; export { default as AccountStep } from "./ui/AccountStep"; export { default as BankDropdown } from "./ui/BankDropdown"; export { default as DesignerAdditionalStep } from "./ui/DesignerAdditionalStep"; +export { default as SignupProgressIcon } from "./ui/SignupProgressIcon"; export { default as TermsProfileStep } from "./ui/TermsProfileStep"; export { default as UserTypeBtn } from "./ui/UserTypeBtn"; export { default as UserTypeStep } from "./ui/UserTypeStep"; diff --git a/src/features/signup/ui/SignupProgressIcon.tsx b/src/features/signup/ui/SignupProgressIcon.tsx new file mode 100644 index 0000000..9245c7e --- /dev/null +++ b/src/features/signup/ui/SignupProgressIcon.tsx @@ -0,0 +1,63 @@ +import { cn } from "@/shared/lib/utils/cn"; + +type SignupProgressStep = 1 | 2 | 3; +type SignupProgressTotalSteps = 2 | 3; + +type SignupProgressIconProps = { + currentStep: SignupProgressStep; + totalSteps: SignupProgressTotalSteps; + className?: string; +}; + +const getStepCircleClassName = (step: number, currentStep: SignupProgressStep) => { + if (step < currentStep) { + return "border border-main-main bg-purple-10 text-main-main"; + } + + if (step === currentStep) { + return "border border-transparent bg-main-main text-white"; + } + + return "border border-transparent bg-gray-30 text-gray-50"; +}; + +const SignupProgressIcon = ({ currentStep, totalSteps, className }: SignupProgressIconProps) => { + const visibleSteps = Array.from({ length: totalSteps }, (_, index) => index + 1); + + return ( +
+ {visibleSteps.map((step, index) => { + const isConnectorActive = step < currentStep; + + return ( +
+ + {index < visibleSteps.length - 1 && ( +
+ ); + })} +
+ ); +}; + +export default SignupProgressIcon; diff --git a/src/shared/assets/icons/icon_step_one_designer.svg b/src/shared/assets/icons/icon_step_one_designer.svg deleted file mode 100644 index 3a26259..0000000 --- a/src/shared/assets/icons/icon_step_one_designer.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/shared/assets/icons/icon_step_one_instructor.svg b/src/shared/assets/icons/icon_step_one_instructor.svg deleted file mode 100644 index e7b4587..0000000 --- a/src/shared/assets/icons/icon_step_one_instructor.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/src/shared/assets/icons/icon_step_three_designer.svg b/src/shared/assets/icons/icon_step_three_designer.svg deleted file mode 100644 index 3424247..0000000 --- a/src/shared/assets/icons/icon_step_three_designer.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/src/shared/assets/icons/icon_step_two_designer.svg b/src/shared/assets/icons/icon_step_two_designer.svg deleted file mode 100644 index 3cd9392..0000000 --- a/src/shared/assets/icons/icon_step_two_designer.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/src/shared/assets/icons/icon_step_two_instructor.svg b/src/shared/assets/icons/icon_step_two_instructor.svg deleted file mode 100644 index 02e2f57..0000000 --- a/src/shared/assets/icons/icon_step_two_instructor.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/shared/assets/icons/index.ts b/src/shared/assets/icons/index.ts index 20791a9..2ad3947 100644 --- a/src/shared/assets/icons/index.ts +++ b/src/shared/assets/icons/index.ts @@ -33,10 +33,5 @@ export { default as ProfileCircleBoldIcon } from "@/shared/assets/icons/icon_pro export { default as SearchIcon } from "@/shared/assets/icons/icon_search.svg"; export { default as SearchBoldIcon } from "@/shared/assets/icons/icon_search_bold.svg"; export { default as ShareIcon } from "@/shared/assets/icons/icon_share.svg"; -export { default as StepOneDesignerIcon } from "@/shared/assets/icons/icon_step_one_designer.svg"; -export { default as StepOneInstructorIcon } from "@/shared/assets/icons/icon_step_one_instructor.svg"; -export { default as StepThreeDesignerIcon } from "@/shared/assets/icons/icon_step_three_designer.svg"; -export { default as StepTwoDesignerIcon } from "@/shared/assets/icons/icon_step_two_designer.svg"; -export { default as StepTwoInstructorIcon } from "@/shared/assets/icons/icon_step_two_instructor.svg"; export { default as UserTypeDesignerIcon } from "@/shared/assets/icons/icon_user_type_designer.svg"; export { default as UserTypeInstructorIcon } from "@/shared/assets/icons/icon_user_type_instructor.svg"; diff --git a/src/widgets/signup/ui/SignupFunnel.tsx b/src/widgets/signup/ui/SignupFunnel.tsx index 73b88ad..f7136e0 100644 --- a/src/widgets/signup/ui/SignupFunnel.tsx +++ b/src/widgets/signup/ui/SignupFunnel.tsx @@ -11,17 +11,11 @@ import { SIGNUP_INITIAL_STEP, SIGNUP_STEPS_BY_ROLE, type SignupFunnelStep, + SignupProgressIcon, type SignupRole, TermsProfileStep, UserTypeStep, } from "@/features/signup"; -import { - StepOneDesignerIcon, - StepOneInstructorIcon, - StepThreeDesignerIcon, - StepTwoDesignerIcon, - StepTwoInstructorIcon, -} from "@/shared/assets/icons"; const SignupFunnel = () => { const router = useRouter(); @@ -71,7 +65,7 @@ const SignupFunnel = () => { return ( } + progressIcon={} onPrev={movePrev} onNext={moveNext} /> @@ -81,7 +75,7 @@ const SignupFunnel = () => { return ( } + progressIcon={} onPrev={movePrev} onNext={moveNext} /> @@ -92,7 +86,7 @@ const SignupFunnel = () => { if (selectedRole === "designer") { return ( } + progressIcon={} nextButtonText="다음" onPrev={movePrev} onNext={moveNext} @@ -102,7 +96,7 @@ const SignupFunnel = () => { return ( } + progressIcon={} nextButtonText="가입하기" onPrev={movePrev} onNext={moveNext} @@ -112,7 +106,7 @@ const SignupFunnel = () => { return ( } + progressIcon={} onPrev={movePrev} onSubmit={moveNext} />