Skip to content

Commit 679afca

Browse files
authored
feat#22-realtor-certification-ui (#24)
2 parents f59ba11 + 94a1534 commit 679afca

23 files changed

Lines changed: 314 additions & 45 deletions

File tree

src/features/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './main';
22
export * from './login';
3+
export * from './realtor-certification';
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { useRef } from 'react';
2+
import { useFormContext } from 'react-hook-form';
3+
4+
import { cn } from '@/shared';
5+
6+
import { type RealtorCertificationFormData } from '../../model';
7+
8+
interface CustomFileUploadProps {
9+
name: keyof RealtorCertificationFormData;
10+
label?: string;
11+
placeholder?: string;
12+
className?: string;
13+
disabled?: boolean;
14+
error?: string;
15+
}
16+
17+
export const CustomFileUpload = ({
18+
name,
19+
label,
20+
placeholder = '파일을 선택하거나 여기에 드래그하세요',
21+
className,
22+
disabled,
23+
error,
24+
}: CustomFileUploadProps) => {
25+
const form = useFormContext<RealtorCertificationFormData>();
26+
const fileInputRef = useRef<HTMLInputElement>(null);
27+
28+
const updateFile = (file: File | null) => {
29+
form.setValue(name, file as RealtorCertificationFormData[keyof RealtorCertificationFormData]);
30+
form.trigger(name);
31+
};
32+
33+
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
34+
const file = event.target.files?.[0] || null;
35+
updateFile(file);
36+
};
37+
38+
const handleDragOver = (event: React.DragEvent) => {
39+
event.preventDefault();
40+
event.currentTarget.classList.add('border-blue-500', 'bg-blue-50');
41+
};
42+
43+
const handleDragLeave = (event: React.DragEvent) => {
44+
event.preventDefault();
45+
event.currentTarget.classList.remove('border-blue-500', 'bg-blue-50');
46+
};
47+
48+
const handleDrop = (event: React.DragEvent) => {
49+
event.preventDefault();
50+
event.currentTarget.classList.remove('border-blue-500', 'bg-blue-50');
51+
52+
const file = event.dataTransfer.files[0] || null;
53+
updateFile(file);
54+
};
55+
56+
const handleClick = () => {
57+
fileInputRef.current?.click();
58+
};
59+
60+
const selectedFile = form.watch(name) as File | null;
61+
62+
return (
63+
<div className={cn('w-full', className)}>
64+
{label && <label className='mb-2 block text-lg font-medium text-gray-700'>{label}</label>}
65+
66+
<div
67+
className={cn(
68+
'relative flex min-h-[120px] cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-6 transition-colors hover:border-gray-400 hover:bg-gray-100',
69+
error && 'border-red-300 bg-red-50',
70+
disabled && 'cursor-not-allowed opacity-50',
71+
)}
72+
onDragOver={handleDragOver}
73+
onDragLeave={handleDragLeave}
74+
onDrop={handleDrop}
75+
onClick={!disabled ? handleClick : undefined}
76+
>
77+
<input
78+
ref={fileInputRef}
79+
type='file'
80+
accept='.pdf,.jpg,.jpeg,.png,.webp'
81+
onChange={handleInputChange}
82+
className='hidden'
83+
disabled={disabled}
84+
/>
85+
86+
{selectedFile ? (
87+
<div className='flex flex-col items-center gap-2 text-center'>
88+
<div className='rounded-full bg-green-100 p-3'>
89+
<svg
90+
className='size-10 text-green-600'
91+
fill='none'
92+
stroke='currentColor'
93+
viewBox='0 0 24 24'
94+
>
95+
<path
96+
strokeLinecap='round'
97+
strokeLinejoin='round'
98+
strokeWidth={2}
99+
d='M5 13l4 4L19 7'
100+
/>
101+
</svg>
102+
</div>
103+
<div>
104+
<p className='text-sm font-medium text-gray-700'>{selectedFile.name}</p>
105+
<p className='text-sm text-gray-500'>
106+
{(selectedFile.size / 1024 / 1024).toFixed(2)} MB
107+
</p>
108+
</div>
109+
<p className='text-sm text-gray-400'>클릭하여 다른 파일 선택</p>
110+
</div>
111+
) : (
112+
<div className='flex flex-col items-center gap-2 text-center'>
113+
<div className='rounded-full bg-gray-200 p-5'>
114+
<svg
115+
className='size-10 text-gray-600'
116+
fill='none'
117+
stroke='currentColor'
118+
viewBox='0 0 24 24'
119+
>
120+
<path
121+
strokeLinecap='round'
122+
strokeLinejoin='round'
123+
strokeWidth={2}
124+
d='M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12'
125+
/>
126+
</svg>
127+
</div>
128+
<div className='flex flex-col gap-2 py-3'>
129+
<p className='text-md flex gap-1 font-medium text-gray-700'>
130+
<span className='text-blue-600 hover:text-blue-500'>클릭하여 파일 선택</span>
131+
<span className='text-gray-500'>또는 드래그 앤 드롭</span>
132+
</p>
133+
<p className='text-sm text-gray-500'>{placeholder}</p>
134+
</div>
135+
<div className='text-sm text-gray-400'>지원 형식: PDF, JPG, PNG, WEBP (최대 10MB)</div>
136+
</div>
137+
)}
138+
</div>
139+
140+
{error && <p className='mt-2 text-sm text-red-600'>{error}</p>}
141+
</div>
142+
);
143+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './CustomFileUpload';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useFormContext } from 'react-hook-form';
2+
3+
import { FormField, FormItem, FormMessage } from '@/shared';
4+
5+
import { type RealtorCertificationFormData } from '../../model';
6+
import { CustomFileUpload } from '../common';
7+
8+
export const FileUploadField = () => {
9+
const form = useFormContext<RealtorCertificationFormData>();
10+
11+
return (
12+
<FormField
13+
control={form.control}
14+
name='certificateFile'
15+
render={({ fieldState: { error } }) => (
16+
<FormItem>
17+
<CustomFileUpload
18+
name='certificateFile'
19+
label='인증서 등록'
20+
placeholder='공인중개사 인증서 파일을 업로드하세요'
21+
error={error?.message}
22+
/>
23+
<FormMessage />
24+
</FormItem>
25+
)}
26+
/>
27+
);
28+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './FileUploadField';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './common';
2+
export * from './features';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// 파일 크기 제한 (10MB)
2+
export const MAX_FILE_SIZE_10MB = 10 * 1024 * 1024;
3+
4+
// 허용되는 파일 타입
5+
export const ACCEPTED_FILE_TYPES = [
6+
'application/pdf',
7+
'image/jpeg',
8+
'image/jpg',
9+
'image/png',
10+
'image/webp',
11+
];
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './certification-file';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './ui';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './schema';

0 commit comments

Comments
 (0)