Files
sam-kd/tenant/index.php

507 lines
30 KiB
PHP
Raw Normal View History

<?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>