11'use client'
22
3- import { useRouter } from 'next/navigation'
4-
5- import { useState } from 'react'
63import { Controller , useForm } from 'react-hook-form'
74
85import { PORTFOLIO_EDITOR_CONTENT } from '@/constants/tiptap'
96import { TipTapEditor } from '@/lib/tiptap/TipTapEditor'
107import { CreatePortfolioRequest } from '@/types/api/Portfolio.types'
11-
12- import { authProxy } from '@/app/api/auth/authProxy '
8+ import { zodResolver } from '@hookform/resolvers/zod'
9+ import { z } from 'zod '
1310
1411import { Button , Link } from '@/components/common/button'
1512import { Container } from '@/components/common/containers'
@@ -25,9 +22,45 @@ import {
2522 TechStackSelect ,
2623} from '@/components/shared/select'
2724
25+ import { useCreatePortfolio } from '@/queries/portfolio'
26+
27+ const createPortfolioSchema = z . object ( {
28+ request : z . object ( {
29+ portTitle : z . string ( ) . nonempty ( '제목을 입력해주세요.' ) ,
30+ portContent : z . string ( ) . nonempty ( '내용을 입력해주세요.' ) ,
31+ portPosition : z . string ( ) . nonempty ( '포지션을 입력해주세요.' ) ,
32+ techStacks : z
33+ . array ( z . string ( ) )
34+ . max ( 10 , '태그는 최대 10개까지 입력할 수 있습니다.' ) ,
35+ tags : z
36+ . array ( z . string ( ) )
37+ . max ( 10 , '태그는 최대 10개까지 입력할 수 있습니다.' )
38+ . optional ( ) ,
39+ links : z
40+ . array ( z . string ( ) )
41+ . max ( 10 , '태그는 최대 10개까지 입력할 수 있습니다.' )
42+ . optional ( ) ,
43+ educations : z
44+ . array ( z . string ( ) )
45+ . max ( 10 , '태그는 최대 10개까지 입력할 수 있습니다.' )
46+ . optional ( ) ,
47+ awards : z
48+ . array ( z . string ( ) )
49+ . max ( 10 , '태그는 최대 10개까지 입력할 수 있습니다.' )
50+ . optional ( ) ,
51+ careers : z
52+ . array ( z . string ( ) )
53+ . max ( 10 , '태그는 최대 10개까지 입력할 수 있습니다.' )
54+ . optional ( ) ,
55+ } ) ,
56+ } )
57+
2858export default function CreatePortfolioPage ( ) : JSX . Element {
59+ const { mutate } = useCreatePortfolio ( )
60+
2961 const methods = useForm < CreatePortfolioRequest > ( {
3062 mode : 'onBlur' ,
63+ resolver : zodResolver ( createPortfolioSchema ) ,
3164 defaultValues : {
3265 request : {
3366 portTitle : '' ,
@@ -36,56 +69,18 @@ export default function CreatePortfolioPage(): JSX.Element {
3669 educations : [ ] ,
3770 awards : [ ] ,
3871 careers : [ ] ,
39- links : [ { type : undefined , url : undefined } ] ,
72+ links : [ ] ,
4073 tags : [ ] ,
4174 } ,
4275 } ,
4376 } )
44- const { handleSubmit, control, watch } = methods
45- const router = useRouter ( )
46- const [ isLoading , setIsLoading ] = useState ( false )
4777
48- const updateProfile = async (
49- data : CreatePortfolioRequest ,
50- profileImage ?: File
51- ) : Promise < ApiResponse > => {
52- const formData = new FormData ( )
53- formData . append (
54- 'request' ,
55- new Blob ( [ JSON . stringify ( data ) ] , { type : 'application/json' } )
56- )
78+ const { handleSubmit, control } = methods
5779
58- if ( profileImage ) {
59- formData . append ( 'profileImage' , profileImage )
60- }
80+ const onSubmit = ( data : CreatePortfolioRequest ) => {
81+ const formdata = new FormData ( )
6182
62- return await authProxy
63- . post ( 'v1/portfolio' , {
64- body : formData ,
65- headers : {
66- 'Content-Type' : 'multipart/form-data' ,
67- } ,
68- } )
69- . json ( )
70- }
71-
72- const onSubmit = async ( data : CreatePortfolioRequest ) => {
73- setIsLoading ( true )
74- try {
75- const file = data . file instanceof FileList ? data . file [ 0 ] : undefined
76- const response = await updateProfile ( data , file )
77-
78- if ( response . isSuccess ) {
79- router . push ( '/portfolio' )
80- } else {
81- alert ( response . message || '포트폴리오 등록에 실패했습니다.' )
82- }
83- } catch ( error ) {
84- console . error ( '포트폴리오 등록 중 오류 발생:' , error )
85- alert ( '포트폴리오 등록 중 오류가 발생했습니다.' )
86- } finally {
87- setIsLoading ( false )
88- }
83+ mutate ( data )
8984 }
9085
9186 return (
@@ -110,50 +105,62 @@ export default function CreatePortfolioPage(): JSX.Element {
110105 < Label required labelText = '포지션' />
111106 < PositionSelect name = 'request.portPosition' />
112107 </ div >
113- < div className = 'mb-20 flex flex-col gap-4' >
114- < Label required labelText = '링크' />
115- < LinkSelect name = { 'request.links' } />
116- </ div >
117108 < div className = 'mb-20 flex flex-col gap-4' >
118109 < Label required labelText = '기술 스택' />
119110 < TechStackSelect name = 'request.techStacks' />
120111 </ div >
121- < div className = 'mb-20 flex flex-col gap-4' >
122- < Label required labelText = '학력' />
123- < EducationSelect name = 'request.educations' />
124- </ div >
125- < div className = 'mb-20 flex flex-col gap-4' >
126- < Label required labelText = '수상 및 기타' />
127- < AwardSelect name = 'request.awards' />
128- </ div >
129- < div className = 'mb-20 flex flex-col gap-4' >
130- < Label required labelText = '경력' />
131- < CareerSelect name = 'request.careers' />
132- </ div >
133112 < div className = 'mb-20 flex flex-col gap-4' >
134113 < Label required labelText = '내용' />
135114 < Controller
136115 name = 'request.portContent'
137116 control = { control }
138117 defaultValue = { '' }
139- render = { ( { field : { onChange } } ) => (
140- < TipTapEditor
141- content = { PORTFOLIO_EDITOR_CONTENT }
142- onChange = { onChange }
143- />
118+ render = { ( { field : { onChange } , fieldState : { error } } ) => (
119+ < div >
120+ < TipTapEditor
121+ content = { PORTFOLIO_EDITOR_CONTENT }
122+ onChange = { onChange }
123+ />
124+ { error ?. message && (
125+ < Form . Message hasError = { ! ! error } >
126+ { error . message }
127+ </ Form . Message >
128+ ) }
129+ </ div >
144130 ) }
145131 />
146132 < Text . Caption variant = 'caption1' color = 'gray500' >
147133 텍스트는 줄 바꿈은 엔터(Enter)를 통해 구분합니다.
148134 </ Text . Caption >
149135 </ div >
136+ < Label required labelText = '태그' className = 'mb-20' >
137+ < Form . TagInput
138+ name = 'request.tags'
139+ placeholder = '태그를 입력하고 엔터를 눌러주세요. 태그 최대 개수는 10개입니다.'
140+ />
141+ </ Label >
142+ < div className = 'mb-20 flex flex-col gap-4' >
143+ < Label labelText = '링크' />
144+ < LinkSelect name = { 'request.links' } />
145+ </ div >
146+
147+ < div className = 'mb-20 flex flex-col gap-4' >
148+ < Label labelText = '학력' />
149+ < EducationSelect name = 'request.educations' />
150+ </ div >
151+ < div className = 'mb-20 flex flex-col gap-4' >
152+ < Label labelText = '수상 및 기타' />
153+ < AwardSelect name = 'request.awards' />
154+ </ div >
155+ < div className = 'mb-20 flex flex-col gap-4' >
156+ < Label labelText = '경력' />
157+ < CareerSelect name = 'request.careers' />
158+ </ div >
150159 < div className = 'flex justify-end gap-10' >
151160 < Link variant = 'outlined' href = '/team' >
152161 취소
153162 </ Link >
154- < Button type = 'submit' disabled = { isLoading } >
155- { isLoading ? '등록 중...' : '등록하기' }
156- </ Button >
163+ < Button type = 'submit' > 등록하기</ Button >
157164 </ div >
158165 </ Form >
159166 </ Container >
0 commit comments