9148c358d0
Topic_Agent/Topic_Blog/Topics/Topics_Biz/Topics_Meeting/Topics_Rag의 마크다운 지식 문서를 Topic_General/Topic_Programming/Topic_Graphic/Topic_Business 4개 카테고리로 재분류. - 중복 제거: frontmatter의 status:duplicate/merged + duplicate_of/redirect_to 필드로 자기 자신을 중복으로 선언한 리다이렉트 stub 1032개 제거, 완전 동일 내용 파일 472개 제거, 동일 파일명·다른 내용 충돌 시 더 큰(완전한) 버전만 유지(162개 제거) — 총 1639개 중복 제거. - 분류: 폴더 단위로 명확한 항목(AI_and_ML/Coding/Architecture 등 → Programming, Comfyui/Visual_Effects → Graphic, Topics_Biz/Topics_Meeting/사업 등 → Business, Poetic_Blog_Writing/창의성/Game_Design 등 → General)은 폴더 우선순위로, 나머지 혼재 폴더(Topic_Agent/Topic_Blog/Topics 루트/Thinking & Reasoning/Other/UI_UX_Assets)는 title/tags 키워드 스코어링으로 파일 단위 분류(불명확한 경우 General로 폴백). 원본 폴더명은 "From_*" 서브폴더로 보존해 추적 가능성 유지. - 최종 배치: Programming 2784 / General 1608 / Graphic 285 / Business 249 = 4926개 문서. - 에이전트 운영 상태(.astra/.agent/.obsidian/sessions/memory/_company/docs/lessons/_shared/src)는 지식 콘텐츠가 아니므로 재분류 대상에서 제외하고 원위치 유지. - Topics/Topic_email(상위 보호 폴더 Topic_email과 파일명 100% 중복) 삭제 — 보호 폴더 자체는 미변경. - 완전히 비게 된 Topic_Agent/Topic_Blog/Topics_Biz/Topics_Rag 폴더 제거.
8.7 KiB
8.7 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 |
|
none | A | 0.9 | applied |
|
2026-05-10 | pending |
|
데이터 중심의 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.
매 응용
- 매 SaaS user management — 매 user search, 매 plan upgrade, 매 quota override.
- 매 CRM contact / deal pipeline — 매 Kanban + Table dual view.
- 매 financial ops — 매 refund, 매 invoice 의 manual issue.
- 매 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
- 부모: Large_Frontend_Projects
- 변형: Internal_Tools
- Adjacent: RBAC · Multi_Tenant
🤖 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 |