diff --git a/.jules/palette.md b/.jules/palette.md index a1cf208b..59449c00 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -74,3 +74,6 @@ ## 2024-07-13 - 일괄 업로드 폼에 프리셋 버튼 및 파일 크기 미리보기 추가 **Learning:** 일괄 파일 업로드 폼에서 대상 바이트(target_bytes) 입력 필드만 제공하면 사용자가 원하는 용량을 바이트 단위로 정확히 계산하기 어려워 사용성이 떨어집니다. 사용자가 여러 파일을 업로드할 때 총 파일 크기를 파악하지 못해 업로드 제한을 초과하거나 잘못된 대상 바이트를 설정할 위험이 큽니다. **Action:** 일괄 파일 업로드 폼에도 단일 파일 업로드 폼과 동일하게 대상 바이트를 쉽게 선택할 수 있는 빠른 프리셋 버튼을 추가하고, `onchange` 이벤트 발생 시 선택된 모든 파일의 크기를 합산하여 사람이 읽기 쉬운 단위(MiB, GiB 등)로 미리보기를 제공하도록 JavaScript 로직을 개선했습니다. +## 2025-02-28 - Dynamic File Size and Batch Limit Validation +**Learning:** When implementing client-side file size validation, do not hardcode the human-readable size limit; instead dynamically format the limit constant. For batch uploads, ensure client-side validation checks both the file count and total combined size against backend limits, providing immediate inline feedback with `setCustomValidity` and `aria-invalid`. +**Action:** Dynamically format limit constants in validation messages and validate aggregate metrics for batch uploads client-side. diff --git a/CHANGELOG.md b/CHANGELOG.md index ebfe94a6..b3c725b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,3 +4,4 @@ ### Added - 다중 파일 업로드 선택 시 즉각적인 파일 개수 피드백 및 제한 초과 경고 메시지 추가 - 일괄 업로드 폼에 대상 바이트 프리셋 버튼과 총 파일 크기 미리보기를 추가하여 사용성을 개선했습니다. +* 🎨 Palette: [UX improvement] 배치 업로드 시 전체 파일 크기 검증 추가 및 동적 에러 메시지 포맷팅 적용 diff --git a/saas_web.py b/saas_web.py index 3a7b0352..86cdfec6 100644 --- a/saas_web.py +++ b/saas_web.py @@ -229,10 +229,11 @@ async def add_security_headers(request: Request, call_next): return; } const text = formatBinaryBytes(file.size); + const limitText = formatBinaryBytes(MAX_UPLOAD_BYTES); if (file.size > MAX_UPLOAD_BYTES) { - input.setCustomValidity('File exceeds 5 GiB limit.'); + 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,14 @@ async def add_security_headers(request: Request, call_next): preview.style.color = '#dc3545'; return; } + const limitText = formatBinaryBytes(MAX_UPLOAD_BYTES); + if (totalSize > 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..03972e28 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("input.setCustomValidity('File exceeds ' + limitText + ' limit.');", html) + self.assertIn("input.setCustomValidity('Total file size exceeds ' + limitText + ' limit.');", html) self.assertIn("preview.style.color = '#0f6674';", html) self.assertIn('onchange="updateFileSizePreview(this)"', html)