Files
to-do/public/index.html
2026-06-15 15:11:50 +09:00

133 lines
5.2 KiB
HTML

<!doctype html>
<!-- Design Ref: §2 public/index.html — fetch 기반 단일 페이지 UI. Plan SC: SC-01 -->
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>To-Do</title>
<style>
:root { --bg:#f6f7f9; --card:#fff; --line:#e5e7eb; --muted:#6b7280; --accent:#2563eb; }
* { box-sizing: border-box; }
body { margin:0; font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
background: var(--bg); color:#111827; }
.wrap { max-width: 640px; margin: 40px auto; padding: 0 16px; }
h1 { font-size: 1.5rem; margin: 0 0 16px; }
form { display:flex; gap:8px; flex-wrap:wrap; background:var(--card);
padding:16px; border:1px solid var(--line); border-radius:12px; }
input[type=text] { flex:1 1 240px; padding:10px 12px; border:1px solid var(--line);
border-radius:8px; font-size:1rem; }
input[type=file] { flex:1 1 200px; font-size:.85rem; }
button { padding:10px 16px; border:0; border-radius:8px; background:var(--accent);
color:#fff; font-size:.95rem; cursor:pointer; }
button.secondary { background:transparent; color:var(--muted); padding:6px 8px; }
button:disabled { opacity:.5; cursor:default; }
ul { list-style:none; padding:0; margin:16px 0 0; }
li { display:flex; align-items:center; gap:10px; background:var(--card);
padding:12px 14px; border:1px solid var(--line); border-radius:10px; margin-bottom:8px; }
li.done .title { text-decoration: line-through; color: var(--muted); }
.title { flex:1; word-break: break-word; }
.meta { font-size:.8rem; }
a.dl { color: var(--accent); text-decoration:none; font-size:.8rem; }
.empty, .err { color: var(--muted); text-align:center; padding:24px 0; }
.err { color:#b91c1c; }
</style>
</head>
<body>
<div class="wrap">
<h1>📝 To-Do</h1>
<form id="add-form">
<input type="text" id="title" placeholder="할 일을 입력하세요" autocomplete="off" required />
<input type="file" id="file" />
<button type="submit" id="add-btn">추가</button>
</form>
<ul id="list"></ul>
<div id="status" class="empty"></div>
</div>
<script>
const listEl = document.getElementById("list");
const statusEl = document.getElementById("status");
const form = document.getElementById("add-form");
const titleEl = document.getElementById("title");
const fileEl = document.getElementById("file");
const addBtn = document.getElementById("add-btn");
const esc = (s) => String(s ?? "").replace(/[&<>"']/g,
(c) => ({ "&":"&amp;", "<":"&lt;", ">":"&gt;", '"':"&quot;", "'":"&#39;" }[c]));
async function api(path, opts) {
const res = await fetch(path, opts);
if (res.status === 204) return null;
const body = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(body.error || `HTTP ${res.status}`);
return body.data;
}
function render(todos) {
listEl.innerHTML = "";
if (!todos.length) { statusEl.textContent = "할 일이 없습니다."; statusEl.className = "empty"; return; }
statusEl.textContent = "";
for (const t of todos) {
const li = document.createElement("li");
if (t.done) li.classList.add("done");
const cb = document.createElement("input");
cb.type = "checkbox"; cb.checked = t.done;
cb.addEventListener("change", () => toggle(t.id, cb.checked));
const span = document.createElement("span");
span.className = "title";
span.innerHTML = esc(t.title) +
(t.hasAttachment
? ` <a class="dl" href="/api/todos/${t.id}/attachment">📎 ${esc(t.attachment_name || "첨부")}</a>`
: "");
const del = document.createElement("button");
del.className = "secondary"; del.textContent = "삭제";
del.addEventListener("click", () => remove(t.id));
li.append(cb, span, del);
listEl.appendChild(li);
}
}
async function load() {
try { render(await api("/api/todos")); }
catch (e) { statusEl.textContent = "불러오기 실패: " + e.message; statusEl.className = "err"; }
}
async function toggle(id, done) {
try { await api(`/api/todos/${id}`, {
method: "PATCH", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ done }) }); await load(); }
catch (e) { alert("변경 실패: " + e.message); await load(); }
}
async function remove(id) {
try { await api(`/api/todos/${id}`, { method: "DELETE" }); await load(); }
catch (e) { alert("삭제 실패: " + e.message); }
}
form.addEventListener("submit", async (ev) => {
ev.preventDefault();
const title = titleEl.value.trim();
if (!title) return;
const fd = new FormData();
fd.append("title", title);
if (fileEl.files[0]) fd.append("file", fileEl.files[0]);
addBtn.disabled = true;
try {
await api("/api/todos", { method: "POST", body: fd });
titleEl.value = ""; fileEl.value = "";
await load();
} catch (e) { alert("추가 실패: " + e.message); }
finally { addBtn.disabled = false; }
});
load();
</script>
</body>
</html>