Community코딩 & 개발github.com

KyeYongChoo/mcq-quiz-page

사용자가 슬라이드나 노트에서 객관식 퀴즈나 연습 문제를 요청할 때 사용합니다. 클릭 가능한 페이지 형태로 정답/오답을 초록/빨강으로 표시하고, 복습 모드, 오답 재시도, PDF 내보내기 기능을 제공합니다. 구조화된 질문과 프로그램 분석 질문도 포함할 수 있습니다.

mcq-quiz-page란 무엇인가요?

이 스킬은 사용자가 강의 슬라이드나 노트, PDF 자료를 기반으로 객관식 퀴즈 또는 연습 문제를 요청할 때 완전한 HTML 퀴즈 페이지를 생성하는 AI 에이전트 도구입니다. 단순한 질문 생성기를 넘어, 실제 학습에 바로 사용할 수 있는 고품질의 대화형 웹 페이지를 제작합니다. 주요 기능으로는 객관식 문제 선택 시 즉시 정답(초록색) 및 오답(빨간색) 피드백 제공, 자세한 설명 표시, 개별 문제 다시 풀기, 오답만 모아서 복습하는 Review 모드, 오답만 다시 풀 수 있는 Retry 탭, PDF 내보내기 및 텍스트 복사 등을 지원합니다. 또한 단답형 또는 코드 분석형 문제(Structured / Program-analysis questions)를 지원하여 학습자가 직접 답변을 입력하고 모범 답안과 비교한 후 스스로 채점할 수 있습니다. 모든 진행 상황과 답변은 브라우저 로컬 스토리지에 저장되어 페이지를 새로고침해도 유지됩니다. 사용자는 Artifact로 게시하거나 로컬 HTML 파일로 제공할 수 있으며, 질문 품질을 위해 균형 잡힌 선택지 분포와 설득력 있는 오답(distractor)을 생성하도록 설계되었습니다.

지원 대상✓Claude Code~Codex CLI✓Cursor
Part ofMobile
npx skills add https://github.com/KyeYongChoo/mcq-quiz-page-skill/tree/HEAD/mcq-quiz-page

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

MCQ 퀴즈 페이지

페이지 디자인은 완료되었습니다. 재디자인하지 마십시오. 여러분의 임무는 좋은 질문을 작성하여 아래 템플릿에 JSON으로 넣고 게시하는 것입니다.

페이지의 기능:

  • 객관식: 옵션을 탭하면 정답은 초록색, 오답은 빨간색으로 표시되고(정답 옵션이 초록색으로 바뀌고 설명이 표시됨). 답변한 모든 질문 아래에 있는 작은 "이 질문 다시 풀기" 버튼은 해당 질문만 초기화합니다.
  • 구조화된 질문 / 프로그램 분석 질문(유형 "short"): 사용자가 답변을 입력하고(선택 사항), 모범 답안 보기 버튼을 누른 후 스스로 "맞혔음" 또는 "틀렸음"을 표시합니다. 틀린 것은 실수로 집계됩니다. 코드 조각은 고정폭 블록으로 표시됩니다.
  • 복습 탭은 틀린 문제만 표시하며, 각 문제 아래에 정답/모범 답안이 바로 표시됩니다. 복습 모드에는 PDF 내보내기, 텍스트로 복사, 오답 재시도 기능이 있습니다.
  • 오답 재시도는 해당 질문을 초기화하고 Retry 탭을 열어 이전 오답만 표시합니다(전체 퀴즈를 스크롤할 필요 없음). 진행률(완료/전체)이 표시되며, 모두 완료되면 "여전히 틀린 N개 다시 시도" 및 "모든 질문으로 돌아가기" 요약이 표시됩니다. Retry 탭은 '모두 초기화'를 누를 때까지 사용 가능합니다.
  • 진행 상황, 입력한 답변 및 재시도 목록은 뷰어의 브라우저에 저장됩니다.

단계

  1. 먼저 원본 자료(슬라이드, 노트, PDF)를 읽으십시오. 질문은 기억이 아닌 자료에서 나와야 합니다. PDF의 경우 pdftotext -layout을 사용하십시오. 사용자가 튜토리얼이나 기출 문제 해설을 첨부한 경우, 해당 자료를 기반으로 MCQ와 구조화/프로그램 분석 질문을 모두 작성하십시오(요청이 없는 한 그대로 복사하지 말고 새로운 변형).
  2. 질문을 JSON 배열(아래 스키마 참조)로 작성하십시오. MCQ의 기본 옵션은 4개입니다. 사용자가 개수를 지정하면 정확히 그 개수를 사용하십시오. 출처에 단답형 또는 코드 관련 질문이 있거나 사용자가 요청하는 경우 구조화/프로그램 분석 질문을 추가하십시오(같은 섹션의 MCQ 뒤에 배치).
  3. 아래 균형 규칙을 사용하여 질문을 확인하십시오. 가능하면 작은 스크립트를 사용하는 것이 좋습니다.
  4. 페이지를 빌드하십시오: 템플릿을 index.html에 복사하고, __DATA__ 텍스트를 JSON으로 바꾼 후(모든 </를 </로 이스케이프), 다음만 변경하십시오: <title>, <h1>, .sub 문장, 저장소 키 bps-quiz-v1(각 퀴즈에 새 키 예: quiz-<topic>-v1을 지정하여 저장된 진행 상황이 섞이지 않도록). 가능하면 node --check를 스크립트에서 실행하십시오(스크립트 블록을 .js 파일로 추출). 빠른 Playwright 스모크 테스트(답변, 표시, 복습, 오답 재시도)를 수행하는 것이 좋습니다.
  5. 수작업으로 빌드된 HTML 아티팩트로 게시하십시오: 먼저 아티팩트 list_types를 호출하십시오(퀴즈 유형이 없음; publish_files를 index.html, 실제 제목, 아이콘 quiz와 함께 사용). 그런 다음 가장 새로운 버전 번호를 사용하여 declare_capabilities를 {"downloads": true}로 호출하십시오. PDF 버튼은 해당 기능이 있을 때만 작동합니다. 기존 퀴즈를 업데이트할 때는 publish_files(mode patch, slug, base_version)로 다시 게시하십시오. 기능 선언은 그대로 유지됩니다. 아티팩트를 게시할 수 없는 경우 index.html을 파일로 제공하십시오(PDF 버튼은 숨겨지고 텍스트 복사는 여전히 작동).
  6. 몇 문장으로 답변하십시오: 페이지가 무엇인지, 질문 수(MCQ 및 구조화)와 출처 섹션, PDF 내보내기가 뷰어에게 다운로드 확인을 요청한다는 점을 설명하십시오. 질문을 채팅에 붙여넣지 마십시오.

JSON 스키마 (질문당 하나의 객체, 순서대로)

객관식:

{"n": 1, "section": "Lecture 4 · Linkability", "q": "질문 텍스트?", "opts": ["A 텍스트", "B 텍스트", "C 텍스트", "D 텍스트"], "ans": 2, "ex": "한 문장 설명."}

구조화 또는 프로그램 분석(선택적 code, 선택적 kind, 선택적 ex):

{"n": 5, "type": "short", "section": "Lecture 4 · Linkability", "kind": "Structured question", "q": "설명하시오 ...?", "model": ["모범 답안 포인트 1", "포인트 2"], "ex": "선택적 추가 참고사항"}
{"n": 6, "type": "short", "section": "...", "kind": "Program analysis", "q": "여기서 무엇이 잘못되었고 어떻게 고칩니까?", "code": "int f(char *s) {\n  char b[8];\n  strcpy(b, s);\n}", "model": ["(a) ...", "(b) ..."]}

kind의 기본값은 "Short answer"입니다. "Structured question" 또는 "Program analysis"를 사용하십시오. 각 model 항목은 하나의 글머리 기호입니다. 하위 부분은 글머리 기호 내에 "(a)", "(b)"로 레이블을 지정하십시오. code는 \n 개행 문자와 2칸 들여쓰기로 작성하십시오.

선택적 옵션별 설명(사용자가 잘못된 옵션 아래에 추가 텍스트를 요청한 경우에만): MCQ에 "notes": ["", "B가 틀린 이유", "", "D가 틀린 이유"]를 추가하십시오. 각 옵션에 대해 동일한 순서로 하나의 문자열이며, 설명이 필요 없는 옵션(일반적으로 정답)에는 ""를 사용하십시오. 설명은 사용자가 답변한 후 각 옵션 아래에 표시되며, 복사/PDF에도 포함됩니다. 요청하지 않는 한 설명을 추가하지 마십시오.

n은 모든 질문에 걸쳐 1..N으로 공백 없이 실행됩니다(페이지는 n-1로 인덱싱됨). ans는 정답 옵션의 0부터 시작하는 인덱스입니다. section은 페이지에서 질문을 그룹화합니다. 한 그룹의 질문에는 동일한 문자열을 사용하고 그룹을 연속적으로 유지하십시오. 가능하면 일반 라틴 텍스트 외의 문자는 피하십시오(PDF는 내장 글꼴을 사용하며, 특수 문자는 ?로 인쇄됨).

질문 품질 규칙 (사용자가 신경 쓰는 사항)

  • 오답은 설득력이 있어야 합니다: 동일한 자료의 실제 개념을 사용하되 잘못된 질문에 연결하십시오(인접 용어의 속성, 근처 숫자, 그럴듯한 메커니즘). 농담 옵션, "위 모든 것"은 사용하지 마십시오.
  • 정답이 가장 길지 않도록 하십시오. 정답과 오답을 비슷한 길이와 세부 수준으로 작성하고, 때로는 오답을 가장 길게 만드십시오. 목표: 완전한 문장 옵션이 있는 질문의 약 30% 이하에서만 정답이 가장 길도록 하십시오(스크립트로 확인하십시오. 초과하기 쉽습니다).
  • 정답 문자를 A-D에 걸쳐 균등하게 분포시키고(약 25%씩), 같은 문자가 세 번 연속으로 나오지 않도록 하십시오. 가장 쉬운 방법: 정답과 세 개의 오답을 별도로 작성한 다음, 코드에서 균형 잡힌 위치 목록으로 섞으십시오.
  • 질문당 하나의 아이디어를 테스트하십시오. 질문 텍스트는 자체적으로 완결되도록 유지하십시오.
  • 모든 MCQ에는 ex에 설명을 포함하여 정답이 맞는 이유를 설명하고(도움이 될 때 오답이 실제로 무엇인지도 설명).
  • 구조화/프로그램 분석 모범 답안은 사용자가 스스로 솔직하게 채점할 수 있도록 출처 솔루션의 핵심 사항을 짧은 글머리 기호로 반영해야 합니다.
  • 슬라이드가 읽을 수 없는 다이어그램뿐인 경우 세부 사항을 지어내지 말고 그 사실을 말하십시오.

템플릿 (index.html)

<!DOCTYPE html>
<html lang="ko"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>주제 퀴즈</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@500&display=swap">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<style>
:root{--bg:#f5f6f8;--card:#ffffff;--ink:#16202e;--mute:#5b6678;--line:#d5dae3;--accent:#1d4ed8;--accent-soft:#e6ecff;
--ok:#0f7a4d;--ok-soft:#dcf3e6;--ok-line:#52b887;--bad:#b42318;--bad-soft:#fde4e1;--bad-line:#e57a70;
--body:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",sans-serif;--mono:"IBM Plex Mono",ui-monospace,Menlo,monospace}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#10151d;--card:#171e29;--ink:#e6eaf1;--mute:#9aa6b8;--line:#2e3849;--accent:#8aa9ff;--accent-soft:#1f2a45;--ok:#6fe0aa;--ok-soft:#12332a;--ok-line:#2f8a63;--bad:#ff9a8f;--bad-soft:#3b1b1a;--bad-line:#a94a42;color-scheme:dark}}
:root[data-theme="dark"]{--bg:#10151d;--card:#171e29;--ink:#e6eaf1;--mute:#9aa6b8;--line:#2e3849;--accent:#8aa9ff;--accent-soft:#1f2a45;--ok:#6fe0aa;--ok-soft:#12332a;--ok-line:#2f8a63;--bad:#ff9a8f;--bad-soft:#3b1b1a;--bad-line:#a94a42;color-scheme:dark}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:400 16px/1.55 var(--body)}
main{max-width:760px;margin:0 auto;padding:24px 16px 64px}
h1{font-size:1.7rem;line-height:1.2;margin:0 0 6px;text-wrap:balance}
.sub{color:var(--mute);margin:0 0 14px}
.bar{position:sticky;top:0;z-index:3;background:var(--bg);border-bottom:1px solid var(--line);padding:10px 0;display:flex;flex-wrap:wrap;gap:8px 10px;align-items:center}
.tabs{display:inline-flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;background:var(--card)}
.tabs button{border:0;background:transparent;color:var(--ink);font:500 .88rem var(--body);padding:8px 14px;cursor:pointer}
.tabs button[aria-pressed="true"]{background:var(--accent);color:var(--bg)}
.stats{font-size:.88rem;color:var(--mute);font-variant-numeric:tabular-nums;display:flex;gap:12px;flex-wrap:wrap}
.stats b{color:var(--ink)}
.acts{margin-left:auto;display:flex;gap:8px;flex-wrap:wrap}
.btn{font:500 .85rem var(--body);color:var(--ink);background:var(--card);border:1px solid var(--line);border-radius:8px;padding:7px 12px;cursor:pointer;text-decoration:none;display:inline-flex;gap:8px;align-items:center}
.btn.primary{background:var(--accent);border-color:var(--accent);color:var(--bg)}
.btn:disabled{opacity:.45;cursor:not-allowed}
.btn:not(:disabled):hover{filter:brightness(.95)}
:focus-visible{outline:3px solid var(--accent);outline-offset:2px}
.jump{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0 0}
.jump a{font:500 .85rem var(--body);color:var(--ink);background:var(--card);border:1px solid var(--line);border-radius:8px;padding:7px 11px;text-decoration:none}
.jump a span{color:var(--mute);margin-left:8px;font-variant-numeric:tabular-nums}
h2{font-size:1.08rem;margin:30px 0 14px;padding-bottom:8px;border-bottom:2px solid var(--accent);text-wrap:balance;scroll-margin-top:70px}
.banner{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px 16px;margin:18px 0 6px}
.banner p{margin:0}.banner p+p{margin-top:8px;color:var(--mute);font-size:.92rem}
.q{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px 16px 14px;margin-bottom:14px;scroll-margin-top:70px}
h3{font-size:1rem;font-weight:600;margin:0 0 12px;display:flex;gap:10px;line-height:1.45}
.num{font:500 .8rem var(--mono);color:var(--accent);background:var(--accent-soft);border-radius:6px;padding:2px 7px;height:fit-content;font-variant-numeric:tabular-nums}
.sec{display:block;font-weight:400;font-size:.78rem;color:var(--mute);margin-bottom:2px}
.opts{display:grid;gap:8px}
.opt{display:flex;gap:10px;align-items:flex-start;text-align:left;width:100%;font:inherit;color:inherit;background:transparent;border:1px solid var(--line);border-radius:8px;padding:10px 12px;cursor:pointer}
.opt b{font:500 .85rem var(--mono);color:var(--mute);min-width:1.2em;padding-top:1px}
.opt:not(:disabled):hover{border-color:var(--accent);background:var(--accent-soft)}
.opt:disabled{cursor:default}
.opt.ok{background:var(--ok-soft);border-color:var(--ok-line)}
.opt.ok b,.opt.ok .tag{color:var(--ok)}
.opt.bad{background:var(--bad-soft);border-color:var(--bad-line)}
.opt.bad b,.opt.bad .tag{color:var(--bad)}
.opt .txt{flex:1}
.tag{font:500 .75rem var(--mono);white-space:nowrap;padding-top:2px}
.fb{margin-top:10px;border-radius:8px;padding:10px 12px;background:var(--accent-soft)}
.fb p{margin:0}.fb .head{font-weight:600}.fb.good .head{color:var(--ok)}.fb.miss .head{color:var(--bad)}
.fb .why{color:var(--mute);font-size:.92rem;margin-top:4px}
.empty{text-align:center;color:var(--mute);padding:36px 12px}
.code{margin:0 0 12px;padding:10px 12px;background:var(--bg);border:1px solid var(--line);border-radius:8px;font:500 .82rem/1.5 var(--mono);overflow-x:auto;white-space:pre;tab-size:2}
textarea.ans{width:100%;font:inherit;color:var(--ink);background:var(--card);border:1px solid var(--line);border-radius:8px;padding:10px 12px;resize:vertical;min-height:84px}
textarea.ans[readonly]{background:var(--bg)}
.row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:10px}
.ask{color:var(--mute);font-size:.9rem}
.model{margin:6px 0 0;padding-left:20px}
.model li{margin:3px 0}
.mark{margin:10px 0 0;font-weight:600;font-size:.92rem}
.mark.good{color:var(--ok)}.mark.miss{color:var(--bad)}
.onote{display:block;margin-top:4px;font-size:.86rem;font-weight:400;color:var(--mute)}
.btn.small{padding:5px 10px;font-size:.8rem;color:var(--mute)}
</style></head><body><main>
<h1>주제 퀴즈</h1>
<p class="sub">강의 자료에서 가져온 N개의 질문입니다. 옵션을 탭하여 답변하세요. 복습 모드에서는 틀린 문제만 보고 PDF로 내보낼 수 있습니다.</p>
<div class="bar" role="toolbar" aria-label="퀴즈 컨트롤">
  <div class="tabs" role="group" aria-label="모드">
    <button type="button" id="tab-quiz" aria-pressed="true">퀴즈</button>
    <button type="button" id="tab-review" aria-pressed="false">복습</button>
    <button type="button" id="tab-retry" aria-pressed="false" hidden>재시도</button>
  </div>
  <div class="stats" id="stats" aria-live="polite"></div>
  <div class="acts" id="acts"></div>
</div>
<div id="view"></div>
</main>
<script>
var DATA=__DATA__;
var L="ABCD", KEY="bps-quiz-v1", DKEY=KEY+"-d", RKEY=KEY+"-r";
var answers={}, drafts={}, retry=[], mode="quiz", dl=null;
try{var s=localStorage.getItem(KEY); if(s) answers=JSON.parse(s)||{};}catch(e){}
try{var s2=localStorage.getItem(DKEY); if(s2) drafts=JSON.parse(s2)||{};}catch(e){}
try{var s3=localStorage.getItem(RKEY); if(s3) retry=JSON.parse(s3)||[];}catch(e){}
function save(){try{localStorage.setItem(KEY,JSON.stringify(answers));}catch(e){}}
function saveD(){try{localStorage.setItem(DKEY,JSON.stringify(drafts));}catch(e){}}
function saveR(){try{localStorage.setItem(RKEY,JSON.stringify(retry));}catch(e){}}
function esc(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");}
function byN(n){return DATA[n-1];}
function isDone(q){var a=answers[q.n]; return q.type==="short" ? (a==="ok"||a==="miss") : a!==undefined;}
function wrongList(){return DATA.filter(function(q){return q.type==="short" ? answers[q.n]==="miss" : (answers[q.n]!==undefined&&answers[q.n]!==q.ans);});}
function doneCount(){return DATA.filter(isDone).length;}

function shortHTML(q,ctx){
  var review=(ctx!=="quiz"), a=answers[q.n], marked=(a==="ok"||a==="miss"), shown=marked||a==="rev";
  var lab=(review?esc(q.section)+" - ":"")+es

…

[생략: 원본 템플릿의 나머지 JavaScript 및 HTML 부분]

관련 스킬