From 91b82878a9ca46c7951a1660673649454b9eb2fc Mon Sep 17 00:00:00 2001 From: Kim Do Young <41275565+edumgt@users.noreply.github.com> Date: Sat, 28 Feb 2026 12:25:24 +0900 Subject: [PATCH] feat: redesign frontend with offcanvas chapter menu --- backend/app/main.py | 15 ++++++++++ frontend/app.js | 58 +++++++++++++++++++++++++++++++++----- frontend/index.html | 68 +++++++++++++++++++++++++++++++++++++-------- frontend/styles.css | 62 ++++++++++++++++++++--------------------- 4 files changed, 152 insertions(+), 51 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 8d86a26..78b4236 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -35,6 +35,11 @@ class ChapterRunResponse(BaseModel): result: dict[str, Any] +class ChapterSourceResponse(BaseModel): + chapter: str + source: str + + def load_chapters() -> list[ChapterSummary]: items: list[ChapterSummary] = [] for chapter_dir in sorted(CHAPTERS_DIR.glob("chapter*")): @@ -79,6 +84,16 @@ def run_chapter(chapter_id: str) -> ChapterRunResponse: return ChapterRunResponse(chapter=chapter_id, result=result) +@app.get("/api/chapters/{chapter_id}/source", response_model=ChapterSourceResponse) +def chapter_source(chapter_id: str) -> ChapterSourceResponse: + chapter_path = CHAPTERS_DIR / chapter_id / "practice.py" + if not chapter_path.exists(): + raise HTTPException(status_code=404, detail="chapter not found") + + source = chapter_path.read_text(encoding="utf-8") + return ChapterSourceResponse(chapter=chapter_id, source=source) + + @app.get("/") def index() -> FileResponse: return FileResponse(FRONTEND_DIR / "index.html") diff --git a/frontend/app.js b/frontend/app.js index f95c244..b71c2ef 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -1,5 +1,22 @@ const listElement = document.getElementById("chapter-list"); const resultElement = document.getElementById("result"); +const sourceElement = document.getElementById("source"); +const selectedChapterElement = document.getElementById("selected-chapter"); + +const offcanvas = document.getElementById("offcanvas"); +const overlay = document.getElementById("overlay"); +const openMenuBtn = document.getElementById("open-menu"); +const closeMenuBtn = document.getElementById("close-menu"); + +function setMenuOpen(isOpen) { + offcanvas.classList.toggle("-translate-x-full", !isOpen); + overlay.classList.toggle("hidden", !isOpen); + offcanvas.setAttribute("aria-hidden", String(!isOpen)); +} + +openMenuBtn.addEventListener("click", () => setMenuOpen(true)); +closeMenuBtn.addEventListener("click", () => setMenuOpen(false)); +overlay.addEventListener("click", () => setMenuOpen(false)); async function loadChapters() { const res = await fetch("/api/chapters"); @@ -8,21 +25,48 @@ async function loadChapters() { listElement.innerHTML = ""; chapters.forEach((chapter) => { const li = document.createElement("li"); - li.className = "chapter-item"; - const title = document.createElement("span"); + const item = document.createElement("div"); + item.className = "chapter-btn"; + + const title = document.createElement("button"); + title.className = "chapter-label"; title.textContent = `${chapter.id} · ${chapter.title}`; + title.type = "button"; + title.onclick = async () => { + await showSource(chapter.id, chapter.title); + setMenuOpen(false); + }; - const button = document.createElement("button"); - button.textContent = "실행"; - button.onclick = async () => runChapter(chapter.id); + const runButton = document.createElement("button"); + runButton.className = "chapter-run"; + runButton.textContent = "실행"; + runButton.type = "button"; + runButton.onclick = async () => { + await showSource(chapter.id, chapter.title); + await runChapter(chapter.id); + setMenuOpen(false); + }; - li.appendChild(title); - li.appendChild(button); + item.appendChild(title); + item.appendChild(runButton); + li.appendChild(item); listElement.appendChild(li); }); } +async function showSource(chapterId, chapterTitle) { + selectedChapterElement.textContent = `${chapterId} · ${chapterTitle}`; + sourceElement.textContent = "소스 로딩 중..."; + const res = await fetch(`/api/chapters/${chapterId}/source`); + if (!res.ok) { + sourceElement.textContent = "소스를 불러오지 못했습니다."; + return; + } + const data = await res.json(); + sourceElement.textContent = data.source; +} + async function runChapter(chapterId) { resultElement.textContent = "실행 중..."; const res = await fetch(`/api/chapters/${chapterId}/run`, { method: "POST" }); diff --git a/frontend/index.html b/frontend/index.html index c7f931a..84e34cc 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,20 +4,64 @@ AI/ML Basic Class Lab + - -
-

AI/ML Python 실습 클래스 (Chapter 01~20)

-

각 챕터를 선택하고 실행 버튼을 눌러 FastAPI 백엔드에서 실습 코드를 실행하세요.

-
- -
-
-

실행 결과

-
아직 실행한 결과가 없습니다.
-
-
+ +
+
+
+
+

AI/ML Python 실습 클래스

+

좌측 메뉴에서 챕터를 열고 실행해 코드와 결과를 동시에 확인하세요.

+
+ +
+
+ +
+
+
+
+

Python 소스

+

챕터를 선택해 소스를 확인하세요.

+
+
아직 선택된 챕터가 없습니다.
+
+ +
+
+

실행 결과

+

선택한 챕터의 run() 실행 결과입니다.

+
+
아직 실행한 결과가 없습니다.
+
+
+
+
+ + + + + diff --git a/frontend/styles.css b/frontend/styles.css index 66e111c..5933389 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -1,47 +1,45 @@ -body { - font-family: Arial, sans-serif; +pre { margin: 0; - background: #f5f7fb; - color: #1f2937; + background: #0f172a; + color: #e2e8f0; + border-radius: 0 0 0.75rem 0.75rem; } -.container { - max-width: 1000px; - margin: 0 auto; - padding: 24px; +.chapter-btn { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + border: 1px solid #cbd5e1; + border-radius: 0.75rem; + padding: 0.75rem; + background: #ffffff; + transition: all 0.2s ease; } -#chapter-list { - list-style: none; - padding: 0; - display: grid; - gap: 10px; +.chapter-btn:hover { + border-color: #2563eb; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); } -.chapter-item { - display: flex; - justify-content: space-between; - align-items: center; - background: #ffffff; - border: 1px solid #d1d5db; - border-radius: 8px; - padding: 12px; +.chapter-label { + text-align: left; + color: #0f172a; + font-weight: 600; + font-size: 0.92rem; } -button { +.chapter-run { border: none; + border-radius: 0.5rem; background: #2563eb; - color: white; - border-radius: 6px; - padding: 6px 12px; + color: #ffffff; + font-weight: 700; + padding: 0.45rem 0.8rem; cursor: pointer; } -pre { - background: #111827; - color: #e5e7eb; - border-radius: 8px; - padding: 16px; - min-height: 200px; - overflow-x: auto; +.chapter-run:hover { + background: #1d4ed8; }