Looka vs Brandmark 비교 마크다운 문서를 세련된 디자인의 PHP 대시보드 페이지로 변환 완료
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
225
barobill/tenant/api.php
Normal file
225
barobill/tenant/api.php
Normal file
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
require_once($_SERVER['DOCUMENT_ROOT'] . "/session.php");
|
||||
include '../../lib/mydb.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
if ($method === 'OPTIONS') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// 퍼미션 체크 (레벨 1 관리자만 접근 가능)
|
||||
// if (!isset($_SESSION['level']) || $_SESSION['level'] != '1') {
|
||||
// echo json_encode(['success' => false, 'message' => '권한이 없습니다.']);
|
||||
// exit;
|
||||
// }
|
||||
|
||||
$pdo = db_connect();
|
||||
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : '';
|
||||
|
||||
try {
|
||||
if (!$pdo) throw new Exception("Database connection failed.");
|
||||
|
||||
// DB명이 정의되지 않았을 경우를 대비해 기본값 설정 혹은 mydb.php의 $DB 사용
|
||||
// 보통 mydb.php에서 $DB 변수를 제공한다고 가정
|
||||
if (!isset($DB)) {
|
||||
global $DB;
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'get_companies':
|
||||
// 모든 회사 가져오기 (파트너-자식 구조)
|
||||
$sql = "SELECT c.*, p.company_name as parent_name, p.barobill_user_id as parent_user_id
|
||||
FROM {$DB}.barobill_companies c
|
||||
LEFT JOIN {$DB}.barobill_companies p ON c.parent_id = p.id
|
||||
ORDER BY c.parent_id ASC, c.id ASC";
|
||||
$stmt = $pdo->query($sql);
|
||||
$companies = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
echo json_encode(['success' => true, 'data' => $companies]);
|
||||
break;
|
||||
|
||||
case 'save_company':
|
||||
// 회사 추가/수정
|
||||
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
||||
|
||||
$company_name = $_POST['company_name'];
|
||||
$corp_num = $_POST['corp_num'];
|
||||
$barobill_user_id = $_POST['barobill_user_id'];
|
||||
$memo = $_POST['memo'];
|
||||
|
||||
// 1. Find ID of 'cbx0913' (Parent)
|
||||
$parent_sql = "SELECT id FROM {$DB}.barobill_companies WHERE barobill_user_id = 'cbx0913' LIMIT 1";
|
||||
$stmt = $pdo->query($parent_sql);
|
||||
$parent_row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// 만약 'cbx0913' 본인이면 parent_id는 NULL
|
||||
if ($barobill_user_id === 'cbx0913') {
|
||||
$parent_id = null;
|
||||
} else {
|
||||
// 부모가 있으면 그 ID, 없으면 NULL (혹은 에러처리)
|
||||
$parent_id = $parent_row ? $parent_row['id'] : null;
|
||||
}
|
||||
|
||||
if ($id > 0) {
|
||||
$sql = "UPDATE {$DB}.barobill_companies SET
|
||||
parent_id = :parent_id,
|
||||
company_name = :company_name,
|
||||
corp_num = :corp_num,
|
||||
barobill_user_id = :barobill_user_id,
|
||||
memo = :memo
|
||||
WHERE id = :id";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
} else {
|
||||
$sql = "INSERT INTO {$DB}.barobill_companies (parent_id, company_name, corp_num, barobill_user_id, memo)
|
||||
VALUES (:parent_id, :company_name, :corp_num, :barobill_user_id, :memo)";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
}
|
||||
|
||||
$stmt->bindValue(':parent_id', $parent_id, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':company_name', $company_name);
|
||||
$stmt->bindValue(':corp_num', $corp_num);
|
||||
$stmt->bindValue(':barobill_user_id', $barobill_user_id);
|
||||
$stmt->bindValue(':memo', $memo);
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception(implode(", ", $stmt->errorInfo()));
|
||||
}
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
case 'delete_company':
|
||||
$id = intval($_POST['id']);
|
||||
$sql = "DELETE FROM {$DB}.barobill_companies WHERE id = :id";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception(implode(", ", $stmt->errorInfo()));
|
||||
}
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
case 'get_cards':
|
||||
$company_id = intval($_GET['company_id']);
|
||||
$sql = "SELECT * FROM {$DB}.company_cards WHERE company_id = :company_id ORDER BY id DESC";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->bindValue(':company_id', $company_id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$cards = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
echo json_encode(['success' => true, 'data' => $cards]);
|
||||
break;
|
||||
|
||||
case 'save_card':
|
||||
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
||||
$company_id = intval($_POST['company_id']);
|
||||
$card_company_code = $_POST['card_company_code'];
|
||||
$card_num = $_POST['card_num'];
|
||||
$web_id = $_POST['web_id'];
|
||||
$web_pwd = $_POST['web_pwd'];
|
||||
|
||||
if ($id > 0) {
|
||||
$sql = "UPDATE {$DB}.company_cards SET
|
||||
card_company_code = :card_company_code,
|
||||
card_num = :card_num,
|
||||
web_id = :web_id,
|
||||
web_pwd = :web_pwd
|
||||
WHERE id = :id";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
} else {
|
||||
$sql = "INSERT INTO {$DB}.company_cards (company_id, card_company_code, card_num, web_id, web_pwd)
|
||||
VALUES (:company_id, :card_company_code, :card_num, :web_id, :web_pwd)";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->bindValue(':company_id', $company_id, PDO::PARAM_INT);
|
||||
}
|
||||
|
||||
$stmt->bindValue(':card_company_code', $card_company_code);
|
||||
$stmt->bindValue(':card_num', $card_num);
|
||||
$stmt->bindValue(':web_id', $web_id);
|
||||
$stmt->bindValue(':web_pwd', $web_pwd);
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception(implode(", ", $stmt->errorInfo()));
|
||||
}
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
case 'delete_card':
|
||||
$id = intval($_POST['id']);
|
||||
$sql = "DELETE FROM {$DB}.company_cards WHERE id = :id";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception(implode(", ", $stmt->errorInfo()));
|
||||
}
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
case 'get_accounts':
|
||||
$company_id = intval($_GET['company_id']);
|
||||
$sql = "SELECT * FROM {$DB}.company_accounts WHERE company_id = :company_id ORDER BY id DESC";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->bindValue(':company_id', $company_id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$accounts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
echo json_encode(['success' => true, 'data' => $accounts]);
|
||||
break;
|
||||
|
||||
case 'save_account':
|
||||
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
||||
$company_id = intval($_POST['company_id']);
|
||||
$bank_code = $_POST['bank_code'];
|
||||
$account_num = $_POST['account_num'];
|
||||
$account_pwd = $_POST['account_pwd'];
|
||||
|
||||
if ($id > 0) {
|
||||
$sql = "UPDATE {$DB}.company_accounts SET
|
||||
bank_code = :bank_code,
|
||||
account_num = :account_num,
|
||||
account_pwd = :account_pwd
|
||||
WHERE id = :id";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
} else {
|
||||
$sql = "INSERT INTO {$DB}.company_accounts (company_id, bank_code, account_num, account_pwd)
|
||||
VALUES (:company_id, :bank_code, :account_num, :account_pwd)";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->bindValue(':company_id', $company_id, PDO::PARAM_INT);
|
||||
}
|
||||
|
||||
$stmt->bindValue(':bank_code', $bank_code);
|
||||
$stmt->bindValue(':account_num', $account_num);
|
||||
$stmt->bindValue(':account_pwd', $account_pwd);
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception(implode(", ", $stmt->errorInfo()));
|
||||
}
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
case 'delete_account':
|
||||
$id = intval($_POST['id']);
|
||||
$sql = "DELETE FROM {$DB}.company_accounts WHERE id = :id";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception(implode(", ", $stmt->errorInfo()));
|
||||
}
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
default:
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid action']);
|
||||
break;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
545
barobill/tenant/index.php
Normal file
545
barobill/tenant/index.php
Normal file
@@ -0,0 +1,545 @@
|
||||
<?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 = ({ onOpenApiInfo }) => (
|
||||
<header className="bg-white/80 backdrop-blur-md border-b border-blue-100/50 sticky top-0 z-50 transition-all shadow-sm">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-18 flex items-center justify-between py-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-blue-600 to-indigo-700 rounded-xl flex items-center justify-center text-white shadow-lg shadow-blue-200/50 ring-4 ring-blue-50">
|
||||
<i data-lucide="building" className="w-5 h-5"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-slate-900 tracking-tight leading-none">테넌트 관리</h1>
|
||||
<p className="text-[10px] text-blue-600 font-semibold mt-1 uppercase tracking-wider opacity-70">Tenant Configuration</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-slate-500 font-medium">
|
||||
<div className="flex bg-slate-100/50 p-1 rounded-xl border border-slate-200/50 mr-2">
|
||||
<a href="../eaccount/index.php" className="flex items-center gap-2 px-3 py-2 rounded-lg hover:text-blue-600 hover:bg-white transition-all duration-200">
|
||||
<i data-lucide="wallet" className="w-4 h-4 text-blue-500"></i> <span>계좌조회</span>
|
||||
</a>
|
||||
<a href="../ecard/index.php" className="flex items-center gap-2 px-3 py-2 rounded-lg hover:text-blue-600 hover:bg-white transition-all duration-200">
|
||||
<i data-lucide="credit-card" className="w-4 h-4 text-purple-500"></i> <span>카드내역</span>
|
||||
</a>
|
||||
<a href="index.php" className="flex items-center gap-2 px-4 py-2 rounded-lg bg-white text-blue-600 shadow-sm border border-blue-100 font-bold">
|
||||
<i data-lucide="building" className="w-4 h-4 text-blue-600"></i> <span>테넌트</span>
|
||||
</a>
|
||||
<a href="../registration/index.php" className="flex items-center gap-2 px-3 py-2 rounded-lg hover:text-blue-600 hover:bg-white transition-all duration-200">
|
||||
<i data-lucide="users" className="w-4 h-4 text-teal-500"></i> <span>바로빌 회원관리</span>
|
||||
</a>
|
||||
<button onClick={(e) => { e.preventDefault(); onOpenApiInfo(); }} className="flex items-center gap-2 px-3 py-2 rounded-lg hover:text-blue-600 hover:bg-white transition-all duration-200">
|
||||
<i data-lucide="book-open" className="w-4 h-4 text-orange-500"></i> <span>API정보</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="h-4 w-px bg-slate-200 mx-2"></div>
|
||||
|
||||
<a href="../etax/index.php" className="flex items-center gap-1.5 px-3 py-2 text-slate-400 hover:text-blue-600 transition-colors">
|
||||
<i data-lucide="file-text" className="w-4 h-4"></i> <span className="hidden lg:inline text-xs">세금계산서</span>
|
||||
</a>
|
||||
<a href="../../index.php" className="flex items-center gap-1.5 px-3 py-2 text-slate-400 hover:text-blue-600 transition-colors">
|
||||
<i data-lucide="home" className="w-4 h-4"></i> <span className="hidden lg:inline text-xs">홈</span>
|
||||
</a>
|
||||
</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);
|
||||
const [isApiInfoModalOpen, setIsApiInfoModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCompanies();
|
||||
lucide.createIcons();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
lucide.createIcons();
|
||||
}, [companies, isCompanyModalOpen, isCardModalOpen, isAccountModalOpen, isApiInfoModalOpen]);
|
||||
|
||||
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 onOpenApiInfo={() => setIsApiInfoModalOpen(true)} />
|
||||
|
||||
<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}
|
||||
/>}
|
||||
|
||||
<ApiInfoModal
|
||||
isOpen={isApiInfoModalOpen}
|
||||
onClose={() => setIsApiInfoModalOpen(false)}
|
||||
/>
|
||||
|
||||
</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 ApiInfoModal = ({ isOpen, onClose }) => {
|
||||
if (!isOpen) return null;
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-2xl w-full max-w-5xl overflow-hidden shadow-2xl animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="p-4 border-b border-slate-100 flex justify-between items-center bg-slate-50/50">
|
||||
<h3 className="font-bold text-slate-800 flex items-center gap-2">
|
||||
<span className="w-1.5 h-6 bg-blue-500 rounded-full"></span>
|
||||
바로빌 API 상세 정보
|
||||
</h3>
|
||||
<button onClick={onClose} className="w-10 h-10 flex items-center justify-center rounded-full text-slate-600 hover:text-slate-900 hover:bg-slate-200 transition-all duration-200 bg-white/50 shadow-sm border border-slate-200">
|
||||
<i data-lucide="x" className="w-6 h-6"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative w-full h-[700px] bg-slate-50">
|
||||
<iframe
|
||||
src="../etax/barobill_api_info.php"
|
||||
className="w-full h-full border-none"
|
||||
title="API Information"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(<App />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user