-
Notifications
You must be signed in to change notification settings - Fork 0
feat#22-realtor-certification-ui #24
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
66a4fa6
0dbb9c6
fbc61e5
a472a81
b28454c
d263898
94d70c5
12a2104
7a26cec
c358b93
94a1534
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 |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| export * from './main'; | ||
| export * from './login'; | ||
| export * from './realtor-certification'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| import { useRef } from 'react'; | ||
| import { useFormContext } from 'react-hook-form'; | ||
|
|
||
| import { cn } from '@/shared'; | ||
|
|
||
| import { type RealtorCertificationFormData } from '../../model'; | ||
|
|
||
| interface CustomFileUploadProps { | ||
| name: keyof RealtorCertificationFormData; | ||
| label?: string; | ||
| placeholder?: string; | ||
| className?: string; | ||
| disabled?: boolean; | ||
| error?: string; | ||
| } | ||
|
|
||
| export const CustomFileUpload = ({ | ||
| name, | ||
| label, | ||
| placeholder = 'ํ์ผ์ ์ ํํ๊ฑฐ๋ ์ฌ๊ธฐ์ ๋๋๊ทธํ์ธ์', | ||
| className, | ||
| disabled, | ||
| error, | ||
| }: CustomFileUploadProps) => { | ||
| const form = useFormContext<RealtorCertificationFormData>(); | ||
| const fileInputRef = useRef<HTMLInputElement>(null); | ||
|
|
||
| const updateFile = (file: File | null) => { | ||
| form.setValue(name, file as RealtorCertificationFormData[keyof RealtorCertificationFormData]); | ||
| form.trigger(name); | ||
| }; | ||
|
|
||
| const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { | ||
| const file = event.target.files?.[0] || null; | ||
| updateFile(file); | ||
| }; | ||
|
|
||
| const handleDragOver = (event: React.DragEvent) => { | ||
| event.preventDefault(); | ||
| event.currentTarget.classList.add('border-blue-500', 'bg-blue-50'); | ||
| }; | ||
|
|
||
| const handleDragLeave = (event: React.DragEvent) => { | ||
| event.preventDefault(); | ||
| event.currentTarget.classList.remove('border-blue-500', 'bg-blue-50'); | ||
| }; | ||
|
|
||
| const handleDrop = (event: React.DragEvent) => { | ||
| event.preventDefault(); | ||
| event.currentTarget.classList.remove('border-blue-500', 'bg-blue-50'); | ||
|
|
||
| const file = event.dataTransfer.files[0] || null; | ||
| updateFile(file); | ||
| }; | ||
|
|
||
| const handleClick = () => { | ||
| fileInputRef.current?.click(); | ||
| }; | ||
|
|
||
| const selectedFile = form.watch(name) as File | null; | ||
|
|
||
| return ( | ||
| <div className={cn('w-full', className)}> | ||
| {label && <label className='mb-2 block text-lg font-medium text-gray-700'>{label}</label>} | ||
|
|
||
| <div | ||
| className={cn( | ||
| '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', | ||
| error && 'border-red-300 bg-red-50', | ||
| disabled && 'cursor-not-allowed opacity-50', | ||
| )} | ||
| onDragOver={handleDragOver} | ||
| onDragLeave={handleDragLeave} | ||
| onDrop={handleDrop} | ||
| onClick={!disabled ? handleClick : undefined} | ||
| > | ||
| <input | ||
| ref={fileInputRef} | ||
| type='file' | ||
| accept='.pdf,.jpg,.jpeg,.png,.webp' | ||
| onChange={handleInputChange} | ||
| className='hidden' | ||
| disabled={disabled} | ||
| /> | ||
|
|
||
| {selectedFile ? ( | ||
| <div className='flex flex-col items-center gap-2 text-center'> | ||
| <div className='rounded-full bg-green-100 p-3'> | ||
| <svg | ||
| className='size-10 text-green-600' | ||
| fill='none' | ||
| stroke='currentColor' | ||
| viewBox='0 0 24 24' | ||
| > | ||
| <path | ||
| strokeLinecap='round' | ||
| strokeLinejoin='round' | ||
| strokeWidth={2} | ||
| d='M5 13l4 4L19 7' | ||
| /> | ||
| </svg> | ||
| </div> | ||
| <div> | ||
| <p className='text-sm font-medium text-gray-700'>{selectedFile.name}</p> | ||
| <p className='text-sm text-gray-500'> | ||
| {(selectedFile.size / 1024 / 1024).toFixed(2)} MB | ||
| </p> | ||
| </div> | ||
| <p className='text-sm text-gray-400'>ํด๋ฆญํ์ฌ ๋ค๋ฅธ ํ์ผ ์ ํ</p> | ||
| </div> | ||
| ) : ( | ||
| <div className='flex flex-col items-center gap-2 text-center'> | ||
| <div className='rounded-full bg-gray-200 p-5'> | ||
| <svg | ||
| className='size-10 text-gray-600' | ||
| fill='none' | ||
| stroke='currentColor' | ||
| viewBox='0 0 24 24' | ||
| > | ||
| <path | ||
| strokeLinecap='round' | ||
| strokeLinejoin='round' | ||
| strokeWidth={2} | ||
| 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' | ||
| /> | ||
| </svg> | ||
| </div> | ||
| <div className='flex flex-col gap-2 py-3'> | ||
| <p className='text-md flex gap-1 font-medium text-gray-700'> | ||
| <span className='text-blue-600 hover:text-blue-500'>ํด๋ฆญํ์ฌ ํ์ผ ์ ํ</span> | ||
| <span className='text-gray-500'>๋๋ ๋๋๊ทธ ์ค ๋๋กญ</span> | ||
| </p> | ||
| <p className='text-sm text-gray-500'>{placeholder}</p> | ||
| </div> | ||
| <div className='text-sm text-gray-400'>์ง์ ํ์: PDF, JPG, PNG, WEBP (์ต๋ 10MB)</div> | ||
|
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. ์ง์ ํ์๊ณผ ์ต๋ ํ์ผ ํฌ๊ธฐ์ ๋ํ ์๋ด ๋ฌธ๊ตฌ๊ฐ ํ๋์ฝ๋ฉ๋์ด ์์ต๋๋ค. ํ์ผ ํ์์ |
||
| </div> | ||
| )} | ||
| </div> | ||
|
|
||
| {error && <p className='mt-2 text-sm text-red-600'>{error}</p>} | ||
| </div> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './CustomFileUpload'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { useFormContext } from 'react-hook-form'; | ||
|
|
||
| import { FormField, FormItem, FormMessage } from '@/shared'; | ||
|
|
||
| import { type RealtorCertificationFormData } from '../../model'; | ||
| import { CustomFileUpload } from '../common'; | ||
|
|
||
| export const FileUploadField = () => { | ||
| const form = useFormContext<RealtorCertificationFormData>(); | ||
|
|
||
| return ( | ||
| <FormField | ||
| control={form.control} | ||
| name='certificateFile' | ||
| render={({ fieldState: { error } }) => ( | ||
| <FormItem> | ||
| <CustomFileUpload | ||
| name='certificateFile' | ||
| label='์ธ์ฆ์ ๋ฑ๋ก' | ||
| placeholder='๊ณต์ธ์ค๊ฐ์ฌ ์ธ์ฆ์ ํ์ผ์ ์ ๋ก๋ํ์ธ์' | ||
| error={error?.message} | ||
| /> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './FileUploadField'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from './common'; | ||
| export * from './features'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| // ํ์ผ ํฌ๊ธฐ ์ ํ (10MB) | ||
| export const MAX_FILE_SIZE_10MB = 10 * 1024 * 1024; | ||
|
|
||
| // ํ์ฉ๋๋ ํ์ผ ํ์ | ||
| export const ACCEPTED_FILE_TYPES = [ | ||
| 'application/pdf', | ||
| 'image/jpeg', | ||
| 'image/jpg', | ||
| 'image/png', | ||
| 'image/webp', | ||
| ]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './certification-file'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './ui'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './schema'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './realtor-certification.schema'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { z } from 'zod'; | ||
|
|
||
| import { ACCEPTED_FILE_TYPES, MAX_FILE_SIZE_10MB } from '../../constants'; | ||
|
|
||
| export const realtorCertificationSchema = z.object({ | ||
| certificateFile: z | ||
| .instanceof(File, { message: '์ธ์ฆ์ ํ์ผ์ ์ ๋ก๋ํด์ฃผ์ธ์.' }) | ||
| .refine((file) => file.size <= MAX_FILE_SIZE_10MB, 'ํ์ผ ํฌ๊ธฐ๋ 10MB ์ดํ์ฌ์ผ ํฉ๋๋ค.') | ||
| .refine( | ||
| (file) => ACCEPTED_FILE_TYPES.includes(file.type), | ||
| 'PDF, JPG, PNG, WEBP ํ์ผ๋ง ์ ๋ก๋ ๊ฐ๋ฅํฉ๋๋ค.', | ||
| ) | ||
| .refine((file) => file.name.trim().length > 0, 'ํ์ผ๋ช ์ด ๋น์ด์์ต๋๋ค.'), | ||
| }); | ||
|
|
||
| export type RealtorCertificationFormData = z.infer<typeof realtorCertificationSchema>; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import { useForm } from 'react-hook-form'; | ||
|
|
||
| import { zodResolver } from '@hookform/resolvers/zod'; | ||
|
|
||
| import { Button, Form } from '@/shared'; | ||
|
|
||
| import { FileUploadField } from '../components'; | ||
| import { type RealtorCertificationFormData, realtorCertificationSchema } from '../model'; | ||
|
|
||
| type props = { | ||
| onSuccess?: () => void; | ||
| }; | ||
|
|
||
| export const CertificationForm = ({ onSuccess }: props) => { | ||
| const form = useForm<RealtorCertificationFormData>({ | ||
| resolver: zodResolver(realtorCertificationSchema), | ||
| defaultValues: { | ||
| certificateFile: undefined, | ||
| }, | ||
| }); | ||
|
|
||
| const onSubmit = async (data: RealtorCertificationFormData) => { | ||
| try { | ||
| // TODO: API ํธ์ถ ๋ก์ง ๊ตฌํ | ||
| console.log('์ ์ถํ ๋ฐ์ดํฐ:', data); | ||
|
|
||
| // API ํธ์ถ ์๋ฎฌ๋ ์ด์ | ||
| await new Promise((resolve) => setTimeout(resolve, 1000)); | ||
|
|
||
| // ์ ์ถ ์ฑ๊ณต ์ ์ํ ๋ณ๊ฒฝ | ||
| onSuccess?.(); | ||
| } catch (error) { | ||
| console.error('์ ์ถ ์คํจ:', error); | ||
| // TODO: ์๋ฌ ์ฒ๋ฆฌ ๋ก์ง | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <Form {...form}> | ||
| <form onSubmit={(e) => e.preventDefault()}> | ||
| <div className='space-y-6'> | ||
| <FileUploadField /> | ||
| <Button | ||
| onClick={form.handleSubmit(onSubmit)} | ||
| type='submit' | ||
| className='h-12 w-full rounded-md text-lg' | ||
| disabled={!form.formState.isValid} | ||
| > | ||
| ์ ์ถํ๊ธฐ | ||
| </Button> | ||
| </div> | ||
| </form> | ||
|
Comment on lines
+40
to
+52
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. ํ์ฌ ํผ ์ ์ถ ๋ก์ง์ด |
||
| </Form> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { useNavigate } from 'react-router-dom'; | ||
|
|
||
| import { Button, ROUTER_PATH } from '@/shared'; | ||
|
|
||
| export const SubmitSuccessSection = () => { | ||
| const navigate = useNavigate(); | ||
|
|
||
| return ( | ||
| <div className='flex flex-col items-center justify-center gap-8 text-center'> | ||
| <div className='flex flex-col items-center justify-center gap-2 text-center'> | ||
| <h1 className='text-2xl font-semibold'>๊ณต์ธ์ค๊ฐ์ฌ ์ธ์ฆ์๊ฐ ์ ์ถ๋์์ต๋๋ค!</h1> | ||
| <p className='text-lg text-gray-500'>๋น ๋ฅธ ๋ด๋ถ ์ฌ์ฌ ํ ์ค๊ฐ์ฌ ์๊ฒฉ์ ๋๋ฆด๊ฒ์!</p> | ||
| </div> | ||
|
|
||
| <Button className='h-12 w-2/3 rounded-md text-lg' onClick={() => navigate(ROUTER_PATH.MAIN)}> | ||
| ๋ฉ์ธ ํ๋ฉด์ผ๋ก ๋์๊ฐ๊ธฐ | ||
| </Button> | ||
| </div> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| export const TitleSection = () => { | ||
| return ( | ||
| <h1 className='flex flex-col items-center text-[28px] font-semibold'> | ||
| <span>๊ณต์ธ์ค๊ฐ์ฌ ์ธ์ฆํ๊ณ </span> | ||
| <span>๊ฐํธํ๊ณ ํฌ๋ช ํ๊ฒ ๊ณ์ฝํด์!</span> | ||
| </h1> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export * from './CertificationForm'; | ||
| export * from './TitleSection'; | ||
| export * from './SubmitSuccessSection'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,22 @@ | ||
| import { useState } from 'react'; | ||
|
|
||
| import { CertificationForm, SubmitSuccessSection, TitleSection } from '@/features'; | ||
| import { AuthLogoSection } from '@/shared'; | ||
|
|
||
| export default function RealtorCertificationPage() { | ||
| return <div>RealtorCertificationPage</div>; | ||
| const [isSubmitted, setIsSubmitted] = useState(false); | ||
|
|
||
| return ( | ||
| <div className='flex h-screen flex-col items-center justify-center'> | ||
| <div className='flex flex-col items-center gap-12'> | ||
| <AuthLogoSection /> | ||
| {!isSubmitted && <TitleSection />} | ||
| {isSubmitted ? ( | ||
| <SubmitSuccessSection /> | ||
| ) : ( | ||
| <CertificationForm onSuccess={() => setIsSubmitted(true)} /> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
This file was deleted.
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1 @@ | ||
| export * from './Header'; | ||
| export * from './Footer'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
accept์์ฑ์ ํ์ผ ํ์์ด ํ๋์ฝ๋ฉ๋์ด ์์ต๋๋ค.src/features/realtor-certification/constants/certification-file.ts์ ์ ์๋ACCEPTED_FILE_TYPES์์๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ์์์ ์ค์ ํ์ฉ ํ์ผ ํ์์ด ํญ์ ๋๊ธฐํ๋์ด ์ ์ง๋ณด์์ฑ์ด ํฅ์๋ฉ๋๋ค.๋จผ์ ์์๋ฅผ import ํ์ธ์:
๊ทธ๋ฐ ๋ค์
accept์์ฑ์ ์์ ํ์ธ์.