// ──────────────────────────────────────────
// 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 = `
`;
}
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}
`;
}
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 `
${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 `
${t('step3Title')}
${t('step3Instruction')}
${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 `
`;
}).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')}
| ${t('colTeam')} |
${t('colModelScore')} |
${t('colModelJudge')} |
${t('colIntuition')} |
${t('colMatch')} |
${rows}
`;
}
// ──────────────────────────────────────────
// 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 `
`;
}).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);