stack-analysis
CHAPTER 79 / 90
읽기 약 2분
FUNCTION
관리자 패널: 주문/상품/사용자 관리
핵심 개념
admin layout·테이블·필터·CSV·권한 — 운영 효율 5배.
본문
관리자 페이지 구조
/admin
├── /dashboard # KPI 요약
├── /orders # 주문 관리
├── /products # 상품 관리
├── /customers # 고객 관리
├── /coupons # 쿠폰 관리
├── /reviews # 리뷰 모더레이션
├── /reports # 매출·트렌드
├── /settings # 사이트 설정
└── /staff # 직원 관리 (권한)권한 체크 미들웨어
// middleware/admin.ts
export function adminOnly(...roles: string[]) {
return async (req: Request, res: Response, next: NextFunction) => {
const staff = await db.staff.findUnique({ where: { userId: req.user!.id } });
if (!staff || !roles.includes(staff.role)) {
return res.status(403).json({ error: 'forbidden' });
}
req.staffRole = staff.role;
next();
};
}
// 사용
app.use('/api/admin', authenticate, adminOnly('admin', 'manager'));
app.delete('/api/admin/products/:id',
authenticate, adminOnly('admin'), // admin만
deleteProduct,
);데이터 테이블 (TanStack Table)
import {
useReactTable, getCoreRowModel, flexRender,
getFilteredRowModel, getSortedRowModel, getPaginationRowModel,
} from '@tanstack/react-table';
const columns = [
{ accessorKey: 'orderNumber', header: '주문번호' },
{
accessorKey: 'createdAt',
header: '주문일',
cell: ({ getValue }) => format(getValue() as Date, 'yyyy-MM-dd HH:mm'),
},
{ accessorKey: 'customer.name', header: '고객명' },
{
accessorKey: 'total',
header: '금액',
cell: ({ getValue }) => `₩${(getValue() as number).toLocaleString()}`,
},
{
accessorKey: 'status',
header: '상태',
cell: ({ getValue }) => <StatusBadge status={getValue() as string} />,
filterFn: 'equals',
},
{
id: 'actions',
cell: ({ row }) => (
<DropdownMenu>
<Item onClick={() => view(row.original.id)}>상세</Item>
<Item onClick={() => ship(row.original.id)}>발송</Item>
<Item onClick={() => refund(row.original.id)}>환불</Item>
</DropdownMenu>
),
},
];
function OrdersTable({ data }: { data: Order[] }) {
const table = useReactTable({
data, columns,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
initialState: { pagination: { pageSize: 50 } },
});
return (
<div>
<FiltersBar table={table} />
<table>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id} onClick={header.column.getToggleSortingHandler()}>
{flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map(row => (
<tr key={row.id}>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
<Pagination table={table} />
</div>
);
}일괄 작업 (Bulk Actions)
function BulkActions({ table }: { table: any }) {
const selected = table.getFilteredSelectedRowModel().rows;
if (selected.length === 0) return null;
return (
<div className="bg-blue-50 p-4 rounded">
<span>{selected.length}개 선택됨</span>
<button onClick={async () => {
const ids = selected.map(r => r.original.id);
if (confirm(`${ids.length}개 일괄 발송?`)) {
await fetch('/api/admin/orders/bulk-ship', {
method: 'POST',
body: JSON.stringify({ orderIds: ids }),
});
}
}}>일괄 발송</button>
<button onClick={() => exportCSV(selected.map(r => r.original))}>
CSV 내보내기
</button>
</div>
);
}상품 일괄 등록 (CSV 업로드)
// 1. CSV 파싱
import { parse } from 'csv-parse/sync';
async function uploadProductsCSV(csvContent: string) {
const records = parse(csvContent, {
columns: true,
skip_empty_lines: true,
});
const validated = records.map((row: any) => ProductCSVSchema.parse(row));
return db.$transaction(
validated.map(p =>
db.product.upsert({
where: { sku: p.sku },
create: p,
update: p,
})
)
);
}
// 2. UI
function ProductImport() {
const [file, setFile] = useState<File | null>(null);
const [preview, setPreview] = useState<any[]>([]);
const [errors, setErrors] = useState<any[]>([]);
const handleUpload = async () => {
const formData = new FormData();
formData.append('file', file!);
const res = await fetch('/api/admin/products/import', {
method: 'POST',
body: formData,
});
const data = await res.json();
setErrors(data.errors);
};
return (
<div>
<input type="file" accept=".csv" onChange={e => {
const f = e.target.files?.[0];
if (f) {
setFile(f);
// 미리보기
f.text().then(t => {
const records = parse(t, { columns: true });
setPreview(records.slice(0, 10));
});
}
}} />
<PreviewTable data={preview} />
<button onClick={handleUpload}>업로드 ({preview.length}건)</button>
</div>
);
}주문 상세 (분할 결제·부분 환불)
function OrderDetailPage({ order }: { order: Order }) {
return (
<div className="grid grid-cols-3 gap-6">
<div className="col-span-2 space-y-4">
<Card title="주문 항목">
<ItemsTable items={order.items} />
</Card>
<Card title="결제 정보">
<PaymentInfo payment={order.payment} />
<Button onClick={() => initRefund(order.id)}>환불 처리</Button>
</Card>
<Card title="배송 정보">
<ShippingInfo shipment={order.shipment} />
<Button onClick={() => updateTracking(order.id)}>송장번호 입력</Button>
</Card>
<Card title="이력">
<Timeline history={order.statusHistory} />
</Card>
</div>
<aside className="space-y-4">
<Card title="고객">
<Link href={`/admin/customers/${order.userId}`}>
{order.customer.name}
</Link>
<p>최근 5개 주문: ₩{order.customer.totalSpent.toLocaleString()}</p>
</Card>
<Card title="배송 주소">
<Address address={order.shippingAddress} />
</Card>
<Card title="메모">
<textarea defaultValue={order.adminNote} />
<Button onClick={saveNote}>저장</Button>
</Card>
</aside>
</div>
);
}다음 챕터
CH.80 "이커머스 종합".
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 프로젝트의 관리자 패널 부분을 분석해서 실전 분석 + 개선 우선순위를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
관리자 패널 관련 실제 서비스 5개를 비교 분석해서 패턴 추출를 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 코드베이스에서 관리자 패널 최적화 가능 위치를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 풀스택 시장의 관리자 패널 트렌드를 솔직히 알려줘.
⭐ 이것만 기억하세요
관리자 패널: 주문/상품/사용자 관리는 이 3가지만 확실히 잡으세요
1.관리자 권한 — admin/manager/staff 3단계 RBAC
2.TanStack Table = 정렬·필터·페이지·일괄 선택 모두
3.CSV 일괄 업로드 + 미리보기 + 에러 표시 = 운영 효율
공유하기
진행도 79 / 90