DAY 01 / SOURCE FILES
예제 코드 · 읽고 비교하기
코드는 ZIP 원본과 아래 열람본으로 함께 제공합니다.
예제 앱은 이 브라우저의 저장소를 사용합니다. 파일 URL에서 저장이 제한되면 앱의 안내에 따라 확인하세요.
파일 선택
- assignment-check-demo/index.html
- assignment-check-demo/README.md
- assignment-check-demo/prompts.txt
- assignment-check-demo/checks.csv
assignment-check-demo/index.html
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="과제를 기록하고 마감일과 완료 상태를 확인하는 학생용 학습 예제. 이 브라우저에만 저장됩니다.">
<title>과제 체크 · 오늘 할 일을 가볍게</title>
<style>
:root{font-family:"Apple SD Gothic Neo","Malgun Gothic",system-ui,sans-serif;color:#183c32;background:#f5f7f0;--ink:#183c32;--muted:#64786e;--line:#d4ded2;--green:#204d3b;--mint:#d9f1bd;--warn:#9c4435;color-scheme:light}
*{box-sizing:border-box}body{margin:0}button,input{font:inherit}button,a,input{outline-offset:4px}button:focus-visible,a:focus-visible,input:focus-visible{outline:3px solid #4b865d}button{cursor:pointer;min-height:48px;border:1px solid var(--line);border-radius:12px;padding:10px 16px;color:var(--ink);background:white}button:hover{background:#edf3e8}a{color:var(--green);text-underline-offset:4px}.wrap{max-width:1140px;margin:auto;padding:28px 28px 45px}.top{display:flex;justify-content:space-between;align-items:center;gap:16px;padding-bottom:24px;border-bottom:1px solid var(--line)}.brand{display:flex;gap:10px;align-items:center;font-weight:800;font-size:18px}.brand svg{width:34px;height:34px}.top a{font-size:13px;padding:14px 0}.eyebrow{font:11px ui-monospace,monospace;letter-spacing:.15em;color:var(--muted);margin:0 0 15px}.hero{display:flex;justify-content:space-between;align-items:flex-end;gap:24px;padding:40px 0 30px}.hero h1{font-size:clamp(34px,4.4vw,54px);line-height:1.16;letter-spacing:-.055em;margin:0 0 13px}.hero p{margin:0;color:var(--muted);font-size:15px;line-height:1.8}.badge{display:inline-flex;align-items:center;gap:7px;font-size:12px;white-space:nowrap;border:1px solid #ccdbc3;padding:9px 13px;border-radius:50px;background:#edf4e5}.badge:before{content:"";width:7px;height:7px;background:#628640;border-radius:50%}.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:26px}.stat{border:1px solid var(--line);border-radius:18px;padding:20px 23px;background:#fff}.stat:first-child{background:var(--green);color:#f2f9eb;border-color:var(--green)}.stat span{font-size:13px;display:block;margin-bottom:10px}.stat strong{font:600 35px ui-monospace,monospace;letter-spacing:-.05em}.stat small{font-size:12px;margin-left:7px;color:inherit}.stat.overdue strong{color:var(--warn)}.workspace{display:grid;grid-template-columns:320px minmax(0,1fr);gap:23px;align-items:start}.panel{background:white;border:1px solid var(--line);border-radius:20px;padding:25px}.panel h2,.list-head h2{font-size:20px;letter-spacing:-.035em;margin:0 0 7px}.panel .intro{font-size:13px;line-height:1.8;color:var(--muted);margin:0 0 24px}.field{margin-bottom:19px}.field label{font-size:13px;display:block;margin-bottom:8px;font-weight:650}.field label span{font-size:11px;font-weight:400;color:var(--muted);margin-left:5px}.field input{width:100%;min-height:48px;border:1px solid #b8c9ba;border-radius:10px;padding:11px 12px;font-size:1rem;color:var(--ink);background:#fcfdf9;min-width:0}.field input:user-invalid,.field input[aria-invalid=true]{border-color:var(--warn);background:#fff6f1}.error{display:none;font-size:12px;color:var(--warn);line-height:1.6;margin:7px 0 0}.field input:user-invalid+.error,.field input[aria-invalid=true]+.error{display:block}.primary{background:var(--green);border-color:var(--green);color:#f3ffe8;width:100%;font-weight:700}.primary:hover{background:#153e2d}.privacy{font-size:12px;color:var(--muted);line-height:1.8;margin:18px 0 0}.list-area{min-width:0}.list-head{display:flex;align-items:center;justify-content:space-between;gap:15px;margin:0 0 18px}.list-head p{font-size:12px;color:var(--muted);margin:0}.sample{background:#e6efdc;border-color:#c8d9bd;font-size:12px;white-space:nowrap}.filters{display:flex;gap:7px;padding:0;margin:0 0 15px;border:0;min-width:0}.filters legend{position:absolute;clip-path:inset(50%);width:1px;height:1px;overflow:hidden}.filters label{cursor:pointer;position:relative;display:flex;align-items:center;gap:7px;min-height:48px;padding:10px 14px;border:1px solid var(--line);border-radius:10px;font-size:13px;background:#fff}.filters label:has(input:checked){background:#e1efd6;border-color:#88a373;font-weight:700}.filters input{accent-color:var(--green);margin:0;width:16px;height:16px}.tasks{padding:0;list-style:none;display:flex;flex-direction:column;gap:11px;margin:0}.task{background:#fff;border:1px solid var(--line);border-radius:16px;padding:17px 17px 14px;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px 14px;align-items:center}.task-check{display:flex;gap:13px;align-items:center;min-height:48px;cursor:pointer;min-width:0}.task-check input{width:22px;height:22px;flex:0 0 22px;accent-color:var(--green);margin:0}.task-title{font-size:16px;line-height:1.5;font-weight:650;overflow-wrap:anywhere}.task.done .task-title{text-decoration:line-through;color:#7d8a80;font-weight:400}.task-meta{grid-column:1/-1;display:flex;flex-wrap:wrap;gap:8px;padding-left:35px;font-size:11px;color:var(--muted);align-items:center}.subject{background:#eef2e9;padding:5px 8px;border-radius:6px}.due{padding:5px 8px;background:#f0f4ea;border-radius:6px}.due.late{color:#963d30;background:#fff0e8}.due.today{color:#5a6b1c;background:#f1f2d7}.delete{font-size:12px;border-color:transparent;color:#768078;padding-inline:10px}.empty{border:1px dashed #c3d2bd;border-radius:18px;padding:50px 25px;text-align:center;background:#f0f5e9}.empty strong{display:block;font-size:18px;margin-bottom:10px}.empty p{font-size:13px;line-height:1.8;color:var(--muted);margin:0}.list-foot{font-size:11px;color:var(--muted);line-height:1.8;margin:15px 0 0}.notice{display:flex;gap:15px;align-items:center;justify-content:space-between;min-height:50px;margin-top:18px;padding:10px 15px;border-radius:12px;background:#e1efd6;font-size:13px;line-height:1.6}.notice:has(span:empty){display:none}.notice button{min-height:40px;font-size:12px;white-space:nowrap}.storage-warning{font-size:13px;line-height:1.8;padding:15px 18px;margin:0 0 22px;background:#fff0df;border:1px solid #e4c39e;border-radius:12px}.storage-warning[hidden],button[hidden],.empty[hidden]{display:none}footer{display:flex;justify-content:space-between;gap:20px;margin-top:35px;padding-top:20px;border-top:1px solid var(--line);font-size:11px;color:var(--muted);line-height:1.9}footer p{margin:0}.skip{position:absolute;top:-100px;left:20px}.skip:focus{top:10px;padding:10px;background:white;z-index:2}
@media(max-width:760px){.wrap{padding:20px 18px 32px}.top{padding-bottom:17px}.hero{padding:29px 0 23px;display:block}.hero .badge{margin-top:17px}.hero h1{font-size:36px}.stats{gap:9px;margin-bottom:20px}.stat{padding:16px 13px;border-radius:14px}.stat strong{font-size:29px}.stat span{font-size:11px}.workspace{grid-template-columns:1fr;gap:25px}.panel{padding:23px}.list-head{margin-bottom:14px}footer{display:block}footer p+p{margin-top:7px}}
@media(prefers-reduced-motion:no-preference){button{transition:background .15s ease}}
</style>
</head>
<body>
<a class="skip" href="#taskTitle">과제 추가로 이동</a>
<div class="wrap">
<header class="top"><div class="brand"><svg viewBox="0 0 36 36" aria-hidden="true"><rect width="36" height="36" rx="11" fill="#204d3b"/><path d="m10 18 5 5 11-11" fill="none" stroke="#d9f1bd" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>과제 체크</div><a href="https://vibe-coding-course-three.vercel.app/assignment-lab.html">따라 만들기 교재 ↗</a></header>
<main>
<section class="hero" aria-labelledby="pageTitle"><div><p class="eyebrow">ONE THING AT A TIME</p><h1 id="pageTitle">오늘 할 일을,<br>하나씩 가볍게.</h1><p>과제를 적고, 마감일을 확인하고, 끝낸 일에 체크하세요.</p></div><span class="badge" id="storageBadge">이 브라우저에 저장</span></section>
<div id="storageWarning" class="storage-warning" role="alert" hidden></div>
<section class="stats" aria-label="과제 현황"><div class="stat"><span>남은 과제</span><strong id="openCount">0</strong><small>개</small></div><div class="stat"><span>오늘 마감</span><strong id="todayCount">0</strong><small>개</small></div><div class="stat overdue"><span>기한 지남</span><strong id="lateCount">0</strong><small>개</small></div></section>
<div class="workspace">
<section class="panel" aria-labelledby="newTaskTitle"><h2 id="newTaskTitle">새 과제 추가</h2><p class="intro">작게 적어두면, 시작하기 쉬워져요.</p>
<noscript><p>이 예제 앱은 JavaScript가 필요합니다. 브라우저에서 JavaScript를 켠 뒤 다시 열어주세요.</p></noscript>
<form id="taskForm" action="#newTaskTitle" method="get"><fieldset id="inputFields" disabled style="border:0;padding:0;margin:0;min-width:0"><legend style="position:absolute;clip-path:inset(50%);width:1px;height:1px;overflow:hidden">과제 정보</legend>
<div class="field"><label for="taskTitle">과제명 <span>필수 · 60자 이내</span></label><input id="taskTitle" name="title" type="text" required maxlength="60" pattern=".*\S.*" placeholder="예: 국어 독서 감상문 쓰기" autocomplete="off" aria-describedby="titleError"><p id="titleError" class="error">⚠ 과제명을 입력해 주세요. 공백만 입력할 수 없어요.</p></div>
<div class="field"><label for="taskSubject">과목 <span>선택 · 20자 이내</span></label><input id="taskSubject" name="subject" type="text" maxlength="20" placeholder="예: 국어" autocomplete="off" aria-describedby="subjectError"><p id="subjectError" class="error">⚠ 과목은 20자 이내로 입력해 주세요.</p></div>
<div class="field"><label for="taskDue">마감일 <span>필수</span></label><input id="taskDue" name="due" type="date" required min="0001-01-01" max="9999-12-31" aria-describedby="dueError"><p id="dueError" class="error">⚠ 올바른 마감일을 선택해 주세요.</p></div>
<button class="primary" type="submit">+ 과제 추가</button>
</fieldset></form>
<p class="privacy">이 브라우저에만 저장돼요. 다른 기기와 자동으로 공유되지 않으며, 사이트 데이터를 지우면 목록도 지워져요.</p>
</section>
<section class="list-area" aria-labelledby="taskListTitle"><div class="list-head"><div><h2 id="taskListTitle">나의 과제</h2><p id="todayLabel"></p></div><button class="sample" type="button" id="sampleBtn">예제 3개 넣기</button></div>
<fieldset class="filters"><legend>과제 표시 필터</legend><label><input type="radio" name="filter" value="all" id="filterAll" checked>전체</label><label><input type="radio" name="filter" value="open" id="filterOpen">미완료</label><label><input type="radio" name="filter" value="done" id="filterDone">완료</label></fieldset>
<ul class="tasks" id="tasks" aria-label="과제 목록"></ul>
<div class="empty" id="empty"><strong id="emptyTitle">아직 등록한 과제가 없어요.</strong><p id="emptyMessage">왼쪽에서 첫 과제를 추가하거나<br>예제 3개로 먼저 둘러보세요.</p></div>
<p class="list-foot" id="listSummary" role="status"></p>
<div class="notice"><span id="status" role="status" aria-live="polite"></span><button id="undoBtn" type="button" hidden>삭제 되돌리기</button></div>
</section>
</div>
</main>
<footer><p>DAY 01 · 무료 AI → HTML → GitHub Pages 학습 예제</p><p>회원가입·서버·외부 API 없이 동작합니다.</p></footer>
</div>
<script>
'use strict';
// 1. 달력 날짜는 시간대가 없는 YYYY-MM-DD 값으로 다룹니다.
const $ = selector => document.querySelector(selector);
const STORAGE_KEY = 'assignment-check-demo:v1';
let items = [], persistent = true, removed = null;
const hasTemporal = typeof Temporal !== 'undefined' && !!Temporal.PlainDate;
function dateValue(date = new Date()) {
return [date.getFullYear(), String(date.getMonth()+1).padStart(2,'0'), String(date.getDate()).padStart(2,'0')].join('-');
}
function validDate(value) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || value.startsWith('0000')) return false;
if (hasTemporal) { try { return Temporal.PlainDate.from(value).toString() === value; } catch { return false; } }
// 단일 파일을 오프라인에서도 쓰기 위한 로컬 구성요소 대체 검증입니다.
const [year,month,day] = value.split('-').map(Number), date = new Date(0);
date.setFullYear(year,month-1,day);
return date.getFullYear()===year && date.getMonth()===month-1 && date.getDate()===day;
}
function compareDates(a,b) {
return hasTemporal ? Temporal.PlainDate.compare(a,b) : a.localeCompare(b);
}
function shiftToday(days) {
if (hasTemporal) return Temporal.Now.plainDateISO().add({days}).toString();
const date = new Date(); date.setDate(date.getDate()+days); return dateValue(date);
}
function labelDate(value) { const [,month,day] = value.split('-'); return `${Number(month)}월 ${Number(day)}일`; }
// 2. 저장 실패를 성공으로 표시하지 않고, 임시 모드를 안내합니다.
function temporaryMode(message) {
persistent = false;
$('#storageBadge').textContent = '임시 모드';
$('#storageWarning').hidden = false;
$('#storageWarning').textContent = message;
}
function validItem(item) {
return item && typeof item.id==='string' && item.id.length<=100 && typeof item.title==='string' && item.title.trim().length>0 && item.title.length<=60 && typeof item.subject==='string' && item.subject.length<=20 && typeof item.due==='string' && validDate(item.due) && typeof item.done==='boolean' && Number.isFinite(item.createdAt);
}
function load() {
try {
const value = localStorage.getItem(STORAGE_KEY);
if (value === null) return;
const parsed = JSON.parse(value);
if (!Array.isArray(parsed) || !parsed.every(validItem) || new Set(parsed.map(item=>item.id)).size!==parsed.length) throw new Error('invalid data');
items = parsed;
} catch {
temporaryMode('저장된 목록을 읽을 수 없어 임시 모드로 시작했어요. 기존 저장 데이터는 덮어쓰지 않습니다. 이 화면의 새 목록은 새로고침하면 사라질 수 있어요.');
}
}
function save() {
if (!persistent) return;
try { localStorage.setItem(STORAGE_KEY,JSON.stringify(items)); }
catch { temporaryMode('브라우저 저장이 차단되었거나 저장 공간이 부족해요. 현재 목록은 화면에 유지되지만 새로고침하면 이번 변경을 잃을 수 있어요.'); }
}
function message(text) { $('#status').textContent=text; }
function make(tag,className,text) { const node=document.createElement(tag); if(className)node.className=className; if(text!==undefined)node.textContent=text; return node; }
function focusTask(id) { const target=[...$('#tasks').querySelectorAll('input')].find(input=>input.dataset.id===id); (target||$('#tasks input')||document.querySelector('[name=filter]:checked')).focus(); }
// 3. 사용자가 입력한 값은 HTML로 실행하지 않고 textContent로 표시합니다.
function render() {
const today = hasTemporal ? Temporal.Now.plainDateISO().toString() : dateValue();
const open=items.filter(item=>!item.done), done=items.length-open.length;
$('#openCount').textContent=open.length;
$('#todayCount').textContent=open.filter(item=>compareDates(item.due,today)===0).length;
$('#lateCount').textContent=open.filter(item=>compareDates(item.due,today)<0).length;
$('#todayLabel').textContent=`오늘 ${labelDate(today)} · 미완료 먼저, 마감일순`;
const filter=document.querySelector('[name=filter]:checked').value;
const shown=items.filter(item=>filter==='all'||(filter==='done'?item.done:!item.done)).sort((a,b)=>Number(a.done)-Number(b.done)||compareDates(a.due,b.due)||a.createdAt-b.createdAt);
const list=$('#tasks'); list.replaceChildren();
for (const item of shown) {
const row=make('li','task'+(item.done?' done':''));
const label=make('label','task-check'), checkbox=make('input');
checkbox.type='checkbox'; checkbox.checked=item.done; checkbox.dataset.id=item.id;
checkbox.setAttribute('aria-label',`${item.title} 완료 표시`);
label.append(checkbox,make('span','task-title',item.title));
checkbox.addEventListener('change',()=>{item.done=checkbox.checked; save(); render(); focusTask(item.id); message(item.done?'과제를 완료했어요.':'다시 미완료로 표시했어요.');});
const remove=make('button','delete','삭제'); remove.type='button'; remove.setAttribute('aria-label',`${item.title} 삭제`);
remove.addEventListener('click',()=>{
removed={item,index:items.indexOf(item)}; items=items.filter(task=>task.id!==item.id);
save(); render(); $('#undoBtn').hidden=false; message('과제를 삭제했어요. 되돌릴 수 있어요.');
($('#tasks input')||$('#taskTitle')).focus();
});
const meta=make('div','task-meta');
meta.append(make('span','subject',item.subject||'과목 없음'));
const state=item.done?'완료':(compareDates(item.due,today)<0?'기한 지남':compareDates(item.due,today)===0?'오늘 마감':'마감');
const due=make('span','due'+(!item.done&&compareDates(item.due,today)<0?' late':!item.done&&compareDates(item.due,today)===0?' today':''),`${labelDate(item.due)} · ${state}`);
meta.append(due); row.append(label,remove,meta); list.append(row);
}
$('#empty').hidden=shown.length>0;
$('#emptyTitle').textContent=items.length===0?'아직 등록한 과제가 없어요.':filter==='done'?'완료한 과제가 아직 없어요.':'남은 과제가 없어요!';
$('#emptyMessage').textContent=items.length===0?'새 과제를 추가하거나 예제 3개로 먼저 둘러보세요.':filter==='done'?'끝낸 과제의 체크박스를 눌러보세요.':'전체 목록에서 과제를 확인하거나 새 과제를 추가해 보세요.';
$('#listSummary').textContent=`전체 ${items.length}개 · 완료 ${done}개 · 현재 ${shown.length}개 표시`;
$('#sampleBtn').disabled=['demo-1','demo-2','demo-3'].every(id=>items.some(item=>item.id===id));
}
// 4. 네이티브 폼 검증과 접근성 상태를 함께 사용합니다.
const form=$('#taskForm'), interacted=new WeakSet();
function syncField(input) { if (input.validity.valid)input.removeAttribute('aria-invalid'); else input.setAttribute('aria-invalid','true'); }
form.addEventListener('invalid',event=>{interacted.add(event.target);syncField(event.target);},true);
form.addEventListener('blur',event=>{if(event.target.matches('input')&&interacted.has(event.target))syncField(event.target);},true);
form.addEventListener('input',event=>{if(event.target.matches('input')){interacted.add(event.target);event.target.setCustomValidity(''); if(event.target.validity.valid)event.target.removeAttribute('aria-invalid');}});
form.addEventListener('submit',event=>{
event.preventDefault();
const title=$('#taskTitle'),subject=$('#taskSubject'),due=$('#taskDue');
title.setCustomValidity(title.value.trim().length>0&&title.value.trim().length<=60?'':'과제명을 1~60자로 입력해 주세요.');
subject.setCustomValidity(subject.value.trim().length<=20?'':'과목은 20자 이내로 입력해 주세요.');
due.setCustomValidity(validDate(due.value)?'':'올바른 마감일을 선택해 주세요.');
[title,subject,due].forEach(syncField);
if (!form.reportValidity()) return;
items.push({id:crypto.randomUUID?crypto.randomUUID():`task-${Date.now()}-${Math.random().toString(36).slice(2)}`,title:title.value.trim(),subject:subject.value.trim(),due:due.value,done:false,createdAt:Date.now()});
save(); $('#filterAll').checked=true; render();
form.reset(); [title,subject,due].forEach(input=>{input.removeAttribute('aria-invalid');interacted.delete(input);}); due.value=hasTemporal?Temporal.Now.plainDateISO().toString():dateValue();
title.focus(); message(persistent?'과제를 추가하고 이 브라우저에 저장했어요.':'과제를 추가했어요. 현재 임시 모드입니다.');
});
document.querySelectorAll('[name=filter]').forEach(input=>input.addEventListener('change',render));
$('#undoBtn').addEventListener('click',()=>{if(!removed)return;const restored=removed.item;items.splice(removed.index,0,restored);removed=null;save();$('#filterAll').checked=true;render();$('#undoBtn').hidden=true;focusTask(restored.id);message('삭제한 과제를 되돌렸어요.');});
$('#sampleBtn').addEventListener('click',()=>{
const samples=[['demo-1','국어 독서 감상문','국어',0],['demo-2','수학 연습문제 5개','수학',1],['demo-3','영어 단어 복습','영어',-1]];
samples.forEach(([id,title,subject,offset],index)=>{if(!items.some(item=>item.id===id))items.push({id,title,subject,due:shiftToday(offset),done:false,createdAt:Date.now()+index});});
save();$('#filterAll').checked=true;render();message('가상 과제 3개를 넣었어요. 오늘 마감과 기한 지남을 비교해 보세요.');
});
load(); $('#taskDue').value=hasTemporal?Temporal.Now.plainDateISO().toString():dateValue(); render();
$('#inputFields').disabled=false;
window.addEventListener('focus',render);
</script>
</body>
</html>
assignment-check-demo/README.md
# 과제 체크 · 학생 실습 예제
[실제 GitHub Pages 앱](https://sky6174.github.io/assignment-check-demo/) · [단계별 실습·프롬프트·강사 설명](https://vibe-coding-course-three.vercel.app/assignment-lab.html)
무료 AI 도구로 만든 단일 HTML 앱을 GitHub Pages에 배포하는 수업의 강사 기준 코드입니다. 특정 무료 서비스에서 이 코드를 생성했다는 실행 기록을 뜻하지 않습니다. 수강생은 Gemini 기본 또는 ChatGPT Free 등 접근 가능한 도구 한 개에 교재의 요청을 입력하고 결과를 비교합니다.
## 기능
과제명·과목·마감일 입력, 완료 체크, 전체/미완료/완료 보기, 마감일순 정렬, 오늘/기한 지남 집계, 삭제와 되돌리기, 가상 예제 3개를 제공합니다. 데이터는 현재 origin과 브라우저의 localStorage에 저장됩니다. 다른 사람·기기와 공유되지 않으며 로컬 파일과 Pages의 저장 영역도 다릅니다. 서버·외부 API·설치·빌드가 필요 없습니다.
## 배포
1. `index.html`을 브라우저에서 열고 입력·완료·새로고침을 확인합니다. 파일 환경의 저장이 제한되면 공개 Pages 또는 로컬 HTTP로 확인합니다.
2. 학생 자신의 GitHub 계정에 `assignment-check` 공개 저장소를 만듭니다.
3. `Add file → Upload files`로 `index.html`을 저장소 최상위에 올리고 commit합니다.
4. `Settings → Pages → Deploy from a branch → main → /(root) → Save`로 설정합니다.
5. 배포 완료 URL을 열고 휴대폰에서 핵심 기능을 확인합니다. 페이지 파일명을 `Index.html`로 바꾸거나 한 단계 아래 폴더에 넣지 않습니다.
6. 수정한 파일을 다시 commit하고 새 배포 완료 후 확인합니다.
공개할 소스에는 가상 예제만 넣습니다. 실제 과제 입력은 브라우저 내부에 보관됩니다. 브라우저의 사이트 데이터 삭제·시크릿 세션 종료·저장 제한에서는 보존되지 않을 수 있습니다. 읽기/쓰기 실패 시 앱이 임시 모드를 표시합니다.
assignment-check-demo/prompts.txt
과제 체크 앱 · 무료 AI 요청
1. 생성
학생용 과제 체크 웹앱을 만들어줘. HTML·CSS·JavaScript를 index.html 한 파일에 넣고 외부 라이브러리·서버·DB·로그인·유료 API·앱 내 AI 호출 없이 실행되게 해줘. 과제명(필수, 공백만 불가,60자 이내), 과목(선택,20자 이내), 마감일(필수)을 입력해 목록에 추가하고 완료 체크를 할 수 있게 해줘. 현재 브라우저의 localStorage에 저장하고 새로고침 뒤 복원해줘. 이 브라우저에만 저장되고 다른 기기와 공유되지 않는다고 화면에 표시해줘. 날짜는 기기의 로컬 달력 날짜로 비교해줘. label과 네이티브 입력·버튼,빈 상태·입력 오류·저장 실패 안내, 휴대폰에서 한 열로 보이는 디자인을 넣어줘. 사용자 입력은 textContent로 표시해줘. 처음에는 목록을 비워두고 가상 예제 버튼은 선택으로 제공해줘. 전체 코드와 저장/실행 방법, 실제 확인할 체크리스트를 줘.
2. 선택 개선
현재 동작하는 과제 추가·완료·localStorage 기능을 유지하며 최소 수정해줘. 전체/미완료/완료 필터와 미완료 우선·마감일순 정렬,남은 과제·오늘 마감·기한 지남 집계를 추가해줘. 오늘 마감은 오늘과 같은 날짜, 기한 지남은 오늘보다 이전인 미완료 과제만 세어줘. 개별 삭제와 되돌리기도 추가해줘. 기능을 바꾸기 전 현재 코드를 읽고 변경 지점을 설명한 뒤 전체 수정 코드를 줘. 새 서버·API·결제·공동 저장은 추가하지 마.
3. 검증
이 과제 체크 앱을 실제 확인한 결과로 검토해줘. 빈 과제명과 공백만 입력,오늘/어제/내일 마감,완료 표시,새로고침 후 복원,휴대폰 화면,공개 Pages URL을 검사해줘. 필터·집계·삭제 되돌리기를 구현했다면 해당 동작도 확인해줘. 다른 브라우저나 기기에 데이터가 자동 공유되지 않는 이유와 file/localhost/Pages 저장 영역 차이를 설명해줘. 오류가 있으면 재현 단계·현재 코드·실제 결과·예상 결과를 근거로 최소 수정해줘. 실행하지 않은 항목을 통과라고 쓰지 마. API 키나 계정 비밀번호는 요구하지 마.assignment-check-demo/checks.csv
구분,검증 항목,입력/행동,기대 결과
필수,과제 추가,국어 독서 감상문·국어·오늘,목록1개와 마감일 표시
필수,빈값·공백,제목 없음 또는 공백만,추가되지 않고 오류 안내
필수,완료 표시,목록의 완료 체크,완료 상태가 바뀜
필수,저장,공개 Pages에서 추가 후 새로고침,같은 브라우저에 목록·완료 유지
필수,공개 주소,github.io URL을 휴대폰에서 열기,앱 실행·화면 가로 잘림 없음
선택,날짜 집계,어제·오늘·내일 미완료3개,기한지남1·오늘마감1·남은과제3
선택,완료 제외,오늘 과제를 완료,오늘마감0·남은과제2
선택,필터,전체/미완료/완료 선택,해당 항목만 표시
선택,삭제/되돌리기,한 항목 삭제 후 되돌리기,같은 제목·날짜·상태 복구
설명,저장 범위,다른 브라우저·기기와 비교,동기화되지 않음을 설명