diff --git a/.jules/palette.md b/.jules/palette.md index 2dcd639e..0aa21155 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -1,3 +1,7 @@ +## 2024-08-10 - 드롭존 클릭 이벤트 지원 +**Learning:** 파일 입력 버튼 대신 넓은 드롭존 영역을 클릭해도 파일 선택 창이 열리도록 하면 모바일 환경이나 마우스 사용자 모두에게 더 넓은 타겟 영역을 제공해 접근성 및 사용성이 향상됩니다. 또한 내부 요소 클릭은 무시하도록 하여 폼 인터랙션 충돌을 막는 것이 중요합니다. +**Action:** 폼의 루트 컨테이너(드롭존)에 클릭 이벤트 리스너를 추가하여 내부의 hidden input을 `.click()`으로 트리거하되, 대화형 자식 요소(input, button, label 등)를 명시적으로 제외하는 조건을 작성하십시오. + ## 2024-07-15 - Dynamic Size formatting and Total Size Validation **Learning:** Hardcoding human-readable sizes (like '5 GiB') in validation error messages is error-prone when the underlying constant changes. Moreover, failing to validate total upload size against backend limits (e.g., MAX_UPLOAD_BYTES) in batch file uploads frustrates users who wait for a large upload to finish only to get a server-side 413 Payload Too Large error. **Action:** Always format backend byte limit constants dynamically (e.g., `formatBinaryBytes(MAX_UPLOAD_BYTES)`) on the client side to display accurate error messages. For multiple file inputs, ensure both the file count and the combined file size are validated against backend limits, giving immediate inline feedback via `setCustomValidity` and `aria-invalid`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cca1ced..5c4fcf25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,3 +11,4 @@ ### Fixed - 단일·일괄 대상 크기 입력을 비웠을 때 이전 custom validity와 `aria-invalid` 상태를 즉시 초기화해 현재 필수 입력 상태를 정확히 전달합니다. +- 드롭존 영역을 클릭하여 파일 탐색기를 열 수 있도록 접근성 및 편의성 개선 diff --git a/plan.md b/plan.md new file mode 100644 index 00000000..7315b4b0 --- /dev/null +++ b/plan.md @@ -0,0 +1,22 @@ +1. **드롭존 클릭 시 파일 탐색기 열기 기능 추가 (`saas_web.py`)** + - 사용자 편의성 향상을 위해 전체 드롭존 영역(`dropZone`, `batchDropZone`)을 클릭하면 해당하는 파일 입력(input type="file") 필드를 프로그래밍 방식으로 클릭하여 파일 탐색기가 열리도록 개선합니다. + - 폼 내부의 입력 요소(INPUT), 버튼(BUTTON), 라벨(LABEL)을 클릭할 때는 이벤트가 전파되어 파일 탐색기가 열리지 않도록 `['INPUT', 'BUTTON', 'LABEL'].includes(e.target.tagName)` 조건을 추가하여 방어합니다. + - `saas_web.py` 내의 HTML `script` 영역에 `replace_with_git_merge_diff`를 사용하여 코드를 적용합니다. + +2. **UI 테스트 업데이트 (`tests/test_saas_web.py`)** + - 새롭게 추가된 기능에 대해 `test_get_ui_includes_binary_file_size_validation` 테스트 내에 `assertIn` 단언문을 추가합니다. + - `assertIn("dropZone.addEventListener('click'", html)` + - `replace_with_git_merge_diff`를 사용하여 `tests/test_saas_web.py` 코드를 업데이트합니다. + +3. **CHANGELOG 및 Palette 저널 기록** + - `CHANGELOG.md`의 끝에 변경 사항을 `cat << 'EOF' >> CHANGELOG.md` 명령어로 추가합니다. + - `.jules/palette.md` 최상단에 새로운 배움(학습)과 조치를 추가합니다. (드롭존 전체 영역을 파일 업로드 타겟으로 만들어 사용자 경험 향상). `bash`의 임시 파일을 사용하여 맨 앞에 추가합니다. + +4. **100% 테스트 커버리지 확인** + - `python3 -m pip install coverage && python3 -m coverage run -m unittest discover -s tests && python3 -m coverage report -m` 명령을 실행하여 모든 테스트가 성공하고 커버리지가 100%인지 확인합니다. + +5. **사전 커밋 단계 완료** + - Complete pre-commit steps to ensure proper testing, verification, review, and reflection are done. + +6. **제출(Submit)** + - "🎨 Palette: 드롭존 클릭하여 파일 선택 기능 추가" 형식의 PR 제목과 적절한 설명을 포함하여 커밋 및 제출합니다. PR 설명에는 무엇을, 왜, 이전/이후(변경사항), 접근성에 대한 설명이 포함되어야 합니다. diff --git a/saas_web.py b/saas_web.py index 0ef95a1e..d0ddc5d1 100644 --- a/saas_web.py +++ b/saas_web.py @@ -403,6 +403,18 @@ async def add_security_headers(request: Request, call_next): } }, false); } + + dropZone.addEventListener('click', (e) => { + if (['INPUT', 'BUTTON', 'LABEL'].includes(e.target.tagName)) return; + fileInput.click(); + }); + + if (batchDropZone) { + batchDropZone.addEventListener('click', (e) => { + if (['INPUT', 'BUTTON', 'LABEL'].includes(e.target.tagName)) return; + batchFileInput.click(); + }); + }
diff --git a/tests/test_saas_web.py b/tests/test_saas_web.py index cd45dbc3..0ee70006 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -57,6 +57,8 @@ def test_get_ui_includes_binary_file_size_validation(self): self.assertIn("Total file size exceeds ' + limitText + ' limit.", html) self.assertIn("preview.style.color = '#0f6674';", html) self.assertIn('onchange="updateFileSizePreview(this)"', html) + self.assertIn("dropZone.addEventListener('click'", html) + self.assertIn("if (['INPUT', 'BUTTON', 'LABEL'].includes(e.target.tagName)) return;", html) def test_security_headers_present_without_plain_http_hsts(self): response = client.get("/")