diff --git a/.jules/palette.md b/.jules/palette.md index 2dcd639e..fb0e12e7 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -81,3 +81,6 @@ ## 2024-08-04 - 숫자 입력 필드 빈 문자열 상태 초기화 처리 **학습:** 숫자 입력 필드에서 빈 문자열('')을 입력할 때 브라우저는 이전의 유효하지 않은 상태를 암시적으로 유지하므로, 사용자 정의 검증을 명시적으로 초기화하지 않으면 네이티브 HTML5 유효성 검사가 정상 작동하지 않을 수 있음을 확인했습니다. **실행:** 인라인 검증 스크립트 작성 시 빈 문자열 상태를 별도로 확인하여 this.setCustomValidity('') 및 this.removeAttribute('aria-invalid')를 명시적으로 호출하는 로직을 추가해야 합니다. +## 2024-08-05 - File type validation for drop zones +**Learning:** The HTML `accept` attribute does not reliably prevent invalid files from being dropped. Always add explicit client-side JavaScript validation against `file.type` and provide inline accessibility feedback using `setCustomValidity` and `aria-invalid` to ensure a smooth, accessible user experience. +**Action:** Add explicit client-side validation against `file.type` for all file inputs and drop zones. diff --git a/saas_web.py b/saas_web.py index 63265e94..04180626 100644 --- a/saas_web.py +++ b/saas_web.py @@ -228,6 +228,13 @@ async def add_security_headers(request: Request, call_next): preview.innerText = ''; return; } + if (file.type && !file.type.startsWith('audio/') && !file.type.startsWith('video/')) { + input.setCustomValidity('Unsupported file type. Please select an audio or video file.'); + input.setAttribute('aria-invalid', 'true'); + preview.innerText = 'Unsupported file type (' + file.type + '). Please select an audio or video file.'; + preview.style.color = '#dc3545'; + return; + } const text = formatBinaryBytes(file.size); if (file.size > MAX_UPLOAD_BYTES) { const limitText = formatBinaryBytes(MAX_UPLOAD_BYTES); @@ -329,8 +336,24 @@ async def add_security_headers(request: Request, call_next): } let totalSize = 0; + let hasInvalidType = false; + let invalidType = ''; for (let i = 0; i < files.length; i++) { - totalSize += files[i].size; + const f = files[i]; + if (f.type && !f.type.startsWith('audio/') && !f.type.startsWith('video/')) { + hasInvalidType = true; + invalidType = f.type; + break; + } + totalSize += f.size; + } + + if (hasInvalidType) { + input.setCustomValidity('Unsupported file type. Please select audio or video files only.'); + input.setAttribute('aria-invalid', 'true'); + preview.innerText = 'Unsupported file type (' + invalidType + ') detected. Please select audio or video files only.'; + preview.style.color = '#dc3545'; + return; } if (files.length > 20) {