const createAiButton = (label, container, request, onSuccess) => { const button = createButton(label, async () => { button.disabled = true; button.textContent = '생성 중…'; const box = document.createElement('div'); box.className = 'ai-box'; box.textContent = 'AI가 답변을 작성하고 있어요…'; container.appendChild(box); try { const result = await request(); box.remove(); onSuccess(result); } catch (err) { box.classList.add('error'); box.textContent = err.message; } button.textContent = label; button.disabled = false; }); return button; }; const renderTwin = (twin, order) => { const box = document.createElement('div'); box.className = 'twin'; const title = document.createElement('p'); title.className = 'twin-title'; title.textContent = `쌍둥이 문제 ${order}. ${twin.question}`; box.appendChild(title); const options = document.createElement('ol'); options.className = 'twin-options'; shuffle([twin.correct_answer, ...twin.incorrect_answers]).forEach((option) => { const item = document.createElement('li'); item.textContent = option; options.appendChild(item); }); box.appendChild(options); const answer = document.createElement('details'); const summary = document.createElement('summary'); summary.textContent = '정답 보기'; const value = document.createElement('span'); value.textContent = ` ${twin.correct_answer}`; answer.append(summary, value); box.appendChild(answer); return box; }; const renderMessage = (role, content) => { const bubble = document.createElement('div'); bubble.className = `chat-message ${role}`; bubble.textContent = content; return bubble; }; const renderChat = (record, question, index) => { const chat = document.createElement('div'); chat.className = 'chat'; const heading = document.createElement('p'); heading.className = 'chat-heading'; heading.textContent = '이어서 질문하기'; chat.appendChild(heading); const log = document.createElement('div'); log.className = 'chat-log'; question.conversation.forEach((m) => log.appendChild(renderMessage(m.role, m.content))); chat.appendChild(log); const form = document.createElement('form'); form.className = 'chat-form'; const input = document.createElement('textarea'); input.rows = 1; input.placeholder = '이 문제에 대해 더 궁금한 점을 물어보세요'; input.setAttribute('aria-label', '후속 질문'); const sendButton = document.createElement('button'); sendButton.type = 'submit'; sendButton.className = 'primary'; sendButton.textContent = '질문'; form.append(input, sendButton); chat.appendChild(form); let pending = false; async function send(message) { const text = message.trim(); if (!text || pending) return; pending = true; sendButton.disabled = true; input.value = ''; log.appendChild(renderMessage('user', text)); const answering = renderMessage('assistant', 'AI가 답변을 작성하고 있어요…'); answering.classList.add('pending'); log.appendChild(answering); answering.scrollIntoView({ block: 'nearest' }); try { const { messages } = await api.ask(record.id, index, text); question.conversation.push(...messages); answering.classList.remove('pending'); answering.textContent = messages.find((m) => m.role === 'assistant')?.content ?? ''; } catch (err) { answering.classList.remove('pending'); answering.classList.add('error'); answering.textContent = err.message; input.value = text; } pending = false; sendButton.disabled = false; } form.onsubmit = (event) => { event.preventDefault(); send(input.value); }; input.onkeydown = (event) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); send(input.value); } }; chat.focusInput = () => input.focus(); return chat; }; const renderFeedbackCard = (record) => { const card = document.createElement('div'); card.className = 'card feedback'; const heading = document.createElement('h2'); heading.textContent = '메타인지 평가'; card.appendChild(heading); const body = document.createElement('div'); card.appendChild(body); const paint = (feedback) => { body.replaceChildren(); const summary = document.createElement('p'); summary.className = 'feedback-summary'; summary.textContent = feedback.summary; const detail = document.createElement('p'); detail.className = 'feedback-detail'; detail.textContent = feedback.detail; const recommendHeading = document.createElement('h3'); recommendHeading.textContent = '학습 추천'; const recommend = document.createElement('p'); recommend.className = 'feedback-detail'; recommend.textContent = feedback.recommendation; body.append(summary, detail, recommendHeading, recommend); }; if (record.feedback) { paint(record.feedback); return card; } body.innerHTML = `
AI가 이번 시험을 분석하고 있어요…
`; api.feedback(record.id) .then(({ feedback }) => { record.feedback = feedback; paint(feedback); }) .catch((err) => { body.replaceChildren(); const error = document.createElement('p'); error.className = 'error-box'; error.textContent = err.message; body.append( error, createAiButton('다시 시도', body, () => api.feedback(record.id), ({ feedback }) => { record.feedback = feedback; paint(feedback); }) ); }); return card; }; const renderQuestionCard = (record, question, index) => { const item = document.createElement('div'); item.className = 'card result-item'; const head = document.createElement('div'); head.className = 'result-head'; const badge = document.createElement('span'); badge.className = `badge ${question.is_correct ? 'ok' : 'bad'}`; badge.textContent = question.is_correct ? '정답' : '오답'; const text = document.createElement('span'); text.textContent = `${index + 1}. ${question.question}`; head.append(badge, text); item.appendChild(head); const answers = document.createElement('p'); answers.className = 'answers'; answers.innerHTML = '내 답: · 정답:
내가 밝힌 확신도: '; const spans = answers.querySelectorAll('span'); spans[0].textContent = question.user_answer ?? '무응답'; spans[1].textContent = question.correct_answer; spans[2].textContent = CONFIDENCE_LABEL(question.confidence); item.appendChild(answers); const detail = document.createElement('div'); detail.className = 'result-detail'; item.appendChild(detail); const explanationSection = document.createElement('div'); explanationSection.className = 'explanation'; detail.appendChild(explanationSection); question.conversation ??= []; const paintExplanation = (explanation) => { const box = document.createElement('div'); box.className = 'ai-box'; box.textContent = explanation; explanationSection.appendChild(box); const chat = renderChat(record, question, index); if (!question.conversation.length) { chat.hidden = true; const askButton = createButton( '이어서 질문하기', () => { chat.hidden = false; askButton.remove(); chat.focusInput(); }, 'ask-toggle' ); explanationSection.appendChild(askButton); } explanationSection.appendChild(chat); }; let twinCount = 0; const paintTwin = (twin) => { twinCount += 1; detail.appendChild(renderTwin(twin, twinCount)); }; if (question.explanation) paintExplanation(question.explanation); (question.children ?? []).forEach(paintTwin); const actions = document.createElement('div'); actions.className = 'result-actions'; const explainButton = createAiButton( 'AI 해설 보기', explanationSection, () => api.explain(record.id, index), ({ explanation }) => { question.explanation = explanation; paintExplanation(explanation); explainButton.remove(); } ); if (!question.explanation) actions.appendChild(explainButton); actions.appendChild( createAiButton( '쌍둥이 문제 만들기', detail, () => api.twin(record.id, index), ({ twin }) => { question.children = [...(question.children ?? []), twin]; paintTwin(twin); } ) ); item.appendChild(actions); return item; }; const renderReport = (record) => { const fragment = document.createDocumentFragment(); const { score } = record; const summary = document.createElement('div'); summary.className = 'card score'; summary.innerHTML = `

확신했지만 틀림
운으로 맞힘
확실히 아는 것

`; summary.querySelector('.range').textContent = `${record.student} · ${record.school} ${record.subject} · ${record.chapter} · ` + `난이도 ${DIFFICULTY_LABEL[record.difficulty] ?? record.difficulty} · ${formatDate(record.createdAt)}`; summary.querySelector('.big').textContent = `${score.percent}점`; summary.querySelector('.count').textContent = `${score.total}문제 중 ${score.correct}문제 정답`; const tiles = summary.querySelectorAll('.insight strong'); tiles[0].textContent = score.overconfident; tiles[1].textContent = score.lucky; tiles[2].textContent = score.solid; summary.querySelector('.note').textContent = score.overconfident ? '확신했지만 틀린 문제가 메타인지의 빈틈입니다. 이 개념부터 다시 확인해 보세요.' : '안다고 착각한 문제가 없습니다. 자신의 이해도를 잘 파악하고 있어요.'; fragment.appendChild(summary); fragment.appendChild(renderFeedbackCard(record)); const list = document.createElement('div'); list.className = 'result-list'; record.questions.forEach((question, index) => list.appendChild(renderQuestionCard(record, question, index)) ); fragment.appendChild(list); return fragment; };