// ────────────────────────────────────────── // State // ────────────────────────────────────────── const state = { step: 1, started: false, // Step 1 meta: { country: '', age: '', gender: '', timestamp: null }, // Step 2 shuffledCases: [], caseIndex: 0, currentJudgment: null, // 1-5 Likert caseStartTime: null, intuitive_judgments: [], // Step 3 pairIndex: 0, sliderValues: Array(10).fill(0), pairwiseRaw: null, ahpResult: null, crWarningShown: false, crWarningPending: false, // Step 4 / 5 comparison: [], post_survey: { q1: null, q2: null, q3: null, q4: null, open_response: '' }, submitted: false, _payload: null, }; // ────────────────────────────────────────── // Render entry point // ────────────────────────────────────────── function renderApp() { renderHeader(); renderMain(); } function renderHeader() { const el = document.getElementById('app-header'); if (!el) return; el.innerHTML = `
${t('appTitle')}
`; } function renderMain() { const el = document.getElementById('app-main'); if (!el) return; switch (state.step) { case 1: el.innerHTML = renderStep1(); bindStep1(); break; case 2: el.innerHTML = renderStep2(); bindStep2(); break; case 3: el.innerHTML = renderStep3(); bindStep3(); break; case 4: el.innerHTML = renderStep4(); break; case 5: el.innerHTML = renderStep5(); bindStep5(); break; case 'complete': el.innerHTML = renderComplete(); bindComplete(); break; } } // ────────────────────────────────────────── // STEP 1 // ────────────────────────────────────────── const COUNTRIES = [ 'Afghanistan','Albania','Algeria','Angola','Argentina','Armenia','Australia','Austria', 'Azerbaijan','Bangladesh','Belarus','Belgium','Bolivia','Bosnia and Herzegovina','Brazil', 'Bulgaria','Cambodia','Cameroon','Canada','Chile','China','Colombia','Congo','Costa Rica', 'Croatia','Cuba','Czech Republic','Denmark','Dominican Republic','Ecuador','Egypt', 'El Salvador','Ethiopia','Finland','France','Georgia','Germany','Ghana','Greece', 'Guatemala','Haiti','Honduras','Hungary','India','Indonesia','Iran','Iraq','Ireland', 'Israel','Italy','Jamaica','Japan','Jordan','Kazakhstan','Kenya','Kuwait','Kyrgyzstan', 'Laos','Lebanon','Libya','Lithuania','Malaysia','Mexico','Moldova','Mongolia','Morocco', 'Mozambique','Myanmar','Nepal','Netherlands','New Zealand','Nicaragua','Nigeria','Norway', 'Pakistan','Palestine','Panama','Paraguay','Peru','Philippines','Poland','Portugal', 'Romania','Russia','Rwanda','Saudi Arabia','Senegal','Serbia','Singapore','Slovakia', 'Somalia','South Africa','South Korea','Spain','Sri Lanka','Sudan','Sweden','Switzerland', 'Syria','Taiwan','Tajikistan','Tanzania','Thailand','Tunisia','Turkey','Turkmenistan', 'Uganda','Ukraine','United Arab Emirates','United Kingdom','United States','Uruguay', 'Uzbekistan','Venezuela','Vietnam','Yemen','Zambia','Zimbabwe','Other', ]; function renderStep1() { const { meta } = state; const notices = t('step1Notices').map(n => `

${n}

`).join(''); const countryOpts = COUNTRIES.map(c => `` ).join(''); const genderOpts = t('step1Q3opts').map(o => ` `).join(''); return `

${t('step1Title')}

${t('step1Intro')}

${notices}
${genderOpts}
`; } function bindStep1() { document.getElementById('sel-country').addEventListener('change', e => { state.meta.country = e.target.value; }); document.getElementById('inp-age').addEventListener('input', e => { state.meta.age = e.target.value; }); document.getElementById('rg-gender').addEventListener('click', e => { const lbl = e.target.closest('label'); if (!lbl) return; const val = lbl.querySelector('input').value; state.meta.gender = val; document.querySelectorAll('#rg-gender label').forEach(l => l.classList.toggle('selected', l.querySelector('input').value === val)); }); document.getElementById('btn-start').addEventListener('click', () => { if (!state.meta.country || !state.meta.age || !state.meta.gender) { alert(t('alertFillAll')); return; } state.meta.timestamp = Date.now(); state.shuffledCases = shuffleCases(); state.caseIndex = 0; state.started = true; enableLeaveWarning(); goStep(2); }); } // ────────────────────────────────────────── // STEP 2 // ────────────────────────────────────────── function renderStep2() { const total = state.shuffledCases.length; const idx = state.caseIndex; const pct = Math.round((idx / total) * 100); const c = state.shuffledCases[idx]; const varNames = t('varNames'); const varRows = VARS.map(key => { const score = c[key]; const level = getScoreLevel(score); return `
${varNames[key]}
${score}
`; }).join(''); const scale = t('judgmentScale'); const likertBtns = scale.map((label, i) => { const val = i + 1; const active = state.currentJudgment === val; return ` `; }).join(''); return `
${idx + 1} / ${total}
Team ${c.teamLetter}
${varRows}

${t('judgmentLabel')}

${likertBtns}

${t('step2Instruction')}

`; } function bindStep2() { const caseStart = Date.now(); document.getElementById('jl-row').addEventListener('click', e => { const btn = e.target.closest('.judgment-likert-btn'); if (!btn) return; const val = parseInt(btn.dataset.val); state.currentJudgment = val; document.querySelectorAll('.judgment-likert-btn').forEach(b => b.classList.toggle('active', parseInt(b.dataset.val) === val)); document.getElementById('btn-case-next').disabled = false; }); document.getElementById('btn-case-next').addEventListener('click', () => { if (state.currentJudgment === null) { alert(t('alertSelectJudgment')); return; } const elapsed = Date.now() - caseStart; const c = state.shuffledCases[state.caseIndex]; state.intuitive_judgments.push({ case_id: c.id, presented_order: state.caseIndex + 1, judgment: state.currentJudgment, // 1-5 response_time_ms: elapsed, }); state.caseIndex++; state.currentJudgment = null; if (state.caseIndex >= state.shuffledCases.length) { goStep(3); } else { renderMain(); window.scrollTo({ top: 0, behavior: 'smooth' }); } }); } // ────────────────────────────────────────── // STEP 3 // ────────────────────────────────────────── const AHP_PAIR_LABELS_KO = [ ['안정성', '수익 가능성'], ['안정성', '자금 효율성'], ['안정성', '사업 계획 완성도'], ['안정성', '사회적 가치'], ['수익 가능성', '자금 효율성'], ['수익 가능성', '사업 계획 완성도'], ['수익 가능성', '사회적 가치'], ['자금 효율성', '사업 계획 완성도'], ['자금 효율성', '사회적 가치'], ['사업 계획 완성도', '사회적 가치'], ]; const AHP_PAIR_LABELS_EN = [ ['Stability', 'Revenue Potential'], ['Stability', 'Capital Efficiency'], ['Stability', 'Business Plan Quality'], ['Stability', 'Social Value'], ['Revenue Potential', 'Capital Efficiency'], ['Revenue Potential', 'Business Plan Quality'], ['Revenue Potential', 'Social Value'], ['Capital Efficiency', 'Business Plan Quality'], ['Capital Efficiency', 'Social Value'], ['Business Plan Quality', 'Social Value'], ]; function getPairLabels(idx) { return currentLang === 'en' ? AHP_PAIR_LABELS_EN[idx] : AHP_PAIR_LABELS_KO[idx]; } function getAHPDescription(sliderPos, leftLabel, rightLabel) { if (sliderPos === 0) return t('ahpEqual'); const ahpVal = Math.abs(sliderToStored(sliderPos)); // 3,5,7,9 const level = t('ahpLevels')[ahpVal] || String(ahpVal); if (sliderPos < 0) return t('ahpLeftMore', leftLabel, level); return t('ahpRightMore', rightLabel, level); } function renderStep3() { const idx = state.pairIndex; const total = AHP_PAIRS.length; const pct = Math.round((idx / total) * 100); const [left, right] = getPairLabels(idx); const sliderVal = state.sliderValues[idx]; const desc = getAHPDescription(sliderVal, left, right); const leftActive = sliderVal < 0 ? 'crit-active-left' : ''; const rightActive = sliderVal > 0 ? 'crit-active-right' : ''; return `
${idx + 1} / ${total}

${t('step3Title')}

${t('step3Instruction')}

${left}
VS
${right}
9753 1 3579
${desc}
${state.crWarningPending ? `

${t('crWarning')}

` : `
`}
`; } function bindStep3() { const idx = state.pairIndex; const [left, right] = getPairLabels(idx); const slider = document.getElementById('ahp-slider'); if (slider) { slider.addEventListener('input', e => { const val = parseInt(e.target.value); state.sliderValues[idx] = val; document.getElementById('ahp-desc').textContent = getAHPDescription(val, left, right); document.getElementById('crit-left').className = `crit crit-left ${val < 0 ? 'crit-active-left' : ''}`; document.getElementById('crit-right').className = `crit crit-right ${val > 0 ? 'crit-active-right' : ''}`; }); } const nextBtn = document.getElementById('btn-pair-next'); if (nextBtn) nextBtn.addEventListener('click', advancePair); const redoBtn = document.getElementById('btn-redo'); if (redoBtn) redoBtn.addEventListener('click', () => { state.crWarningPending = false; state.pairIndex = 0; state.sliderValues = Array(10).fill(0); renderMain(); }); const contBtn = document.getElementById('btn-cr-continue'); if (contBtn) contBtn.addEventListener('click', () => { state.crWarningPending = false; finalizeAHP(); goStep(4); }); } function advancePair() { const total = AHP_PAIRS.length; if (state.pairIndex < total - 1) { state.pairIndex++; renderMain(); window.scrollTo({ top: 0, behavior: 'smooth' }); } else { const raw = state.sliderValues.map(s => sliderToStored(s)); state.pairwiseRaw = raw; const result = runAHP(raw); state.ahpResult = result; if (result.CR > 0.10) { state.crWarningPending = true; state.crWarningShown = true; renderMain(); } else { finalizeAHP(); goStep(4); } } } // Convert Likert 1-5 to judgment category for comparison with model function likertToJudgment(likert) { if (likert >= 4) return 'select'; if (likert <= 2) return 'reject'; return 'neutral'; // 3 } function finalizeAHP() { const weights = state.ahpResult.weights; state.comparison = state.shuffledCases.map((c, i) => { const modelScore = calcModelScore(c, weights); const modelJudgment = modelScore >= 50 ? 'select' : 'reject'; const rawLikert = state.intuitive_judgments[i]?.judgment ?? null; const intuitiveCategory = rawLikert !== null ? likertToJudgment(rawLikert) : null; const match = intuitiveCategory === 'neutral' ? null : intuitiveCategory === modelJudgment ? true : false; return { case_id: c.id, team: c.teamLetter, type: c.type, model_score: Math.round(modelScore * 10) / 10, model_judgment: modelJudgment, intuitive_likert: rawLikert, intuitive_category: intuitiveCategory, match, }; }); } // ────────────────────────────────────────── // STEP 4 // ────────────────────────────────────────── function renderStep4() { const weights = state.ahpResult.weights; const varNames = t('varNames'); const weightRows = VARS.map((key, i) => { const pct = Math.round(weights[i] * 100); return `
${varNames[key]}
${pct}%
`; }).join(''); const comp = state.comparison; // Exclude neutral (match===null) from mismatch counts const decidable = comp.filter(c => c.match !== null); const totalMismatch = decidable.filter(c => !c.match).length; const baseline = decidable.filter(c => c.type === 'baseline'); const spike = decidable.filter(c => c.type === 'spike'); const conflict = decidable.filter(c => c.type === 'conflict'); const { CR } = state.ahpResult; const rows = comp.map(c => { const mis = c.match === false; const neu = c.match === null; const mLabel = c.model_judgment === 'select' ? `${t('select')}` : `${t('reject')}`; const iLabel = c.intuitive_category === 'select' ? `${c.intuitive_likert}` : c.intuitive_category === 'reject' ? `${c.intuitive_likert}` : `${c.intuitive_likert}`; const matchBadge = c.match === true ? `${t('colYes')}` : c.match === false ? `${t('colNo')}` : `${t('colNeutral')}`; return ` Team ${c.team} ${c.model_score} ${mLabel} ${iLabel} ${matchBadge} `; }).join(''); return `

${t('step4Title')}

${t('weightsTitle')}

${weightRows}

${t('statsTitle')}

${totalMismatch}/${decidable.length}
${t('statTotal')}
${baseline.filter(c=>!c.match).length}/${baseline.length}
${t('statBaseline')}
${spike.filter(c=>!c.match).length}/${spike.length}
${t('statSpike')}
${conflict.filter(c=>!c.match).length}/${conflict.length}
${t('statConflict')}
${CR.toFixed(3)}
${t('statCR')}

${t('totalMismatch', totalMismatch, decidable.length)}

${t('tableTitle')}

${rows}
${t('colTeam')} ${t('colModelScore')} ${t('colModelJudge')} ${t('colIntuition')} ${t('colMatch')}
`; } // ────────────────────────────────────────── // STEP 5 // ────────────────────────────────────────── function renderStep5() { const qs = t('likertQ'); const scale = t('likertScale'); const groups = qs.map((q, qi) => { const opts = scale.map((label, si) => { const val = si + 1; const checked = state.post_survey[`q${qi+1}`] === val; return ` `; }).join(''); return `

${qi+1}. ${q}

${opts}
`; }).join(''); return `

${t('step5Title')}

${t('step5Desc')}

${groups}
`; } function bindStep5() { document.querySelectorAll('.likert-options input[type="radio"]').forEach(inp => { inp.addEventListener('change', e => { state.post_survey[e.target.name] = parseInt(e.target.value); }); }); document.getElementById('open-resp').addEventListener('input', e => { state.post_survey.open_response = e.target.value; }); document.getElementById('btn-submit').addEventListener('click', () => { const { q1, q2, q3, q4 } = state.post_survey; if (!q1 || !q2 || !q3 || !q4) { alert(t('alertFillAll')); return; } submitData(); }); } // ────────────────────────────────────────── // Submit & Complete // ────────────────────────────────────────── function buildPayload() { return { meta: { timestamp: state.meta.timestamp, country: state.meta.country, age: state.meta.age, gender: state.meta.gender }, intuitive_judgments: state.intuitive_judgments, ahp: { pairwise_raw: state.pairwiseRaw, weights: state.ahpResult.weights, lambda_max: state.ahpResult.lambdaMax, CI: state.ahpResult.CI, CR: state.ahpResult.CR, cr_warning_shown: state.crWarningShown, }, comparison: state.comparison, post_survey: state.post_survey, }; } async function submitData() { if (state.submitted) return; state.submitted = true; disableLeaveWarning(); const payload = buildPayload(); try { await fetch('/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); } catch (e) { console.warn('Server submit failed:', e); } state._payload = payload; state.step = 'complete'; renderApp(); } function renderComplete() { return `
🎉

${t('completeTitle')}

${t('completeDesc')}

`; } function bindComplete() {} // ────────────────────────────────────────── // Navigation & utilities // ────────────────────────────────────────── function goStep(n) { state.step = n; renderApp(); window.scrollTo({ top: 0, behavior: 'smooth' }); } function enableLeaveWarning() { window._leaveHandler = e => { e.preventDefault(); e.returnValue = t('leaveWarning'); }; window.addEventListener('beforeunload', window._leaveHandler); } function disableLeaveWarning() { if (window._leaveHandler) { window.removeEventListener('beforeunload', window._leaveHandler); window._leaveHandler = null; } } function escHtml(str) { return String(str) .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } document.addEventListener('DOMContentLoaded', renderApp);