- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경 - DB 연결 하드코딩 → .env 기반으로 변경 - MySQL strict mode DATE 오류 수정
511 lines
18 KiB
PHP
511 lines
18 KiB
PHP
<?php
|
|
require_once($_SERVER['DOCUMENT_ROOT'] . "/session.php");
|
|
require_once($_SERVER['DOCUMENT_ROOT'] . "/load_header.php");
|
|
require_once(__DIR__ . '/../lib/mydb.php');
|
|
|
|
// 권한 체크 (레벨 5 이하만 접근)
|
|
if ($level > 5) {
|
|
echo "<script>alert('접근 권한이 없습니다.'); history.back();</script>";
|
|
exit;
|
|
}
|
|
|
|
$pdo = db_connect();
|
|
|
|
// 페이징 설정
|
|
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
|
$per_page = 20;
|
|
$offset = ($page - 1) * $per_page;
|
|
|
|
// 검색 기능
|
|
$search_keyword = isset($_GET['search']) ? trim($_GET['search']) : '';
|
|
$where_clause = '';
|
|
$params = [];
|
|
|
|
if ($search_keyword) {
|
|
$where_clause = " WHERE biz_no LIKE :search OR company_name LIKE :search OR representative LIKE :search";
|
|
$params[':search'] = '%' . $search_keyword . '%';
|
|
}
|
|
|
|
// 전체 레코드 수 조회
|
|
$count_sql = "SELECT COUNT(*) as total FROM biz_cert" . $where_clause;
|
|
$count_stmt = $pdo->prepare($count_sql);
|
|
$count_stmt->execute($params);
|
|
$total_records = $count_stmt->fetch(PDO::FETCH_ASSOC)['total'];
|
|
$total_pages = ceil($total_records / $per_page);
|
|
|
|
// 데이터 조회
|
|
$sql = "SELECT id, biz_no, company_name, representative, open_date, issue_date, created_at
|
|
FROM biz_cert" . $where_clause . "
|
|
ORDER BY id DESC
|
|
LIMIT :limit OFFSET :offset";
|
|
$stmt = $pdo->prepare($sql);
|
|
|
|
foreach ($params as $key => $value) {
|
|
$stmt->bindValue($key, $value, PDO::PARAM_STR);
|
|
}
|
|
$stmt->bindValue(':limit', $per_page, PDO::PARAM_INT);
|
|
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
|
$stmt->execute();
|
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
?>
|
|
|
|
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>사업자등록증 OCR 목록</title>
|
|
<style>
|
|
.ocr-container {
|
|
max-width: 1400px;
|
|
margin: 20px auto;
|
|
padding: 20px;
|
|
}
|
|
.header-section {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 20px;
|
|
}
|
|
.search-section {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
align-items: center;
|
|
}
|
|
.search-section input {
|
|
padding: 8px 12px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
width: 300px;
|
|
}
|
|
.btn {
|
|
padding: 8px 20px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
text-decoration: none;
|
|
display: inline-block;
|
|
}
|
|
.btn-primary {
|
|
background: #0d6efd;
|
|
color: white;
|
|
}
|
|
.btn-primary:hover {
|
|
background: #0b5ed7;
|
|
}
|
|
.btn-secondary {
|
|
background: #6c757d;
|
|
color: white;
|
|
}
|
|
.btn-secondary:hover {
|
|
background: #5c636a;
|
|
}
|
|
.btn-success {
|
|
background: #198754;
|
|
color: white;
|
|
}
|
|
.btn-success:hover {
|
|
background: #157347;
|
|
}
|
|
.data-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
background: white;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
.data-table th {
|
|
background: #f8f9fa;
|
|
padding: 12px;
|
|
text-align: left;
|
|
font-weight: bold;
|
|
border-bottom: 2px solid #dee2e6;
|
|
}
|
|
.data-table td {
|
|
padding: 12px;
|
|
border-bottom: 1px solid #dee2e6;
|
|
}
|
|
.data-table tr:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
.data-table tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
.pagination {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 5px;
|
|
margin-top: 20px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.pagination a, .pagination span {
|
|
padding: 8px 12px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
text-decoration: none;
|
|
color: #333;
|
|
}
|
|
.pagination a:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
.pagination .active {
|
|
background: #0d6efd;
|
|
color: white;
|
|
border-color: #0d6efd;
|
|
}
|
|
.stats {
|
|
color: #6c757d;
|
|
font-size: 14px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.view-link {
|
|
color: #0d6efd;
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
}
|
|
.view-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
.modal {
|
|
display: none;
|
|
position: fixed;
|
|
z-index: 1000;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: auto;
|
|
background-color: rgba(0,0,0,0.4);
|
|
}
|
|
.modal-content {
|
|
background-color: #fefefe;
|
|
margin: 5% auto;
|
|
padding: 20px;
|
|
border: 1px solid #888;
|
|
border-radius: 8px;
|
|
width: 80%;
|
|
max-width: 800px;
|
|
}
|
|
.close {
|
|
color: #aaa;
|
|
float: right;
|
|
font-size: 28px;
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
}
|
|
.close:hover {
|
|
color: #000;
|
|
}
|
|
.detail-group {
|
|
margin-bottom: 15px;
|
|
padding-bottom: 15px;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
.detail-group:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.detail-label {
|
|
font-weight: bold;
|
|
color: #333;
|
|
display: inline-block;
|
|
width: 150px;
|
|
}
|
|
.detail-value {
|
|
color: #666;
|
|
}
|
|
.raw-text-box {
|
|
background: #f8f9fa;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 12px;
|
|
white-space: pre-wrap;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<?php include $_SERVER['DOCUMENT_ROOT'] . "/myheader.php"; ?>
|
|
|
|
<div class="ocr-container">
|
|
<div class="header-section">
|
|
<div>
|
|
<h3><i class="bi bi-list-ul"></i> 사업자등록증 OCR 목록</h3>
|
|
<p class="stats">
|
|
총 <?php echo number_format($total_records); ?>건
|
|
(<?php echo $page; ?> / <?php echo $total_pages; ?> 페이지)
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<a href="index.php" class="btn btn-success">
|
|
<i class="bi bi-plus-circle"></i> 새로 등록
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="search-section">
|
|
<form method="get" action="" style="display: flex; gap: 10px; align-items: center;">
|
|
<input type="text" name="search" placeholder="사업자번호, 상호명, 대표자명 검색..."
|
|
value="<?php echo htmlspecialchars($search_keyword); ?>">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-search"></i> 검색
|
|
</button>
|
|
<?php if ($search_keyword): ?>
|
|
<a href="list.php" class="btn btn-secondary">
|
|
<i class="bi bi-x-circle"></i> 초기화
|
|
</a>
|
|
<?php endif; ?>
|
|
</form>
|
|
</div>
|
|
|
|
<?php if (empty($rows)): ?>
|
|
<div style="text-align: center; padding: 40px; background: white; border-radius: 4px;">
|
|
<i class="bi bi-inbox" style="font-size: 48px; color: #ccc;"></i>
|
|
<p style="color: #999; margin-top: 10px;">
|
|
<?php echo $search_keyword ? '검색 결과가 없습니다.' : '등록된 데이터가 없습니다.'; ?>
|
|
</p>
|
|
</div>
|
|
<?php else: ?>
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 60px;">ID</th>
|
|
<th style="width: 150px;">사업자번호</th>
|
|
<th>상호명</th>
|
|
<th style="width: 120px;">대표자</th>
|
|
<th style="width: 120px;">개업일자</th>
|
|
<th style="width: 120px;">발급일자</th>
|
|
<th style="width: 150px;">등록일시</th>
|
|
<th style="width: 150px;">관리</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($rows as $row): ?>
|
|
<tr>
|
|
<td><?php echo htmlspecialchars($row['id']); ?></td>
|
|
<td><?php echo htmlspecialchars($row['biz_no']); ?></td>
|
|
<td><?php echo htmlspecialchars($row['company_name']); ?></td>
|
|
<td><?php echo htmlspecialchars($row['representative']); ?></td>
|
|
<td><?php echo $row['open_date'] ? htmlspecialchars($row['open_date']) : '-'; ?></td>
|
|
<td><?php echo $row['issue_date'] ? htmlspecialchars($row['issue_date']) : '-'; ?></td>
|
|
<td><?php echo htmlspecialchars($row['created_at']); ?></td>
|
|
<td>
|
|
<a href="javascript:void(0)" class="view-link" onclick="viewDetail(<?php echo $row['id']; ?>)" title="상세보기">
|
|
<i class="bi bi-eye"></i>
|
|
</a>
|
|
<a href="edit.php?id=<?php echo $row['id']; ?>" class="view-link" style="color: #198754; margin-left: 8px;" title="수정">
|
|
<i class="bi bi-pencil"></i>
|
|
</a>
|
|
<a href="javascript:void(0)" class="view-link" style="color: #dc3545; margin-left: 8px;" onclick="confirmDelete(<?php echo $row['id']; ?>, '<?php echo htmlspecialchars($row['company_name'], ENT_QUOTES); ?>')" title="삭제">
|
|
<i class="bi bi-trash"></i>
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
|
|
<?php if ($total_pages > 1): ?>
|
|
<div class="pagination">
|
|
<?php if ($page > 1): ?>
|
|
<a href="?page=1<?php echo $search_keyword ? '&search=' . urlencode($search_keyword) : ''; ?>">처음</a>
|
|
<a href="?page=<?php echo $page - 1; ?><?php echo $search_keyword ? '&search=' . urlencode($search_keyword) : ''; ?>">이전</a>
|
|
<?php endif; ?>
|
|
|
|
<?php
|
|
$start_page = max(1, $page - 5);
|
|
$end_page = min($total_pages, $page + 5);
|
|
|
|
for ($i = $start_page; $i <= $end_page; $i++):
|
|
if ($i == $page):
|
|
?>
|
|
<span class="active"><?php echo $i; ?></span>
|
|
<?php else: ?>
|
|
<a href="?page=<?php echo $i; ?><?php echo $search_keyword ? '&search=' . urlencode($search_keyword) : ''; ?>">
|
|
<?php echo $i; ?>
|
|
</a>
|
|
<?php
|
|
endif;
|
|
endfor;
|
|
?>
|
|
|
|
<?php if ($page < $total_pages): ?>
|
|
<a href="?page=<?php echo $page + 1; ?><?php echo $search_keyword ? '&search=' . urlencode($search_keyword) : ''; ?>">다음</a>
|
|
<a href="?page=<?php echo $total_pages; ?><?php echo $search_keyword ? '&search=' . urlencode($search_keyword) : ''; ?>">마지막</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- 상세보기 모달 -->
|
|
<div id="detailModal" class="modal">
|
|
<div class="modal-content">
|
|
<span class="close" onclick="closeModal()">×</span>
|
|
<h4 style="margin-top: 0;"><i class="bi bi-file-earmark-text"></i> 사업자등록증 상세정보</h4>
|
|
<div id="detailContent">
|
|
<p style="text-align: center; color: #999;">로딩중...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function viewDetail(id) {
|
|
const modal = document.getElementById('detailModal');
|
|
const content = document.getElementById('detailContent');
|
|
|
|
modal.style.display = 'block';
|
|
content.innerHTML = '<p style="text-align: center; color: #999;">로딩중...</p>';
|
|
|
|
fetch('view_detail.php?id=' + id)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.ok) {
|
|
const d = data.data;
|
|
content.innerHTML = `
|
|
<div class="detail-group">
|
|
<span class="detail-label">ID:</span>
|
|
<span class="detail-value">${escapeHtml(d.id)}</span>
|
|
</div>
|
|
<div class="detail-group">
|
|
<span class="detail-label">사업자등록번호:</span>
|
|
<span class="detail-value">${escapeHtml(d.biz_no)}</span>
|
|
</div>
|
|
<div class="detail-group">
|
|
<span class="detail-label">상호명:</span>
|
|
<span class="detail-value">${escapeHtml(d.company_name)}</span>
|
|
</div>
|
|
<div class="detail-group">
|
|
<span class="detail-label">대표자명:</span>
|
|
<span class="detail-value">${escapeHtml(d.representative)}</span>
|
|
</div>
|
|
<div class="detail-group">
|
|
<span class="detail-label">개업일자:</span>
|
|
<span class="detail-value">${d.open_date ? escapeHtml(d.open_date) : '-'}</span>
|
|
</div>
|
|
<div class="detail-group">
|
|
<span class="detail-label">본점 소재지:</span>
|
|
<span class="detail-value">${d.address ? escapeHtml(d.address) : '-'}</span>
|
|
</div>
|
|
<div class="detail-group">
|
|
<span class="detail-label">업태:</span>
|
|
<span class="detail-value">${d.type ? escapeHtml(d.type) : '-'}</span>
|
|
</div>
|
|
<div class="detail-group">
|
|
<span class="detail-label">종목:</span>
|
|
<span class="detail-value">${d.item ? escapeHtml(d.item) : '-'}</span>
|
|
</div>
|
|
<div class="detail-group">
|
|
<span class="detail-label">발급일자:</span>
|
|
<span class="detail-value">${d.issue_date ? escapeHtml(d.issue_date) : '-'}</span>
|
|
</div>
|
|
<div class="detail-group">
|
|
<span class="detail-label">등록일시:</span>
|
|
<span class="detail-value">${escapeHtml(d.created_at)}</span>
|
|
</div>
|
|
${d.raw_text ? `
|
|
<div class="detail-group">
|
|
<span class="detail-label">OCR 원문:</span><br>
|
|
<div class="raw-text-box">${escapeHtml(d.raw_text)}</div>
|
|
</div>
|
|
` : ''}
|
|
<div style="margin-top: 20px; text-align: right; display: flex; gap: 10px; justify-content: flex-end;">
|
|
<a href="edit.php?id=${d.id}" class="btn btn-success">
|
|
<i class="bi bi-pencil"></i> 수정
|
|
</a>
|
|
<button type="button" class="btn" style="background: #dc3545; color: white;" onclick="confirmDeleteFromModal(${d.id}, '${escapeHtml(d.company_name).replace(/'/g, "\\'")}')">
|
|
<i class="bi bi-trash"></i> 삭제
|
|
</button>
|
|
</div>
|
|
`;
|
|
} else {
|
|
content.innerHTML = '<p style="color: red;">오류: ' + escapeHtml(data.error) + '</p>';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
content.innerHTML = '<p style="color: red;">데이터 로드 실패</p>';
|
|
console.error('Error:', error);
|
|
});
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('detailModal').style.display = 'none';
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const map = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
return (text || '').toString().replace(/[&<>"']/g, m => map[m]);
|
|
}
|
|
|
|
// 삭제 확인 (목록에서)
|
|
function confirmDelete(id, companyName) {
|
|
if (confirm(`정말로 "${companyName}" 데이터를 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다.`)) {
|
|
deleteRecord(id);
|
|
}
|
|
}
|
|
|
|
// 삭제 확인 (모달에서)
|
|
function confirmDeleteFromModal(id, companyName) {
|
|
if (confirm(`정말로 "${companyName}" 데이터를 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다.`)) {
|
|
deleteRecord(id);
|
|
}
|
|
}
|
|
|
|
// 삭제 실행
|
|
function deleteRecord(id) {
|
|
fetch('delete.php', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ id: id })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.ok) {
|
|
alert('삭제되었습니다.');
|
|
closeModal();
|
|
location.reload();
|
|
} else {
|
|
alert('삭제 실패: ' + (data.error || '알 수 없는 오류'));
|
|
}
|
|
})
|
|
.catch(error => {
|
|
alert('삭제 중 오류가 발생했습니다.');
|
|
console.error('Error:', error);
|
|
});
|
|
}
|
|
|
|
// 모달 외부 클릭시 닫기
|
|
window.onclick = function(event) {
|
|
const modal = document.getElementById('detailModal');
|
|
if (event.target == modal) {
|
|
closeModal();
|
|
}
|
|
}
|
|
|
|
// 페이지 로드 완료 시 loader 숨기기
|
|
window.addEventListener('load', function() {
|
|
const loadingOverlay = document.getElementById('loadingOverlay');
|
|
if (loadingOverlay) {
|
|
loadingOverlay.style.display = 'none';
|
|
}
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|
|
|