Display bid history for media outlet auctions and fetch bid data
Add a new API endpoint and integrate bid history display into the media outlet auction page. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 069d4324-6c40-4355-955e-c714a50de1ea Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3df548ff-50ae-432f-9be4-25d34eccc983/069d4324-6c40-4355-955e-c714a50de1ea/6XTzcDL
This commit is contained in:
@ -30,6 +30,11 @@ export default function MediaOutletAuction() {
|
|||||||
enabled: !!params?.slug
|
enabled: !!params?.slug
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: bids = [], isLoading: bidsLoading } = useQuery<Bid[]>({
|
||||||
|
queryKey: ["/api/auctions", auction?.id, "bids"],
|
||||||
|
enabled: !!auction?.id
|
||||||
|
});
|
||||||
|
|
||||||
const placeBidMutation = useMutation({
|
const placeBidMutation = useMutation({
|
||||||
mutationFn: async (bidData: { amount: number; qualityScore?: number }) => {
|
mutationFn: async (bidData: { amount: number; qualityScore?: number }) => {
|
||||||
return apiRequest("POST", `/api/media-outlets/${params?.slug}/auction/bids`, bidData);
|
return apiRequest("POST", `/api/media-outlets/${params?.slug}/auction/bids`, bidData);
|
||||||
@ -42,6 +47,7 @@ export default function MediaOutletAuction() {
|
|||||||
setBidAmount("");
|
setBidAmount("");
|
||||||
setQualityScore("");
|
setQualityScore("");
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/media-outlets", params?.slug, "auction"] });
|
queryClient.invalidateQueries({ queryKey: ["/api/media-outlets", params?.slug, "auction"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/auctions", auction?.id, "bids"] });
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
toast({
|
toast({
|
||||||
@ -363,6 +369,88 @@ export default function MediaOutletAuction() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Bid History */}
|
||||||
|
<Card className="mt-8">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center space-x-2">
|
||||||
|
<TrendingUp className="h-5 w-5" />
|
||||||
|
<span>입찰 내역</span>
|
||||||
|
<Badge variant="outline">{bids.length}건</Badge>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{bidsLoading ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
|
<div key={i} className="animate-pulse flex justify-between items-center p-3 border rounded">
|
||||||
|
<div className="h-4 bg-gray-300 rounded w-1/3"></div>
|
||||||
|
<div className="h-4 bg-gray-300 rounded w-1/4"></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : bids.length > 0 ? (
|
||||||
|
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||||
|
{(() => {
|
||||||
|
// Sort bids by time (newest first)
|
||||||
|
const sortedBids = [...bids].sort((a, b) =>
|
||||||
|
new Date(b.createdAt!).getTime() - new Date(a.createdAt!).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find highest bid by amount
|
||||||
|
const highestBid = bids.reduce((highest, current) =>
|
||||||
|
parseFloat(current.amount) > parseFloat(highest.amount) ? current : highest
|
||||||
|
, bids[0]);
|
||||||
|
|
||||||
|
return sortedBids.map((bid, index) => (
|
||||||
|
<div
|
||||||
|
key={bid.id}
|
||||||
|
className={`flex justify-between items-center p-4 border rounded-lg ${
|
||||||
|
index === 0 ? 'bg-green-50 border-green-200' : 'bg-gray-50 border-gray-200'
|
||||||
|
}`}
|
||||||
|
data-testid={`bid-item-${bid.id}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="font-bold text-lg" data-testid={`bid-amount-${bid.id}`}>
|
||||||
|
{formatCurrency(parseFloat(bid.amount))}
|
||||||
|
</span>
|
||||||
|
{bid.id === highestBid.id && (
|
||||||
|
<Badge className="bg-green-600">최고가</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
입찰자: <span data-testid={`bid-bidder-${bid.id}`}>{bid.bidderId.slice(0, 8)}***</span>
|
||||||
|
{bid.qualityScore !== undefined && bid.qualityScore !== null && (
|
||||||
|
<span className="ml-2">품질점수: {bid.qualityScore}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right text-sm text-muted-foreground">
|
||||||
|
<span data-testid={`bid-date-${bid.id}`}>
|
||||||
|
{new Date(bid.createdAt!).toLocaleDateString('ko-KR', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
<TrendingUp className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||||
|
<p>아직 입찰 내역이 없습니다</p>
|
||||||
|
<p className="text-sm">첫 번째 입찰자가 되어보세요!</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Auction Description */}
|
{/* Auction Description */}
|
||||||
<Card className="mt-8">
|
<Card className="mt-8">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@ -168,6 +168,16 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/api/auctions/:id/bids', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const bids = await storage.getBidsByAuctionId(req.params.id);
|
||||||
|
res.json(bids);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching bids:", error);
|
||||||
|
res.status(500).json({ message: "Failed to fetch bids" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/api/auctions/:id/bid', isAuthenticated, async (req: any, res) => {
|
app.post('/api/auctions/:id/bid', isAuthenticated, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.user.claims.sub;
|
const userId = req.user.claims.sub;
|
||||||
|
|||||||
@ -59,6 +59,7 @@ export interface IStorage {
|
|||||||
getAuctionByMediaOutlet(mediaOutletId: string): Promise<Auction | undefined>;
|
getAuctionByMediaOutlet(mediaOutletId: string): Promise<Auction | undefined>;
|
||||||
createAuction(auction: InsertAuction): Promise<Auction>;
|
createAuction(auction: InsertAuction): Promise<Auction>;
|
||||||
placeBid(bid: InsertBid): Promise<Bid>;
|
placeBid(bid: InsertBid): Promise<Bid>;
|
||||||
|
getBidsByAuctionId(auctionId: string): Promise<Bid[]>;
|
||||||
|
|
||||||
// Media outlet request operations
|
// Media outlet request operations
|
||||||
getMediaOutletRequests(status?: string): Promise<MediaOutletRequest[]>;
|
getMediaOutletRequests(status?: string): Promise<MediaOutletRequest[]>;
|
||||||
@ -251,6 +252,14 @@ export class DatabaseStorage implements IStorage {
|
|||||||
|
|
||||||
return newBid;
|
return newBid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getBidsByAuctionId(auctionId: string): Promise<Bid[]> {
|
||||||
|
return await db
|
||||||
|
.select()
|
||||||
|
.from(bids)
|
||||||
|
.where(eq(bids.auctionId, auctionId))
|
||||||
|
.orderBy(desc(bids.createdAt));
|
||||||
|
}
|
||||||
|
|
||||||
// Media outlet request operations
|
// Media outlet request operations
|
||||||
async getMediaOutletRequests(status?: string): Promise<MediaOutletRequest[]> {
|
async getMediaOutletRequests(status?: string): Promise<MediaOutletRequest[]> {
|
||||||
|
|||||||
Reference in New Issue
Block a user