From 5f849c552e6aa239c277d6bbcab7c9d1112b5a42 Mon Sep 17 00:00:00 2001 From: ByungCheol Date: Sat, 11 Jul 2026 11:38:54 +0900 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20+=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 대시보드(/admin/dashboard, 기본 랜딩): 요약 카드(총회원·금일 신규/탈퇴·DAU/WAU/MAU), 접속자 추이(일/주/월 토글), 티어별 회원수, 시군구 지역 분포 — 모두 체크인 기반 - 회원 상세(/admin/members/:id): 프로필 + 강아지/체크인/매칭/구독 이력, 목록에서 상세 버튼 - adminStats API 모듈, 좌측 메뉴에 대시보드 추가 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/api/adminMembers.ts | 39 ++++ src/api/adminStats.ts | 32 +++ src/layouts/AdminLayout.vue | 5 +- src/pages/admin/AdminDashboardPage.vue | 253 ++++++++++++++++++++++ src/pages/admin/AdminLoginPage.vue | 2 +- src/pages/admin/AdminMemberDetailPage.vue | 203 +++++++++++++++++ src/pages/admin/AdminMembersPage.vue | 14 +- src/router/index.ts | 14 +- 8 files changed, 557 insertions(+), 5 deletions(-) create mode 100644 src/api/adminStats.ts create mode 100644 src/pages/admin/AdminDashboardPage.vue create mode 100644 src/pages/admin/AdminMemberDetailPage.vue diff --git a/src/api/adminMembers.ts b/src/api/adminMembers.ts index 4dce6ff..9a57f93 100644 --- a/src/api/adminMembers.ts +++ b/src/api/adminMembers.ts @@ -5,10 +5,49 @@ export type MemberStatus = 'ACTIVE' | 'DORMANT' | 'BANNED' export type MemberTier = 'FREE' | 'AD_FREE' | 'PREMIUM' export type MemberRole = 'USER' | 'ADMIN' +export interface DogDetail { + id: number + name: string + breed: string | null + size: string | null + gender: string | null + neutered: boolean + birthDate: string | null + imageUrl: string | null +} +export interface CheckInDetail { + spotName: string + region: string | null + checkedInAt: string +} +export interface MatchDetail { + id: number + direction: string + counterpartDog: string + status: string + createdAt: string +} +export interface SubscriptionDetail { + tier: string + platform: string + startedAt: string + expiresAt: string + active: boolean +} +export interface MemberDetail { + member: Member + dogs: DogDetail[] + checkIns: CheckInDetail[] + matches: MatchDetail[] + subscriptions: SubscriptionDetail[] +} + /** 관리자 회원관리 API — /api/admin/members (ADMIN 전용) */ export const adminMembersApi = { list: () => http.get('/api/admin/members'), + detail: (id: number) => http.get(`/api/admin/members/${id}`), + updateStatus: (id: number, status: MemberStatus) => http.put(`/api/admin/members/${id}/status`, { status }), diff --git a/src/api/adminStats.ts b/src/api/adminStats.ts new file mode 100644 index 0000000..da43b01 --- /dev/null +++ b/src/api/adminStats.ts @@ -0,0 +1,32 @@ +import { http } from './http' + +export interface Count { + key: string + count: number +} + +export interface StatsSummary { + members: { total: number; byTier: Count[]; byStatus: Count[] } + today: { signups: number; withdrawals: number } + visitors: { dau: number; wau: number; mau: number } +} + +export interface VisitorPoint { + label: string + count: number +} + +export interface RegionStat { + region: string + checkIns: number + users: number +} + +export type VisitorUnit = 'day' | 'week' | 'month' + +/** 관리자 대시보드 통계 API (체크인 기반) */ +export const adminStatsApi = { + summary: () => http.get('/api/admin/stats/summary'), + visitors: (unit: VisitorUnit) => http.get(`/api/admin/stats/visitors?unit=${unit}`), + regions: () => http.get('/api/admin/stats/regions'), +} diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue index 89a1913..c30072e 100644 --- a/src/layouts/AdminLayout.vue +++ b/src/layouts/AdminLayout.vue @@ -58,7 +58,10 @@ const admin = useAdminAuthStore() const drawer = ref(true) -const menu = [{ name: 'admin-members', label: '회원 관리', icon: 'group' }] +const menu = [ + { name: 'admin-dashboard', label: '대시보드', icon: 'dashboard' }, + { name: 'admin-members', label: '회원 관리', icon: 'group' }, +] async function onLogout() { await admin.logout() diff --git a/src/pages/admin/AdminDashboardPage.vue b/src/pages/admin/AdminDashboardPage.vue new file mode 100644 index 0000000..10164fe --- /dev/null +++ b/src/pages/admin/AdminDashboardPage.vue @@ -0,0 +1,253 @@ + + + + + diff --git a/src/pages/admin/AdminLoginPage.vue b/src/pages/admin/AdminLoginPage.vue index a42f068..d19a833 100644 --- a/src/pages/admin/AdminLoginPage.vue +++ b/src/pages/admin/AdminLoginPage.vue @@ -126,7 +126,7 @@ async function finishLogin(login: () => Promise<{ role: string }>) { error.value = '관리자 권한이 없는 계정입니다.' return } - await router.replace({ name: 'admin-members' }) + await router.replace({ name: 'admin-dashboard' }) } catch (e) { error.value = e instanceof ApiError ? e.message : '로그인 중 오류가 발생했습니다.' } finally { diff --git a/src/pages/admin/AdminMemberDetailPage.vue b/src/pages/admin/AdminMemberDetailPage.vue new file mode 100644 index 0000000..04eff28 --- /dev/null +++ b/src/pages/admin/AdminMemberDetailPage.vue @@ -0,0 +1,203 @@ + + + diff --git a/src/pages/admin/AdminMembersPage.vue b/src/pages/admin/AdminMembersPage.vue index a451f3e..8c5ceda 100644 --- a/src/pages/admin/AdminMembersPage.vue +++ b/src/pages/admin/AdminMembersPage.vue @@ -107,9 +107,15 @@ - +