- /admin/drysyrup 페이지 구현 - GET /api/drug-info/drysyrup 전체 목록 API - DELETE /api/drug-info/drysyrup/<sung_code> 삭제 API - 검색, CRUD, 통계 카드 기능
981 lines
36 KiB
HTML
981 lines
36 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>건조시럽 환산계수 관리 - 청춘약국</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--bg-primary: #0f172a;
|
||
--bg-secondary: #1e293b;
|
||
--bg-card: #1e293b;
|
||
--bg-card-hover: #334155;
|
||
--border: #334155;
|
||
--text-primary: #f1f5f9;
|
||
--text-secondary: #94a3b8;
|
||
--text-muted: #64748b;
|
||
--accent-teal: #14b8a6;
|
||
--accent-blue: #3b82f6;
|
||
--accent-purple: #a855f7;
|
||
--accent-amber: #f59e0b;
|
||
--accent-emerald: #10b981;
|
||
--accent-rose: #f43f5e;
|
||
--accent-orange: #f97316;
|
||
--accent-cyan: #06b6d4;
|
||
}
|
||
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
||
body {
|
||
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
|
||
background: var(--bg-primary);
|
||
color: var(--text-primary);
|
||
-webkit-font-smoothing: antialiased;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* ══════════════════ 헤더 ══════════════════ */
|
||
.header {
|
||
background: linear-gradient(135deg, #a855f7 0%, #8b5cf6 50%, #7c3aed 100%);
|
||
padding: 20px 24px;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
}
|
||
.header-inner {
|
||
max-width: 1600px;
|
||
margin: 0 auto;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.header-left h1 {
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
letter-spacing: -0.5px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
.header-left p {
|
||
font-size: 13px;
|
||
opacity: 0.85;
|
||
margin-top: 4px;
|
||
}
|
||
.header-nav {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
.header-nav a {
|
||
color: rgba(255,255,255,0.85);
|
||
text-decoration: none;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
padding: 8px 14px;
|
||
border-radius: 8px;
|
||
background: rgba(255,255,255,0.1);
|
||
transition: all 0.2s;
|
||
}
|
||
.header-nav a:hover {
|
||
background: rgba(255,255,255,0.2);
|
||
color: #fff;
|
||
}
|
||
|
||
/* ══════════════════ 컨텐츠 ══════════════════ */
|
||
.content {
|
||
max-width: 1600px;
|
||
margin: 0 auto;
|
||
padding: 24px;
|
||
}
|
||
|
||
/* ══════════════════ 검색 & 액션 바 ══════════════════ */
|
||
.action-bar {
|
||
background: var(--bg-card);
|
||
border-radius: 16px;
|
||
padding: 20px 24px;
|
||
margin-bottom: 20px;
|
||
border: 1px solid var(--border);
|
||
display: flex;
|
||
gap: 16px;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
.search-group {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
flex: 1;
|
||
max-width: 500px;
|
||
}
|
||
.search-group input {
|
||
flex: 1;
|
||
padding: 12px 16px;
|
||
background: var(--bg-primary);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
font-family: inherit;
|
||
color: var(--text-primary);
|
||
transition: all 0.2s;
|
||
}
|
||
.search-group input:focus {
|
||
outline: none;
|
||
border-color: var(--accent-purple);
|
||
box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.2);
|
||
}
|
||
.search-group input::placeholder { color: var(--text-muted); }
|
||
|
||
.btn {
|
||
padding: 12px 24px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.btn-primary {
|
||
background: linear-gradient(135deg, var(--accent-purple), #7c3aed);
|
||
color: #fff;
|
||
}
|
||
.btn-primary:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(168, 85, 247, 0.4);
|
||
}
|
||
.btn-success {
|
||
background: linear-gradient(135deg, var(--accent-emerald), var(--accent-teal));
|
||
color: #fff;
|
||
}
|
||
.btn-success:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
|
||
}
|
||
.btn-danger {
|
||
background: linear-gradient(135deg, var(--accent-rose), #dc2626);
|
||
color: #fff;
|
||
}
|
||
.btn-danger:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(244, 63, 94, 0.4);
|
||
}
|
||
.btn-secondary {
|
||
background: var(--bg-card-hover);
|
||
color: var(--text-primary);
|
||
border: 1px solid var(--border);
|
||
}
|
||
.btn-secondary:hover {
|
||
background: var(--border);
|
||
}
|
||
|
||
/* ══════════════════ 통계 ══════════════════ */
|
||
.stats-row {
|
||
display: flex;
|
||
gap: 16px;
|
||
margin-bottom: 20px;
|
||
}
|
||
.stat-card {
|
||
background: var(--bg-card);
|
||
border-radius: 14px;
|
||
padding: 20px;
|
||
border: 1px solid var(--border);
|
||
flex: 1;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.stat-card::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 3px;
|
||
}
|
||
.stat-card.purple::before { background: var(--accent-purple); }
|
||
.stat-card.cyan::before { background: var(--accent-cyan); }
|
||
.stat-card.amber::before { background: var(--accent-amber); }
|
||
|
||
.stat-value {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
margin-bottom: 4px;
|
||
}
|
||
.stat-card.purple .stat-value { color: var(--accent-purple); }
|
||
.stat-card.cyan .stat-value { color: var(--accent-cyan); }
|
||
.stat-card.amber .stat-value { color: var(--accent-amber); }
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
/* ══════════════════ 테이블 ══════════════════ */
|
||
.table-container {
|
||
background: var(--bg-card);
|
||
border-radius: 16px;
|
||
border: 1px solid var(--border);
|
||
overflow: hidden;
|
||
}
|
||
.table-header {
|
||
padding: 16px 20px;
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.table-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.badge {
|
||
background: linear-gradient(135deg, var(--accent-purple), #7c3aed);
|
||
color: #fff;
|
||
padding: 4px 10px;
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
th {
|
||
text-align: left;
|
||
padding: 14px 16px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
background: var(--bg-primary);
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
td {
|
||
padding: 14px 16px;
|
||
font-size: 13px;
|
||
border-bottom: 1px solid rgba(255,255,255,0.03);
|
||
vertical-align: middle;
|
||
}
|
||
tr:hover td {
|
||
background: rgba(168, 85, 247, 0.05);
|
||
}
|
||
|
||
.code-cell {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 12px;
|
||
color: var(--accent-cyan);
|
||
}
|
||
.factor-cell {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-weight: 600;
|
||
color: var(--accent-amber);
|
||
}
|
||
.storage-badge {
|
||
display: inline-block;
|
||
padding: 4px 10px;
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
}
|
||
.storage-badge.cold {
|
||
background: rgba(59, 130, 246, 0.2);
|
||
color: var(--accent-blue);
|
||
}
|
||
.storage-badge.room {
|
||
background: rgba(16, 185, 129, 0.2);
|
||
color: var(--accent-emerald);
|
||
}
|
||
|
||
.action-btns {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
.action-btns button {
|
||
padding: 6px 12px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
.btn-edit {
|
||
background: rgba(59, 130, 246, 0.2);
|
||
color: var(--accent-blue);
|
||
}
|
||
.btn-edit:hover {
|
||
background: rgba(59, 130, 246, 0.3);
|
||
}
|
||
.btn-delete {
|
||
background: rgba(244, 63, 94, 0.2);
|
||
color: var(--accent-rose);
|
||
}
|
||
.btn-delete:hover {
|
||
background: rgba(244, 63, 94, 0.3);
|
||
}
|
||
|
||
/* ══════════════════ 빈 상태 ══════════════════ */
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 60px 20px;
|
||
color: var(--text-muted);
|
||
}
|
||
.empty-state .icon {
|
||
font-size: 48px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.empty-state h3 {
|
||
font-size: 16px;
|
||
margin-bottom: 8px;
|
||
color: var(--text-secondary);
|
||
}
|
||
.empty-state p {
|
||
font-size: 13px;
|
||
}
|
||
|
||
/* ══════════════════ 에러 메시지 ══════════════════ */
|
||
.error-banner {
|
||
background: rgba(244, 63, 94, 0.1);
|
||
border: 1px solid rgba(244, 63, 94, 0.3);
|
||
border-radius: 12px;
|
||
padding: 16px 20px;
|
||
margin-bottom: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
color: var(--accent-rose);
|
||
}
|
||
.error-banner.hidden { display: none; }
|
||
.error-banner .icon { font-size: 20px; }
|
||
|
||
/* ══════════════════ 모달 ══════════════════ */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0,0,0,0.7);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 1000;
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
transition: all 0.3s;
|
||
}
|
||
.modal-overlay.active {
|
||
opacity: 1;
|
||
visibility: visible;
|
||
}
|
||
.modal {
|
||
background: var(--bg-secondary);
|
||
border-radius: 20px;
|
||
width: 90%;
|
||
max-width: 600px;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
transform: scale(0.9) translateY(20px);
|
||
transition: all 0.3s;
|
||
}
|
||
.modal-overlay.active .modal {
|
||
transform: scale(1) translateY(0);
|
||
}
|
||
.modal-header {
|
||
padding: 24px;
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.modal-header h2 {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
.modal-close {
|
||
background: none;
|
||
border: none;
|
||
font-size: 24px;
|
||
color: var(--text-muted);
|
||
cursor: pointer;
|
||
padding: 4px;
|
||
line-height: 1;
|
||
}
|
||
.modal-close:hover { color: var(--text-primary); }
|
||
|
||
.modal-body {
|
||
padding: 24px;
|
||
}
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
.form-group label {
|
||
display: block;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.form-group input,
|
||
.form-group select {
|
||
width: 100%;
|
||
padding: 12px 16px;
|
||
background: var(--bg-primary);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
font-family: inherit;
|
||
color: var(--text-primary);
|
||
transition: all 0.2s;
|
||
}
|
||
.form-group input:focus,
|
||
.form-group select:focus {
|
||
outline: none;
|
||
border-color: var(--accent-purple);
|
||
box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.2);
|
||
}
|
||
.form-group input:disabled {
|
||
background: var(--bg-card-hover);
|
||
color: var(--text-muted);
|
||
}
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 16px;
|
||
}
|
||
|
||
.modal-footer {
|
||
padding: 20px 24px;
|
||
border-top: 1px solid var(--border);
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
}
|
||
|
||
/* ══════════════════ 토스트 ══════════════════ */
|
||
.toast-container {
|
||
position: fixed;
|
||
bottom: 24px;
|
||
right: 24px;
|
||
z-index: 2000;
|
||
}
|
||
.toast {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
padding: 16px 20px;
|
||
margin-top: 12px;
|
||
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
animation: slideIn 0.3s ease;
|
||
}
|
||
.toast.success { border-left: 4px solid var(--accent-emerald); }
|
||
.toast.error { border-left: 4px solid var(--accent-rose); }
|
||
@keyframes slideIn {
|
||
from { transform: translateX(100%); opacity: 0; }
|
||
to { transform: translateX(0); opacity: 1; }
|
||
}
|
||
|
||
/* ══════════════════ 로딩 ══════════════════ */
|
||
.loading-spinner {
|
||
display: inline-block;
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 2px solid var(--border);
|
||
border-top-color: var(--accent-purple);
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* ══════════════════ 반응형 ══════════════════ */
|
||
@media (max-width: 768px) {
|
||
.action-bar { flex-direction: column; }
|
||
.search-group { max-width: 100%; width: 100%; }
|
||
.stats-row { flex-direction: column; }
|
||
.form-row { grid-template-columns: 1fr; }
|
||
th, td { padding: 10px 12px; }
|
||
.header-nav { display: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- 헤더 -->
|
||
<header class="header">
|
||
<div class="header-inner">
|
||
<div class="header-left">
|
||
<h1>💧 건조시럽 환산계수 관리</h1>
|
||
<p>건조시럽 mL → g 환산계수 데이터 관리</p>
|
||
</div>
|
||
<nav class="header-nav">
|
||
<a href="/admin">관리자 홈</a>
|
||
<a href="/pmr">PMR</a>
|
||
</nav>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- 컨텐츠 -->
|
||
<div class="content">
|
||
<!-- 에러 배너 -->
|
||
<div id="errorBanner" class="error-banner hidden">
|
||
<span class="icon">⚠️</span>
|
||
<span id="errorMessage">PostgreSQL 연결에 실패했습니다.</span>
|
||
</div>
|
||
|
||
<!-- 액션 바 -->
|
||
<div class="action-bar">
|
||
<div class="search-group">
|
||
<input type="text" id="searchInput" placeholder="성분명, 제품명, 성분코드로 검색..." autocomplete="off">
|
||
<button class="btn btn-primary" onclick="loadData()">🔍 검색</button>
|
||
</div>
|
||
<button class="btn btn-success" onclick="openCreateModal()">➕ 신규 등록</button>
|
||
</div>
|
||
|
||
<!-- 통계 -->
|
||
<div class="stats-row">
|
||
<div class="stat-card purple">
|
||
<div class="stat-value" id="statTotal">-</div>
|
||
<div class="stat-label">전체 등록</div>
|
||
</div>
|
||
<div class="stat-card cyan">
|
||
<div class="stat-value" id="statCold">-</div>
|
||
<div class="stat-label">냉장보관</div>
|
||
</div>
|
||
<div class="stat-card amber">
|
||
<div class="stat-value" id="statRoom">-</div>
|
||
<div class="stat-label">실온보관</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 테이블 -->
|
||
<div class="table-container">
|
||
<div class="table-header">
|
||
<div class="table-title">
|
||
<span>환산계수 목록</span>
|
||
<span class="badge" id="countBadge">0건</span>
|
||
</div>
|
||
</div>
|
||
<div id="tableWrapper">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 120px;">성분코드</th>
|
||
<th>성분명</th>
|
||
<th>제품명</th>
|
||
<th style="width: 100px;">환산계수</th>
|
||
<th style="width: 100px;">보관조건</th>
|
||
<th style="width: 100px;">유효기간</th>
|
||
<th style="width: 130px;">관리</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="dataBody">
|
||
<tr>
|
||
<td colspan="7">
|
||
<div class="empty-state">
|
||
<div class="loading-spinner"></div>
|
||
<p style="margin-top: 12px;">데이터 로딩 중...</p>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 등록/수정 모달 -->
|
||
<div class="modal-overlay" id="editModal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h2 id="modalTitle">건조시럽 등록</h2>
|
||
<button class="modal-close" onclick="closeModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<input type="hidden" id="editMode" value="create">
|
||
|
||
<div class="form-group">
|
||
<label>성분코드 (SUNG_CODE) *</label>
|
||
<input type="text" id="formSungCode" placeholder="예: 535000ASY">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>성분명</label>
|
||
<input type="text" id="formIngredientName" placeholder="예: 아목시실린수화물·클라불란산칼륨">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>제품명</label>
|
||
<input type="text" id="formProductName" placeholder="예: 일성오구멘틴듀오시럽">
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>환산계수 (g/mL)</label>
|
||
<input type="number" id="formConversionFactor" step="0.001" placeholder="예: 0.11">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>조제 후 유효기간</label>
|
||
<input type="text" id="formExpiration" placeholder="예: 7일">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>조제 후 함량</label>
|
||
<input type="text" id="formPostPrepAmount" placeholder="예: 228mg/5ml">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>분말 중 주성분량</label>
|
||
<input type="text" id="formMainIngredientAmt" placeholder="예: 200mg/g">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>보관조건</label>
|
||
<select id="formStorageConditions">
|
||
<option value="실온">실온</option>
|
||
<option value="냉장">냉장</option>
|
||
<option value="냉동">냉동</option>
|
||
<option value="차광">차광</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="closeModal()">취소</button>
|
||
<button class="btn btn-success" onclick="saveData()">💾 저장</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 삭제 확인 모달 -->
|
||
<div class="modal-overlay" id="deleteModal">
|
||
<div class="modal" style="max-width: 400px;">
|
||
<div class="modal-header">
|
||
<h2>삭제 확인</h2>
|
||
<button class="modal-close" onclick="closeDeleteModal()">×</button>
|
||
</div>
|
||
<div class="modal-body" style="text-align: center;">
|
||
<div style="font-size: 48px; margin-bottom: 16px;">🗑️</div>
|
||
<p style="color: var(--text-secondary); margin-bottom: 8px;">
|
||
<strong id="deleteTarget" style="color: var(--accent-rose);"></strong>
|
||
</p>
|
||
<p style="color: var(--text-muted); font-size: 13px;">
|
||
이 항목을 정말 삭제하시겠습니까?<br>삭제 후 복구할 수 없습니다.
|
||
</p>
|
||
</div>
|
||
<div class="modal-footer" style="justify-content: center;">
|
||
<button class="btn btn-secondary" onclick="closeDeleteModal()">취소</button>
|
||
<button class="btn btn-danger" onclick="confirmDelete()">🗑️ 삭제</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 토스트 컨테이너 -->
|
||
<div class="toast-container" id="toastContainer"></div>
|
||
|
||
<script>
|
||
// 전역 변수
|
||
let allData = [];
|
||
let deleteSungCode = null;
|
||
|
||
// 페이지 로드 시
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadData();
|
||
|
||
// 엔터키로 검색
|
||
document.getElementById('searchInput').addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') loadData();
|
||
});
|
||
});
|
||
|
||
// 데이터 로드
|
||
async function loadData() {
|
||
const searchQuery = document.getElementById('searchInput').value.trim();
|
||
const url = searchQuery
|
||
? `/api/drug-info/drysyrup?q=${encodeURIComponent(searchQuery)}`
|
||
: '/api/drug-info/drysyrup';
|
||
|
||
try {
|
||
const response = await fetch(url);
|
||
const result = await response.json();
|
||
|
||
if (!result.success) {
|
||
showError(result.error || 'PostgreSQL 연결에 실패했습니다.');
|
||
renderEmptyState('데이터베이스 연결 오류');
|
||
return;
|
||
}
|
||
|
||
hideError();
|
||
allData = result.data || [];
|
||
renderTable(allData);
|
||
updateStats(allData);
|
||
|
||
} catch (error) {
|
||
console.error('데이터 로드 오류:', error);
|
||
showError('서버 연결에 실패했습니다.');
|
||
renderEmptyState('서버 연결 오류');
|
||
}
|
||
}
|
||
|
||
// 테이블 렌더링
|
||
function renderTable(data) {
|
||
const tbody = document.getElementById('dataBody');
|
||
document.getElementById('countBadge').textContent = `${data.length}건`;
|
||
|
||
if (data.length === 0) {
|
||
renderEmptyState('등록된 환산계수가 없습니다');
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = data.map(item => `
|
||
<tr>
|
||
<td class="code-cell">${escapeHtml(item.sung_code || '')}</td>
|
||
<td>${escapeHtml(item.ingredient_name || '-')}</td>
|
||
<td>${escapeHtml(item.product_name || '-')}</td>
|
||
<td class="factor-cell">${item.conversion_factor !== null ? item.conversion_factor.toFixed(3) : '-'}</td>
|
||
<td>
|
||
<span class="storage-badge ${getStorageClass(item.storage_conditions)}">
|
||
${escapeHtml(item.storage_conditions || '실온')}
|
||
</span>
|
||
</td>
|
||
<td>${escapeHtml(item.expiration_date || '-')}</td>
|
||
<td>
|
||
<div class="action-btns">
|
||
<button class="btn-edit" onclick="openEditModal('${escapeHtml(item.sung_code)}')">✏️ 수정</button>
|
||
<button class="btn-delete" onclick="openDeleteModal('${escapeHtml(item.sung_code)}')">🗑️</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
// 빈 상태 렌더링
|
||
function renderEmptyState(message) {
|
||
document.getElementById('dataBody').innerHTML = `
|
||
<tr>
|
||
<td colspan="7">
|
||
<div class="empty-state">
|
||
<div class="icon">📭</div>
|
||
<h3>${escapeHtml(message)}</h3>
|
||
<p>신규 등록 버튼을 눌러 환산계수를 추가하세요.</p>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
document.getElementById('countBadge').textContent = '0건';
|
||
}
|
||
|
||
// 통계 업데이트
|
||
function updateStats(data) {
|
||
const total = data.length;
|
||
const cold = data.filter(d => (d.storage_conditions || '').includes('냉')).length;
|
||
const room = total - cold;
|
||
|
||
document.getElementById('statTotal').textContent = total.toLocaleString();
|
||
document.getElementById('statCold').textContent = cold.toLocaleString();
|
||
document.getElementById('statRoom').textContent = room.toLocaleString();
|
||
}
|
||
|
||
// 보관조건 클래스
|
||
function getStorageClass(storage) {
|
||
if (!storage) return 'room';
|
||
return storage.includes('냉') ? 'cold' : 'room';
|
||
}
|
||
|
||
// 신규 등록 모달 열기
|
||
function openCreateModal() {
|
||
document.getElementById('modalTitle').textContent = '건조시럽 신규 등록';
|
||
document.getElementById('editMode').value = 'create';
|
||
document.getElementById('formSungCode').value = '';
|
||
document.getElementById('formSungCode').disabled = false;
|
||
document.getElementById('formIngredientName').value = '';
|
||
document.getElementById('formProductName').value = '';
|
||
document.getElementById('formConversionFactor').value = '';
|
||
document.getElementById('formExpiration').value = '';
|
||
document.getElementById('formPostPrepAmount').value = '';
|
||
document.getElementById('formMainIngredientAmt').value = '';
|
||
document.getElementById('formStorageConditions').value = '실온';
|
||
|
||
document.getElementById('editModal').classList.add('active');
|
||
}
|
||
|
||
// 수정 모달 열기
|
||
async function openEditModal(sungCode) {
|
||
try {
|
||
const response = await fetch(`/api/drug-info/drysyrup/${encodeURIComponent(sungCode)}`);
|
||
const result = await response.json();
|
||
|
||
if (!result.success || !result.exists) {
|
||
showToast('데이터를 불러올 수 없습니다.', 'error');
|
||
return;
|
||
}
|
||
|
||
document.getElementById('modalTitle').textContent = '건조시럽 수정';
|
||
document.getElementById('editMode').value = 'edit';
|
||
document.getElementById('formSungCode').value = result.sung_code;
|
||
document.getElementById('formSungCode').disabled = true;
|
||
document.getElementById('formIngredientName').value = result.ingredient_name || '';
|
||
document.getElementById('formProductName').value = result.product_name || '';
|
||
document.getElementById('formConversionFactor').value = result.conversion_factor || '';
|
||
document.getElementById('formExpiration').value = result.expiration_date || '';
|
||
document.getElementById('formPostPrepAmount').value = result.post_prep_amount || '';
|
||
document.getElementById('formMainIngredientAmt').value = result.main_ingredient_amt || '';
|
||
document.getElementById('formStorageConditions').value = result.storage_conditions || '실온';
|
||
|
||
document.getElementById('editModal').classList.add('active');
|
||
|
||
} catch (error) {
|
||
console.error('수정 모달 오류:', error);
|
||
showToast('데이터 로드 실패', 'error');
|
||
}
|
||
}
|
||
|
||
// 모달 닫기
|
||
function closeModal() {
|
||
document.getElementById('editModal').classList.remove('active');
|
||
}
|
||
|
||
// 데이터 저장
|
||
async function saveData() {
|
||
const mode = document.getElementById('editMode').value;
|
||
const sungCode = document.getElementById('formSungCode').value.trim();
|
||
|
||
if (!sungCode) {
|
||
showToast('성분코드는 필수입니다.', 'error');
|
||
return;
|
||
}
|
||
|
||
const data = {
|
||
sung_code: sungCode,
|
||
ingredient_name: document.getElementById('formIngredientName').value.trim(),
|
||
product_name: document.getElementById('formProductName').value.trim(),
|
||
conversion_factor: parseFloat(document.getElementById('formConversionFactor').value) || null,
|
||
expiration_date: document.getElementById('formExpiration').value.trim(),
|
||
post_prep_amount: document.getElementById('formPostPrepAmount').value.trim(),
|
||
main_ingredient_amt: document.getElementById('formMainIngredientAmt').value.trim(),
|
||
storage_conditions: document.getElementById('formStorageConditions').value
|
||
};
|
||
|
||
try {
|
||
const url = mode === 'create'
|
||
? '/api/drug-info/drysyrup'
|
||
: `/api/drug-info/drysyrup/${encodeURIComponent(sungCode)}`;
|
||
const method = mode === 'create' ? 'POST' : 'PUT';
|
||
|
||
const response = await fetch(url, {
|
||
method: method,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showToast(mode === 'create' ? '등록 완료' : '수정 완료', 'success');
|
||
closeModal();
|
||
loadData();
|
||
} else {
|
||
showToast(result.error || '저장 실패', 'error');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('저장 오류:', error);
|
||
showToast('서버 오류', 'error');
|
||
}
|
||
}
|
||
|
||
// 삭제 모달 열기
|
||
function openDeleteModal(sungCode) {
|
||
deleteSungCode = sungCode;
|
||
document.getElementById('deleteTarget').textContent = sungCode;
|
||
document.getElementById('deleteModal').classList.add('active');
|
||
}
|
||
|
||
// 삭제 모달 닫기
|
||
function closeDeleteModal() {
|
||
document.getElementById('deleteModal').classList.remove('active');
|
||
deleteSungCode = null;
|
||
}
|
||
|
||
// 삭제 확인
|
||
async function confirmDelete() {
|
||
if (!deleteSungCode) return;
|
||
|
||
try {
|
||
const response = await fetch(`/api/drug-info/drysyrup/${encodeURIComponent(deleteSungCode)}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showToast('삭제 완료', 'success');
|
||
closeDeleteModal();
|
||
loadData();
|
||
} else {
|
||
showToast(result.error || '삭제 실패', 'error');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('삭제 오류:', error);
|
||
showToast('서버 오류', 'error');
|
||
}
|
||
}
|
||
|
||
// 에러 표시/숨김
|
||
function showError(message) {
|
||
document.getElementById('errorMessage').textContent = message;
|
||
document.getElementById('errorBanner').classList.remove('hidden');
|
||
}
|
||
|
||
function hideError() {
|
||
document.getElementById('errorBanner').classList.add('hidden');
|
||
}
|
||
|
||
// 토스트 메시지
|
||
function showToast(message, type = 'success') {
|
||
const container = document.getElementById('toastContainer');
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast ${type}`;
|
||
toast.innerHTML = `
|
||
<span>${type === 'success' ? '✅' : '❌'}</span>
|
||
<span>${escapeHtml(message)}</span>
|
||
`;
|
||
container.appendChild(toast);
|
||
|
||
setTimeout(() => {
|
||
toast.style.opacity = '0';
|
||
setTimeout(() => toast.remove(), 300);
|
||
}, 3000);
|
||
}
|
||
|
||
// HTML 이스케이프
|
||
function escapeHtml(text) {
|
||
if (!text) return '';
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|