Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
14 changes: 14 additions & 0 deletions saas_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand Down Expand Up @@ -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;
}

Expand Down
Loading