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
1 change: 1 addition & 0 deletions .github/workflows/slides.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
node-version: 22
cache: npm
- run: npm ci
- run: npm test
# 토큰이 테마에 반영됐는지 확인 (tokens.json만 고치고 sync를 잊은 PR 방지)
- run: npm run tokens && git diff --exit-code tokens/tokens.css themes/causal-lab.css
- run: npm run build
Expand Down
12 changes: 12 additions & 0 deletions SLIDES.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ GitHub Actions가 빌드해서 GitHub Pages에 올립니다.
- `slides/<덱 폴더>/`에는 **`index.md`만** 둡니다. 이미지는 `img/`에 둡니다.
- 챕터 노트와 과제 같은 일반 `.md`는 `chapters/`와 `assignments/`에 둡니다. 이 폴더들은 빌드 입력이 아니며 HTML로 자동 변환되지 않습니다. GitHub 문서 화면에서 읽습니다.
- Pages의 덱 목록 페이지는 `slides/<덱 폴더>/index.md`가 있는 폴더를 찾아 만듭니다. 이 파일 이름이 다르면 목록에 나타나지 않습니다.

## 파일 배치 자동 검사

PR의 CI와 HTML·PDF·OT·보고서 빌드, 로컬 미리보기 시작 전에 같은 파일 배치 검사를 실행합니다.

- 발표 폴더에는 파일 이름이 정확히 `index.md`인 파일이 있어야 합니다.
- `slides/<발표 폴더>/index.md` 외의 마크다운은 허용하지 않습니다. `README.md`, `INDEX.MD`, `img/notes.md`나 `.markdown`·`.mdown`·`.markdn` 파일도 차단합니다.
- `chapters/`와 `assignments/`의 `README.md`, 이미지·CSS·JavaScript 같은 발표 보조 파일은 이 규칙에 걸리지 않습니다.

오류에는 잘못된 파일 경로와 수정 방법이 나옵니다. 발표 파일은 `index.md`로 바꾸고, 일반 노트·과제는 해당 폴더로 옮기는 변경을 제안하세요. AI는 파일을 임의로 삭제하지 않고 사용자가 의도한 내용을 확인합니다. 같은 PR 브랜치에서 수정·commit하면 CI가 다시 검사합니다.

Node가 있는 로컬 환경에서는 `npm run check:slides`로 검사만 실행할 수 있습니다. 검증 스모크 테스트는 `npm test`입니다. GitHub 웹만 쓰는 참여자는 설치할 필요 없이 PR의 CI 결과를 확인하면 됩니다.
- Pages에는 PR이 main에 머지된 뒤에 올라갑니다. PR을 열었다고 Pages에 바로 보이지 않습니다.

## 발표 자료 올리기
Expand Down
1 change: 1 addition & 0 deletions TUTOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ GitHub에서 파일을 내려받고, 내 폴더에 작성하고, Pull Request(PR
- 직접 해볼지 작성·제출을 도와줄지는 처음에 한 번 정하고, 내가 바꾸자고 하면 그때 바꿔. 선택을 묻기 전에 네 도구로 어디까지 할 수 있는지 설명해. 도구가 없으면 초안·파일 내용·PR 제목과 설명을 만들고 클릭은 내가 해야 한다고 알려줘. GitHub 쓰기나 브라우저 조작 도구가 실제로 있으면 내가 맡긴 범위에서 업로드·commit·PR 생성을 할 수 있지만 로그인·인증은 내가 한다고 알려줘.
- 내가 이미 알려준 역할·주차·경험·선호는 다시 묻지 마. 직접 학습을 선택했다면 클릭을 대신하지 말고, 작성 지원을 선택했더라도 내 풀이와 근거를 받기 전에 확인하지 않은 결과를 만들어내지 마.
- 파일 추가 메뉴는 화면에 따라 파일 목록 주변의 + 버튼 안에 Add file이 있거나, Add file이 바로 보일 수 있어. 메뉴를 열어 Create new file 또는 Upload files를 찾도록 안내하고, 표시가 다르면 실제 보이는 문구부터 물어봐.
- 제출 전에 아래 경로 표에 맞는 파일 이름인지 확인해. 슬라이드는 slides/<발표 폴더>/index.md 하나만 두고 다른 마크다운을 함께 넣지 마. 일반 노트·러너 기록은 chapters/ 또는 assignments/의 README.md로 안내해. CI가 파일 배치 오류를 내면 경로와 오류를 읽고 같은 PR에서 수정하게 도와줘. 파일을 임의로 삭제하거나 검사 우회를 안내하지 마.

[공통 경로와 제출 완료 조건]
- 러너 과제: templates/runner-record.md 내용을 assignments/<주차>/<github-id>/README.md 에 작성 (예: assignments/w03/<github-id>/README.md)
Expand Down
14 changes: 8 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
"private": true,
"type": "module",
"scripts": {
"test": "node --test scripts/check-slides.test.mjs",
"check:slides": "node scripts/check-slides.mjs",
"tokens": "node scripts/sync-tokens.mjs",
"dev": "marp --no-stdin --server ./slides",
"build:html": "marp --no-stdin",
"build:pdf": "marp --no-stdin --pdf",
"build:reports": "node scripts/build-index.mjs",
"build": "npm run tokens && npm run build:html && npm run build:pdf && node scripts/copy-assets.mjs && npm run build:ot-live && node scripts/build-index.mjs",
"build:ot-live": "node slides/ot-live/build-live.mjs"
"dev": "npm run check:slides && marp --no-stdin --server ./slides",
"build:html": "npm run check:slides && marp --no-stdin",
"build:pdf": "npm run check:slides && marp --no-stdin --pdf",
"build:reports": "npm run check:slides && node scripts/build-index.mjs",
"build": "npm run check:slides && npm run tokens && npm run build:html && npm run build:pdf && node scripts/copy-assets.mjs && npm run build:ot-live && node scripts/build-index.mjs",
"build:ot-live": "npm run check:slides && node slides/ot-live/build-live.mjs"
},
"devDependencies": {
"@marp-team/marp-cli": "^4.5.1"
Expand Down
32 changes: 32 additions & 0 deletions scripts/check-slides.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { existsSync, readdirSync, statSync } from 'node:fs';
import { join, sep } from 'node:path';

const errors = [];
for (const entry of readdirSync('slides', { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const index = `slides/${entry.name}/index.md`;
if (!existsSync(index) || !statSync(index).isFile()) {
errors.push(`${index}: 발표 폴더에는 index.md가 필요합니다.`);
}
}

function check(directory) {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const path = join(directory, entry.name);
if (entry.isDirectory()) check(path);
else if (/\.(md|mdown|markdown|markdn)$/i.test(entry.name)) {
const relative = path.split(sep).join('/');
if (!/^slides\/[^/]+\/index\.md$/.test(relative)) {
errors.push(`${relative}: 마크다운은 slides/<발표 폴더>/index.md만 허용합니다.`);
}
}
}
}
check('slides');

if (errors.length) {
console.error('슬라이드 파일 배치를 확인하세요:\n' + errors.map(error => `- ${error}`).join('\n'));
console.error('발표 파일은 index.md로 저장하고, 일반 노트·과제는 chapters/ 또는 assignments/에 두세요.');
process.exit(1);
}
console.log('슬라이드 파일 배치 검사 통과');
80 changes: 80 additions & 0 deletions scripts/check-slides.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import assert from 'node:assert/strict';
import { cpSync, existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import test from 'node:test';

const root = fileURLToPath(new URL('../', import.meta.url));
const deck = '---\nmarp: true\n---\n# Smoke test\n';

function build(files, command = 'build:html') {
const cwd = mkdtempSync(join(tmpdir(), 'slide-layout-'));
try {
cpSync(join(root, 'package.json'), join(cwd, 'package.json'));
cpSync(join(root, 'scripts'), join(cwd, 'scripts'), { recursive: true });
writeFileSync(join(cwd, '.marprc.yml'), 'inputDir: ./slides\noutput: ./dist\n');
for (const [name, content] of Object.entries(files)) {
mkdirSync(dirname(join(cwd, name)), { recursive: true });
writeFileSync(join(cwd, name), content);
}
const result = spawnSync('npm', ['run', command], { cwd, encoding: 'utf8', timeout: 20000 });
const html = join(cwd, 'dist/demo/index.html');
return { ...result, html: existsSync(html) ? readFileSync(html, 'utf8') : null };
} finally {
rmSync(cwd, { recursive: true, force: true });
}
}

test('HTML build rejects an extra README before Marp can convert it', () => {
const result = build({ 'slides/demo/index.md': deck, 'slides/demo/README.md': '# Notes\n' });
assert.equal(result.status, 1, result.stdout + result.stderr);
assert.match(result.stderr, /slides\/demo\/README\.md/);
assert.equal(result.html, null);
});

test('valid deck, image assets, and notes outside slides still build', () => {
const result = build({
'slides/demo/index.md': deck,
'slides/demo/img/chart.svg': '<svg xmlns="http://www.w3.org/2000/svg"/>',
'chapters/w03/README.md': '# Chapter notes\n',
'assignments/w03/learner/README.md': '# Learner record\n',
});
assert.equal(result.status, 0, result.stdout + result.stderr);
assert.match(result.html, /Smoke test/);
});

test('a deck folder with only assets reports its missing index.md', () => {
const result = build({ 'slides/demo/img/chart.svg': '<svg/>' });
assert.equal(result.status, 1, result.stdout + result.stderr);
assert.match(result.stderr, /slides\/demo\/index\.md/);
});

for (const path of [
'slides/notes.md',
'slides/demo/img/notes.md',
'slides/demo/INDEX.MD',
'slides/demo/notes.markdown',
'slides/demo/notes.mdown',
'slides/demo/notes.markdn',
]) {
test(`rejects Markdown outside the deck entry: ${path}`, () => {
const files = { [path]: '# Notes\n' };
// macOS can treat index.md and INDEX.MD as the same file.
if (path !== 'slides/demo/INDEX.MD') files['slides/demo/index.md'] = deck;
const result = build(files);
assert.equal(result.status, 1, result.stdout + result.stderr);
assert.ok(result.stderr.includes(path), result.stderr);
assert.equal(result.html, null);
});
}

for (const command of ['build', 'build:pdf', 'dev', 'build:ot-live', 'build:reports']) {
test(`${command} also blocks invalid Markdown before its build step`, () => {
const result = build({ 'slides/demo/index.md': deck, 'slides/demo/README.md': '# Notes\n' }, command);
assert.equal(result.status, 1, result.stdout + result.stderr);
assert.match(result.stderr, /slides\/demo\/README\.md/);
assert.equal(result.html, null);
});
}
Loading