diff --git a/app/(admin)/admin/page.tsx b/app/(admin)/admin/page.tsx new file mode 100644 index 00000000..dc147daf --- /dev/null +++ b/app/(admin)/admin/page.tsx @@ -0,0 +1,243 @@ +'use client'; + +import Image from 'next/image'; +import Link from 'next/link'; +import { useEffect, useRef, useState } from 'react'; +import { formatYYYYMMDD } from '@/shared/lib/time'; +import Badge from '@/shared/ui/badge'; +import Checkbox from '@/shared/ui/checkbox'; +import { SingleDropdown } from '@/shared/ui/dropdown'; + +export default function AdminPage() { + return ( +
+ + +
+
+
+

사용자 관리

+ + 50 + + 명의 사용자 + +
+
+ +
+
+ + +
+
+ +
+ +
+
+
+ ); +} + +// todo: API 연결 +const users = [ + { + memberId: 1, + memberStatus: 'ACTIVE', + memberName: '안유진', + joinedAt: '2025-09-23T16:42:01.155Z', + loginMostRecentlyAt: '2025-09-23T16:42:01.155Z', + role: { + roleId: 'ROLE_MEMBER', + roleName: '일반', + }, + }, + { + memberId: 2, + memberStatus: 'ACTIVE', + memberName: '이현서', + joinedAt: '2025-09-23T16:42:01.155Z', + loginMostRecentlyAt: '2025-09-23T16:42:01.155Z', + role: { + roleId: 'ROLE_ADMIN', + roleName: '관리자', + }, + }, +]; + +function UserTable() { + const [selectedIds, setSelectedIds] = useState(() => new Set()); + const headerCheckboxRef = useRef(null); + + const allSelected = users.length > 0 && selectedIds.size === users.length; // 모든 행을 선택했는지 + const someSelected = selectedIds.size > 0 && selectedIds.size < users.length; // 한개 이상 행을 선택했는지 + + useEffect(() => { + if (headerCheckboxRef.current) { + headerCheckboxRef.current.indeterminate = someSelected; + } + }, [someSelected]); + + const toggleAll = () => { + if (allSelected) { + setSelectedIds(new Set()); + } else { + setSelectedIds(new Set(users.map((u) => u.memberId))); + } + }; + + const toggleRow = (id: number) => { + setSelectedIds((prev) => { + const next = new Set(prev); + + if (next.has(id)) next.delete(id); + else next.add(id); + + return next; + }); + }; + + return ( +
+ + + + + + + + + + + + + {users.map((user, idx) => ( + + + + + + + + + ))} + +
+ + + 이름 + + 가입일 + + 최근 로그인 + + 권한 + + 상태 +
+ toggleRow(user.memberId)} + checked={selectedIds.has(user.memberId)} + /> + + {user.memberName} + + {formatYYYYMMDD(user.joinedAt)} + + {formatYYYYMMDD(user.loginMostRecentlyAt)} + + {user.role.roleId === 'ROLE_ADMIN' ? '멘토' : '일반'} + + {user.memberStatus === 'ACTIVE' && ( + + 활성 + + )} + {user.memberStatus === 'PAUSED' && ( + + 영구정지 + + )} + {user.memberStatus === 'PERM_BAN' && ( + + 일시정지 + + )} + {user.memberStatus === 'DORMANT' && ( + + 휴면 + + )} +
+
+ ); +} diff --git a/app/(admin)/layout.tsx b/app/(admin)/layout.tsx new file mode 100644 index 00000000..0c9f2213 --- /dev/null +++ b/app/(admin)/layout.tsx @@ -0,0 +1,38 @@ +import '../global.css'; + +import { GoogleTagManager } from '@next/third-parties/google'; +import { clsx } from 'clsx'; +import type { Metadata } from 'next'; +import localFont from 'next/font/local'; +import MainProvider from '@/app/provider'; + +export const metadata: Metadata = { + title: 'ZERO-ONE', + description: '매일 아침을 함께 시작하는 1:1 기상 스터디 플랫폼, ZERO-ONE', + icons: { + icon: '/favicon.ico', + }, +}; + +const pretendard = localFont({ + src: '../../public/fonts/PretendardVariable.woff2', + variable: '--font-pretendard', + display: 'swap', +}); + +const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID; + +export default function AdminLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {GTM_ID && } + + {children} + + + ); +} diff --git a/app/(my)/layout.tsx b/app/(service)/(my)/layout.tsx similarity index 100% rename from app/(my)/layout.tsx rename to app/(service)/(my)/layout.tsx diff --git a/app/(my)/my-page/page.tsx b/app/(service)/(my)/my-page/page.tsx similarity index 100% rename from app/(my)/my-page/page.tsx rename to app/(service)/(my)/my-page/page.tsx diff --git a/app/(my)/my-study-review/page.tsx b/app/(service)/(my)/my-study-review/page.tsx similarity index 100% rename from app/(my)/my-study-review/page.tsx rename to app/(service)/(my)/my-study-review/page.tsx diff --git a/app/(my)/my-study/page.tsx b/app/(service)/(my)/my-study/page.tsx similarity index 100% rename from app/(my)/my-study/page.tsx rename to app/(service)/(my)/my-study/page.tsx diff --git a/app/layout.tsx b/app/(service)/layout.tsx similarity index 91% rename from app/layout.tsx rename to app/(service)/layout.tsx index 7fda55f8..8be25193 100644 --- a/app/layout.tsx +++ b/app/(service)/layout.tsx @@ -1,11 +1,11 @@ -import './global.css'; +import '../global.css'; import { GoogleTagManager } from '@next/third-parties/google'; +import { clsx } from 'clsx'; import type { Metadata } from 'next'; import localFont from 'next/font/local'; import MainProvider from '@/app/provider'; import Header from '@/widgets/home/header'; -import { clsx } from 'clsx'; export const metadata: Metadata = { title: 'ZERO-ONE', @@ -16,14 +16,14 @@ export const metadata: Metadata = { }; const pretendard = localFont({ - src: '../public/fonts/PretendardVariable.woff2', + src: '../../public/fonts/PretendardVariable.woff2', variable: '--font-pretendard', display: 'swap', }); const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID; -export default function RootLayout({ +export default function ServiceLayout({ children, }: Readonly<{ children: React.ReactNode; diff --git a/app/login/page.tsx b/app/(service)/login/page.tsx similarity index 100% rename from app/login/page.tsx rename to app/(service)/login/page.tsx diff --git a/app/page.tsx b/app/(service)/page.tsx similarity index 100% rename from app/page.tsx rename to app/(service)/page.tsx diff --git a/app/redirection/page.tsx b/app/(service)/redirection/page.tsx similarity index 100% rename from app/redirection/page.tsx rename to app/(service)/redirection/page.tsx diff --git a/app/sign-up/page.tsx b/app/(service)/sign-up/page.tsx similarity index 100% rename from app/sign-up/page.tsx rename to app/(service)/sign-up/page.tsx diff --git a/public/icons/logout.svg b/public/icons/logout.svg new file mode 100644 index 00000000..48f3e4d6 --- /dev/null +++ b/public/icons/logout.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/lib/time.ts b/src/shared/lib/time.ts index 9ef0d565..567c1d9e 100644 --- a/src/shared/lib/time.ts +++ b/src/shared/lib/time.ts @@ -20,6 +20,12 @@ export const getKoreaDate = (targetDate?: Date) => { return koreaNow; }; +export const formatYYYYMMDD = (dateString: string) => { + const onlyDate = new Date(dateString).toISOString().slice(0, 10); + + return onlyDate; +}; + export const formatKoreaYMD = (targetDate?: Date) => format(getKoreaDate(targetDate), 'yyyy-MM-dd');