// ────────────────────────────────────────── // Text (한국어 전용) // ────────────────────────────────────────── const APP_TITLE = '스타트업 지원 심사 판단 실험'; const GRADE_OPTIONS = ['1학년', '2학년', '3학년']; const VAR_LABELS = { 안정성: '안정성', 수익_가능성: '수익 가능성', 자금_효율성: '자금 효율성', 사업_계획_완성도: '사업 계획 완성도', 사회적_가치: '사회적 가치', }; const JUDGMENT_SCALE = ['무조건\n탈락', '탈락', '보통', '선발', '무조건\n선발']; const AHP_LEVELS = { 3: '약간', 5: '꽤', 7: '매우', 9: '극히' }; const LIKERT_QUESTIONS = [ '모델의 판단 결과를 보고 놀라웠다.', '내 직관보다 모델의 판단이 더 합리적이라고 생각한다.', '이 선발 결정을 모델에게 맡겨도 괜찮다고 생각한다.', '이번 판단은 온전히 내 의지로 한 것이라고 느낀다.', ]; const LIKERT_SCALE = [ '전혀\n아니다', '아니다', '보통', '그렇다', '매우\n그렇다', ]; const ALERT_FILL_ALL = '모든 항목을 입력해 주세요.'; const ALERT_SELECT_JUDGMENT = '먼저 판단을 선택해 주세요.'; const LEAVE_WARNING = '실험이 완료되지 않았습니다. 나가시겠습니까?'; // ────────────────────────────────────────── // State // ────────────────────────────────────────── const state = { step: 1, started: false, // Step 1 meta: { grade: '', 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, // 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 = `
${APP_TITLE} 대구국제고 3학년 5반 이승준
`; } 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 // ────────────────────────────────────────── function renderStep1() { const { meta } = state; const gradeOpts = GRADE_OPTIONS.map( (o) => ` `, ).join(''); return `

스타트업 지원 심사 판단 실험

당신은 스타트업 지원 프로그램의 심사위원입니다. 제기되는 질문에 따라서 성실히 답변해주시면 감사하겠습니다.
총 소요 시간은 10분 이내입니다.

${gradeOpts}
`; } function bindStep1() { document.getElementById('rg-grade').addEventListener('click', (e) => { const lbl = e.target.closest('label'); if (!lbl) return; const val = lbl.querySelector('input').value; state.meta.grade = val; document .querySelectorAll('#rg-grade label') .forEach((l) => l.classList.toggle( 'selected', l.querySelector('input').value === val, ), ); }); document.getElementById('btn-start').addEventListener('click', () => { if (!state.meta.grade) { alert(ALERT_FILL_ALL); 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 varRows = VARS.map((key) => { const score = c[key]; const level = getScoreLevel(score); return `
${VAR_LABELS[key]}
${score}
`; }).join(''); const likertBtns = JUDGMENT_SCALE.map((label, i) => { const val = i + 1; const active = state.currentJudgment === val; return ` `; }).join(''); return `

당신은 스타트업 지원 프로그램의 심사위원입니다. 아래 팀의 정보를 보고 지원금 지급 가능성을 직관적으로 평가해주세요.

${idx + 1} / ${total}
Team ${c.teamLetter}
${varRows}

이 팀의 지원금 지급 가능성을 평가하세요

${likertBtns}
`; } 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(ALERT_SELECT_JUDGMENT); 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(); } }); } // ────────────────────────────────────────── // STEP 3 // ────────────────────────────────────────── const AHP_PAIR_LABELS = [ ['안정성', '수익 가능성'], ['안정성', '자금 효율성'], ['안정성', '사업 계획 완성도'], ['안정성', '사회적 가치'], ['수익 가능성', '자금 효율성'], ['수익 가능성', '사업 계획 완성도'], ['수익 가능성', '사회적 가치'], ['자금 효율성', '사업 계획 완성도'], ['자금 효율성', '사회적 가치'], ['사업 계획 완성도', '사회적 가치'], ]; function getPairLabels(idx) { return AHP_PAIR_LABELS[idx]; } function getAHPDescription(sliderPos, leftLabel, rightLabel) { if (sliderPos === 0) return '두 기준이 동등하게 중요합니다'; const ahpVal = Math.abs(sliderToStored(sliderPos)); // 3,5,7,9 const level = AHP_LEVELS[ahpVal] || String(ahpVal); const crit = sliderPos < 0 ? leftLabel : rightLabel; return `${crit}이(가) ${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 `

제시되는 2개의 평가 기준 중 어느 것이 더 중요한지 비교해주세요. 왼쪽이 더 중요하면 슬라이더를 왼쪽으로, 오른쪽이 더 중요하면 오른쪽으로 움직이세요.

${idx + 1} / ${total}

AHP 쌍대비교

${left}
VS
${right}
9753 1 3579
${desc}
`; } 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); } function advancePair() { const total = AHP_PAIRS.length; if (state.pairIndex < total - 1) { state.pairIndex++; renderMain(); } else { const raw = state.sliderValues.map((s) => sliderToStored(s)); state.pairwiseRaw = raw; // CR은 분석용으로 계속 기록하되, 모순이 있어도 재입력 없이 그대로 진행 state.ahpResult = runAHP(raw); 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 weightRows = VARS.map((key, i) => { const pct = Math.round(weights[i] * 100); return `
${VAR_LABELS[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' ? `선발` : `탈락`; const iLabel = c.intuitive_category === 'select' ? `${c.intuitive_likert}` : c.intuitive_category === 'reject' ? `${c.intuitive_likert}` : `${c.intuitive_likert}`; const matchBadge = c.match === true ? `` : c.match === false ? `` : ``; return ` Team ${c.team} ${c.model_score} ${mLabel} ${iLabel} ${matchBadge} `; }) .join(''); return `

결과 비교

나의 판단 기준 (AHP 가중치)

${weightRows}

핵심 통계

${totalMismatch}/${decidable.length}
전체 불일치율
${baseline.filter((c) => !c.match).length}/${baseline.length}
베이스라인
불일치율
${spike.filter((c) => !c.match).length}/${spike.length}
스파이크 사례
불일치율
${conflict.filter((c) => !c.match).length}/${conflict.length}
충돌 사례
불일치율
${CR.toFixed(3)}
CR (일관성 비율)

총 불일치: ${totalMismatch} / ${decidable.length}건 (보통 제외)

사례별 모델 vs 직관 비교

${rows}
모델 점수 모델 판단 직관 (1-5) 일치
`; } // ────────────────────────────────────────── // STEP 5 // ────────────────────────────────────────── function renderStep5() { const groups = LIKERT_QUESTIONS.map((q, qi) => { const opts = LIKERT_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 `

사후 설문

마지막으로 간단한 설문에 답해주세요.

${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(ALERT_FILL_ALL); return; } submitData(); }); } // ────────────────────────────────────────── // Submit & Complete // ────────────────────────────────────────── function buildPayload() { return { meta: { timestamp: state.meta.timestamp, grade: state.meta.grade }, 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: false, // CR 경고 UI 제거 — CSV 컬럼 호환용으로 유지 }, 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 `

참여해 주셔서 감사합니다!

실험이 완료되었습니다. 소중한 시간을 내어 참여해 주셔서 진심으로 감사드립니다.

`; } 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 = LEAVE_WARNING; }; 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);