Initial commit: AutonetSellCar platform with deployment system

- Frontend: Next.js 14 with TypeScript
- Backend: FastAPI with SQLAlchemy
- Agent: Carmodoo sync agent
- Deployment: Docker Compose based staging/production setup
- Scripts: Automated deployment with rollback support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
AutonetSellCar Deploy
2025-12-30 13:24:39 +09:00
commit 1f0dcb1ddb
224 changed files with 55119 additions and 0 deletions

View File

@@ -0,0 +1,356 @@
'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/lib/store';
import { useTranslation } from '@/lib/i18n';
import { withdrawalApi, WithdrawalRequest } from '@/lib/api';
export default function AdminWithdrawalsPage() {
const { t, language } = useTranslation();
const { user, token } = useAuthStore();
const router = useRouter();
const [statusFilter, setStatusFilter] = useState<string>('');
const [requests, setRequests] = useState<WithdrawalRequest[]>([]);
const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState<number | null>(null);
const [processModal, setProcessModal] = useState<{
id: number;
status: string;
note: string;
} | null>(null);
useEffect(() => {
if (!user?.is_admin) {
router.push('/');
return;
}
fetchData();
}, [user, router, statusFilter]);
const fetchData = async () => {
if (!token) return;
setLoading(true);
try {
const data = await withdrawalApi.adminGetAllRequests(statusFilter || undefined);
setRequests(data);
} catch (error) {
console.error('Failed to fetch data:', error);
} finally {
setLoading(false);
}
};
const handleProcess = async () => {
if (!token || !processModal) return;
setActionLoading(processModal.id);
try {
await withdrawalApi.adminProcessRequest(processModal.id, {
status: processModal.status,
admin_note: processModal.note || undefined,
});
setProcessModal(null);
fetchData();
} catch (error: any) {
alert(error.response?.data?.detail || 'Failed to process');
} finally {
setActionLoading(null);
}
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('ko-KR').format(amount);
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'pending':
return <span className="px-2 py-1 bg-yellow-100 text-yellow-700 rounded text-sm">{t.withdrawalPending}</span>;
case 'approved':
return <span className="px-2 py-1 bg-blue-100 text-blue-700 rounded text-sm">{t.withdrawalApproved}</span>;
case 'completed':
return <span className="px-2 py-1 bg-green-100 text-green-700 rounded text-sm">{t.withdrawalCompleted}</span>;
case 'rejected':
return <span className="px-2 py-1 bg-red-100 text-red-700 rounded text-sm">{t.withdrawalRejected}</span>;
default:
return <span className="px-2 py-1 bg-gray-100 text-gray-700 rounded text-sm">{status}</span>;
}
};
const pendingCount = requests.filter(r => r.status === 'pending').length;
if (!user?.is_admin) {
return null;
}
return (
<div className="min-h-screen bg-gray-100 py-8">
<div className="container mx-auto px-4">
{/* Header */}
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-800">
{language === 'ko' ? '출금 관리' : 'Withdrawal Management'}
</h1>
{pendingCount > 0 && (
<p className="text-orange-600 mt-1">
{language === 'ko'
? `${pendingCount}건의 대기 중인 출금 요청이 있습니다`
: `${pendingCount} pending withdrawal request(s)`}
</p>
)}
</div>
</div>
{/* Filter Tabs */}
<div className="flex gap-2 mb-6 flex-wrap">
<button
onClick={() => setStatusFilter('')}
className={`px-4 py-2 rounded-lg transition ${
statusFilter === ''
? 'bg-primary-600 text-white'
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
>
{language === 'ko' ? '전체' : 'All'}
</button>
<button
onClick={() => setStatusFilter('pending')}
className={`px-4 py-2 rounded-lg transition ${
statusFilter === 'pending'
? 'bg-yellow-500 text-white'
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
>
{t.withdrawalPending}
{pendingCount > 0 && (
<span className="ml-2 px-2 py-0.5 bg-red-500 text-white text-xs rounded-full">
{pendingCount}
</span>
)}
</button>
<button
onClick={() => setStatusFilter('approved')}
className={`px-4 py-2 rounded-lg transition ${
statusFilter === 'approved'
? 'bg-blue-500 text-white'
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
>
{t.withdrawalApproved}
</button>
<button
onClick={() => setStatusFilter('completed')}
className={`px-4 py-2 rounded-lg transition ${
statusFilter === 'completed'
? 'bg-green-500 text-white'
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
>
{t.withdrawalCompleted}
</button>
<button
onClick={() => setStatusFilter('rejected')}
className={`px-4 py-2 rounded-lg transition ${
statusFilter === 'rejected'
? 'bg-red-500 text-white'
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
>
{t.withdrawalRejected}
</button>
</div>
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
</div>
) : (
<div className="bg-white rounded-xl shadow-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-600">ID</th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-600">
{language === 'ko' ? '유저ID' : 'User ID'}
</th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-600">
{t.withdrawalAmount}
</th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-600">
{t.taxWithheld}
</th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-600">
{t.netAmount}
</th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-600">
{language === 'ko' ? '은행/계좌' : 'Bank/Account'}
</th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-600">
{language === 'ko' ? '신청일' : 'Requested'}
</th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-600">
{t.status}
</th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-600">
{language === 'ko' ? '액션' : 'Action'}
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{requests.map((request) => (
<tr key={request.id} className="hover:bg-gray-50">
<td className="px-4 py-3 text-sm">{request.id}</td>
<td className="px-4 py-3 text-sm">{request.user_id}</td>
<td className="px-4 py-3 text-sm font-medium">
{formatCurrency(request.amount)}
</td>
<td className="px-4 py-3 text-sm text-red-600">
-{formatCurrency(request.tax_withheld)}
</td>
<td className="px-4 py-3 text-sm font-bold text-primary-600">
{formatCurrency(request.net_amount)}
</td>
<td className="px-4 py-3 text-sm">
<div className="font-medium">{request.bank_name}</div>
<div className="text-gray-500 text-xs font-mono">{request.bank_account}</div>
<div className="text-gray-500 text-xs">{request.account_holder}</div>
</td>
<td className="px-4 py-3 text-sm text-gray-500">
{new Date(request.requested_at).toLocaleDateString()}
</td>
<td className="px-4 py-3 text-sm">
{getStatusBadge(request.status)}
{request.admin_note && (
<div className="text-xs text-gray-500 mt-1" title={request.admin_note}>
{request.admin_note.substring(0, 20)}...
</div>
)}
</td>
<td className="px-4 py-3 text-sm">
{request.status === 'pending' && (
<div className="flex gap-2">
<button
onClick={() => setProcessModal({
id: request.id,
status: 'approved',
note: '',
})}
className="px-3 py-1 bg-blue-600 text-white text-sm rounded hover:bg-blue-700"
>
{language === 'ko' ? '승인' : 'Approve'}
</button>
<button
onClick={() => setProcessModal({
id: request.id,
status: 'rejected',
note: '',
})}
className="px-3 py-1 bg-red-600 text-white text-sm rounded hover:bg-red-700"
>
{language === 'ko' ? '거부' : 'Reject'}
</button>
</div>
)}
{request.status === 'approved' && (
<button
onClick={() => setProcessModal({
id: request.id,
status: 'completed',
note: '',
})}
className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700"
>
{language === 'ko' ? '완료 처리' : 'Complete'}
</button>
)}
</td>
</tr>
))}
{requests.length === 0 && (
<tr>
<td colSpan={9} className="px-4 py-8 text-center text-gray-500">
{t.noWithdrawalHistory}
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)}
{/* Process Modal */}
{processModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl p-6 max-w-md w-full mx-4">
<h3 className="text-lg font-semibold mb-4">
{processModal.status === 'approved' && (language === 'ko' ? '출금 승인' : 'Approve Withdrawal')}
{processModal.status === 'completed' && (language === 'ko' ? '출금 완료 처리' : 'Complete Withdrawal')}
{processModal.status === 'rejected' && (language === 'ko' ? '출금 거부' : 'Reject Withdrawal')}
</h3>
<div className="mb-4">
<p className="text-sm text-gray-600 mb-2">
{language === 'ko' ? '관리자 메모 (선택사항)' : 'Admin note (optional)'}
</p>
<textarea
value={processModal.note}
onChange={(e) => setProcessModal({ ...processModal, note: e.target.value })}
placeholder={
processModal.status === 'rejected'
? (language === 'ko' ? '거부 사유를 입력하세요...' : 'Enter rejection reason...')
: (language === 'ko' ? '메모를 입력하세요...' : 'Enter note...')
}
className="w-full p-3 border border-gray-300 rounded-lg h-24 resize-none"
/>
</div>
{processModal.status === 'completed' && (
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg text-sm text-yellow-700">
{language === 'ko'
? '완료 처리 후에는 취소할 수 없습니다. 실제 입금 후 처리해주세요.'
: 'This action cannot be undone. Please confirm payment has been made.'}
</div>
)}
<div className="flex gap-2 justify-end">
<button
onClick={() => setProcessModal(null)}
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
{language === 'ko' ? '취소' : 'Cancel'}
</button>
<button
onClick={handleProcess}
disabled={actionLoading === processModal.id}
className={`px-4 py-2 text-white rounded-lg disabled:opacity-50 ${
processModal.status === 'rejected'
? 'bg-red-600 hover:bg-red-700'
: processModal.status === 'completed'
? 'bg-green-600 hover:bg-green-700'
: 'bg-blue-600 hover:bg-blue-700'
}`}
>
{actionLoading === processModal.id
? '...'
: processModal.status === 'approved'
? (language === 'ko' ? '승인' : 'Approve')
: processModal.status === 'completed'
? (language === 'ko' ? '완료' : 'Complete')
: (language === 'ko' ? '거부' : 'Reject')}
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
}