From 100001a0a15e2e88687d1869d36b9395c2ecc5f6 Mon Sep 17 00:00:00 2001 From: aken-you Date: Thu, 9 Oct 2025 22:34:05 +0900 Subject: [PATCH 01/12] =?UTF-8?q?refactor:=20=EC=96=B4=EB=93=9C=EB=AF=BC?= =?UTF-8?q?=20layout=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(admin)/admin/page.tsx | 36 +------------------------ app/(admin)/layout.tsx | 9 ++++++- src/widgets/admin/ui/admin-side-bar.tsx | 34 +++++++++++++++++++++++ 3 files changed, 43 insertions(+), 36 deletions(-) create mode 100644 src/widgets/admin/ui/admin-side-bar.tsx diff --git a/app/(admin)/admin/page.tsx b/app/(admin)/admin/page.tsx index 2428c1be..7f691359 100644 --- a/app/(admin)/admin/page.tsx +++ b/app/(admin)/admin/page.tsx @@ -19,41 +19,7 @@ export default async function AdminPage() { return ( -
- - -
- -
-
+
); } diff --git a/app/(admin)/layout.tsx b/app/(admin)/layout.tsx index 0c9f2213..6a18d503 100644 --- a/app/(admin)/layout.tsx +++ b/app/(admin)/layout.tsx @@ -5,6 +5,7 @@ import { clsx } from 'clsx'; import type { Metadata } from 'next'; import localFont from 'next/font/local'; import MainProvider from '@/app/provider'; +import AdminSideBar from '@/widgets/admin/ui/admin-side-bar'; export const metadata: Metadata = { title: 'ZERO-ONE', @@ -31,7 +32,13 @@ export default function AdminLayout({ {GTM_ID && } - {children} + +
+ + +
{children}
+
+
); diff --git a/src/widgets/admin/ui/admin-side-bar.tsx b/src/widgets/admin/ui/admin-side-bar.tsx new file mode 100644 index 00000000..a0f8960f --- /dev/null +++ b/src/widgets/admin/ui/admin-side-bar.tsx @@ -0,0 +1,34 @@ +import Image from 'next/image'; +import Link from 'next/link'; + +export default function AdminSideBar() { + return ( + + ); +} From bf7c79cf70b476b8cea8b4f404a9156e58027506 Mon Sep 17 00:00:00 2001 From: aken-you Date: Thu, 9 Oct 2025 23:05:29 +0900 Subject: [PATCH 02/12] =?UTF-8?q?feat:=20TabMenu=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/tab-menu/index.tsx | 41 +++++++++++++++++++++++++ src/widgets/admin/ui/admin-side-bar.tsx | 15 +++++---- 2 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 src/shared/ui/tab-menu/index.tsx diff --git a/src/shared/ui/tab-menu/index.tsx b/src/shared/ui/tab-menu/index.tsx new file mode 100644 index 00000000..cba11d4b --- /dev/null +++ b/src/shared/ui/tab-menu/index.tsx @@ -0,0 +1,41 @@ +import { cva } from 'class-variance-authority'; +import { cn } from '@/shared/shadcn/lib/utils'; + +interface TabMenuProps { + active?: boolean; + className?: string; + children: React.ReactNode; +} + +const tabMenuVariants = cva( + 'font-designer-14m rounded-100 w-full px-200 py-150', + { + variants: { + color: { + default: + 'bg-fill-neutral-subtle-default text-text-subtle hover:bg-fill-neutral-subtle-hover active:bg-fill-neutral-subtle-pressed', + active: 'bg-background-accent-blue-strong text-text-inverse', + }, + }, + defaultVariants: { + color: 'default', + }, + }, +); + +export default function TabMenu({ + active = false, + className, + children, +}: TabMenuProps) { + return ( +
+ {children} +
+ ); +} diff --git a/src/widgets/admin/ui/admin-side-bar.tsx b/src/widgets/admin/ui/admin-side-bar.tsx index a0f8960f..8112430a 100644 --- a/src/widgets/admin/ui/admin-side-bar.tsx +++ b/src/widgets/admin/ui/admin-side-bar.tsx @@ -1,5 +1,6 @@ import Image from 'next/image'; import Link from 'next/link'; +import TabMenu from '@/shared/ui/tab-menu'; export default function AdminSideBar() { return ( @@ -22,12 +23,14 @@ export default function AdminSideBar() { ); From 8127979091bc340084eca6424fece07e533e3a6f Mon Sep 17 00:00:00 2001 From: aken-you Date: Fri, 10 Oct 2025 00:38:25 +0900 Subject: [PATCH 03/12] =?UTF-8?q?refactor:=20=EC=96=B4=EB=93=9C=EB=AF=BC?= =?UTF-8?q?=20=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EC=BD=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/widgets/admin/ui/admin-side-bar.tsx | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/widgets/admin/ui/admin-side-bar.tsx b/src/widgets/admin/ui/admin-side-bar.tsx index 8112430a..d8b5282e 100644 --- a/src/widgets/admin/ui/admin-side-bar.tsx +++ b/src/widgets/admin/ui/admin-side-bar.tsx @@ -1,35 +1,25 @@ import Image from 'next/image'; import Link from 'next/link'; import TabMenu from '@/shared/ui/tab-menu'; +import LogoutIcon from 'public/icons/logout.svg'; export default function AdminSideBar() { return ( From c2a7dc9da26713b41be2eb18ed697d45db5e9877 Mon Sep 17 00:00:00 2001 From: aken-you Date: Fri, 10 Oct 2025 01:07:15 +0900 Subject: [PATCH 04/12] =?UTF-8?q?refactor:=20AdminPage=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(admin)/admin/page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/(admin)/admin/page.tsx b/app/(admin)/admin/page.tsx index 7f691359..46e92df5 100644 --- a/app/(admin)/admin/page.tsx +++ b/app/(admin)/admin/page.tsx @@ -3,8 +3,6 @@ import { HydrationBoundary, QueryClient, } from '@tanstack/react-query'; -import Image from 'next/image'; -import Link from 'next/link'; import { getMemberListInServer } from '@/features/admin/api/member-list.server'; import MemberListTable from '@/features/admin/ui/member-list-table'; From 140106fadd40119aacdd4e816675921f5e923b81 Mon Sep 17 00:00:00 2001 From: aken-you Date: Fri, 10 Oct 2025 01:09:33 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=83=81=EC=84=B8=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=8E=98=EC=9D=B4=EC=A7=80=20layout=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(admin)/admin/detail/[id]/layout.tsx | 30 ++++++++++++ app/(admin)/admin/detail/[id]/page.tsx | 5 ++ app/(admin)/admin/detail/page.tsx | 5 ++ .../admin/ui/admin-detail-side-bar.tsx | 46 +++++++++++++++++++ 4 files changed, 86 insertions(+) create mode 100644 app/(admin)/admin/detail/[id]/layout.tsx create mode 100644 app/(admin)/admin/detail/[id]/page.tsx create mode 100644 app/(admin)/admin/detail/page.tsx create mode 100644 src/widgets/admin/ui/admin-detail-side-bar.tsx diff --git a/app/(admin)/admin/detail/[id]/layout.tsx b/app/(admin)/admin/detail/[id]/layout.tsx new file mode 100644 index 00000000..66947cef --- /dev/null +++ b/app/(admin)/admin/detail/[id]/layout.tsx @@ -0,0 +1,30 @@ +import { ArrowLeftIcon } from 'lucide-react'; +import Link from 'next/link'; +import AdminDetailSideBar from '@/widgets/admin/ui/admin-detail-side-bar'; + +export default async function AdminDetailLayout({ + children, + params, +}: { + children: React.ReactNode; + params: Promise<{ id: string }>; +}) { + const { id: memberId } = await params; + + return ( +
+
+ + + +

사용자 상세 정보

+
+ +
+ + +
{children}
+
+
+ ); +} diff --git a/app/(admin)/admin/detail/[id]/page.tsx b/app/(admin)/admin/detail/[id]/page.tsx new file mode 100644 index 00000000..3774cfa8 --- /dev/null +++ b/app/(admin)/admin/detail/[id]/page.tsx @@ -0,0 +1,5 @@ +import { notFound } from 'next/navigation'; + +export default function AdminDetailByIdPage() { + notFound(); // /admin/detail/[id] 경로 404 처리 +} diff --git a/app/(admin)/admin/detail/page.tsx b/app/(admin)/admin/detail/page.tsx new file mode 100644 index 00000000..9292f648 --- /dev/null +++ b/app/(admin)/admin/detail/page.tsx @@ -0,0 +1,5 @@ +import { notFound } from 'next/navigation'; + +export default function AdminDetailPage() { + notFound(); // /admin/detail 경로 404 처리 +} diff --git a/src/widgets/admin/ui/admin-detail-side-bar.tsx b/src/widgets/admin/ui/admin-detail-side-bar.tsx new file mode 100644 index 00000000..e00a2949 --- /dev/null +++ b/src/widgets/admin/ui/admin-detail-side-bar.tsx @@ -0,0 +1,46 @@ +'use client'; + +import Link from 'next/link'; + +import { usePathname } from 'next/navigation'; +import TabMenu from '@/shared/ui/tab-menu'; + +interface AdminDetailSideBarProps { + memberId: string; +} + +export default function AdminDetailSideBar({ + memberId, +}: AdminDetailSideBarProps) { + const pathname = usePathname(); + + return ( + + ); +} From 8e0ca1df70a044708617c65d0870550510f41c3a Mon Sep 17 00:00:00 2001 From: aken-you Date: Fri, 10 Oct 2025 01:10:18 +0900 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=20=EA=B3=84=EC=A0=95=20?= =?UTF-8?q?=EC=9D=B4=EB=A0=A5=20=ED=8E=98=EC=9D=B4=EC=A7=80=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 --- .../detail/[id]/account-history/page.tsx | 189 ++++++++++++++++++ .../admin/api/account-history.server.ts | 16 ++ src/features/admin/api/types.ts | 21 ++ src/shared/lib/time.ts | 8 + 4 files changed, 234 insertions(+) create mode 100644 app/(admin)/admin/detail/[id]/account-history/page.tsx create mode 100644 src/features/admin/api/account-history.server.ts diff --git a/app/(admin)/admin/detail/[id]/account-history/page.tsx b/app/(admin)/admin/detail/[id]/account-history/page.tsx new file mode 100644 index 00000000..6ffff85d --- /dev/null +++ b/app/(admin)/admin/detail/[id]/account-history/page.tsx @@ -0,0 +1,189 @@ +import { + dehydrate, + HydrationBoundary, + QueryClient, +} from '@tanstack/react-query'; +import { ArrowRightIcon } from 'lucide-react'; +import { getAccountHistoriesInServer } from '@/features/admin/api/account-history.server'; +import { GetAccountHistoriesResponse } from '@/features/admin/api/types'; +import { formatHHMM, formatYYYYMMDD } from '@/shared/lib/time'; +import Badge from '@/shared/ui/badge'; + +export default async function AccountHistoryPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const queryClient = new QueryClient(); + const { id: memberId } = await params; + + // 서버 side에서 첫 페이지 데이터 미리 가져오기 + await queryClient.prefetchQuery({ + queryKey: ['accountHistory', memberId], + queryFn: () => getAccountHistoriesInServer({ memberId: Number(memberId) }), + }); + + const data: GetAccountHistoriesResponse = await queryClient.getQueryData([ + 'accountHistory', + memberId, + ]); + + return ( + +
+
+
+ + 계정 생성일 + +
+ {formatYYYYMMDD(data.joinedAt)} +
+
+ +
+ + 최근 로그인 + +
+ {data.loginMostRecentlyAt + ? formatYYYYMMDD(data.loginMostRecentlyAt) + : '기록 없음'} +
+
+ +
+ + 권한 + +
일반
+
+ +
+ + 계정 상태 + + + 활성 +
+
+ + + + +
+
+ ); +} + +function RecentLoginHistory({ + loginHists, +}: Pick) { + return ( +
+

최근 로그인 기록

+ +
    + {loginHists.map((hist) => ( +
  • + {formatYYYYMMDD(hist)} + {formatHHMM(hist)} +
  • + ))} +
+
+ ); +} + +function RecentRoleChangeHistory({ + roleChangeHists, +}: Pick) { + return ( +
+

권한 변경 이력

+ + {roleChangeHists.length > 0 ? ( +
    + {roleChangeHists.map((hist) => ( +
  • +
    + {formatYYYYMMDD(hist.changedAt)} + {formatHHMM(hist.changedAt)} +
    + +
    + {hist.from} + + + + {hist.to} +
    +
  • + ))} +
+ ) : ( +
+ 변경 이력이 없습니다. +
+ )} +
+ ); +} + +function RecentStatusChangeHistory({ + memberStatusChangeHists, +}: Pick) { + return ( +
+

+ 계정 상태 변경 이력 +

+ + {memberStatusChangeHists.length > 0 ? ( +
    + {memberStatusChangeHists.map((hist) => ( +
  • +
    + {formatYYYYMMDD(hist.changedAt)} + {formatHHMM(hist.changedAt)} +
    + +
    + + {hist.from} + + + + + + {hist.to} + +
    +
  • + ))} +
+ ) : ( +
+ 변경 이력이 없습니다. +
+ )} +
+ ); +} diff --git a/src/features/admin/api/account-history.server.ts b/src/features/admin/api/account-history.server.ts new file mode 100644 index 00000000..48e48658 --- /dev/null +++ b/src/features/admin/api/account-history.server.ts @@ -0,0 +1,16 @@ +import { axiosServerInstance } from '@/shared/tanstack-query/axios.server'; +import { + GetAccountHistoriesRequest, + GetAccountHistoriesResponse, +} from './types'; + +// 회원 계정 이력 조회 +export const getAccountHistoriesInServer = async ({ + memberId, +}: GetAccountHistoriesRequest): Promise => { + const res = await axiosServerInstance.get( + `/admin/members/${memberId}/account-histories`, + ); + + return res.data.content; +}; diff --git a/src/features/admin/api/types.ts b/src/features/admin/api/types.ts index 9f4d17a9..0f1f91d7 100644 --- a/src/features/admin/api/types.ts +++ b/src/features/admin/api/types.ts @@ -28,3 +28,24 @@ export interface GetMemberListResponse { }; }[]; } + +export interface GetAccountHistoriesRequest { + memberId: number; +} + +export interface GetAccountHistoriesResponse { + memberId: number; + joinedAt: string; + loginMostRecentlyAt: string; + loginHists: string[]; + roleChangeHists: { + changedAt: string; + from: string; + to: string; + }[]; + memberStatusChangeHists: { + changedAt: string; + from: string; + to: string; + }[]; +} diff --git a/src/shared/lib/time.ts b/src/shared/lib/time.ts index 567c1d9e..704f878d 100644 --- a/src/shared/lib/time.ts +++ b/src/shared/lib/time.ts @@ -26,6 +26,14 @@ export const formatYYYYMMDD = (dateString: string) => { return onlyDate; }; +export const formatHHMM = (dateString: string) => { + const date = new Date(dateString); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + + return `${hours}:${minutes}`; +}; + export const formatKoreaYMD = (targetDate?: Date) => format(getKoreaDate(targetDate), 'yyyy-MM-dd'); From 660005492bb40a2aaddc003fe33ac659717cf7dd Mon Sep 17 00:00:00 2001 From: aken-you Date: Fri, 10 Oct 2025 01:18:40 +0900 Subject: [PATCH 07/12] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=EC=9D=84=20=EA=B0=80=EC=A7=80=EA=B3=A0=20?= =?UTF-8?q?=EC=9E=88=EC=9D=80=20=EC=82=AC=EB=9E=8C=EC=9D=B4=20=EC=96=B4?= =?UTF-8?q?=EB=93=9C=EB=AF=BC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20?= =?UTF-8?q?=EC=A0=91=EC=86=8D=ED=95=98=EB=A9=B4,=20=ED=99=88=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- middleware.ts | 20 +++++++++++++++++++- src/shared/lib/jwt.ts | 14 ++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 src/shared/lib/jwt.ts diff --git a/middleware.ts b/middleware.ts index 2e47f1f0..8ba30306 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; +import { decodeJwt } from '@/shared/lib/jwt'; import { getServerCookie } from '@/shared/lib/server-cookie'; import { isNumeric } from '@/shared/lib/validation'; import { isApiError } from '@/shared/tanstack-query/api-error'; @@ -105,10 +106,27 @@ export async function middleware(request: NextRequest) { return NextResponse.redirect(mainUrl); } + if (request.nextUrl.pathname.startsWith('/admin')) { + const decodedJwt = decodeJwt(accessToken); + + if (!decodedJwt || !decodedJwt.roleIds.includes('ROLE_ADMIN')) { + const homeUrl = new URL('/', request.url); + + return NextResponse.redirect(homeUrl); + } + } + return response; } // middleware가 적용될 경로 설정 export const config = { - matcher: ['/', '/my-page', '/my-study', '/my-study-review', '/sign-up'], + matcher: [ + '/', + '/my-page', + '/my-study', + '/my-study-review', + '/sign-up', + '/admin/:path*', + ], }; diff --git a/src/shared/lib/jwt.ts b/src/shared/lib/jwt.ts new file mode 100644 index 00000000..491d9be3 --- /dev/null +++ b/src/shared/lib/jwt.ts @@ -0,0 +1,14 @@ +export const decodeJwt = (token: string) => { + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent( + atob(base64) + .split('') + .map(function (c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }) + .join(''), + ); + + return JSON.parse(jsonPayload); +}; From 4f56de90c7f7b730513cd581a7c9c7b72a4a41ed Mon Sep 17 00:00:00 2001 From: aken-you Date: Fri, 10 Oct 2025 01:27:49 +0900 Subject: [PATCH 08/12] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/detail/[id]/profile/page.tsx | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 app/(admin)/admin/detail/[id]/profile/page.tsx diff --git a/app/(admin)/admin/detail/[id]/profile/page.tsx b/app/(admin)/admin/detail/[id]/profile/page.tsx new file mode 100644 index 00000000..c400e549 --- /dev/null +++ b/app/(admin)/admin/detail/[id]/profile/page.tsx @@ -0,0 +1,140 @@ +import { QueryClient } from '@tanstack/react-query'; +import { getUserProfileInServer } from '@/entities/user/api/get-user-profile.server'; +import { GetUserProfileResponse } from '@/entities/user/api/types'; +import ProfileInfoCard from '@/entities/user/ui/profile-info-card'; +import CakeIcon from '@/features/my-page/ui/icon/cake.svg'; +import GithubIcon from '@/features/my-page/ui/icon/github-logo.svg'; +import GlobeIcon from '@/features/my-page/ui/icon/globe-simple.svg'; +import PhoneIcon from '@/features/my-page/ui/icon/phone.svg'; +import { getSincerityPresetByLevelName } from '@/shared/config/sincerity-temp-presets'; +import UserAvatar from '@/shared/ui/avatar'; +import Badge from '@/shared/ui/badge'; + +// todo: UserProfileModal과 거의 유사하여 나중에 리팩토링하기 +export default async function ProfilePage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const queryClient = new QueryClient(); + const { id: memberId } = await params; + + // 서버 side에서 첫 페이지 데이터 미리 가져오기 + await queryClient.prefetchQuery({ + queryKey: ['userProfile', memberId], + queryFn: () => getUserProfileInServer(Number(memberId)), + }); + + const profile: GetUserProfileResponse = await queryClient.getQueryData([ + 'userProfile', + memberId, + ]); + + const temperPreset = getSincerityPresetByLevelName( + profile.sincerityTemp.levelName, + ); + + return ( +
+
+ + +
+
+ {profile.memberProfile.mbti && ( + {profile.memberProfile.mbti} + )} + {profile.memberProfile.interests.slice(0, 4).map((interest) => ( + + {interest.name} + + ))} +
+ +
+
+ {profile.memberProfile.memberName} +
+ +
+ +
+ {profile.memberProfile.simpleIntroduction} +
+ +
+ } + value={profile.memberProfile.birthDate} + /> + } + value={profile.memberProfile.githubLink?.url} + /> + } value={profile.memberProfile.tel} /> + } + value={profile.memberProfile.blogOrSnsLink?.url} + /> +
+
+
+ +
+ + t.techStackName) + .join(', ')} + /> + t.label) + .join(', ')} + /> + + +
+
+ ); +} + +function Field({ icon, value }: { icon: React.ReactNode; value?: string }) { + return ( +
+ {icon} + + {value ?? ''} + +
+ ); +} From 479d43e09c490717c5a9567076d81079b16036c1 Mon Sep 17 00:00:00 2001 From: aken-you Date: Fri, 10 Oct 2025 01:33:08 +0900 Subject: [PATCH 09/12] =?UTF-8?q?feat:=20=EC=84=B1=EC=8B=A4=EC=98=A8?= =?UTF-8?q?=EB=8F=84=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=99=80=20study=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=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 --- app/(admin)/admin/detail/[id]/sincerity-temp/page.tsx | 3 +++ app/(admin)/admin/detail/[id]/study/page.tsx | 3 +++ src/widgets/admin/ui/admin-detail-side-bar.tsx | 6 ++++-- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 app/(admin)/admin/detail/[id]/sincerity-temp/page.tsx create mode 100644 app/(admin)/admin/detail/[id]/study/page.tsx diff --git a/app/(admin)/admin/detail/[id]/sincerity-temp/page.tsx b/app/(admin)/admin/detail/[id]/sincerity-temp/page.tsx new file mode 100644 index 00000000..ea0e590f --- /dev/null +++ b/app/(admin)/admin/detail/[id]/sincerity-temp/page.tsx @@ -0,0 +1,3 @@ +export default function SincerityTempPage() { + return
SincerityTempPage
; +} diff --git a/app/(admin)/admin/detail/[id]/study/page.tsx b/app/(admin)/admin/detail/[id]/study/page.tsx new file mode 100644 index 00000000..9c4cb849 --- /dev/null +++ b/app/(admin)/admin/detail/[id]/study/page.tsx @@ -0,0 +1,3 @@ +export default function StudyPage() { + return
StudyPage
; +} diff --git a/src/widgets/admin/ui/admin-detail-side-bar.tsx b/src/widgets/admin/ui/admin-detail-side-bar.tsx index e00a2949..2052a7dc 100644 --- a/src/widgets/admin/ui/admin-detail-side-bar.tsx +++ b/src/widgets/admin/ui/admin-detail-side-bar.tsx @@ -36,8 +36,10 @@ export default function AdminDetailSideBar({ - - + + 성실온도 From df23861e73ed836eb618fb92a2c2006f394782a1 Mon Sep 17 00:00:00 2001 From: aken-you Date: Fri, 10 Oct 2025 01:51:28 +0900 Subject: [PATCH 10/12] =?UTF-8?q?style:=20RecentLoginHistory=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=A3=BC=EC=84=9D=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(admin)/admin/detail/[id]/account-history/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(admin)/admin/detail/[id]/account-history/page.tsx b/app/(admin)/admin/detail/[id]/account-history/page.tsx index 6ffff85d..d8a923f6 100644 --- a/app/(admin)/admin/detail/[id]/account-history/page.tsx +++ b/app/(admin)/admin/detail/[id]/account-history/page.tsx @@ -68,7 +68,7 @@ export default async function AccountHistoryPage({ - + {/* */} Date: Sat, 11 Oct 2025 00:21:41 +0900 Subject: [PATCH 11/12] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?api=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/admin/ui/logout-button.tsx | 17 ++++++++++ src/widgets/admin/ui/admin-side-bar.tsx | 45 +++++++++++++++++++++---- 2 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 src/features/admin/ui/logout-button.tsx diff --git a/src/features/admin/ui/logout-button.tsx b/src/features/admin/ui/logout-button.tsx new file mode 100644 index 00000000..5d31e82d --- /dev/null +++ b/src/features/admin/ui/logout-button.tsx @@ -0,0 +1,17 @@ +'use client'; + +import { useLogoutMutation } from '@/features/auth/model/use-auth-mutation'; +import LogoutIcon from 'public/icons/logout.svg'; + +export default function LogoutButton() { + const { mutate: logout } = useLogoutMutation(); + + return ( + + ); +} diff --git a/src/widgets/admin/ui/admin-side-bar.tsx b/src/widgets/admin/ui/admin-side-bar.tsx index d8b5282e..84a09c56 100644 --- a/src/widgets/admin/ui/admin-side-bar.tsx +++ b/src/widgets/admin/ui/admin-side-bar.tsx @@ -1,20 +1,51 @@ -import Image from 'next/image'; +import { QueryClient } from '@tanstack/react-query'; import Link from 'next/link'; +import { getUserProfileInServer } from '@/entities/user/api/get-user-profile.server'; +import { GetUserProfileResponse } from '@/entities/user/api/types'; +import LogoutButton from '@/features/admin/ui/logout-button'; +import { getServerCookie } from '@/shared/lib/server-cookie'; +import UserAvatar from '@/shared/ui/avatar'; import TabMenu from '@/shared/ui/tab-menu'; -import LogoutIcon from 'public/icons/logout.svg'; -export default function AdminSideBar() { +export default async function AdminSideBar() { + const queryClient = new QueryClient(); + + const memberIdStr = await getServerCookie('memberId'); + const memberId = Number(memberIdStr); + + if (!memberId) { + return null; + } + + // 서버 side에서 첫 페이지 데이터 미리 가져오기 + await queryClient.prefetchQuery({ + queryKey: ['userProfile', memberId], + queryFn: () => getUserProfileInServer(memberId), + }); + + const profile: GetUserProfileResponse = await queryClient.getQueryData([ + 'userProfile', + memberId, + ]); + return (