From d866c2b788dfb3f788f32fa11d3abf2764ab89b9 Mon Sep 17 00:00:00 2001 From: dasosann Date: Wed, 11 Mar 2026 00:22:17 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=ED=99=95=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20TermsDrawer=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/profile-image/_components/TermsDrawer.tsx | 23 ++++++- hooks/useNicknameAvailability.ts | 61 +++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 hooks/useNicknameAvailability.ts diff --git a/app/profile-image/_components/TermsDrawer.tsx b/app/profile-image/_components/TermsDrawer.tsx index fb71e53..1152b99 100644 --- a/app/profile-image/_components/TermsDrawer.tsx +++ b/app/profile-image/_components/TermsDrawer.tsx @@ -8,6 +8,7 @@ import { TERMS_TEXT, PRIVACY_TEXT } from "../_constants/terms"; import { Hobby, ProfileSubmitData } from "@/lib/types/profile"; import { useProfile } from "@/providers/profile-provider"; import { useImageUpload, useProfileSignUp } from "@/hooks/useProfileSignUp"; +import { useNicknameAvailability } from "@/hooks/useNicknameAvailability"; import { useRouter } from "next/navigation"; import { HOBBIES, HobbyCategory } from "@/lib/constants/hobbies"; @@ -31,6 +32,7 @@ const TermsDrawer = ({ children }: TermsDrawerProps) => { const { mutateAsync: uploadImage } = useImageUpload(); const { mutate: signUp, isPending: isSubmitting } = useProfileSignUp(); + const { mutateAsync: checkNicknameAvailability } = useNicknameAvailability(); const checkGradient = "linear-gradient(220.53deg, #FF775E -18.87%, #FF4D61 62.05%, #E83ABC 125.76%)"; @@ -146,9 +148,26 @@ const TermsDrawer = ({ children }: TermsDrawerProps) => { const trigger = children ? React.cloneElement(children, { - onClick: (e: React.MouseEvent) => { + onClick: async (e: React.MouseEvent) => { children.props.onClick?.(e); - setIsOpen(true); + + const nickname = (profile.nickname || "").trim(); + + try { + const isAvailable = await checkNicknameAvailability(nickname); + + if (!isAvailable) { + alert("중복된 닉네임입니다. 다른 닉네임을 입력해 주세요."); + return; + } + + setIsOpen(true); + } catch (error) { + console.error("Failed to check nickname availability:", error); + alert( + "닉네임 중복 확인에 실패했습니다. 잠시 후 다시 시도해 주세요.", + ); + } }, }) : null; diff --git a/hooks/useNicknameAvailability.ts b/hooks/useNicknameAvailability.ts new file mode 100644 index 0000000..f2daf61 --- /dev/null +++ b/hooks/useNicknameAvailability.ts @@ -0,0 +1,61 @@ +import { api } from "@/lib/axios"; +import { useMutation } from "@tanstack/react-query"; + +interface NicknameAvailabilityResponse { + code: string; + status: number; + message: string; + data?: + | boolean + | { + available?: boolean; + isAvailable?: boolean; + duplicate?: boolean; + }; +} + +const parseNicknameAvailability = ( + response: NicknameAvailabilityResponse, +): boolean => { + const payload = response.data; + + if (typeof payload === "boolean") { + return payload; + } + + if (payload && typeof payload === "object") { + if (typeof payload.available === "boolean") { + return payload.available; + } + + if (typeof payload.isAvailable === "boolean") { + return payload.isAvailable; + } + + if (typeof payload.duplicate === "boolean") { + return !payload.duplicate; + } + } + + throw new Error("닉네임 중복 검사 응답 형식을 확인할 수 없습니다."); +}; + +const checkNicknameAvailability = async ( + nickname: string, +): Promise => { + const { data: response } = await api.get( + "/api/auth/signup/nickname/availability", + { + params: { nickname }, + }, + ); + + return parseNicknameAvailability(response); +}; + +export const useNicknameAvailability = () => { + return useMutation({ + mutationFn: checkNicknameAvailability, + retry: false, + }); +}; From f262f81e417def9cfa154364cf477182801787cf Mon Sep 17 00:00:00 2001 From: dasosann Date: Wed, 11 Mar 2026 14:51:23 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EB=B0=B1=EC=97=94=EB=93=9C=20API?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/profile-image/_components/TermsDrawer.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/profile-image/_components/TermsDrawer.tsx b/app/profile-image/_components/TermsDrawer.tsx index 1152b99..6c0db4a 100644 --- a/app/profile-image/_components/TermsDrawer.tsx +++ b/app/profile-image/_components/TermsDrawer.tsx @@ -153,6 +153,11 @@ const TermsDrawer = ({ children }: TermsDrawerProps) => { const nickname = (profile.nickname || "").trim(); + if (!nickname) { + alert("닉네임을 입력해 주세요."); + return; + } + try { const isAvailable = await checkNicknameAvailability(nickname); From 5adaf6620b5eb007eced23cb080e7647507f3225 Mon Sep 17 00:00:00 2001 From: dasosann Date: Wed, 11 Mar 2026 15:09:37 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EB=A1=9C=EC=A7=81=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/profile-image/_components/TermsDrawer.tsx | 35 ++++++++++++++----- hooks/useNicknameAvailability.ts | 32 ++--------------- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/app/profile-image/_components/TermsDrawer.tsx b/app/profile-image/_components/TermsDrawer.tsx index 6c0db4a..e2ef54d 100644 --- a/app/profile-image/_components/TermsDrawer.tsx +++ b/app/profile-image/_components/TermsDrawer.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useState } from "react"; import { ChevronRight, ArrowLeft } from "lucide-react"; +import axios from "axios"; import Button from "@/components/ui/Button"; import { SelectCheckButton } from "./ProfileImageSelection"; import ProfileBottomSheet from "./ProfileBottomSheet"; @@ -8,7 +9,10 @@ import { TERMS_TEXT, PRIVACY_TEXT } from "../_constants/terms"; import { Hobby, ProfileSubmitData } from "@/lib/types/profile"; import { useProfile } from "@/providers/profile-provider"; import { useImageUpload, useProfileSignUp } from "@/hooks/useProfileSignUp"; -import { useNicknameAvailability } from "@/hooks/useNicknameAvailability"; +import { + useNicknameAvailability, + NicknameAvailabilityResponse, +} from "@/hooks/useNicknameAvailability"; import { useRouter } from "next/navigation"; import { HOBBIES, HobbyCategory } from "@/lib/constants/hobbies"; @@ -159,15 +163,30 @@ const TermsDrawer = ({ children }: TermsDrawerProps) => { } try { - const isAvailable = await checkNicknameAvailability(nickname); - - if (!isAvailable) { - alert("중복된 닉네임입니다. 다른 닉네임을 입력해 주세요."); - return; + const res = await checkNicknameAvailability(nickname); + + // 200 OK 응답 처리 + if (res.code === "GEN-000") { + const isAvailable = + typeof res.data === "object" ? res.data?.available : res.data; + if (isAvailable) { + setIsOpen(true); + } else { + alert("중복된 닉네임입니다. 다른 닉네임을 입력해 주세요."); + } + } + } catch (error: unknown) { + if (axios.isAxiosError(error)) { + const res = error.response?.data as NicknameAvailabilityResponse; + + // 백엔드에서 400 등 에러 코드를 보낼 때 (MEM-009 등) + if (res?.code === "MEM-009") { + alert("공백은 닉네임으로 사용할 수 없습니다."); + return; + } } - setIsOpen(true); - } catch (error) { + // 그 외 진짜 예상치 못한 에러 (네트워크, 500 등) console.error("Failed to check nickname availability:", error); alert( "닉네임 중복 확인에 실패했습니다. 잠시 후 다시 시도해 주세요.", diff --git a/hooks/useNicknameAvailability.ts b/hooks/useNicknameAvailability.ts index f2daf61..175a2f0 100644 --- a/hooks/useNicknameAvailability.ts +++ b/hooks/useNicknameAvailability.ts @@ -1,7 +1,7 @@ import { api } from "@/lib/axios"; import { useMutation } from "@tanstack/react-query"; -interface NicknameAvailabilityResponse { +export interface NicknameAvailabilityResponse { code: string; status: number; message: string; @@ -14,35 +14,9 @@ interface NicknameAvailabilityResponse { }; } -const parseNicknameAvailability = ( - response: NicknameAvailabilityResponse, -): boolean => { - const payload = response.data; - - if (typeof payload === "boolean") { - return payload; - } - - if (payload && typeof payload === "object") { - if (typeof payload.available === "boolean") { - return payload.available; - } - - if (typeof payload.isAvailable === "boolean") { - return payload.isAvailable; - } - - if (typeof payload.duplicate === "boolean") { - return !payload.duplicate; - } - } - - throw new Error("닉네임 중복 검사 응답 형식을 확인할 수 없습니다."); -}; - const checkNicknameAvailability = async ( nickname: string, -): Promise => { +): Promise => { const { data: response } = await api.get( "/api/auth/signup/nickname/availability", { @@ -50,7 +24,7 @@ const checkNicknameAvailability = async ( }, ); - return parseNicknameAvailability(response); + return response; }; export const useNicknameAvailability = () => {