From 1691638fe9c53fced1967e79f280a79b92170c10 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Mon, 23 Mar 2026 14:41:07 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=A7=80=EA=B0=91=20UI=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=EB=AA=85/=EC=9D=91=EB=8B=B5=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=B6=88=EC=9D=BC=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 서버 실제 응답과 프론트엔드 필드명이 맞지 않아 공개키 미표시, 인벤토리/마켓 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 --- src/api/chain.js | 4 + src/components/wallet/AssetsTab.jsx | 173 ++++++++++++------------ src/components/wallet/InventoryTab.jsx | 20 +-- src/components/wallet/MarketTab.jsx | 47 ++++--- src/components/wallet/WalletSummary.jsx | 6 +- src/components/wallet/WalletTab.jsx | 6 +- 6 files changed, 139 insertions(+), 117 deletions(-) diff --git a/src/api/chain.js b/src/api/chain.js index f636203..5fc018a 100644 --- a/src/api/chain.js +++ b/src/api/chain.js @@ -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', diff --git a/src/components/wallet/AssetsTab.jsx b/src/components/wallet/AssetsTab.jsx index 1a6ec47..c326a4c 100644 --- a/src/components/wallet/AssetsTab.jsx +++ b/src/components/wallet/AssetsTab.jsx @@ -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,94 +66,94 @@ export default function AssetsTab() { return (
- {assets.map((asset) => ( -
-
toggleExpand(asset.id)}> -
- {asset._loaded ? ( - <> - {asset.item_name || asset.template_name || '자산'} - {asset.template_name && asset.item_name !== asset.template_name && ( - {asset.template_name} - )} - #{asset.id} - - ) : ( - 로드 실패 (#{asset.id}) - )} -
- {asset._loaded && ( - - {asset.tradable ? '거래 가능' : '거래 불가'} - - )} -
- - {expanded === asset.id && asset._loaded && ( -
- {/* 속성 */} - {asset.properties && Object.keys(asset.properties).length > 0 && ( -
- {Object.entries(asset.properties).map(([k, v]) => ( - - {k} - {String(v)} - - ))} -
- )} - - {/* 마켓 등록 상태 */} -
- {asset.listed ? ( - 마켓 등록됨 + {assets.map((asset) => { + const isListed = !!asset.active_listing_id; + return ( +
+
toggleExpand(asset.id)}> +
+ {asset._loaded ? ( + <> + {asset.template_id || '자산'} + #{asset.id} + ) : ( - 미등록 + 로드 실패 (#{asset.id}) )}
- - {/* 마켓 등록 버튼 */} - {!asset.listed && asset.tradable && ( -
- {listingAsset === asset.id ? ( -
- setListingPrice(e.target.value)} - min="1" - style={{ width: 140 }} - /> - - -
- ) : ( - - )} -
+ {asset._loaded && ( + + {asset.tradeable ? '거래 가능' : '거래 불가'} + )}
- )} -
- ))} + + {expanded === asset.id && asset._loaded && ( +
+ {/* 속성 */} + {asset.properties && Object.keys(asset.properties).length > 0 && ( +
+ {Object.entries(asset.properties).map(([k, v]) => ( + + {k} + {String(v)} + + ))} +
+ )} + + {/* 마켓 등록 상태 */} +
+ {isListed ? ( + 마켓 등록됨 + ) : ( + 미등록 + )} +
+ + {/* 마켓 등록 버튼 */} + {!isListed && asset.tradeable && ( +
+ {listingAsset === asset.id ? ( +
+ setListingPrice(e.target.value)} + min="1" + style={{ width: 140 }} + /> + + +
+ ) : ( + + )} +
+ )} +
+ )} +
+ ); + })}
); } diff --git a/src/components/wallet/InventoryTab.jsx b/src/components/wallet/InventoryTab.jsx index c49d407..710c53f 100644 --- a/src/components/wallet/InventoryTab.jsx +++ b/src/components/wallet/InventoryTab.jsx @@ -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
불러오는 중...
; - if (!slots || slots.length === 0) return
인벤토리가 비어있습니다
; + + const slots = inventory?.slots; + const entries = slots ? Object.entries(slots) : []; + + if (entries.length === 0) return
인벤토리가 비어있습니다
; return (
- {slots.map((slot, i) => { - const isEmpty = !slot.item_name && !slot.item_id; + {entries.map(([slotName, assetId]) => { + const isEmpty = !assetId; return ( -
+
- {slot.slot_name} + {slotName} - {isEmpty ? '비어있음' : `${slot.item_name} #${slot.item_id}`} + {isEmpty ? '비어있음' : assetId}
); diff --git a/src/components/wallet/MarketTab.jsx b/src/components/wallet/MarketTab.jsx index b277701..aa7bdc9 100644 --- a/src/components/wallet/MarketTab.jsx +++ b/src/components/wallet/MarketTab.jsx @@ -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
불러오는 중...
; 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() {
) : ( filtered.map((listing) => { - const isMine = listing.seller === myAddress; + const isMine = listing.seller === myPubKey; return ( -
+
- {listing.item_name || '아이템'} + {listing.asset_id} {truncateAddr(listing.seller)} @@ -107,18 +118,18 @@ export default function MarketTab() { {isMine ? ( ) : ( )}
diff --git a/src/components/wallet/WalletSummary.jsx b/src/components/wallet/WalletSummary.jsx index 3175aae..01d283c 100644 --- a/src/components/wallet/WalletSummary.jsx +++ b/src/components/wallet/WalletSummary.jsx @@ -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)); }, []); diff --git a/src/components/wallet/WalletTab.jsx b/src/components/wallet/WalletTab.jsx index bf20a8d..41d38ab 100644 --- a/src/components/wallet/WalletTab.jsx +++ b/src/components/wallet/WalletTab.jsx @@ -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() {
공개키
- {truncate(wallet.public_key, 4, 4)} -