← 실습 자료

로컬 할 일 앱 · DB 연결 전

추가·완료·삭제를 연습합니다. 새로고침하면 초기화됩니다.

HTML 복사하기

HTML 직접 보기
<!doctype html>
<html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>할 일 앱 · DB 연결 전</title><style>*{box-sizing:border-box}body{margin:0;background:#f5f3ee;color:#203d35;font:17px/1.6 system-ui,-apple-system,"Noto Sans KR",sans-serif}main{max-width:900px;margin:36px auto;padding:32px;background:#fff;border:1px solid #dcded5;border-radius:24px}h1{font-size:clamp(26px,5vw,40px);line-height:1.3;letter-spacing:-.03em;margin:8px 0 12px}h2{font-size:22px}p{margin:10px 0}.eyebrow{font-size:13px;font-weight:800;letter-spacing:.09em;color:#547060}.muted{color:#68786f;font-size:15px}.note{padding:14px 18px;background:#fff2c8;border-radius:12px;color:#4f481f}.row{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:16px 0}button,input,textarea,select{font:inherit;border-radius:10px;padding:12px 16px}button{background:#245445;color:white;border:0;cursor:pointer;font-weight:700}button.secondary{background:#edf2ed;color:#245445}button:disabled{opacity:.45;cursor:wait}input,textarea,select{border:1px solid #bacbc2;background:#fff;color:#203d35;max-width:100%}input,textarea{flex:1;min-width:180px}textarea{width:100%;min-height:150px;resize:vertical}label{font-weight:700}button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:3px solid #e3ae35;outline-offset:3px}.card{border:1px solid #dce5df;padding:18px;border-radius:16px;margin:12px 0}.big{font-size:clamp(36px,8vw,72px);font-weight:800;text-align:center;padding:40px 0;background:#edf3ed;border-radius:18px}.badge{display:inline-block;background:#ecf2ed;color:#245445;padding:4px 10px;border-radius:7px;font-size:14px}.todo{display:flex;align-items:center;gap:10px;padding:12px 0;border-bottom:1px solid #e7ece8}.todo span{flex:1;overflow-wrap:anywhere}.todo.done span{text-decoration:line-through;color:#809086}.error{color:#9c382a}pre{white-space:pre-wrap;overflow-wrap:anywhere;font:16px/1.7 system-ui,"Noto Sans KR",sans-serif}.hidden{display:none}nav{display:flex;gap:8px;flex-wrap:wrap}@media(max-width:600px){main{margin:0;padding:22px 18px;border-radius:0;border:0;min-height:100vh}.todo{flex-wrap:wrap}button{min-height:48px}}</style></head><body><main><div class="eyebrow">03–04 · PRD로 만든 첫 버전</div><h1>나의 오늘 할 일</h1><p class="note">지금은 화면 메모리만 사용합니다. 새로고침하면 목록이 사라져요. DB와 로그인은 5–6차시에서 연결합니다.</p><label for="title">새 할 일</label><div class="row"><input id="title" maxlength="200" placeholder="예: 내일 수업 준비"><button id="add">추가</button></div><p id="error" class="error" role="status"></p><p id="count" class="muted"></p><div id="list" aria-live="polite"></div><p class="muted">확인 순서: 빈 입력 → 추가 → 완료 전환 → 삭제 → 새로고침</p></main><script>let items=[],nextId=1;const $=id=>document.getElementById(id);function render(){$('list').replaceChildren();$('count').textContent=items.length?'전체 '+items.length+'개 · 완료 '+items.filter(x=>x.done).length+'개':'아직 할 일이 없어요.';items.forEach(item=>{const row=document.createElement('div');row.className='todo'+(item.done?' done':'');const text=document.createElement('span');text.textContent=item.title;const toggle=document.createElement('button');toggle.className='secondary';toggle.textContent=item.done?'완료 취소':'완료';toggle.setAttribute('aria-label',item.title+' '+toggle.textContent);toggle.addEventListener('click',()=>{item.done=!item.done;render()});const remove=document.createElement('button');remove.className='secondary';remove.textContent='삭제';remove.setAttribute('aria-label',item.title+' 삭제');remove.addEventListener('click',()=>{items=items.filter(x=>x.id!==item.id);render()});row.append(text,toggle,remove);$('list').append(row)})}function add(){const title=$('title').value.trim();if(!title||title.length>200){$('error').textContent='1~200자로 할 일을 적어 주세요.';return}items.push({id:nextId++,title,done:false});$('title').value='';$('error').textContent='';render();$('title').focus()}$('add').addEventListener('click',add);const titleInput=$('title');let composing=false;
titleInput.addEventListener('compositionstart',()=>{composing=true});
titleInput.addEventListener('compositionend',()=>{composing=false});
titleInput.addEventListener('keydown',e=>{
  if(e.key!=='Enter'||composing||e.isComposing||e.keyCode===229)return;
  e.preventDefault();
  if(!e.repeat)add();
});render();</script></body></html>