From 3d81e8779a443a72ad3a7301ac41b36040e0b6fe Mon Sep 17 00:00:00 2001 From: aken-you Date: Tue, 30 Sep 2025 00:48:54 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20logout=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EC=BD=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/icons/logout.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 public/icons/logout.svg 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 @@ + + + From ac6abe2273f8e92792fc25015559d5abe5afa7f6 Mon Sep 17 00:00:00 2001 From: aken-you Date: Fri, 3 Oct 2025 00:17:39 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20admin=EA=B3=BC=20service=20?= =?UTF-8?q?=EA=B7=B8=EB=A3=B9=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(admin)/admin/page.tsx | 93 +++++++++++++++++++ app/(admin)/layout.tsx | 38 ++++++++ app/{ => (service)}/(my)/layout.tsx | 0 app/{ => (service)}/(my)/my-page/page.tsx | 0 .../(my)/my-study-review/page.tsx | 0 app/{ => (service)}/(my)/my-study/page.tsx | 0 app/{ => (service)}/layout.tsx | 8 +- app/{ => (service)}/login/page.tsx | 0 app/{ => (service)}/page.tsx | 0 app/{ => (service)}/redirection/page.tsx | 0 app/{ => (service)}/sign-up/page.tsx | 0 11 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 app/(admin)/admin/page.tsx create mode 100644 app/(admin)/layout.tsx rename app/{ => (service)}/(my)/layout.tsx (100%) rename app/{ => (service)}/(my)/my-page/page.tsx (100%) rename app/{ => (service)}/(my)/my-study-review/page.tsx (100%) rename app/{ => (service)}/(my)/my-study/page.tsx (100%) rename app/{ => (service)}/layout.tsx (91%) rename app/{ => (service)}/login/page.tsx (100%) rename app/{ => (service)}/page.tsx (100%) rename app/{ => (service)}/redirection/page.tsx (100%) rename app/{ => (service)}/sign-up/page.tsx (100%) diff --git a/app/(admin)/admin/page.tsx b/app/(admin)/admin/page.tsx new file mode 100644 index 00000000..5e2437cc --- /dev/null +++ b/app/(admin)/admin/page.tsx @@ -0,0 +1,93 @@ +'use client'; + +import Image from 'next/image'; +import Link from 'next/link'; +import { SingleDropdown } from '@/shared/ui/dropdown'; + +export default function AdminPage() { + return ( +
+ + +
+
+
+

사용자 관리

+ + 50 + + 명의 사용자 + +
+
+ +
+
+ + +
+
+
+
+ ); +} 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 From 5d4d4469c7a47043d3f31a0db57c28ad2a743ebc Mon Sep 17 00:00:00 2001 From: aken-you Date: Sat, 4 Oct 2025 00:40:58 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=9D=98=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(admin)/admin/page.tsx | 151 ++++++++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 1 deletion(-) diff --git a/app/(admin)/admin/page.tsx b/app/(admin)/admin/page.tsx index 5e2437cc..bce2d42a 100644 --- a/app/(admin)/admin/page.tsx +++ b/app/(admin)/admin/page.tsx @@ -2,11 +2,14 @@ import Image from 'next/image'; import Link from 'next/link'; +import { useEffect, useRef, useState } from 'react'; +import Badge from '@/shared/ui/badge'; +import Checkbox from '@/shared/ui/checkbox'; import { SingleDropdown } from '@/shared/ui/dropdown'; export default function AdminPage() { return ( -
+
+ +
+ +
); } + +// 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} + + {user.joinedAt} + + {user.loginMostRecentlyAt} + + {user.role.roleId === 'ROLE_ADMIN' ? '멘토' : '일반'} + + {user.memberStatus === 'ACTIVE' && ( + + 활성 + + )} + {user.memberStatus === 'PAUSED' && ( + + 영구정지 + + )} + {user.memberStatus === 'PERM_BAN' && ( + + 일시정지 + + )} + {user.memberStatus === 'DORMANT' && ( + + 휴면 + + )} +
+
+ ); +} From aa1cb88a070d61542616880ed0d2df77ab677a4d Mon Sep 17 00:00:00 2001 From: aken-you Date: Sat, 4 Oct 2025 00:47:54 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=ED=85=8C=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=9D=98=20=EB=82=A0=EC=A7=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=A5=BC=20yyyy-mm-dd=EB=A1=9C=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(admin)/admin/page.tsx | 5 +++-- src/shared/lib/time.ts | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/(admin)/admin/page.tsx b/app/(admin)/admin/page.tsx index bce2d42a..dc147daf 100644 --- a/app/(admin)/admin/page.tsx +++ b/app/(admin)/admin/page.tsx @@ -3,6 +3,7 @@ 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'; @@ -203,10 +204,10 @@ function UserTable() { {user.memberName} - {user.joinedAt} + {formatYYYYMMDD(user.joinedAt)} - {user.loginMostRecentlyAt} + {formatYYYYMMDD(user.loginMostRecentlyAt)} {user.role.roleId === 'ROLE_ADMIN' ? '멘토' : '일반'} 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');