d3ro-voice/docs/v2/phase-V2-6.md
윤찬 f7c50eb2ed docs: Phase V2-6 Admin CRM 고도화 설계서
Admin API(Edge Function) + Swagger UI + CRUD 웹페이지 +
recharts 차트 + 결제 이력 + 감사로그 설계.
2026-04-12 21:18:19 +09:00

13 KiB

Phase V2-6: Admin CRM 고도화 — CRUD + 차트 + 결제 + 감사

Admin CRM(apps/admin)을 운영 도구 수준으로 격상. super_admin/admin 2-tier 권한, 구독 전체 CRUD, 사용량 차트, 결제 이력, 감사로그.


0. 배경

  • Phase 3.3에서 apps/admin 독립 Next.js 앱 + 4개 조회 페이지 완성
  • 현재는 읽기 전용 — 구독/role 변경은 SQL 직접 실행 필요
  • 소수 팀(2~5명) 운영 → super_admin만 위험 조작 가능하게

1. 권한 체계

1.1 Role 확장

role 조회 구독 수정 role 변경 구독 삭제
user X (접근 불가) X X X
admin O (전체) X X X
super_admin O (전체) O O O
  • auth.users.raw_app_meta_data.role: 'user' | 'admin' | 'super_admin'
  • profiles.role CHECK도 동일 확장
  • 기존 RLS: app_metadata.role = 'admin'IN ('admin', 'super_admin') 변경

1.2 Admin Guard 수정

// apps/admin/src/lib/admin-guard.ts
type AdminRole = 'admin' | 'super_admin'

function getAdminRole(user): AdminRole | null
function requireAdmin(user): void         // admin 이상
function requireSuperAdmin(user): void    // super_admin만

2. DB 마이그레이션

2.1 audit_log 테이블

CREATE TABLE public.audit_log (
  id          bigserial PRIMARY KEY,
  admin_id    uuid NOT NULL REFERENCES auth.users(id),
  action      text NOT NULL,        -- 'subscription.update', 'subscription.create', 'subscription.delete', 'user.role_change'
  target_type text NOT NULL,        -- 'subscription', 'profile'
  target_id   uuid NOT NULL,        -- 대상 레코드의 user_id 또는 subscription_id
  before_data jsonb,                -- 변경 전 스냅샷
  after_data  jsonb,                -- 변경 후 스냅샷
  memo        text NOT NULL,        -- 관리자 메모 (필수)
  created_at  timestamptz DEFAULT now()
);

CREATE INDEX idx_audit_log_target ON public.audit_log(target_type, target_id);
CREATE INDEX idx_audit_log_admin  ON public.audit_log(admin_id);
CREATE INDEX idx_audit_log_date   ON public.audit_log(created_at DESC);

RLS: admin 이상 SELECT, super_admin만 INSERT (Edge Function은 service_role).

2.2 subscriptions.admin_note

ALTER TABLE public.subscriptions
  ADD COLUMN IF NOT EXISTS admin_note text;

2.3 profiles.role CHECK 확장

-- 기존: CHECK (role IN ('user', 'admin'))
-- 변경: CHECK (role IN ('user', 'admin', 'super_admin'))
ALTER TABLE public.profiles DROP CONSTRAINT IF EXISTS profiles_role_check;
ALTER TABLE public.profiles
  ADD CONSTRAINT profiles_role_check
  CHECK (role IN ('user', 'admin', 'super_admin'));

2.4 기존 RLS 정책 업데이트

-- admin → admin 또는 super_admin
DROP POLICY IF EXISTS "admin_read_all_profiles" ON public.profiles;
CREATE POLICY "admin_read_all_profiles" ON public.profiles
  FOR SELECT TO authenticated
  USING ((auth.jwt() -> 'app_metadata' ->> 'role') IN ('admin', 'super_admin'));

-- subscriptions, daily_usage 동일 패턴
-- + subscriptions에 UPDATE/INSERT/DELETE 정책 추가 (super_admin만)

CREATE POLICY "super_admin_write_subscriptions" ON public.subscriptions
  FOR ALL TO authenticated
  USING ((auth.jwt() -> 'app_metadata' ->> 'role') = 'super_admin')
  WITH CHECK ((auth.jwt() -> 'app_metadata' ->> 'role') = 'super_admin');

3. Admin API Edge Functions

4개 Edge Function, 모두 verify_jwt = true, 내부에서 admin/super_admin 검증.

3.1 admin-users

Method Action 권한
GET 유저 목록/상세 (기존 Supabase 직접 쿼리 대체) admin+
PATCH role 변경 ({ userId, newRole, memo }) super_admin

role 변경 시:

  1. auth.admin.updateUserById()raw_app_meta_data.role 변경 (service_role 필요)
  2. profiles.role 동기화
  3. audit_log INSERT (before/after diff)

3.2 admin-subscriptions

Method Action 권한
GET 구독 목록/상세 admin+
POST 구독 생성 (VIP 부여 / 레코드 복구) super_admin
PATCH 구독 수정 (tier/기간/상태/admin_note/overage_credits) super_admin
DELETE 구독 삭제 (soft: status='deleted') super_admin

모든 쓰기 작업에 memo 필수 → audit_log 자동 기록.

요청 바디 (PATCH 예시):

interface UpdateSubscriptionBody {
  tier?: 'free' | 'pro' | 'pro_plus'
  status?: 'active' | 'canceled' | 'past_due' | 'expired'
  current_period_end?: string    // ISO 8601
  overage_credits?: number
  admin_note?: string
  memo: string                   // 감사로그 메모 (필수)
}

3.3 admin-payments

Method Action 권한
GET ?userId=... 특정 유저 결제 이력 admin+
GET ?userId=...&source=payple Payple API 직접 조회 admin+

DB 조회: subscriptions 테이블 + audit_log (결제 관련 action 필터) Payple 조회: _shared/payple.tspaypleAuth() → 결제 내역 API 호출

3.4 admin-audit-log

Method Action 권한
GET 감사로그 목록 (필터: target_type, admin_id, 날짜 범위) admin+
GET ?id=... 단건 상세 (before/after diff) admin+

페이지네이션: ?page=1&limit=20&target_type=subscription&from=2026-04-01&to=2026-04-12

3.5 공통 패턴

// _shared/admin-auth.ts (신규)
import { requireUser } from './auth.ts'

type AdminLevel = 'admin' | 'super_admin'

export async function requireAdmin(req: Request): Promise<User> {
  const user = await requireUser(req)
  const role = user.app_metadata?.role
  if (role !== 'admin' && role !== 'super_admin') {
    throw { status: 403, message: 'Admin access required' }
  }
  return user
}

export async function requireSuperAdmin(req: Request): Promise<User> {
  const user = await requireUser(req)
  if (user.app_metadata?.role !== 'super_admin') {
    throw { status: 403, message: 'Super admin access required' }
  }
  return user
}
// _shared/audit.ts (신규)
export async function writeAuditLog(
  supabase: SupabaseClient,
  params: {
    adminId: string
    action: string
    targetType: string
    targetId: string
    beforeData: Record<string, unknown> | null
    afterData: Record<string, unknown> | null
    memo: string
  }
): Promise<void>

3.6 config.toml 추가

[functions.admin-users]
verify_jwt = true

[functions.admin-subscriptions]
verify_jwt = true

[functions.admin-payments]
verify_jwt = true

[functions.admin-audit-log]
verify_jwt = true

4. Swagger UI

4.1 OpenAPI 3.0 Spec

server/supabase/functions/_shared/openapi.json — 4개 Edge Function의 전체 API 명세.

4.2 독립 정적 페이지

apps/admin-swagger/
├── index.html          # swagger-ui-dist CDN + openapi.json 로드
├── openapi.json        # OpenAPI 3.0 spec (빌드 시 _shared에서 복사)
└── README.md
  • swagger-ui-dist CDN에서 로드 (설치 불필요)
  • Bearer token 입력으로 admin 인증
  • 로컬: npx serve apps/admin-swagger 또는 admin 앱에서 링크

5. Admin CRUD 웹페이지 (apps/admin)

5.1 새 페이지/컴포넌트

경로 용도
(admin)/subscriptions/[id]/page.tsx 구독 상세 + 수정/삭제 폼
(admin)/subscriptions/new/page.tsx 구독 생성 (VIP 부여)
(admin)/audit-log/page.tsx 감사로그 목록
(admin)/audit-log/[id]/page.tsx 감사로그 상세 (diff 뷰)
components/subscription-form.tsx 구독 생성/수정 공용 폼
components/role-change-dialog.tsx role 변경 확인 다이얼로그
components/audit-diff-viewer.tsx before/after JSON diff 뷰어
components/memo-dialog.tsx 메모 입력 다이얼로그 (필수)

5.2 기존 페이지 수정

페이지 변경
users/[id]/page.tsx role 변경 버튼 추가 (super_admin만 표시)
subscriptions/page.tsx 각 행에 편집/삭제 버튼 + "새 구독" 버튼 추가
admin-sidebar.tsx Audit Log 메뉴 추가, Swagger UI 외부 링크

5.3 super_admin 가드 UI

// 현재 유저의 role을 체크해서 위험 버튼 표시/숨김
const isSuperAdmin = user.app_metadata?.role === 'super_admin'

// 버튼 예시
{isSuperAdmin && <Button onClick={handleDelete}>구독 삭제</Button>}

5.4 데이터 페칭 패턴

기존: Supabase SDK 직접 쿼리 (RSC) 신규 쓰기 작업: Edge Function 호출 (클라이언트 컴포넌트)

// Edge Function 호출 헬퍼
async function callAdminApi(path: string, options: RequestInit) {
  const supabase = getSupabaseBrowserClient()
  const { data: { session } } = await supabase.auth.getSession()
  return fetch(`${SUPABASE_URL}/functions/v1/${path}`, {
    ...options,
    headers: {
      Authorization: `Bearer ${session?.access_token}`,
      'Content-Type': 'application/json',
      ...options.headers,
    }
  })
}

6. 사용량 차트 (recharts)

6.1 설치

npm install --workspace=@d3ro/admin recharts

6.2 차트 컴포넌트

컴포넌트 차트 유형 데이터
FeatureUsageChart StackedBar / Line feature별 일간 API 호출 (7/14/30일)
UserActivityChart Line DAU/WAU/MAU 추이
TopUsersChart HorizontalBar 유저별 사용량 Top 20
RetentionChart Heatmap / Line 주간/월간 리텐션율

6.3 데이터 소스

기존 daily_usage 테이블 + SQL 집계 RPC 추가:

-- 일별 feature 집계
CREATE OR REPLACE FUNCTION public.admin_usage_by_feature(
  p_from date, p_to date
) RETURNS TABLE(date date, feature text, total_count bigint, unique_users bigint)
LANGUAGE sql SECURITY DEFINER STABLE
AS $$
  SELECT date, feature,
         SUM(count)::bigint AS total_count,
         COUNT(DISTINCT user_id)::bigint AS unique_users
    FROM public.daily_usage
   WHERE date BETWEEN p_from AND p_to
   GROUP BY date, feature
   ORDER BY date, feature;
$$;

-- 유저별 사용량 랭킹
CREATE OR REPLACE FUNCTION public.admin_top_users(
  p_from date, p_to date, p_limit integer DEFAULT 20
) RETURNS TABLE(user_id uuid, name text, total_count bigint, feature_count bigint)
LANGUAGE sql SECURITY DEFINER STABLE
AS $$
  SELECT du.user_id, p.name,
         SUM(du.count)::bigint AS total_count,
         COUNT(DISTINCT du.feature)::bigint AS feature_count
    FROM public.daily_usage du
    JOIN public.profiles p ON p.id = du.user_id
   WHERE du.date BETWEEN p_from AND p_to
   GROUP BY du.user_id, p.name
   ORDER BY total_count DESC
   LIMIT p_limit;
$$;

-- DAU 추이
CREATE OR REPLACE FUNCTION public.admin_dau(
  p_from date, p_to date
) RETURNS TABLE(date date, active_users bigint)
LANGUAGE sql SECURITY DEFINER STABLE
AS $$
  SELECT date, COUNT(DISTINCT user_id)::bigint AS active_users
    FROM public.daily_usage
   WHERE date BETWEEN p_from AND p_to
   GROUP BY date
   ORDER BY date;
$$;

RPC 권한: admin 이상만 호출 가능 (RLS + role 체크).


7. 결제 이력

7.1 DB 조회

subscriptions 테이블 (payment_provider, payple_payer_id, payple_pay_oid, status, tier 변경 이력)

  • audit_log 필터 (action LIKE 'subscription.%')

7.2 Payple API 직접 조회

admin-payments Edge Function에서 _shared/payple.ts 활용:

  • paypleAuth() → 인증 토큰 발급
  • Payple 결제 내역 조회 API 호출 (PCD_PAYER_ID 기반)

7.3 UI

(admin)/users/[id]/page.tsx에 결제 이력 탭 추가:

  • 구독 변경 타임라인 (audit_log 기반)
  • Payple 결제 내역 (API 조회 결과)

8. 구현 순서

Step 내용 파일
8.1 DB 마이그레이션 server/supabase/migrations/20260413000004_admin_enhancement.sql
8.2 공유 유틸 _shared/admin-auth.ts, _shared/audit.ts
8.3 Edge Functions 4개 admin-users/, admin-subscriptions/, admin-payments/, admin-audit-log/
8.4 OpenAPI spec + Swagger UI _shared/openapi.json, apps/admin-swagger/
8.5 Admin 웹 CRUD 페이지 apps/admin/src/ 수정/추가
8.6 recharts 차트 apps/admin/src/components/charts/
8.7 결제 이력 UI users/[id] 탭 확장
8.8 RLS 정책 + config.toml 마이그레이션 + 서버 설정

9. 검증 체크리스트

  • apps/admin typecheck 통과
  • Edge Functions 4개 배포 + 응답 확인
  • Swagger UI에서 전체 API 테스트 가능
  • admin: 조회만 가능, 쓰기 버튼 숨김
  • super_admin: CRUD 전체 동작
  • 구독 수정/삭제 시 audit_log 기록 확인
  • before/after diff 정확성
  • 메모 미입력 시 제출 불가
  • recharts 차트 4종 렌더링
  • 기간 필터(7/14/30일) 동작
  • Payple 결제 이력 조회 동작