|
@@ -1,135 +1,352 @@
|
|
|
'use client';
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
+import '@/app/styles/reward.scss';
|
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
|
import { fetchApi } from '@/lib/utils/client';
|
|
import { fetchApi } from '@/lib/utils/client';
|
|
|
import Loading from '@/app/component/Loading';
|
|
import Loading from '@/app/component/Loading';
|
|
|
import Pagination from '@/app/component/Pagination';
|
|
import Pagination from '@/app/component/Pagination';
|
|
|
import NavTabs from '../navTabs';
|
|
import NavTabs from '../navTabs';
|
|
|
-import { ITEM_KIND_LABEL, ITEM_KIND_DESC, DIRECT_USE_KINDS } from '@/types/account/items';
|
|
|
|
|
-import type { MemberItemRow, MemberItemsResponse, UseItemResult } from '@/types/account/items';
|
|
|
|
|
|
|
+import { ItemKind, ITEM_KIND_LABEL, ITEM_KIND_DESC, DIRECT_USE_KINDS } from '@/types/account/items';
|
|
|
|
|
+import type { MemberItemRow as ConsumableItemRow, MemberItemsResponse as ConsumableItemsResponse, UseItemResult } from '@/types/account/items';
|
|
|
|
|
+import { EquippedBadge, NicknameText } from '@/components/community/EquippedBadges';
|
|
|
|
|
+import {
|
|
|
|
|
+ EQUIP_SLOT_ORDER,
|
|
|
|
|
+ EQUIP_SLOT_LABEL,
|
|
|
|
|
+ EQUIP_SLOT_DESC,
|
|
|
|
|
+ ITEM_KIND_LABEL as STATUS_KIND_LABEL,
|
|
|
|
|
+ parseEffectPayload
|
|
|
|
|
+} from '@/types/reward';
|
|
|
|
|
+import type {
|
|
|
|
|
+ MemberItemRow as StatusItemRow,
|
|
|
|
|
+ MemberItemsResponse as StatusItemsResponse,
|
|
|
|
|
+ EquipResponse,
|
|
|
|
|
+ EquipSlotValue,
|
|
|
|
|
+ EquippedItem
|
|
|
|
|
+} from '@/types/reward';
|
|
|
|
|
|
|
|
const PER_PAGE = 20;
|
|
const PER_PAGE = 20;
|
|
|
|
|
|
|
|
-export default function MemberItemsPage()
|
|
|
|
|
|
|
+// StatusItemRow → EquippedItem 변환 (배지/닉네임 미리보기용)
|
|
|
|
|
+function toEquippedItem(row: StatusItemRow): EquippedItem {
|
|
|
|
|
+ return {
|
|
|
|
|
+ slot: row.equipSlot!,
|
|
|
|
|
+ kind: row.kind,
|
|
|
|
|
+ productName: row.productName,
|
|
|
|
|
+ effectPayload: row.effectPayload,
|
|
|
|
|
+ thumbnail: row.thumbnail
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function formatExpiry(expiresAt: string|null): string {
|
|
|
|
|
+ if (!expiresAt) {
|
|
|
|
|
+ return '기간 제한 없음';
|
|
|
|
|
+ }
|
|
|
|
|
+ const d = new Date(expiresAt);
|
|
|
|
|
+ return `${d.getFullYear()}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')} 만료`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ── 지위 아이템 (슬롯별 장착 관리) ──
|
|
|
|
|
+function StatusItemsSection()
|
|
|
{
|
|
{
|
|
|
- const [items, setItems] = useState<MemberItemRow[]>([]);
|
|
|
|
|
- const [total, setTotal] = useState(0);
|
|
|
|
|
- const [page, setPage] = useState(1);
|
|
|
|
|
|
|
+ const [items, setItems] = useState<StatusItemRow[]>([]);
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [loading, setLoading] = useState(true);
|
|
|
const [busyID, setBusyID] = useState<number|null>(null);
|
|
const [busyID, setBusyID] = useState<number|null>(null);
|
|
|
|
|
|
|
|
const load = useCallback(async () => {
|
|
const load = useCallback(async () => {
|
|
|
setLoading(true);
|
|
setLoading(true);
|
|
|
|
|
|
|
|
- const res = await fetchApi<MemberItemsResponse>(`/api/member/me/items?page=${page}&perPage=${PER_PAGE}`, { silent: true });
|
|
|
|
|
|
|
+ const res = await fetchApi<StatusItemsResponse>('/api/members/me/items?page=1&perPage=100', { silent: true });
|
|
|
|
|
|
|
|
if (res.success && res.data) {
|
|
if (res.success && res.data) {
|
|
|
- setItems(res.data.list);
|
|
|
|
|
- setTotal(res.data.total);
|
|
|
|
|
|
|
+ // 지위 아이템(슬롯 보유)만 취급 — 소모품은 아래 별도 섹션
|
|
|
|
|
+ setItems(res.data.list.filter(c => c.isStatusItem && c.equipSlot != null));
|
|
|
}
|
|
}
|
|
|
else {
|
|
else {
|
|
|
setItems([]);
|
|
setItems([]);
|
|
|
- setTotal(0);
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
setLoading(false);
|
|
setLoading(false);
|
|
|
- }, [page]);
|
|
|
|
|
|
|
+ }, []);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
load();
|
|
load();
|
|
|
}, [load]);
|
|
}, [load]);
|
|
|
|
|
|
|
|
- const useItem = async (item: MemberItemRow) => {
|
|
|
|
|
- if (item.kind === 'PostReset' && !confirm('신고·댓글이 없는 예전 게시글이 모두 삭제됩니다. 계속하시겠어요?')) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ const toggleEquip = async (item: StatusItemRow) => {
|
|
|
setBusyID(item.id);
|
|
setBusyID(item.id);
|
|
|
|
|
|
|
|
- const res = await fetchApi<UseItemResult>(`/api/member/me/items/${item.id}/use`, { method: 'POST', silent: true });
|
|
|
|
|
|
|
+ const action = item.isEquipped ? 'unequip' : 'equip';
|
|
|
|
|
+ const res = await fetchApi<EquipResponse>(`/api/members/me/items/${item.id}/${action}`, { method: 'POST', silent: true });
|
|
|
|
|
|
|
|
setBusyID(null);
|
|
setBusyID(null);
|
|
|
|
|
|
|
|
- if (res.success && res.data) {
|
|
|
|
|
- alert(res.data.message || '아이템을 사용했습니다.');
|
|
|
|
|
|
|
+ if (res.success) {
|
|
|
await load();
|
|
await load();
|
|
|
}
|
|
}
|
|
|
else {
|
|
else {
|
|
|
- alert(res.message || '아이템 사용에 실패했습니다.');
|
|
|
|
|
|
|
+ alert(res.message || '장착 상태 변경에 실패했습니다.');
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- return (
|
|
|
|
|
- <>
|
|
|
|
|
- <NavTabs />
|
|
|
|
|
|
|
+ if (loading) {
|
|
|
|
|
+ return <Loading />;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- <div className='mx-auto px-4 sm:px-8 pb-8'>
|
|
|
|
|
- <h1 className='text-2xl font-bold mb-1'>아이템</h1>
|
|
|
|
|
- <p className='text-sm text-neutral-500 mb-4'>
|
|
|
|
|
- 상점에서 구매한 소모품 아이템입니다. 채팅 밴 해제·예전 글 초기화·경험치 부스터는 여기서 바로 사용하고, 쪽지권·프로필 변경권은 해당 기능 이용 시 자동으로 사용됩니다.
|
|
|
|
|
- </p>
|
|
|
|
|
|
|
+ if (items.length === 0) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className='text-center py-8 text-neutral-500 text-sm'>
|
|
|
|
|
+ 보유한 지위 아이템이 없습니다. 상점에서 배지·닉네임 이펙트·댓글 이펙트를 구매할 수 있습니다.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- <div className='text-sm text-neutral-500 mb-3'>미사용 {total.toLocaleString()}개</div>
|
|
|
|
|
|
|
+ // 슬롯별 그룹
|
|
|
|
|
+ const bySlot = new Map<EquipSlotValue, StatusItemRow[]>();
|
|
|
|
|
+ for (const slot of EQUIP_SLOT_ORDER) {
|
|
|
|
|
+ bySlot.set(slot, []);
|
|
|
|
|
+ }
|
|
|
|
|
+ for (const item of items) {
|
|
|
|
|
+ const arr = bySlot.get(item.equipSlot!) ?? [];
|
|
|
|
|
+ arr.push(item);
|
|
|
|
|
+ bySlot.set(item.equipSlot!, arr);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- {loading ? (
|
|
|
|
|
- <Loading />
|
|
|
|
|
- ) : items.length === 0 ? (
|
|
|
|
|
- <div className='text-center py-20 text-neutral-500'>보유한 아이템이 없습니다.</div>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <>
|
|
|
|
|
- <ul className='flex flex-col gap-3'>
|
|
|
|
|
- {items.map((item) => {
|
|
|
|
|
- const directUse = DIRECT_USE_KINDS.includes(item.kind);
|
|
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className='flex flex-col gap-6'>
|
|
|
|
|
+ {EQUIP_SLOT_ORDER.map((slot) => {
|
|
|
|
|
+ const slotItems = bySlot.get(slot) ?? [];
|
|
|
|
|
+ if (slotItems.length === 0) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div key={slot}>
|
|
|
|
|
+ <div className='mb-2'>
|
|
|
|
|
+ <h3 className='font-semibold'>{EQUIP_SLOT_LABEL[slot]}</h3>
|
|
|
|
|
+ <p className='text-xs text-neutral-500'>{EQUIP_SLOT_DESC[slot]} 슬롯당 1개만 장착됩니다.</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <ul className='flex flex-col gap-2'>
|
|
|
|
|
+ {slotItems.map((item) => {
|
|
|
|
|
+ const equipped = toEquippedItem(item);
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<li
|
|
<li
|
|
|
key={item.id}
|
|
key={item.id}
|
|
|
- className='flex items-center gap-4 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4'
|
|
|
|
|
|
|
+ className={`flex items-center gap-4 rounded-lg border p-4 ${
|
|
|
|
|
+ item.isEquipped
|
|
|
|
|
+ ? 'border-red-400 dark:border-red-600 bg-red-50/50 dark:bg-red-950/20'
|
|
|
|
|
+ : 'border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900'
|
|
|
|
|
+ }`}
|
|
|
>
|
|
>
|
|
|
<div className='size-14 shrink-0 rounded bg-neutral-100 dark:bg-neutral-800 overflow-hidden flex items-center justify-center'>
|
|
<div className='size-14 shrink-0 rounded bg-neutral-100 dark:bg-neutral-800 overflow-hidden flex items-center justify-center'>
|
|
|
{item.thumbnail ? (
|
|
{item.thumbnail ? (
|
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
|
<img src={item.thumbnail} alt={item.productName} className='w-full h-full object-scale-down' />
|
|
<img src={item.thumbnail} alt={item.productName} className='w-full h-full object-scale-down' />
|
|
|
) : (
|
|
) : (
|
|
|
- <span className='text-2xl'>🎁</span>
|
|
|
|
|
|
|
+ <span className='text-2xl'>🏅</span>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div className='flex-1 min-w-0'>
|
|
<div className='flex-1 min-w-0'>
|
|
|
- <div className='font-semibold truncate'>
|
|
|
|
|
- {ITEM_KIND_LABEL[item.kind] ?? item.productName}
|
|
|
|
|
- {item.kind === 'ExpBooster' && item.durationDays > 0 && (
|
|
|
|
|
- <span className='ml-1 text-xs text-neutral-500'>({item.durationDays}일)</span>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ <div className='font-semibold truncate'>{item.productName}</div>
|
|
|
|
|
+ <div className='text-xs text-neutral-500'>
|
|
|
|
|
+ {STATUS_KIND_LABEL[item.kind]} · {formatExpiry(item.expiresAt)}
|
|
|
|
|
+ {item.giftFromName && <span className='ml-1'>· 선물 (from {item.giftFromName})</span>}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {/* 미리보기 — 배지 / 닉네임 이펙트 실제 렌더 */}
|
|
|
|
|
+ <div className='mt-1 text-sm'>
|
|
|
|
|
+ {parseEffectPayload(item.effectPayload) || item.thumbnail ? (
|
|
|
|
|
+ <span className='inline-flex items-center gap-1'>
|
|
|
|
|
+ <NicknameText name='미리보기' items={[equipped]} />
|
|
|
|
|
+ <EquippedBadge items={[equipped]} />
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ) : null}
|
|
|
</div>
|
|
</div>
|
|
|
- <div className='text-xs text-neutral-500 line-clamp-2'>{ITEM_KIND_DESC[item.kind]}</div>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div className='shrink-0'>
|
|
<div className='shrink-0'>
|
|
|
- {directUse ? (
|
|
|
|
|
- <button
|
|
|
|
|
- type='button'
|
|
|
|
|
- onClick={() => useItem(item)}
|
|
|
|
|
- disabled={busyID === item.id}
|
|
|
|
|
- className='px-4 py-2 rounded-md bg-red-600 text-white text-sm font-semibold hover:bg-red-700 disabled:opacity-50'
|
|
|
|
|
- >
|
|
|
|
|
- {busyID === item.id ? '처리 중...' : '사용하기'}
|
|
|
|
|
- </button>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <span className='text-xs text-neutral-400'>기능 이용 시 사용</span>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ <button
|
|
|
|
|
+ type='button'
|
|
|
|
|
+ onClick={() => toggleEquip(item)}
|
|
|
|
|
+ disabled={busyID === item.id}
|
|
|
|
|
+ className={`px-4 py-2 rounded-md text-sm font-semibold disabled:opacity-50 ${
|
|
|
|
|
+ item.isEquipped
|
|
|
|
|
+ ? 'border border-neutral-300 dark:border-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-800'
|
|
|
|
|
+ : 'bg-red-600 text-white hover:bg-red-700'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ {busyID === item.id ? '처리 중...' : (item.isEquipped ? '해제' : '장착')}
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
</li>
|
|
</li>
|
|
|
);
|
|
);
|
|
|
})}
|
|
})}
|
|
|
</ul>
|
|
</ul>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ })}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ── 소모품 아이템 (기존 — 사용/자동소모) ──
|
|
|
|
|
+function ConsumableItemsSection()
|
|
|
|
|
+{
|
|
|
|
|
+ const [items, setItems] = useState<ConsumableItemRow[]>([]);
|
|
|
|
|
+ const [total, setTotal] = useState(0);
|
|
|
|
|
+ const [page, setPage] = useState(1);
|
|
|
|
|
+ const [loading, setLoading] = useState(true);
|
|
|
|
|
+ const [busyID, setBusyID] = useState<number|null>(null);
|
|
|
|
|
+
|
|
|
|
|
+ const load = useCallback(async () => {
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+
|
|
|
|
|
+ const res = await fetchApi<ConsumableItemsResponse>(`/api/member/me/items?page=${page}&perPage=${PER_PAGE}`, { silent: true });
|
|
|
|
|
+
|
|
|
|
|
+ if (res.success && res.data) {
|
|
|
|
|
+ setItems(res.data.list);
|
|
|
|
|
+ setTotal(res.data.total);
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ setItems([]);
|
|
|
|
|
+ setTotal(0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ }, [page]);
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ load();
|
|
|
|
|
+ }, [load]);
|
|
|
|
|
+
|
|
|
|
|
+ const useItem = async (item: ConsumableItemRow) => {
|
|
|
|
|
+ if (item.kind === ItemKind.PostReset && !confirm('신고·댓글이 없는 예전 게시글이 모두 삭제됩니다. 계속하시겠어요?')) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setBusyID(item.id);
|
|
|
|
|
+
|
|
|
|
|
+ const res = await fetchApi<UseItemResult>(`/api/member/me/items/${item.id}/use`, { method: 'POST', silent: true });
|
|
|
|
|
+
|
|
|
|
|
+ setBusyID(null);
|
|
|
|
|
+
|
|
|
|
|
+ if (res.success && res.data) {
|
|
|
|
|
+ alert(res.data.message || '아이템을 사용했습니다.');
|
|
|
|
|
+ await load();
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ alert(res.message || '아이템 사용에 실패했습니다.');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (loading) {
|
|
|
|
|
+ return <Loading />;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (items.length === 0) {
|
|
|
|
|
+ return <div className='text-center py-8 text-neutral-500 text-sm'>보유한 소모품이 없습니다.</div>;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <div className='text-sm text-neutral-500 mb-3'>미사용 {total.toLocaleString()}개</div>
|
|
|
|
|
+
|
|
|
|
|
+ <ul className='flex flex-col gap-3'>
|
|
|
|
|
+ {items.map((item) => {
|
|
|
|
|
+ const directUse = DIRECT_USE_KINDS.includes(item.kind);
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <li
|
|
|
|
|
+ key={item.id}
|
|
|
|
|
+ className='flex items-center gap-4 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4'
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className='size-14 shrink-0 rounded bg-neutral-100 dark:bg-neutral-800 overflow-hidden flex items-center justify-center'>
|
|
|
|
|
+ {item.thumbnail ? (
|
|
|
|
|
+ // eslint-disable-next-line @next/next/no-img-element
|
|
|
|
|
+ <img src={item.thumbnail} alt={item.productName} className='w-full h-full object-scale-down' />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <span className='text-2xl'>🎁</span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className='flex-1 min-w-0'>
|
|
|
|
|
+ <div className='font-semibold truncate'>
|
|
|
|
|
+ {ITEM_KIND_LABEL[item.kind] ?? item.productName}
|
|
|
|
|
+ {item.kind === ItemKind.ExpBooster && item.durationDays > 0 && (
|
|
|
|
|
+ <span className='ml-1 text-xs text-neutral-500'>({item.durationDays}일)</span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className='text-xs text-neutral-500 line-clamp-2'>{ITEM_KIND_DESC[item.kind]}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className='shrink-0'>
|
|
|
|
|
+ {directUse ? (
|
|
|
|
|
+ <button
|
|
|
|
|
+ type='button'
|
|
|
|
|
+ onClick={() => useItem(item)}
|
|
|
|
|
+ disabled={busyID === item.id}
|
|
|
|
|
+ className='px-4 py-2 rounded-md bg-red-600 text-white text-sm font-semibold hover:bg-red-700 disabled:opacity-50'
|
|
|
|
|
+ >
|
|
|
|
|
+ {busyID === item.id ? '처리 중...' : '사용하기'}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <span className='text-xs text-neutral-400'>기능 이용 시 사용</span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </li>
|
|
|
|
|
+ );
|
|
|
|
|
+ })}
|
|
|
|
|
+ </ul>
|
|
|
|
|
+
|
|
|
|
|
+ <hr className='my-4 border-neutral-200 dark:border-neutral-800' />
|
|
|
|
|
+
|
|
|
|
|
+ <Pagination total={total} page={page} perPage={PER_PAGE} onChange={setPage} />
|
|
|
|
|
+ </>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export default function MemberItemsPage()
|
|
|
|
|
+{
|
|
|
|
|
+ const [tab, setTab] = useState<'status'|'consumable'>('status');
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <NavTabs />
|
|
|
|
|
+
|
|
|
|
|
+ <div className='mx-auto px-4 sm:px-8 pb-8'>
|
|
|
|
|
+ <h1 className='text-2xl font-bold mb-1'>아이템</h1>
|
|
|
|
|
+ <p className='text-sm text-neutral-500 mb-4'>
|
|
|
|
|
+ 지위 아이템(배지·닉네임/댓글 이펙트)은 슬롯별로 장착하고, 소모품은 사용하거나 해당 기능 이용 시 자동으로 사용됩니다.
|
|
|
|
|
+ </p>
|
|
|
|
|
|
|
|
- <hr className='my-4 border-neutral-200 dark:border-neutral-800' />
|
|
|
|
|
|
|
+ <div className='mb-5 flex gap-1 border-b border-neutral-200 dark:border-neutral-800'>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type='button'
|
|
|
|
|
+ onClick={() => setTab('status')}
|
|
|
|
|
+ className={`px-4 py-2 text-sm font-semibold border-b-2 -mb-px ${
|
|
|
|
|
+ tab === 'status'
|
|
|
|
|
+ ? 'border-red-600 text-red-600 dark:text-red-400 dark:border-red-400'
|
|
|
|
|
+ : 'border-transparent text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ 지위 아이템
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type='button'
|
|
|
|
|
+ onClick={() => setTab('consumable')}
|
|
|
|
|
+ className={`px-4 py-2 text-sm font-semibold border-b-2 -mb-px ${
|
|
|
|
|
+ tab === 'consumable'
|
|
|
|
|
+ ? 'border-red-600 text-red-600 dark:text-red-400 dark:border-red-400'
|
|
|
|
|
+ : 'border-transparent text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ 소모품
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <Pagination total={total} page={page} perPage={PER_PAGE} onChange={setPage} />
|
|
|
|
|
- </>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ {tab === 'status' ? <StatusItemsSection /> : <ConsumableItemsSection />}
|
|
|
</div>
|
|
</div>
|
|
|
</>
|
|
</>
|
|
|
);
|
|
);
|
|
|
-}
|
|
|
|
|
|
|
+}
|