|
| 1 | +const partFilter = document.getElementById("partFilter"); |
| 2 | +const langFilter = document.getElementById("langFilter"); |
| 3 | +const searchInput = document.getElementById("searchInput"); |
| 4 | +const partsRoot = document.getElementById("parts"); |
| 5 | +const statsRoot = document.getElementById("stats"); |
| 6 | +const tpl = document.getElementById("problemTemplate"); |
| 7 | + |
| 8 | +const MEMORY_SCHEDULE = [1, 3, 7]; |
| 9 | + |
| 10 | +function getReviewTag(problemId) { |
| 11 | + const key = `review:${problemId}`; |
| 12 | + const lastRaw = localStorage.getItem(key); |
| 13 | + if (!lastRaw) { |
| 14 | + return { label: "Not Started", tone: "new", next: "Start today" }; |
| 15 | + } |
| 16 | + const last = new Date(lastRaw); |
| 17 | + const days = Math.floor((Date.now() - last.getTime()) / (1000 * 60 * 60 * 24)); |
| 18 | + let next = MEMORY_SCHEDULE.find((d) => d > days); |
| 19 | + if (!next) { |
| 20 | + return { label: "Review Due", tone: "due", next: "Review now" }; |
| 21 | + } |
| 22 | + return { label: "In Loop", tone: "loop", next: `D+${next}` }; |
| 23 | +} |
| 24 | + |
| 25 | +function retrievalPrompt(problem) { |
| 26 | + return [ |
| 27 | + `Q1. ${problem.algorithm}를 떠올리지 않고 30초 내로 접근 전략을 말할 수 있나요?`, |
| 28 | + `Q2. ${problem.name}를 실제 시스템에서 어디에 적용할지 1개 사례를 답해보세요.`, |
| 29 | + `Q3. ${problem.architectView}를 시간/공간 복잡도와 연결해 설명해보세요.` |
| 30 | + ].join(" "); |
| 31 | +} |
| 32 | + |
| 33 | +function toGitHubLink(path) { |
| 34 | + if (!path) return null; |
| 35 | + return `../${path}`; |
| 36 | +} |
| 37 | + |
| 38 | +function problemMatches(problem, filters) { |
| 39 | + const q = filters.query; |
| 40 | + const matchesQuery = !q |
| 41 | + || `${problem.id} ${problem.name} ${problem.algorithm} ${problem.architectView}`.toLowerCase().includes(q); |
| 42 | + |
| 43 | + if (!matchesQuery) return false; |
| 44 | + if (filters.part !== "all" && String(problem.part) !== filters.part) return false; |
| 45 | + if (filters.lang === "python" && !problem.pythonAvailable) return false; |
| 46 | + if (filters.lang === "java" && !problem.javaAvailable) return false; |
| 47 | + return true; |
| 48 | +} |
| 49 | + |
| 50 | +function statCard(label, value) { |
| 51 | + const el = document.createElement("article"); |
| 52 | + const strong = document.createElement("strong"); |
| 53 | + strong.textContent = value; |
| 54 | + const p = document.createElement("p"); |
| 55 | + p.textContent = label; |
| 56 | + p.style.margin = "0.15rem 0 0"; |
| 57 | + p.style.color = "var(--muted)"; |
| 58 | + p.style.fontSize = "0.86rem"; |
| 59 | + el.append(strong, p); |
| 60 | + return el; |
| 61 | +} |
| 62 | + |
| 63 | +function renderStats(data) { |
| 64 | + statsRoot.innerHTML = ""; |
| 65 | + const s = data.stats; |
| 66 | + const values = [ |
| 67 | + ["Total Problems", String(s.problemCount)], |
| 68 | + ["Parts", String(s.partCount)], |
| 69 | + ["Python Coverage", `${s.pythonCount}/${s.problemCount}`], |
| 70 | + ["Java Coverage", `${s.javaCount}/${s.problemCount}`], |
| 71 | + ]; |
| 72 | + values.forEach(([label, value]) => statsRoot.appendChild(statCard(label, value))); |
| 73 | +} |
| 74 | + |
| 75 | +function renderPartOptions(parts) { |
| 76 | + parts.forEach((part) => { |
| 77 | + const opt = document.createElement("option"); |
| 78 | + opt.value = String(part.part); |
| 79 | + opt.textContent = `Part ${part.part} - ${part.title}`; |
| 80 | + partFilter.appendChild(opt); |
| 81 | + }); |
| 82 | +} |
| 83 | + |
| 84 | +function render(data) { |
| 85 | + const filters = { |
| 86 | + part: partFilter.value, |
| 87 | + lang: langFilter.value, |
| 88 | + query: searchInput.value.trim().toLowerCase(), |
| 89 | + }; |
| 90 | + |
| 91 | + partsRoot.innerHTML = ""; |
| 92 | + data.parts.forEach((part) => { |
| 93 | + const enriched = part.problems.map((p) => ({ ...p, part: part.part })); |
| 94 | + const filtered = enriched.filter((p) => problemMatches(p, filters)); |
| 95 | + if (!filtered.length) return; |
| 96 | + |
| 97 | + const details = document.createElement("details"); |
| 98 | + details.className = "part"; |
| 99 | + details.open = true; |
| 100 | + |
| 101 | + const summary = document.createElement("summary"); |
| 102 | + const left = document.createElement("div"); |
| 103 | + const right = document.createElement("div"); |
| 104 | + left.className = "part-title"; |
| 105 | + right.className = "part-meta"; |
| 106 | + left.textContent = `Part ${part.part}. ${part.title}`; |
| 107 | + right.textContent = `${filtered.length} / ${part.problems.length} problems`; |
| 108 | + summary.append(left, right); |
| 109 | + |
| 110 | + const grid = document.createElement("div"); |
| 111 | + grid.className = "problem-grid"; |
| 112 | + |
| 113 | + filtered.forEach((problem) => { |
| 114 | + const node = tpl.content.firstElementChild.cloneNode(true); |
| 115 | + node.querySelector(".pid").textContent = problem.id; |
| 116 | + node.querySelector("h4").textContent = problem.name; |
| 117 | + node.querySelector(".algo").textContent = `Pattern: ${problem.algorithm}`; |
| 118 | + |
| 119 | + const rt = getReviewTag(problem.id); |
| 120 | + node.querySelector(".arch").textContent = `${problem.architectView} | ${rt.label} (${rt.next})`; |
| 121 | + node.querySelector(".prompt").textContent = retrievalPrompt(problem); |
| 122 | + |
| 123 | + const links = node.querySelector(".links"); |
| 124 | + const py = toGitHubLink(problem.pythonPath); |
| 125 | + const ja = toGitHubLink(problem.javaPath); |
| 126 | + |
| 127 | + if (py) { |
| 128 | + const a = document.createElement("a"); |
| 129 | + a.href = py; |
| 130 | + a.textContent = "Python"; |
| 131 | + a.className = "active"; |
| 132 | + a.target = "_blank"; |
| 133 | + a.rel = "noopener"; |
| 134 | + links.appendChild(a); |
| 135 | + } |
| 136 | + |
| 137 | + if (ja) { |
| 138 | + const a = document.createElement("a"); |
| 139 | + a.href = ja; |
| 140 | + a.textContent = "Java"; |
| 141 | + a.className = "active"; |
| 142 | + a.target = "_blank"; |
| 143 | + a.rel = "noopener"; |
| 144 | + links.appendChild(a); |
| 145 | + } |
| 146 | + |
| 147 | + const mark = document.createElement("a"); |
| 148 | + mark.href = "#"; |
| 149 | + mark.textContent = "Mark Reviewed"; |
| 150 | + mark.addEventListener("click", (e) => { |
| 151 | + e.preventDefault(); |
| 152 | + localStorage.setItem(`review:${problem.id}`, new Date().toISOString()); |
| 153 | + render(data); |
| 154 | + }); |
| 155 | + links.appendChild(mark); |
| 156 | + |
| 157 | + grid.appendChild(node); |
| 158 | + }); |
| 159 | + |
| 160 | + details.append(summary, grid); |
| 161 | + partsRoot.appendChild(details); |
| 162 | + }); |
| 163 | +} |
| 164 | + |
| 165 | +async function init() { |
| 166 | + const res = await fetch("data/problems.json"); |
| 167 | + const data = await res.json(); |
| 168 | + renderStats(data); |
| 169 | + renderPartOptions(data.parts); |
| 170 | + render(data); |
| 171 | + |
| 172 | + [partFilter, langFilter, searchInput].forEach((el) => { |
| 173 | + el.addEventListener("input", () => render(data)); |
| 174 | + el.addEventListener("change", () => render(data)); |
| 175 | + }); |
| 176 | +} |
| 177 | + |
| 178 | +init().catch((error) => { |
| 179 | + partsRoot.innerHTML = `<p>Failed to load data: ${error.message}</p>`; |
| 180 | +}); |
0 commit comments