| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- 'use client';
- import { useCallback, useEffect, useMemo, useState } from 'react';
- import Link from 'next/link';
- import { Search } from 'lucide-react';
- import { fetchApi, getDateTime } from '@/lib/utils/client';
- import Loading from '@/app/component/Loading';
- import Pagination from '@/app/component/Pagination';
- import NavTabs from '../navTabs';
- import {
- ORDER_STATUS_LABEL,
- SHIPMENT_STATUS_LABEL,
- type OrderListResponse,
- type OrderListRow,
- type OrderStatus
- } from '@/types/store';
- const PER_PAGE = 20;
- type FilterMode = 'recent6m' | number;
- function statusBadgeClass(status: OrderStatus): string
- {
- if (status === 5) {
- return 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300';
- }
- if (status === 6) {
- return 'bg-neutral-200 text-neutral-700 dark:bg-neutral-700 dark:text-neutral-300';
- }
- if (status === 7 || status === 8) {
- return 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300';
- }
- return 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300';
- }
- export default function OrdersPage()
- {
- const [orders, setOrders] = useState<OrderListRow[]>([]);
- const [total, setTotal] = useState(0);
- const [page, setPage] = useState(1);
- const [loading, setLoading] = useState(true);
- const [filterMode, setFilterMode] = useState<FilterMode>('recent6m');
- const [keywordInput, setKeywordInput] = useState('');
- const [appliedKeyword, setAppliedKeyword] = useState('');
- const yearChips = useMemo(() => {
- const current = new Date().getFullYear();
- return Array.from({ length: 6 }, (_, i) => current - i);
- }, []);
- const load = useCallback(async () => {
- setLoading(true);
- const params = new URLSearchParams();
- params.set('page', String(page));
- params.set('perPage', String(PER_PAGE));
- if (typeof filterMode === 'number') {
- params.set('year', String(filterMode));
- }
- if (appliedKeyword) {
- params.set('keyword', appliedKeyword);
- }
- const res = await fetchApi<OrderListResponse>(`/api/store/orders?${params.toString()}`, { silent: true });
- if (res.success && res.data) {
- setOrders(res.data.list);
- setTotal(res.data.total);
- }
- else {
- setOrders([]);
- setTotal(0);
- }
- setLoading(false);
- }, [filterMode, appliedKeyword, page]);
- useEffect(() => {
- load();
- }, [load]);
- useEffect(() => {
- setPage(1);
- }, [filterMode, appliedKeyword]);
- const handleSearch = () => {
- setAppliedKeyword(keywordInput.trim());
- };
- const handleKeywordKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
- if (e.key === 'Enter') {
- e.preventDefault();
- handleSearch();
- }
- };
- const handleChipClick = (mode: FilterMode) => {
- setFilterMode(mode);
- };
- const chipClass = (active: boolean): string => active
- ? 'border border-blue-600 bg-blue-600 text-white'
- : 'border border-neutral-300 text-neutral-700 hover:bg-neutral-50 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800';
- return (
- <>
- <NavTabs />
- <div className="px-4 pb-8 sm:px-8">
- <h1 className="text-2xl font-bold mb-4">주문목록</h1>
- {/* 상품 검색 */}
- <div className="relative mb-3 w-full sm:max-w-md">
- <input
- type="search"
- value={keywordInput}
- onChange={(e) => { setKeywordInput(e.target.value); }}
- onKeyDown={handleKeywordKeyDown}
- placeholder="주문한 상품을 검색할 수 있어요!"
- className="w-full border border-neutral-300 dark:border-neutral-700 rounded-md px-4 py-2.5 pr-10 text-sm bg-white dark:bg-neutral-900"
- maxLength={100}
- />
- <button
- type="button"
- onClick={handleSearch}
- aria-label="검색"
- className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-neutral-400 hover:text-blue-600"
- >
- <Search className="size-5" />
- </button>
- </div>
- {/* 기간 필터 칩 */}
- <div className="flex flex-wrap gap-1.5 mb-4">
- <button
- type="button"
- onClick={() => { handleChipClick('recent6m'); }}
- className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${chipClass(filterMode === 'recent6m')}`}
- >
- 최근 6개월
- </button>
- {yearChips.map((y) => (
- <button
- key={y}
- type="button"
- onClick={() => { handleChipClick(y); }}
- className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${chipClass(filterMode === y)}`}
- >
- {y}
- </button>
- ))}
- </div>
- <div className="text-sm text-neutral-500 mb-3">총 {total.toLocaleString()}건</div>
- {loading ? (
- <Loading />
- ) : orders.length === 0 ? (
- <div className="text-center py-20 text-neutral-500">주문 내역이 없습니다.</div>
- ) : (
- <>
- <div className="space-y-3">
- {orders.map((o) => (
- <div key={o.id} className="border border-neutral-200 dark:border-neutral-800 rounded-lg p-4 bg-white dark:bg-neutral-900">
- <div className="flex items-center justify-between mb-3 pb-2 border-b border-neutral-100 dark:border-neutral-800">
- <Link
- href={`/orders/${o.id}`}
- className="text-xs font-mono font-semibold text-blue-600 dark:text-blue-400 hover:underline"
- >
- {o.orderNumber}
- </Link>
- <div className="flex items-center gap-1.5">
- <span className={`px-2 py-0.5 rounded text-xs font-semibold ${statusBadgeClass(o.status)}`}>
- {ORDER_STATUS_LABEL[o.status]}
- </span>
- {o.shipmentStatus !== null && o.shipmentStatus !== undefined && (
- <span className="px-2 py-0.5 rounded text-xs bg-neutral-100 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300">
- 배송 {SHIPMENT_STATUS_LABEL[o.shipmentStatus]}
- </span>
- )}
- {o.hasActiveRefundRequest && (
- <span className="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300">
- 환불 요청 진행중
- </span>
- )}
- </div>
- </div>
- <Link href={`/orders/${o.id}`} className="flex items-center gap-4 hover:bg-neutral-50 dark:hover:bg-neutral-800/40 rounded p-1 -m-1">
- <div className="w-20 h-20 bg-neutral-100 dark:bg-neutral-800 rounded overflow-hidden flex items-center justify-center flex-shrink-0">
- {o.firstItemThumbnail ? (
- // eslint-disable-next-line @next/next/no-img-element
- <img src={o.firstItemThumbnail} alt="" className="w-full h-full object-cover" />
- ) : (
- <span className="text-neutral-400 text-xs">no img</span>
- )}
- </div>
- <div className="flex-1 min-w-0">
- <div className="text-sm font-semibold truncate">
- {o.firstItemName}
- {o.itemCount > 1 && <span className="text-neutral-500 font-normal"> 외 {o.itemCount - 1}건</span>}
- </div>
- <div className="text-xs text-neutral-500 mt-1">
- 총 {o.itemCount}개 상품
- {o.channelName && <span className="ml-2">• 후원 {o.channelName}</span>}
- </div>
- <div className="text-xs text-neutral-500 mt-1">
- 주문 {getDateTime(o.createdAt)}
- </div>
- </div>
- <div className="text-right flex-shrink-0">
- <div className="text-xs text-neutral-500">결제 금액</div>
- <div className="text-lg font-bold text-red-600 dark:text-red-400">
- {o.totalAmount.toLocaleString()}P
- </div>
- </div>
- </Link>
- </div>
- ))}
- </div>
- <Pagination total={total} page={page} perPage={PER_PAGE} onChange={setPage} />
- </>
- )}
- </div>
- </>
- );
- }
|