Skip to content

Commit b33ab14

Browse files
authored
Merge pull request #44 from Leets-Official/38-feat/로그인/회원가입-페이지-구현
[Feat]로그인/회원가입 페이지 구현
2 parents 1ba2827 + 7b611b4 commit b33ab14

25 files changed

Lines changed: 10007 additions & 9609 deletions

eslint.config.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,16 @@ import storybook from 'eslint-plugin-storybook';
1212

1313
export default [
1414
{
15-
ignores: ['dist', 'build', 'node_modules', '.yarn', '.pnp.*', '*.config.js', 'storybook-static'],
15+
ignores: [
16+
'dist',
17+
'build',
18+
'node_modules',
19+
'.yarn',
20+
'.pnp.*',
21+
'*.config.js',
22+
'storybook-static',
23+
'.react-router/**',
24+
],
1625
},
1726
js.configs.recommended,
1827
...tseslint.configs.recommended,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"pretendard": "^1.3.9",
3333
"react": "^19.2.3",
3434
"react-dom": "^19.2.3",
35-
"react-hook-form": "^7.68.0",
35+
"react-hook-form": "^7.71.1",
3636
"react-router": "7.12.0",
3737
"tailwind-merge": "^3.4.0",
3838
"tailwind-variants": "^3.2.2",

src/app/routes.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
import { index, route, type RouteConfig } from '@react-router/dev/routes';
22

3-
export default [index('routes/_index.tsx'), route('playground', 'routes/playground.tsx')] satisfies RouteConfig;
3+
export default [
4+
route('auth/login', 'routes/auth.login.tsx'),
5+
route('auth/signup', 'routes/auth.signup.tsx'),
6+
index('routes/_index.tsx'),
7+
route('playground', 'routes/playground.tsx'),
8+
] satisfies RouteConfig;

src/app/routes/_index.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1+
import { Navigate } from 'react-router';
2+
13
export default function Index() {
2-
return (
3-
<div>
4-
<h1>Home</h1>
5-
</div>
6-
);
4+
return <Navigate to="/auth/login" replace />;
75
}

src/app/routes/auth.login.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import LoginPage from '@pages/auth/ui/LoginPage';
2+
3+
export default function LoginRoute() {
4+
return <LoginPage />;
5+
}

src/app/routes/auth.signup.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import SignupPage from '@pages/auth/ui/SignupPage';
2+
3+
export default function SignupRoute() {
4+
return <SignupPage />;
5+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export const loginStyles = {
2+
wrapper: `
3+
w-screen h-screen
4+
flex items-center justify-center
5+
bg-cover bg-no-repeat
6+
overflow-x-hidden
7+
`,
8+
9+
greeting: `
10+
typo-body-2
11+
text-gray-900
12+
text-center
13+
w-[215px]
14+
`,
15+
16+
kakaoButton: `
17+
transition
18+
hover:opacity-90
19+
`,
20+
};

src/pages/auth/ui/LoginPage.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import kakaoLogin from '@shared/assets/auth/kakao_login_medium_wide.png';
2+
import loginBg from '@shared/assets/background/login_background.png';
3+
import logo from '@shared/assets/logo/logo.svg';
4+
import { loginStyles as s } from './LoginPage.styles';
5+
6+
export default function LoginPage() {
7+
return (
8+
<div className={s.wrapper} style={{ backgroundImage: `url(${loginBg})` }}>
9+
<div className="flex h-full w-full flex-col items-center justify-center gap-[26px] px-6 py-12 md:px-[468px] md:py-[290px]">
10+
<img src={logo} alt="LOOPIT Logo" className="h-[36px] w-[192px]" />
11+
12+
<div className="flex flex-col items-center gap-[61px]">
13+
<p className={s.greeting}>만나서 반가워요!</p>
14+
15+
<button
16+
className={s.kakaoButton}
17+
onClick={() => {
18+
console.log('카카오 로그인 클릭');
19+
}}
20+
>
21+
<img src={kakaoLogin} alt="카카오 로그인" className="h-[45px] w-[300px] object-cover" />
22+
</button>
23+
</div>
24+
</div>
25+
</div>
26+
);
27+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const signupStyles = {
2+
form: `
3+
flex min-h-screen w-full justify-center bg-white overflow-x-hidden
4+
`,
5+
sectionLabel: `
6+
typo-body-2 text-black
7+
`,
8+
};

src/pages/auth/ui/SignupPage.tsx

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { zodResolver } from '@hookform/resolvers/zod';
2+
import pictureIcon from '@shared/assets/icons/common/picture.svg';
3+
import logo from '@shared/assets/logo/logo.svg';
4+
import { Button } from '@shared/ui/Button/Button';
5+
import { Profile } from '@shared/ui/Profile';
6+
import { DateField } from '@shared/ui/TextField';
7+
import { TextField } from '@shared/ui/TextField/TextField';
8+
import { signupSchema, type SignupFormData } from '@shared/utils/schemas';
9+
import { useEffect, useRef, useState } from 'react';
10+
import { useForm } from 'react-hook-form';
11+
import { signupStyles } from './SignupPage.styles';
12+
export default function SignupPage() {
13+
const [profileImage, setProfileImage] = useState<string | undefined>();
14+
const fileInputRef = useRef<HTMLInputElement>(null);
15+
const {
16+
register,
17+
handleSubmit,
18+
formState: { errors },
19+
} = useForm<SignupFormData>({ resolver: zodResolver(signupSchema) });
20+
const onSubmit = (data: SignupFormData) => {
21+
void data;
22+
};
23+
const handleSelectImage = () => {
24+
fileInputRef.current?.click();
25+
};
26+
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
27+
const file = e.target.files?.[0];
28+
if (!file) {
29+
return;
30+
}
31+
if (profileImage) {
32+
URL.revokeObjectURL(profileImage);
33+
}
34+
const imageUrl = URL.createObjectURL(file);
35+
setProfileImage(imageUrl);
36+
};
37+
useEffect(() => {
38+
return () => {
39+
if (profileImage) {
40+
URL.revokeObjectURL(profileImage);
41+
}
42+
};
43+
}, [profileImage]);
44+
return (
45+
<form onSubmit={handleSubmit(onSubmit)} className={signupStyles.form}>
46+
{' '}
47+
<div className="flex w-full max-w-[1440px] flex-col items-center px-6 pb-24 md:px-[120px] md:pb-[258px]">
48+
{' '}
49+
{/* 헤더 */}{' '}
50+
<section className="flex w-full flex-col items-center gap-[10px] bg-green-50 px-6 py-10 md:px-[309px] md:py-[61px]">
51+
{' '}
52+
<header className="flex flex-col items-center gap-[16px] md:flex-row md:gap-[35px]">
53+
{' '}
54+
<img src={logo} alt="LOOPIT Logo" className="h-[36px] w-[192px]" />{' '}
55+
<div className="flex flex-col gap-[8px]">
56+
{' '}
57+
<h1 className="typo-title-3 text-black">회원가입</h1>{' '}
58+
<p className="typo-body-1 text-black">루핏 가입을 위해 회원님 정보를 입력해주세요.</p>{' '}
59+
</div>{' '}
60+
</header>{' '}
61+
</section>{' '}
62+
{/* 콘텐츠 */}{' '}
63+
<section className="mt-[80px] w-full">
64+
{' '}
65+
<div className="flex w-full flex-col items-start gap-[67px] md:w-[1200px]">
66+
{' '}
67+
{/* 프로필 사진 */}
68+
<section className="flex h-[214px] w-full flex-col gap-[16px]">
69+
<span className={signupStyles.sectionLabel}>프로필 사진</span>
70+
<div className="flex w-full justify-center">
71+
<button
72+
type="button"
73+
className="relative h-[182px] w-[182px] cursor-pointer"
74+
onClick={handleSelectImage}
75+
>
76+
<Profile size="lg" image={profileImage} className="h-[182px] w-[182px]" />
77+
<div className="absolute right-0 bottom-0 flex h-[40px] w-[40px] items-center justify-center rounded-full border border-[var(--color-gray-300)] bg-white">
78+
<img src={pictureIcon} alt="사진 선택" className="h-[40px] w-[40px]" />
79+
</div>
80+
<input
81+
ref={fileInputRef}
82+
type="file"
83+
accept="image/*"
84+
className="hidden"
85+
onChange={handleImageChange}
86+
/>
87+
</button>
88+
</div>
89+
</section>
90+
{/* 이메일 */}{' '}
91+
<section className="flex w-full flex-col gap-[16px]">
92+
{' '}
93+
<span className={signupStyles.sectionLabel}>이메일</span>{' '}
94+
<TextField
95+
placeholder="이메일을 입력해주세요"
96+
{...register('email')}
97+
error={Boolean(errors.email)}
98+
helperText={errors.email?.message}
99+
/>{' '}
100+
</section>{' '}
101+
{/* 이름 */}{' '}
102+
<section className="flex w-full flex-col gap-[16px]">
103+
{' '}
104+
<span className={signupStyles.sectionLabel}>이름</span>{' '}
105+
<TextField
106+
placeholder="이름을 입력해주세요"
107+
{...register('name')}
108+
error={Boolean(errors.name)}
109+
helperText={errors.name?.message}
110+
/>{' '}
111+
</section>{' '}
112+
{/* 생년월일 */}{' '}
113+
<section className="flex w-full flex-col gap-[16px]">
114+
{' '}
115+
<span className={signupStyles.sectionLabel}>생년월일</span>{' '}
116+
<DateField
117+
{...register('birthDate')}
118+
error={Boolean(errors.birthDate)}
119+
helperText={errors.birthDate?.message}
120+
/>{' '}
121+
</section>{' '}
122+
{/* 닉네임 */}{' '}
123+
<section className="flex w-full flex-col gap-[16px]">
124+
{' '}
125+
<span className={signupStyles.sectionLabel}>닉네임</span>{' '}
126+
<TextField
127+
placeholder="2~20자의 닉네임"
128+
{...register('nickname')}
129+
error={Boolean(errors.nickname)}
130+
helperText={errors.nickname?.message}
131+
/>{' '}
132+
</section>{' '}
133+
</div>{' '}
134+
</section>{' '}
135+
{/* 회원가입 버튼 */}{' '}
136+
<section className="mt-[127px] flex w-full justify-center md:justify-end">
137+
{' '}
138+
<Button variant="fill" size="auto" className="w-full max-w-[286px]" type="submit">
139+
{' '}
140+
회원가입 완료{' '}
141+
</Button>{' '}
142+
</section>{' '}
143+
</div>{' '}
144+
</form>
145+
);
146+
}

0 commit comments

Comments
 (0)