fix: 지갑 UI 필드명/응답 구조 불일치 수정
서버 실제 응답과 프론트엔드 필드명이 맞지 않아 공개키 미표시, 인벤토리/마켓 TypeError 발생하던 버그 수정. - WalletTab: public_key→pubKeyHex, private_key→privateKey - InventoryTab: 배열→객체(slots) 구조 대응 - AssetsTab: 페이지네이션 응답, tradeable, active_listing_id, template_id - MarketTab: ID 목록→개별 getListing 호출, listing_id→id, asset_id - WalletSummary: 페이지네이션 + 인벤토리 객체 구조 대응 - chain.js: getListing() 함수 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,10 @@ export async function getMarketListings() {
|
||||
return apiFetch('/api/chain/market');
|
||||
}
|
||||
|
||||
export async function getListing(id) {
|
||||
return apiFetch(`/api/chain/market/${id}`);
|
||||
}
|
||||
|
||||
function idempotentPost(path, body) {
|
||||
return apiFetch(path, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -14,8 +14,9 @@ export default function AssetsTab() {
|
||||
const load = useCallback(() => {
|
||||
setLoading(true);
|
||||
getAssets()
|
||||
.then((ids) => {
|
||||
if (!ids || ids.length === 0) {
|
||||
.then((result) => {
|
||||
const ids = result?.ids || result;
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
setAssets([]);
|
||||
return;
|
||||
}
|
||||
@@ -49,7 +50,7 @@ export default function AssetsTab() {
|
||||
await listOnMarket(assetId, price);
|
||||
toast.success('마켓에 등록되었습니다.');
|
||||
setAssets((prev) =>
|
||||
prev.map((a) => (a.id === assetId ? { ...a, listed: true } : a))
|
||||
prev.map((a) => (a.id === assetId ? { ...a, active_listing_id: 'pending' } : a))
|
||||
);
|
||||
setListingAsset(null);
|
||||
setListingPrice('');
|
||||
@@ -65,16 +66,15 @@ export default function AssetsTab() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{assets.map((asset) => (
|
||||
{assets.map((asset) => {
|
||||
const isListed = !!asset.active_listing_id;
|
||||
return (
|
||||
<div key={asset.id} className="asset-item">
|
||||
<div className="asset-header" onClick={() => toggleExpand(asset.id)}>
|
||||
<div>
|
||||
{asset._loaded ? (
|
||||
<>
|
||||
<strong style={{ color: '#fff' }}>{asset.item_name || asset.template_name || '자산'}</strong>
|
||||
{asset.template_name && asset.item_name !== asset.template_name && (
|
||||
<span style={{ marginLeft: 8, fontSize: '0.8rem', opacity: 0.5 }}>{asset.template_name}</span>
|
||||
)}
|
||||
<strong style={{ color: '#fff' }}>{asset.template_id || '자산'}</strong>
|
||||
<span style={{ marginLeft: 12, fontSize: '0.8rem', opacity: 0.4 }}>#{asset.id}</span>
|
||||
</>
|
||||
) : (
|
||||
@@ -82,8 +82,8 @@ export default function AssetsTab() {
|
||||
)}
|
||||
</div>
|
||||
{asset._loaded && (
|
||||
<span className={`wallet-tag ${asset.tradable ? 'wallet-tag-ok' : 'wallet-tag-no'}`}>
|
||||
{asset.tradable ? '거래 가능' : '거래 불가'}
|
||||
<span className={`wallet-tag ${asset.tradeable ? 'wallet-tag-ok' : 'wallet-tag-no'}`}>
|
||||
{asset.tradeable ? '거래 가능' : '거래 불가'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -104,7 +104,7 @@ export default function AssetsTab() {
|
||||
|
||||
{/* 마켓 등록 상태 */}
|
||||
<div style={{ marginTop: 12 }}>
|
||||
{asset.listed ? (
|
||||
{isListed ? (
|
||||
<span className="wallet-tag wallet-tag-listed">마켓 등록됨</span>
|
||||
) : (
|
||||
<span className="wallet-tag wallet-tag-no">미등록</span>
|
||||
@@ -112,7 +112,7 @@ export default function AssetsTab() {
|
||||
</div>
|
||||
|
||||
{/* 마켓 등록 버튼 */}
|
||||
{!asset.listed && asset.tradable && (
|
||||
{!isListed && asset.tradeable && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
{listingAsset === asset.id ? (
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
@@ -152,7 +152,8 @@ export default function AssetsTab() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,32 +4,36 @@ import { useToast } from '../toast/useToast';
|
||||
|
||||
export default function InventoryTab() {
|
||||
const toast = useToast();
|
||||
const [slots, setSlots] = useState([]);
|
||||
const [inventory, setInventory] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
getInventory()
|
||||
.then((data) => { if (!cancelled) setSlots(data); })
|
||||
.then((data) => { if (!cancelled) setInventory(data); })
|
||||
.catch(() => { if (!cancelled) toast.error('인벤토리를 불러오지 못했습니다.'); })
|
||||
.finally(() => { if (!cancelled) setLoading(false); });
|
||||
return () => { cancelled = true; };
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
if (loading) return <div className="wallet-spinner">불러오는 중...</div>;
|
||||
if (!slots || slots.length === 0) return <div className="wallet-empty">인벤토리가 비어있습니다</div>;
|
||||
|
||||
const slots = inventory?.slots;
|
||||
const entries = slots ? Object.entries(slots) : [];
|
||||
|
||||
if (entries.length === 0) return <div className="wallet-empty">인벤토리가 비어있습니다</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{slots.map((slot, i) => {
|
||||
const isEmpty = !slot.item_name && !slot.item_id;
|
||||
{entries.map(([slotName, assetId]) => {
|
||||
const isEmpty = !assetId;
|
||||
return (
|
||||
<div key={slot.slot_name || i} className={`inv-slot${isEmpty ? ' inv-slot-empty' : ''}`}>
|
||||
<div key={slotName} className={`inv-slot${isEmpty ? ' inv-slot-empty' : ''}`}>
|
||||
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: '0.85rem' }}>
|
||||
{slot.slot_name}
|
||||
{slotName}
|
||||
</span>
|
||||
<span style={{ color: isEmpty ? 'rgba(255,255,255,0.25)' : '#fff', fontSize: '0.9rem' }}>
|
||||
{isEmpty ? '비어있음' : `${slot.item_name} #${slot.item_id}`}
|
||||
{isEmpty ? '비어있음' : assetId}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { getMarketListings, getWallet, buyFromMarket, cancelListing } from '../../api/chain';
|
||||
import { getMarketListings, getListing, getWallet, buyFromMarket, cancelListing } from '../../api/chain';
|
||||
import { useToast } from '../toast/useToast';
|
||||
import { useConfirm } from '../confirm/useConfirm';
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function MarketTab() {
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
const [listings, setListings] = useState([]);
|
||||
const [myAddress, setMyAddress] = useState('');
|
||||
const [myPubKey, setMyPubKey] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filter, setFilter] = useState('all');
|
||||
const [processing, setProcessing] = useState(null);
|
||||
@@ -20,9 +20,20 @@ export default function MarketTab() {
|
||||
const load = useCallback(() => {
|
||||
setLoading(true);
|
||||
Promise.all([getMarketListings(), getWallet()])
|
||||
.then(([l, w]) => {
|
||||
setListings(l || []);
|
||||
setMyAddress(w?.address || '');
|
||||
.then(([result, w]) => {
|
||||
const ids = result?.ids || result;
|
||||
setMyPubKey(w?.pubKeyHex || '');
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
setListings([]);
|
||||
return;
|
||||
}
|
||||
return Promise.all(
|
||||
ids.map((id) =>
|
||||
getListing(id)
|
||||
.then((data) => ({ ...data, _loaded: true }))
|
||||
.catch(() => ({ id, _loaded: false }))
|
||||
)
|
||||
).then((items) => setListings(items.filter((l) => l._loaded)));
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('마켓 정보를 불러오지 못했습니다.');
|
||||
@@ -34,12 +45,12 @@ export default function MarketTab() {
|
||||
|
||||
const handleBuy = async (listing) => {
|
||||
const ok = await confirm(
|
||||
`${listing.item_name || '아이템'}을(를) ${Number(listing.price).toLocaleString()} TOL에 구매하시겠습니까?`
|
||||
`${listing.asset_id}을(를) ${Number(listing.price).toLocaleString()} TOL에 구매하시겠습니까?`
|
||||
);
|
||||
if (!ok) return;
|
||||
setProcessing(listing.listing_id);
|
||||
setProcessing(listing.id);
|
||||
try {
|
||||
await buyFromMarket(listing.listing_id);
|
||||
await buyFromMarket(listing.id);
|
||||
toast.success('구매가 완료되었습니다.');
|
||||
load();
|
||||
} catch (err) {
|
||||
@@ -52,9 +63,9 @@ export default function MarketTab() {
|
||||
const handleCancel = async (listing) => {
|
||||
const ok = await confirm('리스팅을 취소하시겠습니까?');
|
||||
if (!ok) return;
|
||||
setProcessing(listing.listing_id);
|
||||
setProcessing(listing.id);
|
||||
try {
|
||||
await cancelListing(listing.listing_id);
|
||||
await cancelListing(listing.id);
|
||||
toast.success('리스팅이 취소되었습니다.');
|
||||
load();
|
||||
} catch (err) {
|
||||
@@ -67,7 +78,7 @@ export default function MarketTab() {
|
||||
if (loading) return <div className="wallet-spinner">불러오는 중...</div>;
|
||||
|
||||
const filtered = filter === 'mine'
|
||||
? listings.filter((l) => l.seller === myAddress)
|
||||
? listings.filter((l) => l.seller === myPubKey)
|
||||
: listings;
|
||||
|
||||
return (
|
||||
@@ -93,11 +104,11 @@ export default function MarketTab() {
|
||||
</div>
|
||||
) : (
|
||||
filtered.map((listing) => {
|
||||
const isMine = listing.seller === myAddress;
|
||||
const isMine = listing.seller === myPubKey;
|
||||
return (
|
||||
<div key={listing.listing_id} className="market-item">
|
||||
<div key={listing.id} className="market-item">
|
||||
<div>
|
||||
<strong style={{ color: '#fff' }}>{listing.item_name || '아이템'}</strong>
|
||||
<strong style={{ color: '#fff' }}>{listing.asset_id}</strong>
|
||||
<span className="market-seller" style={{ marginLeft: 12 }}>
|
||||
{truncateAddr(listing.seller)}
|
||||
</span>
|
||||
@@ -107,18 +118,18 @@ export default function MarketTab() {
|
||||
{isMine ? (
|
||||
<button
|
||||
className="btn-danger-outline"
|
||||
disabled={processing === listing.listing_id}
|
||||
disabled={processing === listing.id}
|
||||
onClick={() => handleCancel(listing)}
|
||||
>
|
||||
{processing === listing.listing_id ? '처리 중...' : '취소'}
|
||||
{processing === listing.id ? '처리 중...' : '취소'}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="btn-primary"
|
||||
disabled={processing === listing.listing_id}
|
||||
disabled={processing === listing.id}
|
||||
onClick={() => handleBuy(listing)}
|
||||
>
|
||||
{processing === listing.listing_id ? '처리 중...' : '구매'}
|
||||
{processing === listing.id ? '처리 중...' : '구매'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -17,9 +17,11 @@ export default function WalletSummary() {
|
||||
getInventory().catch(() => null),
|
||||
]).then(([b, assets, inv]) => {
|
||||
setBalance(b?.balance != null ? b.balance : null);
|
||||
setAssetCount(assets != null ? assets.length : null);
|
||||
const ids = assets?.ids || assets;
|
||||
setAssetCount(Array.isArray(ids) ? ids.length : null);
|
||||
const slots = inv?.slots;
|
||||
setEquippedCount(
|
||||
inv != null ? inv.filter((s) => s.item_name || s.item_id).length : null
|
||||
slots != null ? Object.values(slots).filter(Boolean).length : null
|
||||
);
|
||||
}).finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function WalletTab() {
|
||||
setExporting(true);
|
||||
try {
|
||||
const data = await exportWalletKey(password);
|
||||
setPrivateKey(data.private_key);
|
||||
setPrivateKey(data.privateKey);
|
||||
} catch (err) {
|
||||
if (err.status === 401) {
|
||||
setExportError(err.message);
|
||||
@@ -90,8 +90,8 @@ export default function WalletTab() {
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div className="wallet-label">공개키</div>
|
||||
<div className="wallet-row">
|
||||
<span className="wallet-mono">{truncate(wallet.public_key, 4, 4)}</span>
|
||||
<button className="btn-copy" onClick={() => copyToClipboard(wallet.public_key, '공개키')}>
|
||||
<span className="wallet-mono">{truncate(wallet.pubKeyHex, 4, 4)}</span>
|
||||
<button className="btn-copy" onClick={() => copyToClipboard(wallet.pubKeyHex, '공개키')}>
|
||||
복사
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user