Files
2nd/10_Wiki/Topics/AI_and_ML/데이터 중심의 SaaS 어드민 패널 및 CRM 대시보드 구축.md
T
2026-05-10 22:08:15 +09:00

8.8 KiB

id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, tags, raw_sources, last_reinforced, github_commit, tech_stack
id title category status canonical_id aliases duplicate_of source_trust_level confidence_score verification_status tags raw_sources last_reinforced github_commit tech_stack
wiki-2026-0508-데이터-중심의-saas-어드민-패널-및-crm-대시보드-구 데이터 중심의 SaaS 어드민 패널 및 CRM 대시보드 구축 10_Wiki/Topics verified self
SaaS Admin Panel
CRM Dashboard
Internal Tools
none A 0.9 applied
saas
admin-panel
crm
dashboard
internal-tools
data-driven
2026-05-10 pending
language framework
TypeScript Next.js 15 / Remix / Refine / Retool

데이터 중심의 SaaS 어드민 패널 및 CRM 대시보드 구축

매 한 줄

"매 CRUD 의 X, 매 actionable insight + 매 fast workflow". 매 SaaS admin panel / CRM dashboard 의 modern build — 매 server components, 매 typed RPC (tRPC / Hono RPC), 매 real-time (SSE / WebSocket), 매 RBAC, 매 audit log. 매 2026 의 standard: Next.js 15 + Refine + TanStack Table + shadcn/ui, 매 Retool / Internal 의 low-code alternative.

매 핵심

매 architecture layers

  • Data layer: 매 Postgres + Drizzle/Prisma, 매 read-replica 의 분리.
  • API layer: 매 tRPC / Hono RPC — 매 type-safe end-to-end.
  • UI layer: 매 Server Component 의 default, 매 Client 의 interactive only.
  • Auth layer: 매 Better-Auth / Clerk / Auth.js — 매 RBAC + org/team.

매 essential features

  • Resource CRUD: 매 list / detail / create / edit — 매 generated, 매 customizable.
  • Bulk operations: 매 multi-select + action — 매 1000+ rows 의 handle.
  • Filters & search: 매 server-side, 매 URL state 의 sync.
  • Audit log: 매 who / when / what — 매 immutable.
  • Impersonation: 매 support 의 ticket investigation.
  • Export: 매 CSV / Parquet 의 streaming.

매 응용

  1. 매 SaaS user management — 매 user search, 매 plan upgrade, 매 quota override.
  2. 매 CRM contact / deal pipeline — 매 Kanban + Table dual view.
  3. 매 financial ops — 매 refund, 매 invoice 의 manual issue.
  4. 매 content moderation — 매 report queue 의 batch review.

💻 패턴

tRPC procedure with RBAC (Next.js 15)

import { z } from "zod";
import { adminProcedure, router } from "../trpc";

export const userRouter = router({
  list: adminProcedure
    .input(z.object({
      cursor: z.string().nullish(),
      filter: z.object({ email: z.string().optional() }).default({}),
      limit: z.number().min(1).max(100).default(50),
    }))
    .query(async ({ ctx, input }) => {
      ctx.requirePermission("user:read");
      const rows = await ctx.db.user.findMany({
        where: { email: { contains: input.filter.email } },
        take: input.limit + 1,
        cursor: input.cursor ? { id: input.cursor } : undefined,
        orderBy: { createdAt: "desc" },
      });
      const next = rows.length > input.limit ? rows.pop()!.id : null;
      return { rows, nextCursor: next };
    }),

  upgradePlan: adminProcedure
    .input(z.object({ userId: z.string(), plan: z.enum(["free","pro","ent"]) }))
    .mutation(async ({ ctx, input }) => {
      ctx.requirePermission("user:write");
      await ctx.db.$transaction([
        ctx.db.user.update({ where: { id: input.userId }, data: { plan: input.plan } }),
        ctx.db.auditLog.create({
          data: {
            actorId: ctx.session.userId,
            action: "user.upgradePlan",
            target: input.userId,
            payload: input,
          }
        }),
      ]);
    }),
});

TanStack Table with server-side filters

"use client";
import { useReactTable, getCoreRowModel, flexRender } from "@tanstack/react-table";
import { useQueryState, parseAsString } from "nuqs";

export function UserTable() {
  const [email, setEmail] = useQueryState("email", parseAsString.withDefault(""));
  const { data } = trpc.user.list.useQuery({ filter: { email } });

  const table = useReactTable({
    data: data?.rows ?? [],
    columns: [
      { accessorKey: "email", header: "Email" },
      { accessorKey: "plan", header: "Plan" },
      { accessorKey: "createdAt", header: "Created", cell: c => new Date(c.getValue() as string).toLocaleDateString() },
    ],
    getCoreRowModel: getCoreRowModel(),
  });

  return (
    <div>
      <input value={email} onChange={e => setEmail(e.target.value)} placeholder="매 email filter" />
      <table>...</table>
    </div>
  );
}

Bulk action with optimistic update

const utils = trpc.useUtils();
const bulkSuspend = trpc.user.bulkSuspend.useMutation({
  onMutate: async ({ ids }) => {
    await utils.user.list.cancel();
    const prev = utils.user.list.getData();
    utils.user.list.setData(undefined, old => ({
      ...old!,
      rows: old!.rows.map(r => ids.includes(r.id) ? { ...r, suspended: true } : r),
    }));
    return { prev };
  },
  onError: (_e, _v, ctx) => utils.user.list.setData(undefined, ctx?.prev),
  onSettled: () => utils.user.list.invalidate(),
});

Audit log immutable schema (Drizzle)

import { pgTable, text, timestamp, jsonb, uuid } from "drizzle-orm/pg-core";

export const auditLog = pgTable("audit_log", {
  id: uuid("id").defaultRandom().primaryKey(),
  ts: timestamp("ts").defaultNow().notNull(),
  actorId: text("actor_id").notNull(),
  action: text("action").notNull(),       // e.g. "user.upgradePlan"
  target: text("target"),                  // entity id
  payload: jsonb("payload").$type<Record<string, unknown>>(),
  ip: text("ip"),
  userAgent: text("user_agent"),
});
// 매 trigger 의 UPDATE/DELETE 의 prevent — append-only.

Real-time updates via SSE (Hono)

import { Hono } from "hono";
import { streamSSE } from "hono/streaming";

const app = new Hono();

app.get("/admin/events", async (c) => {
  return streamSSE(c, async (stream) => {
    const sub = pubsub.subscribe("admin");
    try {
      for await (const event of sub) {
        await stream.writeSSE({ data: JSON.stringify(event), event: event.type });
      }
    } finally {
      sub.unsubscribe();
    }
  });
});

Streaming CSV export (1M+ rows)

export async function GET(req: Request) {
  const stream = new ReadableStream({
    async start(controller) {
      controller.enqueue(new TextEncoder().encode("id,email,plan\n"));
      const cursor = db.user.findManyCursor({ batchSize: 1000 });
      for await (const batch of cursor) {
        const csv = batch.map(u => `${u.id},${u.email},${u.plan}`).join("\n") + "\n";
        controller.enqueue(new TextEncoder().encode(csv));
      }
      controller.close();
    },
  });
  return new Response(stream, { headers: { "Content-Type": "text/csv" } });
}

Impersonation with safety guard

export const impersonate = superAdminProcedure
  .input(z.object({ targetUserId: z.string(), reason: z.string().min(10) }))
  .mutation(async ({ ctx, input }) => {
    const token = await signImpersonationToken({
      actor: ctx.session.userId,
      target: input.targetUserId,
      reason: input.reason,
      exp: Date.now() + 30 * 60 * 1000,  // 매 30min cap
    });
    await ctx.db.auditLog.create({ data: {
      actorId: ctx.session.userId,
      action: "user.impersonate",
      target: input.targetUserId,
      payload: { reason: input.reason },
    }});
    return { token };
  });

매 결정 기준

상황 Approach
매 internal-only, 매 small team Retool / Internal — 매 low-code
매 customer-facing admin Next.js 15 + tRPC + Refine — 매 custom
매 1M+ rows Server-side pagination + virtualization mandatory
매 high audit requirement Append-only log + immutable trigger
매 multi-tenant RLS (Postgres row-level security) + org scope

기본값: Next.js 15 (Server Components) + tRPC + Drizzle + shadcn/ui + TanStack Table + Better-Auth.

🔗 Graph

🤖 LLM 활용

언제: 매 CRUD scaffolding, 매 form generation, 매 SQL query draft, 매 audit event description. 언제 X: 매 destructive operation 의 final confirm, 매 RBAC policy 의 author.

안티패턴

  • Client-side pagination on 100k rows: 매 browser 의 freeze.
  • No audit log: 매 compliance 의 fail.
  • Plain UPDATE without transaction: 매 partial failure 의 inconsistent state.
  • Impersonation without expiry: 매 indefinite session 의 hijack risk.
  • CSV blob in memory: 매 OOM — 매 stream 의 mandatory.

🧪 검증 / 중복

  • Verified (Refine docs, Vercel admin patterns, Linear engineering blog).
  • 신뢰도 A.

🕓 Changelog

날짜 변경
2026-05-08 Phase 1
2026-05-10 Manual cleanup — full content with tRPC/RBAC/audit/streaming patterns