Skip to content

Commit 55bbd20

Browse files
authored
[#237] ✨ team API 연동 (#249)
* [#237] 🔧 change api docs path * [#237] ✨ team api services / queries * [#237] ✨ team api related sates handled by useReducer * [#237] ✨ team realted components edited * [#237] ✨ team related constants * [#237] 🔧 team types update * [#237] 🔧 next image domain added * [#237] ✨ team page * [#237] ✨ create team * [#237] 🔧 next config domains => remote patterns * [#237] ✨ team detail * [#237] ✨ update team type with queries * [#237] ✨ new modal contents * [#237] ✨ edit team * [#237] 🐛 build error * [#237] 🐛 merge conflicts
1 parent bcde297 commit 55bbd20

File tree

27 files changed

+959
-723
lines changed

27 files changed

+959
-723
lines changed

next.config.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,24 @@ import type { NextConfig } from 'next'
33

44
const nextConfig: NextConfig = {
55
images: {
6-
domains: ['picsum.photos'],
6+
remotePatterns: [
7+
{
8+
protocol: 'https',
9+
hostname: 'picsum.photos',
10+
},
11+
{
12+
protocol: 'https',
13+
hostname: 'dfdnew.s3.ap-northeast-2.amazonaws.com',
14+
},
15+
{
16+
protocol: 'https',
17+
hostname: 'default-imageurl.com',
18+
},
19+
{
20+
protocol: 'https',
21+
hostname: 'default-imageUrl.com',
22+
},
23+
],
724
},
825
webpack(config) {
926
config.module.rules.push({

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
"format": "prettier --write .",
9292
"test": "jest",
9393
"commit": "cz",
94-
"generate:types": "openapi-typescript http://43.202.50.174:8080/v3/api-docs --output src/types/api/apiSchema.types.d.ts",
94+
"generate:types": "openapi-typescript http://13.125.179.66:8080/v3/api-docs --output src/types/api/apiSchema.types.d.ts",
9595
"storybook": "storybook dev -p 6006",
9696
"build-storybook": "storybook build",
9797
"chromatic": "npx chromatic"
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
'use client'
2+
3+
import { useParams } from 'next/navigation'
4+
5+
import { Controller, useForm } from 'react-hook-form'
6+
7+
import {
8+
positionOptions,
9+
teamTypeOptions,
10+
techStackOptions,
11+
} from '@/constants/selectOptions'
12+
import { TipTapEditor } from '@/lib/tiptap/TipTapEditor'
13+
import {
14+
GetTeamRecruitmentResponse,
15+
UpdateTeamRecruitmentRequest,
16+
} from '@/types/api/Team.types'
17+
import { zodResolver } from '@hookform/resolvers/zod'
18+
import { z } from 'zod'
19+
20+
import { Button, Link } from '@/components/common/button'
21+
import { DeletableChip } from '@/components/common/chip'
22+
import { Container } from '@/components/common/containers'
23+
import { Label } from '@/components/common/label'
24+
import { Text } from '@/components/common/text'
25+
import { Form } from '@/components/shared/form'
26+
import { Select } from '@/components/shared/select'
27+
28+
import { useTeamRecruitment, useUpdateTeamRecruitment } from '@/queries/team'
29+
30+
const MIN_RECRUIT_NUMBER = 1
31+
const MAX_RECRUIT_NUMBER = 10
32+
33+
const createTeamSchema = z.object({
34+
teamTitle: z.string().nonempty('제목을 입력해주세요.'),
35+
teamContent: z.string().nonempty('내용을 입력해주세요.'),
36+
teamType: z.enum(['STUDY', 'PROJECT', 'MENTORING'], {
37+
errorMap: () => ({ message: '모집 유형을 선택해주세요.' }),
38+
}),
39+
teamRecruitmentNum: z
40+
.string()
41+
.min(1, '모집 인원을 입력해주세요.')
42+
.regex(/^\d+$/, '숫자를 입력해주세요.')
43+
.transform(Number)
44+
.refine(val => val >= MIN_RECRUIT_NUMBER, {
45+
message: `최소 ${MIN_RECRUIT_NUMBER}명 이상 모집해야 합니다.`,
46+
})
47+
.refine(val => val <= MAX_RECRUIT_NUMBER, {
48+
message: `최대 ${MAX_RECRUIT_NUMBER}명까지 모집 가능합니다.`,
49+
}),
50+
teamPosition: z.string().min(1, '포지션을 선택해주세요.'),
51+
teamTechStack: z
52+
.array(z.string())
53+
.max(5, '기술 스택은 최대 5개까지 선택 가능합니다.')
54+
.optional(),
55+
teamTags: z
56+
.array(z.string())
57+
.max(10, '태그는 최대 10개까지 입력할 수 있습니다.')
58+
.optional(),
59+
})
60+
61+
export default function UpdateTeamPage(): JSX.Element {
62+
const params = useParams<{ id: string }>()
63+
const teamId = Number(params.id)
64+
65+
const { data: teamDetail, isLoading, isError } = useTeamRecruitment(teamId)
66+
const {
67+
teamTitle,
68+
teamContent,
69+
teamPosition,
70+
teamTechStack,
71+
teamTags,
72+
teamRecruitmentNum,
73+
teamType,
74+
} = (teamDetail as GetTeamRecruitmentResponse) ?? {
75+
teamTitle: '',
76+
teamContent: '',
77+
teamPosition: '',
78+
teamTechStack: [],
79+
teamTags: [],
80+
}
81+
82+
const { mutate } = useUpdateTeamRecruitment(teamId)
83+
84+
const methods = useForm<UpdateTeamRecruitmentRequest>({
85+
mode: 'onBlur',
86+
resolver: zodResolver(createTeamSchema),
87+
defaultValues: {
88+
teamTitle,
89+
teamContent,
90+
teamPosition,
91+
teamTechStack,
92+
teamTags,
93+
teamRecruitmentNum,
94+
teamType,
95+
},
96+
})
97+
98+
const { handleSubmit, control } = methods
99+
const onSubmit = (data: UpdateTeamRecruitmentRequest) => {
100+
console.log(data)
101+
mutate(data)
102+
}
103+
104+
if (isLoading) return <div>d</div>
105+
if (isError) return <div>d</div>
106+
107+
return (
108+
<Container className='mx-auto my-80 flex flex-col gap-40'>
109+
<div className='flex flex-col gap-8'>
110+
<Text.Heading variant='heading2' as='h2' weight='700'>
111+
팀원 찾기
112+
</Text.Heading>
113+
<Text.Body variant='body2' color='gray600'>
114+
함께 성장할 팀원을 찾아보세요!
115+
</Text.Body>
116+
</div>
117+
<Form methods={methods} onSubmit={handleSubmit(onSubmit)}>
118+
<Label required labelText='제목' className='mb-20'>
119+
<Form.Text
120+
name='teamTitle'
121+
required
122+
placeholder='예시)함께 성장할 개발 스터디 팀원을 모집합니다!'
123+
/>
124+
</Label>
125+
<div className='mb-20 flex flex-col gap-4'>
126+
<Label required labelText='모집 유형' />
127+
<Controller
128+
name='teamType'
129+
control={control}
130+
rules={{ required: '모집 유형을 선택해주세요.' }}
131+
render={({ field, fieldState: { error } }) => (
132+
<div>
133+
<Select
134+
options={teamTypeOptions}
135+
selectedValue={field.value || ''}
136+
onSingleChange={field.onChange}
137+
isMulti={false}
138+
>
139+
<Select.Trigger placeholder='모집 유형 선택' />
140+
<Select.Menu>
141+
<Select.Options />
142+
</Select.Menu>
143+
</Select>
144+
{error?.message && (
145+
<Form.Message hasError={!!error}>
146+
{error.message}
147+
</Form.Message>
148+
)}
149+
</div>
150+
)}
151+
/>
152+
</div>
153+
<Label required labelText='모집 인원' className='mb-20'>
154+
<Form.Text
155+
type='number'
156+
name='teamRecruitmentNum'
157+
required
158+
placeholder='모집 인원을 입력해주세요'
159+
className='w-210'
160+
/>
161+
</Label>
162+
<div className='mb-20 flex flex-col gap-4'>
163+
<Label required labelText='포지션' />
164+
<Controller
165+
name='teamPosition'
166+
control={control}
167+
rules={{ required: '모집 유형을 선택해주세요.' }}
168+
render={({ field, fieldState: { error } }) => (
169+
<div>
170+
<Select
171+
options={positionOptions}
172+
selectedValue={field.value || ''}
173+
onSingleChange={field.onChange}
174+
>
175+
<Select.Trigger placeholder='포지션 선택' />
176+
<Select.Menu>
177+
<Select.Options />
178+
</Select.Menu>
179+
</Select>
180+
{error?.message && (
181+
<Form.Message hasError={!!error}>
182+
{error.message}
183+
</Form.Message>
184+
)}
185+
</div>
186+
)}
187+
/>
188+
</div>
189+
190+
<div className='mb-20 flex flex-col gap-4'>
191+
<Label required labelText='내용' />
192+
<Controller
193+
name='teamContent'
194+
control={control}
195+
defaultValue={''}
196+
render={({ field: { onChange }, fieldState: { error } }) => (
197+
<div>
198+
<TipTapEditor content={teamContent} onChange={onChange} />
199+
{error?.message && (
200+
<Form.Message hasError={!!error}>
201+
{error.message}
202+
</Form.Message>
203+
)}
204+
</div>
205+
)}
206+
/>
207+
<Text.Caption variant='caption1' color='gray500'>
208+
텍스트 줄 바꿈은 엔터(Enter)를 통해 구분합니다.
209+
</Text.Caption>
210+
</div>
211+
<div className='mb-20 flex flex-col gap-4'>
212+
<Label labelText='기술 스택' />
213+
<Controller
214+
name='teamTechStack'
215+
control={control}
216+
render={({ field, fieldState: { error } }) => (
217+
<div>
218+
<Select
219+
options={techStackOptions}
220+
selectedValues={field.value}
221+
onMultiChange={field.onChange}
222+
isMulti
223+
>
224+
<Select.Trigger placeholder='기술 스택 선택' />
225+
<Select.Menu>
226+
<Select.Search placeholder='스택을 입력해보세요!' />
227+
<Select.Options />
228+
</Select.Menu>
229+
</Select>
230+
<Text.Caption
231+
variant='caption1'
232+
color='gray500'
233+
className='mt-4'
234+
>
235+
최대 5개까지 선택 가능합니다.
236+
</Text.Caption>
237+
<div className='flex gap-4'>
238+
{(field.value ?? []).map(stack => (
239+
<DeletableChip
240+
key={stack}
241+
label={stack}
242+
onDelete={() => {
243+
field.onChange(
244+
(field.value ?? []).filter(v => v !== stack)
245+
)
246+
}}
247+
/>
248+
))}
249+
</div>
250+
{error?.message && (
251+
<Form.Message hasError={!!error}>
252+
{error.message}
253+
</Form.Message>
254+
)}
255+
</div>
256+
)}
257+
/>
258+
</div>
259+
<Label labelText='태그' className='mb-40'>
260+
<Form.TagInput
261+
name='teamTags'
262+
placeholder='태그를 입력하고 엔터를 눌러주세요. 태그 최대 개수는 10개입니다.'
263+
/>
264+
</Label>
265+
<div className='flex justify-end gap-10'>
266+
<Link variant='outlined' href={`/team/${teamId}`}>
267+
취소
268+
</Link>
269+
<Button type='submit'>수정하기</Button>
270+
</div>
271+
</Form>
272+
</Container>
273+
)
274+
}

0 commit comments

Comments
 (0)