diff --git a/README.md b/README.md index c8a067f..e6e8f54 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ - **상관일까, 인과일까?:** 데이터 상황 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 @@ -65,10 +66,16 @@ L0–L2는 **배우는 단계**로, 무료 자료만으로 끝까지 갈 수 있 팀이 번역한 『[실무로 통하는 인과추론 with 파이썬](https://www.hanbit.co.kr/store/books/look.php?p_code=B6208936856)』(한빛미디어, 2024)과 그 내용을 실무로 잇는 온라인 강의 「[데이터 실무자의 사고력 강화: AI 인과추론 실무](https://www.hanbit.co.kr/product/C0781073345)」가 커리큘럼 L2에 들어 있습니다. 책의 장별 예제 코드와 발표 자료는 무료 레포로 공개되어 있습니다. -## 행사: Data Forum +## 행사: Data Forum과 학회 협력 현업과 학계가 만나는 Data Forum을 2024년부터 열고 있습니다. 회차별 내용과 다음 행사 일정은 허브의 행사 섹션에 있습니다. +한국경영정보학회가 주최하는 학회 행사에도 협력하고 있습니다. 오래 함께한 Builder에게는 이런 학회와 Data Forum에서 발표할 기회가 열립니다. + +- [한국 비즈니스 실험 심포지엄 2026](https://sites.google.com/view/business-experimentation2026/home) (2026.07.11, 한국과학기술회관) +- [한국 비즈니스 실험 심포지엄 2025](https://sites.google.com/view/business-experimentation2025/home) (2025.07.12, 고려대학교 LG-POSCO 경영관) +- [인과추론 여름 워크숍 2024](https://sites.google.com/view/causal-inference2024/home) (2024.06, 온라인) + ## 참여하는 방법 1. **러너로 참여:** 가짜연구소 기수 모집 때 인과추론팀 프로젝트에 러너로 들어와 14주 동안 함께 공부하고 만듭니다. diff --git a/assets/css/hub.css b/assets/css/hub.css index 0f2aeeb..cd5815d 100644 --- a/assets/css/hub.css +++ b/assets/css/hub.css @@ -89,7 +89,7 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a .brand-logo { width: 34px; height: 34px; display: block; } .brand-text { display: flex; flex-direction: column; line-height: 1.1; } .brand-name { font-family: var(--f-display); font-weight: 800; font-size: 17px; letter-spacing: -.01em; } -.brand-sub { font-size: 11.5px; color: var(--muted); letter-spacing: .2px; } +.brand-sub { font-size: 12px; color: var(--muted); letter-spacing: .2px; } .site-nav { display: flex; gap: 2px; overflow-x: auto; scrollbar-width: none; margin-left: auto; } .site-nav::-webkit-scrollbar { display: none; } .site-nav a { color: var(--muted); text-decoration: none; padding: 8px 10px; border-radius: 8px; white-space: nowrap; font-size: 15px; font-weight: 500; } @@ -125,7 +125,7 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a .featured a { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-radius: 12px; background: rgba(255, 255, 255, .12); border: 1px solid rgba(255, 255, 255, .26); color: #fff; text-decoration: none; } .featured a:hover { background: rgba(255, 255, 255, .2); border-color: rgba(255, 255, 255, .5); } .featured a:focus-visible { outline: 3px solid #fff; outline-offset: 2px; } -.featured .tag { flex-shrink: 0; width: 64px; box-sizing: border-box; text-align: center; font-size: 11.5px; font-weight: 800; color: #23276A; background: #fff; border-radius: 6px; padding: 2px 6px; white-space: nowrap; } +.featured .tag { flex-shrink: 0; width: 64px; box-sizing: border-box; text-align: center; font-size: 12px; font-weight: 800; color: #23276A; background: #fff; border-radius: 6px; padding: 2px 6px; white-space: nowrap; } .featured .tx { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; } .featured .t { font-weight: 700; font-size: 15px; letter-spacing: -.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .featured .d { font-size: 12px; color: rgba(255, 255, 255, .74); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } @@ -147,9 +147,9 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a .pt-img.wide { width: 48px; border-radius: 4px; object-position: 70% 30%; } .pt-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; } .pt-text b { font-size: 13px; font-weight: 500; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.pt-lv { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: rgba(255, 255, 255, .72); white-space: nowrap; } -.pt-type { font-size: 10.5px; font-weight: 800; color: #23276A; background: rgba(255, 255, 255, .9); border-radius: 4px; padding: 0 5px; } -.pt-paid { font-size: 10.5px; font-weight: 700; color: rgba(255, 255, 255, .85); border: 1px solid rgba(255, 255, 255, .4); border-radius: 4px; padding: 0 4px; margin-left: -2px; } +.pt-lv { display: flex; align-items: center; gap: 6px; font-size: 12px; color: rgba(255, 255, 255, .72); white-space: nowrap; } +.pt-type { font-size: 12px; font-weight: 800; color: #23276A; background: rgba(255, 255, 255, .9); border-radius: 4px; padding: 0 5px; } +.pt-paid { font-size: 12px; font-weight: 700; color: rgba(255, 255, 255, .85); border: 1px solid rgba(255, 255, 255, .4); border-radius: 4px; padding: 0 4px; margin-left: -2px; } .pt-out { display: grid; place-items: center; width: 34px; flex-shrink: 0; color: #fff; border-left: 1px solid rgba(255, 255, 255, .14); border-radius: 0 12px 12px 0; } .pt-out:hover { background: rgba(255, 255, 255, .1); } .pt-out svg { width: 12px; height: 12px; } @@ -164,6 +164,7 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a .banner-main { display: contents; } .banner-side { order: 1; width: 100%; margin-top: 22px; } .partner { order: 2; } + .academy { order: 3; } .banner-mark { width: 72px; height: 72px; display: none; } .banner-lede br { display: none; } .banner-cta { gap: 8px; } @@ -287,7 +288,7 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a .ep-cover::after { content: ""; position: absolute; inset: 0; background: radial-gradient(circle at 1px 1px, color-mix(in srgb, var(--c) 34%, transparent) 1.2px, transparent 1.6px) 0 0 / 12px 12px; -webkit-mask-image: linear-gradient(120deg, transparent 35%, #000); mask-image: linear-gradient(120deg, transparent 35%, #000); } .ep-no { position: absolute; left: 18px; top: 12px; font-size: 12px; font-weight: 700; letter-spacing: .3px; color: var(--c); z-index: 1; } .ep-badges { position: absolute; right: 10px; top: 10px; display: flex; gap: 4px; z-index: 1; } -.ep-badges .b { font-size: 11px; font-weight: 700; padding: 2px 7px; border-radius: 999px; background: var(--surface); color: var(--ink); white-space: nowrap; border: 1px solid var(--rule); } +.ep-badges .b { font-size: 12px; font-weight: 700; padding: 2px 7px; border-radius: 999px; background: var(--surface); color: var(--ink); white-space: nowrap; border: 1px solid var(--rule); } .ep-badges .b.top { background: #FFD54A; color: #14162B; border-color: #FFD54A; } .ep-badges .b.live { color: var(--live); } .ep-badges .b.live::before { content: "● "; } @@ -388,12 +389,6 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a @media (max-width: 1000px) { .top3 { grid-template-columns: minmax(0, 1fr); } } /* cohort strip: one cell per cohort */ -.strip { display: grid; grid-template-columns: repeat(var(--n), 22px); gap: 4px; } -.sc { box-sizing: border-box; width: 22px; height: 22px; border-radius: 6px; background: var(--surface-2); display: grid; place-items: center; font-size: 10.5px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--on-line); } -.sc.on { background: var(--c); } -.sc.on.live { background: transparent; border: 2px dashed var(--c); color: var(--ink); } -.sc.ran { background: transparent; border: 1.5px solid var(--c); } -.sh { width: 22px; text-align: center; font-size: 11px; font-variant-numeric: tabular-nums; } /* sliding rail */ .people-rail { overflow: hidden; margin: 0 0 32px; -webkit-mask-image: linear-gradient(90deg, transparent, #000 20px, #000 calc(100% - 40px), transparent); mask-image: linear-gradient(90deg, transparent, #000 20px, #000 calc(100% - 40px), transparent); } @@ -418,7 +413,7 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a /* roster */ .roster { background: var(--surface); border: 1px solid var(--rule); border-radius: 16px; overflow: hidden; } -.ro-head, .ro { display: grid; grid-template-columns: 210px auto minmax(0, 1fr) 96px; gap: 20px; align-items: center; padding: 12px 18px; } +.ro-head, .ro { display: grid; grid-template-columns: 210px minmax(0, 236px) minmax(0, 1fr) 96px; gap: 20px; align-items: center; padding: 12px 18px; } .ro-head { font-size: 12px; font-weight: 700; color: var(--muted); padding-block: 10px; border-bottom: 1px solid var(--rule); background: var(--surface-2); } .ro-list { list-style: none; margin: 0; padding: 0; } .ro + .ro { border-top: 1px solid var(--rule); } @@ -439,13 +434,12 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a .ro-projects .ro-more { all: unset; cursor: pointer; font-size: 12.5px; font-weight: 600; color: var(--accent); } .ro-more:hover { text-decoration: underline; } .ro-more:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; border-radius: 4px; } -.live-dot { flex-shrink: 0; font-size: 11.5px; font-weight: 700; color: var(--live); } +.live-dot { flex-shrink: 0; font-size: 12px; font-weight: 700; color: var(--live); } .tot { text-align: right; } .ro-total b { display: block; font-family: var(--f-display); font-size: 16px; white-space: nowrap; font-weight: 800; line-height: 1.2; font-variant-numeric: tabular-nums; } .ro-total span { font-size: 12px; color: var(--muted); white-space: nowrap; } .roster-legend { list-style: none; margin: 0 0 10px; padding: 0; display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 13px; color: var(--muted); } .roster-legend li { display: flex; align-items: center; gap: 6px; } -.roster-legend .sc { width: 14px; height: 14px; border-radius: 4px; } @media (max-width: 600px) { .rail-head, .people-rail { display: none; } } @media (max-width: 760px) { .ro-head { display: none; } @@ -499,15 +493,19 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a .tabs button { font: inherit; font-size: 15px; font-weight: 600; background: none; border: 0; padding: 8px 12px; border-radius: 8px; color: var(--muted); cursor: pointer; min-height: 40px; } .tabs button[aria-selected="true"] { color: var(--ink); background: var(--surface-2); } .map-scroll { overflow-x: auto; padding: 0 8px; } -#metro { display: block; width: 100%; min-width: 860px; height: auto; } +/* below this width the labels would shrink under 12px, so the map scrolls instead */ +#metro { display: block; width: 100%; min-width: 1080px; height: auto; } +.map-scroll { scrollbar-width: thin; } +.map-scroll.more { -webkit-mask-image: linear-gradient(90deg, #000 88%, transparent); mask-image: linear-gradient(90deg, #000 88%, transparent); } +.map-hint { margin: 0 16px 8px; font-size: 13px; color: var(--muted); } #metro .zone rect { fill: var(--surface-2); } #metro .zone.build rect { fill: color-mix(in srgb, var(--l-main) 9%, var(--surface)); } #metro .zone-l { font-family: var(--f-display); font-weight: 800; letter-spacing: -.02em; font-size: 22px; fill: var(--ink); } -#metro .zone-n { font-size: 13px; font-weight: 600; fill: var(--ink); } -#metro .zone-q { font-size: 12px; fill: var(--muted); } +#metro .zone-n { font-size: 14px; font-weight: 600; fill: var(--ink); } +#metro .zone-q { font-size: 13px; fill: var(--muted); } #metro .track { fill: none; stroke: var(--c); stroke-width: 8; stroke-linecap: round; stroke-linejoin: round; } #metro .express { fill: none; stroke: var(--l-main); stroke-width: 3; stroke-dasharray: 6 8; stroke-linecap: round; } -#metro .express-l { font-size: 12px; font-weight: 600; fill: var(--l-main); } +#metro .express-l { font-size: 13px; font-weight: 600; fill: var(--l-main); } #metro .badge circle { fill: var(--c); } #metro .badge text { font-family: var(--f-display); font-weight: 800; letter-spacing: -.02em; font-size: 16px; fill: var(--on-line); text-anchor: middle; dominant-baseline: central; } #metro .st { cursor: pointer; } @@ -516,8 +514,8 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a #metro .st .pill { fill: var(--surface); stroke: var(--ink); stroke-width: 4.5; } #metro .st .sq { fill: var(--surface); stroke: var(--l-main); stroke-width: 3; } #metro .st.ext .halo { fill: none; stroke: var(--c); stroke-width: 2.5; } -#metro .st .label { font-size: 13.5px; font-weight: 600; fill: var(--ink); } -#metro .st.paid .label { font-size: 12.5px; fill: var(--l-main); } +#metro .st .label { font-size: 14.5px; font-weight: 600; fill: var(--ink); } +#metro .st.paid .label { font-size: 13.5px; fill: var(--l-main); } #metro .st .tag rect { fill: var(--live); } #metro .st .tag text { fill: var(--surface); font-size: 10.5px; font-weight: 700; text-anchor: middle; dominant-baseline: central; } #metro .st .done { fill: var(--c); } @@ -533,10 +531,10 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a .line-legend li { display: flex; align-items: center; gap: 6px; } .num { display: inline-block; width: 12px; height: 12px; border-radius: 50%; background: var(--c); flex-shrink: 0; } -/* Vertical lines replace the map on phones */ +/* Vertical lines replace the map on phones and small tablets */ .vlines { display: none; padding: 0 16px; } -@media (max-width: 720px) { - .map-scroll { display: none; } +@media (max-width: 900px) { + .map-scroll, .map-hint { display: none !important; } .vlines { display: grid; gap: 22px; } } .vline h3 { display: flex; align-items: center; gap: 8px; font-size: 16px; margin-bottom: 8px; } @@ -631,9 +629,9 @@ dialog.station::backdrop { background: rgba(10, 14, 20, .55); } .p-timeline li.r::before { box-sizing: border-box; background: var(--surface); border: 2px solid var(--c); } .tl-c { white-space: nowrap; } .tl-c b { display: block; font-size: 15px; font-weight: 800; font-variant-numeric: tabular-nums; } -.tl-c span { font-size: 11.5px; color: var(--muted); } +.tl-c span { font-size: 12px; color: var(--muted); } .tl-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; font-size: 14px; } -.tl-role { font-size: 11.5px; font-weight: 700; color: var(--accent); } +.tl-role { font-size: 12px; font-weight: 700; color: var(--accent); } .p-timeline li.r .tl-role { color: var(--muted); } .tl-name { font-weight: 600; } .tl-note { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 6px; } @@ -735,7 +733,7 @@ button.btn.sm { font-size: 14px; } .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-badge pre { margin: 0; width: 100%; box-sizing: border-box; overflow-x: auto; font-family: var(--f-code); font-size: 12px; 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; } @@ -751,7 +749,7 @@ button.btn.sm { font-size: 14px; } .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; } +.bi-list .rank-tag { position: absolute; top: 10px; right: 12px; font-size: 12px; 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; } } @@ -949,3 +947,114 @@ button.btn.sm { font-size: 14px; } .back-person { font: inherit; font-size: 13.5px; font-weight: 700; align-self: flex-start; margin: 0 0 8px; padding: 4px 10px; border-radius: 999px; border: 1px solid var(--rule); background: var(--surface-2); color: var(--ink); cursor: pointer; } .back-person:hover { border-color: var(--ink); } .back-person:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; } + +/* Cohort chips: only the cohorts a Builder took part in */ +.ro-strip { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 4px; } +.cc { display: inline-flex; align-items: baseline; gap: 4px; padding: 3px 8px; border-radius: 7px; font-size: 12px; font-weight: 600; line-height: 1.35; white-space: nowrap; font-variant-numeric: tabular-nums; } +.cc b { font-size: 12.5px; font-weight: 800; } +.cc i { font-style: normal; font-weight: 800; opacity: .85; } +.cc.b { background: var(--c); color: var(--on-line); } +.cc.b.live { background: transparent; color: var(--ink); outline: 1.5px dashed var(--c); outline-offset: -1.5px; } +.cc.r { background: transparent; color: var(--muted); box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--c) 55%, var(--rule)); } +.cc.r b { color: var(--ink); } +.roster-legend .cc { margin-right: 6px; } +.ro-medal, .pc-medal { display: inline-flex; vertical-align: -4px; margin-left: 6px; } +.ro-medal .medal, .pc-medal .medal { height: 21px; width: auto; } + +/* Medals on the top three cards */ +.t-medals { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; font-size: 13px; } +.t-medals > .medal { height: 46px; width: auto; flex-shrink: 0; } +.t-medals > b { font-size: 14px; font-weight: 800; margin-right: 4px; } +.t-sp { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; font-weight: 700; color: var(--muted); } + +/* Medal ladder: the rules and where they lead */ +.medal-ladder { margin: 0 0 32px; padding: 20px; border-radius: 18px; background: var(--surface); border: 1px solid var(--rule); } +.ml-head p { font-size: 14px; color: var(--muted); margin-top: 4px; max-width: 52em; } +.ml-steps { list-style: none; margin: 16px 0 0; padding: 0; display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; } +.ml-step, .ml-step.stage a { position: relative; display: flex; flex-direction: column; align-items: center; text-align: center; gap: 2px; padding: 12px 8px; border-radius: 14px; background: var(--surface-2); font-size: 12.5px; color: var(--muted); } +.ml-step:not(:last-child)::after { content: ""; position: absolute; right: -10px; top: 38px; width: 7px; height: 7px; border-top: 2px solid var(--muted); border-right: 2px solid var(--muted); transform: rotate(45deg); opacity: .6; } +.ml-step b { font-size: 14px; color: var(--ink); } +.ml-medal { height: 66px; display: grid; place-items: center; margin-bottom: 4px; } +.ml-medal .medal { height: 64px; width: auto; } +.ml-n { font-weight: 800; color: var(--ink); font-variant-numeric: tabular-nums; } +.ml-step.stage { display: block; padding: 0; background: none; } +.ml-step.stage a { height: 100%; min-height: 100%; box-sizing: border-box; justify-content: center; text-decoration: none; background: #14162B; color: rgba(255, 255, 255, .78); } +.ml-step.stage a b { color: #FFD54A; } +.ml-step.stage a:hover { outline: 2px solid #FFD54A; } +.ml-step.stage a:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; } +.ml-special { list-style: none; margin: 14px 0 0; padding: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 18px; font-size: 13px; color: var(--muted); } +.ml-special li { display: inline-flex; align-items: center; gap: 6px; } +.ml-special li.k { font-weight: 800; color: var(--ink); } +.ml-special b { color: var(--ink); } +@media (max-width: 760px) { + .ml-steps { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .ml-step::after { display: none; } + .ml-step.stage { grid-column: 1 / -1; } +} + +/* The person just opened: medal reveal in the dialog, and a mark in the lists behind it */ +.p-award { position: relative; overflow: hidden; display: flex; align-items: center; gap: 16px; padding: 16px 18px; border-radius: 16px; background: linear-gradient(118deg, #23276A, #4A45B5 70%, #6E68BC); color: #fff; } +.p-award::after { content: ""; position: absolute; top: 0; bottom: 0; left: -40%; width: 30%; background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .28), transparent); transform: skewX(-18deg); animation: award-shine 1.4s .25s ease-out 1 both; pointer-events: none; } +.p-award-medal { flex-shrink: 0; animation: award-pop .6s cubic-bezier(.2, 1.5, .45, 1) both; filter: drop-shadow(0 8px 14px rgba(0, 0, 0, .3)); } +.p-award-medal .medal { display: block; height: 104px; width: auto; } +.p-award-body { display: grid; gap: 4px; min-width: 0; } +.p-award .k { font-size: 12px; font-weight: 700; color: rgba(255, 255, 255, .72); } +.p-award .t { font-size: 20px; font-weight: 800; color: #FFD54A; letter-spacing: -.01em; } +.p-award .d { font-size: 14px; color: rgba(255, 255, 255, .9); } +.p-award .btn.ghost { justify-self: start; margin-top: 6px; color: #fff; border-color: rgba(255, 255, 255, .45); } +.p-award .btn.ghost:hover { border-color: #fff; } +.p-ribbons { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px 14px; } +.p-ribbons li { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: rgba(255, 255, 255, .8); } +.p-ribbons li > span { display: flex; flex-direction: column; line-height: 1.3; } +.p-ribbons b { color: #fff; font-size: 13px; } +@keyframes award-pop { from { transform: scale(.4) rotate(-20deg); opacity: 0; } to { transform: none; opacity: 1; } } +@keyframes award-shine { to { left: 120%; } } +@media (max-width: 480px) { .p-award { padding: 14px; gap: 12px; } .p-award-medal .medal { height: 84px; } .p-award .t { font-size: 18px; } } +.ro.is-picked { background: color-mix(in srgb, var(--c) 9%, var(--surface)); box-shadow: inset 4px 0 0 var(--c); } +.tcard.is-picked > button, .pcard.is-picked > button { border-color: var(--c); box-shadow: 0 0 0 3px color-mix(in srgb, var(--c) 30%, transparent); } + +/* Academic events the team works on */ +.stage { margin-top: 28px; padding: 22px; border-radius: 18px; background: var(--banner); color: #fff; scroll-margin-top: 84px; } +.stage-k { font-size: 12.5px; font-weight: 800; color: #FFD54A; } +.stage-head h3 { font-size: 20px; margin-top: 2px; } +.stage-head p { font-size: 14px; color: rgba(255, 255, 255, .82); margin-top: 4px; max-width: 52em; } +.stage-list { list-style: none; margin: 16px 0 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; } +.stage-list a { display: flex; flex-direction: column; gap: 8px; height: 100%; box-sizing: border-box; padding: 16px; border-radius: 14px; background: rgba(255, 255, 255, .1); border: 1px solid rgba(255, 255, 255, .18); color: #fff; text-decoration: none; transition: background .15s ease, border-color .15s ease; } +.stage-list a:hover { background: rgba(255, 255, 255, .16); border-color: rgba(255, 255, 255, .45); } +.stage-list a:focus-visible { outline: 3px solid #FFD54A; outline-offset: 2px; } +.sg-year { font-family: var(--f-display); font-weight: 800; font-size: 28px; line-height: 1; letter-spacing: -.02em; } +.sg-body { display: flex; flex-direction: column; gap: 2px; } +.sg-body b { font-size: 15.5px; line-height: 1.4; } +.sg-en { font-size: 12px; color: rgba(255, 255, 255, .66); } +.sg-about { font-size: 13.5px; color: rgba(255, 255, 255, .88); margin-top: 4px; } +.sg-when { font-size: 12.5px; color: rgba(255, 255, 255, .7); font-variant-numeric: tabular-nums; } +.sg-go { margin-top: auto; font-size: 13px; font-weight: 700; color: #FFD54A; } +.sg-go::after { content: " ↗"; } +@media (max-width: 860px) { .stage-list { grid-template-columns: minmax(0, 1fr); } .stage { padding: 18px; } } + +/* Banner line for the academic partnership */ +.academy { margin: 10px 0 0; max-width: 600px; } +.academy a { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; padding: 8px 12px; border-radius: 10px; background: rgba(255, 255, 255, .1); border: 1px solid rgba(255, 255, 255, .2); color: rgba(255, 255, 255, .85); font-size: 12.5px; text-decoration: none; } +.academy a::after { content: "›"; font-weight: 800; color: #FFD54A; } +.academy a:hover { background: rgba(255, 255, 255, .16); } +.academy a:focus-visible { outline: 3px solid #FFD54A; outline-offset: 2px; } +.academy b { color: #fff; font-weight: 800; } + +/* Builder pages: medal in the hero and the medal box */ +.bp-medal { margin-left: auto; flex-shrink: 0; filter: drop-shadow(0 10px 18px rgba(0, 0, 0, .3)); animation: award-pop .6s cubic-bezier(.2, 1.5, .45, 1) both; } +.bp-medal .medal { display: block; height: 148px; width: auto; } +.bp-tier { font-size: 12.5px; font-weight: 800; padding: 3px 10px; border-radius: 999px; background: rgba(255, 255, 255, .16); color: #FFD54A; } +.bp-tier-line { display: flex; align-items: center; gap: 10px; color: var(--ink) !important; } +.bp-tier-line .medal { height: 52px; width: auto; flex-shrink: 0; } +.bp-tier-line span { display: flex; flex-direction: column; font-size: 13px; color: var(--muted); } +.bp-tier-line b { font-size: 16px; color: var(--ink); } +.bp-medals .p-ribbons li { color: var(--muted); } +.bp-medals .p-ribbons b { color: var(--ink); } +.bi-medal { display: inline-flex; vertical-align: -4px; } +.bi-medal .medal { height: 21px; width: auto; } +@media (max-width: 640px) { .bp-medal .medal { height: 96px; } .bp-hero { flex-wrap: wrap; } .bp-medal { position: absolute; right: 20px; top: 20px; } .bp-hero { position: relative; } .bp-hero .bp-id { padding-right: 0; } } +.bp-kicker { flex-wrap: wrap; row-gap: 6px; } +.bp-kicker .rank-tag, .bp-tier { white-space: nowrap; } +/* inside a dialog on phones the toast sits at the bottom, clear of the close button */ +@media (max-width: 900px) { dialog .toast { top: auto; bottom: calc(16px + env(safe-area-inset-bottom, 0px)); transform: translate(-50%, 180%); } dialog .toast.show { transform: translate(-50%, 0); } } +.bp-badge pre code { font-size: inherit; } diff --git a/assets/js/hub.js b/assets/js/hub.js index 7aec156..afad7d4 100644 --- a/assets/js/hub.js +++ b/assets/js/hub.js @@ -46,6 +46,7 @@ d.projects.forEach((p) => { p.cohort = int(p.cohort); p.runners = p.runners == null ? null : int(p.runners); }); d.events.forEach((e) => { e.order = int(e.order); e.year = int(e.year); e.participants = e.participants == null ? null : int(e.participants); }); d.routes.forEach((r) => { r.id = tok(r.id); }); + d.stages.items = (d.stages.items || []).map((x) => ({ ...x, id: tok(x.id), year: int(x.year) })); d.builders.forEach((b) => { (b.runnerOf || []).forEach((x) => { x.cohort = int(x.cohort); }); }); return d; } @@ -72,7 +73,7 @@ /* ---------- data ---------- */ async function load() { - const names = ["repos", "lines", "levels", "map", "routes", "projects", "events", "articles", "builders", "growth", "stats", "featured", "daily"]; + const names = ["repos", "lines", "levels", "map", "routes", "projects", "events", "articles", "builders", "growth", "stats", "featured", "daily", "stages"]; const data = await Promise.all(names.map((n) => getJSON(`data/${n}.json`))); const d = cleanData(Object.fromEntries(names.map((n, i) => [n, data[i]]))); d.repos.forEach((r) => { @@ -84,7 +85,7 @@ (d.map.paid || []).forEach((p) => state.repos.set(p.id, { repo: p.id, kind: "paid", status: "paid", builders: [], cohorts: [], ...p })); state.lineList = d.lines; d.lines.forEach((l) => state.lines.set(l.id, l)); - Object.assign(state, { levels: d.levels, map: d.map, routes: d.routes, projects: d.projects, events: d.events, articles: d.articles, builders: d.builders, growth: d.growth, stats: d.stats, featured: d.featured, daily: d.daily }); + Object.assign(state, { levels: d.levels, map: d.map, routes: d.routes, projects: d.projects, events: d.events, articles: d.articles, builders: d.builders, growth: d.growth, stats: d.stats, featured: d.featured, daily: d.daily, stages: d.stages }); const pos = new Map(d.map.stations.map((s) => [s.id, s])); d.lines.forEach((l) => { @@ -130,6 +131,41 @@ return b.linkedin ? `${esc(name)}` : esc(name); } + /* ---------- Builder medals ---------- + One medal for how many cohorts someone has led, plus two special ones. The same rules + live in scripts/build_builders.py so the hub, Builder pages and share images agree. */ + const TIERS = [ + { id: "honor", min: 5, name: "명예 Builder", rule: "5개 기수 이상", c: ["#9C95E8", "#33378C"], ink: "#FFFFFF" }, + { id: "gold", min: 3, name: "베테랑 Builder", rule: "3개 기수 이상", c: ["#F8D978", "#B98318"], ink: "#3A2700" }, + { id: "silver", min: 2, name: "다시 온 Builder", rule: "2개 기수", c: ["#E6EBF1", "#8995A6"], ink: "#1B2230" }, + { id: "bronze", min: 1, name: "Builder", rule: "첫 기수", c: ["#E3A36A", "#8E5226"], ink: "#FFFFFF" } + ]; + const SPECIALS = [ + { id: "mentor", name: "멘토", rule: "러너 20명 이상과 함께", c: ["#63D2B1", "#1B7A61"], ink: "#FFFFFF", has: (p) => p.runners >= 20 }, + { id: "rise", name: "러너에서 Builder로", rule: "러너로 참여한 뒤 Builder가 됨", c: ["#FFAE8C", "#C04F33"], ink: "#FFFFFF", has: (p) => p.runnerOf.some((x) => x.cohort < p.first) } + ]; + const tierOf = (p) => TIERS.find((t) => p.cohorts.length >= t.min); + const specialsOf = (p) => SPECIALS.filter((m) => m.has(p)); + let medalUid = 0; + function medalSVG(m, size, n) { + const id = `mg${++medalUid}`, h = Math.round(size * (m.min ? 84 / 64 : 1)); + const grad = ``; + if (m.min) { + return ``; + } + const glyph = m.id === "mentor" + ? `20+` + : ``; + return ``; + } + const tierMedal = (p, size) => medalSVG(tierOf(p), size, p.cohorts.length); + /* ---------- growth ---------- */ function growthSeries() { const runners = (state.growth.uniqueRunners || []).map((x) => ({ x: `${x.cohort}기`, y: x.cumulative, add: x.new })); @@ -491,21 +527,34 @@ } /* One cell per cohort: filled = Builder that cohort (number = runners), ring = joined as a runner. */ - function stripHTML(p, cohorts) { - const ranAs = new Set(p.runnerOf.map((x) => x.cohort)); - return `${cohorts.map((c) => { + /* the cohorts someone took part in, oldest first: filled = led as Builder, outlined = joined as a runner */ + function stripHTML(p) { + const ran = new Set(p.runnerOf.map((x) => x.cohort).filter((c) => !p.byCohort.has(c))); + const all = [...new Set([...p.cohorts, ...ran])].sort((a, b) => a - b); + const chips = all.map((c) => { const its = p.byCohort.get(c); - if (its) { - const n = its.reduce((s, i) => s + (i.runners || 0), 0); - const live = its.some((i) => i.live); - const label = `${c}기 Builder: ${its.map((i) => i.title || i.name).join(", ")}${n ? `, 러너 ${n}명` : ""}${live ? ", 진행 중" : ""}`; - return `${n || ""}`; - } - if (ranAs.has(c)) return ``; - return ``; - }).join("")}`; + if (!its) return `
  • ${c}기러너
  • `; + const n = its.reduce((s, i) => s + (i.runners || 0), 0); + const live = its.some((i) => i.live); + const label = `${c}기 Builder: ${its.map((i) => i.title || i.name).join(", ")}${n ? `, 러너 ${n}명` : ""}${live ? ", 진행 중" : ""}`; + return `
  • ${c}기${live ? "진행 중" : n ? `러너 ${n}` : "Builder"}${its.length > 1 ? `· ${its.length}개` : ""}
  • `; + }).join(""); + return ``; + } + + /* medal rules and the stage they lead to, with how many people hold each */ + function renderMedalLadder(list) { + const box = $("#medal-ladder"); + if (!box) return; + const count = (t) => list.filter((p) => tierOf(p) === t).length; + const steps = [...TIERS].reverse().map((t) => `
  • ${medalSVG(t, 48, t.min)}${esc(t.name)}${esc(t.rule)}${count(t)}명
  • `).join(""); + const sp = SPECIALS.map((m) => `
  • ${medalSVG(m, 22)}${esc(m.name)}${esc(m.rule)} · ${list.filter((p) => m.has(p)).length}명
  • `).join(""); + box.innerHTML = `

    Builder 메달과 다음 무대

    Builder로 연 기수가 쌓일수록 메달이 바뀝니다. 오래 함께한 Builder에게는 학회와 Data Forum에서 발표할 기회가 열립니다.

    +
      ${steps}
    1. 발표 무대비즈니스 실험 심포지엄 · Data Forum
    + `; } + function renderContrib() { const list = builderStats(); state.people = list; @@ -515,9 +564,10 @@ $("#top3").innerHTML = list.slice(0, 3).map((p) => { const recent = [...p.items].reverse().slice(0, 2); - return `
  • `; }; $("#people-rail").innerHTML = rest.length ? `
    ` : ""; - const head = ``; - $("#roster").innerHTML = head + `
      ${list.map((p) => `
    1. - ${p.rank}${avatarHTML(p.name, "sm")}${esc(stripLabel(p))} - ${stripHTML(p, cohorts)} + const head = ``; + $("#roster").innerHTML = head + `
        ${list.map((p) => `
      1. + ${p.rank}${avatarHTML(p.name, "sm")}${esc(tierOf(p).name)}${specialsOf(p).map((m) => ` · ${esc(m.name)}`).join("")} + ${stripHTML(p)}
          ${p.items.slice(-3).reverse().map((i) => `
        • ${i.cohort}기${i.repo ? `` : i.link ? `${esc(i.name)}` : `${esc(i.name)}`}${i.live ? `진행 중` : ""}
        • `).join("")}${p.items.length > 3 ? `
        • ` : ""}
        러너 ${fmt(p.runners)}명프로젝트 ${p.projects}개
      2. `).join("")}
      `; @@ -546,7 +596,8 @@ const contributors = state.stats.totals && state.stats.totals.contributors; $("#n-contrib").textContent = list.length; $("#n-contrib").title = `Builder ${list.length}명`; $("#contrib-sub").textContent = `Builder ${list.length}명이 ${cohorts.length}개 기수 동안 프로젝트 ${state.projects.length}개를 열었습니다.${contributors ? ` GitHub 기여자는 ${fmt(contributors)}명입니다.` : ""}`; - $("#roster-legend").innerHTML = `
    2. Builder로 연 기수, 숫자는 함께한 러너 수
    3. 러너로 참여한 기수
    4. 진행 중
    5. 칸은 왼쪽부터 ${cohorts[0]}기–${cohorts[cohorts.length - 1]}기
    6. `; + $("#roster-legend").innerHTML = `
    7. 8기러너 6Builder로 연 기수와 함께한 러너 수
    8. 7기러너러너로 참여한 기수
    9. 13기진행 중지금 진행 중
    10. `; + renderMedalLadder(list); } /* Three lines a Builder writes after a project; nothing shows until they do. */ @@ -556,11 +607,28 @@ return `
      회고
      ${row("왜 만들었나", r.why)}${row("무엇을 배웠나", r.learned)}${row("다음 사람에게", r.next)}
      `; } + function awardHTML(p) { + const t = tierOf(p), sp = specialsOf(p); + const said = `${p.cohorts.length > 1 ? `${p.cohorts.length}개 기수(${p.cohorts.join("·")}기)` : `${p.first}기`}에서 프로젝트 ${p.projects}개를 열고${p.runners ? ` 러너 ${fmt(p.runners)}명과 함께했습니다.` : " 첫 러너를 기다리고 있습니다."}`; + return `
      + ${tierMedal(p, 84)} +

      Builder 메달

      ${esc(t.name)}

      ${esc(said)}

      + ${sp.length ? `
        ${sp.map((m) => `
      • ${medalSVG(m, 26)}${esc(m.name)}${esc(m.rule)}
      • `).join("")}
      ` : ""} +
      +
      `; + } + /* the person just opened stays marked in the lists behind the dialog */ + function markPerson(name) { + document.querySelectorAll("[data-person-row].is-picked").forEach((el) => el.classList.remove("is-picked")); + document.querySelectorAll("[data-person-row]").forEach((el) => { if (el.dataset.personRow === name) el.classList.add("is-picked"); }); + } + function openPerson(name, source) { const p = (state.people || []).find((x) => x.name === name); if (!p) return; const info = builderInfo(name); state.lastPerson = name; + markPerson(name); track("open_builder", { builder: info.slug || name, source: source || "hub" }); if (window.hubSeen) window.hubSeen("builders", info.slug || name, 3, "builder"); const runnerItems = p.runnerOf.map((x) => { @@ -583,6 +651,7 @@
      ${p.rank <= 3 ? `Top ${p.rank}` : ""}${esc(tenure(p))}
      ${avatarHTML(p.name, "lg")}

      ${esc(p.name)}

      ${info.linkedin ? `LinkedIn` : ""}
      + ${awardHTML(p)} ${info.slug ? `
      Builder 페이지
      ` : ""} ${info.intro ? `

      ${esc(info.intro)}

      ` : ""}
      • ${p.projects}프로젝트
      • ${p.cohorts.length}기수
      • ${fmt(p.runners)}함께한 러너
      • ${repos.length}레포
      @@ -657,6 +726,7 @@ x.tabIndex = on ? 0 : -1; $("#" + x.getAttribute("aria-controls")).hidden = !on; }); + if (state.mapScroll) state.mapScroll(); if (id === "tab-course") shelfSpy(); paintNav(); } @@ -723,6 +793,21 @@ } /* ---------- events ---------- */ + /* academic events the team works on with 한국경영정보학회 */ + function renderStages() { + const st = state.stages, box = $("#stage"); + if (!st || !box || !st.items.length) return; + const evp = (x, placement) => esc(JSON.stringify({ conference: x.id, placement })); + box.innerHTML = `

      With ${esc(st.host)}

      학회와 함께 여는 무대

      ${esc(st.note)}

      +
        ${st.items.map((x) => `
      1. + ${x.year}${esc(x.name)}${esc(x.en)}${esc(x.about)}${esc(x.when)} · ${esc(x.place)}행사 사이트
      2. `).join("")}
      `; + const hl = $("#academy"); + if (hl) { + const years = (re) => st.items.filter((x) => re.test(x.id)).map((x) => x.year).sort().join("·"); + hl.innerHTML = `With ${esc(st.host)}비즈니스 실험 심포지엄 ${years(/^kbes/)} · 인과추론 여름 워크숍 ${years(/^kswci/)}`; + } + } + function renderEvents() { const list = [...state.events].sort((a, b) => (b.upcoming ? 1 : 0) - (a.upcoming ? 1 : 0) || b.order - a.order); const max = Math.max(...list.map((e) => e.participants || 0)) || 1; @@ -1003,6 +1088,67 @@ const mark = $(".banner-mark"); if (mark && mark.complete && mark.naturalWidth) c.drawImage(mark, W - 300, 70, 210, 210); } + function drawMedal(c, cx, cy, r, m, n) { + const g = c.createLinearGradient(cx - r, cy - r, cx + r, cy + r); + g.addColorStop(0, m.c[0]); g.addColorStop(1, m.c[1]); + if (m.min) { + const k = r / 26, top = cy - 56 * k, x = (v) => cx + (v - 32) * k, y = (v) => top + v * k; + c.fillStyle = "#2A2E7A"; c.beginPath(); c.moveTo(x(15), y(0)); c.lineTo(x(30), y(0)); c.lineTo(x(38), y(32)); c.lineTo(x(23), y(32)); c.fill(); + c.fillStyle = "#A9A4F5"; c.beginPath(); c.moveTo(x(49), y(0)); c.lineTo(x(34), y(0)); c.lineTo(x(26), y(32)); c.lineTo(x(41), y(32)); c.fill(); + } + c.fillStyle = g; c.beginPath(); c.arc(cx, cy, r, 0, Math.PI * 2); c.fill(); + c.strokeStyle = "rgba(255,255,255,.6)"; c.lineWidth = r * 0.06; c.beginPath(); c.arc(cx, cy, r * 0.79, 0, Math.PI * 2); c.stroke(); + c.fillStyle = "rgba(255,255,255,.28)"; c.beginPath(); c.ellipse(cx - r * 0.35, cy - r * 0.45, r * 0.35, r * 0.19, -0.5, 0, Math.PI * 2); c.fill(); + if (!m.min) return; + c.fillStyle = m.ink; c.textAlign = "center"; + c.font = `800 ${Math.round(r * 0.72)}px "Pretendard Variable", Pretendard, sans-serif`; c.fillText(String(n), cx, cy + r * 0.2); + c.font = `700 ${Math.round(r * 0.28)}px "Pretendard Variable", Pretendard, sans-serif`; c.fillText("기수", cx, cy + r * 0.6); + c.textAlign = "left"; + } + function drawMedalCard(canvas, p) { + const W = 1200, H = 630, c = canvas.getContext("2d"), t = tierOf(p), sp = specialsOf(p), info = builderInfo(p.name); + canvas.width = W; canvas.height = H; + const g = c.createLinearGradient(0, 0, W, H); + g.addColorStop(0, "#1C1F57"); g.addColorStop(0.55, "#333689"); g.addColorStop(1, "#6E68BC"); + c.fillStyle = g; c.fillRect(0, 0, W, H); + const glow = c.createRadialGradient(930, 300, 0, 930, 300, 360); + glow.addColorStop(0, "rgba(255,255,255,.22)"); glow.addColorStop(1, "rgba(255,255,255,0)"); + c.fillStyle = glow; c.fillRect(0, 0, W, H); + const font = (w, px) => `${w} ${px}px "Pretendard Variable", Pretendard, "Apple SD Gothic Neo", "Noto Sans KR", sans-serif`; + c.fillStyle = "rgba(255,255,255,.82)"; c.font = font(600, 26); c.fillText("Causal Inference Hub Builder · 가짜연구소 인과추론팀", 80, 104); + c.fillStyle = "#FFD54A"; c.font = font(800, 34); c.fillText(t.name, 80, 186); + c.fillStyle = "#fff"; c.font = font(800, 96); c.fillText(p.name, 80, 292); + c.fillStyle = "rgba(255,255,255,.88)"; c.font = font(600, 30); + c.fillText(`${tenure(p)}`, 80, 352); + c.fillText(`프로젝트 ${p.projects}개 · 함께한 러너 ${fmt(p.runners)}명`, 80, 398); + sp.forEach((m, i) => { + const x = 80 + i * 300, y = 470; + drawMedal(c, x + 22, y, 22, m); + c.fillStyle = m.ink; c.font = font(800, 15); c.textAlign = "center"; c.fillText(m.id === "mentor" ? "20+" : "↑", x + 22, y + 6); c.textAlign = "left"; + c.fillStyle = "#fff"; c.font = font(700, 24); c.fillText(m.name, x + 56, y + 9); + }); + c.fillStyle = "rgba(255,255,255,.7)"; c.font = font(500, 22); + c.fillText(`causalinferencelab.github.io/builders/${info.slug || ""}`, 80, H - 56); + drawMedal(c, 930, 330, 150, t, p.cohorts.length); + } + async function openMedalCard(name) { + const p = (state.people || []).find((x) => x.name === name); + if (!p) return; + const info = builderInfo(name); + track("medal_card_open", { builder: info.slug || name, tier: tierOf(p).id }); + $("#station-body").innerHTML = `
      + +
      + +

      ${esc(p.name)} · ${esc(tierOf(p).name)} 메달 카드

      + +

      링크드인이나 블로그, 발표 자료에 올려 보세요. Builder 페이지 링크를 함께 붙이면 이력이 바로 열립니다.

      + +
      `; + showDialog(); + try { if (document.fonts) await document.fonts.load('800 96px "Pretendard Variable"'); } catch (e) { /* draw with fallback fonts */ } + drawMedalCard($("#station-body .card-canvas"), p); + } async function openCard(levelId) { const lv = state.levels.find((l) => l.id === levelId); if (!lv || !levelDone(levelId)) return; @@ -1027,19 +1173,20 @@ function saveCard(levelId) { const canvas = $("#station-body .card-canvas"); if (!canvas) return; - const name = `causal-inference-hub-${levelId}.png`; + const medal = levelId.startsWith("medal:"), content = medal ? "medal_card" : "finish_card"; + const name = medal ? `causal-inference-hub-builder-${levelId.slice(6)}.png` : `causal-inference-hub-${levelId}.png`; canvas.toBlob(async (blob) => { if (!blob) return; const file = typeof File === "function" ? new File([blob], name, { type: "image/png" }) : null; /* phones: hand the image to the share sheet; elsewhere download it */ if (file && navigator.canShare && navigator.canShare({ files: [file] })) { - try { await navigator.share({ files: [file], title: "Causal Inference Hub 완주 카드" }); track("share", { content: "finish_card", method: "share_sheet" }); return; } catch (e) { if (e && e.name === "AbortError") return; } + try { await navigator.share({ files: [file], title: medal ? "Causal Inference Hub Builder 메달" : "Causal Inference Hub 완주 카드" }); track("share", { content, method: "share_sheet" }); return; } catch (e) { if (e && e.name === "AbortError") return; } } const a = document.createElement("a"); a.download = name; a.href = URL.createObjectURL(blob); document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 4000); - track("share", { content: "finish_card", method: "download" }); + track("share", { content, method: "download" }); }, "image/png"); } @@ -1107,6 +1254,20 @@ $("#station-body .station-close").focus(); } + /* tell people the map continues to the right when it is wider than the screen */ + function watchMapScroll() { + const box = $("#map-scroll"), hint = $("#map-hint"); + if (!box) return; + const update = () => { + const more = box.scrollWidth - box.clientWidth > 4; + hint.hidden = !more; + box.classList.toggle("more", more && box.scrollLeft + box.clientWidth < box.scrollWidth - 4); + }; + box.addEventListener("scroll", update, { passive: true }); + window.addEventListener("resize", update); + state.mapScroll = update; + update(); + } function closeStation() { const d = dialog(); if (d.open) d.close(); } /* Deep links: #station= opens that entry, so other team sites (the web book, for one) can link straight to it. */ @@ -1161,6 +1322,8 @@ if (card) { openCard(card.dataset.card); return; } const nl = e.target.closest("[data-next-level]"); if (nl) { closeStation(); gotoLevel(nl.dataset.nextLevel); return; } + const mc = e.target.closest("[data-medal-card]"); + if (mc) { openMedalCard(mc.dataset.medalCard); return; } const dl = e.target.closest("[data-card-save]"); if (dl) { saveCard(dl.dataset.cardSave); return; } const ride = e.target.closest("[data-ride]"); @@ -1239,7 +1402,7 @@ $("#shelf").addEventListener("scroll", shelfSpy, { passive: true }); renderContrib(); renderLegend(); renderMap(); wireTabs(); - renderFeatured(); renderPartner(); renderEvents(); renderArticles(); renderDaily(); + renderFeatured(); renderPartner(); renderEvents(); renderStages(); renderArticles(); renderDaily(); watchMapScroll(); if (location.hash === "#contrib") { selectTab("tab-contrib"); requestAnimationFrame(() => $("#curriculum").scrollIntoView()); } openFromHash(); if (location.hash === "#start" || new URLSearchParams(location.search).has("quiz")) openQuiz(); diff --git a/assets/og/builders/bang-taemo.jpg b/assets/og/builders/bang-taemo.jpg index f9a0b53..0107f1f 100644 Binary files a/assets/og/builders/bang-taemo.jpg and b/assets/og/builders/bang-taemo.jpg differ diff --git a/assets/og/builders/cho-haechang.jpg b/assets/og/builders/cho-haechang.jpg index cc57a76..8aea1d5 100644 Binary files a/assets/og/builders/cho-haechang.jpg and b/assets/og/builders/cho-haechang.jpg differ diff --git a/assets/og/builders/choi-seyoung.jpg b/assets/og/builders/choi-seyoung.jpg index ee69b1c..f69df2f 100644 Binary files a/assets/og/builders/choi-seyoung.jpg and b/assets/og/builders/choi-seyoung.jpg differ diff --git a/assets/og/builders/kim-sangwon.jpg b/assets/og/builders/kim-sangwon.jpg index a45c67d..feb2b2c 100644 Binary files a/assets/og/builders/kim-sangwon.jpg and b/assets/og/builders/kim-sangwon.jpg differ diff --git a/assets/og/builders/kim-seongsu.jpg b/assets/og/builders/kim-seongsu.jpg index 48d296b..9a9414e 100644 Binary files a/assets/og/builders/kim-seongsu.jpg and b/assets/og/builders/kim-seongsu.jpg differ diff --git a/assets/og/builders/kwon-myeongseok.jpg b/assets/og/builders/kwon-myeongseok.jpg index e7a79a0..935ef1d 100644 Binary files a/assets/og/builders/kwon-myeongseok.jpg and b/assets/og/builders/kwon-myeongseok.jpg differ diff --git a/assets/og/builders/kwon-namtaek.jpg b/assets/og/builders/kwon-namtaek.jpg index a043fe8..11e48b0 100644 Binary files a/assets/og/builders/kwon-namtaek.jpg and b/assets/og/builders/kwon-namtaek.jpg differ diff --git a/assets/og/builders/lee-dongwook.jpg b/assets/og/builders/lee-dongwook.jpg index 768c1b3..46b6b50 100644 Binary files a/assets/og/builders/lee-dongwook.jpg and b/assets/og/builders/lee-dongwook.jpg differ diff --git a/assets/og/builders/park-isak.jpg b/assets/og/builders/park-isak.jpg index a183f91..42a7ea1 100644 Binary files a/assets/og/builders/park-isak.jpg and b/assets/og/builders/park-isak.jpg differ diff --git a/assets/og/builders/park-sangho.jpg b/assets/og/builders/park-sangho.jpg index 2d8aa3b..b4ce9ba 100644 Binary files a/assets/og/builders/park-sangho.jpg and b/assets/og/builders/park-sangho.jpg differ diff --git a/assets/og/builders/shin-jinsoo.jpg b/assets/og/builders/shin-jinsoo.jpg index 1cb1a2c..3b877aa 100644 Binary files a/assets/og/builders/shin-jinsoo.jpg and b/assets/og/builders/shin-jinsoo.jpg differ diff --git a/data/stages.json b/data/stages.json new file mode 100644 index 0000000..56080c2 --- /dev/null +++ b/data/stages.json @@ -0,0 +1,36 @@ +{ + "host": "한국경영정보학회", + "note": "한국경영정보학회가 주최하는 학회 행사에 가짜연구소 인과추론팀이 협력하고 있습니다. 오래 함께한 Builder에게는 이런 학회와 Data Forum에서 발표할 기회가 열립니다.", + "items": [ + { + "id": "kbes2026", + "year": 2026, + "name": "한국 비즈니스 실험 심포지엄 2026", + "en": "Korea Business Experimentation Symposium 2026", + "when": "2026.07.11", + "place": "한국과학기술회관", + "about": "생성형 AI 시대의 데이터 사이언스와 실험", + "url": "https://sites.google.com/view/business-experimentation2026/home" + }, + { + "id": "kbes2025", + "year": 2025, + "name": "한국 비즈니스 실험 심포지엄 2025", + "en": "Korea Business Experimentation Symposium 2025", + "when": "2025.07.12", + "place": "고려대학교 LG-POSCO 경영관", + "about": "A/B 테스트와 현장 실험을 다루는 국내 첫 산학 심포지엄", + "url": "https://sites.google.com/view/business-experimentation2025/home" + }, + { + "id": "kswci2024", + "year": 2024, + "name": "인과추론 여름 워크숍 2024", + "en": "Korea Summer Workshop on Causal Inference 2024", + "when": "2024.06.13 – 06.28", + "place": "온라인", + "about": "실무를 위한 인과추론과 의사결정을 위한 데이터과학", + "url": "https://sites.google.com/view/causal-inference2024/home" + } + ] +} diff --git a/index.html b/index.html index 5574ced..629b4c3 100644 --- a/index.html +++ b/index.html @@ -55,6 +55,7 @@

      Causal Inference Hub

      With 한빛미디어팀이 옮긴 책과 그 내용을 실무로 잇는 강의입니다. 둘 다 커리큘럼 L2에 들어 있습니다.

        +