diff --git a/.jules/palette.md b/.jules/palette.md index 2dcd639e..a871b67a 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -1,3 +1,7 @@ + +## 2024-08-08 - Clickable Drop Zones +**Learning:** When expanding the drop zone to the entire container, users expect to be able to click anywhere in that large zone to open the file chooser, especially since they have a pointer cursor. Not hooking up the click event creates a broken mental model where only the tiny native button works. +**Action:** Always add a click listener to expanded drop zones to programmatically trigger the hidden file input, while ignoring clicks on other interactive elements. ## 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..a5725f79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,3 +11,6 @@ ### Fixed - 단일·일괄 대상 크기 입력을 비웠을 때 이전 custom validity와 `aria-invalid` 상태를 즉시 초기화해 현재 필수 입력 상태를 정확히 전달합니다. + +### Changed +- 파일 업로드 드롭 존 영역을 클릭 가능하게 개선 (포인터 커서 및 클릭 동작 추가) diff --git a/saas_web.py b/saas_web.py index 0ef95a1e..4b66d9ba 100644 --- a/saas_web.py +++ b/saas_web.py @@ -157,7 +157,7 @@ async def add_security_headers(request: Request, call_next): .help-text { color: #6c757d; font-size: 0.85em; display: inline-block; margin-top: 4px; } .spinner { display: inline-block; width: 1em; height: 1em; vertical-align: -0.125em; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spinner-border .75s linear infinite; margin-right: 8px; } @keyframes spinner-border { to { transform: rotate(360deg); } } - .box { transition: background-color 0.2s, border-color 0.2s; } + .box { transition: background-color 0.2s, border-color 0.2s; cursor: pointer; } .box.dragover { background-color: #f8f9fa; border-color: #0056b3; border-style: dashed; } .preset-container { margin-top: 8px; display: flex; gap: 8px; flex-wrap: wrap; } .preset-btn { padding: 4px 8px; font-size: 0.85em; background-color: #e9ecef; color: #495057; border: 1px solid #ced4da; border-radius: 4px; cursor: pointer; } @@ -393,6 +393,10 @@ async def add_security_headers(request: Request, call_next): updateFileSizePreview(fileInput); } }, false); + dropZone.addEventListener('click', (e) => { + if (['INPUT', 'BUTTON', 'LABEL'].includes(e.target.tagName)) return; + fileInput.click(); + }); if (batchDropZone) { batchDropZone.addEventListener('drop', (e) => { let dt = e.dataTransfer; @@ -402,6 +406,10 @@ async def add_security_headers(request: Request, call_next): updateBatchFilePreview(batchFileInput); } }, false); + 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..13adf575 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -44,6 +44,7 @@ def test_get_ui_includes_accessible_file_input_helpers(self): self.assertIn('aria-describedby="file_help file_size_preview"', html) self.assertIn('id="file_help"', html) self.assertIn('class="required-star" aria-hidden="true"', html) + self.assertIn("fileInput.click()", html) def test_get_ui_includes_binary_file_size_validation(self): response = client.get("/") @@ -596,6 +597,7 @@ def test_get_ui_includes_batch_upload_form(self): self.assertIn('onchange="updateBatchFilePreview(this)"', html) self.assertIn('id="batch_files_preview"', html) self.assertIn('function updateBatchFilePreview(input)', html) + self.assertIn("batchFileInput.click()", html) @unittest.skipUnless(_HAS_FASTAPI, "fastapi not installed (optional integration dependency)")