507 lines
30 KiB
PHP
507 lines
30 KiB
PHP
|
|
<?php
|
||
|
|
require_once($_SERVER['DOCUMENT_ROOT'] . "/session.php");
|
||
|
|
|
||
|
|
// 권한 체크
|
||
|
|
if ($_SESSION['level'] != '1') {
|
||
|
|
echo "<script>alert('접근 권한이 없습니다.'); location.href='/';</script>";
|
||
|
|
exit;
|
||
|
|
}
|
||
|
|
?>
|
||
|
|
<!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',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
animation: {
|
||
|
|
'fade-in-up': 'fadeInUp 0.3s ease-out forwards',
|
||
|
|
},
|
||
|
|
keyframes: {
|
||
|
|
fadeInUp: {
|
||
|
|
'0%': { opacity: '0', transform: 'translateY(10px)' },
|
||
|
|
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
<style>
|
||
|
|
.scrollbar-hide::-webkit-scrollbar { display: none; }
|
||
|
|
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
|
||
|
|
</style>
|
||
|
|
|
||
|
|
<!-- 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>
|
||
|
|
|
||
|
|
<!-- Icons: Lucide React -->
|
||
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||
|
|
</head>
|
||
|
|
<body class="bg-background text-slate-800 antialiased overflow-hidden h-screen flex flex-col">
|
||
|
|
<div id="root" class="h-full flex flex-col"></div>
|
||
|
|
|
||
|
|
<script type="text/babel">
|
||
|
|
const { useState, useEffect, useRef } = React;
|
||
|
|
|
||
|
|
// --- Header Component ---
|
||
|
|
const Header = () => {
|
||
|
|
const handleRefresh = () => {
|
||
|
|
window.location.reload();
|
||
|
|
};
|
||
|
|
return (
|
||
|
|
<header className="bg-white border-b border-gray-100 sticky top-0 z-40 flex-none">
|
||
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<i data-lucide="building" className="w-6 h-6 text-blue-600"></i>
|
||
|
|
<h1 className="text-lg font-semibold text-slate-900">바로빌 테넌트 관리</h1>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center gap-4">
|
||
|
|
<a href="../eaccount/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">
|
||
|
|
<i data-lucide="wallet" className="w-4 h-4"></i>
|
||
|
|
계좌내역 조회
|
||
|
|
</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">
|
||
|
|
<i data-lucide="receipt" className="w-4 h-4"></i>
|
||
|
|
전자세금계산서
|
||
|
|
</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">
|
||
|
|
<i data-lucide="credit-card" className="w-4 h-4"></i>
|
||
|
|
법인카드 내역
|
||
|
|
</a>
|
||
|
|
<a href="../index.php" className="text-sm text-slate-500 hover:text-slate-900 flex items-center gap-1">
|
||
|
|
<i data-lucide="home" className="w-4 h-4"></i>
|
||
|
|
홈으로
|
||
|
|
</a>
|
||
|
|
<button onClick={handleRefresh} className="text-sm text-slate-500 hover:text-slate-900 flex items-center gap-1">
|
||
|
|
<i data-lucide="refresh-cw" className="w-4 h-4"></i>
|
||
|
|
새로고침
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</header>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
// --- Icons ---
|
||
|
|
const TrashIcon = ({ className }) => (
|
||
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
|
||
|
|
);
|
||
|
|
const EditIcon = ({ className }) => (
|
||
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
|
||
|
|
);
|
||
|
|
const CreditCardIcon = ({ className }) => (
|
||
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>
|
||
|
|
);
|
||
|
|
const BankIcon = ({ className }) => (
|
||
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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"/><path d="M10 16h4"/><path d="M12 12v4"/></svg> // Simplified bank/money icon
|
||
|
|
);
|
||
|
|
const PlusIcon = ({ className }) => (
|
||
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M5 12h14"/><path d="M12 5v14"/></svg>
|
||
|
|
);
|
||
|
|
|
||
|
|
// --- Main App Component ---
|
||
|
|
const App = () => {
|
||
|
|
const [companies, setCompanies] = useState([]);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
|
||
|
|
// Modals state
|
||
|
|
const [isCompanyModalOpen, setIsCompanyModalOpen] = useState(false);
|
||
|
|
const [editingCompany, setEditingCompany] = useState(null);
|
||
|
|
|
||
|
|
const [isCardModalOpen, setIsCardModalOpen] = useState(false);
|
||
|
|
const [selectedCompanyForCards, setSelectedCompanyForCards] = useState(null);
|
||
|
|
|
||
|
|
const [isAccountModalOpen, setIsAccountModalOpen] = useState(false);
|
||
|
|
const [selectedCompanyForAccounts, setSelectedCompanyForAccounts] = useState(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
fetchCompanies();
|
||
|
|
lucide.createIcons();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
lucide.createIcons();
|
||
|
|
}, [companies, isCompanyModalOpen, isCardModalOpen, isAccountModalOpen]);
|
||
|
|
|
||
|
|
const fetchCompanies = async () => {
|
||
|
|
try {
|
||
|
|
const res = await fetch('api.php?action=get_companies');
|
||
|
|
const json = await res.json();
|
||
|
|
if (json.success) setCompanies(json.data);
|
||
|
|
} catch (e) { console.error(e); }
|
||
|
|
setLoading(false);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleDeleteCompany = async (id) => {
|
||
|
|
if (!confirm("정말 삭제하시겠습니까? 관련 데이터가 모두 삭제됩니다.")) return;
|
||
|
|
const fd = new FormData();
|
||
|
|
fd.append('id', id);
|
||
|
|
await fetch('api.php?action=delete_company', { method: 'POST', body: fd });
|
||
|
|
fetchCompanies();
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex flex-col h-full bg-gray-50">
|
||
|
|
<Header />
|
||
|
|
|
||
|
|
<main className="flex-1 overflow-auto p-4 sm:p-6 lg:p-8">
|
||
|
|
<div className="max-w-7xl mx-auto">
|
||
|
|
<div className="flex justify-between items-center mb-6">
|
||
|
|
<h2 className="text-xl font-bold text-gray-800">등록된 회사 목록</h2>
|
||
|
|
<button
|
||
|
|
onClick={() => { setEditingCompany(null); setIsCompanyModalOpen(true); }}
|
||
|
|
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg shadow-sm flex items-center gap-2 transition-colors"
|
||
|
|
>
|
||
|
|
<PlusIcon className="w-4 h-4" />
|
||
|
|
회사 등록
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{loading ? (
|
||
|
|
<div className="text-center py-10 text-gray-500">로딩중...</div>
|
||
|
|
) : (
|
||
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||
|
|
<table className="min-w-full divide-y divide-gray-200">
|
||
|
|
<thead className="bg-gray-50">
|
||
|
|
<tr>
|
||
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
||
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">회사명</th>
|
||
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">파트너</th>
|
||
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">사업자번호</th>
|
||
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">바로빌 ID</th>
|
||
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">비고</th>
|
||
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">리소스</th>
|
||
|
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">관리</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody className="bg-white divide-y divide-gray-200 text-sm">
|
||
|
|
{companies.length === 0 && (
|
||
|
|
<tr><td colSpan="7" className="px-6 py-8 text-center text-gray-400">등록된 회사가 없습니다.</td></tr>
|
||
|
|
)}
|
||
|
|
{companies.map(company => (
|
||
|
|
<tr key={company.id} className="hover:bg-gray-50 transition-colors">
|
||
|
|
<td className="px-6 py-4 whitespace-nowrap text-gray-500">{company.id}</td>
|
||
|
|
<td className="px-6 py-4 whitespace-nowrap font-medium text-gray-900">
|
||
|
|
{company.parent_user_id ? <span className="text-blue-600 mr-1">[{company.parent_user_id}]</span> : null}
|
||
|
|
{company.company_name}
|
||
|
|
</td>
|
||
|
|
<td className="px-6 py-4 whitespace-nowrap text-gray-500">
|
||
|
|
{company.parent_name ? (
|
||
|
|
<span>{company.parent_name} <span className="text-xs text-gray-400">({company.parent_user_id})</span></span>
|
||
|
|
) : '-'}
|
||
|
|
</td>
|
||
|
|
<td className="px-6 py-4 whitespace-nowrap text-gray-500">{company.corp_num}</td>
|
||
|
|
<td className="px-6 py-4 whitespace-nowrap text-gray-500">{company.barobill_user_id}</td>
|
||
|
|
<td className="px-6 py-4 whitespace-nowrap text-gray-500">
|
||
|
|
{company.memo && company.memo.length > 10 ? company.memo.substring(0, 10) + '...' : company.memo}
|
||
|
|
</td>
|
||
|
|
<td className="px-6 py-4 whitespace-nowrap space-x-2">
|
||
|
|
<button
|
||
|
|
onClick={() => { setSelectedCompanyForCards(company); setIsCardModalOpen(true); }}
|
||
|
|
className="inline-flex items-center px-2.5 py-1.5 border border-indigo-200 text-xs font-medium rounded text-indigo-700 bg-indigo-50 hover:bg-indigo-100"
|
||
|
|
>
|
||
|
|
카드
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={() => { setSelectedCompanyForAccounts(company); setIsAccountModalOpen(true); }}
|
||
|
|
className="inline-flex items-center px-2.5 py-1.5 border border-emerald-200 text-xs font-medium rounded text-emerald-700 bg-emerald-50 hover:bg-emerald-100"
|
||
|
|
>
|
||
|
|
계좌
|
||
|
|
</button>
|
||
|
|
</td>
|
||
|
|
<td className="px-6 py-4 whitespace-nowrap text-right space-x-2">
|
||
|
|
<button onClick={() => { setEditingCompany(company); setIsCompanyModalOpen(true); }} className="text-blue-600 hover:text-blue-900 transition-colors"><EditIcon className="w-4 h-4" /></button>
|
||
|
|
<button onClick={() => handleDeleteCompany(company.id)} className="text-red-500 hover:text-red-700 transition-colors"><TrashIcon className="w-4 h-4" /></button>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
))}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</main>
|
||
|
|
|
||
|
|
{/* Company Modal */}
|
||
|
|
{isCompanyModalOpen && <CompanyModal
|
||
|
|
isOpen={isCompanyModalOpen}
|
||
|
|
onClose={() => setIsCompanyModalOpen(false)}
|
||
|
|
company={editingCompany}
|
||
|
|
onSaved={fetchCompanies}
|
||
|
|
/>}
|
||
|
|
|
||
|
|
{/* Cards Modal */}
|
||
|
|
{isCardModalOpen && <CardsModal
|
||
|
|
isOpen={isCardModalOpen}
|
||
|
|
onClose={() => setIsCardModalOpen(false)}
|
||
|
|
company={selectedCompanyForCards}
|
||
|
|
/>}
|
||
|
|
|
||
|
|
{/* Accounts Modal */}
|
||
|
|
{isAccountModalOpen && <AccountsModal
|
||
|
|
isOpen={isAccountModalOpen}
|
||
|
|
onClose={() => setIsAccountModalOpen(false)}
|
||
|
|
company={selectedCompanyForAccounts}
|
||
|
|
/>}
|
||
|
|
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
// --- Sub Components ---
|
||
|
|
const ModalLayout = ({ title, onClose, children }) => {
|
||
|
|
// Close on Escape
|
||
|
|
useEffect(() => {
|
||
|
|
const handleEsc = (e) => { if(e.key === 'Escape') onClose(); };
|
||
|
|
window.addEventListener('keydown', handleEsc);
|
||
|
|
return () => window.removeEventListener('keydown', handleEsc);
|
||
|
|
}, [onClose]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black bg-opacity-50 backdrop-blur-sm animate-fade-in-up">
|
||
|
|
<div className="bg-white rounded-2xl shadow-xl w-full max-w-lg overflow-hidden flex flex-col max-h-[90vh]">
|
||
|
|
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
|
||
|
|
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
|
||
|
|
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-1 rounded-full hover:bg-gray-200 transition-all">
|
||
|
|
<i data-lucide="x" className="w-5 h-5"></i>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div className="p-6 overflow-y-auto">
|
||
|
|
{children}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
const CompanyModal = ({ isOpen, onClose, company, onSaved }) => {
|
||
|
|
const handleSubmit = async (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
const formData = new FormData(e.target);
|
||
|
|
if (company) formData.append('id', company.id);
|
||
|
|
|
||
|
|
const res = await fetch('api.php?action=save_company', { method: 'POST', body: formData });
|
||
|
|
const json = await res.json();
|
||
|
|
if (json.success) {
|
||
|
|
onSaved();
|
||
|
|
onClose();
|
||
|
|
} else {
|
||
|
|
alert('저장 실패: ' + json.message);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<ModalLayout title={company ? '회사 정보 수정' : '새 회사 등록'} onClose={onClose}>
|
||
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">회사명</label>
|
||
|
|
<input type="text" name="company_name" defaultValue={company?.company_name} required className="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-2 px-3 border" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">사업자번호 (10자리)</label>
|
||
|
|
<input type="text" name="corp_num" defaultValue={company?.corp_num} required className="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-2 px-3 border" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">바로빌 User ID</label>
|
||
|
|
<input type="text" name="barobill_user_id" defaultValue={company?.barobill_user_id} required className="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-2 px-3 border" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">비고</label>
|
||
|
|
<textarea name="memo" defaultValue={company?.memo} className="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-2 px-3 border" rows="3"></textarea>
|
||
|
|
</div>
|
||
|
|
<div className="pt-4 flex justify-end gap-2">
|
||
|
|
<button type="button" onClick={onClose} className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">취소</button>
|
||
|
|
<button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">저장</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</ModalLayout>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
const CardsModal = ({ isOpen, onClose, company }) => {
|
||
|
|
const [cards, setCards] = useState([]);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if(company) loadCards();
|
||
|
|
}, [company]);
|
||
|
|
|
||
|
|
const loadCards = async () => {
|
||
|
|
const res = await fetch(`api.php?action=get_cards&company_id=${company.id}`);
|
||
|
|
const json = await res.json();
|
||
|
|
if(json.success) setCards(json.data);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleAdd = async (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
const fd = new FormData(e.target);
|
||
|
|
fd.append('company_id', company.id);
|
||
|
|
await fetch('api.php?action=save_card', { method: 'POST', body: fd });
|
||
|
|
e.target.reset();
|
||
|
|
loadCards();
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleDelete = async (id) => {
|
||
|
|
if(!confirm('삭제하시겠습니까?')) return;
|
||
|
|
const fd = new FormData();
|
||
|
|
fd.append('id', id);
|
||
|
|
await fetch('api.php?action=delete_card', { method: 'POST', body: fd });
|
||
|
|
loadCards();
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<ModalLayout title={`${company?.company_name} - 법인카드 관리`} onClose={onClose}>
|
||
|
|
<div className="space-y-6">
|
||
|
|
<form onSubmit={handleAdd} className="bg-gray-50 p-4 rounded-lg border border-gray-100 grid grid-cols-2 gap-3">
|
||
|
|
<div className="col-span-1">
|
||
|
|
<label className="text-xs font-semibold text-gray-500">카드사</label>
|
||
|
|
<select name="card_company_code" className="w-full border-gray-300 rounded text-sm py-1.5 mt-1">
|
||
|
|
<option value="Samsung">삼성</option>
|
||
|
|
<option value="Hyundai">현대</option>
|
||
|
|
<option value="Shinhan">신한</option>
|
||
|
|
<option value="Kb">국민</option>
|
||
|
|
<option value="Bc">BC</option>
|
||
|
|
<option value="Lotte">롯데</option>
|
||
|
|
<option value="Hana">하나</option>
|
||
|
|
<option value="Nonghyup">농협</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div className="col-span-1">
|
||
|
|
<label className="text-xs font-semibold text-gray-500">카드번호</label>
|
||
|
|
<input type="text" name="card_num" required className="w-full border-gray-300 rounded text-sm py-1.5 mt-1" placeholder="1234-5678..." />
|
||
|
|
</div>
|
||
|
|
<div className="col-span-1">
|
||
|
|
<label className="text-xs font-semibold text-gray-500">Web ID</label>
|
||
|
|
<input type="text" name="web_id" required className="w-full border-gray-300 rounded text-sm py-1.5 mt-1" />
|
||
|
|
</div>
|
||
|
|
<div className="col-span-1">
|
||
|
|
<label className="text-xs font-semibold text-gray-500">Web PW</label>
|
||
|
|
<input type="password" name="web_pwd" required className="w-full border-gray-300 rounded text-sm py-1.5 mt-1" />
|
||
|
|
</div>
|
||
|
|
<div className="col-span-2">
|
||
|
|
<button type="submit" className="w-full bg-indigo-600 text-white py-2 rounded text-sm font-medium hover:bg-indigo-700">카드 추가</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<h4 className="text-sm font-bold text-gray-700">등록된 카드 목록</h4>
|
||
|
|
{cards.length === 0 ? <p className="text-xs text-gray-400">등록된 카드가 없습니다.</p> : (
|
||
|
|
<ul className="divide-y divide-gray-100 border border-gray-100 rounded-lg overflow-hidden">
|
||
|
|
{cards.map(c => (
|
||
|
|
<li key={c.id} className="p-3 flex justify-between items-center bg-white hover:bg-gray-50">
|
||
|
|
<div>
|
||
|
|
<p className="text-sm font-medium text-gray-900">{c.card_company_code} <span className="text-gray-400 font-normal">|</span> {c.card_num}</p>
|
||
|
|
<p className="text-xs text-gray-400">ID: {c.web_id}</p>
|
||
|
|
</div>
|
||
|
|
<button onClick={() => handleDelete(c.id)} className="text-red-400 hover:text-red-600 p-1"><TrashIcon className="w-4 h-4"/></button>
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</ModalLayout>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
const AccountsModal = ({ isOpen, onClose, company }) => {
|
||
|
|
const [accounts, setAccounts] = useState([]);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if(company) loadAccounts();
|
||
|
|
}, [company]);
|
||
|
|
|
||
|
|
const loadAccounts = async () => {
|
||
|
|
const res = await fetch(`api.php?action=get_accounts&company_id=${company.id}`);
|
||
|
|
const json = await res.json();
|
||
|
|
if(json.success) setAccounts(json.data);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleAdd = async (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
const fd = new FormData(e.target);
|
||
|
|
fd.append('company_id', company.id);
|
||
|
|
await fetch('api.php?action=save_account', { method: 'POST', body: fd });
|
||
|
|
e.target.reset();
|
||
|
|
loadAccounts();
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleDelete = async (id) => {
|
||
|
|
if(!confirm('삭제하시겠습니까?')) return;
|
||
|
|
const fd = new FormData();
|
||
|
|
fd.append('id', id);
|
||
|
|
await fetch('api.php?action=delete_account', { method: 'POST', body: fd });
|
||
|
|
loadAccounts();
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<ModalLayout title={`${company?.company_name} - 계좌 관리`} onClose={onClose}>
|
||
|
|
<div className="space-y-6">
|
||
|
|
<form onSubmit={handleAdd} className="bg-gray-50 p-4 rounded-lg border border-gray-100 grid grid-cols-2 gap-3">
|
||
|
|
<div className="col-span-1">
|
||
|
|
<label className="text-xs font-semibold text-gray-500">은행코드</label>
|
||
|
|
<input type="text" name="bank_code" required className="w-full border-gray-300 rounded text-sm py-1.5 mt-1" placeholder="004 (국민)" />
|
||
|
|
</div>
|
||
|
|
<div className="col-span-1">
|
||
|
|
<label className="text-xs font-semibold text-gray-500">계좌번호</label>
|
||
|
|
<input type="text" name="account_num" required className="w-full border-gray-300 rounded text-sm py-1.5 mt-1" placeholder="123-456-..." />
|
||
|
|
</div>
|
||
|
|
<div className="col-span-2">
|
||
|
|
<label className="text-xs font-semibold text-gray-500">계좌 비밀번호</label>
|
||
|
|
<input type="password" name="account_pwd" required className="w-full border-gray-300 rounded text-sm py-1.5 mt-1" />
|
||
|
|
</div>
|
||
|
|
<div className="col-span-2">
|
||
|
|
<button type="submit" className="w-full bg-emerald-600 text-white py-2 rounded text-sm font-medium hover:bg-emerald-700">계좌 추가</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<h4 className="text-sm font-bold text-gray-700">등록된 계좌 목록</h4>
|
||
|
|
{accounts.length === 0 ? <p className="text-xs text-gray-400">등록된 계좌가 없습니다.</p> : (
|
||
|
|
<ul className="divide-y divide-gray-100 border border-gray-100 rounded-lg overflow-hidden">
|
||
|
|
{accounts.map(a => (
|
||
|
|
<li key={a.id} className="p-3 flex justify-between items-center bg-white hover:bg-gray-50">
|
||
|
|
<div>
|
||
|
|
<p className="text-sm font-medium text-gray-900">Code: {a.bank_code}</p>
|
||
|
|
<p className="text-xs text-gray-500">{a.account_num}</p>
|
||
|
|
</div>
|
||
|
|
<button onClick={() => handleDelete(a.id)} className="text-red-400 hover:text-red-600 p-1"><TrashIcon className="w-4 h-4"/></button>
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</ModalLayout>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||
|
|
root.render(<App />);
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|