-
Notifications
You must be signed in to change notification settings - Fork 0
이미지 크기 검증 로직 및 민감 데이터 정리 #402
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1c27bf9
d98822f
6b56234
caac426
3b472e6
b768c7d
56ff11e
54fd6c5
887f589
0cde969
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,6 +4,9 @@ import Image from 'next/image'; | |||||||||||||||||||||||
| import { useState, DragEvent, ChangeEvent, useRef } from 'react'; | ||||||||||||||||||||||||
| import Button from '@/components/ui/button'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // 클라이언트에서 선제 차단하여 불필요한 413 에러 및 대용량 업로드 요청을 방지. | ||||||||||||||||||||||||
| const DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024; // 5MB | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const inputStyles = { | ||||||||||||||||||||||||
| base: 'rounded-100 flex w-full flex-col items-center justify-center border-2 p-500', | ||||||||||||||||||||||||
| dragging: 'border-border-brand bg-fill-brand-subtle-hover', | ||||||||||||||||||||||||
|
|
@@ -13,12 +16,31 @@ const inputStyles = { | |||||||||||||||||||||||
| export default function ImageUploadInput({ | ||||||||||||||||||||||||
| image, | ||||||||||||||||||||||||
| onChangeImage, | ||||||||||||||||||||||||
| maxSizeBytes = DEFAULT_MAX_SIZE_BYTES, | ||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||
| image?: string; | ||||||||||||||||||||||||
| onChangeImage: (file: File | undefined) => void; | ||||||||||||||||||||||||
| maxSizeBytes?: number; | ||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||
| const fileInputRef = useRef<HTMLInputElement>(null); | ||||||||||||||||||||||||
| const [isDragging, setIsDragging] = useState(false); | ||||||||||||||||||||||||
| const [sizeError, setSizeError] = useState<string | null>(null); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const validateAndChange = (file: File) => { | ||||||||||||||||||||||||
| if (!file.type.startsWith('image/')) { | ||||||||||||||||||||||||
| setSizeError(null); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| if (file.size > maxSizeBytes) { | ||||||||||||||||||||||||
| const maxMb = (maxSizeBytes / 1024 / 1024).toFixed(0); | ||||||||||||||||||||||||
| setSizeError(`이미지 파일 크기는 ${maxMb}MB 이하만 업로드할 수 있어요.`); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| setSizeError(null); | ||||||||||||||||||||||||
| onChangeImage(file); | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const handleOpenFileDialog = () => { | ||||||||||||||||||||||||
| fileInputRef.current?.click(); | ||||||||||||||||||||||||
|
|
@@ -49,77 +71,80 @@ export default function ImageUploadInput({ | |||||||||||||||||||||||
| e.stopPropagation(); | ||||||||||||||||||||||||
| setIsDragging(false); | ||||||||||||||||||||||||
| const file = e.dataTransfer.files[0]; | ||||||||||||||||||||||||
| if (file && file.type.startsWith('image/')) { | ||||||||||||||||||||||||
| onChangeImage(file); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| if (file) validateAndChange(file); | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => { | ||||||||||||||||||||||||
| const file = e.target.files?.[0]; | ||||||||||||||||||||||||
| if (file && file.type.startsWith('image/')) { | ||||||||||||||||||||||||
| onChangeImage(file); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| if (file) validateAndChange(file); | ||||||||||||||||||||||||
| e.target.value = ''; // 같은 파일 재업로드 허용을 위해 입력값 초기화 | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const handleRemove = () => { | ||||||||||||||||||||||||
| setSizeError(null); | ||||||||||||||||||||||||
| if (fileInputRef.current) fileInputRef.current.value = ''; | ||||||||||||||||||||||||
| onChangeImage(undefined); | ||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||
| onDrop={handleDrop} | ||||||||||||||||||||||||
| onDragEnter={handleDragEnter} | ||||||||||||||||||||||||
| onDragLeave={handleDragLeave} | ||||||||||||||||||||||||
| onDragOver={handleDragOver} | ||||||||||||||||||||||||
| className={`${inputStyles.base} ${isDragging ? inputStyles.dragging : inputStyles.notDragging}`} | ||||||||||||||||||||||||
| > | ||||||||||||||||||||||||
| {!image ? ( | ||||||||||||||||||||||||
| <div className="flex flex-col items-center justify-center gap-300"> | ||||||||||||||||||||||||
| <div className="flex flex-col items-center justify-center gap-150"> | ||||||||||||||||||||||||
| <div className="flex flex-col gap-1"> | ||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||
| onDrop={handleDrop} | ||||||||||||||||||||||||
| onDragEnter={handleDragEnter} | ||||||||||||||||||||||||
| onDragLeave={handleDragLeave} | ||||||||||||||||||||||||
| onDragOver={handleDragOver} | ||||||||||||||||||||||||
| className={`${inputStyles.base} ${isDragging ? inputStyles.dragging : inputStyles.notDragging}`} | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n src/components/ui/image-upload-input.tsx | head -150Repository: code-zero-to-one/study-platform-client Length of output: 5532 🏁 Script executed: grep -r "clsx\|tailwind-merge\|class-variance-authority" --include="package.json" --include="*.ts" --include="*.tsx" | head -20Repository: code-zero-to-one/study-platform-client Length of output: 1650 🏁 Script executed: cat -n src/components/ui/button/index.tsx | head -50Repository: code-zero-to-one/study-platform-client Length of output: 2111 🏁 Script executed: cat -n src/components/ui/button/index.tsx | sed -n '50,70p'Repository: code-zero-to-one/study-platform-client Length of output: 739 🏁 Script executed: cat -n src/components/ui/\(shadcn\)/lib/utils.tsRepository: code-zero-to-one/study-platform-client Length of output: 292 동적 클래스 조합을 CVA와 현재 템플릿 문자열 조합은 프로젝트 스타일 가이드와 불일치합니다. 다른 UI 컴포넌트(Button, Badge, Chip 등)와 같이 CVA를 사용하고, 프로젝트의 제안 패치+import { cva } from 'class-variance-authority';
+import { cn } from '@/components/ui/(shadcn)/lib/utils';
import Image from 'next/image';
import { useState, DragEvent, ChangeEvent, useRef } from 'react';
import Button from '@/components/ui/button';
-const inputStyles = {
- base: 'rounded-100 flex w-full flex-col items-center justify-center border-2 p-500',
- dragging: 'border-border-brand bg-fill-brand-subtle-hover',
- notDragging: 'border-gray-300 border-dashed',
-};
+const inputStyles = cva(
+ 'rounded-100 flex w-full flex-col items-center justify-center border-2 p-500',
+ {
+ variants: {
+ dragging: {
+ true: 'border-border-brand bg-fill-brand-subtle-hover',
+ false: 'border-gray-300 border-dashed',
+ },
+ },
+ defaultVariants: {
+ dragging: false,
+ },
+ },
+);
...
- className={`${inputStyles.base} ${isDragging ? inputStyles.dragging : inputStyles.notDragging}`}
+ className={cn(inputStyles({ dragging: isDragging }))}🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| > | ||||||||||||||||||||||||
| {!image ? ( | ||||||||||||||||||||||||
| <div className="flex flex-col items-center justify-center gap-300"> | ||||||||||||||||||||||||
| <div className="flex flex-col items-center justify-center gap-150"> | ||||||||||||||||||||||||
| <Image | ||||||||||||||||||||||||
| src="/icons/camera.svg" | ||||||||||||||||||||||||
| width={32} | ||||||||||||||||||||||||
| height={32} | ||||||||||||||||||||||||
| alt="파일 업로드" | ||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||
|
Comment on lines
+101
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Check if the file exists
fd "image-upload-input.tsx" src/Repository: code-zero-to-one/study-platform-client Length of output: 122 🏁 Script executed: # Check next.config configuration
cat -n next.config.tsRepository: code-zero-to-one/study-platform-client Length of output: 5783 🏁 Script executed: # Search for SVG imports to see current patterns
rg "\.svg['\"]" src/ -A 2 -B 2 | head -40Repository: code-zero-to-one/study-platform-client Length of output: 2824 🏁 Script executed: # Check if icons/camera.svg exists
fd "camera.svg"Repository: code-zero-to-one/study-platform-client Length of output: 105 🏁 Script executed: # Read the image-upload-input.tsx file to verify the current code
cat -n src/components/ui/image-upload-input.tsx | head -120 | tail -30Repository: code-zero-to-one/study-platform-client Length of output: 1328 🏁 Script executed: # Check the complete context around lines 101-106
sed -n '95,110p' src/components/ui/image-upload-input.tsxRepository: code-zero-to-one/study-platform-client Length of output: 709 🏁 Script executed: # Check imports at the top of the file to understand current pattern
head -20 src/components/ui/image-upload-input.tsxRepository: code-zero-to-one/study-platform-client Length of output: 683 SVG를 React 컴포넌트로 import해 주세요.
제안 패치+import CameraIcon from 'public/icons/camera.svg';
...
-<Image
- src="/icons/camera.svg"
- width={32}
- height={32}
- alt="파일 업로드"
-/>
+<CameraIcon
+ width={32}
+ height={32}
+ aria-label="파일 업로드"
+/>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| <span className="font-designer-18m text-text-default"> | ||||||||||||||||||||||||
| 드래그하여 파일 업로드 | ||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||
| ref={fileInputRef} | ||||||||||||||||||||||||
| type="file" | ||||||||||||||||||||||||
| accept="image/*" | ||||||||||||||||||||||||
| onChange={handleFileChange} | ||||||||||||||||||||||||
| className="hidden" | ||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||
| onClick={handleOpenFileDialog} | ||||||||||||||||||||||||
| color="primary" | ||||||||||||||||||||||||
| size="small" | ||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||
| > | ||||||||||||||||||||||||
| 파일 업로드 | ||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||
| <div className="relative"> | ||||||||||||||||||||||||
| <Image | ||||||||||||||||||||||||
| src="/icons/camera.svg" | ||||||||||||||||||||||||
| width={32} | ||||||||||||||||||||||||
| height={32} | ||||||||||||||||||||||||
| alt="파일 업로드" | ||||||||||||||||||||||||
| src={image} | ||||||||||||||||||||||||
| alt="preview" | ||||||||||||||||||||||||
| width={240} | ||||||||||||||||||||||||
| height={180} | ||||||||||||||||||||||||
| className="rounded-lg object-cover" | ||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||
| <span className="font-designer-18m text-text-default"> | ||||||||||||||||||||||||
| 드래그하여 파일 업로드 | ||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||
| onClick={handleRemove} | ||||||||||||||||||||||||
| aria-label="이미지 삭제" | ||||||||||||||||||||||||
| className="bg-background-dimmer border-border-inverse text-text-inverse absolute top-0 right-0 flex h-[36px] w-[36px] translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border" | ||||||||||||||||||||||||
| > | ||||||||||||||||||||||||
| ✕ | ||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||
| ref={fileInputRef} | ||||||||||||||||||||||||
| type="file" | ||||||||||||||||||||||||
| accept="image/*" | ||||||||||||||||||||||||
| onChange={handleFileChange} | ||||||||||||||||||||||||
| className="hidden" | ||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||
| onClick={handleOpenFileDialog} | ||||||||||||||||||||||||
| color="primary" | ||||||||||||||||||||||||
| size="small" | ||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||
| > | ||||||||||||||||||||||||
| 파일 업로드 | ||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||
| <div className="relative"> | ||||||||||||||||||||||||
| <Image | ||||||||||||||||||||||||
| src={image} | ||||||||||||||||||||||||
| alt="preview" | ||||||||||||||||||||||||
| width={240} | ||||||||||||||||||||||||
| height={180} | ||||||||||||||||||||||||
| className="rounded-lg object-cover" | ||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||
| onClick={handleRemove} | ||||||||||||||||||||||||
| className="bg-background-dimmer border-border-inverse text-text-inverse absolute top-0 right-0 flex h-[36px] w-[36px] translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border" | ||||||||||||||||||||||||
| > | ||||||||||||||||||||||||
| ✕ | ||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| {sizeError && <p className="text-text-danger text-sm">{sizeError}</p>} | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오류 메시지에 라이브 리전을 추가해 주세요. 동적으로 바뀌는 검증 오류는 스크린리더에 즉시 전달되도록 제안 패치- {sizeError && <p className="text-text-danger text-sm">{sizeError}</p>}
+ {sizeError && (
+ <p role="alert" aria-live="polite" className="text-text-danger text-sm">
+ {sizeError}
+ </p>
+ )}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.