diff --git a/.jules/palette.md b/.jules/palette.md index a1cf208b..ee9ca5f9 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -1,3 +1,7 @@ +## 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`. + ## 2024-07-12 - Intercepting batch form submissions for testing visual loading states **Learning:** Extending the learning from 2024-06-13, intercepting form submissions using `e.preventDefault()` via `page.evaluate()` is essential for capturing screenshot and video evidence of loading states (e.g., button disabling, spinner appearing) on forms like batch upload where the submission would normally reload the page or download an archive. **Action:** When testing visual loading states with Playwright, always inject an event listener using `page.evaluate()` to call `e.preventDefault()` on the form's `submit` event to freeze the UI in its loading state for verification. diff --git a/CHANGELOG.md b/CHANGELOG.md index ebfe94a6..56f817c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,3 +4,4 @@ ### Added - 다중 파일 업로드 선택 시 즉각적인 파일 개수 피드백 및 제한 초과 경고 메시지 추가 - 일괄 업로드 폼에 대상 바이트 프리셋 버튼과 총 파일 크기 미리보기를 추가하여 사용성을 개선했습니다. +- 클라이언트 측 폼 검증 시 하드코딩된 '5 GiB' 텍스트를 동적으로 변환되도록 수정하고 일괄 업로드 폼에 최대 크기(MAX_UPLOAD_BYTES) 검증 피드백을 추가했습니다. diff --git a/saas_web.py b/saas_web.py index 3a7b0352..8270c9e9 100644 --- a/saas_web.py +++ b/saas_web.py @@ -230,9 +230,10 @@ async def add_security_headers(request: Request, call_next): } const text = formatBinaryBytes(file.size); if (file.size > MAX_UPLOAD_BYTES) { - input.setCustomValidity('File exceeds 5 GiB limit.'); + const limitText = formatBinaryBytes(MAX_UPLOAD_BYTES); + input.setCustomValidity('File exceeds ' + limitText + ' limit.'); input.setAttribute('aria-invalid', 'true'); - preview.innerText = 'Selected file size: ' + text + ' (exceeds 5 GiB limit)'; + preview.innerText = 'Selected file size: ' + text + ' (exceeds ' + limitText + ' limit)'; preview.style.color = '#dc3545'; return; } @@ -325,6 +326,15 @@ async def add_security_headers(request: Request, call_next): preview.style.color = '#dc3545'; return; } + + if (totalSize > MAX_UPLOAD_BYTES) { + const limitText = formatBinaryBytes(MAX_UPLOAD_BYTES); + input.setCustomValidity('Total file size exceeds ' + limitText + ' limit.'); + input.setAttribute('aria-invalid', 'true'); + preview.innerText = 'Selected ' + files.length + ' file(s) (' + formatBinaryBytes(totalSize) + ', exceeds ' + limitText + ' limit)'; + preview.style.color = '#dc3545'; + return; + } preview.innerText = 'Selected ' + files.length + ' file(s) (' + formatBinaryBytes(totalSize) + ')'; } diff --git a/tests/test_saas_web.py b/tests/test_saas_web.py index 57b879d1..cd45dbc3 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -52,7 +52,9 @@ def test_get_ui_includes_binary_file_size_validation(self): self.assertIn("const MAX_UPLOAD_BYTES = 5 * 1024 * 1024 * 1024;", html) self.assertIn("['B', 'KiB', 'MiB', 'GiB']", html) - self.assertIn("File exceeds 5 GiB limit.", html) + self.assertIn("const limitText = formatBinaryBytes(MAX_UPLOAD_BYTES);", html) + self.assertIn("File exceeds ' + limitText + ' limit.", html) + self.assertIn("Total file size exceeds ' + limitText + ' limit.", html) self.assertIn("preview.style.color = '#0f6674';", html) self.assertIn('onchange="updateFileSizePreview(this)"', html)