Skip to content
Closed
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
5 changes: 4 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,13 @@ jobs:
run: |
for f in data/*.json; do python3 -m json.tool "$f" > /dev/null || { echo "Invalid JSON: $f"; exit 1; }; done

- name: Build Builder pages and badges
run: python3 scripts/build_builders.py

- name: Collect site files
run: |
mkdir _site
cp -r index.html article.html 404.html .nojekyll assets data articles _site/
cp -r index.html article.html game.html 404.html .nojekyll assets data articles builders badges _site/

- name: Publish to gh-pages
uses: peaceiris/actions-gh-pages@v4
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.DS_Store
_site/
node_modules/
/builders/
/badges/
__pycache__/
8 changes: 1 addition & 7 deletions 404.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@
<html lang="ko">
<head>
<meta charset="utf-8">
<script async src="https://www.googletagmanager.com/gtag/js?id=G-BEBVKYH8HL"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-BEBVKYH8HL');
</script>
<script src="/assets/js/ga.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>페이지를 찾을 수 없습니다 | Causal Inference Hub</title>
<link rel="icon" href="/assets/img/team-mark.png">
Expand Down
24 changes: 22 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
| 파일 | 고치는 경우 |
| --- | --- |
| `data/featured.json` | 첫 화면 배너의 대표 오픈소스 링크를 바꿀 때. `repo`를 적으면 스타 수가 함께 나옵니다. |
| `data/projects.json` | 기수별 프로젝트를 추가할 때. 러너는 인원수(`runners`)만 적습니다. |
| `data/builders.json` | Builder의 LinkedIn, GitHub 아이디(프로필 사진에 쓰임), 한 줄 소개, Builder가 되기 전 러너로 참여한 프로젝트(`runnerOf`)를 넣을 때 |
| `data/projects.json` | 기수별 프로젝트를 추가할 때. 러너는 인원수(`runners`)만 적습니다. Builder가 회고를 보내 주면 `"retro": {"why": "…", "learned": "…", "next": "…"}`로 넣습니다. 비어 있으면 화면에 나오지 않습니다. |
| `data/builders.json` | Builder의 LinkedIn, GitHub 아이디(프로필 사진에 쓰임), 한 줄 소개, Builder가 되기 전 러너로 참여한 프로젝트(`runnerOf`)를 넣을 때. `slug`는 Builder 페이지 주소(`/builders/<slug>.html`)가 되므로 한 번 정하면 바꾸지 않습니다. |
| `data/growth.json` | 기수별 누적 러너 수. 프로젝트 시트를 xlsx로 내려받아 `python scripts/runners_from_sheet.py 파일.xlsx > data/growth.json`으로 만듭니다. 이름은 파일에 남지 않습니다. |
| `data/events.json` | Data Forum 같은 행사를 추가할 때 |
| `data/repos.json` | 레포를 추가하거나 설명·단계·트랙·상태를 바꿀 때 |
| `data/map.json` | 트랙 지도에서 레포의 위치를 정할 때. 책과 강의(`paid`)의 표지와 소개도 여기서 고칩니다. |
| `data/routes.json` | 경로 안내 항목을 바꿀 때 |
| `data/articles.json`, `articles/*.md` | 읽을거리를 추가할 때 |
| `data/game.json` | ‘상관일까, 인과일까?’ 문항을 고칠 때. `answer`는 `corr` 또는 `cause`이고, 문항마다 웹북 장 링크(`chapter`)를 답니다. |
| `data/stats.json` | 직접 고치지 않습니다. 매주 GitHub Actions가 스타 추이와 기여자 수를 갱신합니다. |

레포를 추가할 때의 예시입니다.
Expand All @@ -43,6 +44,25 @@

`stars`는 기본값이고, 사이트는 열릴 때 GitHub에서 최신 스타 수와 마지막 커밋 날짜를 불러옵니다.

## Builder 페이지와 배지

`builders/<slug>.html`, `builders/index.html`, `badges/<slug>.svg`는 `scripts/build_builders.py`가 데이터 파일로 만듭니다. 만들어지는 파일이라 커밋하지 않고, 배포할 때 GitHub Actions가 실행합니다. 로컬에서 보려면 먼저 실행하세요.

```bash
python3 scripts/build_builders.py
```

링크를 공유할 때 보이는 이미지(`assets/og/builders/<slug>.jpg`)는 따로 만들어 커밋합니다. Builder가 새로 생기거나 숫자가 바뀌면 다시 만들어 주세요. 이미지가 없는 페이지는 허브 기본 이미지를 씁니다.

```bash
pip install playwright && python -m playwright install chromium
python3 scripts/builder_cards.py
```

## 이벤트 측정

`assets/js/track.js`의 `hubTrack(이름, 파라미터)`가 GA4로 이벤트를 보냅니다. GA4가 설치되지 않았으면 아무 일도 하지 않습니다. 이벤트 이름과 파라미터는 PM 문서의 이벤트 사전을 따르고, 새 이벤트를 넣을 때는 사전에도 같이 적습니다. HTML에서는 `data-event="click_join" data-event-params='{"target":"template","source":"builder_page"}'`처럼 속성만으로도 보낼 수 있습니다. 로컬에서 확인하려면 콘솔에서 `window.HUB_DEBUG = true`를 켭니다.

## 로컬에서 보기

```bash
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@

팀 레포 전체는 허브의 트랙 지도와 커리큘럼 탭에서 볼 수 있습니다.

## 허브에서 해 볼 것

- **3문항으로 시작점 찾기:** 경험, 하는 일, 원하는 것을 고르면 볼 순서와 첫 자료를 추천합니다. [바로 해 보기](https://causalinferencelab.github.io/?quiz=1)
- **상관일까, 인과일까?:** 데이터 상황 10개를 보고 상관인지 인과인지 고르는 2분짜리 게임입니다. 답마다 관련 웹북 장으로 이어집니다. [게임 하기](https://causalinferencelab.github.io/game.html)
- **완주 카드:** 한 단계의 자료를 모두 학습 완료로 표시하면, 공유할 수 있는 완주 카드 이미지를 만들 수 있습니다.
- **Builder 페이지:** 프로젝트를 연 Builder마다 이력, 만든 레포, 배지를 모은 페이지가 있습니다. [Builder 전체 보기](https://causalinferencelab.github.io/builders/)

## 커리큘럼: L0–L4

L0–L2는 **배우는 단계**로, 무료 자료만으로 끝까지 갈 수 있습니다. L3–L4는 **만드는 단계**입니다.
Expand Down
8 changes: 1 addition & 7 deletions article.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@
<html lang="ko">
<head>
<meta charset="utf-8">
<script async src="https://www.googletagmanager.com/gtag/js?id=G-BEBVKYH8HL"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-BEBVKYH8HL');
</script>
<script src="assets/js/ga.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>읽을거리 | Causal Inference Hub</title>
<meta name="description" content="가짜연구소 인과추론팀이 쓴 글과 독자를 위한 자료입니다.">
Expand Down
140 changes: 140 additions & 0 deletions assets/css/hub.css
Original file line number Diff line number Diff line change
Expand Up @@ -656,3 +656,143 @@ dialog.station::backdrop { background: rgba(10, 14, 20, .55); }
.reader aside ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.back { display: inline-flex; margin-bottom: 20px; font-weight: 600; }
.notice { background: var(--surface); border: 1px solid var(--rule); border-radius: 12px; padding: 18px; margin-bottom: 16px; }

/* Start-point quiz entry, quiz dialog, finish card, Builder share */
.start-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin: -2px 0 10px; }
.start-btn { font: inherit; font-size: 14px; font-weight: 600; display: inline-flex; align-items: center; gap: 8px; min-height: 40px; padding: 0 14px; border-radius: 999px; border: 1.5px dashed color-mix(in srgb, var(--accent) 55%, var(--rule)); background: var(--accent-soft); color: var(--ink); cursor: pointer; }
.start-btn b { color: var(--accent); font-weight: 800; }
.start-btn:hover { border-style: solid; border-color: var(--accent); }
.start-btn:focus-visible, .start-game:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.start-game { font-size: 14px; font-weight: 600; color: var(--muted); text-decoration: none; }
.start-game::after { content: " ›"; }
.start-game:hover { color: var(--ink); text-decoration: underline; }
.btn.sm { min-height: 36px; padding: 0 12px; font-size: 14px; margin-top: 0; }
button.btn { font: inherit; font-weight: 600; cursor: pointer; border: 0; }
button.btn.ghost { border: 1.5px solid var(--rule); }
button.btn.sm { font-size: 14px; }

.quiz-step { display: flex; align-items: center; justify-content: space-between; gap: 12px; font-size: 13px; font-weight: 700; color: var(--accent); padding-right: 48px; }
.quiz-dots { display: inline-flex; gap: 4px; }
.quiz-dots i { width: 22px; height: 4px; border-radius: 2px; background: var(--rule); }
.quiz-dots i.on { background: var(--accent); }
.quiz h2 { font-size: 22px; }
.quiz-opts { display: grid; gap: 8px; }
.quiz-opts button { font: inherit; font-size: 15.5px; font-weight: 600; text-align: left; min-height: 52px; padding: 0 16px; border-radius: 12px; border: 1.5px solid var(--rule); background: var(--surface); color: var(--ink); cursor: pointer; }
.quiz-opts button:hover { border-color: var(--accent); background: var(--accent-soft); }
.quiz-opts button[aria-pressed="true"] { border-color: var(--accent); }
.quiz-opts button:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.quiz-back { all: unset; cursor: pointer; justify-self: start; font-size: 14px; font-weight: 600; color: var(--muted); }
.quiz-back:hover { color: var(--ink); text-decoration: underline; }
.quiz-back:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; border-radius: 4px; }
.quiz-note { font-size: 13px; color: var(--muted); }
.quiz-stops { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.quiz-stops li { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 12px; background: color-mix(in srgb, var(--c) 8%, var(--surface)); border: 1px solid color-mix(in srgb, var(--c) 22%, var(--rule)); }
.quiz-stops .n { font-family: var(--f-display); font-weight: 800; font-size: 20px; color: var(--c); width: 30px; flex-shrink: 0; }
.quiz-stops b { display: block; font-size: 15px; }
.quiz-stops li span span { font-size: 12.5px; color: var(--muted); }

.done-banner { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; padding: 12px 14px; border-radius: 12px; background: color-mix(in srgb, var(--live) 10%, var(--surface)); border: 1px solid color-mix(in srgb, var(--live) 35%, var(--rule)); font-size: 14px; }
.cont-chip.card { border-color: var(--live); color: var(--live); }
.card-canvas { width: 100%; height: auto; border-radius: 12px; box-shadow: var(--shadow); }
.card-note { font-size: 13.5px; color: var(--muted); }
.p-share { display: flex; flex-wrap: wrap; gap: 8px; }
.tl-retro { margin-top: 4px; font-size: 13px; }
.tl-retro summary { cursor: pointer; font-weight: 600; color: var(--accent); }
.tl-retro dl { margin: 6px 0 0; display: grid; grid-template-columns: 92px minmax(0, 1fr); gap: 4px 10px; }
.tl-retro dt { color: var(--muted); }
.tl-retro dd { margin: 0; }
.quiz-stops li.is-optional { opacity: .6; }

/* Builder pages (builders/*.html, generated by scripts/build_builders.py) */
.bp { padding-block: 28px 64px; }
.bp .back { font-size: 14px; color: var(--muted); text-decoration: none; }
.bp .back::before { content: "‹ "; }
.bp .back:hover { color: var(--ink); }
.bp-hero { display: flex; align-items: center; gap: 22px; margin: 18px 0 18px; padding: 26px 28px; border-radius: 20px; background: var(--banner); color: #fff; }
.bp-hero .avatar.lg { width: 96px; height: 96px; font-size: 40px; background: rgba(255, 255, 255, .16); color: #fff; box-shadow: 0 0 0 3px rgba(255, 255, 255, .3); }
.bp-id { min-width: 0; }
.bp-kicker { display: flex; align-items: center; gap: 8px; font-size: 13px; color: rgba(255, 255, 255, .8); }
.bp-hero h1 { font-family: var(--f-display); font-size: clamp(32px, 5vw, 46px); font-weight: 800; letter-spacing: -.03em; line-height: 1.1; margin-top: 4px; }
.bp-since { font-size: 15px; color: rgba(255, 255, 255, .86); margin-top: 6px; }
.bp-intro { font-size: 15px; margin-top: 8px; max-width: 40em; }
.bp-links { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 12px; }
.bp-links a { color: #fff; font-weight: 600; }
.bp-hero .btn.ghost { color: #fff; border-color: rgba(255, 255, 255, .45); background: transparent; }
.bp-stats { margin-bottom: 28px; }
.bp-grid { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 32px; align-items: start; }
.bp h2 { font-size: 18px; margin-bottom: 12px; }
.bp-grid aside { display: grid; gap: 28px; }
.bp-repos { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.bp-repos a { display: flex; gap: 10px; align-items: flex-start; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--rule); background: var(--surface); color: var(--ink); text-decoration: none; }
.bp-repos a:hover { border-color: var(--c); }
.bp-repos .num { margin-top: 5px; }
.bp-repos b { display: block; font-size: 15px; }
.bp-repos a span span { font-size: 13px; color: var(--muted); }
.bp-badge { padding: 16px; border-radius: 14px; border: 1px solid var(--rule); background: var(--surface); display: grid; gap: 10px; justify-items: start; }
.bp-badge p { font-size: 13.5px; color: var(--muted); }
.bp-badge pre { margin: 0; width: 100%; box-sizing: border-box; overflow-x: auto; font-family: var(--f-code); font-size: 11.5px; padding: 10px; border-radius: 8px; background: var(--surface-2); }
.bp-cta { margin-top: 40px; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 16px 24px; padding: 22px 24px; border-radius: 16px; background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent) 25%, var(--rule)); }
.bp-cta h2 { margin-bottom: 4px; }
.bp-cta p { font-size: 14.5px; color: var(--muted); max-width: 36em; }
.bp-cta-links { display: flex; flex-wrap: wrap; gap: 8px; }
.bp-cta .btn { margin-top: 0; }
.bi-title { font-family: var(--f-display); font-size: clamp(30px, 4vw, 40px); font-weight: 800; letter-spacing: -.02em; margin-top: 16px; }
.bi-sub { color: var(--muted); margin: 6px 0 20px; }
.bi-list { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 10px; }
.bi-list a { position: relative; display: flex; gap: 14px; align-items: center; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--rule); background: var(--surface); color: var(--ink); text-decoration: none; }
.bi-list a:hover { border-color: var(--c); }
.bi-list a:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.bi-list .avatar.lg { width: 52px; height: 52px; font-size: 22px; }
.bi-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.bi-body b { font-size: 16px; }
.bi-body span { font-size: 13px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bi-list .rank-tag { position: absolute; top: 10px; right: 12px; font-size: 11px; padding: 1px 8px; }
@media (max-width: 860px) { .bp-grid { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 560px) { .bp-hero { flex-direction: column; align-items: flex-start; padding: 22px 20px; } .bp-hero .avatar.lg { width: 72px; height: 72px; font-size: 30px; } }

/* Game page (game.html) */
.gm { padding-block: 40px 72px; }
.gm-card { max-width: 720px; margin: 0 auto; padding: 36px 36px 32px; border-radius: 22px; background: var(--surface); border: 1px solid var(--rule); box-shadow: var(--shadow); display: grid; gap: 16px; justify-items: start; }
.gm-kicker { font-size: 13px; font-weight: 700; color: var(--accent); }
.gm-card h1 { font-family: var(--f-display); font-size: clamp(32px, 6vw, 48px); font-weight: 800; letter-spacing: -.03em; line-height: 1.1; }
.gm-card h2 { font-size: 22px; line-height: 1.4; }
.gm-lede { font-size: 16px; color: var(--muted); }
.gm-card .btn { margin-top: 0; }
.gm-progress { width: 100%; display: flex; align-items: center; gap: 12px; font-size: 13px; font-weight: 700; color: var(--muted); font-variant-numeric: tabular-nums; }
.gm-bar { flex: 1; height: 6px; border-radius: 3px; background: var(--surface-2); overflow: hidden; }
.gm-bar i { display: block; height: 100%; background: var(--accent); transition: width .3s ease; }
.gm-q { font-size: clamp(19px, 3.2vw, 23px) !important; }
.gm-opts { width: 100%; display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.gm-opts button { font: inherit; font-size: 16px; font-weight: 700; min-height: 64px; padding: 10px 14px; border-radius: 14px; border: 1.5px solid var(--rule); background: var(--surface); color: var(--ink); cursor: pointer; }
.gm-opts button:hover:not(:disabled) { border-color: var(--accent); background: var(--accent-soft); }
.gm-opts button:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.gm-opts button:disabled { cursor: default; opacity: .55; }
.gm-opts button.ok { opacity: 1; border-color: var(--live); background: color-mix(in srgb, var(--live) 12%, var(--surface)); }
.gm-opts button.no { opacity: 1; border-color: var(--l-research); background: color-mix(in srgb, var(--l-research) 10%, var(--surface)); }
.gm-feedback { width: 100%; display: grid; gap: 10px; justify-items: start; font-size: 15px; }
.gm-feedback:empty { display: none; }
.gm-verdict { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; font-size: 18px; font-weight: 800; }
.gm-verdict.ok { color: var(--live); }
.gm-verdict.no { color: var(--l-research); }
.gm-verdict span { font-size: 14px; font-weight: 600; color: var(--muted); }
.gm-read a { font-weight: 700; }
.gm-score { font-family: var(--f-display); font-size: 22px; color: var(--muted); }
.gm-score b { font-size: 72px; font-weight: 800; color: var(--ink); letter-spacing: -.03em; }
.gm-review { width: 100%; padding: 14px 16px; border-radius: 12px; background: var(--surface-2); }
.gm-review h3 { font-size: 14px; margin-bottom: 6px; }
.gm-review ul { margin: 0; padding-left: 18px; font-size: 14.5px; display: grid; gap: 4px; }
.gm-cta { display: flex; flex-wrap: wrap; gap: 8px; }
@media (max-width: 560px) { .gm-card { padding: 24px 20px; } .gm-opts { grid-template-columns: minmax(0, 1fr); } }
.bp-cta .btn.ghost { border-color: color-mix(in srgb, var(--accent) 40%, var(--rule)); }
.bp .back::before { content: "‹"; margin-right: 4px; }
.bp-self { font-size: 13px; color: rgba(255, 255, 255, .8); margin-top: 10px; }
.bp-self a { color: #fff; font-weight: 600; }
.bp-badge pre { white-space: pre-wrap; word-break: break-all; }
.card-name { display: grid; gap: 6px; font-size: 13.5px; font-weight: 600; color: var(--muted); }
.card-name input { font: inherit; font-size: 15px; font-weight: 500; color: var(--ink); background: var(--surface); border: 1.5px solid var(--rule); border-radius: 10px; padding: 9px 12px; max-width: 280px; }
.card-name input:focus-visible { outline: 3px solid var(--focus); outline-offset: 1px; }
.gm-q:focus { outline: none; }
.gm-claim { width: 100%; font-size: 17px; font-weight: 700; padding: 12px 14px; border-radius: 12px; background: var(--accent-soft); }
.gm-claim span { display: block; font-size: 12.5px; font-weight: 700; color: var(--accent); margin-bottom: 2px; }
.gm-split { font-size: 13.5px; color: var(--muted); }
.gm-score:focus { outline: none; }
Binary file added assets/img/og-game.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions assets/js/ga.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* Google Analytics 4 for every page of the hub. The first ID is the hub's property;
the second is the tag the web book already uses, kept so visits can be followed
across both sites until one property is chosen. Local previews are not counted. */
(() => {
const IDS = ["G-C5V0X2SGD3", "G-BEBVKYH8HL"];
if (location.hostname !== "causalinferencelab.github.io") return;
const s = document.createElement("script");
s.async = true;
s.src = `https://www.googletagmanager.com/gtag/js?id=${IDS[0]}`;
document.head.appendChild(s);
window.dataLayer = window.dataLayer || [];
window.gtag = function gtag() { window.dataLayer.push(arguments); };
window.gtag("js", new Date());
IDS.forEach((id) => window.gtag("config", id));
})();
Loading