Skip to content

Commit

Permalink
[Feat] 유저 등록 페이지 구현 (#58)
Browse files Browse the repository at this point in the history
* rename: JoinProgressBar -> RegisterProgress

* feat: ProfileForm button 제거 및 submitAction prop 추가

* feat: Button type prop 추가

* feat: form hook에서 onsubmit 삭제, form prop으로 받게 변경

* feat: formConfig form hook에서 분리

* feat: PortfolioInput 구현

* feat: profile, portfolio step component 구현

* feat: 유저 등록 페이지 구현

* chore: app router user register page 추가

* refactor: auth 관련 파일들 index 처리

* refactor: register page index 처리
  • Loading branch information
csk6314 authored Nov 29, 2024
1 parent 9cbe80a commit 9986b52
Show file tree
Hide file tree
Showing 21 changed files with 319 additions and 42 deletions.
5 changes: 5 additions & 0 deletions src/app/appRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ArchiveListPage,
WriteGatheringPage,
WriteArchivePage,
RegisterPage,
} from '@/pages';
import { Layout } from '@/widgets';

Expand Down Expand Up @@ -46,6 +47,10 @@ const AppRouter = () => {
path: '/user',
element: <>{/** userPage */}</>,
},
{
path: '/register',
element: <RegisterPage />,
},
],
},
]);
Expand Down
35 changes: 28 additions & 7 deletions src/features/auth/form.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { FormConfigType } from './form.types';
import { JOB_CATEGORIES, type FormValues } from './form.types';

interface useProfileFormProps {
formConfig: FormConfigType;
formConfig: FormConfigType<FormValues>;
}

export const useProfileForm = ({ formConfig }: useProfileFormProps) => {
Expand All @@ -21,7 +21,7 @@ export const useProfileForm = ({ formConfig }: useProfileFormProps) => {
minorJobGroup: null,
jobTitle: '',
division: 'student',
url: [{ value: '' }],
url: [],
imageUrl: '',
},
});
Expand Down Expand Up @@ -57,13 +57,34 @@ export const useProfileForm = ({ formConfig }: useProfileFormProps) => {
method.setValue('minorJobGroup', null, { shouldValidate: true });
}, [majorJobGroup, method]);

const onSubmit = (data: FormValues) => {
console.log('폼 데이터:', data);
};

return {
formStructure,
method,
onSubmit,
};
};

export const usePortfolioInput = () => {
const [portfolioUrl, setPortfolioUrl] = useState<string>('');
const [error, setError] = useState<string>('');

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPortfolioUrl(e.target.value);
validate(e.target.value);
};

const validate = (value: string) => {
if (value.trim() === '') {
setError('포트폴리오 URL을 입력해주세요.');
return false;
}

if (!(value.startsWith('https://') || value.startsWith('http://'))) {
setError('URL 형식이 잘못됬습니다.');
return false;
}

setError('');
return true;
};
return { portfolioUrl, error, handleInputChange, validate };
};
4 changes: 2 additions & 2 deletions src/features/auth/form.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ interface FieldSetInfo {
inputs: InputInfo[];
}

export interface FormConfigType {
export interface FormConfigType<T extends object> {
structure: FieldSetInfo[];
validation: ObjectSchema<FormValues>;
validation: ObjectSchema<T>;
}

export const JOB_CATEGORIES = [
Expand Down
6 changes: 3 additions & 3 deletions src/features/auth/form.utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as yup from 'yup';

import type { FormConfigType } from './form.types';
import type { FormConfigType, FormValues } from './form.types';
import { JOB_CATEGORIES, JOB_DIVISION } from './form.types';

export const formValidation = yup.object({
Expand Down Expand Up @@ -36,10 +36,10 @@ export const formValidation = yup.object({
)
.max(5, 'URL은 최대 5개 까지 작성 가능합니다.')
.defined(),
imageUrl: yup.string().required('프로필 이미지를 등록해주세요.'),
imageUrl: yup.string().defined(), //.required('프로필 이미지를 등록해주세요.'),
});

export const formConfig: FormConfigType = {
export const formConfig: FormConfigType<FormValues> = {
structure: [
{
title: '기본 정보',
Expand Down
6 changes: 5 additions & 1 deletion src/features/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
export { GoogleLogin } from './ui/GoogleLogin';
export * from './ui';
export * from './form.hook';
export * from './form.types';
export * from './form.utils';
export * from './progress.type';
14 changes: 14 additions & 0 deletions src/features/auth/ui/PortfolioInput.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.portfolioInput {
width: 100%;
max-width: 446px;

& > .errorMessage {
display: inline-block;
width: 100%;
padding-top: 0.25rem;
font-size: 0.875rem;
font-weight: 600;
color: $red;
text-align: center;
}
}
18 changes: 18 additions & 0 deletions src/features/auth/ui/PortfolioInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import styles from './PortfolioInput.module.scss';

import { Input } from '@/shared/ui';

interface PortfolioInputProps {
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
error: string;
value: string;
}

export const PortfolioInput = ({ handleInputChange, error, value }: PortfolioInputProps) => {
return (
<div className={styles.portfolioInput}>
<Input onChange={handleInputChange} placeholder='https://' value={value} />
{error && <span className={styles.errorMessage}>{error}</span>}
</div>
);
};
11 changes: 0 additions & 11 deletions src/features/auth/ui/ProfileForm.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,3 @@
font-weight: bold;
}
}

.submitBtnWrapper {
display: flex;
justify-content: flex-end;
width: 100%;
padding: 1rem 0;

& > button {
padding: 0.625rem 2rem;
}
}
20 changes: 12 additions & 8 deletions src/features/auth/ui/ProfileForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@ import { FormProvider } from 'react-hook-form';
import { FormField } from './FormField';
import styles from './ProfileForm.module.scss';
import { useProfileForm } from '../form.hook';
import { formConfig } from '../form.utils';
import type { FormConfigType, FormValues } from '../form.types';

import { Button } from '@/shared/ui';
interface ProfileFormProps {
formConfig: FormConfigType<FormValues>;
onSubmit: (data: FormValues) => void;
}

export const ProfileForm = () => {
const { method, formStructure, onSubmit } = useProfileForm({ formConfig });
export const ProfileForm = ({ onSubmit, formConfig }: ProfileFormProps) => {
const { method, formStructure } = useProfileForm({ formConfig });

return (
<FormProvider {...method}>
<form className={styles.profileForm} onSubmit={method.handleSubmit(onSubmit)}>
<form
className={styles.profileForm}
id='profile-form'
onSubmit={method.handleSubmit(onSubmit)}
>
{formStructure.map(section => (
<fieldset className={styles.formSection} key={section.title}>
<legend>{section.title}</legend>
Expand All @@ -21,9 +28,6 @@ export const ProfileForm = () => {
})}
</fieldset>
))}
<div className={styles.submitBtnWrapper}>
<Button type='submit'>다음</Button>
</div>
</form>
</FormProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
display: flex;
column-gap: 0.5rem;
align-items: center;
padding: 1rem;
}

.progressDot {
.progressStep {
display: flex;
flex-direction: column;
align-items: center;
min-width: 7.5rem;

& > i {
display: flex;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import cn from 'classnames';
import React from 'react';

import styles from './JoinProgressBar.module.scss';
import styles from './RegisterProgress.module.scss';
import type { JoinProgressStage } from '../progress.type';

interface ProgressBarProps {
Expand All @@ -14,14 +14,14 @@ interface JoinProgressBarProps {
joinStages: JoinProgressStage[];
}

const ProgressDot = ({
const ProgressStep = ({
currentStage,
stage,
name,
}: JoinProgressStage & { currentStage: number }) => {
return (
<div
className={cn(styles.progressDot, {
className={cn(styles.progressStep, {
[styles.active]: currentStage > stage,
[styles.doing]: stage === currentStage,
})}
Expand All @@ -42,12 +42,12 @@ const ProgressBar = ({ currentStage, prevStage }: ProgressBarProps) => {
);
};

export const JoinProgressbar = ({ currentStage, joinStages }: JoinProgressBarProps) => {
export const RegisterProgress = ({ currentStage, joinStages }: JoinProgressBarProps) => {
return (
<div className={styles.progressWrapper}>
{joinStages.map((stage, idx) => (
<React.Fragment key={stage.name}>
<ProgressDot currentStage={currentStage} name={stage.name} stage={stage.stage} />
<ProgressStep currentStage={currentStage} name={stage.name} stage={stage.stage} />
{idx !== joinStages.length - 1 && (
<ProgressBar currentStage={currentStage} prevStage={stage.stage} />
)}
Expand Down
6 changes: 6 additions & 0 deletions src/features/auth/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from './FormField';
export * from './FormInputs';
export * from './GoogleLogin';
export * from './PortfolioInput';
export * from './ProfileForm';
export * from './RegisterProgress';
32 changes: 32 additions & 0 deletions src/pages/RegisterPage/RegisterPage.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
padding: 2rem 7.5rem;
}

.sectionHeader {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 1024px;

& > h2 {
font-size: 1.5rem;
font-weight: 600;
}

& > div {
min-width: 90%;
padding: 2rem 0;
}
}

.sectionContent {
flex: 1;
width: 100%;
max-width: 1024px;
}
25 changes: 25 additions & 0 deletions src/pages/RegisterPage/RegisterPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useState } from 'react';

import styles from './RegisterPage.module.scss';

import { RegisterProgress, JOIN_STAGES } from '@/features/auth';
import { ProfileStep, PortfolioStep } from '@/widgets/RegisterUser';

const StepComponentList = [ProfileStep, PortfolioStep];

export const RegisterPage = () => {
const [stage, setStage] = useState<number>(1);

const StepComponent = StepComponentList[stage - 1];

return (
<section className={styles.container}>
<header className={styles.sectionHeader}>
<h2>회원가입</h2>
<RegisterProgress currentStage={stage} joinStages={JOIN_STAGES} />
</header>

<div className={styles.sectionContent}>{<StepComponent setStage={setStage} />}</div>
</section>
);
};
3 changes: 2 additions & 1 deletion src/pages/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './GatheringListPage';
export { WriteArchivePage } from './WriteArchivePage/WriteArchivePage';
export { DetailArchivePage } from './DetailArchivePage/DetailArchivePage';
export { RegisterPage } from './RegisterPage/RegisterPage';
export { WriteGatheringPage } from './WriteGatheringPage/WriteGatheringPage';
export { ArchiveListPage } from './ArchiveListPage/ArchiveListPage';
export { ArchiveListPage } from './ArchiveListPage/ArchiveListPage';
11 changes: 9 additions & 2 deletions src/shared/ui/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@ interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
skin?: ButtonSkins;
className?: string;
type?: 'submit' | 'reset' | 'button';
}

export const Button = ({ children, skin = 'primary', className, ...restProps }: Props) => {
export const Button = ({
children,
type = 'button',
skin = 'primary',
className,
...restProps
}: Props) => {
return (
<button className={cn(styles.btn, styles[skin], className)} {...restProps}>
<button className={cn(styles.btn, styles[skin], className)} type={type} {...restProps}>
{children}
</button>
);
Expand Down
Loading

1 comment on commit 9986b52

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚡ Lighthouse report for http://localhost:3000/

Category Score
🔴 Performance 40
🟢 Accessibility 95
🟢 Best Practices 100
🟠 SEO 83

Detailed Metrics

Metric Value
🔴 First Contentful Paint 39.2 s
🔴 Largest Contentful Paint 66.0 s
🟠 Total Blocking Time 260 ms
🟢 Cumulative Layout Shift 0
🔴 Speed Index 50.6 s

Please sign in to comment.