Tothemnoo's picture
Upload 71 files
97b5000 verified
Raw
History Blame Contribute Delete
26.7 kB
<!doctype html>
<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>