AI/ML Python 실습 클래스 (Chapter 01~20)
-각 챕터를 선택하고 실행 버튼을 눌러 FastAPI 백엔드에서 실습 코드를 실행하세요.
-실행 결과
-아직 실행한 결과가 없습니다.-
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 @@
각 챕터를 선택하고 실행 버튼을 눌러 FastAPI 백엔드에서 실습 코드를 실행하세요.
-아직 실행한 결과가 없습니다.-
좌측 메뉴에서 챕터를 열고 실행해 코드와 결과를 동시에 확인하세요.
+챕터를 선택해 소스를 확인하세요.
+아직 선택된 챕터가 없습니다.+
선택한 챕터의 run() 실행 결과입니다.
+아직 실행한 결과가 없습니다.+