790 lines
29 KiB
HTML
790 lines
29 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">
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<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;
|
|
}
|
|
|
|
* { 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, #0891b2 0%, #06b6d4 50%, #22d3ee 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: 1400px;
|
|
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: 1400px;
|
|
margin: 0 auto;
|
|
padding: 24px;
|
|
}
|
|
|
|
/* ══════════════════ 검색 영역 ══════════════════ */
|
|
.search-section {
|
|
background: var(--bg-card);
|
|
border-radius: 12px;
|
|
padding: 20px 24px;
|
|
margin-bottom: 20px;
|
|
border: 1px solid var(--border);
|
|
}
|
|
.search-row {
|
|
display: flex;
|
|
gap: 16px;
|
|
flex-wrap: wrap;
|
|
align-items: flex-end;
|
|
}
|
|
.search-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
.search-group label {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
}
|
|
.search-group input, .search-group select {
|
|
padding: 10px 14px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-family: inherit;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
min-width: 200px;
|
|
}
|
|
.search-group input:focus, .search-group select:focus {
|
|
outline: none;
|
|
border-color: var(--accent-teal);
|
|
box-shadow: 0 0 0 3px rgba(20, 184, 166, 0.2);
|
|
}
|
|
|
|
/* ══════════════════ 약품 검색 자동완성 ══════════════════ */
|
|
.drug-search-wrap {
|
|
position: relative;
|
|
flex: 1;
|
|
min-width: 280px;
|
|
}
|
|
.drug-search-wrap input {
|
|
width: 100%;
|
|
}
|
|
.drug-search-results {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
max-height: 320px;
|
|
overflow-y: auto;
|
|
z-index: 50;
|
|
display: none;
|
|
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
|
margin-top: 4px;
|
|
}
|
|
.drug-search-results.show {
|
|
display: block;
|
|
}
|
|
.drug-item {
|
|
padding: 12px 16px;
|
|
cursor: pointer;
|
|
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
transition: background 0.2s;
|
|
}
|
|
.drug-item:hover {
|
|
background: var(--bg-card-hover);
|
|
}
|
|
.drug-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.drug-item-name {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
margin-bottom: 4px;
|
|
}
|
|
.drug-item-info {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
.drug-item-barcode {
|
|
color: var(--accent-teal);
|
|
}
|
|
.search-btn {
|
|
background: var(--accent-teal);
|
|
color: #fff;
|
|
border: none;
|
|
padding: 10px 24px;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
.search-btn:hover {
|
|
background: #0d9488;
|
|
transform: translateY(-1px);
|
|
}
|
|
.search-btn:disabled {
|
|
background: var(--text-muted);
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
/* ══════════════════ 통계 카드 ══════════════════ */
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 24px;
|
|
}
|
|
.stat-card {
|
|
background: var(--bg-card);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
border: 1px solid var(--border);
|
|
}
|
|
.stat-card .label {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 8px;
|
|
}
|
|
.stat-card .value {
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
.stat-card .sub {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
margin-top: 4px;
|
|
}
|
|
.stat-card.teal .value { color: var(--accent-teal); }
|
|
.stat-card.blue .value { color: var(--accent-blue); }
|
|
.stat-card.amber .value { color: var(--accent-amber); }
|
|
.stat-card.emerald .value { color: var(--accent-emerald); }
|
|
.stat-card.rose .value { color: var(--accent-rose); }
|
|
|
|
/* ══════════════════ 차트 영역 ══════════════════ */
|
|
.chart-section {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 20px;
|
|
margin-bottom: 24px;
|
|
}
|
|
@media (max-width: 1000px) {
|
|
.chart-section { grid-template-columns: 1fr; }
|
|
}
|
|
.chart-card {
|
|
background: var(--bg-card);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
border: 1px solid var(--border);
|
|
}
|
|
.chart-card h3 {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.chart-container {
|
|
position: relative;
|
|
height: 280px;
|
|
}
|
|
|
|
/* ══════════════════ 데이터 테이블 ══════════════════ */
|
|
.table-section {
|
|
background: var(--bg-card);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
border: 1px solid var(--border);
|
|
}
|
|
.table-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 16px;
|
|
}
|
|
.table-header h3 {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
}
|
|
.table-wrapper {
|
|
overflow-x: auto;
|
|
}
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
th, td {
|
|
padding: 12px 16px;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
th {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
td {
|
|
font-size: 14px;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
tr:hover {
|
|
background: var(--bg-card-hover);
|
|
}
|
|
.price-up { color: var(--accent-rose); }
|
|
.price-down { color: var(--accent-emerald); }
|
|
.price-same { color: var(--text-muted); }
|
|
|
|
/* ══════════════════ 빈 상태 ══════════════════ */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
color: var(--text-muted);
|
|
}
|
|
.empty-state .icon {
|
|
font-size: 48px;
|
|
margin-bottom: 16px;
|
|
}
|
|
.empty-state p {
|
|
font-size: 14px;
|
|
}
|
|
|
|
/* ══════════════════ 로딩 ══════════════════ */
|
|
.loading {
|
|
display: none;
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: var(--text-secondary);
|
|
}
|
|
.loading.active { display: block; }
|
|
.spinner {
|
|
width: 32px;
|
|
height: 32px;
|
|
border: 3px solid var(--border);
|
|
border-top-color: var(--accent-teal);
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto 12px;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
|
/* ══════════════════ 제품 정보 ══════════════════ */
|
|
.product-info {
|
|
background: var(--bg-card);
|
|
border-radius: 12px;
|
|
padding: 20px 24px;
|
|
margin-bottom: 20px;
|
|
border: 1px solid var(--border);
|
|
display: none;
|
|
}
|
|
.product-info.active { display: block; }
|
|
.product-info h2 {
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
color: var(--accent-teal);
|
|
margin-bottom: 8px;
|
|
}
|
|
.product-info .barcode {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 14px;
|
|
color: var(--text-muted);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header class="header">
|
|
<div class="header-inner">
|
|
<div class="header-left">
|
|
<h1>📈 가격 변동 추이</h1>
|
|
<p>제품별 판매가/마진 변화 분석</p>
|
|
</div>
|
|
<nav class="header-nav">
|
|
<a href="/admin">관리자</a>
|
|
<a href="/admin/sales/pos">POS 매출</a>
|
|
<a href="/admin/stock-analytics">재고 분석</a>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="content">
|
|
<!-- 검색 -->
|
|
<section class="search-section">
|
|
<div class="search-row">
|
|
<div class="search-group drug-search-wrap">
|
|
<label>바코드 또는 약품명</label>
|
|
<input type="text" id="searchQuery" placeholder="약품명 또는 바코드 입력..." autocomplete="off">
|
|
<div class="drug-search-results" id="drugSearchResults"></div>
|
|
</div>
|
|
<div class="search-group">
|
|
<label>기간</label>
|
|
<select id="periodSelect">
|
|
<option value="90">최근 3개월</option>
|
|
<option value="180">최근 6개월</option>
|
|
<option value="365" selected>최근 1년</option>
|
|
<option value="730">최근 2년</option>
|
|
<option value="0">전체 기간</option>
|
|
</select>
|
|
</div>
|
|
<button class="search-btn" id="searchBtn" onclick="searchProduct()">
|
|
🔍 조회
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 제품 정보 -->
|
|
<section class="product-info" id="productInfo">
|
|
<h2 id="productName">-</h2>
|
|
<p class="barcode">바코드: <span id="productBarcode">-</span></p>
|
|
</section>
|
|
|
|
<!-- 통계 카드 -->
|
|
<section class="stats-grid" id="statsGrid" style="display: none;">
|
|
<div class="stat-card teal">
|
|
<div class="label">현재 판매가</div>
|
|
<div class="value" id="currentPrice">-</div>
|
|
<div class="sub" id="priceChange">-</div>
|
|
</div>
|
|
<div class="stat-card blue">
|
|
<div class="label">현재 입고가</div>
|
|
<div class="value" id="currentCost">-</div>
|
|
<div class="sub" id="costChange">-</div>
|
|
</div>
|
|
<div class="stat-card emerald">
|
|
<div class="label">현재 마진율</div>
|
|
<div class="value" id="currentMargin">-</div>
|
|
<div class="sub" id="marginRange">-</div>
|
|
</div>
|
|
<div class="stat-card amber">
|
|
<div class="label">총 판매건수</div>
|
|
<div class="value" id="totalSales">-</div>
|
|
<div class="sub" id="salesPeriod">-</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 차트 -->
|
|
<section class="chart-section" id="chartSection" style="display: none;">
|
|
<div class="chart-card">
|
|
<h3>💰 판매가 변동 추이</h3>
|
|
<div class="chart-container">
|
|
<canvas id="priceChart"></canvas>
|
|
</div>
|
|
</div>
|
|
<div class="chart-card">
|
|
<h3>📊 마진율 변동 추이</h3>
|
|
<div class="chart-container">
|
|
<canvas id="marginChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 상세 테이블 -->
|
|
<section class="table-section" id="tableSection" style="display: none;">
|
|
<div class="table-header">
|
|
<h3>📋 일별 상세 내역</h3>
|
|
</div>
|
|
<div class="table-wrapper">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>날짜</th>
|
|
<th>판매가</th>
|
|
<th>입고가</th>
|
|
<th>마진</th>
|
|
<th>마진율</th>
|
|
<th>판매건수</th>
|
|
<th>변동</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="dataTable">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 로딩 -->
|
|
<div class="loading" id="loading">
|
|
<div class="spinner"></div>
|
|
<p>데이터 조회 중...</p>
|
|
</div>
|
|
|
|
<!-- 빈 상태 -->
|
|
<div class="empty-state" id="emptyState">
|
|
<div class="icon">📊</div>
|
|
<p>바코드 또는 약품명을 검색하여<br>가격 변동 추이를 확인하세요</p>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
let priceChart = null;
|
|
let marginChart = null;
|
|
let searchTimeout = null;
|
|
|
|
// 초기화
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const searchInput = document.getElementById('searchQuery');
|
|
|
|
// 입력 시 자동완성
|
|
searchInput.addEventListener('input', function() {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => searchDrugs(this.value), 300);
|
|
});
|
|
|
|
// 포커스 시 결과 표시
|
|
searchInput.addEventListener('focus', function() {
|
|
if (this.value.length >= 2) {
|
|
searchDrugs(this.value);
|
|
}
|
|
});
|
|
|
|
// 외부 클릭 시 드롭다운 숨기기
|
|
document.addEventListener('click', function(e) {
|
|
if (!e.target.closest('.drug-search-wrap')) {
|
|
document.getElementById('drugSearchResults').classList.remove('show');
|
|
}
|
|
});
|
|
|
|
// 엔터키 검색
|
|
searchInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
document.getElementById('drugSearchResults').classList.remove('show');
|
|
searchProduct();
|
|
}
|
|
});
|
|
});
|
|
|
|
// 약품 자동완성 검색
|
|
async function searchDrugs(query) {
|
|
const resultsDiv = document.getElementById('drugSearchResults');
|
|
|
|
if (!query || query.length < 2) {
|
|
resultsDiv.classList.remove('show');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/price-trend/search?q=${encodeURIComponent(query)}&limit=15`);
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.items.length > 0) {
|
|
resultsDiv.innerHTML = data.items.map(item => `
|
|
<div class="drug-item" onclick="selectDrug('${escapeHtml(item.barcode)}', '${escapeHtml(item.product_name)}')">
|
|
<div class="drug-item-name">${escapeHtml(item.product_name)}</div>
|
|
<div class="drug-item-info">
|
|
바코드: <span class="drug-item-barcode">${item.barcode}</span>
|
|
· 판매건수: ${item.sale_count.toLocaleString()}건
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
resultsDiv.classList.add('show');
|
|
} else {
|
|
resultsDiv.innerHTML = '<div class="drug-item"><div class="drug-item-name" style="color:var(--text-muted)">검색 결과 없음</div></div>';
|
|
resultsDiv.classList.add('show');
|
|
}
|
|
} catch (err) {
|
|
console.error('약품 검색 실패:', err);
|
|
}
|
|
}
|
|
|
|
function selectDrug(barcode, productName) {
|
|
document.getElementById('searchQuery').value = barcode;
|
|
document.getElementById('drugSearchResults').classList.remove('show');
|
|
searchProduct();
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text || '';
|
|
return div.innerHTML;
|
|
}
|
|
|
|
async function searchProduct() {
|
|
const query = document.getElementById('searchQuery').value.trim();
|
|
const period = document.getElementById('periodSelect').value;
|
|
|
|
if (!query) {
|
|
alert('바코드 또는 약품명을 입력하세요');
|
|
return;
|
|
}
|
|
|
|
// UI 초기화
|
|
document.getElementById('emptyState').style.display = 'none';
|
|
document.getElementById('loading').classList.add('active');
|
|
document.getElementById('productInfo').classList.remove('active');
|
|
document.getElementById('statsGrid').style.display = 'none';
|
|
document.getElementById('chartSection').style.display = 'none';
|
|
document.getElementById('tableSection').style.display = 'none';
|
|
document.getElementById('searchBtn').disabled = true;
|
|
|
|
try {
|
|
const response = await fetch(`/api/price-trend?query=${encodeURIComponent(query)}&period=${period}`);
|
|
const data = await response.json();
|
|
|
|
if (!data.success) {
|
|
throw new Error(data.error || '조회 실패');
|
|
}
|
|
|
|
if (!data.data || data.data.length === 0) {
|
|
document.getElementById('emptyState').innerHTML = `
|
|
<div class="icon">🔍</div>
|
|
<p>"${query}"에 대한 판매 기록이 없습니다</p>
|
|
`;
|
|
document.getElementById('emptyState').style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
// 데이터 표시
|
|
displayData(data);
|
|
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
document.getElementById('emptyState').innerHTML = `
|
|
<div class="icon">⚠️</div>
|
|
<p>오류: ${error.message}</p>
|
|
`;
|
|
document.getElementById('emptyState').style.display = 'block';
|
|
} finally {
|
|
document.getElementById('loading').classList.remove('active');
|
|
document.getElementById('searchBtn').disabled = false;
|
|
}
|
|
}
|
|
|
|
function displayData(result) {
|
|
const data = result.data;
|
|
const stats = result.stats;
|
|
const productName = result.product_name || '알 수 없음';
|
|
const barcode = result.barcode;
|
|
|
|
// 제품 정보
|
|
document.getElementById('productName').textContent = productName;
|
|
document.getElementById('productBarcode').textContent = barcode;
|
|
document.getElementById('productInfo').classList.add('active');
|
|
|
|
// 통계
|
|
document.getElementById('currentPrice').textContent = formatNumber(stats.current_price) + '원';
|
|
document.getElementById('currentCost').textContent = formatNumber(stats.current_cost) + '원';
|
|
document.getElementById('currentMargin').textContent = stats.current_margin.toFixed(1) + '%';
|
|
document.getElementById('totalSales').textContent = formatNumber(stats.total_count) + '건';
|
|
|
|
document.getElementById('priceChange').textContent =
|
|
`범위: ${formatNumber(stats.min_price)}원 ~ ${formatNumber(stats.max_price)}원`;
|
|
document.getElementById('costChange').textContent =
|
|
`범위: ${formatNumber(stats.min_cost)}원 ~ ${formatNumber(stats.max_cost)}원`;
|
|
document.getElementById('marginRange').textContent =
|
|
`범위: ${stats.min_margin.toFixed(1)}% ~ ${stats.max_margin.toFixed(1)}%`;
|
|
document.getElementById('salesPeriod').textContent =
|
|
`${stats.first_date} ~ ${stats.last_date}`;
|
|
|
|
document.getElementById('statsGrid').style.display = 'grid';
|
|
|
|
// 차트 데이터 준비
|
|
const labels = data.map(d => d.date.substring(0, 10));
|
|
const prices = data.map(d => d.avg_price);
|
|
const margins = data.map(d => d.margin_rate);
|
|
|
|
// 판매가 차트
|
|
if (priceChart) priceChart.destroy();
|
|
const priceCtx = document.getElementById('priceChart').getContext('2d');
|
|
priceChart = new Chart(priceCtx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: '판매가',
|
|
data: prices,
|
|
borderColor: '#14b8a6',
|
|
backgroundColor: 'rgba(20, 184, 166, 0.1)',
|
|
fill: true,
|
|
tension: 0.3,
|
|
pointRadius: 3,
|
|
pointHoverRadius: 6
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { display: false }
|
|
},
|
|
scales: {
|
|
x: {
|
|
ticks: { color: '#64748b', maxTicksLimit: 10 },
|
|
grid: { color: '#334155' }
|
|
},
|
|
y: {
|
|
ticks: {
|
|
color: '#64748b',
|
|
callback: v => formatNumber(v) + '원'
|
|
},
|
|
grid: { color: '#334155' }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// 마진율 차트
|
|
if (marginChart) marginChart.destroy();
|
|
const marginCtx = document.getElementById('marginChart').getContext('2d');
|
|
marginChart = new Chart(marginCtx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: '마진율',
|
|
data: margins,
|
|
borderColor: '#f59e0b',
|
|
backgroundColor: 'rgba(245, 158, 11, 0.1)',
|
|
fill: true,
|
|
tension: 0.3,
|
|
pointRadius: 3,
|
|
pointHoverRadius: 6
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { display: false }
|
|
},
|
|
scales: {
|
|
x: {
|
|
ticks: { color: '#64748b', maxTicksLimit: 10 },
|
|
grid: { color: '#334155' }
|
|
},
|
|
y: {
|
|
ticks: {
|
|
color: '#64748b',
|
|
callback: v => v.toFixed(1) + '%'
|
|
},
|
|
grid: { color: '#334155' }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
document.getElementById('chartSection').style.display = 'grid';
|
|
|
|
// 테이블
|
|
const tbody = document.getElementById('dataTable');
|
|
tbody.innerHTML = '';
|
|
|
|
let prevPrice = null;
|
|
data.forEach((row, idx) => {
|
|
let changeClass = 'price-same';
|
|
let changeText = '-';
|
|
|
|
if (idx > 0 && prevPrice !== null) {
|
|
if (row.avg_price > prevPrice) {
|
|
changeClass = 'price-up';
|
|
changeText = '↑ ' + formatNumber(row.avg_price - prevPrice);
|
|
} else if (row.avg_price < prevPrice) {
|
|
changeClass = 'price-down';
|
|
changeText = '↓ ' + formatNumber(prevPrice - row.avg_price);
|
|
}
|
|
}
|
|
prevPrice = row.avg_price;
|
|
|
|
tbody.innerHTML += `
|
|
<tr>
|
|
<td>${row.date}</td>
|
|
<td>${formatNumber(row.avg_price)}원</td>
|
|
<td>${formatNumber(row.avg_cost)}원</td>
|
|
<td>${formatNumber(row.avg_margin)}원</td>
|
|
<td>${row.margin_rate.toFixed(1)}%</td>
|
|
<td>${row.count}건</td>
|
|
<td class="${changeClass}">${changeText}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
document.getElementById('tableSection').style.display = 'block';
|
|
}
|
|
|
|
function formatNumber(num) {
|
|
return Math.round(num).toLocaleString('ko-KR');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|