pharmacy-pos-qr-system/backend/templates/admin_drysyrup.html
thug0bin 2cc9ec6bb1 feat(admin): 건조시럽 환산계수 관리 페이지 추가
- /admin/drysyrup 페이지 구현
- GET /api/drug-info/drysyrup 전체 목록 API
- DELETE /api/drug-info/drysyrup/<sung_code> 삭제 API
- 검색, CRUD, 통계 카드 기능
2026-03-12 17:15:28 +09:00

981 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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()">&times;</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()">&times;</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>