Translate all user-facing text to English and update UI elements
This commit addresses the user's request to translate all visible text within the application to English. It also updates the login modal to display the SAPIENS logo correctly. Additionally, various UI components and messages related to articles, community posts, auctions, and login/error handling have been localized. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9a264234-c5d7-4dcc-adf3-a954b149b30d Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3df548ff-50ae-432f-9be4-25d34eccc983/9a264234-c5d7-4dcc-adf3-a954b149b30d/CMG42YQ
This commit is contained in:
@ -34,8 +34,8 @@ export default function LoginModal({ isOpen, onClose }: LoginModalProps) {
|
||||
if (response.ok) {
|
||||
const user = await response.json();
|
||||
toast({
|
||||
title: "로그인 성공",
|
||||
description: `환영합니다, ${user.firstName}님!`,
|
||||
title: "Login Successful",
|
||||
description: `Welcome, ${user.firstName}!`,
|
||||
});
|
||||
|
||||
// Invalidate auth queries to refresh user state
|
||||
@ -48,15 +48,15 @@ export default function LoginModal({ isOpen, onClose }: LoginModalProps) {
|
||||
} else {
|
||||
const error = await response.json();
|
||||
toast({
|
||||
title: "로그인 실패",
|
||||
description: error.message || "잘못된 아이디 또는 비밀번호입니다.",
|
||||
title: "Login Failed",
|
||||
description: error.message || "Invalid username or password.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "로그인 오류",
|
||||
description: "로그인 중 오류가 발생했습니다.",
|
||||
title: "Login Error",
|
||||
description: "An error occurred during login.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
@ -79,7 +79,7 @@ export default function LoginModal({ isOpen, onClose }: LoginModalProps) {
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username">아이디</Label>
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Input
|
||||
id="username"
|
||||
type="text"
|
||||
@ -87,13 +87,13 @@ export default function LoginModal({ isOpen, onClose }: LoginModalProps) {
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
onInvalid={(e) => (e.target as HTMLInputElement).setCustomValidity('Please enter your username')}
|
||||
onInput={(e) => (e.target as HTMLInputElement).setCustomValidity('')}
|
||||
placeholder="아이디를 입력하세요"
|
||||
placeholder="Enter your username"
|
||||
required
|
||||
data-testid="input-username"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">비밀번호</Label>
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
@ -101,7 +101,7 @@ export default function LoginModal({ isOpen, onClose }: LoginModalProps) {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onInvalid={(e) => (e.target as HTMLInputElement).setCustomValidity('Please enter your password')}
|
||||
onInput={(e) => (e.target as HTMLInputElement).setCustomValidity('')}
|
||||
placeholder="비밀번호를 입력하세요"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
data-testid="input-password"
|
||||
/>
|
||||
@ -113,7 +113,7 @@ export default function LoginModal({ isOpen, onClose }: LoginModalProps) {
|
||||
disabled={isLoading}
|
||||
data-testid="button-login-submit"
|
||||
>
|
||||
{isLoading ? "로그인 중..." : "로그인"}
|
||||
{isLoading ? "Logging in..." : "Login"}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@ -122,7 +122,7 @@ export default function LoginModal({ isOpen, onClose }: LoginModalProps) {
|
||||
disabled={isLoading}
|
||||
data-testid="button-login-cancel"
|
||||
>
|
||||
취소
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -99,8 +99,8 @@ export default function Article() {
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string | Date | null) => {
|
||||
if (!dateString) return '미정';
|
||||
return new Intl.DateTimeFormat('ko-KR', {
|
||||
if (!dateString) return 'TBD';
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
@ -179,8 +179,8 @@ export default function Article() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold mb-4">기사를 찾을 수 없습니다</h1>
|
||||
<Button onClick={() => setLocation("/")}>홈으로 돌아가기</Button>
|
||||
<h1 className="text-2xl font-bold mb-4">Article Not Found</h1>
|
||||
<Button onClick={() => setLocation("/")}>Return Home</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -78,16 +78,16 @@ export default function Community() {
|
||||
const diffInMs = now.getTime() - postDate.getTime();
|
||||
const diffInMinutes = Math.floor(diffInMs / 60000);
|
||||
|
||||
if (diffInMinutes < 1) return "방금";
|
||||
if (diffInMinutes < 60) return `${diffInMinutes}분 전`;
|
||||
if (diffInMinutes < 1) return "just now";
|
||||
if (diffInMinutes < 60) return `${diffInMinutes}m ago`;
|
||||
|
||||
const diffInHours = Math.floor(diffInMinutes / 60);
|
||||
if (diffInHours < 24) return `${diffInHours}시간 전`;
|
||||
if (diffInHours < 24) return `${diffInHours}h ago`;
|
||||
|
||||
const diffInDays = Math.floor(diffInHours / 24);
|
||||
if (diffInDays < 30) return `${diffInDays}일 전`;
|
||||
if (diffInDays < 30) return `${diffInDays}d ago`;
|
||||
|
||||
return postDate.toLocaleDateString('ko-KR');
|
||||
return postDate.toLocaleDateString('en-US');
|
||||
};
|
||||
|
||||
const filteredPosts = posts.filter(post =>
|
||||
@ -104,14 +104,14 @@ export default function Community() {
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">로딩 중...</p>
|
||||
<p className="text-gray-600">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!outlet) {
|
||||
return <div>미디어 아울렛을 찾을 수 없습니다</div>;
|
||||
return <div>Media outlet not found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -232,9 +232,9 @@ export default function Community() {
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900" data-testid="text-community-title">
|
||||
{outlet.name} 커뮤니티
|
||||
{outlet.name} Community
|
||||
</h1>
|
||||
<p className="text-sm text-gray-600 mt-1">자유롭게 의견을 나눠보세요</p>
|
||||
<p className="text-sm text-gray-600 mt-1">Share your thoughts freely</p>
|
||||
</div>
|
||||
|
||||
<Dialog open={isNewPostOpen} onOpenChange={setIsNewPostOpen}>
|
||||
@ -248,12 +248,12 @@ export default function Community() {
|
||||
}}
|
||||
data-testid="button-new-post"
|
||||
>
|
||||
글쓰기
|
||||
Write Post
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl" data-testid="dialog-new-post">
|
||||
<DialogHeader>
|
||||
<DialogTitle>새 글 작성</DialogTitle>
|
||||
<DialogTitle>Create New Post</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
@ -268,7 +268,7 @@ export default function Community() {
|
||||
<div>
|
||||
<Input
|
||||
name="title"
|
||||
placeholder="제목을 입력하세요"
|
||||
placeholder="Enter title"
|
||||
required
|
||||
data-testid="input-post-title"
|
||||
/>
|
||||
@ -276,7 +276,7 @@ export default function Community() {
|
||||
<div>
|
||||
<Textarea
|
||||
name="content"
|
||||
placeholder="내용을 입력하세요"
|
||||
placeholder="Enter content"
|
||||
rows={10}
|
||||
required
|
||||
data-testid="textarea-post-content"
|
||||
@ -285,16 +285,16 @@ export default function Community() {
|
||||
<div>
|
||||
<Input
|
||||
name="imageUrl"
|
||||
placeholder="이미지 URL (선택사항)"
|
||||
placeholder="Image URL (optional)"
|
||||
data-testid="input-post-image-url"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button type="button" variant="outline" onClick={() => setIsNewPostOpen(false)} data-testid="button-cancel-post">
|
||||
취소
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={createPostMutation.isPending} data-testid="button-submit-post">
|
||||
{createPostMutation.isPending ? '작성 중...' : '작성하기'}
|
||||
{createPostMutation.isPending ? 'Creating...' : 'Create Post'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -311,10 +311,10 @@ export default function Community() {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="latest">최신순</SelectItem>
|
||||
<SelectItem value="views">조회순</SelectItem>
|
||||
<SelectItem value="likes">추천순</SelectItem>
|
||||
<SelectItem value="replies">댓글순</SelectItem>
|
||||
<SelectItem value="latest">Latest</SelectItem>
|
||||
<SelectItem value="views">Most Views</SelectItem>
|
||||
<SelectItem value="likes">Most Likes</SelectItem>
|
||||
<SelectItem value="replies">Most Replies</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@ -323,7 +323,7 @@ export default function Community() {
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="검색"
|
||||
placeholder="Search"
|
||||
className="w-64 pl-10"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
@ -336,7 +336,7 @@ export default function Community() {
|
||||
{postsLoading ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">로딩 중...</p>
|
||||
<p className="text-gray-600">Loading...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
@ -360,13 +360,13 @@ export default function Community() {
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
{post.isNotice && (
|
||||
<Badge variant="destructive" className="text-xs" data-testid={`badge-notice-${post.id}`}>
|
||||
공지
|
||||
Notice
|
||||
</Badge>
|
||||
)}
|
||||
{post.isPinned && !post.isNotice && (
|
||||
<Badge variant="secondary" className="text-xs" data-testid={`badge-pin-${post.id}`}>
|
||||
<Pin className="h-3 w-3 mr-1" />
|
||||
고정
|
||||
Pinned
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@ -437,7 +437,7 @@ export default function Community() {
|
||||
|
||||
{filteredPosts.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500">아직 작성된 글이 없습니다.</p>
|
||||
<p className="text-gray-500">No posts yet.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -70,16 +70,16 @@ export default function CommunityPostPage() {
|
||||
const diffInMs = now.getTime() - postDate.getTime();
|
||||
const diffInMinutes = Math.floor(diffInMs / 60000);
|
||||
|
||||
if (diffInMinutes < 1) return "방금";
|
||||
if (diffInMinutes < 60) return `${diffInMinutes}분 전`;
|
||||
if (diffInMinutes < 1) return "just now";
|
||||
if (diffInMinutes < 60) return `${diffInMinutes}m ago`;
|
||||
|
||||
const diffInHours = Math.floor(diffInMinutes / 60);
|
||||
if (diffInHours < 24) return `${diffInHours}시간 전`;
|
||||
if (diffInHours < 24) return `${diffInHours}h ago`;
|
||||
|
||||
const diffInDays = Math.floor(diffInHours / 24);
|
||||
if (diffInDays < 30) return `${diffInDays}일 전`;
|
||||
if (diffInDays < 30) return `${diffInDays}d ago`;
|
||||
|
||||
return postDate.toLocaleDateString('ko-KR');
|
||||
return postDate.toLocaleDateString('en-US');
|
||||
};
|
||||
|
||||
const canDeletePost = () => {
|
||||
@ -92,14 +92,14 @@ export default function CommunityPostPage() {
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">로딩 중...</p>
|
||||
<p className="text-gray-600">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!post) {
|
||||
return <div>게시글을 찾을 수 없습니다</div>;
|
||||
return <div>Post not found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -114,7 +114,7 @@ export default function CommunityPostPage() {
|
||||
data-testid="button-back"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
커뮤니티로 돌아가기
|
||||
Back to Community
|
||||
</Button>
|
||||
|
||||
{outlet && (
|
||||
@ -144,12 +144,12 @@ export default function CommunityPostPage() {
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
{post.isNotice && (
|
||||
<Badge variant="destructive" className="text-xs" data-testid="badge-notice">
|
||||
공지
|
||||
Notice
|
||||
</Badge>
|
||||
)}
|
||||
{post.isPinned && !post.isNotice && (
|
||||
<Badge variant="secondary" className="text-xs" data-testid="badge-pin">
|
||||
고정
|
||||
Pinned
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@ -217,7 +217,7 @@ export default function CommunityPostPage() {
|
||||
data-testid="button-like"
|
||||
>
|
||||
<ThumbsUp className="h-4 w-4 mr-2" />
|
||||
추천 ({post.likeCount})
|
||||
Like ({post.likeCount})
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@ -226,7 +226,7 @@ export default function CommunityPostPage() {
|
||||
{/* Replies Section */}
|
||||
<div className="mb-6">
|
||||
<h2 className="text-lg font-semibold mb-4" data-testid="text-replies-title">
|
||||
댓글 {post.replyCount}개
|
||||
{post.replyCount} Replies
|
||||
</h2>
|
||||
|
||||
{/* Reply Form */}
|
||||
@ -234,7 +234,7 @@ export default function CommunityPostPage() {
|
||||
<Card className="mb-6">
|
||||
<CardContent className="p-4">
|
||||
<Textarea
|
||||
placeholder="댓글을 입력하세요"
|
||||
placeholder="Write a reply"
|
||||
value={replyContent}
|
||||
onChange={(e) => setReplyContent(e.target.value)}
|
||||
rows={3}
|
||||
@ -247,7 +247,7 @@ export default function CommunityPostPage() {
|
||||
disabled={!replyContent.trim() || createReplyMutation.isPending}
|
||||
data-testid="button-submit-reply"
|
||||
>
|
||||
{createReplyMutation.isPending ? '작성 중...' : '댓글 작성'}
|
||||
{createReplyMutation.isPending ? 'Posting...' : 'Post Reply'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@ -255,7 +255,7 @@ export default function CommunityPostPage() {
|
||||
) : (
|
||||
<Card className="mb-6">
|
||||
<CardContent className="p-4 text-center text-gray-500">
|
||||
댓글을 작성하려면 로그인이 필요합니다.
|
||||
Please login to write a reply.
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
@ -264,7 +264,7 @@ export default function CommunityPostPage() {
|
||||
{repliesLoading ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-3"></div>
|
||||
<p className="text-gray-600 text-sm">댓글 로딩 중...</p>
|
||||
<p className="text-gray-600 text-sm">Loading replies...</p>
|
||||
</div>
|
||||
) : replies.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
@ -283,7 +283,7 @@ export default function CommunityPostPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500">첫 번째 댓글을 작성해보세요!</p>
|
||||
<p className="text-gray-500">Be the first to reply!</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -238,7 +238,14 @@ export default function Landing() {
|
||||
<Dialog open={showLoginModal} onOpenChange={setShowLoginModal}>
|
||||
<DialogContent className="sm:max-w-md" data-testid="modal-login">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Login to SAPIENS</DialogTitle>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<span>Login to</span>
|
||||
<img
|
||||
src="/attached_assets/logo_black_1759181850935.png"
|
||||
alt="SAPIENS"
|
||||
className="h-4 w-auto"
|
||||
/>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleLoginSubmit} className="space-y-4">
|
||||
<div>
|
||||
|
||||
@ -45,8 +45,8 @@ export default function MediaOutletAuction() {
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: "입찰 성공",
|
||||
description: "입찰이 성공적으로 등록되었습니다.",
|
||||
title: "Bid Successful",
|
||||
description: "Your bid has been successfully placed.",
|
||||
});
|
||||
setBidAmount("");
|
||||
setQualityScore("");
|
||||
@ -55,8 +55,8 @@ export default function MediaOutletAuction() {
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({
|
||||
title: "입찰 실패",
|
||||
description: error.message || "입찰 중 오류가 발생했습니다.",
|
||||
title: "Bid Failed",
|
||||
description: error.message || "An error occurred while placing your bid.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
@ -65,8 +65,8 @@ export default function MediaOutletAuction() {
|
||||
const handlePlaceBid = () => {
|
||||
if (!bidAmount) {
|
||||
toast({
|
||||
title: "입찰 금액 필요",
|
||||
description: "입찰 금액을 입력해주세요.",
|
||||
title: "Bid Amount Required",
|
||||
description: "Please enter a bid amount.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
@ -75,8 +75,8 @@ export default function MediaOutletAuction() {
|
||||
const amount = parseFloat(bidAmount);
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
toast({
|
||||
title: "유효하지 않은 금액",
|
||||
description: "올바른 입찰 금액을 입력해주세요.",
|
||||
title: "Invalid Amount",
|
||||
description: "Please enter a valid bid amount.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
@ -84,8 +84,8 @@ export default function MediaOutletAuction() {
|
||||
|
||||
if (auction && auction.currentBid && amount <= parseFloat(auction.currentBid)) {
|
||||
toast({
|
||||
title: "입찰 금액 부족",
|
||||
description: `현재 최고 입찰가(${auction.currentBid}원)보다 높은 금액을 입력해주세요.`,
|
||||
title: "Bid Too Low",
|
||||
description: `Please enter an amount higher than the current bid (₩${auction.currentBid}).`,
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
@ -114,15 +114,15 @@ export default function MediaOutletAuction() {
|
||||
const now = new Date();
|
||||
const diff = end.getTime() - now.getTime();
|
||||
|
||||
if (diff <= 0) return "경매 종료";
|
||||
if (diff <= 0) return "Auction Ended";
|
||||
|
||||
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 (days > 0) return `${days}d ${hours}h left`;
|
||||
if (hours > 0) return `${hours}h ${minutes}m left`;
|
||||
return `${minutes}m left`;
|
||||
};
|
||||
|
||||
if (outletLoading || auctionLoading) {
|
||||
@ -143,9 +143,9 @@ export default function MediaOutletAuction() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold mb-2">언론매체를 찾을 수 없습니다</h1>
|
||||
<p className="text-muted-foreground mb-4">요청하신 언론매체가 존재하지 않습니다.</p>
|
||||
<Button onClick={() => setLocation("/")}>홈으로 가기</Button>
|
||||
<h1 className="text-2xl font-bold mb-2">Media Outlet Not Found</h1>
|
||||
<p className="text-muted-foreground mb-4">The requested media outlet does not exist.</p>
|
||||
<Button onClick={() => setLocation("/")}>Go Home</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -166,10 +166,10 @@ export default function MediaOutletAuction() {
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button variant="ghost" onClick={() => setLocation(`/media/${params?.slug}`)}>
|
||||
← 언론매체로 돌아가기
|
||||
← Back to Outlet
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setLocation("/")}>
|
||||
홈
|
||||
Home
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -178,12 +178,12 @@ export default function MediaOutletAuction() {
|
||||
|
||||
<main className="max-w-4xl mx-auto px-6 py-8 pb-24">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold mb-2">진행 중인 경매가 없습니다</h1>
|
||||
<h1 className="text-2xl font-bold mb-2">No Active Auction</h1>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
{outlet.name}의 관리자 권한에 대한 경매가 현재 진행되고 있지 않습니다.
|
||||
There is currently no active auction for {outlet.name} management rights.
|
||||
</p>
|
||||
<Button onClick={() => setLocation(`/media/${params?.slug}`)}>
|
||||
언론매체 페이지로 돌아가기
|
||||
Back to Outlet Page
|
||||
</Button>
|
||||
</div>
|
||||
</main>
|
||||
@ -236,7 +236,7 @@ export default function MediaOutletAuction() {
|
||||
)}
|
||||
|
||||
<div className="flex-1">
|
||||
<h1 className="text-3xl font-bold mb-2">{outlet.name} 관리자 권한 경매</h1>
|
||||
<h1 className="text-3xl font-bold mb-2">{outlet.name} Management Rights Auction</h1>
|
||||
<p className="text-lg text-muted-foreground mb-4">
|
||||
{outlet.description}
|
||||
</p>
|
||||
@ -260,7 +260,7 @@ export default function MediaOutletAuction() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Gavel className="h-5 w-5" />
|
||||
<span>경매 정보</span>
|
||||
<span>Auction Info</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
@ -268,7 +268,7 @@ export default function MediaOutletAuction() {
|
||||
<div className="flex items-center justify-between p-4 bg-muted rounded-lg">
|
||||
<div className="flex items-center space-x-2">
|
||||
<TrendingUp className="h-4 w-4 text-green-600" />
|
||||
<span className="font-medium">현재 최고가</span>
|
||||
<span className="font-medium">Current Highest</span>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-green-600">
|
||||
{formatCurrency(parseFloat(auction.currentBid || "0"))}
|
||||
@ -278,7 +278,7 @@ export default function MediaOutletAuction() {
|
||||
<div className="flex items-center justify-between p-4 bg-muted rounded-lg">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Clock className="h-4 w-4 text-orange-600" />
|
||||
<span className="font-medium">남은 시간</span>
|
||||
<span className="font-medium">Time Left</span>
|
||||
</div>
|
||||
<span className="text-lg font-bold text-orange-600">
|
||||
{formatTimeRemaining(auction.endDate)}
|
||||
@ -289,7 +289,7 @@ export default function MediaOutletAuction() {
|
||||
<div className="flex items-center justify-between p-4 bg-muted rounded-lg">
|
||||
<div className="flex items-center space-x-2">
|
||||
<User className="h-4 w-4 text-blue-600" />
|
||||
<span className="font-medium">최고 입찰자</span>
|
||||
<span className="font-medium">Top Bidder</span>
|
||||
</div>
|
||||
<span className="text-lg font-bold text-blue-600">
|
||||
{auction.highestBidderId.slice(0, 3)}***
|
||||
@ -300,7 +300,7 @@ export default function MediaOutletAuction() {
|
||||
{auction.qualityScore && (
|
||||
<div className="flex items-center justify-between p-4 bg-muted rounded-lg">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-medium">품질 점수</span>
|
||||
<span className="font-medium">Quality Score</span>
|
||||
</div>
|
||||
<span className="text-lg font-bold">
|
||||
{auction.qualityScore}/100
|
||||
@ -314,41 +314,41 @@ export default function MediaOutletAuction() {
|
||||
{/* Bidding Form */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>입찰하기</CardTitle>
|
||||
<CardTitle>Place Bid</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!isAuthenticated ? (
|
||||
<div className="text-center p-6">
|
||||
<p className="text-muted-foreground mb-4">
|
||||
입찰하려면 로그인이 필요합니다.
|
||||
You must be logged in to place a bid.
|
||||
</p>
|
||||
<Button onClick={() => setLocation("/")}>
|
||||
로그인하러 가기
|
||||
Go to Login
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="bidAmount">입찰 금액 (원)</Label>
|
||||
<Label htmlFor="bidAmount">Bid Amount (₩)</Label>
|
||||
<Input
|
||||
id="bidAmount"
|
||||
type="number"
|
||||
value={bidAmount}
|
||||
onChange={(e) => setBidAmount(e.target.value)}
|
||||
placeholder={`${parseFloat(auction.currentBid || "0") + 1000} 이상`}
|
||||
placeholder={`Minimum ₩${parseFloat(auction.currentBid || "0") + 1000}`}
|
||||
min={parseFloat(auction.currentBid || "0") + 1}
|
||||
data-testid="input-bid-amount"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="qualityScore">품질 점수 (선택사항, 0-100)</Label>
|
||||
<Label htmlFor="qualityScore">Quality Score (optional, 0-100)</Label>
|
||||
<Input
|
||||
id="qualityScore"
|
||||
type="number"
|
||||
value={qualityScore}
|
||||
onChange={(e) => setQualityScore(e.target.value)}
|
||||
placeholder="품질 점수를 입력하세요"
|
||||
placeholder="Enter quality score"
|
||||
min={0}
|
||||
max={100}
|
||||
data-testid="input-quality-score"
|
||||
@ -361,11 +361,11 @@ export default function MediaOutletAuction() {
|
||||
className="w-full"
|
||||
data-testid="button-place-bid"
|
||||
>
|
||||
{placeBidMutation.isPending ? "입찰 중..." : "입찰하기"}
|
||||
{placeBidMutation.isPending ? "Placing Bid..." : "Place Bid"}
|
||||
</Button>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
* 입찰이 성공하면 취소할 수 없습니다.
|
||||
* Bids cannot be canceled once placed.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@ -378,8 +378,8 @@ export default function MediaOutletAuction() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<TrendingUp className="h-5 w-5" />
|
||||
<span>입찰 내역</span>
|
||||
<Badge variant="outline">{bids.length}건</Badge>
|
||||
<span>Bid History</span>
|
||||
<Badge variant="outline">{bids.length} bids</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@ -420,20 +420,20 @@ export default function MediaOutletAuction() {
|
||||
{formatCurrency(parseFloat(bid.amount))}
|
||||
</span>
|
||||
{bid.id === highestBid.id && (
|
||||
<Badge className="bg-green-600">최고가</Badge>
|
||||
<Badge className="bg-green-600">Highest</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
입찰자: <span data-testid={`bid-bidder-${bid.id}`}>{bid.bidderId.slice(0, 8)}***</span>
|
||||
Bidder: <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>
|
||||
<span className="ml-2">Quality: {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', {
|
||||
{new Date(bid.createdAt!).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
@ -448,8 +448,8 @@ export default function MediaOutletAuction() {
|
||||
) : (
|
||||
<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>
|
||||
<p>No bids yet</p>
|
||||
<p className="text-sm">Be the first to bid!</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
@ -458,23 +458,23 @@ export default function MediaOutletAuction() {
|
||||
{/* Auction Description */}
|
||||
<Card className="mt-8">
|
||||
<CardHeader>
|
||||
<CardTitle>경매 안내</CardTitle>
|
||||
<CardTitle>Auction Guide</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="prose prose-sm max-w-none text-muted-foreground">
|
||||
<p>
|
||||
이 경매는 <strong>{outlet.name}</strong>의 관리자 권한을 획득하기 위한 경매입니다.
|
||||
관리자 권한을 획득하면 다음과 같은 기능을 사용할 수 있습니다:
|
||||
This auction is for acquiring management rights to <strong>{outlet.name}</strong>.
|
||||
With management rights, you will be able to:
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-1 mt-4">
|
||||
<li>언론매체의 기사 관리 및 편집</li>
|
||||
<li>예측시장 이벤트 생성 및 관리</li>
|
||||
<li>언론매체 프로필 정보 수정</li>
|
||||
<li>구독자 및 팔로워 관리</li>
|
||||
<li>Manage and edit media outlet articles</li>
|
||||
<li>Create and manage prediction market events</li>
|
||||
<li>Edit media outlet profile information</li>
|
||||
<li>Manage subscribers and followers</li>
|
||||
</ul>
|
||||
<p className="mt-4">
|
||||
경매는 <strong>{formatTimeRemaining(auction.endDate)}</strong> 후에 종료됩니다.
|
||||
최고 입찰자가 관리자 권한을 획득하게 됩니다.
|
||||
The auction will end in <strong>{formatTimeRemaining(auction.endDate)}</strong>.
|
||||
The highest bidder will acquire management rights.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user