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)