Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@
## 2024-07-15 - Expand clickable area of project cards
**Learning:** Using an anchor tag to wrap an entire card (block-level element) can result in verbose and confusing screen reader output. However, restricting the clickable area to just the title makes the UI harder to interact with (violating Fitts's Law).
**Action:** Apply `position: relative` to the card container and use a `::after` pseudo-element with `position: absolute; inset: 0;` on the title's anchor tag. This expands the clickable area to the whole card while keeping semantic and accessible HTML structure.
## 2026-09-05 - 외부 링크 다국어 지원 접근성 개선
**Learning:** target="_blank" 링크에 title 속성을 추가하여 접근성을 높이는 것이 좋으나, 하드코딩할 경우 다국어 지원(i18n)이 깨집니다.
**Action:** 기존 i18n 로직을 확장하여 data-i18n-title 속성을 처리하도록 하고 조건부로 텍스트를 할당함으로써 화면 낭독기가 언어 설정에 맞게 동적으로 읽을 수 있도록 합니다.
18 changes: 14 additions & 4 deletions i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const messages = {
"nav.references": "참고문헌",
"nav.work": "작업",
"nav.skipToContent": "본문으로 건너뛰기",
"externalLink": "새 창에서 열기",
"hero.title": "맥락지혜 연구실",
"hero.labName": "Contextual Wisdom Lab",
"hero.thesis": "구슬이 서 말이어도 꿰어야 보배이듯, 문서, 메일, 로그, 회의록을 맥락 안에서 엮어 사람이 무엇을 결정하고 무엇을 실행할지 보이게 하는 AI 의사결정 지원 시스템을 연구하고 만듭니다.",
Expand Down Expand Up @@ -165,6 +166,7 @@ const messages = {
"nav.references": "References",
"nav.work": "Work",
"nav.skipToContent": "Skip to main content",
"externalLink": "Opens in a new window",
"hero.title": "Contextual Wisdom Lab",
"hero.labName": "Research Lab",
"hero.thesis": "A research lab building AI decision support systems. Even a heap of beads becomes treasure only when threaded; we compose context across documents, mail, logs, and meeting notes so people can see what to decide and what to do next.",
Expand Down Expand Up @@ -365,14 +367,22 @@ function setLanguage(lang) {

if (!isInitialDefault) {
if (!i18nNodes) {
i18nNodes = document.querySelectorAll("[data-i18n]");
i18nNodes = document.querySelectorAll("[data-i18n], [data-i18n-title]");
}

// Only update textContent if it actually changed to avoid layout recalculations
i18nNodes.forEach((node) => {
const newText = dict[node.dataset.i18n];
if (newText && node.textContent !== newText) {
node.textContent = newText;
if (node.hasAttribute("data-i18n")) {
const newText = dict[node.dataset.i18n];
if (newText && node.textContent !== newText) {
node.textContent = newText;
}
}
if (node.hasAttribute("data-i18n-title")) {
const newTitle = dict[node.dataset.i18nTitle];
if (newTitle && node.getAttribute("title") !== newTitle) {
node.setAttribute("title", newTitle);
}
}
});
}
Expand Down
32 changes: 16 additions & 16 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<a href="#projects" data-i18n="nav.projects">프로젝트</a>
<a href="#forks" data-i18n="nav.forks">Fork</a>
<a href="#work" data-i18n="nav.work">작업</a>
<a target="_blank" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab">GitHub</a>
<a target="_blank" data-i18n-title="externalLink" title="새 창에서 열기" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab">GitHub</a>
</nav>
<div class="language-switch" role="group" aria-label="Language">
<button type="button" data-lang="ko" lang="ko" aria-pressed="true" aria-label="한국어">KO</button>
Expand All @@ -59,7 +59,7 @@ <h1 data-i18n="hero.title">맥락지혜 연구실</h1>
구슬이 서 말이어도 꿰어야 보배이듯, 문서, 메일, 로그, 회의록을 맥락 안에서 엮어 사람이 무엇을 결정하고 무엇을 실행할지 보이게 하는 AI 의사결정 지원 시스템을 연구하고 만듭니다.
</p>
<div class="hero-actions" role="group" aria-label="Homepage actions">
<a target="_blank" rel="noopener noreferrer" class="button primary" href="https://github.com/ContextualWisdomLab" data-i18n="hero.primaryCta">GitHub 보기</a>
<a target="_blank" data-i18n-title="externalLink" title="새 창에서 열기" rel="noopener noreferrer" class="button primary" href="https://github.com/ContextualWisdomLab" data-i18n="hero.primaryCta">GitHub 보기</a>
<a class="button secondary" href="#dikw" data-i18n="hero.secondaryCta">DIKW 보기</a>
</div>
</div>
Expand Down Expand Up @@ -254,19 +254,19 @@ <h2 data-i18n="references.title">참고문헌</h2>
<ol class="reference-list">
<li>
<span data-i18n="references.ackoff">Ackoff, R. L. (1989). From data to wisdom. Journal of Applied Systems Analysis, 16(1), 3-9.</span>
<a target="_blank" rel="noopener noreferrer" href="https://faculty.ung.edu/kmelton/documents/datawisdom.pdf">https://faculty.ung.edu/kmelton/documents/datawisdom.pdf</a>
<a target="_blank" data-i18n-title="externalLink" title="새 창에서 열기" rel="noopener noreferrer" href="https://faculty.ung.edu/kmelton/documents/datawisdom.pdf">https://faculty.ung.edu/kmelton/documents/datawisdom.pdf</a>
</li>
<li>
<span data-i18n="references.baskarada">Baskarada, S., &amp; Koronios, A. (2013). Data, information, knowledge, wisdom (DIKW): A semiotic theoretical and empirical exploration of the hierarchy and its quality dimension. Australasian Journal of Information Systems, 18(1).</span>
<a target="_blank" rel="noopener noreferrer" href="https://doi.org/10.3127/ajis.v18i1.748">https://doi.org/10.3127/ajis.v18i1.748</a>
<a target="_blank" data-i18n-title="externalLink" title="새 창에서 열기" rel="noopener noreferrer" href="https://doi.org/10.3127/ajis.v18i1.748">https://doi.org/10.3127/ajis.v18i1.748</a>
</li>
<li>
<span data-i18n="references.fricke">Frické, M. (2009). The knowledge pyramid: A critique of the DIKW hierarchy. Journal of Information Science, 35(2), 131-142.</span>
<a target="_blank" rel="noopener noreferrer" href="https://doi.org/10.1177/0165551508094050">https://doi.org/10.1177/0165551508094050</a>
<a target="_blank" data-i18n-title="externalLink" title="새 창에서 열기" rel="noopener noreferrer" href="https://doi.org/10.1177/0165551508094050">https://doi.org/10.1177/0165551508094050</a>
</li>
<li>
<span data-i18n="references.brienza">Brienza, J. P., Kung, F. Y. H., Santos, H. C., Bobocel, D. R., &amp; Grossmann, I. (2018). Wisdom, bias, and balance: Toward a process-sensitive measurement of wisdom-related cognition. Journal of Personality and Social Psychology, 115(6), 1093-1126.</span>
<a target="_blank" rel="noopener noreferrer" href="https://doi.org/10.1037/pspp0000171">https://doi.org/10.1037/pspp0000171</a>
<a target="_blank" data-i18n-title="externalLink" title="새 창에서 열기" rel="noopener noreferrer" href="https://doi.org/10.1037/pspp0000171">https://doi.org/10.1037/pspp0000171</a>
</li>
</ol>
</section>
Expand Down Expand Up @@ -327,39 +327,39 @@ <h2 data-i18n="projects.title">공개 프로젝트</h2>
</div>
<div class="naruon-grid project-grid">
<article>
<h3><a target="_blank" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab/naruon" data-i18n="projects.naruonTitle">Naruon</a></h3>
<h3><a target="_blank" data-i18n-title="externalLink" title="새 창에서 열기" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab/naruon" data-i18n="projects.naruonTitle">Naruon</a></h3>
<p data-i18n="projects.naruonBody">메일, 첨부, 일정, 작업을 맥락으로 묶어 판단과 실행으로 연결하는 AI 이메일 워크스페이스입니다.</p>
</article>
<article>
<h3><a target="_blank" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab/pg-erd-cloud" data-i18n="projects.pgErdTitle">pg-erd-cloud</a></h3>
<h3><a target="_blank" data-i18n-title="externalLink" title="새 창에서 열기" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab/pg-erd-cloud" data-i18n="projects.pgErdTitle">pg-erd-cloud</a></h3>
<p data-i18n="projects.pgErdBody">PostgreSQL 스키마를 리버스 엔지니어링하고 ERD와 DDL 공유 흐름으로 관리하는 클라우드 MVP입니다.</p>
</article>
<article>
<h3><a target="_blank" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab/bandscope" data-i18n="projects.bandscopeTitle">BandScope</a></h3>
<h3><a target="_blank" data-i18n-title="externalLink" title="새 창에서 열기" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab/bandscope" data-i18n="projects.bandscopeTitle">BandScope</a></h3>
<p data-i18n="projects.bandscopeBody">곡을 섹션, 역할, 템포, 연습 우선순위로 분석하는 로컬 우선 리허설 앱입니다.</p>
</article>
<article>
<h3><a target="_blank" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab/codec-carver" data-i18n="projects.codecCarverTitle">codec-carver</a></h3>
<h3><a target="_blank" data-i18n-title="externalLink" title="새 창에서 열기" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab/codec-carver" data-i18n="projects.codecCarverTitle">codec-carver</a></h3>
<p data-i18n="projects.codecCarverBody">긴 녹음을 메타데이터를 보존한 FLAC/Opus 조각으로 변환하는 Python CLI입니다.</p>
</article>
<article>
<h3><a target="_blank" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab/newsdom-api" data-i18n="projects.newsdomTitle">newsdom-api</a></h3>
<h3><a target="_blank" data-i18n-title="externalLink" title="새 창에서 열기" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab/newsdom-api" data-i18n="projects.newsdomTitle">newsdom-api</a></h3>
<p data-i18n="projects.newsdomBody">스캔된 일본어 신문 PDF를 기사, 제목, 본문, 이미지 구조의 DOM형 JSON으로 파싱하는 API입니다.</p>
</article>
<article>
<h3><a target="_blank" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab/scopeweave" data-i18n="projects.scopeweaveTitle">scopeweave</a></h3>
<h3><a target="_blank" data-i18n-title="externalLink" title="새 창에서 열기" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab/scopeweave" data-i18n="projects.scopeweaveTitle">scopeweave</a></h3>
<p data-i18n="projects.scopeweaveBody">트리 편집, 진행률 계산, CSV/JSON, 주간 Gantt를 지원하는 정적 HTML/CSS/JS WBS 플래너입니다.</p>
</article>
<article>
<h3><a target="_blank" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab/VibeSec" data-i18n="projects.vibesecTitle">VibeSec</a></h3>
<h3><a target="_blank" data-i18n-title="externalLink" title="새 창에서 열기" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab/VibeSec" data-i18n="projects.vibesecTitle">VibeSec</a></h3>
<p data-i18n="projects.vibesecBody">바이브코딩 앱을 위한 보안 가드레일입니다. AI 개발 도구 규칙, 정적 점검, 리뷰와 수정 프롬프트를 다룹니다.</p>
</article>
<article>
<h3><a target="_blank" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab/fast-mlsirm" data-i18n="projects.fastMlsirmTitle">fast-mlsirm</a></h3>
<h3><a target="_blank" data-i18n-title="externalLink" title="새 창에서 열기" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab/fast-mlsirm" data-i18n="projects.fastMlsirmTitle">fast-mlsirm</a></h3>
<p data-i18n="projects.fastMlsirmBody">MLSIRM/MLS2PLM 모형의 시뮬레이션, 적합, 복원 진단을 빠르게 수행하는 Python/Rust 패키지입니다.</p>
</article>
<article>
<h3><a target="_blank" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab/waf-ids-ai-soc" data-i18n="projects.wafIdsTitle">waf-ids-ai-soc</a></h3>
<h3><a target="_blank" data-i18n-title="externalLink" title="새 창에서 열기" rel="noopener noreferrer" href="https://github.com/ContextualWisdomLab/waf-ids-ai-soc" data-i18n="projects.wafIdsTitle">waf-ids-ai-soc</a></h3>
<p data-i18n="projects.wafIdsBody">DNSBL 게시 기능을 갖춘 Rust 우선 WAF/IDS/AI SOC 게이트웨이입니다.</p>
</article>
</div>
Expand Down Expand Up @@ -431,7 +431,7 @@ <h2 data-i18n="work.title">연구에서 제품으로</h2>
>
<p>
<span data-i18n="footer.founded">Founded by</span>
<a target="_blank" rel="noopener noreferrer" href="https://github.com/seonghobae">Seongho Bae</a>.
<a target="_blank" data-i18n-title="externalLink" title="새 창에서 열기" rel="noopener noreferrer" href="https://github.com/seonghobae">Seongho Bae</a>.
<span data-i18n="footer.line">Context into judgment. Judgment into action.</span>
</p>
</footer>
Expand Down
18 changes: 18 additions & 0 deletions tests/test_a11y_ux.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Test UX/A11y requirements."""
from pathlib import Path

def test_external_links_have_title() -> None:
"""Test that all external links have a dynamic title attribute for a11y."""
html = Path("index.html").read_text(encoding="utf-8")
js = Path("i18n.js").read_text(encoding="utf-8")

# Verify dynamic logic
assert "data-i18n-title" in js
assert '"externalLink": "새 창에서 열기"' in js

# Check target="_blank" has associated attributes using independent tokens
for line in html.splitlines():
if 'target="_blank"' in line:
assert "data-i18n-title" in line
assert "externalLink" in line
assert "title" in line
Loading