Skip to content
Draft
4 changes: 4 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@
## 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-03 - External link new-window warning
**Learning:** Localizing a `title` tooltip is useful supplemental metadata, but it is not a dependable advance warning for keyboard, touch, or assistive-technology users. W3C's current guidance treats new-window warnings as advisory good practice and notes that `title` is poorly discoverable without pointer hover.
**Action:** When this site keeps `target="_blank"`, provide the localized new-window notice through ordinary accessible link semantics (for example, referenced descriptive text) and treat `title` as supplemental only. Verify both KO/EN switching and browser keyboard/touch/accessibility behavior before claiming completion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# CHANGELOG

## [Unreleased]
- **UX/접근성 개선**: 모든 외부 링크(`target="_blank"`)에 새 창 열림 사전 경고를 안내하는 visually hidden 설명을 `aria-describedby`로 연결하여 접근성을 향상시켰습니다. (2026-09-04)
- **보안 개선**: `i18n.js`에서 잘못된 언어 요청 시 `console.warn` 메시지에 사용자 입력값이 직접 포함되지 않도록 수정하여 로그 인젝션(Log Injection) 취약점을 제거했습니다.
- **성능 개선**: `.skip-link` 애니메이션을 `top`에서 `transform: translateY()`로 변경하여 전환 중 레이아웃 재계산을 줄일 수 있도록 했습니다. 실제 효과는 브라우저별 측정 대상입니다.
- **렌더링 힌트 정합성**: 첫 화면의 eager 이미지와 단일 LCP 후보에서 강제 `decoding="async"`를 제거해 HTML 표준의 기본 `auto` 판단에 맡기고, 지연 로드 이미지에는 비동기 디코딩 힌트를 유지했습니다. 정적 테스트가 eager, lazy, LCP 후보 집합의 존재와 조합을 검증하며, 실제 LCP 효과는 배포 후 실측 대상으로 유지합니다.
Expand Down
8 changes: 6 additions & 2 deletions i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ const messages = {
"work.fourTitle": "작업 흐름",
"work.fourBody": "반복 탐색은 줄이고 근거 확인과 사람의 판단은 남기는 흐름.",
"footer.founded": "Founded by",
"footer.line": "Context into judgment. Judgment into action."
"footer.line": "Context into judgment. Judgment into action.",
"common.newWindow": "새 창에서 열림"
},
en: {
metaTitle: "Contextual Wisdom Lab",
Expand Down Expand Up @@ -291,7 +292,8 @@ const messages = {
"work.fourTitle": "Agentic workflows",
"work.fourBody": "Workflows that reduce repeated search while preserving evidence checks and human judgment.",
"footer.founded": "Founded by",
"footer.line": "Context into judgment. Judgment into action."
"footer.line": "Context into judgment. Judgment into action.",
"common.newWindow": "Opens in a new window"
}
};

Expand Down Expand Up @@ -375,6 +377,8 @@ function setLanguage(lang) {
node.textContent = newText;
}
});


}

langButtons.forEach((button) => {
Expand Down
33 changes: 17 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" aria-describedby="new-window-desc" 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" aria-describedby="new-window-desc" 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" aria-describedby="new-window-desc" 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" aria-describedby="new-window-desc" 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" aria-describedby="new-window-desc" 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" aria-describedby="new-window-desc" 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" aria-describedby="new-window-desc" 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" aria-describedby="new-window-desc" 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" aria-describedby="new-window-desc" 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" aria-describedby="new-window-desc" 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" aria-describedby="new-window-desc" 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" aria-describedby="new-window-desc" 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" aria-describedby="new-window-desc" 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" aria-describedby="new-window-desc" 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" aria-describedby="new-window-desc" 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,9 +431,10 @@ <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" aria-describedby="new-window-desc" 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>
<span id="new-window-desc" class="visually-hidden" data-i18n="common.newWindow">새 창에서 열림</span>
</body>
</html>
13 changes: 13 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -845,3 +845,16 @@ main:focus-visible {
scroll-behavior: auto !important;
}
}

/* ⚡ Bolt: Visually hidden utility for accessible semantics */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
27 changes: 26 additions & 1 deletion test_i18n.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
</head>
<body>
<div data-i18n="hero.title">맥락지혜 연구실</div>
<a href="https://github.com" target="_blank" aria-describedby="new-window-desc">GitHub</a>
<span id="new-window-desc" class="visually-hidden" data-i18n="common.newWindow">새 창에서 열림</span>

<button type="button" data-lang="ko" aria-pressed="true">KO</button>
<button type="button" data-lang="en" aria-pressed="false">EN</button>
<img id="footer-logo" src="assets/context-wisdom-lab-logo.svg" alt="맥락지혜 연구실 · Contextual Wisdom Lab">
Expand Down Expand Up @@ -35,7 +38,7 @@
}
}

requestAnimationFrame(() => {
requestAnimationFrame(async () => {
console.log("Running tests...");

assertEqual(currentLang, "ko", "Initial language should be 'ko'");
Expand All @@ -46,10 +49,32 @@
const titleNode = document.querySelector('[data-i18n="hero.title"]');
assertEqual(titleNode.textContent, "Contextual Wisdom Lab", "Title text should change to English");

const descNode = document.getElementById("new-window-desc");
assertEqual(descNode.textContent, "Opens in a new window", "Description text should change to English");

// Test 3: Switch back to KO updates nodes correctly
setLanguage("ko");
assertEqual(currentLang, "ko", "Language should be 'ko' after setting back");
assertEqual(titleNode.textContent, "맥락지혜 연구실", "Title text back to Korean");
assertEqual(descNode.textContent, "새 창에서 열림", "Description text back to Korean");

// External links need a localized non-title warning that keyboard/touch/AT users can reach.
const response = await fetch("index.html", { cache: "no-store" });
const pageText = await response.text();
const page = new DOMParser().parseFromString(pageText, "text/html");
const externalLinks = Array.from(page.querySelectorAll('a[target="_blank"]'));
const allLinksDescribeNewWindow = externalLinks.length > 0 && externalLinks.every((link) => {
const ids = (link.getAttribute("aria-describedby") || "").trim().split(/\s+/).filter(Boolean);
return ids.some((id) => {
const description = page.getElementById(id);
return description && description.getAttribute("data-i18n") === "common.newWindow";
});
});
assertEqual(
allLinksDescribeNewWindow,
true,
"Every target=_blank link should reference a localized non-title new-window description"
);

// Test 4: Sentinel Security - Input Validation for untrusted inputs
setLanguage("__proto__");
Expand Down
Loading