Skip to content

Commit 79a8c3a

Browse files
authored
[#216] ✨ 인증 API 관련 (#222)
* [#216] 🔧 use images fetched from server (Plan to replace Image component with img tag in future) * [#216] 💄 adjust spacing * [#216] ✨ manage error message state * [#216] ✨ Add My Page API test section (update after resolving authentication token issues) * [#216] ✨ Manage message state based on validation * [#216] ♻️ Include all fields in res value * [#216] ✨ Display name in modal on successful registration * [#216] 🐛 fix typos * [#216] ✨ Integrate API headers for login, logout, and registration endpoints * [#216] ✨ implement API endpoints for user registration, login, and email duplication check * [#216] ✨ work in progress on My Page related API implementation * [#219] ✨ implement authorized request handling * [#219] ✨ implement getProfile, updateProfile api * [#212] ✨ integrate mypage api * [#212] ✨ web accessibility improvement
1 parent 2ce91f4 commit 79a8c3a

File tree

18 files changed

+539
-208
lines changed

18 files changed

+539
-208
lines changed

next.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ const nextConfig: NextConfig = {
4040
}
4141

4242
export default nextConfig
43+

src/app/(pages)/forgot-password/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default function FindPassword(): JSX.Element {
2727
다음
2828
</Button>
2929
<div className='flex flex-col items-center'>
30-
<Text.Body variant='body3' color='gray600'>
30+
<Text.Body variant='body3' color='gray600' className='flex gap-x-2'>
3131
이메일이 기억나지 않는다면?
3232
<Highlight>고객센터</Highlight>
3333
</Text.Body>

src/app/(pages)/login/page.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useEffect } from 'react'
3+
import { useEffect, useState } from 'react'
44
import { useForm } from 'react-hook-form'
55

66
import { zodResolver } from '@hookform/resolvers/zod'
@@ -34,6 +34,8 @@ const signInSchema = z.object({
3434
type SignInForm = z.infer<typeof signInSchema>
3535

3636
export default function Login(): JSX.Element {
37+
const [errorMessage, setErrorMessage] = useState<string | null>(null)
38+
3739
const methods = useForm<SignInForm>({
3840
mode: 'onBlur',
3941
resolver: zodResolver(signInSchema),
@@ -49,9 +51,10 @@ export default function Login(): JSX.Element {
4951
setValue,
5052
} = methods
5153

52-
const { mutate: signIn } = useSignInMutation()
54+
const { mutate: signIn } = useSignInMutation(setErrorMessage)
5355

5456
const onSubmit = (data: SignInForm) => {
57+
setErrorMessage(null)
5558
if (data.rememberEmail) {
5659
localStorage.setItem('rememberedEmail', data.email)
5760
} else {
@@ -101,6 +104,11 @@ export default function Login(): JSX.Element {
101104
비밀번호 찾기
102105
</Link>
103106
</div>
107+
<div className='pt-10'>
108+
<Text.Caption variant='caption1' color='negative'>
109+
{errorMessage}
110+
</Text.Caption>
111+
</div>
104112
<Button disabled={!isValid} type='submit' fullWidth className='my-20'>
105113
로그인
106114
</Button>
@@ -111,7 +119,7 @@ export default function Login(): JSX.Element {
111119
</Text.Body>
112120
<Divider isVertical={false} className='w-188' />
113121
</div>
114-
<Link variant='outlined' href='/sign-up' fullWidth className='mt-20'>
122+
<Link variant='outlined' href='/signup' fullWidth className='mt-20'>
115123
회원가입
116124
</Link>
117125
</Form>

src/app/(pages)/mypage/page.tsx

Lines changed: 117 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
'use client'
22

3+
import Image from 'next/image'
4+
5+
import { useRef, useState } from 'react'
36
import { Controller, useForm } from 'react-hook-form'
47

58
import { IcProfile, IcProfileCard } from '@/assets/IconList'
69
import { positionOptions, techStackOptions } from '@/constants/selectOptions'
10+
import { AffiliationType, UpdateProfileRequest } from '@/types/api/MyPage.types'
711

812
import { Button } from '@/components/common/button'
913
import { DeletableChip } from '@/components/common/chip'
@@ -13,42 +17,74 @@ import { Text } from '@/components/common/text'
1317
import { Form } from '@/components/shared/form'
1418
import { Select } from '@/components/shared/select'
1519

16-
interface FormValues {
17-
name: string
18-
nickname: string
19-
introduction: string
20-
gitHub: string
21-
position: string[]
22-
techStacks: string[]
23-
affiliation: string
24-
}
20+
import { useAuthStore } from '@/stores/useAuthStore'
21+
22+
import { getProfile, updateProfile } from '@/services/mypage'
2523

2624
export default function MyPage(): JSX.Element {
27-
const methods = useForm<FormValues>({
25+
const { user } = useAuthStore()
26+
27+
const methods = useForm<UpdateProfileRequest>({
2828
mode: 'onChange',
2929
defaultValues: {
30-
name: '',
31-
nickname: '',
32-
introduction: '',
33-
gitHub: '',
34-
position: [],
35-
techStacks: [],
36-
affiliation: '',
30+
request: {
31+
imageUrl: '',
32+
nickname: user?.nickname,
33+
introduction: '',
34+
gitHub: '',
35+
affiliation: 'COMPANY_SCHOOL',
36+
},
3737
},
3838
})
3939

40-
const { control } = methods
41-
42-
const onSubmit = (data: FormValues) => {
43-
console.log('Form Submitted:', data)
44-
}
40+
const { control, watch } = methods
41+
const values = watch()
4542

4643
const affiliationOptions = [
4744
{ label: '회사 ‧ 학교', value: 'COMPANY_SCHOOL' },
4845
{ label: '프리랜서', value: 'FREELANCER' },
4946
{ label: '기타', value: 'OTHER' },
5047
]
5148

49+
const testApiCall = async () => {
50+
try {
51+
const response = await getProfile()
52+
console.log('getProfile API 결과:', response.result)
53+
} catch (error) {
54+
console.error('API 호출 에러:', error)
55+
}
56+
}
57+
58+
const [preview, setPreview] = useState<string | null>(null)
59+
const fileInputRef = useRef<HTMLInputElement>(null)
60+
61+
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
62+
const file = event.target.files?.[0]
63+
if (file) {
64+
const reader = new FileReader()
65+
reader.onloadend = () => setPreview(reader.result as string)
66+
reader.readAsDataURL(file)
67+
}
68+
}
69+
70+
const handleButtonClick = () => {
71+
fileInputRef.current?.click()
72+
}
73+
74+
const onSubmit = async (data: UpdateProfileRequest) => {
75+
if (!fileInputRef.current?.files?.[0]) {
76+
return
77+
}
78+
79+
try {
80+
const profileImage = fileInputRef.current.files[0] || null
81+
const response = await updateProfile(data, profileImage)
82+
console.log('프로필 업데이트 성공:', response)
83+
} catch (error) {
84+
console.error('프로필 업데이트 실패:', error)
85+
}
86+
}
87+
5288
return (
5389
<div className='h-auto max-w-954'>
5490
<Text.Heading as='h2' variant='heading2' className='pb-8 pt-33'>
@@ -58,6 +94,12 @@ export default function MyPage(): JSX.Element {
5894
기본 정보 및 프로필을 설정할 수 있습니다.
5995
</Text.Body>
6096

97+
<div className='mb-20'>
98+
<Button variant='contained' onClick={testApiCall}>
99+
API 테스트
100+
</Button>
101+
</div>
102+
61103
<div className='w-954 rounded-12 bg-common-white p-40'>
62104
<div className='flex flex-row gap-x-20 pb-20'>
63105
<IcProfileCard width='40' height='40' />
@@ -87,17 +129,54 @@ export default function MyPage(): JSX.Element {
87129
<Form methods={methods} onSubmit={methods.handleSubmit(onSubmit)}>
88130
<div className='mb-20 flex flex-row gap-x-60'>
89131
<Label labelText='프로필 사진' className='w-146' />
90-
<div className='flex gap-x-20'>
91-
<button>
92-
<IcProfile width='60' height='60' />
132+
<div className='flex items-center gap-x-20'>
133+
<button onClick={handleButtonClick}>
134+
{preview ? (
135+
<div className='h-60 w-60 overflow-hidden rounded-full'>
136+
<Image
137+
src={preview}
138+
alt='프로필 이미지'
139+
width={60}
140+
height={60}
141+
className='h-full w-full object-cover'
142+
/>
143+
</div>
144+
) : (
145+
<IcProfile width='60' height='60' />
146+
)}
93147
</button>
94-
<Button variant='outlined'>프로필 변경</Button>
148+
<Button variant='outlined' onClick={handleButtonClick}>
149+
프로필 변경
150+
</Button>
151+
<input
152+
type='file'
153+
accept='image/*'
154+
ref={fileInputRef}
155+
className='hidden'
156+
onChange={handleFileChange}
157+
/>
95158
</div>
96159
</div>
160+
97161
<div className='mb-20 flex flex-row gap-x-60'>
98162
<Label labelText='이름' className='w-146' />
99163
<div className='w-500'>
100-
<Form.Text name='name' className='h-48' />
164+
<Form.Text
165+
name='request.name'
166+
className='h-48 text-gray-500'
167+
disabled
168+
/>
169+
</div>
170+
</div>
171+
172+
<div className='mb-20 flex flex-row gap-x-60'>
173+
<Label labelText='이메일' className='w-146' />
174+
<div className='w-500'>
175+
<Form.Text
176+
name='request.email'
177+
className='h-48 text-gray-500'
178+
disabled
179+
/>
101180
</div>
102181
</div>
103182

@@ -109,14 +188,14 @@ export default function MyPage(): JSX.Element {
109188
<div className='mb-20 flex flex-row gap-x-60'>
110189
<Label labelText='닉네임' className='w-146' />
111190
<div className='w-500'>
112-
<Form.Text name='nickname' className='h-48' />
191+
<Form.Text name='request.nickname' className='h-48' />
113192
</div>
114193
</div>
115194

116195
<div className='mb-20 flex flex-row gap-x-60'>
117196
<Label labelText='소개' className='w-146' />
118197
<div className='w-500'>
119-
<Form.TextArea size='sm' name='introduction' fullWidth />
198+
<Form.TextArea size='sm' name='request.introduction' fullWidth />
120199
</div>
121200
</div>
122201

@@ -126,7 +205,7 @@ export default function MyPage(): JSX.Element {
126205
<Text.Body variant='body2' color='gray500'>
127206
https://github.com/
128207
</Text.Body>
129-
<Form.Text name='gitHub' placeholder='입력' />
208+
<Form.Text name='request.gitHub' placeholder='입력' />
130209
</div>
131210
</div>
132211

@@ -141,12 +220,11 @@ export default function MyPage(): JSX.Element {
141220
커리어
142221
</Text.Heading>
143222

144-
<div className='mb-20 flex flex-row items-center gap-x-60'>
223+
{/* <div className='mb-20 flex flex-row items-center gap-x-60'>
145224
<Label labelText='포지션' className='h-48 w-146' />
146225
<Controller
147226
name='position'
148227
control={control}
149-
rules={{ required: '기술 스택을 선택해주세요.' }}
150228
render={({ field, fieldState: { error } }) => (
151229
<div>
152230
<Select
@@ -193,7 +271,6 @@ export default function MyPage(): JSX.Element {
193271
<Controller
194272
name='techStacks'
195273
control={control}
196-
rules={{ required: '기술 스택을 선택해주세요.' }}
197274
render={({ field, fieldState: { error } }) => (
198275
<div>
199276
<Select
@@ -233,12 +310,13 @@ export default function MyPage(): JSX.Element {
233310
</div>
234311
)}
235312
/>
236-
</div>
313+
</div> */}
314+
237315
<div className='mb-40 flex gap-x-60'>
238316
<Label labelText='소속' className='w-146' />
239317
<div className='flex w-500 gap-x-40'>
240318
<Form.Radio
241-
name='affiliation'
319+
name='request.affiliation'
242320
options={affiliationOptions}
243321
rules={{ required: '소속을 선택해주세요.' }}
244322
/>
@@ -248,9 +326,12 @@ export default function MyPage(): JSX.Element {
248326
<Button size='lg' className='bg-semantic-negative'>
249327
회원 탈퇴
250328
</Button>
251-
<Button size='lg' disabled>
329+
<Button size='lg' type='submit'>
252330
프로필 저장
253331
</Button>
332+
<Button size='lg' onClick={() => console.log(values)}>
333+
테스트
334+
</Button>
254335
</div>
255336
</Form>
256337
</div>

0 commit comments

Comments
 (0)