From a9f9c61d0fb06a35a8b94ce3a0373c8558f31e0c Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Thu, 23 Jul 2026 20:57:11 +0000
Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=EB=94=94=EB=A0=89=ED=86=A0?=
=?UTF-8?q?=EB=A6=AC=20=EB=AA=A9=EB=A1=9D=20=EB=B9=88=20=ED=8F=B4=EB=8D=94?=
=?UTF-8?q?=20=EC=8B=9C=EA=B0=81=ED=99=94=20=EB=B0=8F=20=EC=A0=91=EA=B7=BC?=
=?UTF-8?q?=EC=84=B1=20=ED=96=A5=EC=83=81=20(UX)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 빈 디렉토리 렌더링 시 일반적인 닫힌 폴더 아이콘 대신 열린 폴더 아이콘(`📂`)을 표시하도록 개선
- `aria-label`을 '빈 디렉토리'로 변경하여 스크린 리더 사용자에게 컨텍스트 제공
- `.empty-dir` CSS를 Flexbox로 개선하여 아이콘 및 텍스트 정렬 최적화
- `Files.newDirectoryStream`을 사용하여 모든 파일을 로드하지 않고 성능 효율적으로 빈 폴더 여부 확인
- 100% JaCoCo 테스트 커버리지 유지
---
.jules/palette.md | 4 ++++
src/main/kotlin/html4tree/main.kt | 21 ++++++++++++++++++---
src/test/kotlin/html4tree/MainTest.kt | 8 ++++++++
3 files changed, 30 insertions(+), 3 deletions(-)
diff --git a/.jules/palette.md b/.jules/palette.md
index 9a12c9de..020780f7 100644
--- a/.jules/palette.md
+++ b/.jules/palette.md
@@ -48,3 +48,7 @@
## 2024-08-01 - 네이티브 브라우저 UI의 다크 모드 지원 강제
**학습:** CSS 미디어 쿼리(`@media (prefers-color-scheme: dark)`)를 통해 다크 모드를 지원하더라도, 브라우저의 네이티브 UI 요소(스크롤바, 기본 폼 컨트롤, 기본 백그라운드 등)는 테마 변경을 인식하지 못해 어두운 테마 환경에서 밝은 스크롤바가 표시되는 등 시각적 불일치를 초래합니다.
**조치:** 항상 HTML 문서의 `
` 영역에 `` 메타 태그를 명시적으로 추가하여 브라우저 수준에서 사용자의 시스템 테마(다크 모드 등)를 완전히 상속받아 일관성 있는 네이티브 UI를 렌더링하도록 보장하십시오.
+
+## 2024-05-25 - 디렉토리 목록 빈 폴더 시각화 향상
+**학습:** 디렉토리 목록에서 비어있는 디렉토리에 대해 일반 디렉토리와 동일한 닫힌 폴더 아이콘과 '디렉토리' 라벨을 제공하면, 사용자가 불필요하게 비어있는 폴더를 클릭하여 탐색하게 되는 UX 저하가 발생합니다.
+**조치:** 디렉토리 트리를 렌더링할 때 하위 디렉토리가 비어있는지 확인(성능을 위해 `Files.newDirectoryStream` 사용)하고, 비어있는 경우 열린 폴더 아이콘(`📂`)과 명확한 라벨('빈 디렉토리')을 제공하여 사용자에게 즉각적인 시각적 피드백을 제공하십시오.
diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt
index b4558624..cf3aa49f 100644
--- a/src/main/kotlin/html4tree/main.kt
+++ b/src/main/kotlin/html4tree/main.kt
@@ -307,6 +307,9 @@ fun process_dir(curr_dir: File, excludeSet: Set? = null, dirFiles: Array
padding: 0.5rem;
opacity: 0.7;
font-style: italic;
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5rem;
}
"""
@@ -360,9 +363,21 @@ ${cssContent}
} catch (e: Exception) {
}
if (!isSymbolicLink) {
+ var isEmptyDir = false
+ if (isLinkedDirectory) {
+ try {
+ val stream = Files.newDirectoryStream(it.toPath())
+ try {
+ isEmptyDir = !stream.iterator().hasNext()
+ } finally {
+ stream.close()
+ }
+ } catch (e: Exception) {
+ }
+ }
val encodedHref = if (isLinkedDirectory) { "./${fileName.urlEncodePath()}/" } else { "./${fileName.urlEncodePath()}" }
- val ariaLabel = "${fileName} ${if (isLinkedDirectory) { "디렉토리" } else { "파일" }}".escapeHtml()
- val icon = if (isLinkedDirectory) { "📁" } else { "📄" }
+ val ariaLabel = "${fileName} ${if (isLinkedDirectory) { if (isEmptyDir) "빈 디렉토리" else "디렉토리" } else { "파일" }}".escapeHtml()
+ val icon = if (isLinkedDirectory) { if (isEmptyDir) "📂" else "📁" } else { "📄" }
l.append(""" ${icon} ${fileName.escapeHtml()}""")
l.append('\n')
}
@@ -370,7 +385,7 @@ ${cssContent}
}
if(l.isEmpty()){
- l.append(""" 이 디렉토리는 비어 있습니다.
""")
+ l.append(""" 📂 이 디렉토리는 비어 있습니다.
""")
l.append('\n')
}
diff --git a/src/test/kotlin/html4tree/MainTest.kt b/src/test/kotlin/html4tree/MainTest.kt
index 13494714..b761e7bd 100644
--- a/src/test/kotlin/html4tree/MainTest.kt
+++ b/src/test/kotlin/html4tree/MainTest.kt
@@ -94,6 +94,7 @@ class MainTest {
val htmlContent = indexFile.readText()
assertTrue(htmlContent.contains(""))
assertTrue(htmlContent.contains("이 디렉토리는 비어 있습니다."))
+ assertTrue(htmlContent.contains("📂"))
assertTrue(htmlContent.contains("role=\"list\""))
}
@@ -298,6 +299,9 @@ class MainTest {
fun testProcessDir() {
val subdir = File(tempDir, "subdir")
subdir.mkdir()
+ File(subdir, "child.txt").createNewFile()
+ val emptydir = File(tempDir, "emptydir")
+ emptydir.mkdir()
File(tempDir, "file1.txt").createNewFile()
File(tempDir, "test.ignore").createNewFile()
File(tempDir, ".html4ignore").writeText("*.ignore")
@@ -320,9 +324,13 @@ class MainTest {
assertTrue(htmlContent.contains("title=\"file1.txt 파일\""))
assertTrue(htmlContent.contains("aria-label=\"subdir 디렉토리\""))
assertTrue(htmlContent.contains("title=\"subdir 디렉토리\""))
+ assertTrue(htmlContent.contains("aria-label=\"emptydir 빈 디렉토리\""))
+ assertTrue(htmlContent.contains("title=\"emptydir 빈 디렉토리\""))
assertTrue(htmlContent.contains("file1.txt"))
assertTrue(htmlContent.contains("subdir/"))
+ assertTrue(htmlContent.contains("emptydir/"))
assertTrue(htmlContent.contains("📁"))
+ assertTrue(htmlContent.contains("📂"))
assertFalse(htmlContent.contains("test.ignore"))
assertTrue(htmlContent.contains("Content-Security-Policy"))
val hashMatch = Regex("""style-src '(sha256-[A-Za-z0-9+/=]+)'""").find(htmlContent)?.groupValues?.get(1)