OPEN HYPER STEP
← 목록으로 (stack-analysis)
STACK-ANALYSIS · 79 / 90
stack-analysis
CHAPTER 79 / 90
읽기 약 2
FUNCTION

관리자 패널: 주문/상품/사용자 관리


핵심 개념

admin layout·테이블·필터·CSV·권한 — 운영 효율 5배.

본문

관리자 페이지 구조

📋 코드 (10줄)
/admin
├── /dashboard      # KPI 요약
├── /orders         # 주문 관리
├── /products       # 상품 관리
├── /customers      # 고객 관리
├── /coupons        # 쿠폰 관리
├── /reviews        # 리뷰 모더레이션
├── /reports        # 매출·트렌드
├── /settings       # 사이트 설정
└── /staff          # 직원 관리 (권한)

권한 체크 미들웨어

TYPESCRIPT📋 코드 (19줄)
// 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)

TSX📋 코드 (78줄)
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)

TSX📋 코드 (22줄)
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 업로드)

TYPESCRIPT📋 코드 (58줄)
// 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>
  );
}

주문 상세 (분할 결제·부분 환불)

TSX📋 코드 (43줄)
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