From 7b33c376e4037566dac939c697b6e4a90c9f6af3 Mon Sep 17 00:00:00 2001 From: dasosann Date: Wed, 18 Feb 2026 23:42:56 +0900 Subject: [PATCH 1/8] update: token --- app/tokens.css | Bin 17294 -> 47206 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/tokens.css b/app/tokens.css index fa402ed02e606582932db59920ceb6024eed0ea1..805846e94329ff3590d9ed00ee3e31140a3821b1 100644 GIT binary patch literal 47206 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 Date: Sun, 22 Feb 2026 00:11:59 +0900 Subject: [PATCH 2/8] feat: Introduce a design token system and add the initial StartOnBoarding component. --- app/tokens.css | Bin 47206 -> 26718 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/tokens.css b/app/tokens.css index 805846e94329ff3590d9ed00ee3e31140a3821b1..0766459b806ddae8785e60a66dd207482e20236e 100644 GIT binary patch literal 26718 zcmbtdZI9!|4gP+=LO@X<397rb{IcH7m-Y%2=+_{BK(^&w_q5j5mUDLv^54rL=Y2?t zo4o~s+>m^R^9*M+<-oOGQ3pd@TEI{{LghbS3iCF@ad=O zm)rTVdOg3sRF~VC#PHwh(m6w`_vy}MMq_S#jy zf2gX*%P@@ie|5cU_g|ha!|i;i+t*k4>$Q8Verqp3)b)Pohf96EU5@R(L&=XngLP@s z-!AoT7{}qI?z`id+V3tcU|+laa6Yt`@Ad2H{AC{Ja(a4ZfnG1C7Xvc@>{G`B4X^F~ zG=6t}*uU1tTi=%kyWw)^F7>57oNmAw4W0g0pNBKk#Zy0gt#@Y7&Zybm<;*ywLw9U% z{pkAbf$qoZFL(II+6)J9^_Kw{974-(=ns$8XZUYugIT}z?FE$Sf1Csd@bz>UpKIG4 z%snFTE?7)E42cHhQ&-@cBgAv2C84bwBq$J5wnv5f6IGY6c}@buJo^}ZkSB`}gCo66g6 z2kQAa(otfE$@s_$+n*uE%P{w%-pHJvky*)QIjkGZ!a4Sh!YP>mT$c$Mh1KD7ZDEiQ zh>2y`GMs{UGw=7w#87u0>qXt2a~$-5U(kgs5zuvPFOz%2v2R~|Kv^VO<+!C~qajJk zHVtE9XLvbeLnhs1QP`b@7ACB{b86zYCBI~eW7iz-Rx}336i#<$MSC?$(s09(q?YWY z5~vlWj$V6sK!u$p4h5K%@AX~XF7K=f`4KG&7a=L>Xt%rr%av$}<6z7(gJ}9V<yLIRKzs1bhEwt4LbyWk2mDhA z*Wma9Ia?p$XVJ16+n}*erUZ$h`P$x@AI99!hTw&4gPv{hPbpl1VRtk?g^Z?obud4L zaEqu*{8I=i1QxCNiG?RPNJdY#G|q99d7Pugb-cjYx<-nN9SmBN{mU*+zclW5Nd#N5 zTN~|SYmZG7!dC1S$F|)hym+yp``B$edmc-S-SW7MU^z&#<9^H(7P9^G_lP8{eQUm^Rh8DW#ae7ux!IAmk{h`}mI>B2l(^TWA*Rz*j;Fno~bQQQEfAyrJ8tqtRC*{ zJfF^f?icIO_<9?Ur>;Lx7FHimeeZmt?J?W^t&^v7W@ywH!|)K03`rw3#juSJxlwn~ zvn4eOn_JnHmHAkYcQb_@vj5j9_&-A5Qyr%=cU>>bSH5my7^l zby9M$=Q#E3A;5?QUuEcrCmYqM=;Raz)PI8D)y*@9)8@PloiMu_dKqU^Q1^i)>Z0|< zr?Wm8e1S^Oss63)Z@%#G&6S*nbGzR|ChS~`f#aOd^kCP!YkujVHudsYt+V#)Z3iWe z8kX>im4uyxjtzbv_2(ICcN=Lq$WXi6Ny7=owP!~cYIb{RII~cDd02UE545S!oO}Mv z)Rm_f(`sFejxDCuIvAZ-s5LvNm`3Z|I-F2UYk4%Wl|Pex!**qleYVj=soQmJlL-R1 zmD?s$G=x$bZI!m|3bW^)4z}qV6dUbsf8DpHQf0Sfqf5N_s+x&J*XZKNt~VREaFR5C zsMBwL)AZ42dc)_%A0Phk1>xIx>QCdTyJ9UEs?Gm`$5|Cxp>|wVAII;n13dK1(HZrs z%0Hb*6Dx9KnGI7Uob53%XiZIE1NTL01b4VWfry zT^;N~;CAsoZWlGrEf1^ac2V=(@~~=d7pAPiaLdE0xore)oA+_s2;B0pYHk~WTOL-; zZ6k2Y!>YMm3fwN=$L&(!mWNeyyA-(PVb$C&1#Wp*HMc8)+tvHHT?yRsuxf5s0=GP@ zn%kAYEf1^ab}ewbejm4MfmYNx7r5nN)!g0--14w$Znpxr+xKz16}aVL)!c3c zZh2TWw_AZ*9#+lmgTU>>`?!4&xaDEh+&&20@~~=d9|UfBSS`1Wke4;@%gX>3_$p81 z0<4x>fCawPuv%^b7Wh)bs<{>NvW8Ddv^j4h;d5 zJ|$6eE97MYteRUPFB4$Z+zNS_0ITL!$jcf&B~f!L;d5J|$6eE97MYteRUPFB4$Z+zNS_0ITL!$jcf&B~f!LAiCRArMi3~3dMHe65grAyfd%;@~ zf3_bqj(W0h*@PA2#yru1XfuV2uP?j>)V?<3*Qf0@x&<(g7Acp$h3f(HKtWf)N!~e9 z;w3gSxb%X}8-dAa2HCtF^F~}_BA}*s6O_2AQGtwaoxzJK@PHBbc8U(KDPX|MMM@0N z6fo9Wh_#HMhmbJvajRkjG=-7CJ5fqDK(oN0mnCNbNKLRdH#C$8biF6r4G>%t7*3J* z0}%9{SY;lmQBaD$fFh5e2~kAgBGd`YZ7UtGfI?TZIA$Rc(4l}LKN`hiL7$nmV%yDK z7@ZSzZ$|Q?Zntm1*^1*)g9r$C{GM63ICN0t$Ff=J_%JsgxLMU~9q0kKbD#jO^XOav zMcxO^)`MMfJ5Tm%_^d0Le&JE{YB&A@Md2dI9AH%Q;}i=&P@tI`r^q8H<^=(m{n571uSBQF zd!U&QMBsJ<6lmtcDHcA2TM)C$IH*b}cq>ASW3NzTJ7oM3jCrK!>RI7axcV@Q6I6K= zcdbVUCM_7J= zXGH{lU&@pihO^qi1dL60_^`COt+@Pyobka5#f2E;vaz%ztS!4Jyi5qpAeZ|UgT)&2 z;bMj2yazecGv=-$(@rWGiY%iczulooV%Q3${-{dBEDCGKEqfG+%sQ#mqsS^6@~bK2 z^85-2a;8@+blE;L)>wlBi1K!!|44%YzzqdIO2C^3xn%5;^a*>XG6P{Ud4>Yo^XQw3 zK7Efi!dSkXhwxc@4l$jhpte(y2IBMY2{*is1TvA_pc6@I9uFePwgnFooy3D9d|);D zCQELgi4)W4<09w}d^fO2j5(c25dRsE+>;#1D^9}MENTwSswGh2iyQ1h$;5)G*+TNp zgQbxQhHN+l%McAzTKSBrYx#;1Wwg8U(~{tn{pw?~_83j)V5}a$xa3xcfQay%{v57j zAnw;yfC|vShj|%_Ms1a%q<-4PNRjs~r1#T6L2m*l$?p|-iM|4tjs$N2CIbbsjp>-> zY+jH6=Wrb4u}#f=Z&~3lUn)qDeKlYb&TFw__GJMDMsx~d{J?`y+@7-KY77%z*xy`5 zVa-GMvr*hzO&!qjC>C%S(eX1@wHS0#iUk}-bYhBCYfMhe(K&8S0Z4^qf<*2$2a$@P zCZo_i7xOrwhQZ+|B0ef+lnC~Pi)lBass@9A zFI>z3`0_?+Keu(@q)0ml3hfIQ(=J2>kJ5*^tp~f2cAl)PF}Ve0x@@eMMUdFxbophv zES^o@=k}$bHia^`A6OJ=N3i0Vb?CLGJehlJc@TbowBotw3u;ro%*BsIks`<(V%!TC zViY?i)(;fU&5cu%5oYEE0fhb0%BJ@trzCqYHy?-~?FJ~En+vDJ`VcBBlgl`$O`*6F zjJ$lgN=&Ss{Hr6=9x)}E1PPO9mutq7bVCVh?8vAzKy{PFq(h13EEc|SA%=d8<*POE z80fRF@e3C!EMK-Mu~c~{`sFm`G5YGvJYil{Mw`!E(JeV=Enm2h$tp`bR1x^+?F#?C zNd?bEyxmbzW`8yR%#OV;l>A!__U=>o<^5x2zKGaf%&`m+epoEmhwkZb$tSJ3&#O*p O&^G%ceiG7r;PL-%Jahg4 literal 47206 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 Date: Sun, 22 Feb 2026 00:12:13 +0900 Subject: [PATCH 3/8] feat: Add `StartOnBoarding` component to display an animated welcome message and a button to begin the onboarding process. --- .../_components/StartOnBoarding.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/app/onboarding/_components/StartOnBoarding.tsx b/app/onboarding/_components/StartOnBoarding.tsx index 3fd4de0..79327fe 100644 --- a/app/onboarding/_components/StartOnBoarding.tsx +++ b/app/onboarding/_components/StartOnBoarding.tsx @@ -1,18 +1,23 @@ "use client"; import Button from "@/components/ui/Button"; import React, { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; const StartOnBoarding = () => { + const router = useRouter(); const [showFirst, setShowFirst] = useState(false); const [showSecond, setShowSecond] = useState(false); + const [showThird, setShowThird] = useState(false); const [buttonDisabled, setButtonDisabled] = useState(true); useEffect(() => { const t1 = setTimeout(() => setShowFirst(true), 100); const t2 = setTimeout(() => setShowSecond(true), 2100); + const t3 = setTimeout(() => setShowThird(true), 4100); return () => { clearTimeout(t1); clearTimeout(t2); + clearTimeout(t3); }; }, []); @@ -43,12 +48,24 @@ const StartOnBoarding = () => { opacity: showSecond ? 1 : 0, transition: "opacity 0.7s ease", }} - onTransitionEnd={() => setButtonDisabled(false)} > 원활한 매칭을 위해 여러분의 정보가 필요해요! + setButtonDisabled(false)} + > + 다음 설문을 빠르게 진행해 볼까요? + - From 2a7af15aa80e34020355b3e4886541bf061a444f Mon Sep 17 00:00:00 2001 From: dasosann Date: Mon, 23 Feb 2026 00:16:21 +0900 Subject: [PATCH 4/8] =?UTF-8?q?fix:=20=EC=9E=90=EC=B2=B4=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20nextjs=EC=97=90=EC=84=9C=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=EC=85=89=ED=8A=B8=EB=A1=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8=20=EB=B0=8F=20?= =?UTF-8?q?=EC=BF=A0=ED=82=A4=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/login/_components/LoginForm.tsx | 2 +- lib/actions/loginAction.ts | 59 ++++++++++++++++++++++++++++- lib/server-api.ts | 30 +++++++++++++-- 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/app/login/_components/LoginForm.tsx b/app/login/_components/LoginForm.tsx index 842a469..0adcad5 100644 --- a/app/login/_components/LoginForm.tsx +++ b/app/login/_components/LoginForm.tsx @@ -49,7 +49,7 @@ export const LoginForm = () => { /> {!state.success && state.message && ( - * 이메일 혹은 비밀번호가 틀립니다 + * {state.message} )} diff --git a/lib/actions/loginAction.ts b/lib/actions/loginAction.ts index 7f21f9b..3d086ea 100644 --- a/lib/actions/loginAction.ts +++ b/lib/actions/loginAction.ts @@ -1,5 +1,6 @@ "use server"; +import { redirect } from "next/navigation"; import { serverApi } from "@/lib/server-api"; import { isAxiosError } from "axios"; @@ -21,11 +22,62 @@ export async function loginAction( const email = formData.get("email"); const password = formData.get("password"); + let redirectUrl: string | null = null; + try { - await serverApi.post({ + const { finalUrl, setCookie } = await serverApi.post({ path: "/api/auth/login", body: { email, password }, }); + + // 🍪 백엔드로부터 받은 쿠키가 있다면 브라우저에 배달해줍니다. + if (setCookie) { + const { cookies } = await import("next/headers"); + const cookieStore = await cookies(); + + setCookie.forEach((cookieStr) => { + // Axios가 준 쿠키 문자열을 파싱합니다 (name=value; Path=/ ...) + const [nameValue, ...options] = cookieStr.split(";"); + const [name, value] = nameValue.split("="); + + const cookieOptions: { + path?: string; + httpOnly?: boolean; + secure?: boolean; + maxAge?: number; + expires?: Date; + sameSite?: "strict" | "lax" | "none" | boolean; + } = {}; + + options.forEach((opt) => { + const [key, val] = opt.trim().split("="); + const k = key.toLowerCase(); + if (k === "path") cookieOptions.path = val; + if (k === "httponly") cookieOptions.httpOnly = true; + if (k === "secure") cookieOptions.secure = true; + if (k === "max-age") cookieOptions.maxAge = parseInt(val); + if (k === "expires") cookieOptions.expires = new Date(val); + if (k === "samesite") + cookieOptions.sameSite = val.toLowerCase() as + | "strict" + | "lax" + | "none"; + }); + + cookieStore.set(name, value, cookieOptions); + }); + } + + if (finalUrl) { + // https://comatching.site/onboarding -> /onboarding 추출 + try { + const url = new URL(finalUrl); + redirectUrl = url.pathname + url.search; + } catch { + // 이미 상대 경로인 경우 + redirectUrl = finalUrl; + } + } } catch (error) { if (isAxiosError(error)) { const status = error.response?.status; @@ -45,5 +97,10 @@ export async function loginAction( }; } + // 성공 시 리다이렉트 (Next.js 규칙: try-catch 밖에서 호출 권장) + if (redirectUrl) { + redirect(redirectUrl); + } + return { success: true, message: "로그인 성공" }; } diff --git a/lib/server-api.ts b/lib/server-api.ts index 3d26b4a..db13347 100644 --- a/lib/server-api.ts +++ b/lib/server-api.ts @@ -11,6 +11,8 @@ if (!API_URL) { const serverClient = axios.create({ baseURL: API_URL, timeout: 10000, + maxRedirects: 0, // 🚨 자동으로 리다이렉트를 따라가지 않도록 설정 + validateStatus: (status) => (status >= 200 && status < 300) || status === 302, // 302도 성공으로 간주 headers: { "Content-Type": "application/json", }, @@ -62,7 +64,7 @@ serverClient.interceptors.response.use( // 원래 요청 재시도 return serverClient(originalRequest); - } catch (reissueError) { + } catch { // 재발급 실패 → 로그인 페이지로 redirect("/login"); } @@ -80,23 +82,43 @@ type RequestOptions = { body?: unknown; // POST, PUT 등에서 보낼 데이터 }; +// 요청 결과 타입 정의 +type ApiResponse = { + data: T; + finalUrl?: string; + setCookie?: string[]; // ✅ 백엔드에서 온 쿠키 헤더 +}; + // 3. 통합 요청 함수 async function request( method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", { path, params, headers, body }: RequestOptions, -): Promise { +): Promise> { // Axios 설정 객체 const config: AxiosRequestConfig = { url: path, method, headers, - params, // Axios가 객체를 쿼리스트링(?key=value)으로 자동 변환해줍니다. (buildQuery 불필요) + params, data: body, }; try { const response = await serverClient.request(config); - return response.data; + + // 💡 성공 시 로그를 출력합니다. + console.log(`[Server API Success] ${method} ${path}`, { + status: response.status, + location: response.headers["location"], // 리다이렉트 지점 + hasCookie: !!response.headers["set-cookie"], + }); + + return { + data: response.data, + finalUrl: + response.headers["location"] || response.request?.res?.responseUrl, + setCookie: response.headers["set-cookie"] as string[], + }; } catch (error) { if (isAxiosError(error)) { const status = error.response?.status; From 29410f64a9538d40d2186d7cc8e049ad22721252 Mon Sep 17 00:00:00 2001 From: dasosann Date: Mon, 23 Feb 2026 00:17:59 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=EB=93=9C=EB=A1=AD=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20Input=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ui/FormSelect.tsx | 103 +++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 components/ui/FormSelect.tsx diff --git a/components/ui/FormSelect.tsx b/components/ui/FormSelect.tsx new file mode 100644 index 0000000..46eee9a --- /dev/null +++ b/components/ui/FormSelect.tsx @@ -0,0 +1,103 @@ +import React from "react"; +import { cn } from "@/lib/utils"; +import { ChevronDown } from "lucide-react"; + +// 옵션 타입 정의 (일반적인 단일 옵션) +export type SelectOption = { + value: string; + label: string; +}; + +// 옵션 그룹 타입 정의 (대학 - 계열 - 학과 처럼 묶여있는 경우) +export type SelectOptionGroup = { + label: string; + options: SelectOption[]; +}; + +interface FormSelectProps extends Omit< + React.SelectHTMLAttributes, + "id" | "name" +> { + id: string; + name: string; + options: (SelectOption | SelectOptionGroup)[]; + placeholder?: string; + error?: boolean; +} + +const SELECT_CONTAINER_STYLE = { + background: + "linear-gradient(180deg, rgba(248, 248, 248, 0.03) 0%, rgba(248, 248, 248, 0.24) 100%)", +}; + +const SELECT_CLASSNAME = + "all:unset box-border w-full border-b border-gray-300 px-2 py-[14.5px] pr-8 leading-[19px] typo-16-500 text-color-gray-900 outline-none appearance-none cursor-pointer"; + +// 타입 가드: 옵션 그룹인지 확인 +function isOptionGroup( + option: SelectOption | SelectOptionGroup, +): option is SelectOptionGroup { + return "options" in option; +} + +const FormSelect = ({ + id, + name, + options, + placeholder, + className = "", + style = {}, + error = false, + ...rest +}: FormSelectProps) => { + return ( +
+ + + {/* 오른쪽에 화살표 아이콘 (포인터 이벤트 무시하여 클릭 방해 안 함) */} +
+ +
+
+ ); +}; + +export default FormSelect; From 710dae09a7e026b979dfc526aac759b3f3c761d9 Mon Sep 17 00:00:00 2001 From: dasosann Date: Mon, 23 Feb 2026 21:52:49 +0900 Subject: [PATCH 6/8] =?UTF-8?q?fix:=20=ED=8C=A8=EC=8A=A4=EC=9B=8C=EB=93=9C?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/register/_components/PasswordStep.tsx | 32 ++++++++++++++++++----- lib/validators.ts | 9 ++++--- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/app/register/_components/PasswordStep.tsx b/app/register/_components/PasswordStep.tsx index e94545b..aa46930 100644 --- a/app/register/_components/PasswordStep.tsx +++ b/app/register/_components/PasswordStep.tsx @@ -79,18 +79,36 @@ export const PasswordStep = ({ )} -
+
- - 8~20자 이내 + + 8자 이상 - - 영문 대소문자, 숫자 포함 + + 영문, 숫자, 특수문자(@$!%*#?&) 포함
diff --git a/lib/validators.ts b/lib/validators.ts index 2aeb441..65d6ab2 100644 --- a/lib/validators.ts +++ b/lib/validators.ts @@ -4,7 +4,10 @@ export const validateEmail = (email: string): boolean => { }; export const validatePasswordLength = (password: string): boolean => - password.length >= 8 && password.length <= 20; + password.length >= 8; -export const validatePasswordPattern = (password: string): boolean => - /[a-zA-Z]/.test(password) && /[0-9]/.test(password); +export const validatePasswordPattern = (password: string): boolean => { + const passwordRegex = + /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/; + return passwordRegex.test(password); +}; From 069be3a7ced20385f04c9450cff1eee415cf18f8 Mon Sep 17 00:00:00 2001 From: dasosann Date: Mon, 23 Feb 2026 23:55:55 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=EA=B2=80=EC=A6=9D=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/register/_components/ScreenRegister.tsx | 14 +++------- app/register/_components/VerificationStep.tsx | 20 +++++++------- hooks/useVerifyEmail.ts | 27 ++++++++++++++++++- 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/app/register/_components/ScreenRegister.tsx b/app/register/_components/ScreenRegister.tsx index 7acaac2..c7da289 100644 --- a/app/register/_components/ScreenRegister.tsx +++ b/app/register/_components/ScreenRegister.tsx @@ -15,7 +15,7 @@ export const ScreenRegister = () => { const [password, setPassword] = useState(""); const { mutate: sendEmail, isPending: isSendingEmail } = useSendEmail(); - const { mutate: verifyEmail, isPending: isVerifyingEmail } = useVerifyEmail(); + const { verify, isPending: isVerifyingEmail } = useVerifyEmail(); const { mutate: signUp, isPending: isSigningUp } = useSignUp(); const handleEmailSubmit = (e: React.SyntheticEvent) => { @@ -25,17 +25,11 @@ export const ScreenRegister = () => { }); }; - const handleVerify = (code: string, onError: () => void) => { - verifyEmail( + const handleVerify = (code: string, onError: (msg?: string) => void) => { + verify( { email, code }, { - onSuccess: (data) => { - if (data.status === 200) { - setStep(3); - } else { - onError(); - } - }, + onSuccess: () => setStep(3), onError, }, ); diff --git a/app/register/_components/VerificationStep.tsx b/app/register/_components/VerificationStep.tsx index 89cda7a..5790e57 100644 --- a/app/register/_components/VerificationStep.tsx +++ b/app/register/_components/VerificationStep.tsx @@ -6,7 +6,7 @@ type VerificationStepProps = { email: string; verificationCode: string; onVerificationCodeChange: (code: string) => void; - onVerify: (code: string, onError: () => void) => void; + onVerify: (code: string, onError: (msg?: string) => void) => void; onResend: () => void; isVerifying?: boolean; }; @@ -20,7 +20,7 @@ export const VerificationStep = ({ isVerifying = false, }: VerificationStepProps) => { const [timeLeft, setTimeLeft] = useState(300); // 5분 = 300초 - const [isWrong, setIsWrong] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); useEffect(() => { if (timeLeft <= 0) return; @@ -40,19 +40,21 @@ export const VerificationStep = ({ const handleVerificationCodeChange = (value: string) => { onVerificationCodeChange(value); - if (isWrong && value) { - setIsWrong(false); + if (errorMessage && value) { + setErrorMessage(""); } }; const handleSubmit = (e: React.SyntheticEvent) => { e.preventDefault(); - onVerify(verificationCode, () => setIsWrong(true)); + onVerify(verificationCode, (msg?: string) => + setErrorMessage(msg || "인증번호를 다시 확인해 주세요"), + ); }; const handleResend = () => { setTimeLeft(300); // 타이머 리셋 - setIsWrong(false); // 에러 상태 초기화 + setErrorMessage(""); // 에러 상태 초기화 onResend(); }; @@ -96,7 +98,7 @@ export const VerificationStep = ({ required value={verificationCode} onChange={(e) => handleVerificationCodeChange(e.target.value)} - error={isWrong} + error={!!errorMessage} maxLength={6} inputMode="numeric" className="flex-1" @@ -110,9 +112,9 @@ export const VerificationStep = ({ 재전송
- {isWrong && ( + {errorMessage && ( - *인증번호를 다시 확인해 주세요 + *{errorMessage} )} diff --git a/hooks/useVerifyEmail.ts b/hooks/useVerifyEmail.ts index e2f3a01..8f5e77d 100644 --- a/hooks/useVerifyEmail.ts +++ b/hooks/useVerifyEmail.ts @@ -1,5 +1,6 @@ import { api } from "@/lib/axios"; import { useMutation } from "@tanstack/react-query"; +import { isAxiosError } from "axios"; type VerifyEmailRequest = { email: string; @@ -23,8 +24,32 @@ const verifyEmail = async ( }; export const useVerifyEmail = () => { - return useMutation({ + const mutation = useMutation({ mutationFn: verifyEmail, retry: 1, }); + + const verify = ( + payload: VerifyEmailRequest, + options: { onSuccess: () => void; onError: (msg?: string) => void }, + ) => { + mutation.mutate(payload, { + onSuccess: (data) => { + if (data.status === 200) { + options.onSuccess(); + } else { + options.onError(data.message); + } + }, + onError: (error) => { + if (isAxiosError(error) && error.response?.data?.message) { + options.onError(error.response.data.message); + } else { + options.onError(); + } + }, + }); + }; + + return { ...mutation, verify }; }; From a05a6fa226a222bd19847dc802f94bdcb8eeb833 Mon Sep 17 00:00:00 2001 From: dasosann Date: Tue, 24 Feb 2026 15:04:34 +0900 Subject: [PATCH 8/8] =?UTF-8?q?feat:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81=EC=9D=B4=EC=97=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/register/_components/PasswordStep.tsx | 7 ++++--- components/ui/FormInput.tsx | 2 +- components/ui/FormSelect.tsx | 2 +- lib/actions/loginAction.ts | 6 ++++-- lib/validators.ts | 9 ++++++--- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/register/_components/PasswordStep.tsx b/app/register/_components/PasswordStep.tsx index aa46930..2f24549 100644 --- a/app/register/_components/PasswordStep.tsx +++ b/app/register/_components/PasswordStep.tsx @@ -1,4 +1,4 @@ -import Button from "@/components/ui/Button"; +import Button from "@/components/ui/Button"; import FormInput from "@/components/ui/FormInput"; import { validatePasswordLength, @@ -22,6 +22,7 @@ export const PasswordStep = ({ const isLengthValid = validatePasswordLength(password); const isPatternValid = validatePasswordPattern(password); + const isPasswordValid = isLengthValid && isPatternValid; return (
@@ -93,7 +94,7 @@ export const PasswordStep = ({ : "stroke-color-text-caption2" } /> - 8자 이상 + 8자 이상 20자 이하
-
diff --git a/components/ui/FormInput.tsx b/components/ui/FormInput.tsx index 12965b6..62591d0 100644 --- a/components/ui/FormInput.tsx +++ b/components/ui/FormInput.tsx @@ -18,7 +18,7 @@ const INPUT_STYLE = { "linear-gradient(180deg, rgba(248, 248, 248, 0.03) 0%, rgba(248, 248, 248, 0.24) 100%)", }; const INPUT_CLASSNAME = - "all:unset box-border w-full border-b border-gray-300 px-2 py-[14.5px] leading-[19px] typo-16-500 placeholder:text-[#B3B3B3] text-color-gray-900 outline-none"; + "all:unset box-border w-full border-b border-gray-300 px-2 py-[14.5px] leading-[19px] typo-16-500 placeholder:text-color-text-caption2 text-color-gray-900 outline-none"; // 안전한 속성 화이트리스트 (XSS 방지) const SAFE_INPUT_ATTRIBUTES = [ diff --git a/components/ui/FormSelect.tsx b/components/ui/FormSelect.tsx index 46eee9a..4755fe5 100644 --- a/components/ui/FormSelect.tsx +++ b/components/ui/FormSelect.tsx @@ -58,7 +58,7 @@ const FormSelect = ({ className={cn( SELECT_CLASSNAME, error && "border-color-flame-500", - !rest.value && "text-[#B3B3B3]", // placeholder 색상 처리 + !rest.value && "text-color-text-caption2", // placeholder 색상 처리 className, )} style={{ ...SELECT_CONTAINER_STYLE, ...style }} diff --git a/lib/actions/loginAction.ts b/lib/actions/loginAction.ts index 3d086ea..22004fc 100644 --- a/lib/actions/loginAction.ts +++ b/lib/actions/loginAction.ts @@ -38,7 +38,8 @@ export async function loginAction( setCookie.forEach((cookieStr) => { // Axios가 준 쿠키 문자열을 파싱합니다 (name=value; Path=/ ...) const [nameValue, ...options] = cookieStr.split(";"); - const [name, value] = nameValue.split("="); + const [name, ...nameParts] = nameValue.split("="); + const value = nameParts.join("="); const cookieOptions: { path?: string; @@ -50,7 +51,8 @@ export async function loginAction( } = {}; options.forEach((opt) => { - const [key, val] = opt.trim().split("="); + const [key, ...valueParts] = opt.trim().split("="); + const val = valueParts.join("="); const k = key.toLowerCase(); if (k === "path") cookieOptions.path = val; if (k === "httponly") cookieOptions.httpOnly = true; diff --git a/lib/validators.ts b/lib/validators.ts index 65d6ab2..aabcecf 100644 --- a/lib/validators.ts +++ b/lib/validators.ts @@ -3,11 +3,14 @@ export const validateEmail = (email: string): boolean => { return emailRegex.test(email); }; -export const validatePasswordLength = (password: string): boolean => - password.length >= 8; +export const validatePasswordLength = ( + password: string, + minLength = 8, + maxLength = 20, +): boolean => password.length >= minLength && password.length <= maxLength; export const validatePasswordPattern = (password: string): boolean => { const passwordRegex = - /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/; + /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]+$/; return passwordRegex.test(password); };