stock / templates /index.html
seawolf2357's picture
Update templates/index.html
92a70f1 verified
Raw
History Blame Contribute Delete
114 kB
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>๐Ÿ“ˆ AI ์ฃผ์‹ ๋ฆฌ์„œ์น˜ ์„ผํ„ฐ v5.5</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<!-- TradingView Widget -->
<script type="text/javascript" src="https://s3.tradingview.com/tv.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', sans-serif; background: linear-gradient(135deg, #0a0a1a 0%, #1a1a2e 100%); padding: 20px; color: #e8e8e8; min-height: 100vh; }
.container { max-width: 1600px; margin: 0 auto; background: rgba(26, 26, 46, 0.95); border-radius: 20px; padding: 40px; box-shadow: 0 20px 60px rgba(0,0,0,0.5); border: 1px solid rgba(0,217,255,0.2); }
h1 { text-align: center; background: linear-gradient(135deg, #00d9ff, #00ff88); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 10px; font-size: 2.5em; }
.subtitle { text-align: center; color: #888; margin-bottom: 30px; }
.indices-bar { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; margin-bottom: 30px; }
.index-card { background: linear-gradient(135deg, rgba(0,217,255,0.1), rgba(0,255,136,0.05)); border: 1px solid rgba(0,217,255,0.3); border-radius: 15px; padding: 15px; text-align: center; }
.index-name { font-size: 0.85em; color: #888; margin-bottom: 5px; }
.index-value { font-size: 1.4em; font-weight: 700; color: #fff; }
.index-change { font-size: 0.9em; font-weight: 600; margin-top: 5px; }
.positive { color: #00ff88; }
.negative { color: #ff4757; }
.tabs { display: flex; gap: 10px; margin-bottom: 30px; border-bottom: 2px solid rgba(255,255,255,0.1); overflow-x: auto; }
.tab { padding: 15px 30px; background: transparent; border: none; cursor: pointer; font-size: 1em; font-weight: 600; color: #888; border-radius: 10px 10px 0 0; white-space: nowrap; }
.tab.active { background: linear-gradient(135deg, #00d9ff, #00ff88); color: #1a1a2e; }
.tab:hover:not(.active) { background: rgba(255,255,255,0.1); color: #fff; }
.tab-content { display: none; animation: fadeIn 0.5s; }
.tab-content.active { display: block; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
/* ์„ผํ‹ฐ๋จผํŠธ ๊ฒŒ์ด์ง€ */
.sentiment-section { background: rgba(255,255,255,0.03); border-radius: 15px; padding: 25px; margin-bottom: 30px; border: 1px solid rgba(255,255,255,0.1); }
.sentiment-title { color: #00d9ff; font-size: 1.2em; font-weight: 700; margin-bottom: 20px; text-align: center; }
.gauge-container { display: flex; justify-content: center; align-items: center; flex-direction: column; }
.gauge-wrapper { position: relative; width: 300px; height: 180px; }
.gauge-labels { display: flex; justify-content: space-between; width: 100%; margin-top: 10px; font-size: 0.7em; color: #888; }
.gauge-value { text-align: center; margin-top: 15px; }
.gauge-value .number { font-size: 2em; font-weight: 700; }
.gauge-value .label { font-size: 1.1em; margin-top: 5px; }
/* ์Šคํƒ ํžˆํŠธ๋งต (finviz ์Šคํƒ€์ผ) */
.heatmap-section { margin-bottom: 30px; }
.heatmap-container { background: rgba(0,0,0,0.3); border-radius: 15px; padding: 20px; border: 1px solid rgba(255,255,255,0.1); }
.heatmap-title { color: #00d9ff; font-size: 1.1em; font-weight: 700; margin-bottom: 15px; }
.stock-heatmap { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 4px; }
.heatmap-cell { padding: 12px 8px; border-radius: 4px; text-align: center; cursor: pointer; transition: all 0.3s; position: relative; min-height: 70px; display: flex; flex-direction: column; justify-content: center; }
.heatmap-cell:hover { transform: scale(1.05); z-index: 10; box-shadow: 0 5px 20px rgba(0,0,0,0.5); }
.heatmap-cell .ticker { font-weight: 700; font-size: 0.9em; color: #fff; }
.heatmap-cell .change { font-size: 0.8em; font-weight: 600; margin-top: 4px; }
.heatmap-cell .price { font-size: 0.7em; color: rgba(255,255,255,0.7); margin-top: 2px; }
.heatmap-cell.strong-up { background: linear-gradient(135deg, #00c853, #00e676); }
.heatmap-cell.up { background: linear-gradient(135deg, #4caf50, #66bb6a); }
.heatmap-cell.slight-up { background: linear-gradient(135deg, #8bc34a, #9ccc65); }
.heatmap-cell.neutral { background: linear-gradient(135deg, #607d8b, #78909c); }
.heatmap-cell.slight-down { background: linear-gradient(135deg, #ff9800, #ffa726); }
.heatmap-cell.down { background: linear-gradient(135deg, #f44336, #ef5350); }
.heatmap-cell.strong-down { background: linear-gradient(135deg, #c62828, #d32f2f); }
/* ์ด์Šˆ ํ‚ค์›Œ๋“œ ํƒœ๊ทธ ํด๋ผ์šฐ๋“œ */
.keyword-cloud { display: flex; flex-wrap: wrap; gap: 10px; padding: 20px; background: rgba(255,255,255,0.03); border-radius: 15px; margin-bottom: 30px; }
.keyword-tag { padding: 8px 16px; border-radius: 20px; font-weight: 600; cursor: pointer; transition: all 0.3s; }
.keyword-tag:hover { transform: scale(1.1); }
.keyword-tag.size-xl { font-size: 1.3em; padding: 12px 24px; }
.keyword-tag.size-lg { font-size: 1.1em; padding: 10px 20px; }
.keyword-tag.size-md { font-size: 0.95em; }
.keyword-tag.size-sm { font-size: 0.85em; padding: 6px 12px; }
/* ํ‹ฐ์ปค ํ…Œ์ดํ”„ */
.ticker-tape-container { margin: -40px -40px 0 -40px; border-radius: 20px 20px 0 0; overflow: hidden; }
/* ๋งˆ์ผ“ ์˜ค๋ฒ„๋ทฐ ๊ทธ๋ฆฌ๋“œ */
.market-overview-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin-bottom: 30px; }
@media (max-width: 1200px) { .market-overview-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 768px) { .market-overview-grid { grid-template-columns: 1fr; } }
.market-card { background: rgba(255,255,255,0.03); border-radius: 15px; padding: 20px; border: 1px solid rgba(255,255,255,0.1); display: flex; flex-direction: column; align-items: center; }
.market-card-title { color: #00d9ff; font-size: 1em; font-weight: 700; margin-bottom: 15px; text-align: center; }
.chart-container { width: 100%; height: 180px; position: relative; }
.chart-legend { display: flex; justify-content: center; gap: 15px; margin-top: 10px; font-size: 0.85em; }
.legend-item { display: flex; align-items: center; gap: 5px; }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; }
/* ์œ„์ ฏ ์„น์…˜ */
.widget-section { margin-bottom: 30px; }
.widget-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; }
@media (max-width: 992px) { .widget-grid { grid-template-columns: 1fr; } }
.widget-card { background: rgba(0,0,0,0.3); border-radius: 15px; overflow: hidden; border: 1px solid rgba(255,255,255,0.1); min-height: 400px; }
.widget-card-full { background: rgba(0,0,0,0.3); border-radius: 15px; overflow: hidden; border: 1px solid rgba(255,255,255,0.1); width: 100%; }
/* ํžˆํŠธ๋งต ํƒญ */
.heatmap-tabs { display: flex; gap: 8px; }
.heatmap-tab { padding: 8px 16px; background: rgba(255,255,255,0.1); border: none; border-radius: 20px; color: #888; cursor: pointer; font-size: 0.9em; font-weight: 600; transition: all 0.3s; }
.heatmap-tab:hover { background: rgba(255,255,255,0.2); color: #fff; }
.heatmap-tab.active { background: linear-gradient(135deg, #00d9ff, #00ff88); color: #1a1a2e; }
/* ํŽ˜์ด์ง€๋„ค์ด์…˜ ํƒญ */
.pagination-tabs { display: flex; gap: 8px; }
.page-tab { padding: 8px 16px; background: rgba(255,255,255,0.1); border: none; border-radius: 20px; color: #888; cursor: pointer; font-size: 0.85em; font-weight: 600; transition: all 0.3s; }
.page-tab:hover { background: rgba(255,255,255,0.2); color: #fff; }
.page-tab.active { background: linear-gradient(135deg, #9b59b6, #8e44ad); color: #fff; }
/* ์Šคํฌ๋ฆฌ๋‹ ์„œ๋ธŒ ํƒญ */
.sub-tabs { display: flex; gap: 10px; margin-bottom: 10px; border-bottom: 2px solid rgba(255,255,255,0.1); padding-bottom: 15px; }
.sub-tab { padding: 12px 30px; background: rgba(255,255,255,0.05); border: 2px solid rgba(255,255,255,0.1); border-radius: 12px; color: #888; cursor: pointer; font-size: 1em; font-weight: 700; transition: all 0.3s; }
.sub-tab:hover { background: rgba(255,255,255,0.1); color: #fff; border-color: rgba(255,255,255,0.3); }
.sub-tab.active { background: linear-gradient(135deg, #00d9ff, #00ff88); color: #1a1a2e; border-color: transparent; }
/* ์Šคํฌ๋ฆฌ๋‹ ์„œ๋ธŒ ์ฝ˜ํ…์ธ  */
.screening-sub-content { display: none; }
.screening-sub-content.active { display: block; }
/* ์Šคํฌ๋ฆฌ๋‹ ์„น์…˜ */
.screening-section { background: rgba(255,255,255,0.02); border-radius: 15px; padding: 20px; border: 1px solid rgba(255,255,255,0.1); }
/* ๋‰ด์Šค/๋ฆฌ์„œ์น˜ ์นด๋“œ */
.news-card { background: rgba(255,255,255,0.03); border-radius: 15px; padding: 25px; margin-bottom: 20px; border-left: 4px solid #00d9ff; }
.news-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 15px; flex-wrap: wrap; gap: 10px; }
.news-title { font-size: 1.15em; font-weight: 700; color: #fff; flex: 1; line-height: 1.5; }
.news-meta { display: flex; gap: 10px; flex-wrap: wrap; }
.badge { padding: 5px 12px; border-radius: 20px; font-size: 0.8em; font-weight: 600; }
.ticker-badge { background: linear-gradient(135deg, #00d9ff, #00ff88); color: #1a1a2e; }
.market-badge { background: linear-gradient(135deg, #9b59b6, #8e44ad); color: #fff; }
.impact-high { background: #ff4757; color: white; }
.impact-medium { background: #ffa502; color: #1a1a2e; }
.impact-low { background: #2ed573; color: #1a1a2e; }
.sentiment-positive { background: rgba(0,255,136,0.2); color: #00ff88; border: 1px solid #00ff88; }
.sentiment-negative { background: rgba(255,71,87,0.2); color: #ff4757; border: 1px solid #ff4757; }
.sentiment-neutral { background: rgba(255,165,2,0.2); color: #ffa502; border: 1px solid #ffa502; }
.news-summary { color: #bbb; line-height: 1.8; margin-bottom: 15px; font-size: 0.95em; }
.news-content { color: #999; line-height: 1.7; margin-bottom: 15px; font-size: 0.9em; padding: 15px; background: rgba(0,0,0,0.2); border-radius: 10px; }
.analysis-box { background: rgba(0,217,255,0.08); border-radius: 10px; padding: 15px; margin-top: 15px; }
.analysis-label { display: inline-block; background: linear-gradient(135deg, #00d9ff, #00ff88); color: #1a1a2e; padding: 5px 12px; border-radius: 15px; font-size: 0.85em; font-weight: 600; margin-bottom: 10px; }
.news-link { display: inline-block; background: linear-gradient(135deg, #00d9ff, #00ff88); color: #1a1a2e; padding: 10px 20px; border-radius: 8px; text-decoration: none; font-size: 0.9em; font-weight: 600; margin-top: 10px; transition: all 0.3s; border: none; cursor: pointer; }
.news-link:hover { transform: scale(1.05); box-shadow: 0 5px 15px rgba(0,217,255,0.3); }
.research-card { background: rgba(255,255,255,0.03); border-radius: 15px; padding: 25px; margin-bottom: 20px; border: 1px solid rgba(255,255,255,0.1); border-left: 4px solid #00ff88; }
.research-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 10px; }
.research-ticker { font-size: 1.8em; font-weight: 800; background: linear-gradient(135deg, #00d9ff, #00ff88); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.rating-badge { padding: 10px 20px; border-radius: 25px; font-size: 1em; font-weight: 700; }
.rating-strong-buy { background: linear-gradient(135deg, #00ff88, #00d9ff); color: #1a1a2e; }
.rating-buy { background: #2ed573; color: #1a1a2e; }
.rating-hold { background: #ffa502; color: #1a1a2e; }
.rating-sell { background: #ff4757; color: white; }
.metrics-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 15px; margin-bottom: 20px; }
.metric-card { background: rgba(255,255,255,0.05); padding: 15px; border-radius: 10px; text-align: center; }
.metric-label { color: #888; font-size: 0.85em; margin-bottom: 5px; }
.metric-value { font-size: 1.2em; font-weight: 700; color: #fff; }
/* ์Šค์ผ€์ค„๋Ÿฌ ์ƒํƒœ ํ‘œ์‹œ */
.scheduler-status {
display: flex; align-items: center; justify-content: center; gap: 10px;
background: rgba(155, 89, 182, 0.1); border: 1px solid rgba(155, 89, 182, 0.3);
border-radius: 25px; padding: 10px 20px; margin: 15px auto; max-width: 500px;
}
.scheduler-indicator { font-size: 1.2em; animation: pulse 2s infinite; }
.scheduler-indicator.active { color: #00ff88; }
.scheduler-indicator.inactive { color: #ff4757; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
#schedulerText { color: #bbb; font-size: 0.9em; }
.soma-agent { background: rgba(255,255,255,0.02); border-radius: 12px; padding: 20px; margin-bottom: 15px; border-left: 3px solid; }
.soma-supervisor { border-color: #9b59b6; }
.soma-researcher { border-color: #3498db; }
.soma-critic { border-color: #e74c3c; }
.soma-analyst { border-color: #00ff88; }
.soma-header { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.soma-icon { width: 35px; height: 35px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 1.1em; }
.soma-name { font-weight: 700; font-size: 0.95em; }
.soma-content { color: #bbb; line-height: 1.8; font-size: 0.95em; }
.deep-analysis-btn { background: linear-gradient(135deg, #9b59b6, #8e44ad); color: white; border: none; padding: 12px 24px; border-radius: 25px; font-size: 0.9em; font-weight: 700; cursor: pointer; transition: all 0.3s; }
.deep-analysis-btn:hover { transform: scale(1.05); box-shadow: 0 10px 30px rgba(155, 89, 182, 0.4); }
.download-btn { background: linear-gradient(135deg, #27ae60, #2ecc71); color: white; border: none; padding: 12px 24px; border-radius: 25px; font-size: 0.9em; font-weight: 700; cursor: pointer; transition: all 0.3s; text-decoration: none; margin: 5px; display: inline-flex; align-items: center; gap: 8px; }
.download-btn:hover { transform: scale(1.05); box-shadow: 0 10px 30px rgba(39, 174, 96, 0.4); }
.download-btn.pdf { background: linear-gradient(135deg, #e74c3c, #c0392b); }
/* ์Šคํฌ๋ฆฌ๋‹ ํ…Œ์ด๋ธ” */
.screening-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; }
.stock-card { background: rgba(255,255,255,0.03); border-radius: 15px; padding: 20px; border: 1px solid rgba(255,255,255,0.1); transition: all 0.3s; }
.stock-card:hover { transform: translateY(-5px); box-shadow: 0 10px 30px rgba(0,217,255,0.2); border-color: #00d9ff; }
.stock-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
.stock-card-ticker { font-size: 1.4em; font-weight: 800; color: #00d9ff; }
.stock-card-rank { background: linear-gradient(135deg, #00d9ff, #00ff88); color: #1a1a2e; padding: 5px 12px; border-radius: 15px; font-weight: 700; font-size: 0.85em; }
.stock-card-name { color: #888; font-size: 0.9em; margin-bottom: 15px; }
.stock-card-price { font-size: 1.5em; font-weight: 700; margin-bottom: 5px; }
.stock-card-change { font-size: 1.1em; font-weight: 600; margin-bottom: 15px; }
.mini-chart { width: 100%; height: 60px; background: rgba(0,0,0,0.2); border-radius: 8px; margin-bottom: 15px; overflow: hidden; }
.stock-card-metrics { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; font-size: 0.85em; }
.stock-card-metric { display: flex; justify-content: space-between; padding: 8px; background: rgba(255,255,255,0.03); border-radius: 5px; }
.stock-card-metric-label { color: #888; }
.stock-card-metric-value { font-weight: 600; }
.view-chart-btn { width: 100%; margin-top: 15px; padding: 10px; background: rgba(0,217,255,0.2); border: 1px solid #00d9ff; color: #00d9ff; border-radius: 8px; cursor: pointer; font-weight: 600; transition: all 0.3s; }
.view-chart-btn:hover { background: #00d9ff; color: #1a1a2e; }
/* ๋ชจ๋‹ฌ */
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); overflow: auto; }
.modal.active { display: flex; justify-content: center; align-items: flex-start; padding: 30px 20px; }
.modal-content { background: #1a1a2e; border-radius: 20px; padding: 30px; max-width: 1200px; width: 100%; border: 1px solid rgba(0,217,255,0.3); max-height: 95vh; overflow-y: auto; }
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; position: sticky; top: -30px; background: #1a1a2e; padding: 10px 0; z-index: 10; }
.modal-close { background: none; border: none; color: #888; font-size: 2em; cursor: pointer; }
.modal-close:hover { color: #ff4757; }
/* ์‹ฌ์ธต ๋ถ„์„ ์„น์…˜ */
.deep-section { margin-bottom: 20px; padding: 20px; background: rgba(255,255,255,0.03); border-radius: 12px; }
.deep-section h3 { color: #00d9ff; margin-bottom: 15px; font-size: 1.05em; }
.deep-section .content { color: #bbb; line-height: 1.8; font-size: 0.95em; }
.deep-section .content table { width: 100%; border-collapse: collapse; margin: 15px 0; }
.deep-section .content th { background: rgba(0,217,255,0.2); padding: 10px; text-align: left; color: #00d9ff; }
.deep-section .content td { padding: 10px; border-bottom: 1px solid rgba(255,255,255,0.1); }
.deep-section .content ul, .deep-section .content ol { margin: 10px 0 10px 20px; }
.deep-section .content li { margin-bottom: 8px; }
.deep-section .content strong { color: #fff; }
.deep-section .content p { margin-bottom: 10px; }
.chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin: 20px 0; }
.chart-box { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 15px; }
.chart-box h4 { color: #00d9ff; margin-bottom: 10px; font-size: 0.9em; text-align: center; }
.chart-box canvas { max-height: 200px; }
.loading { display: none; text-align: center; padding: 40px; }
.loading.active { display: block; }
.spinner { border: 4px solid rgba(255,255,255,0.1); border-top: 4px solid #00d9ff; border-radius: 50%; width: 50px; height: 50px; animation: spin 1s linear infinite; margin: 0 auto 20px; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
/* TradingView ์ฐจํŠธ ๋ชจ๋‹ฌ */
.chart-modal-content { max-width: 1000px; }
#tradingview-chart { width: 100%; height: 500px; border-radius: 10px; overflow: hidden; }
.button-group { text-align: center; margin: 40px 0; display: flex; justify-content: center; gap: 15px; flex-wrap: wrap; }
.refresh-btn { background: linear-gradient(135deg, #00d9ff, #00ff88); color: #1a1a2e; border: none; padding: 15px 35px; font-size: 1em; font-weight: 700; border-radius: 50px; cursor: pointer; transition: all 0.3s; }
.refresh-btn:hover { transform: scale(1.05); box-shadow: 0 10px 30px rgba(0,217,255,0.4); }
.refresh-btn.secondary { background: transparent; border: 2px solid #00d9ff; color: #00d9ff; }
.timestamp { text-align: center; color: #666; margin-top: 30px; font-size: 0.9em; padding: 15px; background: rgba(255,255,255,0.02); border-radius: 10px; }
.footer { text-align: center; margin-top: 30px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.1); color: #666; font-size: 0.85em; }
.empty-state { text-align: center; padding: 60px; color: #888; }
.empty-state h3 { margin-bottom: 15px; color: #00d9ff; }
@media (max-width: 768px) {
body { padding: 10px; }
.container { padding: 20px; }
h1 { font-size: 1.8em; }
.tab { padding: 12px 15px; font-size: 0.85em; }
.screening-grid { grid-template-columns: 1fr; }
.stock-heatmap { grid-template-columns: repeat(3, 1fr); }
}
</style>
</head>
<body>
<div class="container">
<h1>๐Ÿ“ˆ AI ์ฃผ์‹ ๋ฆฌ์„œ์น˜ ์„ผํ„ฐ v4.2</h1>
<p class="subtitle">
TOP 30 ์ข…๋ชฉ ์‹ค์‹œ๊ฐ„ ๋ถ„์„ | ์Šคํƒ ํžˆํŠธ๋งต | ์ฐจํŠธ ๋ถ„์„ | ์‹ฌ์ธต ๋ถ„์„ + PDF/DOCX<br>
<span style="color: #00d9ff;">AGI SOMA ๋ฉ€ํ‹ฐ์—์ด์ „ํŠธ | ์กฐ์‚ฌ์žโ†’๋น„ํ‰๊ฐ€โ†’๊ฐ๋…์ž ํ˜‘์—… ์‹œ์Šคํ…œ</span>
</p>
<!-- ์ง€์ˆ˜ -->
<div class="indices-bar">
{% for idx in indices %}
<div class="index-card">
<div class="index-name">{{ idx.name }}</div>
<div class="index-value">{{ "{:,.2f}".format(idx.price) }}</div>
<div class="index-change {{ 'positive' if idx.change >= 0 else 'negative' }}">
{{ "+" if idx.change >= 0 else "" }}{{ "{:.2f}".format(idx.change) }} ({{ "{:.2f}".format(idx.change_percent) }}%)
</div>
</div>
{% endfor %}
</div>
<!-- ํƒญ -->
<div class="tabs">
<button class="tab active" onclick="switchTab(event, 'market')">๐ŸŒ ์‹œ์žฅ ๋ถ„์„</button>
<button class="tab" onclick="switchTab(event, 'breaking')">๐Ÿ”ฅ ์†๋ณด ({{ breaking_news|length }})</button>
<button class="tab" onclick="switchTab(event, 'research')">๐Ÿ“ˆ ๋ฆฌ์„œ์น˜ ({{ research_reports|length }})</button>
<button class="tab" onclick="switchTab(event, 'screening')">๐Ÿ“Š ์Šคํฌ๋ฆฌ๋‹ ({{ screening_data|length }})</button>
</div>
<!-- ์‹œ์žฅ ๋ถ„์„ ํƒญ -->
<div id="market-content" class="tab-content active">
<!-- ํ‹ฐ์ปค ํ…Œ์ดํ”„ (์ƒ๋‹จ ํ๋ฅด๋Š” ์ฃผ๊ฐ€) -->
<div class="ticker-tape-container">
<div id="ticker-tape-widget"></div>
</div>
<h2 style="color: #00d9ff; margin-bottom: 20px; margin-top: 20px;">๐ŸŒ ์‹œ์žฅ ์ข…ํ•ฉ ๋ถ„์„</h2>
<!-- 3์—ด ๊ทธ๋ฆฌ๋“œ: ์„ผํ‹ฐ๋จผํŠธ + ์„นํ„ฐ ํผํฌ๋จผ์Šค + ์ƒ์Šน/ํ•˜๋ฝ ๋น„์œจ -->
<div class="market-overview-grid">
<!-- ์„ผํ‹ฐ๋จผํŠธ ๊ฒŒ์ด์ง€ -->
<div class="market-card">
<div class="market-card-title">๐Ÿ“Š ์‹œ์žฅ ์„ผํ‹ฐ๋จผํŠธ</div>
<div class="gauge-container">
<div class="gauge-wrapper" style="width: 200px; height: 120px;">
<canvas id="sentimentGauge"></canvas>
</div>
<div class="gauge-labels" style="font-size: 0.6em;">
<span>๋งค๋„</span>
<span>์ค‘๋ฆฝ</span>
<span>๋งค์ˆ˜</span>
</div>
<div class="gauge-value" style="margin-top: 10px;">
<div class="number" id="sentimentValue" style="font-size: 1.8em;">-</div>
<div class="label" id="sentimentLabel" style="color: #00d9ff; font-size: 0.95em;">๋ถ„์„ ์ค‘...</div>
</div>
</div>
</div>
<!-- ์„นํ„ฐ๋ณ„ ํผํฌ๋จผ์Šค ๋ฐ” ์ฐจํŠธ -->
<div class="market-card">
<div class="market-card-title">๐Ÿ“ˆ ์„นํ„ฐ๋ณ„ ํผํฌ๋จผ์Šค</div>
<div class="chart-container">
<canvas id="sectorChart"></canvas>
</div>
</div>
<!-- ์ƒ์Šน/ํ•˜๋ฝ ๋น„์œจ ๋„๋„› -->
<div class="market-card">
<div class="market-card-title">๐Ÿฉ ์ƒ์Šน/ํ•˜๋ฝ ๋น„์œจ</div>
<div class="chart-container">
<canvas id="upDownChart"></canvas>
</div>
<div id="upDownLegend" class="chart-legend"></div>
</div>
</div>
<!-- ๋งˆ์ผ“ ์˜ค๋ฒ„๋ทฐ ์œ„์ ฏ (์ „์ฒด ๋„ˆ๋น„) -->
<div class="widget-section">
<h3 style="color: #00d9ff; margin-bottom: 15px;">๐Ÿ“Š ๊ธ€๋กœ๋ฒŒ ์‹œ์žฅ ํ˜„ํ™ฉ</h3>
<div class="widget-card-full" id="market-overview-widget" style="height: 450px;"></div>
</div>
<!-- ์ด์Šˆ ํ‚ค์›Œ๋“œ ํƒœ๊ทธ ํด๋ผ์šฐ๋“œ -->
<h3 style="color: #00d9ff; margin-bottom: 15px; margin-top: 30px;">๐Ÿ”ฅ ์‹œ์žฅ ์ด์Šˆ ํ‚ค์›Œ๋“œ (24h)</h3>
<div class="keyword-cloud" id="keywordCloud"></div>
<!-- ์Šคํƒ ํžˆํŠธ๋งต with ํƒญ -->
<div class="heatmap-section">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h3 style="color: #00d9ff; margin: 0;">๐Ÿ“ˆ ์‹œ์žฅ ํžˆํŠธ๋งต</h3>
<div class="heatmap-tabs">
<button class="heatmap-tab active" onclick="switchHeatmap('SPX500', this)">S&P 500</button>
<button class="heatmap-tab" onclick="switchHeatmap('NASDAQ100', this)">NASDAQ 100</button>
<button class="heatmap-tab" onclick="switchHeatmap('DJI', this)">DOW 30</button>
</div>
</div>
<div id="stock-heatmap-widget" style="height: 500px; border-radius: 15px; overflow: hidden;"></div>
</div>
</div>
<!-- ์†๋ณด ํƒญ -->
<div id="breaking-content" class="tab-content">
<h2 style="color: #00d9ff; margin-bottom: 20px;">๐Ÿ”ฅ 24์‹œ๊ฐ„ ์ด๋‚ด ์ฃผ์š” ๋‰ด์Šค</h2>
{% if breaking_news %}
{% for news in breaking_news %}
<div class="news-card">
<div class="news-header">
<div class="news-title">{{ news.title }}</div>
<div class="news-meta">
{% if news.news_type == 'market' %}
<span class="badge market-badge">{{ news.ticker }}</span>
{% else %}
<span class="badge ticker-badge">{{ news.ticker }}</span>
{% endif %}
<span class="badge impact-{{ news.impact_level }}">{{ news.impact_text }}</span>
<span class="badge sentiment-{{ news.sentiment }}">{{ news.sentiment_text }}</span>
</div>
</div>
{% if news.title_original and news.title_original != news.title %}
<div style="color: #666; font-size: 0.85em; margin-bottom: 10px; font-style: italic;">๐Ÿ“ {{ news.title_original }}</div>
{% endif %}
<div class="news-summary">{{ news.summary }}</div>
{% if news.content %}
<div class="news-content">{{ news.content }}</div>
{% endif %}
<div class="analysis-box">
<span class="analysis-label">๐Ÿ’ก ํˆฌ์ž ํฌ์ธํŠธ</span>
<div style="color: #bbb; line-height: 1.7;">{{ news.investment_point }}</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 15px; flex-wrap: wrap; gap: 10px;">
<div>
<span style="color: #666; font-size: 0.85em;">๐Ÿ“… {{ news.published_at }}</span>
{% if news.source %}
<span style="color: #00d9ff; font-size: 0.85em; margin-left: 10px;">๐Ÿ“ฐ {{ news.source }}</span>
{% endif %}
</div>
<div>
{% if news.url %}
<a href="{{ news.url }}" target="_blank" class="news-link" style="margin-right: 5px;">๐Ÿ”— ์›๋ฌธ ๋ณด๊ธฐ</a>
{% endif %}
<button class="news-link" onclick="searchNews('{{ news.title|e }}')">๐Ÿ” ๊ด€๋ จ ๊ฒ€์ƒ‰</button>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<h3>๐Ÿ“ญ 24์‹œ๊ฐ„ ์ด๋‚ด ๋‰ด์Šค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค</h3>
</div>
{% endif %}
</div>
<!-- ๋ฆฌ์„œ์น˜ ํƒญ -->
<div id="research-content" class="tab-content">
<h2 style="color: #00d9ff; margin-bottom: 20px;">๐Ÿ“ˆ AGI SOMA ๋ฆฌ์„œ์น˜ + ์‹ฌ์ธต ๋ถ„์„</h2>
{% if research_reports %}
{% for report in research_reports %}
<div class="research-card">
<div class="research-header">
<div>
<span class="research-ticker">{{ report.ticker }}</span>
<span style="color: #888; margin-left: 10px;">{{ report.company_name }}</span>
</div>
<span class="rating-badge rating-{{ report.rating_class }}">{{ report.rating }}</span>
</div>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-label">ํ˜„์žฌ๊ฐ€</div>
<div class="metric-value">${{ "{:,.2f}".format(report.current_price) }}</div>
</div>
<div class="metric-card">
<div class="metric-label">๋ชฉํ‘œ๊ฐ€</div>
<div class="metric-value" style="color: #00ff88;">${{ "{:,.2f}".format(report.target_price) }}</div>
</div>
<div class="metric-card">
<div class="metric-label">๐Ÿ“‰ 52์ฃผ ๊ณ ์ ๋Œ€๋น„</div>
{% set stock = screening_data|selectattr('ticker', 'equalto', report.ticker)|first %}
{% set from_high = stock.from_high if stock else -10 %}
<div class="metric-value" style="color: {% if from_high > -5 %}#00ff88{% elif from_high > -15 %}#ffa502{% else %}#ff4757{% endif %};">{{ from_high }}%</div>
<div style="font-size: 0.7em; color: #888;">{% if from_high > -5 %}๊ณ ์ ๊ทผ์ฒ˜{% elif from_high > -15 %}์กฐ์ •๊ตฌ๊ฐ„{% else %}๋ฐ˜๋“ฑ์—ฌ๋ ฅโ†‘{% endif %}</div>
</div>
<div class="metric-card">
<div class="metric-label">๐Ÿ’ฐ ์‹œ๊ฐ€์ด์•ก</div>
{% set mcap = stock.market_cap if stock else 0 %}
<div class="metric-value" style="color: #00d9ff;">${{ "{:,.0f}".format(mcap / 1e9) }}B</div>
<div style="font-size: 0.7em; color: #888;">{% if mcap > 500e9 %}๋Œ€ํ˜•์ฃผ{% elif mcap > 100e9 %}์ค‘๋Œ€ํ˜•{% else %}์ค‘ํ˜•์ฃผ{% endif %}</div>
</div>
<div class="metric-card">
<div class="metric-label">๐Ÿ“Š ๊ฑฐ๋ž˜๋Ÿ‰</div>
{% set vol = stock.volume if stock else 0 %}
<div class="metric-value" style="color: #a29bfe;">{{ "{:,.1f}".format(vol / 1e6) }}M</div>
<div style="font-size: 0.7em; color: #888;">์ผ ๊ฑฐ๋ž˜๋Ÿ‰</div>
</div>
</div>
<div class="soma-agent soma-supervisor">
<div class="soma-header">
<div class="soma-icon" style="background: #9b59b6;">๐Ÿ‘”</div>
<span class="soma-name" style="color: #9b59b6;">๊ฐ๋…์ž (Supervisor)</span>
</div>
<div class="soma-content">{{ report.supervisor_analysis }}</div>
</div>
<div class="soma-agent soma-researcher">
<div class="soma-header">
<div class="soma-icon" style="background: #3498db;">๐Ÿ”ฌ</div>
<span class="soma-name" style="color: #3498db;">์กฐ์‚ฌ์ž (Researcher)</span>
</div>
<div class="soma-content">{{ report.researcher_analysis }}</div>
</div>
<div style="margin-top: 20px;">
<button class="deep-analysis-btn" onclick="runDeepAnalysis('{{ report.ticker }}', '{{ report.company_name }}')">๐Ÿ”ฌ ์‹ฌ์ธต ๋ถ„์„</button>
<span id="deep-status-{{ report.ticker }}" style="margin-left: 15px; color: #888;"></span>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state"><h3>๐Ÿ“ญ ๋ฆฌ์„œ์น˜ ๋ณด๊ณ ์„œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค</h3></div>
{% endif %}
</div>
<!-- ์Šคํฌ๋ฆฌ๋‹ ํƒญ -->
<div id="screening-content" class="tab-content">
<!-- ์„œ๋ธŒ ํƒญ (DOW 30 / NASDAQ 100) -->
<div class="sub-tabs">
<button class="sub-tab active" onclick="switchScreeningTab('dow30', this)">๐Ÿ›๏ธ DOW 30</button>
<button class="sub-tab" onclick="switchScreeningTab('nasdaq100', this)">๐Ÿ’ป NASDAQ 100</button>
</div>
<!-- DOW 30 ์ฝ˜ํ…์ธ  -->
<div id="dow30-content" class="screening-sub-content active">
<h3 style="color: #00d9ff; margin: 20px 0;">๐Ÿ›๏ธ DOW 30 ์ข…๋ชฉ ์Šคํฌ๋ฆฌ๋‹</h3>
<div class="screening-grid">
{% set dow30_tickers = ['UNH', 'GS', 'HD', 'CAT', 'CRM', 'MCD', 'V', 'TRV', 'AXP', 'JPM', 'IBM', 'BA', 'MMM', 'JNJ', 'PG', 'MRK', 'NKE', 'DIS', 'KO', 'WMT', 'VZ', 'DOW', 'SHW', 'AAPL', 'MSFT', 'AMZN', 'NVDA', 'HON', 'AMGN', 'CSCO'] %}
{% for stock in screening_data %}
{% if stock.ticker in dow30_tickers %}
<div class="stock-card">
<div class="stock-card-header">
<span class="stock-card-ticker">{{ stock.ticker }}</span>
<span class="stock-card-rank">#{{ stock.rank }}</span>
</div>
<div class="stock-card-name">{{ stock.name[:20] }}{% if stock.name|length > 20 %}...{% endif %}</div>
<div class="stock-card-price">${{ "{:,.2f}".format(stock.price) }}</div>
<div class="stock-card-change {{ 'positive' if stock.change_percent >= 0 else 'negative' }}">
{{ "{:+.2f}".format(stock.change_percent) }}%
</div>
<!-- TradingView ๋ฏธ๋‹ˆ ์ฐจํŠธ -->
<div class="mini-chart" id="mini-chart-dow-{{ stock.ticker }}"></div>
<div class="stock-card-metrics">
<div class="stock-card-metric">
<span class="stock-card-metric-label">RSI</span>
<span class="stock-card-metric-value">{{ "{:.1f}".format(stock.rsi) }}</span>
</div>
<div class="stock-card-metric">
<span class="stock-card-metric-label">PER</span>
<span class="stock-card-metric-value">{{ "{:.1f}".format(stock.pe_ratio) }}</span>
</div>
<div class="stock-card-metric">
<span class="stock-card-metric-label">์‹œ์ด</span>
<span class="stock-card-metric-value">${{ "{:.0f}".format(stock.market_cap / 1e9) }}B</span>
</div>
</div>
<button class="view-chart-btn" onclick="openChartModal('{{ stock.ticker }}', '{{ stock.name }}', '{{ stock.exchange|default('NYSE') }}')">๐Ÿ“ˆ ์ฐจํŠธ</button>
</div>
{% endif %}
{% endfor %}
</div>
</div>
<!-- NASDAQ 100 ์ฝ˜ํ…์ธ  -->
<div id="nasdaq100-content" class="screening-sub-content">
<div style="display: flex; justify-content: space-between; align-items: center; margin: 20px 0;">
<h3 style="color: #00d9ff; margin: 0;">๐Ÿ’ป NASDAQ 100 ์ข…๋ชฉ ์Šคํฌ๋ฆฌ๋‹</h3>
<div class="pagination-tabs">
<button class="page-tab active" onclick="showNasdaqPage(1, this)">1-35</button>
<button class="page-tab" onclick="showNasdaqPage(2, this)">36-70</button>
<button class="page-tab" onclick="showNasdaqPage(3, this)">71-101</button>
</div>
</div>
<!-- NASDAQ ํŽ˜์ด์ง€ 1: 1-35 -->
<div class="screening-grid nasdaq-page" id="nasdaq-page-1">
{% for stock in screening_data[:35] %}
<div class="stock-card">
<div class="stock-card-header">
<span class="stock-card-ticker">{{ stock.ticker }}</span>
<span class="stock-card-rank">#{{ loop.index }}</span>
</div>
<div class="stock-card-name">{{ stock.name[:20] }}{% if stock.name|length > 20 %}...{% endif %}</div>
<div class="stock-card-price">${{ "{:,.2f}".format(stock.price) }}</div>
<div class="stock-card-change {{ 'positive' if stock.change_percent >= 0 else 'negative' }}">
{{ "{:+.2f}".format(stock.change_percent) }}%
</div>
<!-- TradingView ๋ฏธ๋‹ˆ ์ฐจํŠธ -->
<div class="mini-chart" id="mini-chart-nq1-{{ stock.ticker }}"></div>
<div class="stock-card-metrics">
<div class="stock-card-metric">
<span class="stock-card-metric-label">RSI</span>
<span class="stock-card-metric-value">{{ "{:.1f}".format(stock.rsi) }}</span>
</div>
<div class="stock-card-metric">
<span class="stock-card-metric-label">PER</span>
<span class="stock-card-metric-value">{{ "{:.1f}".format(stock.pe_ratio) }}</span>
</div>
<div class="stock-card-metric">
<span class="stock-card-metric-label">์‹œ์ด</span>
<span class="stock-card-metric-value">${{ "{:.0f}".format(stock.market_cap / 1e9) }}B</span>
</div>
</div>
<button class="view-chart-btn" onclick="openChartModal('{{ stock.ticker }}', '{{ stock.name }}', '{{ stock.exchange|default('NASDAQ') }}')">๐Ÿ“ˆ ์ฐจํŠธ</button>
</div>
{% endfor %}
</div>
<!-- NASDAQ ํŽ˜์ด์ง€ 2: 36-70 -->
<div class="screening-grid nasdaq-page" id="nasdaq-page-2" style="display: none;">
{% for stock in screening_data[35:70] %}
<div class="stock-card">
<div class="stock-card-header">
<span class="stock-card-ticker">{{ stock.ticker }}</span>
<span class="stock-card-rank">#{{ loop.index + 35 }}</span>
</div>
<div class="stock-card-name">{{ stock.name[:20] }}{% if stock.name|length > 20 %}...{% endif %}</div>
<div class="stock-card-price">${{ "{:,.2f}".format(stock.price) }}</div>
<div class="stock-card-change {{ 'positive' if stock.change_percent >= 0 else 'negative' }}">
{{ "{:+.2f}".format(stock.change_percent) }}%
</div>
<!-- TradingView ๋ฏธ๋‹ˆ ์ฐจํŠธ -->
<div class="mini-chart" id="mini-chart-nq2-{{ stock.ticker }}"></div>
<div class="stock-card-metrics">
<div class="stock-card-metric">
<span class="stock-card-metric-label">RSI</span>
<span class="stock-card-metric-value">{{ "{:.1f}".format(stock.rsi) }}</span>
</div>
<div class="stock-card-metric">
<span class="stock-card-metric-label">PER</span>
<span class="stock-card-metric-value">{{ "{:.1f}".format(stock.pe_ratio) }}</span>
</div>
<div class="stock-card-metric">
<span class="stock-card-metric-label">์‹œ์ด</span>
<span class="stock-card-metric-value">${{ "{:.0f}".format(stock.market_cap / 1e9) }}B</span>
</div>
</div>
<button class="view-chart-btn" onclick="openChartModal('{{ stock.ticker }}', '{{ stock.name }}', '{{ stock.exchange|default('NASDAQ') }}')">๐Ÿ“ˆ ์ฐจํŠธ</button>
</div>
{% endfor %}
</div>
<!-- NASDAQ ํŽ˜์ด์ง€ 3: 71-101 -->
<div class="screening-grid nasdaq-page" id="nasdaq-page-3" style="display: none;">
{% for stock in screening_data[70:101] %}
<div class="stock-card">
<div class="stock-card-header">
<span class="stock-card-ticker">{{ stock.ticker }}</span>
<span class="stock-card-rank">#{{ loop.index + 70 }}</span>
</div>
<div class="stock-card-name">{{ stock.name[:20] }}{% if stock.name|length > 20 %}...{% endif %}</div>
<div class="stock-card-price">${{ "{:,.2f}".format(stock.price) }}</div>
<div class="stock-card-change {{ 'positive' if stock.change_percent >= 0 else 'negative' }}">
{{ "{:+.2f}".format(stock.change_percent) }}%
</div>
<!-- TradingView ๋ฏธ๋‹ˆ ์ฐจํŠธ -->
<div class="mini-chart" id="mini-chart-nq3-{{ stock.ticker }}"></div>
<div class="stock-card-metrics">
<div class="stock-card-metric">
<span class="stock-card-metric-label">RSI</span>
<span class="stock-card-metric-value">{{ "{:.1f}".format(stock.rsi) }}</span>
</div>
<div class="stock-card-metric">
<span class="stock-card-metric-label">PER</span>
<span class="stock-card-metric-value">{{ "{:.1f}".format(stock.pe_ratio) }}</span>
</div>
<div class="stock-card-metric">
<span class="stock-card-metric-label">์‹œ์ด</span>
<span class="stock-card-metric-value">${{ "{:.0f}".format(stock.market_cap / 1e9) }}B</span>
</div>
</div>
<button class="view-chart-btn" onclick="openChartModal('{{ stock.ticker }}', '{{ stock.name }}', '{{ stock.exchange|default('NASDAQ') }}')">๐Ÿ“ˆ ์ฐจํŠธ</button>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="button-group">
<button class="refresh-btn" onclick="location.reload()">๐Ÿ”„ ์ƒˆ๋กœ๊ณ ์นจ</button>
<button class="refresh-btn secondary" onclick="location.href='/?refresh=true'">๐Ÿ”ฅ ๋ฐ์ดํ„ฐ ๊ฐฑ์‹ </button>
<button class="refresh-btn" style="background: linear-gradient(135deg, #9b59b6, #8e44ad);" onclick="triggerAutoRefresh()">โฐ ์ฆ‰์‹œ ์ž๋™๊ฐฑ์‹ </button>
</div>
<!-- ์Šค์ผ€์ค„๋Ÿฌ ์ƒํƒœ ํ‘œ์‹œ -->
<div class="scheduler-status" id="schedulerStatus">
<span class="scheduler-indicator">โฐ</span>
<span id="schedulerText">์ž๋™ ๊ฐฑ์‹  ์ƒํƒœ ํ™•์ธ ์ค‘...</span>
</div>
<div class="timestamp">โฐ ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ: {{ timestamp }}</div>
<div class="footer">
<p>๐Ÿ“ˆ AI ์ฃผ์‹ ๋ฆฌ์„œ์น˜ ์„ผํ„ฐ v5.8 | NASDAQ 100 + DOW 30 | ์ž๋™ ๊ฐฑ์‹  ์‹œ์Šคํ…œ | Ginigen AI</p>
<p style="margin-top: 10px;">โš ๏ธ ๋ณธ ์„œ๋น„์Šค๋Š” ํˆฌ์ž ์ฐธ๊ณ ์šฉ์ด๋ฉฐ, ํˆฌ์ž ๊ฒฐ์ •์€ ๋ณธ์ธ์˜ ํŒ๋‹จ๊ณผ ์ฑ…์ž„ ํ•˜์— ์ด๋ฃจ์–ด์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค.</p>
</div>
</div>
<!-- ์‹ฌ์ธต ๋ถ„์„ ๋ชจ๋‹ฌ -->
<div id="deepAnalysisModal" class="modal">
<div class="modal-content" id="deepAnalysisContent">
<div class="modal-header">
<h2 style="color: #00d9ff;">๐Ÿ”ฌ <span id="modal-ticker"></span> ์‹ฌ์ธต ๋ถ„์„</h2>
<button class="modal-close" onclick="closeModal('deepAnalysisModal')">&times;</button>
</div>
<div id="loading-indicator" class="loading">
<div class="spinner"></div>
<p>๐Ÿค– SOMA ์—์ด์ „ํŠธ๊ฐ€ ์‹ฌ์ธต ๋ถ„์„ ์ค‘์ž…๋‹ˆ๋‹ค...</p>
</div>
<div id="deep-analysis-result" style="display: none;">
<div class="chart-grid">
<div class="chart-box"><h4>๐Ÿ“Š ํˆฌ์ž ์ง€ํ‘œ</h4><canvas id="metricsChart"></canvas></div>
<div class="chart-box"><h4>๐ŸŽฏ ๋ชฉํ‘œ๊ฐ€ ๋ถ„์„</h4><canvas id="priceChart"></canvas></div>
<div class="chart-box"><h4>โš–๏ธ ๋ฆฌ์Šคํฌ/๊ธฐํšŒ</h4><canvas id="riskChart"></canvas></div>
<div class="chart-box"><h4>๐Ÿ“ˆ ์„ฑ์žฅ ์ „๋ง</h4><canvas id="growthChart"></canvas></div>
</div>
<div class="metrics-grid" style="margin-bottom: 25px;">
<div class="metric-card"><div class="metric-label">ํ˜„์žฌ๊ฐ€</div><div class="metric-value" id="result-current-price">-</div></div>
<div class="metric-card"><div class="metric-label">๋ชฉํ‘œ๊ฐ€</div><div class="metric-value" style="color: #00ff88;" id="result-target-price">-</div></div>
<div class="metric-card"><div class="metric-label">ํˆฌ์ž์˜๊ฒฌ</div><div class="metric-value" style="color: #00d9ff;" id="result-rating">-</div></div>
</div>
<!-- ํƒ„๋ ฅ์„ฑ ์˜ˆ์ธก ์‹œ๊ฐ์  UI -->
<div class="elasticity-container" style="margin-bottom: 25px; padding: 20px; background: rgba(0,0,0,0.3); border-radius: 15px; border: 1px solid rgba(255,255,255,0.1);">
<h4 style="color: #00d9ff; margin-bottom: 15px; text-align: center;">๐Ÿ“Š ํƒ„๋ ฅ์„ฑ ์˜ˆ์ธก</h4>
<!-- ์‹œ๊ฐ์  ๋ฐ” -->
<div class="elasticity-bar-container" style="position: relative; height: 50px; margin: 20px 0;">
<div class="elasticity-scale" style="display: flex; justify-content: space-between; font-size: 11px; color: #888; margin-bottom: 5px;">
<span id="elasticity-min">-30%</span>
<span>0%</span>
<span id="elasticity-max">+40%</span>
</div>
<div class="elasticity-track" style="position: relative; height: 20px; background: linear-gradient(to right, #ff4757 0%, #ff4757 40%, #888 40%, #888 45%, #00ff88 45%, #00ff88 100%); border-radius: 10px; overflow: hidden;">
<!-- ์˜ˆ์ธก ๋ฒ”์œ„ ํ‘œ์‹œ -->
<div id="elasticity-range" style="position: absolute; height: 100%; background: rgba(0,217,255,0.4); border: 2px solid #00d9ff; border-radius: 8px;"></div>
<!-- ๊ธฐ๋ณธ ์˜ˆ์ธก์น˜ ๋งˆ์ปค -->
<div id="elasticity-marker" style="position: absolute; top: -8px; width: 4px; height: 36px; background: #fff; border-radius: 2px; box-shadow: 0 0 10px rgba(255,255,255,0.8);"></div>
</div>
</div>
<!-- ์ˆ˜์น˜ ํ‘œ์‹œ - ํ™•๋ฅ  ๊ฐ•์กฐ -->
<div style="display: flex; justify-content: space-between; gap: 15px; margin-top: 20px;">
<div style="flex: 1; text-align: center; padding: 15px; background: rgba(255,71,87,0.15); border-radius: 10px; border: 1px solid rgba(255,71,87,0.3);">
<div style="font-size: 12px; color: #ff6b7a; margin-bottom: 5px;">๐Ÿ“‰ ํ•˜๋ฝ ๋ฆฌ์Šคํฌ</div>
<div id="result-downside" style="font-size: 22px; font-weight: bold; color: #ff4757;">-</div>
<div style="font-size: 11px; color: #888; margin-top: 3px;"><span id="result-down-prob">-</span> ํ™•๋ฅ </div>
</div>
<div style="flex: 1; text-align: center; padding: 15px; background: rgba(0,217,255,0.15); border-radius: 10px; border: 1px solid rgba(0,217,255,0.3);">
<div style="font-size: 12px; color: #00d9ff; margin-bottom: 5px;">๐ŸŽฏ ๊ธฐ๋Œ€ ์ˆ˜์ต</div>
<div id="result-prediction" style="font-size: 24px; font-weight: bold; color: #00d9ff;">-</div>
<div style="font-size: 11px; color: #888; margin-top: 3px;">R/R <span id="result-rr">-</span></div>
</div>
<div style="flex: 1; text-align: center; padding: 15px; background: rgba(0,255,136,0.15); border-radius: 10px; border: 1px solid rgba(0,255,136,0.3);">
<div style="font-size: 12px; color: #00ff88; margin-bottom: 5px;">๐Ÿ“ˆ ์ƒ์Šน ์ž ์žฌ๋ ฅ</div>
<div id="result-upside" style="font-size: 22px; font-weight: bold; color: #00ff88;">-</div>
<div style="font-size: 11px; color: #888; margin-top: 3px;"><span id="result-up-prob">-</span> ํ™•๋ฅ </div>
</div>
</div>
<div style="text-align: center; margin-top: 12px; font-size: 11px; color: #666;">
โ€ป AI ๊ธฐ๋ฐ˜ ๋ถ„์„์ด๋ฉฐ ์‹ค์ œ ํˆฌ์ž ์†์ต์„ ๋ณด์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค
</div>
</div>
<div class="deep-section" style="background: rgba(0,217,255,0.1); border: 1px solid rgba(0,217,255,0.3);"><h3>๐Ÿ“‹ ํ•ต์‹ฌ ์š”์•ฝ</h3><div class="content" id="result-executive-summary"></div></div>
<div class="deep-section"><h3>๐Ÿข ํšŒ์‚ฌ ๊ฐœ์š”</h3><div class="content" id="result-company-overview"></div></div>
<div class="deep-section"><h3>๐Ÿ’ฐ ์žฌ๋ฌด ๋ถ„์„</h3><div class="content" id="result-financial-analysis"></div></div>
<div class="deep-section"><h3>๐Ÿ“ˆ ๊ธฐ์ˆ ์  ๋ถ„์„</h3><div class="content" id="result-technical-analysis"></div></div>
<div class="deep-section"><h3>๐Ÿญ ์‚ฐ์—… ๋ถ„์„</h3><div class="content" id="result-industry-analysis"></div></div>
<div class="deep-section"><h3>โš ๏ธ ๋ฆฌ์Šคํฌ ํ‰๊ฐ€</h3><div class="content" id="result-risk-assessment"></div></div>
<div class="deep-section"><h3>๐Ÿ’ก ํˆฌ์ž ๋…ผ๋ฆฌ</h3><div class="content" id="result-investment-thesis"></div></div>
<div class="deep-section"><h3>๐ŸŽฏ ๋ชฉํ‘œ ์ฃผ๊ฐ€</h3><div class="content" id="result-price-targets"></div></div>
<div class="deep-section"><h3>๐Ÿš€ ์นดํƒˆ๋ฆฌ์ŠคํŠธ</h3><div class="content" id="result-catalysts"></div></div>
<div class="deep-section" style="background: rgba(0,255,136,0.1); border: 1px solid rgba(0,255,136,0.3);"><h3>โœ… ์ตœ์ข… ๊ถŒ๊ณ </h3><div class="content" id="result-final-recommendation" style="font-weight: 600;"></div></div>
<div style="text-align: center; margin-top: 30px;">
<a id="download-docx-btn" class="download-btn" style="display: none;">๐Ÿ“„ DOCX</a>
<button id="download-pdf-btn" class="download-btn pdf" style="display: none;" onclick="downloadAsPDF()">๐Ÿ“‘ PDF</button>
</div>
</div>
</div>
</div>
<!-- TradingView ์ฐจํŠธ ๋ชจ๋‹ฌ -->
<div id="chartModal" class="modal">
<div class="modal-content chart-modal-content">
<div class="modal-header">
<h2 style="color: #00d9ff;">๐Ÿ“ˆ <span id="chart-modal-title"></span></h2>
<button class="modal-close" onclick="closeModal('chartModal')">&times;</button>
</div>
<div id="tradingview-chart"></div>
</div>
</div>
<script>
// ์ „์—ญ ๋ณ€์ˆ˜
let currentTicker = '';
let sentimentGaugeInstance = null;
let metricsChartInstance = null, priceChartInstance = null, riskChartInstance = null, growthChartInstance = null;
let tvWidget = null;
const newsData = {{ breaking_news | tojson | safe }};
const screeningData = {{ screening_data | tojson | safe }};
if (typeof marked !== 'undefined') {
marked.setOptions({ breaks: true, gfm: true, tables: true });
}
// ์Šค์ผ€์ค„๋Ÿฌ ์ƒํƒœ ์กฐํšŒ
async function updateSchedulerStatus() {
try {
const response = await fetch('/api/scheduler/status');
const data = await response.json();
if (data.success) {
const scheduler = data.scheduler;
const indicator = document.querySelector('.scheduler-indicator');
const text = document.getElementById('schedulerText');
if (scheduler.is_running) {
indicator.classList.add('active');
indicator.classList.remove('inactive');
indicator.textContent = '๐ŸŸข';
let statusText = `์ž๋™ ๊ฐฑ์‹  ํ™œ์„ฑ (${scheduler.interval_hours}์‹œ๊ฐ„ ์ฃผ๊ธฐ)`;
if (scheduler.next_run) {
const nextRun = new Date(scheduler.next_run);
statusText += ` | ๋‹ค์Œ: ${nextRun.toLocaleTimeString('ko-KR')}`;
}
if (scheduler.refresh_count > 0) {
statusText += ` | ๊ฐฑ์‹  ${scheduler.refresh_count}ํšŒ`;
}
text.textContent = statusText;
} else {
indicator.classList.remove('active');
indicator.classList.add('inactive');
indicator.textContent = '๐Ÿ”ด';
text.textContent = '์ž๋™ ๊ฐฑ์‹  ๋น„ํ™œ์„ฑ';
}
}
} catch (e) {
console.log('์Šค์ผ€์ค„๋Ÿฌ ์ƒํƒœ ์กฐํšŒ ์‹คํŒจ:', e);
}
}
// ์ฆ‰์‹œ ์ž๋™๊ฐฑ์‹  ํŠธ๋ฆฌ๊ฑฐ
async function triggerAutoRefresh() {
if (!confirm('์ง€๊ธˆ ์ฆ‰์‹œ ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐฑ์‹ ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?\n(๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์‹คํ–‰๋˜๋ฉฐ ๋ช‡ ๋ถ„์ด ์†Œ์š”๋ฉ๋‹ˆ๋‹ค)')) {
return;
}
try {
const response = await fetch('/api/scheduler/trigger');
const data = await response.json();
if (data.success) {
alert('โœ… ์ž๋™ ๊ฐฑ์‹ ์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์‹œ์ž‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค.\n\n์™„๋ฃŒ๋˜๋ฉด ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜์„ธ์š”.');
} else {
alert('โŒ ๊ฐฑ์‹  ํŠธ๋ฆฌ๊ฑฐ ์‹คํŒจ: ' + (data.error || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'));
}
} catch (e) {
alert('โŒ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์˜ค๋ฅ˜: ' + e.message);
}
}
// ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์Šค์ผ€์ค„๋Ÿฌ ์ƒํƒœ ์กฐํšŒ
document.addEventListener('DOMContentLoaded', function() {
updateSchedulerStatus();
// 1๋ถ„๋งˆ๋‹ค ์Šค์ผ€์ค„๋Ÿฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
setInterval(updateSchedulerStatus, 60000);
});
function switchTab(event, tabName) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
event.currentTarget.classList.add('active');
document.getElementById(tabName + '-content').classList.add('active');
if (tabName === 'market') setTimeout(initMarketTab, 100);
if (tabName === 'screening') setTimeout(() => loadMiniChartsForPage('dow'), 500);
}
function searchNews(title) {
window.open(`https://www.google.com/search?q=${encodeURIComponent(title)}&tbm=nws`, '_blank');
}
// NASDAQ 100 ํŽ˜์ด์ง€ ์ „ํ™˜
function showNasdaqPage(pageNum, btn) {
// ๋ชจ๋“  ํŽ˜์ด์ง€ ์ˆจ๊ธฐ๊ธฐ
document.querySelectorAll('.nasdaq-page').forEach(p => p.style.display = 'none');
// ์„ ํƒํ•œ ํŽ˜์ด์ง€ ํ‘œ์‹œ
document.getElementById('nasdaq-page-' + pageNum).style.display = 'grid';
// ํƒญ ํ™œ์„ฑํ™”
document.querySelectorAll('.page-tab').forEach(t => t.classList.remove('active'));
btn.classList.add('active');
// ํ•ด๋‹น ํŽ˜์ด์ง€ ๋ฏธ๋‹ˆ์ฐจํŠธ ๋กœ๋“œ
setTimeout(() => loadMiniChartsForPage('nq' + pageNum), 300);
}
// ์Šคํฌ๋ฆฌ๋‹ ์„œ๋ธŒํƒญ ์ „ํ™˜ (DOW 30 / NASDAQ 100)
function switchScreeningTab(tabName, btn) {
// ๋ชจ๋“  ์„œ๋ธŒ ์ฝ˜ํ…์ธ  ์ˆจ๊ธฐ๊ธฐ
document.querySelectorAll('.screening-sub-content').forEach(c => c.classList.remove('active'));
// ์„ ํƒํ•œ ์ฝ˜ํ…์ธ  ํ‘œ์‹œ
document.getElementById(tabName + '-content').classList.add('active');
// ํƒญ ํ™œ์„ฑํ™”
document.querySelectorAll('.sub-tab').forEach(t => t.classList.remove('active'));
btn.classList.add('active');
// ํ•ด๋‹น ํƒญ ๋ฏธ๋‹ˆ์ฐจํŠธ ๋กœ๋“œ
if (tabName === 'dow30') {
setTimeout(() => loadMiniChartsForPage('dow'), 300);
} else {
setTimeout(() => loadMiniChartsForPage('nq1'), 300);
}
}
// ํŠน์ • ํŽ˜์ด์ง€์˜ ๋ฏธ๋‹ˆ์ฐจํŠธ๋งŒ ๋กœ๋“œ
// TradingView ์‹ฌ๋ณผ ๋งคํ•‘ (์‹ค์ œ ๊ฑฐ๋ž˜์†Œ์™€ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Œ)
// NYSE ์ข…๋ชฉ (TradingView์—์„œ NYSE๋กœ ํ‘œ์‹œ๋˜๋Š” ์ข…๋ชฉ๋งŒ)
const NYSE_TICKERS = ['UNH', 'GS', 'HD', 'CAT', 'CRM', 'MCD', 'V', 'TRV', 'AXP', 'JPM', 'IBM', 'BA', 'MMM', 'JNJ', 'PG', 'MRK', 'NKE', 'DIS', 'KO', 'WMT', 'VZ', 'DOW', 'SHW', 'TRI', 'LIN', 'HON', 'AEP', 'CEG'];
// TradingView ํŠน์ˆ˜ ์‹ฌ๋ณผ ๋งคํ•‘ (์‹ค์ œ ๊ฑฐ๋ž˜์†Œ์™€ TradingView ํ‘œ์‹œ๊ฐ€ ๋‹ค๋ฅธ ๊ฒฝ์šฐ)
const TRADINGVIEW_SYMBOLS = {
'WMT': 'NASDAQ:WMT', // Walmart - ์‹ค์ œ NYSE์ง€๋งŒ TradingView๋Š” NASDAQ์œผ๋กœ ํ‘œ์‹œ
'TRI': 'NYSE:TRI', // Thomson Reuters
'LIN': 'NYSE:LIN', // Linde
'HON': 'NASDAQ:HON', // Honeywell
'AEP': 'NASDAQ:AEP', // American Electric Power
'CEG': 'NASDAQ:CEG', // Constellation Energy
'ASML': 'NASDAQ:ASML', // ASML (๋„ค๋œ๋ž€๋“œ์ง€๋งŒ NASDAQ ADR)
'AZN': 'NASDAQ:AZN', // AstraZeneca
'PDD': 'NASDAQ:PDD' // PDD Holdings
};
// ํ‹ฐ์ปค์— ๋งž๋Š” TradingView ์‹ฌ๋ณผ ๋ฐ˜ํ™˜
function getTradingViewSymbol(ticker, defaultExchange) {
// ํŠน์ˆ˜ ๋งคํ•‘์ด ์žˆ์œผ๋ฉด ์‚ฌ์šฉ
if (TRADINGVIEW_SYMBOLS[ticker]) {
return TRADINGVIEW_SYMBOLS[ticker];
}
// NYSE ์ข…๋ชฉ์ด๋ฉด NYSE ์‚ฌ์šฉ
if (NYSE_TICKERS.includes(ticker)) {
return `NYSE:${ticker}`;
}
// ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ
return `${defaultExchange || 'NASDAQ'}:${ticker}`;
}
function loadMiniChartsForPage(prefix) {
if (!screeningData) return;
const containers = document.querySelectorAll(`[id^="mini-chart-${prefix}-"]`);
containers.forEach(container => {
if (container.dataset.loaded) return;
container.dataset.loaded = 'true';
const ticker = container.id.replace(`mini-chart-${prefix}-`, '');
const stock = screeningData.find(s => s.ticker === ticker);
if (!stock) return;
// TradingView ์‹ฌ๋ณผ ๊ฐ€์ ธ์˜ค๊ธฐ
const symbol = getTradingViewSymbol(ticker, stock.exchange);
try {
new TradingView.MediumWidget({
symbols: [[ticker, `${symbol}|3D`]],
chartOnly: true,
width: '100%',
height: 60,
locale: 'kr',
colorTheme: 'dark',
autosize: false,
showVolume: false,
hideDateRanges: true,
scalePosition: 'no',
scaleMode: 'Normal',
fontFamily: 'sans-serif',
noTimeScale: true,
chartType: 'area',
lineColor: stock.change_percent >= 0 ? '#00ff88' : '#ff4757',
bottomColor: stock.change_percent >= 0 ? 'rgba(0,255,136,0.1)' : 'rgba(255,71,87,0.1)',
lineWidth: 2,
container_id: container.id
});
} catch (e) {
console.log('๋ฏธ๋‹ˆ์ฐจํŠธ ๋กœ๋“œ ์‹คํŒจ:', ticker, e);
}
});
}
function initMarketTab() {
try {
initSentimentGauge();
initSectorChart();
initUpDownChart();
initKeywordCloud();
initTradingViewWidgets();
} catch (e) {
console.error('initMarketTab ์˜ค๋ฅ˜:', e);
}
}
// ์„นํ„ฐ๋ณ„ ํผํฌ๋จผ์Šค ์ฐจํŠธ
let sectorChartInstance = null;
function initSectorChart() {
try {
const canvas = document.getElementById('sectorChart');
if (!canvas || !screeningData || screeningData.length === 0) return;
if (sectorChartInstance) sectorChartInstance.destroy();
// ์„นํ„ฐ๋ณ„ ํ‰๊ท  ๋“ฑ๋ฝ๋ฅ  ๊ณ„์‚ฐ
const sectorData = {};
screeningData.forEach(s => {
const sector = s.sector || 'Other';
if (!sectorData[sector]) sectorData[sector] = { sum: 0, count: 0 };
sectorData[sector].sum += s.change_percent || 0;
sectorData[sector].count++;
});
const sectors = Object.keys(sectorData).map(k => ({
name: k,
avg: sectorData[k].sum / sectorData[k].count
})).sort((a, b) => b.avg - a.avg);
if (sectors.length === 0) return;
const labels = sectors.map(s => s.name.replace('Consumer ', '').replace('Communication', 'Comm'));
const data = sectors.map(s => s.avg);
const colors = data.map(v => v >= 0 ? '#00ff88' : '#ff4757');
sectorChartInstance = new Chart(canvas, {
type: 'bar',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: colors,
borderRadius: 4,
barThickness: 18
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: {
grid: { color: 'rgba(255,255,255,0.1)' },
ticks: { color: '#888', callback: v => v + '%' }
},
y: {
grid: { display: false },
ticks: { color: '#ccc', font: { size: 11 } }
}
}
}
});
} catch (e) {
console.error('initSectorChart ์˜ค๋ฅ˜:', e);
}
}
// ์ƒ์Šน/ํ•˜๋ฝ ๋น„์œจ ๋„๋„› ์ฐจํŠธ
let upDownChartInstance = null;
function initUpDownChart() {
try {
const canvas = document.getElementById('upDownChart');
if (!canvas || !screeningData || screeningData.length === 0) return;
if (upDownChartInstance) upDownChartInstance.destroy();
let up = 0, down = 0, flat = 0;
screeningData.forEach(s => {
const chg = s.change_percent || 0;
if (chg > 0.1) up++;
else if (chg < -0.1) down++;
else flat++;
});
upDownChartInstance = new Chart(canvas, {
type: 'doughnut',
data: {
labels: ['์ƒ์Šน', 'ํ•˜๋ฝ', '๋ณดํ•ฉ'],
datasets: [{
data: [up, down, flat],
backgroundColor: ['#00ff88', '#ff4757', '#ffa502'],
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '60%',
plugins: {
legend: { display: false }
}
}
});
// ๋ ˆ์ „๋“œ ํ‘œ์‹œ
const legendEl = document.getElementById('upDownLegend');
if (legendEl) {
legendEl.innerHTML = `
<div class="legend-item"><div class="legend-dot" style="background:#00ff88"></div>์ƒ์Šน ${up}๊ฐœ</div>
<div class="legend-item"><div class="legend-dot" style="background:#ff4757"></div>ํ•˜๋ฝ ${down}๊ฐœ</div>
<div class="legend-item"><div class="legend-dot" style="background:#ffa502"></div>๋ณดํ•ฉ ${flat}๊ฐœ</div>
`;
}
} catch (e) {
console.error('initUpDownChart ์˜ค๋ฅ˜:', e);
}
}
// TradingView ์œ„์ ฏ ์ดˆ๊ธฐํ™”
function initTradingViewWidgets() {
try {
// ํ‹ฐ์ปค ํ…Œ์ดํ”„
const tickerTape = document.getElementById('ticker-tape-widget');
if (tickerTape && !tickerTape.dataset.loaded) {
tickerTape.dataset.loaded = 'true';
tickerTape.innerHTML = `
<div class="tradingview-widget-container">
<div class="tradingview-widget-container__widget"></div>
</div>
`;
const script = document.createElement('script');
script.src = 'https://s3.tradingview.com/external-embedding/embed-widget-ticker-tape.js';
script.async = true;
script.innerHTML = JSON.stringify({
"symbols": [
{"proName": "FOREXCOM:SPXUSD", "title": "S&P 500"},
{"proName": "FOREXCOM:NSXUSD", "title": "NASDAQ"},
{"proName": "BITSTAMP:BTCUSD", "title": "Bitcoin"},
{"proName": "NASDAQ:NVDA", "title": "NVIDIA"},
{"proName": "NASDAQ:AAPL", "title": "Apple"},
{"proName": "NASDAQ:MSFT", "title": "Microsoft"},
{"proName": "NASDAQ:GOOGL", "title": "Alphabet"},
{"proName": "NASDAQ:AMZN", "title": "Amazon"},
{"proName": "NASDAQ:META", "title": "Meta"},
{"proName": "NASDAQ:TSLA", "title": "Tesla"}
],
"showSymbolLogo": true,
"isTransparent": true,
"displayMode": "adaptive",
"colorTheme": "dark",
"locale": "kr"
});
tickerTape.querySelector('.tradingview-widget-container').appendChild(script);
}
// ๋งˆ์ผ“ ์˜ค๋ฒ„๋ทฐ
const marketOverview = document.getElementById('market-overview-widget');
if (marketOverview && !marketOverview.dataset.loaded) {
marketOverview.dataset.loaded = 'true';
marketOverview.innerHTML = `<div class="tradingview-widget-container" style="height:100%;width:100%"><div class="tradingview-widget-container__widget" style="height:100%;width:100%"></div></div>`;
const script = document.createElement('script');
script.src = 'https://s3.tradingview.com/external-embedding/embed-widget-market-overview.js';
script.async = true;
script.innerHTML = JSON.stringify({
"colorTheme": "dark",
"dateRange": "1D",
"showChart": true,
"locale": "kr",
"largeChartUrl": "",
"isTransparent": true,
"showSymbolLogo": true,
"showFloatingTooltip": true,
"width": "100%",
"height": "100%",
"tabs": [
{
"title": "์ง€์ˆ˜",
"symbols": [
{"s": "FOREXCOM:SPXUSD", "d": "S&P 500"},
{"s": "FOREXCOM:NSXUSD", "d": "NASDAQ"},
{"s": "FOREXCOM:DJI", "d": "Dow Jones"},
{"s": "INDEX:VIX", "d": "VIX"}
],
"originalTitle": "Indices"
},
{
"title": "๋น…ํ…Œํฌ",
"symbols": [
{"s": "NASDAQ:NVDA", "d": "NVIDIA"},
{"s": "NASDAQ:AAPL", "d": "Apple"},
{"s": "NASDAQ:MSFT", "d": "Microsoft"},
{"s": "NASDAQ:GOOGL", "d": "Alphabet"},
{"s": "NASDAQ:META", "d": "Meta"}
],
"originalTitle": "Big Tech"
}
]
});
marketOverview.querySelector('.tradingview-widget-container').appendChild(script);
}
// ํžˆํŠธ๋งต
const heatmap = document.getElementById('stock-heatmap-widget');
if (heatmap && !heatmap.dataset.loaded) {
heatmap.dataset.loaded = 'true';
heatmap.innerHTML = `<div class="tradingview-widget-container" style="height:100%;width:100%"><div class="tradingview-widget-container__widget" style="height:100%;width:100%"></div></div>`;
const script = document.createElement('script');
script.src = 'https://s3.tradingview.com/external-embedding/embed-widget-stock-heatmap.js';
script.async = true;
script.innerHTML = JSON.stringify({
"exchanges": [],
"dataSource": "SPX500",
"grouping": "sector",
"blockSize": "market_cap_basic",
"blockColor": "change",
"locale": "kr",
"symbolUrl": "",
"colorTheme": "dark",
"hasTopBar": false,
"isDataSet498or": true,
"isZoomEnabled": true,
"hasSymbolTooltip": true,
"isMonoSize": false,
"width": "100%",
"height": "100%"
});
heatmap.querySelector('.tradingview-widget-container').appendChild(script);
}
} catch (e) {
console.error('initTradingViewWidgets ์˜ค๋ฅ˜:', e);
}
}
// ํžˆํŠธ๋งต ์ „ํ™˜ ํ•จ์ˆ˜
function switchHeatmap(dataSource, btn) {
// ํƒญ ํ™œ์„ฑํ™”
document.querySelectorAll('.heatmap-tab').forEach(t => t.classList.remove('active'));
btn.classList.add('active');
// ํžˆํŠธ๋งต ์žฌ๋กœ๋“œ
const heatmap = document.getElementById('stock-heatmap-widget');
heatmap.innerHTML = `<div class="tradingview-widget-container" style="height:100%;width:100%"><div class="tradingview-widget-container__widget" style="height:100%;width:100%"></div></div>`;
const script = document.createElement('script');
script.src = 'https://s3.tradingview.com/external-embedding/embed-widget-stock-heatmap.js';
script.async = true;
script.innerHTML = JSON.stringify({
"exchanges": [],
"dataSource": dataSource,
"grouping": "sector",
"blockSize": "market_cap_basic",
"blockColor": "change",
"locale": "kr",
"symbolUrl": "",
"colorTheme": "dark",
"hasTopBar": false,
"isDataSetEnabled": true,
"isZoomEnabled": true,
"hasSymbolTooltip": true,
"isMonoSize": false,
"width": "100%",
"height": "100%"
});
heatmap.querySelector('.tradingview-widget-container').appendChild(script);
}
function initSentimentGauge() {
const canvas = document.getElementById('sentimentGauge');
if (!canvas) return;
if (sentimentGaugeInstance) sentimentGaugeInstance.destroy();
let score = 50;
let pos = 0, neg = 0;
newsData.forEach(n => { if (n.sentiment === 'positive') pos++; else if (n.sentiment === 'negative') neg++; });
if (newsData.length > 0) score = ((pos * 100) + ((newsData.length - pos - neg) * 50)) / newsData.length;
if (screeningData && screeningData.length > 0) {
const avgChange = screeningData.reduce((s, x) => s + (x.change_percent || 0), 0) / screeningData.length;
score += avgChange * 5;
}
score = Math.max(0, Math.min(100, score));
sentimentGaugeInstance = new Chart(canvas, {
type: 'doughnut',
data: { datasets: [{ data: [score, 100 - score], backgroundColor: [score < 20 ? '#ff4757' : score < 40 ? '#ffa502' : score < 60 ? '#78909c' : score < 80 ? '#2ed573' : '#00ff88', 'rgba(255,255,255,0.05)'], borderWidth: 0 }] },
options: { circumference: 180, rotation: -90, cutout: '75%', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { enabled: false } } }
});
document.getElementById('sentimentValue').textContent = score.toFixed(1);
let label = 'Neutral', color = '#78909c';
if (score < 20) { label = 'Strong Sell'; color = '#ff4757'; }
else if (score < 40) { label = 'Sell'; color = '#ffa502'; }
else if (score < 60) { label = 'Neutral'; color = '#78909c'; }
else if (score < 80) { label = 'Buy'; color = '#2ed573'; }
else { label = 'Strong Buy'; color = '#00ff88'; }
document.getElementById('sentimentLabel').textContent = label;
document.getElementById('sentimentLabel').style.color = color;
}
function initKeywordCloud() {
const container = document.getElementById('keywordCloud');
if (!container) return;
container.innerHTML = '';
const keywords = {};
// ํ•œ๊ธ€ + ์˜์–ด ํ‚ค์›Œ๋“œ ํ™•์žฅ
const kwList = [
// ๊ฑฐ์‹œ๊ฒฝ์ œ
'๊ธˆ๋ฆฌ', '์ธํ”Œ๋ ˆ์ด์…˜', 'Fed', 'FOMC', '๊ธˆ๋ฆฌ์ธํ•˜', '๊ธˆ๋ฆฌ๋™๊ฒฐ', 'CPI', 'GDP', '๊ณ ์šฉ', '์‹ค์—…๋ฅ ',
'interest rate', 'inflation', 'rate cut', 'rate hike', 'economy',
// ๋ฌด์—ญ/์ง€์ •ํ•™
'๊ด€์„ธ', '๋ฌด์—ญ', '์ˆ˜์ถœ', '์ˆ˜์ž…', '์ œ์žฌ', 'ํŠธ๋Ÿผํ”„', 'Trump', '์ค‘๊ตญ', 'China', '๋Ÿฌ์‹œ์•„',
'์šฐํฌ๋ผ์ด๋‚˜', '์ „์Ÿ', 'tariff', 'trade war',
// ์—๋„ˆ์ง€
'์œ ๊ฐ€', 'OPEC', '์›์œ ', '์ฒœ์—ฐ๊ฐ€์Šค', 'oil', 'crude', 'energy',
// ๊ธฐ์ˆ /AI
'AI', '์ธ๊ณต์ง€๋Šฅ', '๋ฐ˜๋„์ฒด', 'GPU', 'chip', 'semiconductor', 'NVIDIA', 'AMD', 'Intel',
'OpenAI', 'ChatGPT', 'Gemini', '๋ฐ์ดํ„ฐ์„ผํ„ฐ', 'cloud',
// ์ „๊ธฐ์ฐจ/๋ฐฐํ„ฐ๋ฆฌ
'๋ฐฐํ„ฐ๋ฆฌ', 'EV', '์ „๊ธฐ์ฐจ', 'Tesla', '๋ฆฌํŠฌ', '์ถฉ์ „', 'electric vehicle',
// ์•”ํ˜ธํ™”ํ
'Bitcoin', 'BTC', 'crypto', '์•”ํ˜ธํ™”ํ', 'ETF', 'Ethereum',
// ๊ธฐ์—… ์‹ค์ 
'์‹ค์ ', '๋งค์ถœ', '์ˆœ์ด์ต', 'earnings', 'revenue', 'profit', 'beat', 'miss', 'guidance',
// M&A/IPO
'M&A', 'IPO', '์ธ์ˆ˜', 'ํ•ฉ๋ณ‘', 'acquisition', 'merger',
// ๊ทœ์ œ
'๊ทœ์ œ', 'SEC', 'FTC', 'antitrust', '๋ฐ˜๋…์ ', '์†Œ์†ก', 'lawsuit',
// ๊ธฐํƒ€ ์ด์Šˆ
'๋ฐฐ๋‹น', 'dividend', '์ž์‚ฌ์ฃผ', 'buyback', '๋ฆฌ์ฝœ', 'recall', 'ํŒŒ์—…', 'strike',
// ๋น…ํ…Œํฌ
'Apple', 'Microsoft', 'Google', 'Amazon', 'Meta', 'Netflix'
];
// ๋‰ด์Šค ์ œ๋ชฉ + ๋‚ด์šฉ์—์„œ ํ‚ค์›Œ๋“œ ์ถ”์ถœ
newsData.forEach(n => {
const text = ((n.title || '') + ' ' + (n.content || '') + ' ' + (n.title_original || '')).toLowerCase();
kwList.forEach(k => {
const kLower = k.toLowerCase();
if (text.includes(kLower)) {
// ๋Œ€์†Œ๋ฌธ์ž ํ†ต์ผ (ํ‘œ์‹œ์šฉ)
const displayKey = k.charAt(0).toUpperCase() + k.slice(1);
keywords[displayKey] = (keywords[displayKey] || 0) + 1;
}
});
});
// ์Šคํฌ๋ฆฌ๋‹ ๋ฐ์ดํ„ฐ์—์„œ ์„นํ„ฐ ํ‚ค์›Œ๋“œ ์ถ”๊ฐ€
if (screeningData) {
screeningData.forEach(s => {
if (s.sector) {
const sectorKey = s.sector.replace('Consumer ', '');
keywords[sectorKey] = (keywords[sectorKey] || 0) + 1;
}
});
}
const sorted = Object.entries(keywords).sort((a, b) => b[1] - a[1]).slice(0, 20);
const max = sorted[0] ? sorted[0][1] : 1;
const colors = ['#ff4757', '#ffa502', '#2ed573', '#00d9ff', '#9b59b6', '#e74c3c', '#3498db', '#1abc9c', '#f39c12', '#e91e63', '#00ff88', '#ff6b81', '#7bed9f', '#70a1ff', '#5352ed'];
sorted.forEach(([kw, cnt], i) => {
const tag = document.createElement('span');
tag.className = 'keyword-tag';
const ratio = cnt / max;
tag.classList.add(ratio > 0.7 ? 'size-xl' : ratio > 0.4 ? 'size-lg' : ratio > 0.2 ? 'size-md' : 'size-sm');
tag.style.backgroundColor = colors[i % colors.length];
tag.style.color = '#fff';
tag.textContent = `${kw} (${cnt})`;
tag.onclick = () => window.open(`https://www.google.com/search?q=${encodeURIComponent(kw + ' stock market news')}&tbm=nws`, '_blank');
container.appendChild(tag);
});
if (sorted.length === 0) {
// ๊ธฐ๋ณธ ํŠธ๋ Œ๋“œ ํ‚ค์›Œ๋“œ ํ‘œ์‹œ
const defaultKeywords = ['AI', 'Fed', '๋ฐ˜๋„์ฒด', 'Tesla', 'NVIDIA', 'Bitcoin', '์‹ค์ ', '๊ธˆ๋ฆฌ'];
defaultKeywords.forEach((kw, i) => {
const tag = document.createElement('span');
tag.className = 'keyword-tag size-md';
tag.style.backgroundColor = colors[i % colors.length];
tag.style.color = '#fff';
tag.textContent = kw;
tag.onclick = () => window.open(`https://www.google.com/search?q=${encodeURIComponent(kw + ' stock market news')}&tbm=nws`, '_blank');
container.appendChild(tag);
});
}
}
function initStockHeatmap() {
const container = document.getElementById('stockHeatmap');
container.innerHTML = '';
if (!screeningData || screeningData.length === 0) {
container.innerHTML = '<div style="color:#888;padding:20px;">๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</div>';
return;
}
// ์‹œ๊ฐ€์ด์•ก ๊ธฐ์ค€ ์ตœ๋Œ€๊ฐ’ ๊ณ„์‚ฐ
const maxMarketCap = Math.max(...screeningData.map(s => s.market_cap || 0));
screeningData.forEach(stock => {
const cell = document.createElement('div');
cell.className = 'heatmap-cell';
const chg = stock.change_percent || 0;
// ์‹œ๊ฐ€์ด์•ก ๋น„์ค‘์— ๋”ฐ๋ฅธ ํฌ๊ธฐ ์กฐ์ ˆ
const mcapRatio = (stock.market_cap || 0) / maxMarketCap;
let cellSize = 80; // ๊ธฐ๋ณธ ํฌ๊ธฐ
let fontSize = 0.85;
if (mcapRatio > 0.8) { cellSize = 140; fontSize = 1.2; }
else if (mcapRatio > 0.5) { cellSize = 120; fontSize = 1.05; }
else if (mcapRatio > 0.3) { cellSize = 100; fontSize = 0.95; }
else if (mcapRatio > 0.15) { cellSize = 90; fontSize = 0.9; }
cell.style.minWidth = cellSize + 'px';
cell.style.minHeight = (cellSize * 0.7) + 'px';
cell.style.flex = `0 0 ${cellSize}px`;
// ๋“ฑ๋ฝ๋ฅ ์— ๋”ฐ๋ฅธ ์ƒ‰์ƒ
if (chg > 3) cell.classList.add('strong-up');
else if (chg > 1) cell.classList.add('up');
else if (chg > 0) cell.classList.add('slight-up');
else if (chg > -1) cell.classList.add('neutral');
else if (chg > -3) cell.classList.add('slight-down');
else if (chg > -5) cell.classList.add('down');
else cell.classList.add('strong-down');
cell.innerHTML = `<div class="ticker" style="font-size:${fontSize}em">${stock.ticker}</div><div class="change">${chg >= 0 ? '+' : ''}${chg.toFixed(2)}%</div><div class="price">$${stock.price.toFixed(2)}</div>`;
cell.onclick = () => openChartModal(stock.ticker, stock.name, stock.exchange || 'NASDAQ');
container.appendChild(cell);
});
}
function initMiniCharts() {
if (!screeningData) return;
screeningData.forEach((stock, i) => {
const container = document.getElementById(`mini-chart-${stock.ticker}`);
if (!container || container.dataset.loaded) return;
container.dataset.loaded = 'true';
container.innerHTML = '';
// ๊ฑฐ๋ž˜์†Œ์— ๋”ฐ๋ฅธ ์‹ฌ๋ณผ ์„ค์ •
const exchange = stock.exchange || 'NASDAQ';
const symbol = `${exchange}:${stock.ticker}`;
new TradingView.MediumWidget({
symbols: [[stock.ticker, `${symbol}|3D`]],
chartOnly: true,
width: '100%',
height: 60,
locale: 'kr',
colorTheme: 'dark',
autosize: false,
showVolume: false,
hideDateRanges: true,
scalePosition: 'no',
scaleMode: 'Normal',
fontFamily: 'sans-serif',
noTimeScale: true,
chartType: 'area',
lineColor: stock.change_percent >= 0 ? '#00ff88' : '#ff4757',
bottomColor: stock.change_percent >= 0 ? 'rgba(0,255,136,0.1)' : 'rgba(255,71,87,0.1)',
lineWidth: 2,
container_id: `mini-chart-${stock.ticker}`
});
});
}
function openChartModal(ticker, name, exchange) {
// TradingView ์‹ฌ๋ณผ ๊ฐ€์ ธ์˜ค๊ธฐ
const symbol = getTradingViewSymbol(ticker, exchange);
document.getElementById('chart-modal-title').textContent = `${ticker} - ${name}`;
document.getElementById('chartModal').classList.add('active');
document.getElementById('tradingview-chart').innerHTML = '';
new TradingView.widget({
autosize: true,
symbol: symbol,
interval: 'D',
timezone: 'Asia/Seoul',
theme: 'dark',
style: '1',
locale: 'kr',
toolbar_bg: '#1a1a2e',
enable_publishing: false,
allow_symbol_change: true,
container_id: 'tradingview-chart',
hide_side_toolbar: false,
studies: ['RSI@tv-basicstudies', 'MASimple@tv-basicstudies']
});
}
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('active');
}
function renderMarkdown(text) {
if (!text) return '-';
if (typeof marked !== 'undefined') return marked.parse(text);
return text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>').replace(/\n/g, '<br>');
}
function updateElasticityUI(report) {
// ํƒ„๋ ฅ์„ฑ ๋ฐ์ดํ„ฐ ์ถ”์ถœ (๊ธฐ๋ณธ๊ฐ’ ์„ค์ •)
const upside = report.expected_upside || 15;
const downside = report.expected_downside || -10;
const prediction = report.base_prediction || report.upside || 8;
const upProb = report.up_probability || 55;
const downProb = report.down_probability || 45;
const riskReward = report.risk_reward || 1.5;
// ์ˆ˜์น˜ ํ‘œ์‹œ
document.getElementById('result-upside').textContent = `+${upside}%`;
document.getElementById('result-downside').textContent = `${downside}%`;
document.getElementById('result-prediction').textContent = prediction >= 0 ? `+${prediction}%` : `${prediction}%`;
document.getElementById('result-prediction').style.color = prediction >= 0 ? '#00d9ff' : '#ff4757';
document.getElementById('result-up-prob').textContent = `${upProb}%`;
document.getElementById('result-down-prob').textContent = `${downProb}%`;
document.getElementById('result-rr').textContent = `${riskReward}:1`;
// ์‹œ๊ฐ์  ๋ฐ” ์—…๋ฐ์ดํŠธ
const minVal = Math.min(downside, -30);
const maxVal = Math.max(upside, 40);
const range = maxVal - minVal;
// ์Šค์ผ€์ผ ๋ผ๋ฒจ ์—…๋ฐ์ดํŠธ
document.getElementById('elasticity-min').textContent = `${Math.round(minVal)}%`;
document.getElementById('elasticity-max').textContent = `+${Math.round(maxVal)}%`;
// ๋ฒ”์œ„ ๋ฐ” ์œ„์น˜ ๊ณ„์‚ฐ (downside ~ upside)
const rangeLeft = ((downside - minVal) / range) * 100;
const rangeRight = ((upside - minVal) / range) * 100;
const rangeEl = document.getElementById('elasticity-range');
rangeEl.style.left = `${rangeLeft}%`;
rangeEl.style.width = `${rangeRight - rangeLeft}%`;
// ๊ธฐ๋ณธ ์˜ˆ์ธก ๋งˆ์ปค ์œ„์น˜
const markerPos = ((prediction - minVal) / range) * 100;
const markerEl = document.getElementById('elasticity-marker');
markerEl.style.left = `calc(${markerPos}% - 2px)`;
}
function initAnalysisCharts(report) {
[metricsChartInstance, priceChartInstance, riskChartInstance, growthChartInstance].forEach(c => c && c.destroy());
const cp = report.current_price || 100, tp = report.target_price || 112;
const mc = document.getElementById('metricsChart');
if (mc) metricsChartInstance = new Chart(mc, { type: 'radar', data: { labels: ['์ˆ˜์ต์„ฑ', '์„ฑ์žฅ์„ฑ', '์•ˆ์ •์„ฑ', '๋ฐธ๋ฅ˜์—์ด์…˜', '๋ชจ๋ฉ˜ํ…€'], datasets: [{ data: [75+Math.random()*20, 70+Math.random()*25, 65+Math.random()*20, 60+Math.random()*25, 72+Math.random()*20], backgroundColor: 'rgba(0,217,255,0.2)', borderColor: '#00d9ff', borderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { r: { beginAtZero: true, max: 100, ticks: { color: '#888', backdropColor: 'transparent' }, grid: { color: 'rgba(255,255,255,0.1)' }, pointLabels: { color: '#ccc', font: { size: 10 } } } } } });
const pc = document.getElementById('priceChart');
if (pc) priceChartInstance = new Chart(pc, { type: 'bar', data: { labels: ['ํ˜„์žฌ๊ฐ€', '๋ชฉํ‘œ๊ฐ€', '์ปจ์„ผ์„œ์Šค'], datasets: [{ data: [cp, tp, tp*0.95], backgroundColor: ['#3498db', '#00ff88', '#9b59b6'], borderRadius: 5 }] }, options: { responsive: true, maintainAspectRatio: false, indexAxis: 'y', plugins: { legend: { display: false } }, scales: { x: { grid: { color: 'rgba(255,255,255,0.1)' }, ticks: { color: '#888' } }, y: { grid: { display: false }, ticks: { color: '#ccc' } } } } });
const rc = document.getElementById('riskChart');
if (rc) riskChartInstance = new Chart(rc, { type: 'doughnut', data: { labels: ['๊ธฐํšŒ', '๋ฆฌ์Šคํฌ', '์ค‘๋ฆฝ'], datasets: [{ data: [55+Math.random()*15, 25+Math.random()*10, 20], backgroundColor: ['#00ff88', '#ff4757', '#ffa502'], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { color: '#ccc', font: { size: 10 } } } } } });
const gc = document.getElementById('growthChart');
if (gc) growthChartInstance = new Chart(gc, { type: 'line', data: { labels: ['2024', '2025E', '2026E', '2027E'], datasets: [{ label: '๋งค์ถœ', data: [8,12,15,18].map(v => v+Math.random()*5), borderColor: '#00d9ff', fill: true, backgroundColor: 'rgba(0,217,255,0.1)', tension: 0.4 }, { label: 'EPS', data: [10,15,20,25].map(v => v+Math.random()*5), borderColor: '#00ff88', fill: true, backgroundColor: 'rgba(0,255,136,0.1)', tension: 0.4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { color: '#ccc', font: { size: 10 } } } }, scales: { y: { grid: { color: 'rgba(255,255,255,0.1)' }, ticks: { color: '#888' } }, x: { grid: { display: false }, ticks: { color: '#ccc' } } } } });
}
async function runDeepAnalysis(ticker, companyName, forceRefresh = false) {
currentTicker = ticker;
const statusEl = document.getElementById(`deep-status-${ticker}`);
if (statusEl) statusEl.innerHTML = 'โณ ๋ถ„์„ ์ค‘...';
document.getElementById('modal-ticker').textContent = `${ticker} (${companyName})`;
document.getElementById('deepAnalysisModal').classList.add('active');
document.getElementById('loading-indicator').classList.add('active');
document.getElementById('deep-analysis-result').style.display = 'none';
// ์žฌ์ƒ์„ฑ ๋ฒ„ํŠผ ์ˆจ๊ธฐ๊ธฐ
const regenerateBtn = document.getElementById('regenerate-btn');
if (regenerateBtn) regenerateBtn.style.display = 'none';
try {
// ํƒ€์ž„์•„์›ƒ ์„ค์ • (2๋ถ„)
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 120000);
const url = forceRefresh
? `/api/deep-analysis/${ticker}?force=true`
: `/api/deep-analysis/${ticker}`;
const res = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
const data = await res.json();
if (data.success) {
const report = data.report;
// ๋ถ€๋ถ„ ์ƒ์„ฑ ๊ฐ์ง€ (ํ•ต์‹ฌ ํ•„๋“œ ๋ˆ„๋ฝ ์ฒดํฌ)
const requiredFields = ['executive_summary', 'company_overview', 'financial_analysis', 'final_recommendation'];
const missingFields = requiredFields.filter(f => !report[f] || report[f].length < 50);
if (missingFields.length > 0) {
console.warn('๋ถ€๋ถ„ ์ƒ์„ฑ ๊ฐ์ง€:', missingFields);
displayDeepAnalysisResult(report, ticker, true); // ๋ถˆ์™„์ „ ํ‘œ์‹œ
if (statusEl) statusEl.innerHTML = 'โš ๏ธ ์ผ๋ถ€ ๋ฏธ์™„์„ฑ';
} else {
displayDeepAnalysisResult(report, ticker, false);
if (statusEl) statusEl.innerHTML = data.from_cache ? '๐Ÿ’พ ์บ์‹œ' : 'โœ… ์™„๋ฃŒ';
}
} else {
if (statusEl) statusEl.innerHTML = 'โŒ ์‹คํŒจ';
showAnalysisError(data.error || '๋ถ„์„ ์‹คํŒจ');
}
} catch (e) {
if (e.name === 'AbortError') {
if (statusEl) statusEl.innerHTML = 'โฑ๏ธ ํƒ€์ž„์•„์›ƒ';
showAnalysisError('๋ถ„์„ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.');
} else {
if (statusEl) statusEl.innerHTML = 'โŒ ์˜ค๋ฅ˜';
showAnalysisError('๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
}
}
document.getElementById('loading-indicator').classList.remove('active');
}
function showAnalysisError(message) {
document.getElementById('deep-analysis-result').style.display = 'block';
document.getElementById('deep-analysis-result').innerHTML = `
<div style="text-align: center; padding: 40px;">
<div style="font-size: 3em; margin-bottom: 20px;">โŒ</div>
<h3 style="color: #ff4757; margin-bottom: 15px;">${message}</h3>
<button class="deep-analysis-btn" onclick="runDeepAnalysis(currentTicker, '', true)" style="margin-top: 20px;">
๐Ÿ”„ ๋‹ค์‹œ ์‹œ๋„
</button>
</div>
`;
}
function displayDeepAnalysisResult(report, ticker, isIncomplete = false) {
// ๊ฐ ํ•„๋“œ ๋ Œ๋”๋ง (์—†์œผ๋ฉด ๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€)
const fields = ['executive_summary', 'company_overview', 'financial_analysis', 'technical_analysis', 'industry_analysis', 'risk_assessment', 'investment_thesis', 'price_targets', 'catalysts', 'final_recommendation'];
fields.forEach(key => {
const elId = `result-${key.replace(/_/g, '-')}`;
const el = document.getElementById(elId);
if (el) {
const content = report[key];
if (content && content.length > 20) {
el.innerHTML = renderMarkdown(content);
} else {
el.innerHTML = '<span style="color: #888;">โณ ์ƒ์„ฑ ์ค‘ ๋˜๋Š” ๋ฐ์ดํ„ฐ ์—†์Œ</span>';
}
}
});
document.getElementById('result-current-price').textContent = report.current_price ? `$${report.current_price.toLocaleString()}` : '-';
document.getElementById('result-target-price').textContent = report.target_price ? `$${report.target_price.toLocaleString()}` : '-';
document.getElementById('result-rating').textContent = report.rating || 'Buy';
// ํƒ„๋ ฅ์„ฑ ์˜ˆ์ธก UI ์—…๋ฐ์ดํŠธ
updateElasticityUI(report);
setTimeout(() => initAnalysisCharts(report), 100);
document.getElementById('download-docx-btn').href = `/api/download-docx/${ticker}`;
document.getElementById('download-docx-btn').style.display = 'inline-flex';
document.getElementById('download-pdf-btn').style.display = 'inline-flex';
// ๋ถˆ์™„์ „ ์‹œ ์žฌ์ƒ์„ฑ ๋ฒ„ํŠผ ํ‘œ์‹œ
let regenerateBtn = document.getElementById('regenerate-btn');
if (!regenerateBtn) {
regenerateBtn = document.createElement('button');
regenerateBtn.id = 'regenerate-btn';
regenerateBtn.className = 'download-btn';
regenerateBtn.style.background = 'linear-gradient(135deg, #e74c3c, #c0392b)';
regenerateBtn.innerHTML = '๐Ÿ”„ ์žฌ์ƒ์„ฑ';
regenerateBtn.onclick = () => runDeepAnalysis(currentTicker, '', true);
document.getElementById('download-pdf-btn').parentNode.appendChild(regenerateBtn);
}
if (isIncomplete) {
regenerateBtn.style.display = 'inline-flex';
} else {
regenerateBtn.style.display = 'none';
}
document.getElementById('deep-analysis-result').style.display = 'block';
}
async function downloadAsPDF() {
const btn = document.getElementById('download-pdf-btn');
const originalText = btn.innerHTML;
btn.innerHTML = 'โณ ์ค€๋น„ ์ค‘...';
btn.disabled = true;
try {
// ๋ถ„์„ ๊ฒฐ๊ณผ ์˜์—ญ
const resultArea = document.getElementById('deep-analysis-result');
if (!resultArea) {
throw new Error('๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.');
}
// ํ”„๋ฆฐํŠธ์šฉ HTML ์ƒ์„ฑ
const printContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${currentTicker} ์‹ฌ์ธต ๋ถ„์„ ๋ฆฌํฌํŠธ</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
background: white;
color: #333;
padding: 30px;
line-height: 1.6;
}
h1 { color: #0066cc; margin-bottom: 20px; font-size: 24px; border-bottom: 2px solid #0066cc; padding-bottom: 10px; }
h2 { color: #0066cc; margin: 25px 0 15px 0; font-size: 18px; }
h3 { color: #009966; margin: 20px 0 10px 0; font-size: 16px; }
p { margin-bottom: 10px; }
.header { text-align: center; margin-bottom: 30px; padding: 20px; background: #f5f5f5; border-radius: 10px; }
.header h1 { border: none; margin: 0; }
.header .date { color: #666; font-size: 14px; margin-top: 10px; }
.metrics { display: flex; justify-content: space-around; margin: 20px 0; padding: 15px; background: #f9f9f9; border-radius: 10px; }
.metric { text-align: center; }
.metric-label { color: #666; font-size: 12px; }
.metric-value { font-size: 20px; font-weight: bold; color: #009966; }
.section { margin-bottom: 25px; padding: 20px; background: #fafafa; border-radius: 10px; border-left: 3px solid #0066cc; }
.section-title { color: #0066cc; font-size: 16px; font-weight: bold; margin-bottom: 15px; }
.content { color: #333; }
ul, ol { margin-left: 20px; margin-bottom: 10px; }
li { margin-bottom: 5px; }
strong { color: #000; }
.footer { text-align: center; margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #999; font-size: 12px; }
@page { margin: 1cm; }
</style>
</head>
<body>
<div class="header">
<h1>๐Ÿ“Š ${currentTicker} ์‹ฌ์ธต ๋ถ„์„ ๋ฆฌํฌํŠธ</h1>
<div class="date">์ƒ์„ฑ์ผ: ${new Date().toLocaleDateString('ko-KR')} | AI Stock Research Center</div>
</div>
<div class="metrics">
<div class="metric">
<div class="metric-label">ํ˜„์žฌ๊ฐ€</div>
<div class="metric-value">${document.getElementById('result-current-price')?.textContent || '-'}</div>
</div>
<div class="metric">
<div class="metric-label">๋ชฉํ‘œ๊ฐ€</div>
<div class="metric-value">${document.getElementById('result-target-price')?.textContent || '-'}</div>
</div>
<div class="metric">
<div class="metric-label">ํˆฌ์ž์˜๊ฒฌ</div>
<div class="metric-value">${document.getElementById('result-rating')?.textContent || '-'}</div>
</div>
</div>
<div class="section" style="background: #f0f8ff; border-left-color: #0099cc;">
<div class="section-title">๐Ÿ“Š ์ˆ˜์ต๋ฅ  ์ „๋ง</div>
<div style="display: flex; justify-content: space-around; text-align: center; margin: 15px 0;">
<div>
<div style="color: #cc0000; font-size: 12px;">๐Ÿ“‰ ํ•˜๋ฝ ๋ฆฌ์Šคํฌ</div>
<div style="font-size: 20px; font-weight: bold; color: #cc0000;">${document.getElementById('result-downside')?.textContent || '-'}</div>
<div style="font-size: 11px; color: #666;">${document.getElementById('result-down-prob')?.textContent || '-'} ํ™•๋ฅ </div>
</div>
<div>
<div style="color: #0066cc; font-size: 12px;">๐ŸŽฏ ๊ธฐ๋Œ€ ์ˆ˜์ต</div>
<div style="font-size: 24px; font-weight: bold; color: #0066cc;">${document.getElementById('result-prediction')?.textContent || '-'}</div>
<div style="font-size: 11px; color: #666;">R/R ${document.getElementById('result-rr')?.textContent || '-'}</div>
</div>
<div>
<div style="color: #009966; font-size: 12px;">๐Ÿ“ˆ ์ƒ์Šน ์ž ์žฌ๋ ฅ</div>
<div style="font-size: 20px; font-weight: bold; color: #009966;">${document.getElementById('result-upside')?.textContent || '-'}</div>
<div style="font-size: 11px; color: #666;">${document.getElementById('result-up-prob')?.textContent || '-'} ํ™•๋ฅ </div>
</div>
</div>
<div style="text-align: center; font-size: 10px; color: #999;">โ€ป AI ๊ธฐ๋ฐ˜ ๋ถ„์„์ด๋ฉฐ ์‹ค์ œ ํˆฌ์ž ์†์ต์„ ๋ณด์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค</div>
</div>
<div class="section">
<div class="section-title">๐Ÿ“‹ ํ•ต์‹ฌ ์š”์•ฝ</div>
<div class="content">${document.getElementById('result-executive-summary')?.innerHTML || '-'}</div>
</div>
<div class="section">
<div class="section-title">๐Ÿข ํšŒ์‚ฌ ๊ฐœ์š”</div>
<div class="content">${document.getElementById('result-company-overview')?.innerHTML || '-'}</div>
</div>
<div class="section">
<div class="section-title">๐Ÿ’ฐ ์žฌ๋ฌด ๋ถ„์„</div>
<div class="content">${document.getElementById('result-financial-analysis')?.innerHTML || '-'}</div>
</div>
<div class="section">
<div class="section-title">๐Ÿ“ˆ ๊ธฐ์ˆ ์  ๋ถ„์„</div>
<div class="content">${document.getElementById('result-technical-analysis')?.innerHTML || '-'}</div>
</div>
<div class="section">
<div class="section-title">๐Ÿญ ์‚ฐ์—… ๋ถ„์„</div>
<div class="content">${document.getElementById('result-industry-analysis')?.innerHTML || '-'}</div>
</div>
<div class="section">
<div class="section-title">โš ๏ธ ๋ฆฌ์Šคํฌ ํ‰๊ฐ€</div>
<div class="content">${document.getElementById('result-risk-assessment')?.innerHTML || '-'}</div>
</div>
<div class="section">
<div class="section-title">๐Ÿ’ก ํˆฌ์ž ๋…ผ๋ฆฌ</div>
<div class="content">${document.getElementById('result-investment-thesis')?.innerHTML || '-'}</div>
</div>
<div class="section">
<div class="section-title">๐ŸŽฏ ๋ชฉํ‘œ ์ฃผ๊ฐ€</div>
<div class="content">${document.getElementById('result-price-targets')?.innerHTML || '-'}</div>
</div>
<div class="section">
<div class="section-title">๐Ÿš€ ์นดํƒˆ๋ฆฌ์ŠคํŠธ</div>
<div class="content">${document.getElementById('result-catalysts')?.innerHTML || '-'}</div>
</div>
<div class="section">
<div class="section-title">โœ… ์ตœ์ข… ๊ถŒ๊ณ </div>
<div class="content">${document.getElementById('result-final-recommendation')?.innerHTML || '-'}</div>
</div>
<div class="footer">
<p>๋ณธ ๋ณด๊ณ ์„œ๋Š” AI ๊ธฐ๋ฐ˜ ์ž๋™ ๋ถ„์„ ๊ฒฐ๊ณผ์ด๋ฉฐ, ํˆฌ์ž ๊ฒฐ์ •์€ ๋ณธ์ธ์˜ ํŒ๋‹จ๊ณผ ์ฑ…์ž„ ํ•˜์— ์ด๋ฃจ์–ด์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค.</p>
<p>ยฉ ${new Date().getFullYear()} AI Stock Research Center | Ginigen AI</p>
</div>
</body>
</html>
`;
// ์ˆจ๊ฒจ์ง„ iframe ์‚ฌ์šฉ (ํŒ์—… ์ฐจ๋‹จ ์šฐํšŒ)
let printFrame = document.getElementById('print-frame');
if (!printFrame) {
printFrame = document.createElement('iframe');
printFrame.id = 'print-frame';
printFrame.style.position = 'absolute';
printFrame.style.top = '-10000px';
printFrame.style.left = '-10000px';
printFrame.style.width = '1px';
printFrame.style.height = '1px';
document.body.appendChild(printFrame);
}
const frameDoc = printFrame.contentWindow || printFrame.contentDocument;
const doc = frameDoc.document || frameDoc;
doc.open();
doc.write(printContent);
doc.close();
// ๋กœ๋“œ ์™„๋ฃŒ ๋Œ€๊ธฐ ํ›„ ํ”„๋ฆฐํŠธ
setTimeout(() => {
try {
printFrame.contentWindow.focus();
printFrame.contentWindow.print();
btn.innerHTML = 'โœ… ์™„๋ฃŒ!';
setTimeout(() => { btn.innerHTML = originalText; }, 2000);
} catch (printErr) {
console.error('ํ”„๋ฆฐํŠธ ์˜ค๋ฅ˜:', printErr);
btn.innerHTML = 'โŒ ์‹คํŒจ';
setTimeout(() => { btn.innerHTML = originalText; }, 3000);
}
btn.disabled = false;
}, 500);
} catch (e) {
console.error('PDF ์ƒ์„ฑ ์˜ค๋ฅ˜:', e);
btn.innerHTML = 'โŒ ์‹คํŒจ';
setTimeout(() => { btn.innerHTML = originalText; }, 3000);
alert('PDF ์ƒ์„ฑ ์‹คํŒจ: ' + e.message);
btn.disabled = false;
}
}
document.addEventListener('keydown', e => { if (e.key === 'Escape') { closeModal('deepAnalysisModal'); closeModal('chartModal'); } });
document.getElementById('deepAnalysisModal').addEventListener('click', e => { if (e.target.id === 'deepAnalysisModal') closeModal('deepAnalysisModal'); });
document.getElementById('chartModal').addEventListener('click', e => { if (e.target.id === 'chartModal') closeModal('chartModal'); });
document.addEventListener('DOMContentLoaded', () => setTimeout(initMarketTab, 100));
</script>
</body>
</html>