Files
sam-kd/eaccount/index.php

1422 lines
85 KiB
PHP
Raw Permalink Normal View History

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>계좌 입출금내역 조회 - 바로빌 연동</title>
<!-- Fonts: Pretendard -->
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.8/dist/web/static/pretendard.css" />
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Pretendard', 'sans-serif'],
},
colors: {
background: 'rgb(250, 250, 250)',
primary: {
DEFAULT: '#2563eb', // blue-600
foreground: '#ffffff',
},
accent: {
DEFAULT: '#3b82f6', // blue-500
light: '#dbeafe', // blue-100
}
},
borderRadius: {
'card': '12px',
}
}
}
}
</script>
<!-- React & ReactDOM -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!-- Babel for JSX -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body class="bg-background text-slate-800 antialiased">
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useRef } = React;
// SVG Icon Components
const Icons = {
wallet: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/>
<path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/>
<path d="M18 12a2 2 0 0 0 0 4h4v-4Z"/>
</svg>
),
bank: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 21h18"/>
<path d="M5 21v-7"/>
<path d="M19 21v-7"/>
<path d="M10 9L3 21"/>
<path d="M14 9l7 12"/>
<rect x="2" y="3" width="20" height="5"/>
<line x1="12" x2="12" y1="21" y2="8"/>
</svg>
),
arrowUp: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-red-500">
<line x1="12" y1="19" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>
</svg>
),
arrowDown: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-blue-500">
<line x1="12" y1="5" x2="12" y2="19"></line>
<polyline points="19 12 12 19 5 12"></polyline>
</svg>
),
search: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.3-4.3"/>
</svg>
),
fileText: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/>
<path d="M14 2v4a2 2 0 0 0 2 2h4"/>
<path d="M10 9H8"/>
<path d="M16 13H8"/>
<path d="M16 17H8"/>
</svg>
),
home: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
),
chevronsLeft: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m11 17-5-5 5-5"/>
<path d="m18 17-5-5 5-5"/>
</svg>
),
chevronLeft: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m15 18-6-6 6-6"/>
</svg>
),
chevronRight: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m9 18 6-6-6-6"/>
</svg>
),
chevronsRight: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m6 17 5-5-5-5"/>
<path d="m13 17 5-5-5-5"/>
</svg>
),
download: ({ className }) => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" x2="12" y1="15" y2="3"/>
</svg>
),
info: ({ className }) => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<circle cx="12" cy="12" r="10"/>
<path d="M12 16v-4"/>
<path d="M12 8h.01"/>
</svg>
),
receipt: ({ className }) => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<path d="M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1-2-1Z"/>
<path d="M14 8H8"/>
<path d="M16 12H8"/>
<path d="M13 16H8"/>
</svg>
),
creditCard: ({ className }) => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<rect width="20" height="14" x="2" y="5" rx="2"/>
<line x1="2" x2="22" y1="10" y2="10"/>
</svg>
),
x: ({ className }) => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<path d="M18 6 6 18"/>
<path d="m6 6 12 12"/>
</svg>
)
};
// Header Component
const Header = ({ activeTab, onTabChange, tenants, currentTenantId, onTenantChange }) => (
<header className="bg-white border-b border-gray-100 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="h-16 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center text-blue-600 font-bold">
🏦
</div>
<h1 className="text-lg font-semibold text-slate-900">계좌 입출금내역 조회</h1>
</div>
<div className="flex items-center gap-4">
{/* 테넌트 선택 드롭다운 */}
{tenants.length > 0 && (
<select
value={currentTenantId}
onChange={(e) => onTenantChange(e.target.value)}
className="text-sm border border-slate-300 rounded-lg px-3 py-1.5 bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{tenants.map(tenant => (
<option key={tenant.id} value={tenant.id}>
{tenant.name} {tenant.has_account ? '✓' : '(계좌없음)'}
</option>
))}
</select>
)}
<a href="../tenant/index.php" className="text-sm text-slate-500 hover:text-slate-900 flex items-center gap-1 bg-slate-100 hover:bg-slate-200 px-3 py-1.5 rounded-lg transition-colors">
<Icons.bank />
바로빌 테넌트 관리
</a>
<a href="../etax/index.php" className="text-sm text-slate-500 hover:text-slate-900 flex items-center gap-1 bg-slate-100 hover:bg-slate-200 px-3 py-1.5 rounded-lg transition-colors">
<Icons.receipt />
전자세금계산서
</a>
<a href="../ecard/index.php" className="text-sm text-slate-500 hover:text-slate-900 flex items-center gap-1 bg-slate-100 hover:bg-slate-200 px-3 py-1.5 rounded-lg transition-colors">
<Icons.creditCard />
법인카드 내역
</a>
<a href="../index.php" className="text-sm text-slate-500 hover:text-slate-900 flex items-center gap-1">
<Icons.home />
홈으로
</a>
</div>
</div>
{/* 탭 메뉴 */}
<div className="flex gap-1 border-t border-gray-100">
<button
onClick={() => onTabChange('main')}
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 ${
activeTab === 'main'
? 'border-blue-600 text-blue-600'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
입출금 내역
</button>
<button
onClick={() => onTabChange('status')}
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 ${
activeTab === 'status'
? 'border-blue-600 text-blue-600'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
계좌 등록 상태
</button>
<button
onClick={() => onTabChange('debug')}
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 ${
activeTab === 'debug'
? 'border-blue-600 text-blue-600'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
개발자 로그
</button>
</div>
</div>
</header>
);
// StatCard Component
const StatCard = ({ title, value, subtext, icon, color = 'blue' }) => (
<div className={`bg-white rounded-card p-6 shadow-sm border border-slate-100 hover:shadow-md transition-shadow`}>
<div className="flex items-start justify-between mb-4">
<h3 className="text-sm font-medium text-slate-500">{title}</h3>
<div className={`p-2 bg-${color}-50 rounded-lg text-${color}-600`}>
{icon}
</div>
</div>
<div className="text-2xl font-bold text-slate-900 mb-1">{value}</div>
{subtext && <div className="text-xs text-slate-400">{subtext}</div>}
</div>
);
// Account Selector Component
const AccountSelector = ({ accounts, selectedAccount, onSelect }) => (
<div className="flex flex-wrap gap-2">
<button
onClick={() => onSelect('')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedAccount === ''
? 'bg-blue-600 text-white'
: 'bg-white border border-slate-200 text-slate-700 hover:bg-slate-50'
}`}
>
전체 계좌
</button>
{accounts.map(acc => (
<button
key={acc.bankAccountNum}
onClick={() => onSelect(acc.bankAccountNum)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedAccount === acc.bankAccountNum
? 'bg-blue-600 text-white'
: 'bg-white border border-slate-200 text-slate-700 hover:bg-slate-50'
}`}
>
{acc.bankName} {acc.bankAccountNum ? acc.bankAccountNum.slice(-4) : ''} ({acc.accountName || '별칭없음'})
</button>
))}
</div>
);
// TxtExportModal Component (Copied and adapted from ecard)
// Diagnostic Modal Component (복사 가능한 텍스트 모달)
const DiagnosticModal = ({ isOpen, onClose, title, content }) => {
const textareaRef = React.useRef(null);
const [copied, setCopied] = React.useState(false);
const handleCopy = () => {
if (textareaRef.current) {
textareaRef.current.select();
document.execCommand('copy');
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white rounded-lg shadow-xl w-full max-w-3xl max-h-[90vh] flex flex-col">
<div className="flex items-center justify-between p-6 border-b border-slate-200">
<div>
<h3 className="text-xl font-bold text-slate-900">{title}</h3>
<p className="text-sm text-slate-500 mt-1">텍스트를 선택하여 복사할 있습니다</p>
</div>
<div className="flex gap-2">
<button
onClick={handleCopy}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
copied
? 'bg-green-600 text-white'
: 'bg-blue-600 text-white hover:bg-blue-700'
}`}
>
{copied ? '✓ 복사됨' : '전체 복사'}
</button>
<button
onClick={onClose}
className="p-2 rounded-lg text-slate-400 hover:text-slate-600 hover:bg-slate-100"
>
<Icons.x className="w-5 h-5" />
</button>
</div>
</div>
<div className="flex-1 overflow-auto p-6">
<textarea
ref={textareaRef}
value={content}
readOnly
className="w-full h-full min-h-[400px] font-mono text-xs border border-slate-300 rounded-lg p-4 bg-slate-50 focus:outline-none focus:ring-2 focus:ring-blue-500"
style={{ resize: 'none' }}
/>
</div>
</div>
</div>
);
};
const TxtExportModal = ({ isOpen, onClose, logs }) => {
const [copied, setCopied] = useState(false);
const textareaRef = useRef(null);
if (!isOpen) return null;
const generateTSV = () => {
const headers = ['거래일시', '계좌번호', '은행명', '입금액', '출금액', '잔액', '적요', '내용', '상대방'];
const headerRow = headers.join('\t');
const dataRows = logs.map(log => [
log.transDateTime,
log.bankAccountNum,
log.bankName,
log.deposit,
log.withdraw,
log.balance,
log.summary,
log.memo,
log.cast
].join('\t'));
return headerRow + '\n' + dataRows.join('\n');
};
const tsvData = generateTSV();
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(tsvData);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
if (textareaRef.current) {
textareaRef.current.select();
document.execCommand('copy');
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" onClick={onClose}>
<div className="bg-white rounded-lg shadow-xl w-full max-w-6xl max-h-[90vh] flex flex-col" onClick={e => e.stopPropagation()}>
<div className="p-6 border-b border-slate-200 flex justify-between items-center">
<div>
<h3 className="text-xl font-bold text-slate-900">내역 추출</h3>
<p className="text-sm text-slate-500 mt-1">엑셀에 붙여넣기 가능한 형식입니다</p>
</div>
<div className="flex gap-2">
<button
onClick={handleCopy}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
copied
? 'bg-green-600 text-white'
: 'bg-blue-600 text-white hover:bg-blue-700'
}`}
>
{copied ? '✓ 복사됨' : '전체 복사'}
</button>
<button
onClick={onClose}
className="p-2 rounded-lg text-slate-400 hover:text-slate-600 hover:bg-slate-100"
>
<Icons.x className="w-5 h-5" />
</button>
</div>
</div>
<div className="flex-1 overflow-auto p-6">
<textarea
ref={textareaRef}
value={tsvData}
readOnly
className="w-full h-full min-h-[400px] font-mono text-xs border border-slate-300 rounded-lg p-4 bg-slate-50 focus:outline-none focus:ring-2 focus:ring-blue-500"
style={{ resize: 'none' }}
/>
</div>
</div>
</div>
);
};
// Usage Table Component
const UsageTable = ({ logs, loading }) => {
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
);
}
if (logs.length === 0) {
return (
<div className="text-center py-20 text-slate-400">
<div className="flex justify-center mb-4"><Icons.wallet /></div>
<p>조회된 입출금 내역이 없습니다.</p>
</div>
);
}
return (
<div className="overflow-x-auto">
<table className="w-full text-left text-sm text-slate-600">
<thead className="bg-slate-50 text-xs uppercase font-medium text-slate-500">
<tr>
<th className="px-4 py-3">거래일시</th>
<th className="px-4 py-3">계좌정보</th>
<th className="px-4 py-3">적요/내용</th>
<th className="px-4 py-3 text-right text-blue-600">입금</th>
<th className="px-4 py-3 text-right text-red-600">출금</th>
<th className="px-4 py-3 text-right">잔액</th>
<th className="px-4 py-3">상대방</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{logs.map((log, index) => (
<tr key={index} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3 whitespace-nowrap">
<div className="font-medium text-slate-900">
{log.transDateTime || (log.transDate && log.transTime ?
(() => {
const date = String(log.transDate || '');
const time = String(log.transTime || '');
if (date && time) {
if (date.length === 8 && time.length >= 4) {
const formatted = `${date.substring(0,4)}-${date.substring(4,6)}-${date.substring(6,8)} ${time.substring(0,2)}:${time.substring(2,4)}`;
return time.length >= 6 ? formatted + ':' + time.substring(4,6) : formatted;
} else if (date.length === 10 && date.includes('-')) {
// 이미 포맷된 날짜
const formatted = `${date} ${time.substring(0,2)}:${time.substring(2,4)}`;
return time.length >= 6 ? formatted + ':' + time.substring(4,6) : formatted;
}
}
return log.transDate || log.transTime || '-';
})() :
(log.transDate || log.transTime || '-'))}
</div>
</td>
<td className="px-4 py-3">
<div className="font-medium text-slate-900">{log.bankName}</div>
<div className="text-xs text-slate-400 font-mono">
{log.bankAccountNum ? '****' + log.bankAccountNum.slice(-4) : '-'}
</div>
</td>
<td className="px-4 py-3">
<div className="font-medium text-slate-900">{log.summary}</div>
{log.memo && <div className="text-xs text-slate-400">{log.memo}</div>}
</td>
<td className="px-4 py-3 text-right font-medium text-blue-600">
{log.deposit > 0 ? log.depositFormatted + '원' : '-'}
</td>
<td className="px-4 py-3 text-right font-medium text-red-600">
{log.withdraw > 0 ? log.withdrawFormatted + '원' : '-'}
</td>
<td className="px-4 py-3 text-right text-slate-700">
{log.balanceFormatted}
</td>
<td className="px-4 py-3 text-slate-500">
{log.cast}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
// Date Range & Quick Date Buttons (Same as Ecard)
const DateRangeSelector = ({ startDate, endDate, onStartChange, onEndChange, onSearch }) => (
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-2">
<label className="text-sm text-slate-600">시작일</label>
<input
type="date"
value={startDate}
onChange={(e) => onStartChange(e.target.value)}
className="rounded-lg border border-slate-200 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none"
/>
</div>
<span className="text-slate-400">~</span>
<div className="flex items-center gap-2">
<label className="text-sm text-slate-600">종료일</label>
<input
type="date"
value={endDate}
onChange={(e) => onEndChange(e.target.value)}
className="rounded-lg border border-slate-200 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none"
/>
</div>
<button
onClick={onSearch}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium transition-colors flex items-center gap-2"
>
<Icons.search />
조회
</button>
</div>
);
const QuickDateButtons = ({ onSelect }) => {
const buttons = [
{ label: '오늘', days: 0 },
{ label: '7일', days: 7 },
{ label: '30일', days: 30 },
{ label: '3개월', days: 90 },
{ label: '6개월', days: 180 },
];
return (
<div className="flex gap-2">
{buttons.map(btn => (
<button
key={btn.days}
onClick={() => onSelect(btn.days)}
className="px-3 py-1.5 text-xs bg-slate-100 hover:bg-slate-200 rounded-md text-slate-600 transition-colors"
>
{btn.label}
</button>
))}
</div>
);
};
// Main App Component
const App = () => {
const [loading, setLoading] = useState(true);
const [accounts, setAccounts] = useState([]);
const [selectedAccount, setSelectedAccount] = useState('');
const [logs, setLogs] = useState([]);
const [pagination, setPagination] = useState({});
const [summary, setSummary] = useState({});
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [error, setError] = useState(null);
const [txtExportModalOpen, setTxtExportModalOpen] = useState(false);
const [activeTab, setActiveTab] = useState('main'); // 'main', 'status', 'debug'
const [accountStatus, setAccountStatus] = useState(null);
const [accountStatusLoading, setAccountStatusLoading] = useState(false);
const [debugLogs, setDebugLogs] = useState([]);
const [diagnosticModal, setDiagnosticModal] = useState({ open: false, content: '', title: '' });
const [tenants, setTenants] = useState([]);
const [currentTenantId, setCurrentTenantId] = useState('');
// 날짜 초기화
useEffect(() => {
const today = new Date();
const thirtyDaysAgo = new Date(today);
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
setEndDate(today.toISOString().split('T')[0]);
setStartDate(thirtyDaysAgo.toISOString().split('T')[0]);
}, []);
// 테넌트 목록 로드
useEffect(() => {
loadTenants();
}, []);
// 계좌 목록 로드 (테넌트 변경 시)
useEffect(() => {
if (currentTenantId) {
loadAccounts();
}
}, [currentTenantId]);
// 사용내역 로드 (날짜 설정 후)
useEffect(() => {
if (startDate && endDate) {
loadUsage();
}
}, [startDate, endDate, selectedAccount]);
const loadTenants = async () => {
try {
const response = await fetch('api/get_tenants.php');
const data = await response.json();
if (data.success) {
setTenants(data.tenants || []);
// 기본값: '(주)주일기업' 찾기
let defaultTenantId = data.current_tenant_id;
if (!defaultTenantId && data.tenants && data.tenants.length > 0) {
const juilTenant = data.tenants.find(t =>
t.name.includes('주일기업') ||
t.name.includes('주일') ||
t.user_id === 'juil5130'
);
defaultTenantId = juilTenant ? juilTenant.id : data.tenants[0].id;
}
// 테넌트 ID 설정 및 자동으로 계좌 로드
if (defaultTenantId) {
setCurrentTenantId(defaultTenantId);
// 세션에 테넌트 ID 저장
const formData = new FormData();
formData.append('tenant_id', defaultTenantId);
fetch('api/set_tenant.php', {
method: 'POST',
body: formData
}).then(() => {
// 테넌트 설정 후 계좌 자동 로드
loadAccounts();
});
}
}
} catch (err) {
console.error('테넌트 목록 로드 오류:', err);
}
};
const changeTenant = async (tenantId) => {
if (!tenantId) return;
try {
// 먼저 상태 초기화
setCurrentTenantId(tenantId);
setAccounts([]);
setAccountStatus(null);
setLogs([]);
setError(null);
setSelectedAccount('');
// 세션에 테넌트 ID 저장
const formData = new FormData();
formData.append('tenant_id', tenantId);
const response = await fetch('api/set_tenant.php', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
// 계좌 목록 자동 로드
await loadAccounts();
} else {
console.error('테넌트 변경 실패:', data.error);
setError('테넌트 변경 실패: ' + data.error);
}
} catch (err) {
console.error('테넌트 변경 오류:', err);
setError('테넌트 변경 중 오류가 발생했습니다: ' + err.message);
}
};
const loadAccounts = async () => {
try {
const response = await fetch('api/accounts.php');
const data = await response.json();
if (data.success) {
setAccounts(data.accounts || []);
} else {
console.error('계좌 목록 조회 실패:', data.error);
// 계좌 목록 조회 실패 시에도 에러 표시 (특히 -50214 오류)
if (data.error && data.error.includes('-50214')) {
setError(data.error);
}
}
} catch (err) {
console.error('계좌 목록 로드 오류:', err);
}
};
const loadUsage = async (page = 1) => {
setLoading(true);
setError(null);
try {
// 전체 계좌 선택 시 빈 문자열 전달 (서버에서 모든 계좌 조회)
const accountNum = selectedAccount || '';
const params = new URLSearchParams({
startDate: startDate.replace(/-/g, ''),
endDate: endDate.replace(/-/g, ''),
accountNum: accountNum,
page: page,
limit: 50
});
const response = await fetch(`api/transactions.php?${params}`);
const data = await response.json();
if (data.success) {
setLogs(data.data.logs || []);
setPagination(data.data.pagination || {});
setSummary(data.data.summary || {});
// 디버그: API 응답 구조 확인
if (data.debug_api_structure) {
console.log('=== API 응답 구조 디버그 ===');
console.log('첫 번째 로그 구조:', data.debug_api_structure.first_log_structure);
console.log('날짜/시간 필드:', data.debug_api_structure.first_log_datetime);
console.log('적요 필드:', data.debug_api_structure.first_log_summary);
}
// 디버그: 첫 번째 로그의 원본 데이터 확인
if (data.debug_first_log_raw) {
console.log('=== 첫 번째 로그 원본 데이터 ===');
console.log('모든 키:', Object.keys(data.debug_first_log_raw));
console.log('전체 데이터:', JSON.stringify(data.debug_first_log_raw, null, 2));
}
// 디버그: 전체 계좌 조회 시 첫 번째 계좌의 첫 번째 로그 확인
if (data.debug_all_accounts) {
console.log('=== 전체 계좌 조회 디버그 ===');
const rawFields = data.debug_all_accounts.first_account_first_log?.log_raw_fields;
if (rawFields) {
console.log('첫 번째 계좌의 첫 번째 로그 원본 필드 (모든 키):', Object.keys(rawFields));
console.log('첫 번째 계좌의 첫 번째 로그 원본 필드 (전체):', JSON.stringify(rawFields, null, 2));
}
console.log('첫 번째 계좌의 첫 번째 로그 파싱된 데이터:', data.debug_all_accounts.first_account_first_log?.parsed);
}
// 디버그: 첫 번째 로그의 파싱된 데이터 확인
if (data.data.logs && data.data.logs.length > 0) {
console.log('=== 첫 번째 로그 파싱된 데이터 ===');
console.log('transDateTime:', data.data.logs[0].transDateTime);
console.log('transDate:', data.data.logs[0].transDate);
console.log('transTime:', data.data.logs[0].transTime);
console.log('summary:', data.data.logs[0].summary);
console.log('cast:', data.data.logs[0].cast);
console.log('branch:', data.data.logs[0].branch);
console.log('전체 첫 번째 로그:', data.data.logs[0]);
}
// 디버그 로그 저장
if (data.debug) {
setDebugLogs(prev => [{
type: 'API_CALL',
method: 'GetPeriodBankAccountTransLog',
timestamp: new Date().toISOString(),
request: data.debug.request,
request_xml: data.debug.request_xml,
response_xml: data.debug.response_xml,
success: true,
api_structure: data.debug_api_structure,
first_log_raw: data.debug_first_log_raw
}, ...prev].slice(0, 20)); // 최근 20개만 유지
}
} else {
setError(data.error);
setLogs([]);
// 에러도 로그에 저장
setDebugLogs(prev => [{
type: 'API_ERROR',
method: 'GetPeriodBankAccountTransLog',
timestamp: new Date().toISOString(),
error: data.error,
error_code: data.error_code,
success: false
}, ...prev].slice(0, 20));
}
} catch (err) {
setError('서버 통신 오류: ' + err.message);
setLogs([]);
} finally {
setLoading(false);
}
};
const handleQuickDateSelect = (days) => {
const today = new Date();
const startDateObj = new Date(today);
startDateObj.setDate(startDateObj.getDate() - days);
setEndDate(today.toISOString().split('T')[0]);
setStartDate(startDateObj.toISOString().split('T')[0]);
};
// 계좌 등록 상태 로드
const loadAccountStatus = async () => {
setAccountStatusLoading(true);
try {
const response = await fetch('api/account_status.php');
const data = await response.json();
setAccountStatus(data);
// 디버그 로그 저장
if (data.debug) {
setDebugLogs(prev => [{
type: 'API_CALL',
method: 'GetBankAccountEx',
timestamp: new Date().toISOString(),
request: data.debug.request,
request_xml: data.debug.request_xml,
response_xml: data.debug.response_xml,
success: data.success
}, ...prev].slice(0, 20));
}
} catch (err) {
console.error('계좌 상태 로드 오류:', err);
setAccountStatus({
success: false,
error: '계좌 상태 로드 실패: ' + err.message
});
} finally {
setAccountStatusLoading(false);
}
};
// 탭 변경 시 계좌 상태 로드
useEffect(() => {
if (activeTab === 'status') {
loadAccountStatus();
}
}, [activeTab]);
const formatCurrency = (val) => new Intl.NumberFormat('ko-KR').format(val || 0) + '원';
return (
<div className="min-h-screen pb-20">
<Header
activeTab={activeTab}
onTabChange={setActiveTab}
tenants={tenants}
currentTenantId={currentTenantId}
onTenantChange={changeTenant}
/>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
{/* 메인 탭: 입출금 내역 */}
{activeTab === 'main' && (
<>
{/* 테넌트 정보 표시 */}
{tenants.length > 0 && (
<section>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-blue-600 font-semibold">현재 조회 :</span>
<span className="text-blue-800">
{tenants.find(t => t.id === currentTenantId)?.name || '테넌트 선택 필요'}
</span>
{selectedAccount && accounts.length > 0 && (() => {
const selectedAcc = accounts.find(acc => {
const accNum = (acc.bankAccountNum || '').replace(/-/g, '');
const selectedNum = selectedAccount.replace(/-/g, '');
return accNum === selectedNum;
});
if (selectedAcc) {
return (
<span className="text-blue-700 text-sm">
- {selectedAcc.bankName} {selectedAcc.bankAccountNum}
</span>
);
}
return null;
})()}
{accounts.length === 0 && (
<span className="text-xs bg-yellow-100 text-yellow-800 px-2 py-1 rounded">
(등록된 계좌 없음)
</span>
)}
</div>
</div>
</section>
)}
{/* 통계 카드 */}
<section>
<h2 className="text-xl font-bold text-slate-900 mb-6">입출금 현황</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<StatCard
title="총 입금액"
value={formatCurrency(summary.totalDeposit)}
subtext={`조회기간 합계`}
icon={<Icons.arrowDown />}
color="blue"
/>
<StatCard
title="총 출금액"
value={formatCurrency(summary.totalWithdraw)}
subtext={`조회기간 합계`}
icon={<Icons.arrowUp />}
color="red"
/>
<StatCard
title="등록된 계좌"
value={`${accounts.length}개`}
subtext="사용 가능한 계좌"
icon={<Icons.bank />}
/>
<StatCard
title="거래건수"
value={`${(summary.count || 0).toLocaleString()}건`}
subtext="전체 입출금 건수"
icon={<Icons.fileText />}
color="slate"
/>
</div>
</section>
{/* 필터 섹션 */}
<section className="bg-white rounded-card shadow-sm border border-slate-100 p-6 space-y-4">
<div className="flex justify-between items-start">
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
<span className="text-blue-600"><Icons.search /></span>
조회 조건
</h2>
<QuickDateButtons onSelect={handleQuickDateSelect} />
</div>
{accounts.length > 0 && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">계좌 선택</label>
<AccountSelector
accounts={accounts}
selectedAccount={selectedAccount}
onSelect={setSelectedAccount}
/>
</div>
)}
<div className="flex justify-between items-end">
<DateRangeSelector
startDate={startDate}
endDate={endDate}
onStartChange={setStartDate}
onEndChange={setEndDate}
onSearch={() => loadUsage(1)}
/>
<div className="flex gap-2">
<button
onClick={() => setTxtExportModalOpen(true)}
className="text-sm text-slate-500 hover:text-blue-600 flex items-center gap-1 bg-white border border-slate-200 px-3 py-2 rounded-lg"
>
<Icons.download className="w-4 h-4" />
내역 내보내기
</button>
</div>
</div>
</section>
{/* 내역 테이블 */}
<section>
{error ? (
<div className="bg-red-50 border border-red-200 text-red-600 p-4 rounded-lg">
<div className="flex items-start gap-3">
<div className="text-xl">⚠️</div>
<div className="flex-1">
<p className="font-semibold mb-2">{error}</p>
{error.includes('-50214') && (
<div className="mt-3 p-3 bg-white rounded border border-red-200 text-sm">
<p className="font-medium mb-2">해결 방법:</p>
<ol className="list-decimal list-inside space-y-1 text-slate-700">
<li>바로빌 사이트(<a href="https://www.barobill.co.kr" target="_blank" className="text-blue-600 hover:underline">https://www.barobill.co.kr</a>) 로그인</li>
<li>계좌 관리 메뉴에서 해당 계좌 확인</li>
<li>계좌 비밀번호가 변경되었는지 확인</li>
<li>인증서가 만료되지 않았는지 확인</li>
<li>필요시 계좌 재등록 또는 비밀번호 재설정</li>
</ol>
</div>
)}
</div>
</div>
</div>
) : (
<UsageTable logs={logs} loading={loading} />
)}
{/* Pagination (Simplified) */}
{pagination.maxPageNum > 1 && (
<div className="flex justify-center mt-6 gap-2">
<button
onClick={() => loadUsage(Math.max(1, pagination.currentPage - 1))}
disabled={pagination.currentPage === 1}
className="px-3 py-1 rounded bg-white border disabled:opacity-50"
>
이전
</button>
<span className="px-3 py-1">
{pagination.currentPage} / {pagination.maxPageNum}
</span>
<button
onClick={() => loadUsage(Math.min(pagination.maxPageNum, pagination.currentPage + 1))}
disabled={pagination.currentPage === pagination.maxPageNum}
className="px-3 py-1 rounded bg-white border disabled:opacity-50"
>
다음
</button>
</div>
)}
</section>
</>
)}
{/* 계좌 등록 상태 탭 */}
{activeTab === 'status' && (
<section className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h2 className="text-xl font-bold text-slate-900">계좌 등록 상태</h2>
<p className="text-sm text-slate-500 mt-1">
{accountStatus && accountStatus.success ? (
<>
{tenants.find(t => t.id === currentTenantId)?.name || '현재 테넌트'}
{accountStatus.count > 0 && (
<span className="text-green-600 font-medium">
{' - ' + accountStatus.message}
</span>
)}
{accountStatus.count === 0 && (
<span className="text-yellow-600">
{' - 등록된 계좌가 없습니다.'}
</span>
)}
</>
) : accountStatus && !accountStatus.success ? (
<span className="text-red-600">
{tenants.find(t => t.id === currentTenantId)?.name || '현재 테넌트'} - 오류 발생
</span>
) : (
<>
{tenants.find(t => t.id === currentTenantId)?.name || '현재 테넌트'} - 로딩 ...
</>
)}
</p>
</div>
<div className="flex gap-2">
<button
onClick={async () => {
try {
const response = await fetch('api/check_api_config.php');
const data = await response.json();
setDiagnosticModal({
open: true,
title: 'API 설정 진단 결과',
content: JSON.stringify(data, null, 2)
});
} catch (err) {
setDiagnosticModal({
open: true,
title: '진단 오류',
content: '진단 오류: ' + err.message
});
}
}}
className="px-4 py-2 bg-slate-600 text-white rounded-lg hover:bg-slate-700 text-sm font-medium"
>
API 설정 진단
</button>
<button
onClick={async () => {
try {
const response = await fetch('api/debug_accounts.php');
const data = await response.json();
setDiagnosticModal({
open: true,
title: '계좌 정보 디버깅 결과',
content: JSON.stringify(data, null, 2)
});
} catch (err) {
setDiagnosticModal({
open: true,
title: '디버깅 오류',
content: '디버깅 오류: ' + err.message
});
}
}}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm font-medium"
>
계좌 정보 디버깅
</button>
<button
onClick={loadAccountStatus}
disabled={accountStatusLoading}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium flex items-center gap-2"
>
{accountStatusLoading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
로딩 ...
</>
) : (
<>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
<path d="M21 3v5h-5"/>
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
<path d="M8 16H3v5"/>
</svg>
새로고침
</>
)}
</button>
</div>
</div>
{/* API 설정 진단 경고 */}
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
<div className="flex items-start gap-3">
<div className="text-yellow-600 text-xl">⚠️</div>
<div className="flex-1">
<p className="font-semibold text-yellow-800 mb-2">API 설정 확인 필요</p>
<p className="text-sm text-yellow-700 mb-2">
바로빌 사이트에서는 조회가 되지만 API로는 되는 경우, API 설정을 확인하세요.
</p>
<button
onClick={async () => {
try {
const response = await fetch('api/check_api_config.php');
const data = await response.json();
console.log('API 설정 진단:', data);
// 상세 정보를 모달로 표시
const diagnostics = data.diagnostics;
let message = '=== API 설정 진단 결과 ===\n\n';
message += 'CERTKEY:\n';
message += ` 파일 존재: ${diagnostics.cert_key.file_exists ? '✓' : '✗'}\n`;
message += ` 설정됨: ${diagnostics.cert_key.is_set ? '✓' : '✗'}\n`;
message += ` 길이: ${diagnostics.cert_key.length}자\n`;
message += ` 미리보기: ${diagnostics.cert_key.preview}\n\n`;
message += '사업자번호:\n';
message += ` 파일 존재: ${diagnostics.corp_num.file_exists ? '✓' : '✗'}\n`;
message += ` 설정됨: ${diagnostics.corp_num.is_set ? '✓' : '✗'}\n`;
message += ` 값: ${diagnostics.corp_num.value || '없음'}\n\n`;
if (diagnostics.api_test) {
message += 'API 테스트:\n';
message += ` 성공: ${diagnostics.api_test.success ? '✓' : '✗'}\n`;
if (diagnostics.api_test.error) {
message += ` 오류: ${diagnostics.api_test.error}\n`;
if (diagnostics.api_test.error_code) {
message += ` 오류 코드: ${diagnostics.api_test.error_code}\n`;
}
}
}
if (data.recommendations && data.recommendations.length > 0) {
message += '\n권장 사항:\n';
data.recommendations.forEach((rec, idx) => {
if (rec) message += `${idx + 1}. ${rec}\n`;
});
}
setDiagnosticModal({
open: true,
title: 'API 설정 진단 결과',
content: message
});
} catch (err) {
setDiagnosticModal({
open: true,
title: '진단 오류',
content: '진단 오류: ' + err.message
});
}
}}
className="text-sm bg-yellow-600 text-white px-3 py-1.5 rounded hover:bg-yellow-700"
>
API 설정 진단 실행
</button>
</div>
</div>
</div>
{accountStatus === null || accountStatusLoading ? (
<div className="flex items-center justify-center py-20">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
) : accountStatus.success ? (
<div className="bg-white rounded-card shadow-sm border border-slate-100 p-6">
{accountStatus.warning_count > 0 ? (
<div className="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-yellow-800 font-semibold">⚠️ {accountStatus.message}</p>
<p className="text-sm text-yellow-700 mt-2">
로컬 DB에만 등록된 계좌는 바로빌 API에 등록해야 실제 조회가 가능합니다.
</p>
{accountStatus.local_count !== undefined && (
<p className="text-sm text-yellow-600 mt-1">
전체: {accountStatus.count} (사용 가능: {accountStatus.available_count || 0}, 바로빌 미등록: {accountStatus.warning_count})
</p>
)}
</div>
) : (
<div className="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg">
<p className="text-green-800 font-semibold"> {accountStatus.message}</p>
{accountStatus.available_count !== undefined && (
<p className="text-sm text-green-600 mt-1">
사용 가능한 계좌: {accountStatus.available_count}
</p>
)}
</div>
)}
{accountStatus.barobill_error && (
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-red-600">
⚠️ 바로빌 API 오류: {accountStatus.barobill_error}
{accountStatus.barobill_error_code && ` (코드: ${accountStatus.barobill_error_code})`}
</p>
</div>
)}
{accountStatus.accounts && accountStatus.accounts.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-left">은행명</th>
<th className="px-4 py-3 text-left">계좌번호</th>
<th className="px-4 py-3 text-left">계좌명</th>
<th className="px-4 py-3 text-right">잔액</th>
<th className="px-4 py-3 text-center">상태</th>
<th className="px-4 py-3 text-left">등록일</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{accountStatus.accounts.map((acc, idx) => (
<tr key={idx} className="hover:bg-slate-50">
<td className="px-4 py-3 font-medium">
{acc.bankName}
{acc.source && (
<span className={`ml-2 px-1.5 py-0.5 rounded text-xs ${
acc.source === 'local_db_only' ? 'bg-red-100 text-red-700' :
acc.source === 'barobill_api' ? 'bg-green-100 text-green-700' :
acc.source === 'both' ? 'bg-blue-100 text-blue-700' :
2025-12-16 20:05:46 +09:00
acc.source === 'barobill_api_error' ? 'bg-orange-100 text-orange-700' :
acc.source === 'local_db' ? 'bg-yellow-100 text-yellow-700' :
'bg-slate-100 text-slate-700'
}`}>
{acc.source === 'local_db_only' ? '⚠️ 로컬만' :
acc.source === 'barobill_api' ? '✓ 바로빌' :
acc.source === 'both' ? '✓ 통합' :
2025-12-16 20:05:46 +09:00
acc.source === 'barobill_api_error' ? '❓ 상태미확인' :
acc.source === 'local_db' ? '로컬' : '알 수 없음'}
</span>
)}
</td>
<td className="px-4 py-3 font-mono text-slate-600">{acc.bankAccountNum}</td>
<td className="px-4 py-3">{acc.accountName || '-'}</td>
<td className="px-4 py-3 text-right font-medium">{formatCurrency(acc.balance)}</td>
<td className="px-4 py-3 text-center">
<span className={`px-2 py-1 rounded text-xs font-medium ${
acc.status == 1 ? 'bg-green-100 text-green-700' :
acc.status == 0 ? 'bg-yellow-100 text-yellow-700' :
acc.statusText === '바로빌 미등록' ? 'bg-red-100 text-red-700' :
acc.source === 'local_db_only' ? 'bg-red-100 text-red-700' :
2025-12-16 20:05:46 +09:00
acc.source === 'barobill_api_error' ? 'bg-orange-100 text-orange-700' :
'bg-slate-100 text-slate-700'
}`}>
{acc.statusText}
</span>
2025-12-16 20:05:46 +09:00
{acc.warning && !acc.api_error && (
<div className="mt-1 text-xs text-red-600">
⚠️ 바로빌 API에 등록 필요
</div>
)}
2025-12-16 20:05:46 +09:00
{acc.api_error && (
<div className="mt-1 text-xs text-orange-600">
⚠️ API 연동 실패
</div>
)}
</td>
<td className="px-4 py-3 text-slate-500">{acc.issueDate || '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="text-center py-12 text-slate-400">
<p>등록된 계좌가 없습니다.</p>
</div>
)}
</div>
) : (
<div className="bg-red-50 border border-red-200 text-red-600 p-4 rounded-lg">
<p className="font-semibold">오류: {accountStatus.error}</p>
{accountStatus.error_code && (
<p className="text-sm mt-1">오류 코드: {accountStatus.error_code}</p>
)}
</div>
)}
</section>
)}
{/* 개발자 로그 탭 */}
{activeTab === 'debug' && (
<section className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-xl font-bold text-slate-900">개발자 로그</h2>
<button
onClick={() => setDebugLogs([])}
className="px-4 py-2 bg-slate-600 text-white rounded-lg hover:bg-slate-700 text-sm font-medium"
>
로그 초기화
</button>
</div>
<div className="bg-slate-900 rounded-lg p-4 text-slate-100 font-mono text-xs overflow-x-auto">
{debugLogs.length === 0 ? (
<p className="text-slate-400">API 호출 로그가 없습니다. 조회를 실행하면 로그가 표시됩니다.</p>
) : (
<div className="space-y-4">
{debugLogs.map((log, idx) => (
<div key={idx} className="border-b border-slate-700 pb-4 last:border-0">
<div className="flex items-center gap-2 mb-2">
<span className={`px-2 py-1 rounded text-xs font-bold ${
log.success ? 'bg-green-600' : 'bg-red-600'
}`}>
{log.success ? 'SUCCESS' : 'ERROR'}
</span>
<span className="text-slate-400">{log.method}</span>
<span className="text-slate-500">{new Date(log.timestamp).toLocaleString('ko-KR')}</span>
</div>
{log.request && (
<div className="mb-2">
<div className="text-slate-400 mb-1">Request:</div>
<pre className="bg-slate-800 p-2 rounded overflow-x-auto text-xs">
{JSON.stringify(log.request, null, 2)}
</pre>
</div>
)}
{log.request_xml && (
<div className="mb-2">
<div className="text-slate-400 mb-1">SOAP Request XML:</div>
<pre className="bg-slate-800 p-2 rounded overflow-x-auto text-xs whitespace-pre-wrap">
{log.request_xml}
</pre>
</div>
)}
{log.response_xml && (
<div className="mb-2">
<div className="text-slate-400 mb-1">SOAP Response XML:</div>
<pre className="bg-slate-800 p-2 rounded overflow-x-auto text-xs whitespace-pre-wrap">
{log.response_xml}
</pre>
</div>
)}
{log.error && (
<div className="text-red-400">
<div className="text-slate-400 mb-1">Error:</div>
<div>{log.error}</div>
{log.error_code && (
<div className="text-xs text-slate-500 mt-1">Error Code: {log.error_code}</div>
)}
</div>
)}
</div>
))}
</div>
)}
</div>
</section>
)}
</main>
<TxtExportModal
isOpen={txtExportModalOpen}
onClose={() => setTxtExportModalOpen(false)}
logs={logs}
/>
<DiagnosticModal
isOpen={diagnosticModal.open}
onClose={() => setDiagnosticModal({ open: false, content: '', title: '' })}
title={diagnosticModal.title}
content={diagnosticModal.content}
/>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>