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
9 changes: 8 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ on:
push:
branches: [main]
schedule:
# stats once a week (Monday), the activity feed every day
- cron: "17 0 * * 1"
- cron: "17 21 * * *"
workflow_dispatch:

permissions:
Expand All @@ -24,7 +26,7 @@ jobs:
persist-credentials: false

- name: Refresh GitHub stats
if: github.event_name != 'push'
if: github.event_name == 'workflow_dispatch' || github.event.schedule == '17 0 * * 1'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
Expand All @@ -37,6 +39,11 @@ jobs:
git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:main
fi

- name: Build the activity feed
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python3 scripts/build_activity.py data/activity.json || echo "activity feed kept as is"

- name: Check data files
run: python3 scripts/check_data.py

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ python3 scripts/builder_cards.py

## 활동 여권과 실험실

스탬프 정의는 `assets/js/passport.js`의 `STAMPS`에 있습니다. `kind: "auto"`는 허브가 알아서 찍고(`window.hubStamp("post")`), `kind: "self"`는 행사 후기, 강의 후기, 레포 댓글처럼 허브 밖에서 하는 일이라 방문자가 ‘했어요’를 눌러 찍습니다. 이미 받은 스탬프는 다시 주지 않고, 기록은 `localStorage`에만 남습니다. 웹북 독자 스탬프는 같은 도메인의 웹북이 남기는 `cil-book-read`(읽은 장)를 보고 찍고, 역 정보 창의 '내 진도'와 '이어 읽기'는 `cil-book-read`, `cil-book-last`를 읽습니다. `/#passport`로 들어오면 여권이 바로 열립니다. 교란 실험실(`lab.html`, `assets/js/lab.js`)의 데이터는 페이지에서 만드는 가상 사용자입니다.
스탬프 정의는 `assets/js/passport.js`의 `STAMPS`에 있습니다. `kind: "auto"`는 허브가 알아서 찍고(`window.hubStamp("post")`), `kind: "self"`는 행사 후기, 강의 후기, 레포 댓글처럼 허브 밖에서 하는 일이라 방문자가 ‘했어요’를 눌러 찍습니다. 로드맵 스테이지 스탬프(`stage_L0`–`stage_L4`)는 그 스테이지의 퀘스트를 모두 클리어하면 허브가 찍습니다. L3·L4처럼 `data/levels.json`에 `quests`(스탬프 id 목록)가 있는 스테이지는 그 스탬프들이 퀘스트이고, 없는 스테이지는 무료 자료가 퀘스트입니다. 이미 받은 스탬프는 다시 주지 않고, 기록은 `localStorage`에만 남습니다. 웹북 독자 스탬프는 같은 도메인의 웹북이 남기는 `cil-book-read`(읽은 장)를 보고 찍고, 역 정보 창의 '내 진도'와 '이어 읽기'는 `cil-book-read`, `cil-book-last`를 읽습니다. `/#passport`로 들어오면 여권이 바로 열립니다. 교란 실험실(`lab.html`, `assets/js/lab.js`)의 데이터는 페이지에서 만드는 가상 사용자입니다.

## 이벤트 측정

Expand Down
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,26 +41,26 @@

- **교란 실험실:** 가상 사용자 800명으로 교란이 단순 비교를 얼마나 속이는지 슬라이더로 직접 움직여 봅니다. [실험실 열기](https://causalinferencelab.github.io/lab.html)
- **연재:** 팀원이 쓰는 글을 모읍니다. 매주 월요일 ‘이번 주 포스트’가 바뀌고, 글쓴이를 작가 카드로 소개합니다.
- **활동 여권:** 자료 읽기, 웹북 한 장 완독 같은 배우기 스탬프와 함께, 행사 사진 후기, 강의·책 후기, 레포 댓글, 러너 참여, 첫 기여, Builder 되기 스탬프를 모읍니다. 기록은 내 브라우저에만 남습니다.
- **활동 여권:** 로드맵 스테이지 클리어 스탬프 다섯 개, 자료 읽기와 웹북 한 장 완독 같은 배우기 스탬프와 함께, 행사 사진 후기, 강의·책 후기, 레포 댓글, 러너 참여, 첫 기여, Builder 되기 스탬프를 모읍니다. 기록은 내 브라우저에만 남습니다.
- **3문항으로 시작점 찾기:** 경험, 하는 일, 원하는 것을 고르면 볼 순서와 첫 자료를 추천합니다. [바로 해 보기](https://causalinferencelab.github.io/?quiz=1)
- **상관일까, 인과일까?:** 데이터 상황 10개를 보고 상관인지 인과인지 고르는 2분짜리 게임입니다. 답마다 관련 웹북 장으로 이어집니다. [게임 하기](https://causalinferencelab.github.io/game.html)
- **완주 카드:** 한 단계의 자료를 모두 학습 완료로 표시하면, 공유할 수 있는 완주 카드 이미지를 만들 수 있습니다.
- **Builder 페이지:** 프로젝트를 연 Builder마다 이력, 만든 레포, 배지를 모은 페이지가 있습니다. [Builder 전체 보기](https://causalinferencelab.github.io/builders/)
- **Builder 메달:** Builder로 연 기수가 쌓이면 메달이 바뀝니다. 첫 기수는 Builder, 2개 기수는 다시 온 Builder, 3개 기수 이상은 베테랑 Builder, 5개 기수 이상은 명예 Builder입니다. 러너 20명 이상과 함께한 멘토, 러너로 시작해 Builder가 된 사람에게는 특별 메달이 붙습니다. 메달 카드 이미지는 허브에서 바로 만들 수 있습니다.

## 커리큘럼: L0–L4
## 커리큘럼: L0–L4 로드맵

L0–L2는 **배우는 단계**로, 무료 자료만으로 끝까지 갈 수 있습니다. L3–L4는 **만드는 단계**입니다.
다섯 단계를 한 길 위의 스테이지로 엮었습니다. L0–L2(WORLD 1 · 배우기)는 무료 자료가 퀘스트이고, 무료 자료만으로 끝까지 갈 수 있습니다. L3–L4(WORLD 2 · 만들기)는 이슈 댓글, 첫 PR, 러너 참여, Builder 되기처럼 직접 하는 일이 퀘스트입니다. 스테이지의 퀘스트를 모두 클리어하면 활동 여권에 스테이지 스탬프가 찍힙니다.

| 단계 | 질문 | 대표 자료 |
| --- | --- | --- |
| L0 Foundation | 왜 인과추론인가 | [Causal Studio](https://github.com/CausalInferenceLab/causal-studio) |
| L1 Theory & Concept | 어떻게 추정하나 | [Brave & True 인과추론 번역서](https://github.com/CausalInferenceLab/Causal-Inference-with-Python), [온라인 실험 이해하기](https://github.com/CausalInferenceLab/OCE-Materials) |
| L2 Application | 내 데이터에 어떻게 쓰나 | [책 예제 코드](https://github.com/CausalInferenceLab/causal-inference-in-python-code), [산업 사례 템플릿](https://github.com/CausalInferenceLab/causal-inference-practice), [특강·발표 자료](https://github.com/CausalInferenceLab/causal-inference-lecture) 외 |
| L3 Amplification | 분석을 어떻게 키우나 | [Lang2SQL](https://github.com/CausalInferenceLab/Lang2SQL), [정책 효과 분석 에이전트](https://github.com/CausalInferenceLab/policy-effect-analytics-agent) |
| L4 Research | 무엇이 아직 안 풀렸나 | [ccwr](https://github.com/CausalInferenceLab/ccwr), RWE 가이드라인·적응형 설계 스터디, 외부 오픈소스 기여 |
| L0 입문 | 왜 인과추론인가 | [Causal Studio](https://github.com/CausalInferenceLab/causal-studio) |
| L1 개념 완성 | 어떻게 추정하나 | [Brave & True 인과추론 번역서](https://github.com/CausalInferenceLab/Causal-Inference-with-Python), [온라인 실험 이해하기](https://github.com/CausalInferenceLab/OCE-Materials) |
| L2 실전 적용 | 내 데이터에 어떻게 쓰나 | [책 예제 코드](https://github.com/CausalInferenceLab/causal-inference-in-python-code), [산업 사례 템플릿](https://github.com/CausalInferenceLab/causal-inference-practice), [특강·발표 자료](https://github.com/CausalInferenceLab/causal-inference-lecture) 외 |
| L3 도구 개발 | 분석을 어떻게 키우나 | [Lang2SQL](https://github.com/CausalInferenceLab/Lang2SQL), [정책 효과 분석 에이전트](https://github.com/CausalInferenceLab/policy-effect-analytics-agent) |
| L4 연구 | 무엇이 아직 안 풀렸나 | [ccwr](https://github.com/CausalInferenceLab/ccwr), RWE 가이드라인·적응형 설계 스터디, 외부 오픈소스 기여 |

자료는 주제에 따라 여섯 트랙(입문·기본, ML, 연구, 실험, 행동·마케팅, AI 에이전트)으로도 나뉩니다. 허브에서 "처음 시작해요", "A/B 테스트를 해요" 같은 목표를 고르면 볼 순서를 안내합니다.
자료는 주제에 따라 여섯 트랙(입문·기본, ML, 연구, 실험, 행동·마케팅, AI 에이전트)으로도 나뉩니다. 허브의 트랙 지도에서 노선을 고르면 그 주제만 따라갈 수 있고, 어디서 시작할지 모르겠다면 3문항으로 볼 순서를 추천받습니다.

## 한빛미디어와 함께

Expand Down
137 changes: 135 additions & 2 deletions assets/css/hub.css
Original file line number Diff line number Diff line change
Expand Up @@ -533,9 +533,15 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a

/* Vertical lines replace the map on phones and small tablets */
.vlines { display: none; padding: 0 16px; }
/* phones get the real map, panned sideways, and the line-by-line list folded under it */
.vl-fold { display: none; }
@media (max-width: 900px) {
.map-scroll, .map-hint { display: none !important; }
.vlines { display: grid; gap: 22px; }
.vl-fold { display: block; margin: 10px 16px 0; border-radius: 12px; background: var(--surface-2); }
.vl-fold > summary { cursor: pointer; padding: 12px 14px; font-size: 14px; font-weight: 700; list-style: none; }
.vl-fold > summary::-webkit-details-marker { display: none; }
.vl-fold > summary::after { content: " ▾"; color: var(--muted); }
.vl-fold[open] > summary::after { content: " ▴"; }
.vl-fold .vlines { display: grid; gap: 22px; padding: 4px 14px 16px; }
}
.vline h3 { display: flex; align-items: center; gap: 8px; font-size: 16px; margin-bottom: 8px; }
.vline ol { list-style: none; margin: 0; padding: 0; }
Expand Down Expand Up @@ -1599,3 +1605,130 @@ button.btn.sm { font-size: 14px; }
.stage-goal.pop { animation: sg-pop .7s ease; }
@keyframes sg-pop { 0% { transform: scale(.96); } 55% { transform: scale(1.02); } 100% { transform: scale(1); } }
}

/* ---------- track map: numbered lines, a one-row guide, the picker on top, quests cleared on the map ---------- */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.map-how { list-style: none; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 20px; margin: 4px 16px 12px; padding: 10px 14px; border-radius: 12px; background: var(--surface-2); }
.map-how li { gap: 8px; font-size: 13px; }
.ln-no { display: inline-grid; place-items: center; width: 22px; height: 22px; border-radius: 50%; background: var(--c); color: var(--on-line); font-family: var(--f-display); font-size: 12.5px; font-weight: 800; line-height: 1; flex-shrink: 0; }
.ln-no.sm { width: 18px; height: 18px; font-size: 12px; margin-left: 3px; }
.line-legend { margin: 0 0 6px; align-items: center; }
.ll-k { font-size: 13px; font-weight: 800; color: var(--ink); margin-right: 4px; }
.ll { padding: 5px 12px 5px 5px; }
.ll[aria-pressed="true"] .ln-no { background: var(--on-line); color: var(--c); }
.line-desc { margin: 0 16px 8px; }
#metro .badge circle { stroke: var(--surface); stroke-width: 3; }
#metro .badge text { font-size: 15px; }
#metro .zone-n { font-weight: 800; }
#metro .st.cleared .ring, #metro .st.cleared .pill { fill: #FFC93C; }
#metro .st .done { fill: none; stroke: #14162B; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
#metro .pin rect, #metro .pin path { fill: var(--accent); }
#metro .pin text { fill: #fff; font-size: 12px; font-weight: 800; text-anchor: middle; dominant-baseline: central; }
@media (prefers-reduced-motion: no-preference) {
#metro .pin { animation: pin-bob 2.4s ease-in-out infinite; }
@keyframes pin-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
}
.vline h3 { flex-wrap: wrap; }
.vl-tag { font-size: 13px; font-weight: 600; color: var(--muted); }
.vline li { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 8px; }
.vline .lv { margin-left: 0; padding: 1px 7px; border-radius: 999px; background: var(--surface-2); font-weight: 700; color: var(--muted); }
.vl-x { display: inline-flex; align-items: center; font-size: 12px; font-weight: 700; color: var(--muted); }
.vl-done { padding: 1px 8px; border-radius: 999px; background: #FFE9A8; color: #6B4500; font-size: 12px; font-weight: 800; }
.vline li.on::after { background: #FFC93C; }
.vl-pin { padding: 1px 8px; border-radius: 999px; background: var(--accent); color: #fff; font-size: 12px; font-weight: 800; }
@media (max-width: 860px) { .map-how li:nth-child(-n+2) { display: none; } }
/* pins keep white text readable in both themes */
.st-pin, .vl-pin { background: #4C4FD0; }
#metro .pin rect, #metro .pin path { fill: #4C4FD0; }

/* ---------- one record (the passport), action quests on WORLD 2, the start finder beside the tab note ---------- */
.note-row { margin: -6px 0 14px; }
.note-row .tab-note { margin: 0; }
.note-row .route-chips { gap: 6px; }
.note-row .route-chips .chip { font-size: 13.5px; padding: 6px 12px; }
.qh { all: unset; box-sizing: border-box; cursor: pointer; display: inline-flex; align-items: center; gap: 10px; padding: 7px 14px; border-radius: 999px; background: var(--surface); font-size: 13.5px; color: var(--muted); }
.qh:hover { box-shadow: 0 4px 14px rgba(20, 22, 43, .08); }
.qh:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.route-note:empty { display: none; }
.lv-sub { margin: 18px 0 -6px; font-size: 13px; font-weight: 800; color: var(--muted); letter-spacing: .02em; }
#shelf .ep.act { --c: var(--l-ai); }
.aq-body { display: flex; flex-direction: column; gap: 10px; padding: 16px 18px 12px; flex: 1; }
.aq-body .stamp { width: 44px; height: 44px; font-size: 16px; flex-shrink: 0; }
.ep-clear:disabled { cursor: default; }

/* ---------- Level labels ---------- */
.st-lv { margin-top: 8px; font-size: 12px; font-weight: 700; letter-spacing: .02em; color: var(--muted); }
.st-lv + .st-name { margin-top: 0; }
.lv-id { display: flex; flex-direction: column; align-items: flex-start; line-height: .9; }
.lv-id small { font-family: var(--f-body); font-size: 13px; font-weight: 800; letter-spacing: .04em; color: var(--muted); }
#metro .zone-l { font-size: 18px; }
@media (max-width: 900px) {
.line-legend { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-bottom: 4px; }
.line-legend::-webkit-scrollbar { display: none; }
.line-legend li { flex-shrink: 0; }
}

/* ---------- alive: the activity line under the hero, the feed with names, the loop ---------- */
.avatar.xs { width: 22px; height: 22px; font-size: 12px; position: relative; }
.live { display: flex; align-items: center; gap: 10px 14px; margin-top: 10px; padding: 10px 16px; border-radius: 14px; background: var(--surface); font-size: 14px; min-height: 46px; }
.live[hidden] { display: none; }
.lv-k { display: inline-flex; align-items: center; gap: 7px; font-weight: 800; white-space: nowrap; flex-shrink: 0; }
.lv-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--live); box-shadow: 0 0 0 0 color-mix(in srgb, var(--live) 50%, transparent); }
@media (prefers-reduced-motion: no-preference) {
.lv-dot { animation: lv-pulse 2s ease-out infinite; }
@keyframes lv-pulse { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--live) 55%, transparent); } 100% { box-shadow: 0 0 0 9px transparent; } }
}
.lv-line { display: flex; align-items: center; gap: 6px; min-width: 0; flex: 1; white-space: nowrap; overflow: hidden; }
.lv-what { display: inline-flex; align-items: baseline; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.lv-line time, .ac-list time { color: var(--muted); font-size: 13px; flex-shrink: 0; margin-left: 4px; }
.lv-more { flex-shrink: 0; font-size: 13.5px; font-weight: 700; color: var(--accent); text-decoration: none; }
.lv-more:hover { text-decoration: underline; }
.ac-who { display: inline-flex; align-items: center; gap: 6px; font-weight: 700; color: var(--ink); text-decoration: none; flex-shrink: 0; }
.ac-who:hover { text-decoration: underline; }
.ac-repo { all: unset; cursor: pointer; font-weight: 700; color: var(--ink); margin-left: 4px; }
span.ac-repo { cursor: default; }
button.ac-repo:hover { text-decoration: underline; }
.ac-repo:focus-visible, .ac-who:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; border-radius: 4px; }
.ac-verb { color: var(--ink); }
.ac-title { margin-left: 6px; color: var(--muted); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
a.ac-title:hover { text-decoration: underline; }
.lv-what .ac-title { display: none; }
@media (max-width: 760px) {
.live { flex-wrap: wrap; }
.lv-line { flex-basis: 100%; order: 3; }
.lv-more { margin-left: auto; }
}
.activity { margin-top: 12px; padding: 18px 20px; border-radius: 20px; background: var(--surface); }
.activity[hidden] { display: none; }
.ac-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 14px; }
.ac-head h3 { font-size: 17px; }
.ac-head p { font-size: 14px; color: var(--muted); }
.ac-people { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; margin-top: 12px; }
.ac-n { font-size: 14px; color: var(--muted); }
.ac-n b { font-size: 20px; color: var(--ink); margin-right: 2px; }
.ac-people ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
.ac-people li .ac-who { padding: 3px 10px 3px 3px; border-radius: 999px; background: var(--surface-2); font-size: 13.5px; }
.ac-list { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 0; }
.ac-list li { display: flex; align-items: baseline; gap: 6px; padding: 9px 0; border-top: 1px solid var(--rule); font-size: 14px; min-width: 0; }
.ac-list li .ac-who { align-self: center; }
.ac-what { display: flex; align-items: baseline; min-width: 0; flex: 1; overflow: hidden; white-space: nowrap; }
.ac-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 14px; margin-top: 12px; font-size: 12.5px; color: var(--muted); }
@media (max-width: 760px) {
.ac-list li { flex-wrap: wrap; }
.ac-what { flex-basis: 100%; white-space: normal; flex-wrap: wrap; }
.ac-title { white-space: normal; }
}
.loop { list-style: none; margin: 0 0 14px; padding: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; counter-reset: none; position: relative; }
.loop > li > a { display: flex; flex-direction: column; gap: 2px; height: 100%; padding: 14px 16px; border-radius: 16px; background: var(--surface); color: var(--ink); text-decoration: none; position: relative; }
.loop > li > a:hover { box-shadow: 0 6px 20px rgba(20, 22, 43, .08); }
.loop > li > a:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.loop > li:not(.lp-back):not(:nth-child(4)) > a::after { content: "›"; position: absolute; right: -10px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; border-radius: 50%; background: var(--accent); color: #fff; font-size: 13px; font-weight: 800; display: grid; place-items: center; z-index: 1; line-height: 1; }
.lp-n { font-family: var(--f-display); font-size: 22px; font-weight: 800; color: var(--accent); line-height: 1; }
.loop b { font-size: 15.5px; margin-top: 4px; }
.loop a span:last-child { font-size: 13.5px; color: var(--muted); }
.lp-back { grid-column: 1 / -1; padding: 8px 14px; border-radius: 12px; border: 1.5px dashed color-mix(in srgb, var(--accent) 45%, var(--rule)); color: var(--muted); font-size: 13.5px; text-align: center; }
@media (max-width: 760px) {
.loop { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.loop > li:nth-child(2) > a::after { display: none; }
}
#join .steps li::before { display: none; }
Loading