From 1d0fe2560d3ee92807ee46264ccd51f92ca80826 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 11 Sep 2026 17:42:14 +0000 Subject: [PATCH] feat: add client-side file type validation in UI Add three-state UX validation for file uploads (valid, warning for unknown MIME type, error for invalid type). --- .jules/palette.md | 4 ++++ saas_web.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/.jules/palette.md b/.jules/palette.md index 2dcd639e..073e0dce 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -81,3 +81,7 @@ ## 2024-08-04 - 숫자 입력 필드 빈 문자열 상태 초기화 처리 **학습:** 숫자 입력 필드에서 빈 문자열('')을 입력할 때 브라우저는 이전의 유효하지 않은 상태를 암시적으로 유지하므로, 사용자 정의 검증을 명시적으로 초기화하지 않으면 네이티브 HTML5 유효성 검사가 정상 작동하지 않을 수 있음을 확인했습니다. **실행:** 인라인 검증 스크립트 작성 시 빈 문자열 상태를 별도로 확인하여 this.setCustomValidity('') 및 this.removeAttribute('aria-invalid')를 명시적으로 호출하는 로직을 추가해야 합니다. + +## 2024-09-12 - 클라이언트 측 파일 유형 3단계 유효성 검사 UX +**학습:** 파일 입력의 HTML `accept` 속성은 쉽게 우회될 수 있습니다. `file.type`에 대해 엄격한 클라이언트 측 유효성 검사를 수행하면 브라우저가 MIME 유형을 감지하지 못할 경우(빈 문자열로 나타남) 유효한 파일임에도 업로드를 차단할 수 있습니다. 추가로, 유효하지 않은 파일 선택 후 유효한 파일로 변경했을 때 상태를 초기화하지 않으면 폼 제출이 영구적으로 차단됩니다. +**실행:** 파일 유형 유효성 검사를 위한 3단계 UX를 구현합니다: 알려진 유효한 유형은 허용하고, 알려지지 않은 유형(빈 `file.type`)에 대해서는 경고 색상(#856404)을 사용하여 경고만 표시(차단 안 함)하며, 유효하지 않은 유형은 `setCustomValidity` 및 `aria-invalid`를 사용하여 차단합니다. 그리고 항상 유효성 검사 초기에 이전 오류 상태를 빈 문자열로 리셋해야 합니다. diff --git a/saas_web.py b/saas_web.py index 63265e94..4bb07dc1 100644 --- a/saas_web.py +++ b/saas_web.py @@ -237,6 +237,17 @@ async def add_security_headers(request: Request, call_next): preview.style.color = '#dc3545'; return; } + if (file.type && !file.type.startsWith('audio/') && !file.type.startsWith('video/')) { + input.setCustomValidity('Invalid file type: ' + file.type + '. Please select audio or video.'); + input.setAttribute('aria-invalid', 'true'); + preview.innerText = 'Selected file size: ' + text + ' (invalid type: ' + file.type + ')'; + preview.style.color = '#dc3545'; + return; + } else if (!file.type) { + preview.innerText = 'Selected file size: ' + text + ' (warning: unknown file type)'; + preview.style.color = '#856404'; + return; + } preview.innerText = 'Selected file size: ' + text; } @@ -329,8 +340,16 @@ async def add_security_headers(request: Request, call_next): } let totalSize = 0; + let invalidType = null; + let unknownTypeCount = 0; + for (let i = 0; i < files.length; i++) { totalSize += files[i].size; + if (files[i].type && !files[i].type.startsWith('audio/') && !files[i].type.startsWith('video/')) { + invalidType = files[i].type; + } else if (!files[i].type) { + unknownTypeCount++; + } } if (files.length > 20) { @@ -349,6 +368,19 @@ async def add_security_headers(request: Request, call_next): preview.style.color = '#dc3545'; return; } + + if (invalidType) { + input.setCustomValidity('Invalid file type: ' + invalidType + '. Please select audio or video.'); + input.setAttribute('aria-invalid', 'true'); + preview.innerText = 'Selected ' + files.length + ' file(s) (' + formatBinaryBytes(totalSize) + ', invalid type: ' + invalidType + ')'; + preview.style.color = '#dc3545'; + return; + } else if (unknownTypeCount > 0) { + preview.innerText = 'Selected ' + files.length + ' file(s) (' + formatBinaryBytes(totalSize) + ', warning: ' + unknownTypeCount + ' unknown file type' + (unknownTypeCount > 1 ? 's' : '') + ')'; + preview.style.color = '#856404'; + return; + } + preview.innerText = 'Selected ' + files.length + ' file(s) (' + formatBinaryBytes(totalSize) + ')'; }