Skip to content

Commit f465409

Browse files
authored
Merge pull request #15 from DDD-Community/develop
[deploy] : 로그인 & 회원가입 API 연결
2 parents 3fe8cc3 + bf4ecbf commit f465409

File tree

11 files changed

+195
-98
lines changed

11 files changed

+195
-98
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useRouter } from 'next/navigation';
55
import { useEffect } from 'react';
66
import { InputField } from '@/shared/components/InputField';
77
import { useToast } from '@/shared/components/toast';
8-
import { useFetchLogin } from './useFetchLogin';
8+
import { useFetchLogin } from '../hooks/useFetchLogin';
99
import { tokenController } from '@/shared/lib/token';
1010

1111
interface LoginFormData {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import { useState } from 'react';
4-
import { postLoginApi } from './api';
4+
import { postLoginApi } from '../api';
55
import { tokenController } from '@/shared/lib/token';
66

77
export function useFetchLogin() {

src/app/login/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Link from 'next/link';
22
import Image from 'next/image';
3-
import LoginForm from './LoginForm';
3+
import LoginForm from './components/LoginForm';
44

55
export default function LoginPage() {
66
return (

src/app/signup/api.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { JobRole, SignupFormData } from '@/app/signup/type';
2+
import { apiClient } from '@/shared/lib/apiClient';
3+
import { CommonResponse } from '@/shared/type/response';
4+
5+
interface SignUpRequest extends Omit<SignupFormData, 'privacyPolicy' | 'termsOfService'> {
6+
requiredConsent: {
7+
isPrivacyPolicyAgreed: boolean;
8+
isServiceTermsAgreed: boolean;
9+
};
10+
}
11+
12+
interface SignUpResponse {}
13+
14+
interface JobRolesResponse extends CommonResponse<{ jobRoles: JobRole[] }> {}
15+
16+
export async function postSignUp(req: SignupFormData) {
17+
const { privacyPolicy, termsOfService, ...rest } = req;
18+
const request: SignUpRequest = {
19+
...rest,
20+
requiredConsent: {
21+
isPrivacyPolicyAgreed: true,
22+
isServiceTermsAgreed: true,
23+
},
24+
};
25+
return await apiClient.post<SignUpResponse, SignUpResponse>('/auth/signup', request);
26+
}
27+
28+
export async function getJobRoles() {
29+
const { data } = await apiClient.get<JobRolesResponse>('/resource/jobroles');
30+
return data.data.jobRoles;
31+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
import { getJobRoles } from '../api';
5+
import { JobRole } from '../type';
6+
7+
interface SelectJobButtonGroupProps {
8+
selectedJobId: string;
9+
onJobSelect: (jobId: string) => void;
10+
}
11+
12+
export const SelectJobButtonGroup = ({ selectedJobId, onJobSelect }: SelectJobButtonGroupProps) => {
13+
const [jobRoles, setJobRoles] = useState<JobRole[]>([]);
14+
const [isLoading, setIsLoading] = useState(true);
15+
const [error, setError] = useState<string | null>(null);
16+
17+
useEffect(() => {
18+
const fetchJobRoles = async () => {
19+
try {
20+
setIsLoading(true);
21+
setError(null);
22+
const roles = await getJobRoles();
23+
setJobRoles(roles);
24+
} catch (err) {
25+
setError('직업 목록을 불러오는데 실패했습니다.');
26+
console.error('Failed to fetch job roles:', err);
27+
} finally {
28+
setIsLoading(false);
29+
}
30+
};
31+
32+
fetchJobRoles();
33+
}, []);
34+
35+
if (isLoading) {
36+
return (
37+
<div className="grid grid-cols-2 gap-3">
38+
{Array.from({ length: 6 }).map((_, index) => (
39+
<div key={index} className="h-12 bg-gray-700 rounded-lg animate-pulse" />
40+
))}
41+
</div>
42+
);
43+
}
44+
45+
if (error) {
46+
return <div className="text-red-400 text-sm text-center py-4">{error}</div>;
47+
}
48+
49+
return (
50+
<div className="grid grid-cols-3 gap-3">
51+
{jobRoles.map(job => (
52+
<button
53+
key={job.id}
54+
type="button"
55+
onClick={() => onJobSelect(job.id)}
56+
className={`h-12 px-4 rounded-lg border transition-all duration-200 ${
57+
selectedJobId === job.id
58+
? 'bg-[#8C7FF7] border-[#8C7FF7] text-white'
59+
: 'bg-transparent border-gray-600 text-gray-300 hover:border-gray-500 hover:text-white'
60+
}`}
61+
>
62+
{job.name}
63+
</button>
64+
))}
65+
</div>
66+
);
67+
};
Lines changed: 17 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,14 @@
11
'use client';
22

33
import { useForm } from 'react-hook-form';
4-
import { useRouter } from 'next/navigation';
54
import { InputField } from '@/shared/components/InputField';
6-
import { SignupDialogButton } from '@/app/signup/SignupDialogButton';
7-
8-
type CareerYearType = 'NEWBIE' | 'JUNIOR' | 'MID' | 'SENIOR' | 'LEAD';
9-
10-
// 회원가입 시 필요한 데이터타입
11-
interface SignupFormData {
12-
email: string;
13-
password: string;
14-
name: string;
15-
jobRoleId: string;
16-
careerYear: '' | CareerYearType;
17-
privacyPolicy: boolean; // 개인정보수집동의 관련해서 백엔드 저장이 필요할듯?
18-
termsOfService: boolean; // 개인정보수집동의 관련해서 백엔드 저장이 필요할듯?
19-
}
5+
import { SignupDialogButton } from '@/app/signup/components/SignupDialogButton';
6+
import { SelectJobButtonGroup } from '@/app/signup/components/SelectJobButtonGroup';
7+
import { SignupFormData } from '@/app/signup/type';
8+
import { useFetchSignUp } from '@/app/signup/hooks/ useFetchSignUp';
209

2110
const SignUpForm = () => {
22-
const router = useRouter();
11+
const { isSubmitting, isSignupSuccess, fetchSignUp } = useFetchSignUp();
2312
const {
2413
watch,
2514
setValue,
@@ -38,12 +27,8 @@ const SignUpForm = () => {
3827
});
3928
const jobRoleId = watch('jobRoleId');
4029

41-
const onSubmit = async (data: SignupFormData) => {
42-
console.log('Form submitted:', data);
43-
};
44-
4530
return (
46-
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 w-full">
31+
<form className="space-y-6 w-full">
4732
<InputField
4833
label="Email"
4934
type="email"
@@ -59,7 +44,7 @@ const SignUpForm = () => {
5944
errorMessage={errors.email?.message as string}
6045
/>
6146
<InputField
62-
label="PW"
47+
label="비밀번호"
6348
type="password"
6449
placeholder="비밀번호를 입력해주세요."
6550
{...register('password', {
@@ -89,35 +74,10 @@ const SignUpForm = () => {
8974

9075
<div className="space-y-2">
9176
<label className="block text-sm font-medium text-gray-300">직무</label>
92-
<div className="flex gap-2">
93-
<button
94-
type="button"
95-
className={`px-4 py-2 rounded-lg ${
96-
jobRoleId === 'jobId1' ? 'bg-[#8C7FF7] text-white' : 'bg-[#2C2C2E] text-gray-400'
97-
}`}
98-
onClick={() => setValue('jobRoleId', 'jobId1', { shouldValidate: true })}
99-
>
100-
기획
101-
</button>
102-
<button
103-
type="button"
104-
className={`px-4 py-2 rounded-lg ${
105-
jobRoleId === 'jobId2' ? 'bg-[#8C7FF7] text-white' : 'bg-[#2C2C2E] text-gray-400'
106-
}`}
107-
onClick={() => setValue('jobRoleId', 'jobId2', { shouldValidate: true })}
108-
>
109-
디자이너
110-
</button>
111-
<button
112-
type="button"
113-
className={`px-4 py-2 rounded-lg ${
114-
jobRoleId === 'jobId3' ? 'bg-[#8C7FF7] text-white' : 'bg-[#2C2C2E] text-gray-400'
115-
}`}
116-
onClick={() => setValue('jobRoleId', 'jobId3', { shouldValidate: true })}
117-
>
118-
개발자
119-
</button>
120-
</div>
77+
<SelectJobButtonGroup
78+
selectedJobId={jobRoleId}
79+
onJobSelect={jobId => setValue('jobRoleId', jobId, { shouldValidate: true })}
80+
/>
12181
{errors.jobRoleId && <p className="text-xs text-red-500">{errors.jobRoleId.message as string}</p>}
12282
</div>
12383

@@ -157,7 +117,12 @@ const SignUpForm = () => {
157117
</label>
158118
{errors.termsOfService && <p className="text-xs text-red-500">{errors.termsOfService.message as string}</p>}
159119
</div>
160-
<SignupDialogButton isValid={isValid} />
120+
<SignupDialogButton
121+
isValid={isValid}
122+
isSubmitting={isSubmitting}
123+
isSignupSuccess={isSignupSuccess}
124+
onClick={handleSubmit(fetchSignUp)}
125+
/>
161126
</form>
162127
);
163128
};

src/app/signup/SignupDialogButton.tsx renamed to src/app/signup/components/SignupDialogButton.tsx

Lines changed: 16 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,23 @@
11
'use client';
22

3-
import { useState } from 'react';
3+
import { useEffect, useState } from 'react';
44
import { useRouter } from 'next/navigation';
55
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/shared/components/dialog';
66

77
interface SignupButtonProps {
88
isValid: boolean;
9+
onClick: () => void;
10+
isSignupSuccess: boolean;
11+
isSubmitting: boolean;
912
}
1013

11-
export const SignupDialogButton = ({ isValid }: SignupButtonProps) => {
14+
export const SignupDialogButton = ({ isValid, onClick, isSignupSuccess, isSubmitting }: SignupButtonProps) => {
1215
const router = useRouter();
13-
const [isSubmitting, setIsSubmitting] = useState(false);
1416
const [showSuccessDialog, setShowSuccessDialog] = useState(false);
1517

16-
const handleSignup = async () => {
17-
if (!isValid) return;
18-
setShowSuccessDialog(true);
19-
20-
// try {
21-
// setIsSubmitting(true);
22-
//
23-
// // 회원가입 API 호출
24-
// const response = await fetch('/api/signup', {
25-
// method: 'POST',
26-
// headers: {
27-
// 'Content-Type': 'application/json',
28-
// },
29-
// body: JSON.stringify(formData),
30-
// });
31-
//
32-
// if (!response.ok) {
33-
// throw new Error('회원가입에 실패했습니다.');
34-
// }
35-
//
36-
// // 성공 시 Dialog 표시
37-
// setShowSuccessDialog(true);
38-
// } catch (error) {
39-
// console.error('회원가입 실패:', error);
40-
// // TODO: 에러 처리 (토스트 메시지 등)
41-
// } finally {
42-
// setIsSubmitting(false);
43-
// }
44-
};
18+
useEffect(() => {
19+
if (isSignupSuccess) setShowSuccessDialog(true);
20+
}, [isSignupSuccess]);
4521

4622
const handleSuccessConfirm = () => {
4723
setShowSuccessDialog(false);
@@ -51,14 +27,16 @@ export const SignupDialogButton = ({ isValid }: SignupButtonProps) => {
5127
return (
5228
<>
5329
<button
54-
onClick={handleSignup}
55-
type="submit"
56-
disabled={!isValid}
30+
type="button"
31+
onClick={onClick}
32+
disabled={!isValid || isSubmitting}
5733
className={`w-full py-3 rounded-lg font-medium transition-colors ${
58-
isValid ? 'bg-white text-black hover:bg-gray-100' : 'bg-[#2C2C2E] text-gray-500 cursor-not-allowed'
34+
isValid && !isSubmitting
35+
? 'bg-white text-black hover:bg-gray-100'
36+
: 'bg-[#2C2C2E] text-gray-500 cursor-not-allowed'
5937
}`}
6038
>
61-
가입하기
39+
{isSubmitting ? '가입 중...' : '가입하기'}
6240
</button>
6341
<Dialog open={showSuccessDialog} onOpenChange={setShowSuccessDialog}>
6442
<DialogContent className="sm:max-w-md">
@@ -69,11 +47,7 @@ export const SignupDialogButton = ({ isValid }: SignupButtonProps) => {
6947
</svg>
7048
</div>
7149
<DialogTitle className="text-center">회원가입 완료</DialogTitle>
72-
<DialogDescription className="text-center">
73-
GROWIT 회원가입이 성공적으로 완료되었습니다.
74-
<br />
75-
로그인 페이지로 이동합니다.
76-
</DialogDescription>
50+
<DialogDescription className="text-center">GROWIT 에 오신것을 환영해요!.</DialogDescription>
7751
</DialogHeader>
7852
<div className="flex justify-center">
7953
<button onClick={handleSuccessConfirm} className="w-full">
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useState } from 'react';
2+
import { AxiosError } from 'axios';
3+
import { SignupFormData } from '@/app/signup/type';
4+
import { CommonError } from '@/shared/type/response';
5+
import { useToast } from '@/shared/components/toast';
6+
import { postSignUp } from '@/app/signup/api';
7+
8+
export function useFetchSignUp() {
9+
const { showToast } = useToast();
10+
const [isSubmitting, setSubmitting] = useState<boolean>(false);
11+
const [isSignupSuccess, setSignupSuccess] = useState<boolean>(false);
12+
13+
const fetchSignUp = async (data: SignupFormData) => {
14+
try {
15+
setSubmitting(true);
16+
await postSignUp(data);
17+
setSubmitting(false);
18+
setSignupSuccess(true);
19+
} catch (error) {
20+
const axiosError = error as AxiosError<CommonError>;
21+
if (axiosError.isAxiosError && axiosError.response?.data.message) {
22+
const errorMessage = axiosError.response
23+
? axiosError.response?.data.message
24+
: '예상치 못한 문제가 발생했습니다.';
25+
showToast(errorMessage);
26+
}
27+
setSubmitting(false);
28+
setSignupSuccess(false);
29+
}
30+
};
31+
32+
return {
33+
isSubmitting,
34+
isSignupSuccess,
35+
fetchSignUp,
36+
};
37+
}

src/app/signup/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Link from 'next/link';
22
import Image from 'next/image';
3-
import SignUpForm from './SignUpForm';
3+
import SignUpForm from './components/SignUpForm';
44

55
export default function SignupPage() {
66
return (

src/app/signup/type.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export type CareerYearType = 'NEWBIE' | 'JUNIOR' | 'MID' | 'SENIOR' | 'LEAD';
2+
3+
export interface SignupFormData {
4+
email: string;
5+
password: string;
6+
name: string;
7+
jobRoleId: string;
8+
careerYear: '' | CareerYearType;
9+
privacyPolicy: boolean; // 개인정보수집동의 관련해서 백엔드 저장이 필요할듯?
10+
termsOfService: boolean; // 개인정보수집동의 관련해서 백엔드 저장이 필요할듯?
11+
}
12+
13+
export interface JobRole {
14+
id: string;
15+
name: string;
16+
}

0 commit comments

Comments
 (0)