Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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*")):
Expand Down Expand Up @@ -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")
Expand Down
58 changes: 51 additions & 7 deletions frontend/app.js
Original file line number Diff line number Diff line change
@@ -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");
Expand All @@ -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" });
Expand Down
68 changes: 56 additions & 12 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,64 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI/ML Basic Class Lab</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/static/styles.css" />
</head>
<body>
<main class="container">
<h1>AI/ML Python 실습 클래스 (Chapter 01~20)</h1>
<p>각 챕터를 선택하고 <strong>실행</strong> 버튼을 눌러 FastAPI 백엔드에서 실습 코드를 실행하세요.</p>
<section>
<ul id="chapter-list"></ul>
</section>
<section>
<h2>실행 결과</h2>
<pre id="result">아직 실행한 결과가 없습니다.</pre>
</section>
</main>
<body class="bg-slate-100 text-slate-900">
<div class="min-h-screen">
<header class="sticky top-0 z-30 border-b border-slate-200 bg-white/90 backdrop-blur">
<div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-3 sm:px-6 lg:px-8">
<div>
<h1 class="text-xl font-bold sm:text-2xl">AI/ML Python 실습 클래스</h1>
<p class="text-sm text-slate-600">좌측 메뉴에서 챕터를 열고 실행해 코드와 결과를 동시에 확인하세요.</p>
</div>
<button
id="open-menu"
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-blue-700"
type="button"
>
메뉴 열기
</button>
</div>
</header>

<main class="mx-auto max-w-7xl px-4 py-5 sm:px-6 lg:px-8">
<div class="grid min-h-[70vh] grid-cols-1 gap-4 lg:grid-cols-2">
<section class="rounded-xl border border-slate-200 bg-white shadow-sm">
<div class="border-b border-slate-200 px-4 py-3">
<h2 class="font-semibold">Python 소스</h2>
<p id="selected-chapter" class="text-sm text-slate-500">챕터를 선택해 소스를 확인하세요.</p>
</div>
<pre id="source" class="h-[58vh] overflow-auto p-4 text-sm leading-6">아직 선택된 챕터가 없습니다.</pre>
</section>

<section class="rounded-xl border border-slate-200 bg-white shadow-sm">
<div class="border-b border-slate-200 px-4 py-3">
<h2 class="font-semibold">실행 결과</h2>
<p class="text-sm text-slate-500">선택한 챕터의 run() 실행 결과입니다.</p>
</div>
<pre id="result" class="h-[58vh] overflow-auto p-4 text-sm leading-6">아직 실행한 결과가 없습니다.</pre>
</section>
</div>
</main>
</div>

<div id="overlay" class="fixed inset-0 z-40 hidden bg-slate-950/40"></div>

<aside
id="offcanvas"
class="fixed left-0 top-0 z-50 h-full w-[min(90vw,400px)] -translate-x-full border-r border-slate-200 bg-white shadow-2xl transition-transform duration-300"
aria-hidden="true"
>
<div class="flex items-center justify-between border-b border-slate-200 px-4 py-3">
<h2 class="font-bold">챕터 메뉴</h2>
<button id="close-menu" class="rounded-md px-2 py-1 text-slate-600 hover:bg-slate-100" type="button">닫기</button>
</div>
<div class="p-3">
<ul id="chapter-list" class="space-y-2"></ul>
</div>
</aside>

<script src="/static/app.js"></script>
</body>
</html>
62 changes: 30 additions & 32 deletions frontend/styles.css
Original file line number Diff line number Diff line change
@@ -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;
}
Loading