diff --git a/.jules/palette.md b/.jules/palette.md index 2dcd639e..cbdaebc8 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -81,3 +81,7 @@ ## 2024-08-04 - 숫자 입력 필드 빈 문자열 상태 초기화 처리 **학습:** 숫자 입력 필드에서 빈 문자열('')을 입력할 때 브라우저는 이전의 유효하지 않은 상태를 암시적으로 유지하므로, 사용자 정의 검증을 명시적으로 초기화하지 않으면 네이티브 HTML5 유효성 검사가 정상 작동하지 않을 수 있음을 확인했습니다. **실행:** 인라인 검증 스크립트 작성 시 빈 문자열 상태를 별도로 확인하여 this.setCustomValidity('') 및 this.removeAttribute('aria-invalid')를 명시적으로 호출하는 로직을 추가해야 합니다. + +## 2026-09-06 - Drag-and-drop file type client-side validation +**Learning:** The native `accept` attribute on file inputs often fails to prevent invalid file drops. Without explicit client-side validation, dragging unsupported files allows form submission to proceed, resulting in a jarring server error page instead of an accessible inline warning. +**Action:** Always implement explicit client-side `file.type` checking for file inputs (especially those acting as drop zones) and show inline validation errors via `setCustomValidity` and `aria-invalid`. diff --git a/saas_web.py b/saas_web.py index 63265e94..7d29da71 100644 --- a/saas_web.py +++ b/saas_web.py @@ -229,6 +229,13 @@ async def add_security_headers(request: Request, call_next): return; } const text = formatBinaryBytes(file.size); + if (!file.type.startsWith('audio/') && !file.type.startsWith('video/')) { + input.setCustomValidity('Unsupported content type; select an audio or video file.'); + input.setAttribute('aria-invalid', 'true'); + preview.innerText = 'Selected file type unsupported: ' + file.name; + preview.style.color = '#dc3545'; + return; + } if (file.size > MAX_UPLOAD_BYTES) { const limitText = formatBinaryBytes(MAX_UPLOAD_BYTES); input.setCustomValidity('File exceeds ' + limitText + ' limit.'); @@ -330,6 +337,13 @@ async def add_security_headers(request: Request, call_next): let totalSize = 0; for (let i = 0; i < files.length; i++) { + if (!files[i].type.startsWith('audio/') && !files[i].type.startsWith('video/')) { + input.setCustomValidity('Unsupported content type; select audio or video files.'); + input.setAttribute('aria-invalid', 'true'); + preview.innerText = 'Selected file type unsupported: ' + files[i].name; + preview.style.color = '#dc3545'; + return; + } totalSize += files[i].size; }