Spaces:
Sleeping
Sleeping
| <html lang="ko"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <title>LoL ์น๋ฆฌ ์์ธก (10/15๋ถ ๋ถ๋ฆฌ UI)</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <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=Gowun+Dodum&family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet"> | |
| <style> | |
| body { font-family: "Noto Sans KR", system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 24px; background: radial-gradient(1200px 600px at 20% -10%, rgba(28,60,92,0.55), transparent 60%), radial-gradient(1000px 600px at 120% 10%, rgba(12,26,48,0.6), transparent 60%), #0a1428; color: #e5e7eb; min-height: 100vh; } | |
| .container { max-width: 1100px; margin: 0 auto; } | |
| h1 { margin: 0 0 8px; font-family: "Gowun Dodum", sans-serif; letter-spacing: 0.2px; color: #f3f4f6; font-size: 38px; } | |
| .version { color: #666; font-size: 15px; margin-bottom: 16px; } | |
| .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } | |
| fieldset { border: 1px solid rgba(201,164,92,0.25); border-radius: 16px; padding: 16px; background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.15)); box-shadow: inset 0 0 0 1px rgba(255,255,255,0.03), 0 8px 24px rgba(0,0,0,0.35); } | |
| legend { padding: 0 6px; font-weight: 600; color: #f0d9a0; font-family: "Gowun Dodum", sans-serif; letter-spacing: 0.2px; font-size: 20px; } | |
| .team-features-section { margin-top: 16px; margin-bottom: 16px; } | |
| .team-features-section:first-of-type { margin-top: 0; } | |
| .team-features-title { font-size: 17px; font-weight: 600; color: #f0d9a0; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid rgba(201,164,92,0.2); font-family: "Gowun Dodum", sans-serif; letter-spacing: 0.2px; } | |
| .team-features-section.blue-team .team-features-title { color: #4a9eff; } | |
| .team-features-section.red-team .team-features-title { color: #ff6b6b; } | |
| .row { display: grid; grid-template-columns: 1fr auto; gap: 8px; align-items: center; margin-bottom: 8px; } | |
| label { font-size: 16px; color: #cbd5e1; } | |
| input[type="number"] { width: 140px; max-width: 45vw; justify-self: end; padding: 8px 10px; border: 1px solid rgba(148,163,184,0.28); border-radius: 10px; background: rgba(255,255,255,0.03); color: #e2e8f0; box-shadow: none; appearance: textfield; -moz-appearance: textfield; } | |
| input[type="number"]:focus { outline: none; border-color: rgba(201,164,92,0.32); box-shadow: none; } | |
| input[type="number"]::-webkit-outer-spin-button, | |
| input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } | |
| .checkbox-row { display: flex; gap: 16px; flex-wrap: wrap; margin: 8px 0 4px; } | |
| .actions { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 16px; } | |
| button { padding: 12px 18px; border: 0; border-radius: 10px; cursor: pointer; transition: transform .08s ease, box-shadow .2s ease; font-weight: 600; font-size: 16px; } | |
| .primary { background: linear-gradient(180deg, #1e2a44, #0f1b33); color: #f6f4eb; border: 1px solid rgba(201,164,92,0.28); box-shadow: 0 4px 14px rgba(0,0,0,0.35); } | |
| .primary:hover { transform: translateY(-1px); box-shadow: 0 8px 18px rgba(0,0,0,0.45); } | |
| .secondary { background: rgba(255,255,255,0.06); color: #e2e8f0; border: 1px solid rgba(148,163,184,0.28); } | |
| .muted { color: #94a3b8; font-size: 14px; } | |
| .card { border: 1px solid rgba(201,164,92,0.25); border-radius: 16px; padding: 16px; margin-top: 16px; background: rgba(255,255,255,0.02); box-shadow: inset 0 0 0 1px rgba(255,255,255,0.03), 0 8px 24px rgba(0,0,0,0.35); } | |
| table { width: 100%; border-collapse: collapse; } | |
| th, td { text-align: left; padding: 12px; border-bottom: 1px dashed rgba(148,163,184,0.25); color: #e5e7eb; font-size: 15px; } | |
| .error { color: #fecaca; margin-top: 8px; white-space: pre-wrap; } | |
| .ok { color: #a7f3d0; } | |
| .spinner { display: none; } | |
| .cards { display: grid; grid-template-columns: 1fr; gap: 12px; } | |
| @media (min-width: 900px) { .cards { grid-template-columns: 1fr 1fr 1fr; } } | |
| .team-selection { margin: 20px 0; } | |
| .team-container { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 30px; | |
| flex-wrap: wrap; | |
| } | |
| .team-section { | |
| text-align: center; | |
| min-width: 200px; | |
| } | |
| .team-section h3 { margin: 0 0 12px; font-size: 20px; color: #f0d9a0; font-family: "Gowun Dodum", sans-serif; letter-spacing: 0.2px; } | |
| .team-section.blue-team h3 { color: #4a9eff; } | |
| .team-section.red-team h3 { color: #ff6b6b; } | |
| .team-selector { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .team-select { padding: 8px 12px; border: 1px solid rgba(148,163,184,0.35); border-radius: 10px; font-size: 14px; min-width: 150px; background: rgba(14,29,54,0.9); color: #e2e8f0; } | |
| .team-select:focus { outline: none; border-color: rgba(201,164,92,0.32); } | |
| .team-select option { color: #e2e8f0; background: rgba(10,20,40,0.98); } | |
| .team-logo { width: 96px; height: 96px; border: 1px solid rgba(201,164,92,0.35); border-radius: 14px; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.03); overflow: hidden; box-shadow: 0 8px 22px rgba(0,0,0,0.45), inset 0 0 0 1px rgba(255,255,255,0.05); } | |
| .team-logo img { | |
| max-width: 100%; | |
| max-height: 100%; | |
| object-fit: contain; | |
| } | |
| .vs-text { font-size: 32px; font-weight: 700; color: #f0d9a0; margin: 0 20px; font-family: "Gowun Dodum", sans-serif; text-shadow: 0 0 12px rgba(201,164,92,0.25); letter-spacing: 0.2px; } | |
| @media (max-width: 900px) { | |
| .grid { grid-template-columns: 1fr; } | |
| .row { grid-template-columns: 1fr; } | |
| input[type="number"] { width: 100%; max-width: 100%; justify-self: stretch; box-sizing: border-box; } | |
| } | |
| .meters { display: grid; grid-template-columns: 1fr; gap: 12px; margin-top: 8px; } | |
| .meter { background: rgba(255,255,255,0.04); border: 1px solid rgba(201,164,92,0.25); border-radius: 14px; padding: 10px 12px; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.03); } | |
| .meter-label { display: flex; justify-content: space-between; align-items: center; font-size: 15px; color: #e5e7eb; margin-bottom: 8px; } | |
| .meter-bar { position: relative; height: 14px; background: rgba(14,29,54,0.8); border-radius: 999px; overflow: hidden; border: 1px solid rgba(148,163,184,0.25); } | |
| .meter-fill { position: absolute; top: 0; left: 0; height: 100%; width: 50%; background: linear-gradient(90deg, #1b7ecb, #3ec6ff); box-shadow: 0 0 18px rgba(62,198,255,0.45); transition: width .4s ease; } | |
| .meter-fill.gold { background: linear-gradient(90deg, #d1b06b, #c9a45c); box-shadow: 0 0 18px rgba(201,164,92,0.45); } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>LoL ์น๋ฆฌ ์์ธก</h1> | |
| <div id="version" class="version"></div> | |
| <div class="team-selection"> | |
| <div class="team-container"> | |
| <div class="team-section blue-team"> | |
| <h3>๋ธ๋ฃจ ํ</h3> | |
| <div class="team-selector"> | |
| <select id="leftTeam" class="team-select"> | |
| <option value="">ํ์ ์ ํํ์ธ์</option> | |
| <option value="AL">AL</option> | |
| <option value="BLG">BLG</option> | |
| <option value="CFO">CFO</option> | |
| <option value="FLY">FLY</option> | |
| <option value="G2">G2</option> | |
| <option value="KT">KT</option> | |
| <option value="PSG">PSG</option> | |
| <option value="T1">T1</option> | |
| <option value="TES">TES</option> | |
| <option value="GEN.G">GEN.G</option> | |
| <option value="HLE">HLE</option> | |
| </select> | |
| <div class="team-logo" id="leftTeamLogo"></div> | |
| </div> | |
| </div> | |
| <div class="vs-text">VS</div> | |
| <div class="team-section red-team"> | |
| <h3>๋ ๋ ํ</h3> | |
| <div class="team-selector"> | |
| <select id="rightTeam" class="team-select"> | |
| <option value="">ํ์ ์ ํํ์ธ์</option> | |
| <option value="AL">AL</option> | |
| <option value="BLG">BLG</option> | |
| <option value="CFO">CFO</option> | |
| <option value="FLY">FLY</option> | |
| <option value="G2">G2</option> | |
| <option value="KT">KT</option> | |
| <option value="PSG">PSG</option> | |
| <option value="T1">T1</option> | |
| <option value="TES">TES</option> | |
| <option value="GEN.G">GEN.G</option> | |
| <option value="HLE">HLE</option> | |
| </select> | |
| <div class="team-logo" id="rightTeamLogo"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid"> | |
| <fieldset> | |
| <legend>10๋ถ ์ํฉ (๋ธ๋ฃจ ํ ๊ธฐ์ค)</legend> | |
| <div><strong>์ค๋ธ์ ํธ ์ํฉ</strong></div> | |
| <div id="obj10" class="checkbox-row"></div> | |
| <div class="muted">โป ์๋์ ๋งค์น 10๋ถ ์์ ์ ์ ๋ ฅ</div> | |
| <div class="team-features-section blue-team"> | |
| <div class="team-features-title">๋ธ๋ฃจ ํ</div> | |
| <div id="feat10-blue"></div> | |
| </div> | |
| <div class="team-features-section red-team"> | |
| <div class="team-features-title">๋ ๋ ํ</div> | |
| <div id="feat10-red"></div> | |
| </div> | |
| <div class="muted" style="margin-top: 8px;"><strong>์ถ๊ฐ ํผ์ณ:</strong></div> | |
| <div id="calc10" class="muted" style="font-size: 12px; line-height: 1.4;"></div> | |
| </fieldset> | |
| <fieldset> | |
| <legend>15๋ถ ์ํฉ (๋ธ๋ฃจ ํ ๊ธฐ์ค)</legend> | |
| <div><strong>์ค๋ธ์ ํธ ์ํฉ</strong></div> | |
| <div id="obj15" class="checkbox-row"></div> | |
| <div class="muted">โป ์๋์ ๋งค์น 15๋ถ ์์ ์ ์ ๋ ฅ</div> | |
| <div class="team-features-section blue-team"> | |
| <div class="team-features-title">๋ธ๋ฃจ ํ</div> | |
| <div id="feat15-blue"></div> | |
| </div> | |
| <div class="team-features-section red-team"> | |
| <div class="team-features-title">๋ ๋ ํ</div> | |
| <div id="feat15-red"></div> | |
| </div> | |
| <div class="muted" style="margin-top: 8px;"><strong>์ถ๊ฐ ํผ์ณ:</strong></div> | |
| <div id="calc15" class="muted" style="font-size: 12px; line-height: 1.4;"></div> | |
| </fieldset> | |
| </div> | |
| <div class="actions"> | |
| <button id="btnPredict10" class="primary">10๋ถ๋ง ์์ธก</button> | |
| <button id="btnPredict15" class="primary">15๋ถ / (๋ ๋ค ์ ๋ ฅ ์) ํตํฉ</button> | |
| <button id="btnReset" class="secondary">์ด๊ธฐํ</button> | |
| <div id="busy" class="muted spinner">์์ธก ์ค...</div> | |
| </div> | |
| <div class="meters"> | |
| <div class="meter" id="meter10" style="display:none;"> | |
| <div class="meter-label"><span>10๋ถ ๋ฉํ ์น๋ฅ </span><span id="meter10Val">-</span></div> | |
| <div class="meter-bar"><div class="meter-fill" id="meter10Fill"></div></div> | |
| </div> | |
| <div class="meter" id="meter15" style="display:none;"> | |
| <div class="meter-label"><span>15๋ถ ๋ฉํ ์น๋ฅ </span><span id="meter15Val">-</span></div> | |
| <div class="meter-bar"><div class="meter-fill" id="meter15Fill"></div></div> | |
| </div> | |
| <div class="meter" id="meterAll" style="display:none;"> | |
| <div class="meter-label"><span>ํตํฉ ๋ฉํ ์น๋ฅ </span><span id="meterAllVal">-</span></div> | |
| <div class="meter-bar"><div class="meter-fill gold" id="meterAllFill"></div></div> | |
| </div> | |
| </div> | |
| <div id="err" class="error"></div> | |
| <div class="cards"> | |
| <div class="card"> | |
| <h3>10๋ถ ๊ฒฐ๊ณผ</h3> | |
| <table><tbody id="res10"></tbody></table> | |
| </div> | |
| <div class="card"> | |
| <h3>15๋ถ ๊ฒฐ๊ณผ</h3> | |
| <table><tbody id="res15"></tbody></table> | |
| </div> | |
| <div class="card"> | |
| <h3>๋ฉํ(ํตํฉ) ๊ฒฐ๊ณผ</h3> | |
| <div id="resMeta" class="muted">๋ ์์ (10ยท15)์ ๋ชจ๋ ์ ๋ ฅํ๋ฉด ํ์๋ฉ๋๋ค.</div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const MODEL_INFO_URL = '/model-info'; | |
| const PREDICT_URL = '/predict'; | |
| const DEFAULT_ZERO = 0; | |
| function makeNumberRow(id, label) { | |
| const row = document.createElement('div'); | |
| row.className = 'row'; | |
| const lab = document.createElement('label'); | |
| lab.setAttribute('for', id); | |
| lab.textContent = label; | |
| const inp = document.createElement('input'); | |
| inp.type = 'number'; | |
| inp.step = 'any'; | |
| inp.id = id; | |
| inp.value = DEFAULT_ZERO; | |
| inp.addEventListener('input', () => { | |
| updateCalculatedValues(inp.id.includes('f10__') ? 10 : 15); | |
| }); | |
| row.appendChild(lab); row.appendChild(inp); | |
| return row; | |
| } | |
| function makeCheckbox(id, label) { | |
| const wrap = document.createElement('label'); | |
| const cb = document.createElement('input'); | |
| cb.type = 'checkbox'; cb.id = id; cb.value = 1; | |
| wrap.appendChild(cb); wrap.append(' ' + label); | |
| return wrap; | |
| } | |
| let feat10List = [], feat15List = [], objList = [], version = 'n/a'; | |
| const featureNameMap = { | |
| 'goldat10': '๋ธ๋ฃจํ ๊ณจ๋', | |
| 'csat10': '๋ธ๋ฃจํCS', | |
| 'killsat10': '๋ธ๋ฃจํ ํฌ', | |
| 'assistsat10': '๋ธ๋ฃจํ ์ด์์คํธ', | |
| 'deathsat10': '๋ธ๋ฃจํ ๋ฐ์ค', | |
| 'opp_goldat10': '๋ ๋ํ ๊ณจ๋', | |
| 'opp_csat10': '๋ ๋ํ CS', | |
| 'opp_killsat10': '๋ ๋ํ ํฌ', | |
| 'opp_assistsat10': '๋ ๋ํ ์ด์์คํธ', | |
| 'opp_deathsat10': '๋ ๋ํ ๋ฐ์ค', | |
| 'goldat15': '๋ธ๋ฃจํ ๊ณจ๋', | |
| 'csat15': '๋ธ๋ฃจํCS', | |
| 'killsat15': '๋ธ๋ฃจํ ํฌ', | |
| 'assistsat15': '๋ธ๋ฃจํ ์ด์์คํธ', | |
| 'deathsat15': '๋ธ๋ฃจํ ๋ฐ์ค', | |
| 'opp_goldat15': '๋ ๋ํ ๊ณจ๋', | |
| 'opp_csat15': '๋ ๋ํ CS', | |
| 'opp_killsat15': '๋ ๋ํ ํฌ', | |
| 'opp_assistsat15': '๋ ๋ํ ์ด์์คํธ', | |
| 'opp_deathsat15': '๋ ๋ํ ๋ฐ์ค', | |
| //์ค๋ธ์ ํธ | |
| 'firstdragon': '์ฒซ ๋๋๊ณค', | |
| 'firstherald': '์ฒซ ์ ๋ น', | |
| 'firsttower': '์ฒซ ํ์', | |
| 'firstblood': 'ํผ์คํธ ๋ธ๋ฌ๋', | |
| 'firstmidtower': '์ฒซ ๋ฏธ๋ ํ์' | |
| }; | |
| function getKoreanName(featureName) { | |
| return featureNameMap[featureName] || featureName; | |
| } | |
| async function loadModelInfo() { | |
| const r = await fetch(MODEL_INFO_URL); | |
| const info = await r.json(); | |
| version = info.version || 'n/a'; | |
| document.getElementById('version').textContent = `๋ชจ๋ธ ๋ฒ์ : ${version}`; | |
| const all10 = info.required_features_at10 || []; | |
| const all15 = info.required_features_at15 || []; | |
| objList = info.objective_features || []; | |
| const obj10 = document.getElementById('obj10'); | |
| const obj15 = document.getElementById('obj15'); | |
| objList.forEach(name => { | |
| obj10.appendChild(makeCheckbox(`obj10__${name}`, getKoreanName(name))); | |
| obj15.appendChild(makeCheckbox(`obj15__${name}`, getKoreanName(name))); | |
| }); | |
| const autoCalcFeatures = [ | |
| 'golddiffat10', 'csdiffat10', | |
| 'golddiffat15', 'csdiffat15' | |
| ]; | |
| feat10List = all10.filter(c => !objList.includes(c) && !autoCalcFeatures.includes(c)); | |
| feat15List = all15.filter(c => !objList.includes(c) && !autoCalcFeatures.includes(c)); | |
| const feat10Blue = feat10List.filter(name => !name.startsWith('opp_')); | |
| const feat10Red = feat10List.filter(name => name.startsWith('opp_')); | |
| const feat15Blue = feat15List.filter(name => !name.startsWith('opp_')); | |
| const feat15Red = feat15List.filter(name => name.startsWith('opp_')); | |
| const f10Blue = document.getElementById('feat10-blue'); | |
| const f15Blue = document.getElementById('feat15-blue'); | |
| feat10Blue.forEach(name => f10Blue.appendChild(makeNumberRow(`f10__${name}`, getKoreanName(name)))); | |
| feat15Blue.forEach(name => f15Blue.appendChild(makeNumberRow(`f15__${name}`, getKoreanName(name)))); | |
| const f10Red = document.getElementById('feat10-red'); | |
| const f15Red = document.getElementById('feat15-red'); | |
| feat10Red.forEach(name => f10Red.appendChild(makeNumberRow(`f10__${name}`, getKoreanName(name)))); | |
| feat15Red.forEach(name => f15Red.appendChild(makeNumberRow(`f15__${name}`, getKoreanName(name)))); | |
| updateCalculatedValues(10); | |
| updateCalculatedValues(15); | |
| } | |
| function calculateDerivedFeatures(baseFeatures, which) { | |
| const derived = {}; | |
| if (baseFeatures[`goldat${which}`] !== undefined && baseFeatures[`opp_goldat${which}`] !== undefined) { | |
| derived[`golddiffat${which}`] = baseFeatures[`goldat${which}`] - baseFeatures[`opp_goldat${which}`]; | |
| } | |
| if (baseFeatures[`csat${which}`] !== undefined && baseFeatures[`opp_csat${which}`] !== undefined) { | |
| derived[`csdiffat${which}`] = baseFeatures[`csat${which}`] - baseFeatures[`opp_csat${which}`]; | |
| } | |
| return derived; | |
| } | |
| function updateCalculatedValues(which) { | |
| const calcDiv = document.getElementById(`calc${which}`); | |
| if (!calcDiv) return; | |
| const baseFeatures = {}; | |
| const featList = which === 10 ? feat10List : feat15List; | |
| featList.forEach(name => { | |
| const id = which === 10 ? `f10__${name}` : `f15__${name}`; | |
| const el = document.getElementById(id); | |
| const val = el && el.value !== '' ? Number(el.value) : 0; | |
| baseFeatures[name] = isNaN(val) ? 0 : val; | |
| }); | |
| const derived = calculateDerivedFeatures(baseFeatures, which); | |
| const lines = []; | |
| if (derived[`golddiffat${which}`] !== undefined) { | |
| lines.push(`๊ณจ๋์ฐจ์ด: ${derived[`golddiffat${which}`].toFixed(0)}`); | |
| } | |
| if (derived[`csdiffat${which}`] !== undefined) { | |
| lines.push(`CS์ฐจ์ด: ${derived[`csdiffat${which}`].toFixed(0)}`); | |
| } | |
| calcDiv.textContent = lines.length > 0 ? lines.join(' | ') : '๊ฐ์ ์ ๋ ฅํ๋ฉด ์๋ ๊ณ์ฐ๋ฉ๋๋ค'; | |
| } | |
| function gatherFeaturesAt(which, featList) { | |
| const obj = {}; | |
| objList.forEach(name => { | |
| const id = which === 10 ? `obj10__${name}` : `obj15__${name}`; | |
| const el = document.getElementById(id); | |
| obj[name] = el && el.checked ? 1 : 0; | |
| }); | |
| featList.forEach(name => { | |
| const id = which === 10 ? `f10__${name}` : `f15__${name}`; | |
| const el = document.getElementById(id); | |
| const val = el && el.value !== '' ? Number(el.value) : 0; | |
| obj[name] = isNaN(val) ? 0 : val; | |
| }); | |
| const derived = calculateDerivedFeatures(obj, which); | |
| Object.assign(obj, derived); | |
| return obj; | |
| } | |
| function clearResults() { | |
| document.getElementById('res10').innerHTML = ''; | |
| document.getElementById('res15').innerHTML = ''; | |
| document.getElementById('resMeta').innerHTML = '๋ ์์ (10ยท15)์ ๋ชจ๋ ์ ๋ ฅํ๋ฉด ํ์๋ฉ๋๋ค.'; | |
| document.getElementById('resMeta').className = 'muted'; | |
| document.getElementById('err').textContent = ''; | |
| setMeter('10', null); | |
| setMeter('15', null); | |
| setMeter('All', null); | |
| } | |
| function clearSpecificResult(which) { | |
| if (which === 10) { | |
| document.getElementById('res10').innerHTML = ''; | |
| } else if (which === 15) { | |
| document.getElementById('res15').innerHTML = ''; | |
| } | |
| } | |
| function addRow(tbodyId, label, value) { | |
| const tb = document.getElementById(tbodyId); | |
| const tr = document.createElement('tr'); | |
| const td1 = document.createElement('td'); td1.textContent = label; | |
| const td2 = document.createElement('td'); td2.textContent = value; | |
| tr.appendChild(td1); tr.appendChild(td2); | |
| tb.appendChild(tr); | |
| } | |
| function setMeter(prefix, prob) { | |
| const meter = document.getElementById(`meter${prefix}`); | |
| const valEl = document.getElementById(`meter${prefix}Val`); | |
| const fillEl = document.getElementById(`meter${prefix}Fill`); | |
| if (!meter || !valEl || !fillEl) return; | |
| if (prob == null || isNaN(prob)) { | |
| meter.style.display = 'none'; | |
| return; | |
| } | |
| const pct = Math.max(0, Math.min(1, Number(prob))); | |
| meter.style.display = ''; | |
| valEl.textContent = `${(pct * 100).toFixed(1)}%`; | |
| fillEl.style.width = `${pct * 100}%`; | |
| } | |
| function renderResult(json, which = null) { | |
| const r0 = (json.results && json.results[0]) ? json.results[0] : {}; | |
| if (which === 10 || which === null) { | |
| if (r0.p10) { | |
| clearSpecificResult(10); | |
| addRow('res10', '10๋ถ RF', r0.p10.rf.toFixed(3)); | |
| addRow('res10', '10๋ถ XGB', r0.p10.xgb.toFixed(3)); | |
| addRow('res10', '10๋ถ LR', r0.p10.lr.toFixed(3)); | |
| } | |
| if (r0.meta_prob_10 != null) { | |
| addRow('res10', '10๋ถ ๋ฉํ', r0.meta_prob_10.toFixed(3)); | |
| setMeter('10', r0.meta_prob_10); | |
| } | |
| } | |
| if (which === 15 || which === null) { | |
| if (r0.p15) { | |
| clearSpecificResult(15); | |
| addRow('res15', '15๋ถ RF', r0.p15.rf.toFixed(3)); | |
| addRow('res15', '15๋ถ XGB', r0.p15.xgb.toFixed(3)); | |
| addRow('res15', '15๋ถ LR', r0.p15.lr.toFixed(3)); | |
| } | |
| if (r0.meta_prob_15 != null) { | |
| addRow('res15', '15๋ถ ๋ฉํ', r0.meta_prob_15.toFixed(3)); | |
| setMeter('15', r0.meta_prob_15); | |
| } | |
| } | |
| const hasBoth = !!(r0.p10 && r0.p15); | |
| if (hasBoth && r0.meta_prob_all != null) { | |
| const parts = []; | |
| parts.push(`ํตํฉ ๋ฉํ ํ๋ฅ : <strong>${r0.meta_prob_all.toFixed(3)}</strong>`); | |
| const metaDiv = document.getElementById('resMeta'); | |
| metaDiv.innerHTML = `${parts.join(' ยท ')}`; | |
| metaDiv.className = 'ok'; | |
| setMeter('All', r0.meta_prob_all); | |
| } | |
| } | |
| async function callPredict(payload) { | |
| const r = await fetch(PREDICT_URL, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload), | |
| }); | |
| if (!r.ok) throw new Error(`HTTP ${r.status}\n${await r.text()}`); | |
| return r.json(); | |
| } | |
| async function doPredict10() { | |
| document.getElementById('busy').style.display = 'inline'; | |
| document.getElementById('err').textContent = ''; | |
| try { | |
| const features_at10 = gatherFeaturesAt(10, feat10List); | |
| console.log('10๋ถ ์์ธก ์ ์ก ๋ฐ์ดํฐ:', features_at10); | |
| const payload = { sample: { features_at10 }, return_reversal: false }; | |
| const json = await callPredict(payload); | |
| renderResult(json, 10); | |
| } catch (e) { | |
| document.getElementById('err').textContent = e.message; | |
| } finally { | |
| document.getElementById('busy').style.display = 'none'; | |
| } | |
| } | |
| async function doPredict15() { | |
| document.getElementById('busy').style.display = 'inline'; | |
| document.getElementById('err').textContent = ''; | |
| try { | |
| const features_at10 = gatherFeaturesAt(10, feat10List); | |
| const features_at15 = gatherFeaturesAt(15, feat15List); | |
| const has10 = Object.values(features_at10).some(v => v !== 0); | |
| const has15 = Object.values(features_at15).some(v => v !== 0); | |
| if (!has10 && !has15) throw new Error('์ต์ํ 15๋ถ ๊ฐ์ ์ ๋ ฅํ์ธ์.'); | |
| const payload = { sample: {}, return_reversal: true }; | |
| if (has10) { | |
| payload.sample.features_at10 = features_at10; | |
| console.log('10๋ถ ๋ฐ์ดํฐ:', features_at10); | |
| } | |
| if (has15) { | |
| payload.sample.features_at15 = features_at15; | |
| console.log('15๋ถ ๋ฐ์ดํฐ:', features_at15); | |
| } | |
| const json = await callPredict(payload); | |
| if (has10 && has15) { | |
| renderResult(json, null); // ํตํฉ ์์ธก | |
| } else if (has15) { | |
| renderResult(json, 15); // 15๋ถ๋ง ์์ธก | |
| } else if (has10) { | |
| renderResult(json, 10); // 10๋ถ๋ง ์์ธก | |
| } | |
| } catch (e) { | |
| document.getElementById('err').textContent = e.message; | |
| } finally { | |
| document.getElementById('busy').style.display = 'none'; | |
| } | |
| } | |
| function doReset() { | |
| document.getElementById('err').textContent = ''; | |
| clearResults(); | |
| } | |
| function updateTeamLogo(teamSelectId, logoDivId) { | |
| const select = document.getElementById(teamSelectId); | |
| const logoDiv = document.getElementById(logoDivId); | |
| if (select.value) { | |
| const img = document.createElement('img'); | |
| img.src = `/static/image/${select.value}.png`; | |
| img.alt = select.value; | |
| img.onerror = function() { | |
| logoDiv.innerHTML = `<div style="color: #666; font-size: 12px;">${select.value}</div>`; | |
| }; | |
| logoDiv.innerHTML = ''; | |
| logoDiv.appendChild(img); | |
| } else { | |
| logoDiv.innerHTML = ''; | |
| } | |
| } | |
| function setupTeamSelectors() { | |
| const leftTeamSelect = document.getElementById('leftTeam'); | |
| const rightTeamSelect = document.getElementById('rightTeam'); | |
| leftTeamSelect.addEventListener('change', function() { | |
| updateTeamLogo('leftTeam', 'leftTeamLogo'); | |
| }); | |
| rightTeamSelect.addEventListener('change', function() { | |
| updateTeamLogo('rightTeam', 'rightTeamLogo'); | |
| }); | |
| } | |
| window.addEventListener('DOMContentLoaded', async () => { | |
| await loadModelInfo(); | |
| setupTeamSelectors(); | |
| document.querySelectorAll('input[type="number"]').forEach(inp => { | |
| inp.addEventListener('wheel', (e) => { e.preventDefault(); }, { passive: false }); | |
| }); | |
| document.getElementById('btnPredict10').addEventListener('click', doPredict10); | |
| document.getElementById('btnPredict15').addEventListener('click', doPredict15); | |
| document.getElementById('btnReset').addEventListener('click', doReset); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |