From 5f0cfc82be64c06e60a4853f4b302472df7c4a49 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Sat, 22 Nov 2025 02:18:12 +0900 Subject: [PATCH 01/17] =?UTF-8?q?fix:=20settings=20=EC=97=90=EC=84=9C=20se?= =?UTF-8?q?tting=20=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/setting/page.tsx | 4 ++-- src/shared/components/features/header/Header.tsx | 4 ++-- src/shared/constants/router-path.ts | 2 +- src/shared/hooks/useSelectChange.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/setting/page.tsx b/src/app/setting/page.tsx index afa321f..de3652b 100644 --- a/src/app/setting/page.tsx +++ b/src/app/setting/page.tsx @@ -1,3 +1,3 @@ -export default function SettingsPage() { - return
Settings Page
; +export default function SettingPage() { + return
Setting Page
; } diff --git a/src/shared/components/features/header/Header.tsx b/src/shared/components/features/header/Header.tsx index 3ae82d6..560fd88 100644 --- a/src/shared/components/features/header/Header.tsx +++ b/src/shared/components/features/header/Header.tsx @@ -35,10 +35,10 @@ export const Header = () => { Portfolio - Settings + Setting diff --git a/src/shared/constants/router-path.ts b/src/shared/constants/router-path.ts index 2e248f6..eca7e92 100644 --- a/src/shared/constants/router-path.ts +++ b/src/shared/constants/router-path.ts @@ -6,6 +6,6 @@ export const ROUTER_PATH = { AUTO_TRADE: "/auto-trade", TRADE: "/trade", PORTFOLIO: "/portfolio", - SETTINGS: "/settings", + SETTING: "/setting", WALLET: "/wallet", }; diff --git a/src/shared/hooks/useSelectChange.ts b/src/shared/hooks/useSelectChange.ts index 797b20c..46c89f3 100644 --- a/src/shared/hooks/useSelectChange.ts +++ b/src/shared/hooks/useSelectChange.ts @@ -26,7 +26,7 @@ export const useSelectChange = () => { console.error("로그아웃 에러:", error); } } else if (value === "setting") { - router.push(ROUTER_PATH.SETTINGS); + router.push(ROUTER_PATH.SETTING); } }; From 7d8d36c7e8d6739bb532eeb51a2f7728659a0e39 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Sat, 22 Nov 2025 02:26:58 +0900 Subject: [PATCH 02/17] =?UTF-8?q?fix:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/components/features/header/Header.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/shared/components/features/header/Header.tsx b/src/shared/components/features/header/Header.tsx index 560fd88..e2936d8 100644 --- a/src/shared/components/features/header/Header.tsx +++ b/src/shared/components/features/header/Header.tsx @@ -1,13 +1,10 @@ import Image from "next/image"; import Link from "next/link"; -import { UserRound, Wallet } from "lucide-react"; - import Logo from "@/shared/assets/logo.webp"; import { ROUTER_PATH } from "../../../constants"; import { LoginButton } from "../../common"; -import { Avatar, AvatarFallback, AvatarImage } from "../../ui"; export const Header = () => { return ( From e58fb7b938986e26d22cc17eec05856a747c5957 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Sat, 22 Nov 2025 04:05:15 +0900 Subject: [PATCH 03/17] =?UTF-8?q?feat:=20setting=20page=20ui=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/setting/page.tsx | 76 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/src/app/setting/page.tsx b/src/app/setting/page.tsx index de3652b..ec04c86 100644 --- a/src/app/setting/page.tsx +++ b/src/app/setting/page.tsx @@ -1,3 +1,77 @@ +"use client"; + +import { Avatar, AvatarFallback, AvatarImage, Button, Label, useGetSession } from "@/shared"; +import { Camera, UserRound } from "lucide-react"; + export default function SettingPage() { - return
Setting Page
; + const session = useGetSession(); + + const userAvatarUrl = session?.user?.user_metadata?.avatar_url || ""; + const userNickname = session?.user?.user_metadata?.nickname || "Unknown"; + const userEmail = session?.user?.email || ""; + const lastUpdated = session?.user?.updated_at || ""; + + const year = lastUpdated ? new Date(lastUpdated).getFullYear() : ""; + const month = lastUpdated ? String(new Date(lastUpdated).getMonth() + 1).padStart(2, "0") : ""; + const day = lastUpdated ? String(new Date(lastUpdated).getDate()).padStart(2, "0") : ""; + + const Hour = lastUpdated ? String(new Date(lastUpdated).getHours()).padStart(2, "0") : ""; + const Minutes = lastUpdated ? String(new Date(lastUpdated).getMinutes()).padStart(2, "0") : ""; + const Seconds = lastUpdated ? String(new Date(lastUpdated).getSeconds()).padStart(2, "0") : ""; + + const formattedLastUpdated = lastUpdated ? `${year}년 ${month}월 ${day}일 ${Hour}:${Minutes}:${Seconds}` : "N/A"; + + return ( +
+

프로필 설정

+ +
+
+
+ + + + + + + +
+

프로필 사진 변경

+
+ +
+
+ +
+ {userEmail} +
+
+ +
+ +
+
+ {userNickname} +
+ +
+
+
+ +
+ {formattedLastUpdated} +
+
+
+
+
+ ); } From aac35461c082cd84ccb34e838da1a566d6c44f3d Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Sat, 22 Nov 2025 04:31:02 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20=EB=82=A0=EC=A7=9C=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=ED=8C=85=20=ED=95=A8=EC=88=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/setting/utils/formatted-date.ts | 13 +++++++++++++ src/features/setting/utils/index.ts | 1 + 2 files changed, 14 insertions(+) create mode 100644 src/features/setting/utils/formatted-date.ts create mode 100644 src/features/setting/utils/index.ts diff --git a/src/features/setting/utils/formatted-date.ts b/src/features/setting/utils/formatted-date.ts new file mode 100644 index 0000000..df790dc --- /dev/null +++ b/src/features/setting/utils/formatted-date.ts @@ -0,0 +1,13 @@ +export const formattedDate = (dateString: string): string => { + const date = dateString ? new Date(dateString) : null; + + const year = date ? date.getFullYear() : ""; + const month = date ? String(date.getMonth() + 1).padStart(2, "0") : ""; + const day = date ? String(date.getDate()).padStart(2, "0") : ""; + + const Hour = date ? String(date.getHours()).padStart(2, "0") : ""; + const Minutes = date ? String(date.getMinutes()).padStart(2, "0") : ""; + const Seconds = date ? String(date.getSeconds()).padStart(2, "0") : ""; + + return `${year}년 ${month}월 ${day}일 ${Hour}:${Minutes}:${Seconds}`; +}; diff --git a/src/features/setting/utils/index.ts b/src/features/setting/utils/index.ts new file mode 100644 index 0000000..0c8d06d --- /dev/null +++ b/src/features/setting/utils/index.ts @@ -0,0 +1 @@ +export * from "./formatted-date"; From 53d155bd68e4f89876f6aa938817c29414068439 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Sat, 22 Nov 2025 04:31:21 +0900 Subject: [PATCH 05/17] =?UTF-8?q?feat:=20user=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=ED=9B=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/setting/hooks/index.ts | 1 + src/features/setting/hooks/useUserInfo.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 src/features/setting/hooks/index.ts create mode 100644 src/features/setting/hooks/useUserInfo.ts diff --git a/src/features/setting/hooks/index.ts b/src/features/setting/hooks/index.ts new file mode 100644 index 0000000..f34a465 --- /dev/null +++ b/src/features/setting/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useUserInfo"; diff --git a/src/features/setting/hooks/useUserInfo.ts b/src/features/setting/hooks/useUserInfo.ts new file mode 100644 index 0000000..20d482a --- /dev/null +++ b/src/features/setting/hooks/useUserInfo.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useGetSession } from "@/shared"; + +export const useUserInfo = () => { + const session = useGetSession(); + + const userAvatarUrl = session?.user?.user_metadata?.avatar_url || ""; + const userNickname = session?.user?.user_metadata?.nickname || "Unknown"; + const userEmail = session?.user?.email || ""; + const lastUpdated = session?.user?.updated_at || ""; + + return { userAvatarUrl, userNickname, userEmail, lastUpdated }; +}; From f84c3336de6309db0a9e707bc41fb49d2f1f10d5 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Sat, 22 Nov 2025 04:31:59 +0900 Subject: [PATCH 06/17] =?UTF-8?q?refactor:=20setting=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/setting/page.tsx | 69 +-------------------- src/features/index.ts | 1 + src/features/setting/index.ts | 1 + src/features/setting/ui/ProfileSection.tsx | 31 +++++++++ src/features/setting/ui/UserInfoSection.tsx | 41 ++++++++++++ src/features/setting/ui/index.ts | 2 + 6 files changed, 79 insertions(+), 66 deletions(-) create mode 100644 src/features/setting/index.ts create mode 100644 src/features/setting/ui/ProfileSection.tsx create mode 100644 src/features/setting/ui/UserInfoSection.tsx create mode 100644 src/features/setting/ui/index.ts diff --git a/src/app/setting/page.tsx b/src/app/setting/page.tsx index ec04c86..304b171 100644 --- a/src/app/setting/page.tsx +++ b/src/app/setting/page.tsx @@ -1,76 +1,13 @@ -"use client"; - -import { Avatar, AvatarFallback, AvatarImage, Button, Label, useGetSession } from "@/shared"; -import { Camera, UserRound } from "lucide-react"; +import { ProfileSection, UserInfoSection } from "@/features"; export default function SettingPage() { - const session = useGetSession(); - - const userAvatarUrl = session?.user?.user_metadata?.avatar_url || ""; - const userNickname = session?.user?.user_metadata?.nickname || "Unknown"; - const userEmail = session?.user?.email || ""; - const lastUpdated = session?.user?.updated_at || ""; - - const year = lastUpdated ? new Date(lastUpdated).getFullYear() : ""; - const month = lastUpdated ? String(new Date(lastUpdated).getMonth() + 1).padStart(2, "0") : ""; - const day = lastUpdated ? String(new Date(lastUpdated).getDate()).padStart(2, "0") : ""; - - const Hour = lastUpdated ? String(new Date(lastUpdated).getHours()).padStart(2, "0") : ""; - const Minutes = lastUpdated ? String(new Date(lastUpdated).getMinutes()).padStart(2, "0") : ""; - const Seconds = lastUpdated ? String(new Date(lastUpdated).getSeconds()).padStart(2, "0") : ""; - - const formattedLastUpdated = lastUpdated ? `${year}년 ${month}월 ${day}일 ${Hour}:${Minutes}:${Seconds}` : "N/A"; - return (

프로필 설정

-
-
- - - - - - - -
-

프로필 사진 변경

-
- -
-
- -
- {userEmail} -
-
- -
- -
-
- {userNickname} -
- -
-
-
- -
- {formattedLastUpdated} -
-
-
+ +
); diff --git a/src/features/index.ts b/src/features/index.ts index 2483cb0..c33a01f 100644 --- a/src/features/index.ts +++ b/src/features/index.ts @@ -1,4 +1,5 @@ export * from "./home"; export * from "./login"; +export * from "./setting"; export * from "./signup"; export * from "./wallet"; diff --git a/src/features/setting/index.ts b/src/features/setting/index.ts new file mode 100644 index 0000000..4aedf59 --- /dev/null +++ b/src/features/setting/index.ts @@ -0,0 +1 @@ +export * from "./ui"; diff --git a/src/features/setting/ui/ProfileSection.tsx b/src/features/setting/ui/ProfileSection.tsx new file mode 100644 index 0000000..d26d87d --- /dev/null +++ b/src/features/setting/ui/ProfileSection.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { Avatar, AvatarFallback, AvatarImage, Button } from "@/shared"; +import { Camera, UserRound } from "lucide-react"; + +import { useUserInfo } from "../hooks"; + +export const ProfileSection = () => { + const { userAvatarUrl } = useUserInfo(); + + return ( +
+
+ + + + + + + +
+

프로필 사진 변경

+
+ ); +}; diff --git a/src/features/setting/ui/UserInfoSection.tsx b/src/features/setting/ui/UserInfoSection.tsx new file mode 100644 index 0000000..6b3e9df --- /dev/null +++ b/src/features/setting/ui/UserInfoSection.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { Button, Label } from "@/shared"; + +import { useUserInfo } from "../hooks"; +import { formattedDate } from "../utils"; + +export const UserInfoSection = () => { + const { userEmail, userNickname, lastUpdated } = useUserInfo(); + + const formattedLastUpdated = formattedDate(lastUpdated || ""); + + return ( +
+
+ +
+ {userEmail} +
+
+ +
+ +
+
+ {userNickname} +
+ +
+
+
+ +
+ {formattedLastUpdated} +
+
+
+ ); +}; diff --git a/src/features/setting/ui/index.ts b/src/features/setting/ui/index.ts new file mode 100644 index 0000000..1c91256 --- /dev/null +++ b/src/features/setting/ui/index.ts @@ -0,0 +1,2 @@ +export * from "./ProfileSection"; +export * from "./UserInfoSection"; From 06de52161789bc485e3b64488b464f8acf7dd224 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Sat, 22 Nov 2025 15:42:42 +0900 Subject: [PATCH 07/17] =?UTF-8?q?feat:=20shadcn=20skeleton=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/components/ui/index.ts | 1 + src/shared/components/ui/skeleton.tsx | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 src/shared/components/ui/skeleton.tsx diff --git a/src/shared/components/ui/index.ts b/src/shared/components/ui/index.ts index 43ba4b7..4946e48 100644 --- a/src/shared/components/ui/index.ts +++ b/src/shared/components/ui/index.ts @@ -9,6 +9,7 @@ export * from "./input"; export * from "./label"; export * from "./select"; export * from "./separator"; +export * from "./skeleton"; export * from "./sonner"; export * from "./spinner"; export * from "./table"; diff --git a/src/shared/components/ui/skeleton.tsx b/src/shared/components/ui/skeleton.tsx new file mode 100644 index 0000000..c270195 --- /dev/null +++ b/src/shared/components/ui/skeleton.tsx @@ -0,0 +1,7 @@ +import { cn } from "../../utils"; + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return
; +} + +export { Skeleton }; From 0dc423757012e7f82e8047ce1b24caac4b4d7bd2 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Sat, 22 Nov 2025 15:44:30 +0900 Subject: [PATCH 08/17] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=EC=83=81=ED=83=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/setting/hooks/useUserInfo.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/features/setting/hooks/useUserInfo.ts b/src/features/setting/hooks/useUserInfo.ts index 20d482a..038e9db 100644 --- a/src/features/setting/hooks/useUserInfo.ts +++ b/src/features/setting/hooks/useUserInfo.ts @@ -1,14 +1,15 @@ "use client"; -import { useGetSession } from "@/shared"; +import { useGetSession, useIsSessionLoaded } from "@/shared"; export const useUserInfo = () => { const session = useGetSession(); + const isLoaded = useIsSessionLoaded(); const userAvatarUrl = session?.user?.user_metadata?.avatar_url || ""; const userNickname = session?.user?.user_metadata?.nickname || "Unknown"; const userEmail = session?.user?.email || ""; const lastUpdated = session?.user?.updated_at || ""; - return { userAvatarUrl, userNickname, userEmail, lastUpdated }; + return { userAvatarUrl, userNickname, userEmail, lastUpdated, isLoaded }; }; From c2044f9e6cd488e7b92dc45b95e68e93db00909b Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Sat, 22 Nov 2025 15:44:52 +0900 Subject: [PATCH 09/17] =?UTF-8?q?refactor:=20=EC=9C=A0=EC=A0=80=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=95=84=EB=93=9C=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../setting/components/common/index.ts | 0 .../components/features/EmailField.tsx | 18 +++++++++ .../components/features/LastLoginField.tsx | 25 ++++++++++++ .../components/features/NicknameField.tsx | 23 +++++++++++ .../setting/components/features/index.ts | 3 ++ src/features/setting/components/index.ts | 1 + src/features/setting/ui/UserInfoSection.tsx | 40 +++---------------- 7 files changed, 75 insertions(+), 35 deletions(-) create mode 100644 src/features/setting/components/common/index.ts create mode 100644 src/features/setting/components/features/EmailField.tsx create mode 100644 src/features/setting/components/features/LastLoginField.tsx create mode 100644 src/features/setting/components/features/NicknameField.tsx create mode 100644 src/features/setting/components/features/index.ts create mode 100644 src/features/setting/components/index.ts diff --git a/src/features/setting/components/common/index.ts b/src/features/setting/components/common/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/features/setting/components/features/EmailField.tsx b/src/features/setting/components/features/EmailField.tsx new file mode 100644 index 0000000..5170d38 --- /dev/null +++ b/src/features/setting/components/features/EmailField.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { Label, Skeleton } from "@/shared"; + +import { useUserInfo } from "../../hooks"; + +export const EmailField = () => { + const { userEmail, isLoaded } = useUserInfo(); + + return ( +
+ +
+ {!isLoaded ? :

{userEmail}

} +
+
+ ); +}; diff --git a/src/features/setting/components/features/LastLoginField.tsx b/src/features/setting/components/features/LastLoginField.tsx new file mode 100644 index 0000000..a5b42eb --- /dev/null +++ b/src/features/setting/components/features/LastLoginField.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { Label, Skeleton } from "@/shared"; + +import { useUserInfo } from "../../hooks"; +import { formattedDate } from "../../utils"; + +export const LastLoginField = () => { + const { lastUpdated, isLoaded } = useUserInfo(); + + const formattedLastUpdated = formattedDate(lastUpdated || ""); + + return ( +
+ +
+ {!isLoaded ? ( + + ) : ( +

{formattedLastUpdated}

+ )} +
+
+ ); +}; diff --git a/src/features/setting/components/features/NicknameField.tsx b/src/features/setting/components/features/NicknameField.tsx new file mode 100644 index 0000000..f4d5c32 --- /dev/null +++ b/src/features/setting/components/features/NicknameField.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { Button, Label } from "@/shared"; + +import { useUserInfo } from "../../hooks"; + +export const NicknameField = () => { + const { userNickname } = useUserInfo(); + + return ( +
+ +
+
+ {userNickname} +
+ +
+
+ ); +}; diff --git a/src/features/setting/components/features/index.ts b/src/features/setting/components/features/index.ts new file mode 100644 index 0000000..8971f9e --- /dev/null +++ b/src/features/setting/components/features/index.ts @@ -0,0 +1,3 @@ +export * from "./EmailField"; +export * from "./LastLoginField"; +export * from "./NicknameField"; diff --git a/src/features/setting/components/index.ts b/src/features/setting/components/index.ts new file mode 100644 index 0000000..0e84926 --- /dev/null +++ b/src/features/setting/components/index.ts @@ -0,0 +1 @@ +export * from "./features"; diff --git a/src/features/setting/ui/UserInfoSection.tsx b/src/features/setting/ui/UserInfoSection.tsx index 6b3e9df..6c07b00 100644 --- a/src/features/setting/ui/UserInfoSection.tsx +++ b/src/features/setting/ui/UserInfoSection.tsx @@ -1,41 +1,11 @@ -"use client"; - -import { Button, Label } from "@/shared"; - -import { useUserInfo } from "../hooks"; -import { formattedDate } from "../utils"; +import { EmailField, LastLoginField, NicknameField } from "../components"; export const UserInfoSection = () => { - const { userEmail, userNickname, lastUpdated } = useUserInfo(); - - const formattedLastUpdated = formattedDate(lastUpdated || ""); - return ( -
-
- -
- {userEmail} -
-
- -
- -
-
- {userNickname} -
- -
-
-
- -
- {formattedLastUpdated} -
-
+
+ + +
); }; From 6109a702f15bb07b12e914c412d09b08dff96046 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Sat, 22 Nov 2025 16:47:21 +0900 Subject: [PATCH 10/17] =?UTF-8?q?feat:=20nickname=20=EB=B3=80=EA=B2=BD=20r?= =?UTF-8?q?oute=20handler=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/user/nickname/route.ts | 50 ++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/app/api/user/nickname/route.ts diff --git a/src/app/api/user/nickname/route.ts b/src/app/api/user/nickname/route.ts new file mode 100644 index 0000000..a8b62d6 --- /dev/null +++ b/src/app/api/user/nickname/route.ts @@ -0,0 +1,50 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +import { createClient as createServerClient } from "@/shared/utils/supabase/server"; + +export async function PATCH(request: Request) { + try { + const supabase = createServerClient(cookies()); + const { data: userData, error: userError } = await supabase.auth.getUser(); + + if (userError || !userData.user) { + return NextResponse.json({ status: "error", data: null, error: "Unauthorized" }, { status: 401 }); + } + + const body = await request.json(); + const { nickname } = body; + + // 닉네임 검증 + if (!nickname || typeof nickname !== "string") { + return NextResponse.json({ status: "error", data: null, error: "닉네임을 입력해주세요." }, { status: 400 }); + } + + if (nickname.length < 2 || nickname.length > 20) { + return NextResponse.json( + { status: "error", data: null, error: "닉네임은 2자 이상 20자 이하로 입력해주세요." }, + { status: 400 }, + ); + } + + // 사용자 메타데이터 업데이트 + const { data, error } = await supabase.auth.updateUser({ + data: { + nickname, + }, + }); + + if (error) { + console.error("Nickname update error:", error); + return NextResponse.json({ status: "error", data: null, error: error.message }, { status: 500 }); + } + + return NextResponse.json( + { status: "success", data: { nickname: data.user.user_metadata.nickname } }, + { status: 200 }, + ); + } catch (error) { + console.error("Nickname API error:", error); + return NextResponse.json({ status: "error", data: null, error: "Internal server error" }, { status: 500 }); + } +} From edaae32473fe315f9334a0364fef018afe0e97d8 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Sat, 22 Nov 2025 16:47:31 +0900 Subject: [PATCH 11/17] =?UTF-8?q?feat:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../setting/apis/change-nickname.api.ts | 26 +++++++++++++++++++ src/features/setting/apis/index.ts | 1 + 2 files changed, 27 insertions(+) create mode 100644 src/features/setting/apis/change-nickname.api.ts create mode 100644 src/features/setting/apis/index.ts diff --git a/src/features/setting/apis/change-nickname.api.ts b/src/features/setting/apis/change-nickname.api.ts new file mode 100644 index 0000000..94f866e --- /dev/null +++ b/src/features/setting/apis/change-nickname.api.ts @@ -0,0 +1,26 @@ +import { APIResponse } from "@/shared"; + +export interface ChangeNicknameParams { + nickname: string; +} + +export type ChangeNicknameResponse = APIResponse<{ + nickname: string; +}>; + +export const changeNicknameAPI = async ({ nickname }: ChangeNicknameParams): Promise => { + const response = await fetch("/api/user/nickname", { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ nickname }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "닉네임 변경에 실패했습니다."); + } + + return response.json(); +}; diff --git a/src/features/setting/apis/index.ts b/src/features/setting/apis/index.ts new file mode 100644 index 0000000..ab3fb33 --- /dev/null +++ b/src/features/setting/apis/index.ts @@ -0,0 +1 @@ +export * from "./change-nickname.api"; From 5bae75732ee281f4db17aa700cfbb5ab9520ce58 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Sat, 22 Nov 2025 16:47:53 +0900 Subject: [PATCH 12/17] =?UTF-8?q?fix:=20=EC=9D=98=EB=AF=B8=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=8A=A4=ED=83=80=EC=9D=BC=EB=A7=81=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/setting/components/features/LastLoginField.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/setting/components/features/LastLoginField.tsx b/src/features/setting/components/features/LastLoginField.tsx index a5b42eb..21cfd04 100644 --- a/src/features/setting/components/features/LastLoginField.tsx +++ b/src/features/setting/components/features/LastLoginField.tsx @@ -13,7 +13,7 @@ export const LastLoginField = () => { return (
-
+
{!isLoaded ? ( ) : ( From c445c2d26e3e0eb6f29272f405e28e5a5844598f Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Sat, 22 Nov 2025 16:48:47 +0900 Subject: [PATCH 13/17] =?UTF-8?q?feat:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85?= =?UTF-8?q?=EC=97=90=20supabase=20=EC=84=B8=EC=85=98=20=EA=B0=B1=EC=8B=A0?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/setting/hooks/index.ts | 1 + .../setting/hooks/useChangeNickname.ts | 60 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 src/features/setting/hooks/useChangeNickname.ts diff --git a/src/features/setting/hooks/index.ts b/src/features/setting/hooks/index.ts index f34a465..f54065d 100644 --- a/src/features/setting/hooks/index.ts +++ b/src/features/setting/hooks/index.ts @@ -1 +1,2 @@ export * from "./useUserInfo"; +export * from "./useChangeNickname"; diff --git a/src/features/setting/hooks/useChangeNickname.ts b/src/features/setting/hooks/useChangeNickname.ts new file mode 100644 index 0000000..67a1535 --- /dev/null +++ b/src/features/setting/hooks/useChangeNickname.ts @@ -0,0 +1,60 @@ +import { useState } from "react"; + +import { useMutation } from "@tanstack/react-query"; +import { toast } from "sonner"; + +import { createClient } from "@/shared/utils/supabase"; + +import { changeNicknameAPI } from "../apis"; + +type Props = { + userNickname: string; +}; + +export const useChangeNickname = ({ userNickname }: Props) => { + const [fieldState, setFieldState] = useState<"view" | "edit">("view"); + const [inputValue, setInputValue] = useState(userNickname); + const supabase = createClient(); + + const onChangeNickname = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + }; + + const { mutate: changeNickname, isPending } = useMutation({ + mutationFn: () => changeNicknameAPI({ nickname: inputValue }), + onError: (error: Error) => { + toast.error(error.message); + setInputValue(userNickname); // 에러 시 원래 값으로 복구 + }, + onSuccess: async () => { + toast.success("닉네임이 변경되었습니다."); + setFieldState("view"); + + // Supabase 세션 강제 갱신 + await supabase.auth.refreshSession(); + + // 페이지 새로고침으로 UI 갱신 + window.location.reload(); + }, + }); + + const onClickEditButton = () => { + if (fieldState === "edit") { + if (inputValue.trim() === userNickname) { + setFieldState("view"); + return; + } + changeNickname(); + } else { + setFieldState("edit"); + } + }; + + return { + fieldState, + inputValue, + onChangeNickname, + onClickEditButton, + isPending, + }; +}; From b3f04b8ad9c2e6b15c7093f577a30f6a4983f8ce Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Sat, 22 Nov 2025 16:48:56 +0900 Subject: [PATCH 14/17] =?UTF-8?q?feat:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/features/NicknameField.tsx | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/features/setting/components/features/NicknameField.tsx b/src/features/setting/components/features/NicknameField.tsx index f4d5c32..fde89f5 100644 --- a/src/features/setting/components/features/NicknameField.tsx +++ b/src/features/setting/components/features/NicknameField.tsx @@ -1,21 +1,35 @@ "use client"; -import { Button, Label } from "@/shared"; +import { Button, Input, Label, Skeleton, Spinner } from "@/shared"; -import { useUserInfo } from "../../hooks"; +import { useChangeNickname, useUserInfo } from "../../hooks"; export const NicknameField = () => { - const { userNickname } = useUserInfo(); + const { userNickname, isLoaded } = useUserInfo(); + + const { fieldState, inputValue, onChangeNickname, onClickEditButton, isPending } = useChangeNickname({ + userNickname, + }); return (
-
- {userNickname} -
-
From 52858f51dde09a1073c4f84ddec1153091a6d7f8 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Sat, 22 Nov 2025 17:03:20 +0900 Subject: [PATCH 15/17] =?UTF-8?q?fix:=20toast=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EB=B0=8F=20=EC=83=89=EC=83=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/provider/AppProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/provider/AppProvider.tsx b/src/shared/provider/AppProvider.tsx index b8009be..0818d6a 100644 --- a/src/shared/provider/AppProvider.tsx +++ b/src/shared/provider/AppProvider.tsx @@ -10,7 +10,7 @@ export const AppProvider = ({ children }: Props) => { return ( {children} - + ); }; From 026c151ee68e871bad71bfdb06101159a3fe8b2c Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Sat, 22 Nov 2025 17:03:46 +0900 Subject: [PATCH 16/17] =?UTF-8?q?fix:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=ED=9B=84=20supabase=20=EC=84=B8=EC=85=98?= =?UTF-8?q?=20=EC=83=88=EB=A1=9C=EA=B3=A0=EC=B9=A8=20=EA=B8=B0=EB=8A=A5=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/features/setting/hooks/useChangeNickname.ts | 12 +++++++----- src/features/setting/hooks/useUserInfo.ts | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/features/setting/hooks/useChangeNickname.ts b/src/features/setting/hooks/useChangeNickname.ts index 67a1535..a06a723 100644 --- a/src/features/setting/hooks/useChangeNickname.ts +++ b/src/features/setting/hooks/useChangeNickname.ts @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useMutation } from "@tanstack/react-query"; import { toast } from "sonner"; @@ -16,6 +16,11 @@ export const useChangeNickname = ({ userNickname }: Props) => { const [inputValue, setInputValue] = useState(userNickname); const supabase = createClient(); + // userNickname이 변경되면 inputValue 동기화 (세션 새로고침 후) + useEffect(() => { + setInputValue(userNickname); + }, [userNickname]); + const onChangeNickname = (e: React.ChangeEvent) => { setInputValue(e.target.value); }; @@ -30,11 +35,8 @@ export const useChangeNickname = ({ userNickname }: Props) => { toast.success("닉네임이 변경되었습니다."); setFieldState("view"); - // Supabase 세션 강제 갱신 + // Supabase 세션 강제 갱신 (AuthProvider가 감지하여 Zustand 스토어 업데이트) await supabase.auth.refreshSession(); - - // 페이지 새로고침으로 UI 갱신 - window.location.reload(); }, }); diff --git a/src/features/setting/hooks/useUserInfo.ts b/src/features/setting/hooks/useUserInfo.ts index 038e9db..0e0f572 100644 --- a/src/features/setting/hooks/useUserInfo.ts +++ b/src/features/setting/hooks/useUserInfo.ts @@ -7,7 +7,7 @@ export const useUserInfo = () => { const isLoaded = useIsSessionLoaded(); const userAvatarUrl = session?.user?.user_metadata?.avatar_url || ""; - const userNickname = session?.user?.user_metadata?.nickname || "Unknown"; + const userNickname = session?.user?.user_metadata?.nickname || ""; const userEmail = session?.user?.email || ""; const lastUpdated = session?.user?.updated_at || ""; From 625de44c07f442832188176a2b65e2233d09fb3c Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Sat, 22 Nov 2025 17:09:17 +0900 Subject: [PATCH 17/17] =?UTF-8?q?feat:=20=EC=BD=94=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EB=9F=BF=20=EC=A7=80=EC=B9=A8=EC=84=9C=20=EC=BB=A4=EB=B0=8B=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index cd57444..961bbe7 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,5 @@ next-env.d.ts # local env files .env.local /.vscode -/docs \ No newline at end of file +/docs +/.github/copilot-instructions.md \ No newline at end of file