Skip to content

Commit 5bfa365

Browse files
committed
버전 0.0.7: 학습 카드 정리 + DataFrame 표 출력 + 전역 터미널·댓글 + 사이드바/런처 수정
- 검증 시스템·예측 카드·빈 셀 플레이스홀더 제거; 섹션 개요 3열+상세설명 재배치 및 가독성 개선 - 셀 + 버튼을 에디터 박스 위/아래 모서리 고정, 실행 버튼 우측·아이콘만·모두실행; 탑바 제거 floating 컨트롤; 기본 언어 한국어 - polars/pandas DataFrame 구조화 직렬화 + 표 렌더; 전역 터미널(백엔드 PTY); 레슨별 Giscus 댓글 - 사이드바 펼침 상태 localStorage 영속+useMemo로 버벅임 제거; 커리큘럼 클릭 시 본문 갱신(remount); SW stale 캐시 빈 화면 방지 - 런처 더블클릭 시 창 안 뜨던 문제 수정(인자 없이 실행 시 창); 런처 0.2.1 - 랜딩 사이트 재설계; preflight 9/9 + 에디터·랜딩 빌드 통과
1 parent 26e6a53 commit 5bfa365

44 files changed

Lines changed: 2507 additions & 803 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,45 @@ in this file (see `docs/skills/ops/release/git-and-release.md`).
88

99
(next release accumulates here)
1010

11+
## 0.0.7 - 2026-05-31
12+
13+
학습 화면을 정리하고(검증 사족 제거·셀/섹션 배치 개선), polars DataFrame 출력을 깨진 HTML 덤프에서 표로 고치고,
14+
전역 터미널과 레슨별 댓글(Giscus)을 추가했다. 사이드바 네비게이션 버벅임을 없애고, 더블클릭으로 창이 안 뜨던 런처를 고쳤다.
15+
16+
### Added
17+
18+
- 전역 터미널: 좌측 사이드바 터미널 버튼 → 본문을 밀어 올리는 하단 패널(xterm.js)에 실제 로컬 셸 연결. 백엔드 PTY 추가(`src/codaro/api/terminalWebSocket.py`, `terminalRouter.py`; Windows는 pywinpty/ConPTY, POSIX는 `pty.fork`).
19+
- 레슨 하단 댓글 — GitHub Discussions 기반 Giscus(`editor/src/components/curriculum/lessonComments.tsx`, `editor/src/lib/giscusConfig.ts`). 레슨마다 `lesson:<category>/<contentId>` 토론에 매핑, 오프라인이면 안내로 graceful degrade.
20+
- DataFrame 표 렌더 컴포넌트(`editor/src/components/app/appPrimitives.tsx``DataFrameOutput`)와 사이드바 펼침 상태 영속 훅(`editor/src/hooks/useSidebarExpansionState.ts`).
21+
- 랜딩: 브랜드 아바타 이미지/웹폰트 자산 추가.
22+
23+
### Changed
24+
25+
- 에디터 탑바 제거 → floating 컨트롤(`topBar.tsx``TopControls`): SNS 아이콘을 최상단 우측으로, 본문/우측 패널이 세로 전체를 사용. 좌·우상단 아이콘 클러스터 갭 축소.
26+
- 노트북 셀: `+` 버튼을 코드/마크다운 에디터 박스의 위·아래 모서리에 고정(`notebookPanel.tsx`), 셀 간격 축소. 실행 버튼을 에디터 우측·아이콘만·"모두 실행"으로.
27+
- 커리큘럼 섹션 개요 재배치(이번 섹션에서 공부할 것·왜 유용한지·팁을 한 행 3열, 상세 설명은 단독 전체 폭 행) + 가독성 개선(아이콘 틴트 칩·좌측 액센트 보더·본문 대비 상향).
28+
- 시스템 기본 표시 언어를 한국어로(`useLocaleState.ts`, `localeCopy.ts`). 후원 카드 색상/디자인 보강.
29+
- PWA service worker(`editor/vite.config.ts`): `skipWaiting`/`clientsClaim`/`cleanupOutdatedCaches`로 재빌드 후 stale 캐시가 빈 화면을 만들던 문제 방지.
30+
- 랜딩 사이트 재설계(`landing/src/styles.css` 대폭 갱신, `App.jsx`·prerender 스크립트 갱신).
31+
- 런처 버전 0.2.0 → 0.2.1.
32+
33+
### Fixed
34+
35+
- 런처를 더블클릭하면 창이 안 뜨던 문제 — 인자 없이 실행하면 기본 launch로 네이티브 창을 띄우도록(`launcher/codaro-launcher/src/main.rs`, `command: Option<Command>` + `LaunchArgs::default()`).
36+
- 커리큘럼 사이드바에서 다른 레슨을 클릭해도 본문(코드 셀)이 안 바뀌던 문제 — 레슨 변경 시 `CurriculumView``key`로 remount(`mainSurface.tsx`).
37+
- 사이드바 네비게이션 때 펼쳐둔 카테고리가 초기화돼 버벅이던 문제 — 펼침 상태를 localStorage에 영속 + 트리 빌드 `useMemo`(`productSidebar.tsx`).
38+
- polars `DataFrame.head()` 출력이 `<style>`+이스케이프된 HTML 텍스트로 덤프되던 문제 — polars/pandas를 구조화 직렬화(`src/codaro/runtime/localWorker.py``_serializeDataFrame`)하고 프론트에서 표로 렌더.
39+
40+
### Removed
41+
42+
- 검증 시스템 전체 — "검증하기" 버튼, "검증 기준 / 실행 조건 / 확인할 것" 박스, 실습 헤더의 "확인:" 텍스트. 셀마다 비슷한 generic 보일러플레이트라 학습 카드를 어지럽혀 제거(실제 평가 로직은 백엔드에 남김).
43+
- 실행 전 예측 카드, 빈 코드 셀의 입력 플레이스홀더.
44+
45+
### Verification
46+
47+
- `uv run python -X utf8 tests/run.py preflight` → 9/9 게이트 통과(root-clean·docs·backend·widget-bridge·app-runtime·mobile-layout·editor-build·curriculum-quality-matrix·curriculum-executability).
48+
- 에디터 빌드(`npm --prefix editor run build`)와 랜딩 빌드(`npm --prefix landing run build`) 통과.
49+
1150
## 0.0.6 - 2026-05-31
1251

1352
Names and brands the launcher. The download is now `Codaro.exe` (was `CodaroLauncher.exe`)

editor/package-lock.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

editor/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
"@lezer/highlight": "^1.2.1",
2020
"@radix-ui/react-slot": "^1.2.3",
2121
"@radix-ui/react-tabs": "^1.1.13",
22+
"@xterm/addon-fit": "^0.10.0",
23+
"@xterm/xterm": "^5.5.0",
2224
"class-variance-authority": "^0.7.1",
2325
"clsx": "^2.1.1",
2426
"lucide-react": "^0.577.0",

editor/src/App.tsx

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import {
55
} from "@/lib/appBootstrap";
66
import { MainSurface } from "@/components/app/mainSurface";
77
import { ProductSidebar } from "@/components/app/productSidebar";
8-
import { TopBar } from "@/components/app/topBar";
8+
import { TopControls } from "@/components/app/topBar";
99
import { ProviderSettingsSheet } from "@/components/assistant/providerSettingsSheet";
10+
import { TerminalPanel } from "@/components/terminal/terminalPanel";
1011
import type { AutomationSection, SurfaceMode } from "@/lib/surfaceModel";
1112
import { useAppBootstrapEffect } from "@/hooks/useAppBootstrapEffect";
1213
import { useAssistantTurnState } from "@/hooks/useAssistantTurnState";
@@ -176,9 +177,11 @@ function App() {
176177
const {
177178
canRun,
178179
currentResult,
180+
notebookRunning,
179181
resetRuntimeState,
180182
results,
181183
runBlock,
184+
runNotebook,
182185
runningBlockId,
183186
sessionId,
184187
setSessionId,
@@ -297,17 +300,13 @@ function App() {
297300
setSurface(nextSurface);
298301
}, [categories, selectCurriculumCategory, selectedCategory, setSurface]);
299302

300-
const runActiveNotebookBlock = useCallback(() => {
301-
if (selectedBlock) {
302-
runBlock(selectedBlock);
303-
}
304-
}, [runBlock, selectedBlock]);
305-
306303
const copyDiagnosticExport = useCallback(async () => {
307304
const payload = await codaroApi.systemDiagnosticsExport();
308305
await writeClipboardText(JSON.stringify(payload, null, 2));
309306
}, []);
310307

308+
const [terminalOpen, setTerminalOpen] = useState(false);
309+
311310
return (
312311
<LocaleProvider value={localeState}>
313312
<WidgetSessionProvider sessionId={sessionId}>
@@ -337,23 +336,20 @@ function App() {
337336
onDeleteCustomCurriculum={deleteCustomCurriculum}
338337
onSurfaceChange={selectSurface}
339338
onToggleTheme={toggleThemeMode}
339+
terminalOpen={terminalOpen}
340+
onToggleTerminal={() => setTerminalOpen((current) => !current)}
340341
/>
341342

342-
<SidebarInset className="h-svh min-h-0 min-w-0 overflow-hidden">
343-
<div className="flex h-full min-h-0 flex-col">
344-
<TopBar
345-
assistantCollapsed={assistantCollapsed}
346-
canRun={canRun}
347-
loadState={loadState}
348-
notebookRunning={Boolean(runningBlockId)}
349-
notice={notice}
350-
showSidebarTrigger
351-
surface={surface}
352-
onCopyDiagnosticExport={copyDiagnosticExport}
353-
onRunNotebook={runActiveNotebookBlock}
354-
onToggleAssistant={() => setAssistantCollapsed((current) => !current)}
355-
/>
356-
<div className="min-h-0 flex-1">
343+
<SidebarInset className="relative flex h-svh min-h-0 min-w-0 flex-col overflow-hidden">
344+
<TopControls
345+
assistantCollapsed={assistantCollapsed}
346+
notice={notice}
347+
showSidebarTrigger
348+
surface={surface}
349+
onCopyDiagnosticExport={copyDiagnosticExport}
350+
onToggleAssistant={() => setAssistantCollapsed((current) => !current)}
351+
/>
352+
<div className="min-h-0 flex-1">
357353
<MainSurface
358354
aiConnecting={aiConnecting}
359355
aiProfile={aiProfile}
@@ -397,7 +393,9 @@ function App() {
397393
onRefreshAutomation={refreshAutomation}
398394
onRenameDocument={renameNotebookDocument}
399395
onOpenSharePackCurriculum={openSharePackCurriculum}
396+
notebookRunning={notebookRunning}
400397
onRunBlock={runBlock}
398+
onRunNotebook={runNotebook}
401399
onRunTask={runTask}
402400
onSelectBlock={selectBlock}
403401
onSelectCurriculumBlock={setSelectedCurriculumBlockId}
@@ -407,8 +405,12 @@ function App() {
407405
}}
408406
onToggleEStop={toggleEStop}
409407
/>
410-
</div>
411408
</div>
409+
{terminalOpen ? (
410+
<div className="h-72 min-h-0 shrink-0">
411+
<TerminalPanel themeMode={themeMode} onClose={() => setTerminalOpen(false)} />
412+
</div>
413+
) : null}
412414
</SidebarInset>
413415

414416
<ProviderSettingsSheet

editor/src/components/app/appPrimitives.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export function ExecutionOutput({
106106
const packageError = result.status === "package-error";
107107
const hasError = packageError || result.status === "error" || Boolean(result.stderr);
108108
const widgetDescriptor = !hasError && isWidgetDescriptor(result.data) ? result.data : null;
109+
const dataframeData = !widgetDescriptor && !hasError && result.type === "dataframe" ? asDataFramePayload(result.data) : null;
109110
const output = result.stderr || result.stdout || stringifyData(result.data) || t("runtime.noOutput");
110111
return (
111112
<div
@@ -127,6 +128,8 @@ export function ExecutionOutput({
127128
descriptor={widgetDescriptor}
128129
/>
129130
</div>
131+
) : dataframeData ? (
132+
<DataFrameOutput data={dataframeData} />
130133
) : (
131134
<ScrollArea className="max-h-72">
132135
<pre className="whitespace-pre-wrap font-mono text-sm leading-6">{output}</pre>
@@ -152,6 +155,63 @@ export function ExecutionOutput({
152155
);
153156
}
154157

158+
type DataFramePayload = {
159+
columns: string[];
160+
rows: Array<Record<string, unknown>>;
161+
totalRows?: number;
162+
truncated?: boolean;
163+
};
164+
165+
function asDataFramePayload(data: unknown): DataFramePayload | null {
166+
if (!data || typeof data !== "object") return null;
167+
const record = data as { columns?: unknown; rows?: unknown };
168+
if (!Array.isArray(record.columns) || !Array.isArray(record.rows)) return null;
169+
return {
170+
columns: record.columns.map((column) => String(column)),
171+
rows: record.rows as Array<Record<string, unknown>>,
172+
totalRows: typeof (data as { totalRows?: unknown }).totalRows === "number" ? (data as { totalRows: number }).totalRows : undefined,
173+
truncated: Boolean((data as { truncated?: unknown }).truncated),
174+
};
175+
}
176+
177+
function formatDataFrameCell(value: unknown): string {
178+
if (value === null || value === undefined) return "";
179+
if (typeof value === "object") return JSON.stringify(value);
180+
return String(value);
181+
}
182+
183+
function DataFrameOutput({ data }: { data: DataFramePayload }) {
184+
const { columns, rows } = data;
185+
const totalRows = data.totalRows ?? rows.length;
186+
return (
187+
<div className="overflow-hidden rounded-md border bg-background" data-execution-output-mode="dataframe">
188+
<div className="max-h-72 overflow-auto">
189+
<table className="w-full border-collapse text-left text-xs">
190+
<thead className="sticky top-0 bg-muted/70">
191+
<tr>
192+
{columns.map((column) => (
193+
<th key={column} className="whitespace-nowrap border-b px-2 py-1 font-medium">{column}</th>
194+
))}
195+
</tr>
196+
</thead>
197+
<tbody>
198+
{rows.map((row, rowIndex) => (
199+
<tr key={rowIndex} className="odd:bg-muted/20">
200+
{columns.map((column) => (
201+
<td key={column} className="whitespace-nowrap px-2 py-1 font-mono">{formatDataFrameCell(row[column])}</td>
202+
))}
203+
</tr>
204+
))}
205+
</tbody>
206+
</table>
207+
</div>
208+
<div className="border-t bg-muted/30 px-2 py-1 text-[10px] text-muted-foreground">
209+
shape: ({totalRows}, {columns.length}){data.truncated ? ` · 상위 ${rows.length}행 표시` : ""}
210+
</div>
211+
</div>
212+
);
213+
}
214+
155215
export function Metric({ label, value, tone = "default" }: { label: string; value: string; tone?: "default" | "warning" }) {
156216
return (
157217
<Card className={tone === "warning" ? "bg-muted/40" : ""}>

editor/src/components/app/mainSurface.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type MainSurfaceProps = {
4949
eStop: EStopStatus;
5050
loadState: LoadState;
5151
messages: AssistantMessage[];
52+
notebookRunning: boolean;
5253
pendingBlocks: BlockConfig[];
5354
prompt: string;
5455
referenceLoading: boolean;
@@ -75,6 +76,7 @@ type MainSurfaceProps = {
7576
onRenameDocument: (title: string) => void;
7677
onRejectPendingBlocks: () => void;
7778
onRunBlock: (block: BlockConfig) => void;
79+
onRunNotebook: () => void;
7880
onRunTask: (task: TaskDefinition) => void;
7981
onSelectBlock: (blockId: string) => void;
8082
onSelectCurriculumBlock: (blockId: string) => void;
@@ -125,6 +127,7 @@ function MainSurfaceContent(props: MainSurfaceProps) {
125127
cellHelpByBlockId={props.cellHelpByBlockId}
126128
document={props.document}
127129
drafts={props.drafts}
130+
notebookRunning={props.notebookRunning}
128131
pendingBlocks={props.pendingBlocks}
129132
results={props.results}
130133
runningBlockId={props.runningBlockId}
@@ -137,6 +140,7 @@ function MainSurfaceContent(props: MainSurfaceProps) {
137140
onRenameDocument={props.onRenameDocument}
138141
onRejectPendingBlocks={props.onRejectPendingBlocks}
139142
onRunBlock={props.onRunBlock}
143+
onRunNotebook={props.onRunNotebook}
140144
onSelectBlock={props.onSelectBlock}
141145
/>
142146
{props.assistantCollapsed ? null : (
@@ -210,6 +214,7 @@ function MainSurfaceContent(props: MainSurfaceProps) {
210214
)}
211215
>
212216
<CurriculumView
217+
key={`${props.selectedCategory}/${props.selectedContentId}`}
213218
apiOnline={props.apiOnline}
214219
canRun={props.canRun}
215220
cellHelpByBlockId={props.cellHelpByBlockId}
@@ -227,13 +232,7 @@ function MainSurfaceContent(props: MainSurfaceProps) {
227232
renderCodeCellEditor={({ autoFocus = true, block, draft, onChange, onFocus, onRun }) => (
228233
<CodeCellEditor
229234
autoFocus={autoFocus}
230-
placeholderText={
231-
block.role === "snippet"
232-
? t("cell.placeholder.snippet")
233-
: block.role === "exercise"
234-
? "Python 코드를 입력하세요."
235-
: t("cell.placeholder.code")
236-
}
235+
placeholderText=""
237236
value={draft}
238237
onChange={onChange}
239238
onFocus={onFocus}

0 commit comments

Comments
 (0)