Naruon
+Naruon
메일, 첨부, 일정, 작업을 맥락으로 묶어 판단과 실행으로 연결하는 AI 이메일 워크스페이스입니다.
diff --git a/.Jules/palette.md b/.Jules/palette.md index 8a7cf4c..4f2ec31 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -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-06 - 외부 링크 새 창 열림 접근성 속성 지원 추가 +**Learning:** 새 창에서 열리는 외부 링크에 스크린 리더용 텍스트가 없으면 사용자가 혼란을 느낄 수 있으며 접근성 문제가 발생합니다. 다국어를 지원하는 사이트에서 `title`이나 `aria-label` 같은 접근성 속성을 추가할 때는 정적으로 텍스트를 하드코딩하지 않고 번역 스크립트(`i18n.js`)를 확장하여 동적으로 언어 변경이 반영되도록 해야 함을 배웠습니다. +**Action:** 새 창에서 열리는 링크에는 항상 접근성을 위한 속성(`data-i18n-title`)을 추가합니다. 커스텀 다국어 스크립트 수정 시에는 기존 `textContent`가 덮어써지지 않도록 조건부 처리를 해야 합니다. diff --git a/CHANGELOG.md b/CHANGELOG.md index e097a55..5d08d15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,3 +17,8 @@ - **성능 개선**: `i18n.js`에서 초기 로드 시 기본 언어가 한국어(ko)인 경우 불필요한 DOM 순회 및 텍스트 업데이트를 생략하도록 개선했습니다. - **테스트 추가**: 다국어 처리 로직의 무결성을 검증하기 위해 `test_i18n.html` 테스트 파일을 추가했습니다. - **보안 개선**: 메인 페이지(`index.html`)의 Content-Security-Policy를 `default-src 'none'` 기반의 엄격한 화이트리스트 방식으로 강화하여 잠재적인 공격 표면을 최소화했습니다. + +## [Unreleased] + +### Added +- **UI/UX**: 새 창에서 열리는 외부 링크에 접근성을 위한 다국어 지원 툴팁(`title` 속성) 추가. diff --git a/i18n.js b/i18n.js index 00b81d3..d06eaab 100644 --- a/i18n.js +++ b/i18n.js @@ -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", @@ -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" } }; @@ -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); + } } }); } diff --git a/index.html b/index.html index a2dbbd7..25c8e2c 100644 --- a/index.html +++ b/index.html @@ -42,7 +42,7 @@ 프로젝트 Fork 작업 - GitHub + GitHub
메일, 첨부, 일정, 작업을 맥락으로 묶어 판단과 실행으로 연결하는 AI 이메일 워크스페이스입니다.
PostgreSQL 스키마를 리버스 엔지니어링하고 ERD와 DDL 공유 흐름으로 관리하는 클라우드 MVP입니다.
곡을 섹션, 역할, 템포, 연습 우선순위로 분석하는 로컬 우선 리허설 앱입니다.
긴 녹음을 메타데이터를 보존한 FLAC/Opus 조각으로 변환하는 Python CLI입니다.
스캔된 일본어 신문 PDF를 기사, 제목, 본문, 이미지 구조의 DOM형 JSON으로 파싱하는 API입니다.
트리 편집, 진행률 계산, CSV/JSON, 주간 Gantt를 지원하는 정적 HTML/CSS/JS WBS 플래너입니다.
바이브코딩 앱을 위한 보안 가드레일입니다. AI 개발 도구 규칙, 정적 점검, 리뷰와 수정 프롬프트를 다룹니다.
MLSIRM/MLS2PLM 모형의 시뮬레이션, 적합, 복원 진단을 빠르게 수행하는 Python/Rust 패키지입니다.
DNSBL 게시 기능을 갖춘 Rust 우선 WAF/IDS/AI SOC 게이트웨이입니다.
Founded by - Seongho Bae. + Seongho Bae. Context into judgment. Judgment into action.
diff --git a/tests/test_i18n_ux.py b/tests/test_i18n_ux.py new file mode 100644 index 0000000..dbc9fcd --- /dev/null +++ b/tests/test_i18n_ux.py @@ -0,0 +1,10 @@ +"""Test that i18n.js supports data-i18n-title properly.""" +from pathlib import Path +import re + +def test_i18n_title_support() -> None: + """Test that i18n.js statically parses [data-i18n-title].""" + content = Path("i18n.js").read_text(encoding="utf-8") + assert "[data-i18n], [data-i18n-title]" in content + assert 'node.hasAttribute("data-i18n-title")' in content + assert 'node.setAttribute("title"' in content diff --git a/tests/test_index_ux.py b/tests/test_index_ux.py new file mode 100644 index 0000000..a4b59d2 --- /dev/null +++ b/tests/test_index_ux.py @@ -0,0 +1,11 @@ +"""Test that index.html has UX enhancements.""" +from pathlib import Path +import re + +def test_external_links_have_title() -> None: + """Test that external links have data-i18n-title.""" + content = Path("index.html").read_text(encoding="utf-8") + links = re.findall(r']*target="_blank"[^>]*>', content) + for link in links: + assert 'data-i18n-title' in link + assert 'common.newWindow' in link