From b2c5d1ab38ac02c0be7397d9b9b78c73ebce67f7 Mon Sep 17 00:00:00 2001 From: kimjaehyeon0101 <47347352-kimjaehyeon0101@users.noreply.replit.com> Date: Mon, 29 Sep 2025 18:56:57 +0000 Subject: [PATCH] Add a page for media outlets to auction administrative rights Adds a new route and component for media outlet auctions, allowing users to bid on administrative rights. 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/jvFIdY3 --- client/src/App.tsx | 2 + client/src/pages/MediaOutletAuction.tsx | 396 ++++++++++++++++++++++++ 2 files changed, 398 insertions(+) create mode 100644 client/src/pages/MediaOutletAuction.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 09e222c..76b4526 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -12,6 +12,7 @@ import AdminDashboard from "@/pages/AdminDashboard"; import SuperAdminDashboard from "@/pages/SuperAdminDashboard"; import Auctions from "@/pages/Auctions"; import AuctionGuide from "@/pages/AuctionGuide"; +import MediaOutletAuction from "@/pages/MediaOutletAuction"; import NotFound from "@/pages/not-found"; function Router() { @@ -20,6 +21,7 @@ function Router() { return ( + diff --git a/client/src/pages/MediaOutletAuction.tsx b/client/src/pages/MediaOutletAuction.tsx new file mode 100644 index 0000000..f1e0c72 --- /dev/null +++ b/client/src/pages/MediaOutletAuction.tsx @@ -0,0 +1,396 @@ +import { useState } from "react"; +import { useQuery, useMutation } from "@tanstack/react-query"; +import { useRoute, useLocation } from "wouter"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { useToast } from "@/hooks/use-toast"; +import { queryClient, apiRequest } from "@/lib/queryClient"; +import { useAuth } from "@/hooks/useAuth"; +import { Clock, Gavel, TrendingUp, User } from "lucide-react"; +import type { MediaOutlet, Auction, Bid } from "@shared/schema"; + +export default function MediaOutletAuction() { + const [, params] = useRoute("/media/:slug/auction"); + const [, setLocation] = useLocation(); + const [bidAmount, setBidAmount] = useState(""); + const [qualityScore, setQualityScore] = useState(""); + const { user, isAuthenticated } = useAuth(); + const { toast } = useToast(); + + const { data: outlet, isLoading: outletLoading } = useQuery({ + queryKey: ["/api/media-outlets", params?.slug], + enabled: !!params?.slug + }); + + const { data: auction, isLoading: auctionLoading } = useQuery({ + queryKey: ["/api/media-outlets", params?.slug, "auction"], + enabled: !!params?.slug + }); + + const placeBidMutation = useMutation({ + mutationFn: async (bidData: { amount: number; qualityScore?: number }) => { + return apiRequest(`/api/media-outlets/${params?.slug}/auction/bids`, { + method: "POST", + body: JSON.stringify(bidData), + }); + }, + onSuccess: () => { + toast({ + title: "입찰 성공", + description: "입찰이 성공적으로 등록되었습니다.", + }); + setBidAmount(""); + setQualityScore(""); + queryClient.invalidateQueries({ queryKey: ["/api/media-outlets", params?.slug, "auction"] }); + }, + onError: (error: any) => { + toast({ + title: "입찰 실패", + description: error.message || "입찰 중 오류가 발생했습니다.", + variant: "destructive", + }); + }, + }); + + const handlePlaceBid = () => { + if (!bidAmount) { + toast({ + title: "입찰 금액 필요", + description: "입찰 금액을 입력해주세요.", + variant: "destructive", + }); + return; + } + + const amount = parseFloat(bidAmount); + if (isNaN(amount) || amount <= 0) { + toast({ + title: "유효하지 않은 금액", + description: "올바른 입찰 금액을 입력해주세요.", + variant: "destructive", + }); + return; + } + + if (auction && amount <= auction.currentBid) { + toast({ + title: "입찰 금액 부족", + description: `현재 최고 입찰가(${auction.currentBid}원)보다 높은 금액을 입력해주세요.`, + variant: "destructive", + }); + return; + } + + const bidData: { amount: number; qualityScore?: number } = { amount }; + if (qualityScore) { + const score = parseFloat(qualityScore); + if (!isNaN(score) && score >= 0 && score <= 100) { + bidData.qualityScore = score; + } + } + + placeBidMutation.mutate(bidData); + }; + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: 'KRW' + }).format(amount); + }; + + const formatTimeRemaining = (endDate: string) => { + const end = new Date(endDate); + const now = new Date(); + const diff = end.getTime() - now.getTime(); + + if (diff <= 0) return "경매 종료"; + + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + + if (days > 0) return `${days}일 ${hours}시간 남음`; + if (hours > 0) return `${hours}시간 ${minutes}분 남음`; + return `${minutes}분 남음`; + }; + + if (outletLoading || auctionLoading) { + return ( +
+
+
+
+
+
+
+
+
+ ); + } + + if (!outlet) { + return ( +
+
+

언론매체를 찾을 수 없습니다

+

요청하신 언론매체가 존재하지 않습니다.

+ +
+
+ ); + } + + if (!auction) { + return ( +
+
+
+
+
+
+ S +
+ SAPIENS +
+ +
+ + +
+
+
+
+ +
+
+

진행 중인 경매가 없습니다

+

+ {outlet.name}의 관리자 권한에 대한 경매가 현재 진행되고 있지 않습니다. +

+ +
+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+
+
+ S +
+ SAPIENS +
+ +
+ + +
+
+
+
+ +
+ {/* Outlet Header */} +
+
+ {outlet.imageUrl ? ( + {outlet.name} + ) : ( +
+ + {outlet.name.charAt(0)} + +
+ )} + +
+

{outlet.name} 관리자 권한 경매

+

+ {outlet.description} +

+
+ + {outlet.category} + + {outlet.tags?.map((tag) => ( + + {tag} + + ))} +
+
+
+
+ +
+ {/* Auction Information */} + + + + + 경매 정보 + + + +
+
+
+ + 현재 최고가 +
+ + {formatCurrency(auction.currentBid)} + +
+ +
+
+ + 남은 시간 +
+ + {formatTimeRemaining(auction.endDate)} + +
+ + {auction.highestBidder && ( +
+
+ + 최고 입찰자 +
+ + {auction.highestBidder.slice(0, 3)}*** + +
+ )} + + {auction.qualityScore && ( +
+
+ 품질 점수 +
+ + {auction.qualityScore}/100 + +
+ )} +
+
+
+ + {/* Bidding Form */} + + + 입찰하기 + + + {!isAuthenticated ? ( +
+

+ 입찰하려면 로그인이 필요합니다. +

+ +
+ ) : ( +
+
+ + setBidAmount(e.target.value)} + placeholder={`${auction.currentBid + 1000} 이상`} + min={auction.currentBid + 1} + data-testid="input-bid-amount" + /> +
+ +
+ + setQualityScore(e.target.value)} + placeholder="품질 점수를 입력하세요" + min={0} + max={100} + data-testid="input-quality-score" + /> +
+ + + +

+ * 입찰이 성공하면 취소할 수 없습니다. +

+
+ )} +
+
+
+ + {/* Auction Description */} + + + 경매 안내 + + +
+

+ 이 경매는 {outlet.name}의 관리자 권한을 획득하기 위한 경매입니다. + 관리자 권한을 획득하면 다음과 같은 기능을 사용할 수 있습니다: +

+
    +
  • 언론매체의 기사 관리 및 편집
  • +
  • 예측시장 이벤트 생성 및 관리
  • +
  • 언론매체 프로필 정보 수정
  • +
  • 구독자 및 팔로워 관리
  • +
+

+ 경매는 {formatTimeRemaining(auction.endDate)} 후에 종료됩니다. + 최고 입찰자가 관리자 권한을 획득하게 됩니다. +

+
+
+
+
+
+ ); +} \ No newline at end of file