diff --git a/.jules/palette.md b/.jules/palette.md
index 9a12c9de..32d8ad5f 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-08-05 - 빈 디렉토리와 내용이 있는 디렉토리 시각적/시맨틱 구분
+**학습:** 디렉토리 목록에서 비어있는 디렉토리와 그렇지 않은 디렉토리가 시각적으로나 시맨틱하게 동일하게 표시되면, 사용자는 빈 디렉토리 내부를 불필요하게 탐색하게 되어 불편함을 느낍니다.
+**조치:** 디렉토리 탐색 시 해당 디렉토리가 비어있는지 미리 확인하여, 비어있을 경우 닫힌 폴더 아이콘 대신 열린 폴더 아이콘(📂)을 사용하고 `aria-label`과 `title` 속성에 '빈 디렉토리'라고 명시함으로써 불필요한 내비게이션을 방지하고 접근성을 개선하십시오.
diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt
index b4558624..b2410592 100644
--- a/src/main/kotlin/html4tree/main.kt
+++ b/src/main/kotlin/html4tree/main.kt
@@ -240,7 +240,7 @@ fun write_index_file(curr_dir: File, content: String) {
}
}
-fun process_dir(curr_dir: File, excludeSet: Set? = null, dirFiles: Array? = null){
+fun process_dir(curr_dir: File, excludeSet: Set? = null, dirFiles: Array? = null, newDirectoryStream: (java.nio.file.Path) -> java.nio.file.DirectoryStream = { Files.newDirectoryStream(it) }){
val exclude: Set = excludeSet ?: process_ignore_file(curr_dir)
@@ -360,9 +360,21 @@ ${cssContent}
} catch (e: Exception) {
}
if (!isSymbolicLink) {
+ var isEmptyDir = false
+ if (isLinkedDirectory) {
+ try {
+ val stream = 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')
}
diff --git a/src/test/kotlin/html4tree/AttrExceptionTest.kt b/src/test/kotlin/html4tree/AttrExceptionTest.kt
index 98973eb1..04461674 100644
--- a/src/test/kotlin/html4tree/AttrExceptionTest.kt
+++ b/src/test/kotlin/html4tree/AttrExceptionTest.kt
@@ -21,4 +21,20 @@ class AttrExceptionTest {
tempDir.deleteRecursively()
}
}
+
+ @Test
+ fun testExceptionInNewDirectoryStream() {
+ val tempDir = Files.createTempDirectory("stream_test").toFile()
+ try {
+ val subDir = File(tempDir, "subdir")
+ subDir.mkdir()
+ process_dir(tempDir, setOf(), arrayOf(subDir)) { _ ->
+ throw java.io.IOException("Mock exception")
+ }
+ val indexHtml = File(tempDir, "index.html")
+ assertTrue(indexHtml.exists())
+ } finally {
+ tempDir.deleteRecursively()
+ }
+ }
}
diff --git a/src/test/kotlin/html4tree/MainTest.kt b/src/test/kotlin/html4tree/MainTest.kt
index 13494714..a548cafa 100644
--- a/src/test/kotlin/html4tree/MainTest.kt
+++ b/src/test/kotlin/html4tree/MainTest.kt
@@ -86,6 +86,18 @@ class MainTest {
}
}
+ @Test
+ fun testEmptyDirectoryIcon() {
+ val emptyDir = File(tempDir, "empty_subdir")
+ emptyDir.mkdir()
+ go(tempDir.absolutePath, 0)
+ val indexFile = File(tempDir, "index.html")
+ val htmlContent = indexFile.readText()
+ assertTrue(htmlContent.contains("📂"))
+ assertTrue(htmlContent.contains("aria-label=\"empty_subdir 빈 디렉토리\""))
+ assertTrue(htmlContent.contains("title=\"empty_subdir 빈 디렉토리\""))
+ }
+
@Test
fun testGoEmptyDir() {
go(tempDir.absolutePath, -1)
@@ -298,6 +310,7 @@ class MainTest {
fun testProcessDir() {
val subdir = File(tempDir, "subdir")
subdir.mkdir()
+ File(subdir, "some_file.txt").createNewFile()
File(tempDir, "file1.txt").createNewFile()
File(tempDir, "test.ignore").createNewFile()
File(tempDir, ".html4ignore").writeText("*.ignore")