Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…roject/WEB1_1_ZeroOne_FE into feat/#82-gathering-api
  • Loading branch information
joarthvr committed Dec 7, 2024
2 parents fdf7c0c + 7484dd5 commit 3cbba3f
Show file tree
Hide file tree
Showing 62 changed files with 1,396 additions and 186 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@
"react-hook-form": "^7.53.2",
"react-router-dom": "^6.28.0",
"react-select": "^5.8.3",
"react-slick": "^0.30.2",
"sass": "^1.81.0",
"slick-carousel": "^1.8.1",
"sweetalert2": "^11.14.5",
"yup": "^1.4.0",
"zustand": "^5.0.1"
Expand Down Expand Up @@ -74,6 +76,7 @@
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"@types/react-slick": "^0.23.13",
"@typescript-eslint/eslint-plugin": "^8.12.2",
"@typescript-eslint/parser": "^8.12.2",
"@vitejs/plugin-react": "^4.3.3",
Expand Down
7 changes: 6 additions & 1 deletion src/app/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { getLocalAccessToken, removeLocalAccessToken } from '@/features/auth/aut
import { useUserStore } from '@/features/user/model/user.store';
import { getMyProfile } from '@/features/user/user.api';
import { setInterceptorEvents } from '@/shared/api/baseApi';
import { useRegistAlarm } from '@/shared/hook/useRegistAlarm';
import { customConfirm } from '@/shared/ui';

const AuthProvider = ({ children }: { children: ReactNode }) => {
const navigate = useNavigate();
const accessToken = getLocalAccessToken();
const { setUserData, clearUserData } = useUserStore(state => state.actions);
const { setUserData, clearUserData, done } = useUserStore(state => state.actions);
useRegistAlarm();

useEffect(() => {
const getUserData = async () => {
Expand All @@ -20,9 +22,12 @@ const AuthProvider = ({ children }: { children: ReactNode }) => {
const userData = await getMyProfile().then(res => res.data);
if (!userData) throw new Error('유저 정보를 찾을 수가 없습니다.');
setUserData(userData);

done();
return userData;
}
clearUserData();
done();
} catch (error) {
console.error('유저 데이터를 불러오는 중 오류가 발생했습니다.', error);
}
Expand Down
3 changes: 2 additions & 1 deletion src/app/appRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
WriteArchivePage,
WriteGatheringPage,
LikeListPage,
MainPage,
} from '@/pages';
import { Layout } from '@/widgets';

Expand All @@ -24,7 +25,7 @@ const AppRouter = () => {
children: [
{
path: '/',
element: <>{/** mainPage */}</>,
element: <MainPage />,
},
{
path: '/portfolio',
Expand Down
1 change: 1 addition & 0 deletions src/app/styles/variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ $header-horizontal-padding: 4rem;
$form-gray-color: #d7d7d7;
$form-section-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 25%);
$form-input-color: #999a99;
$form-input-disabled: #f0f0f0;

// 그라데이션
$palette-gradient: linear-gradient(151deg, $red 0%, $yellow 32%, $green 71%, $blue 100%);
Expand Down
3 changes: 3 additions & 0 deletions src/features/archive/archive.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,6 @@ export const getMyArchiveList = () =>
api.get<GetArchiveListApiResponse>('/archive/me').then(res => res.data);

export const patchArchiveOrder = (data: PatchArchiveOrderDTO) => api.patch('/archive', data);

export const getPopularArchive = () =>
api.get<GetArchiveListApiResponse>('/archive/main').then(res => res.data);
7 changes: 7 additions & 0 deletions src/features/archive/archive.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
putComment,
getMyArchiveList,
patchArchiveOrder,
getPopularArchive,
} from './archive.api';
import type {
BaseArchiveDTO,
Expand Down Expand Up @@ -62,6 +63,12 @@ export const useArchive = (archiveId: number) =>
queryFn: () => getArchive(archiveId),
});

export const usePopularArchive = () =>
useQuery({
queryKey: ['/archive/main'],
queryFn: () => getPopularArchive(),
});

export const useComments = (archiveId: number) => {
return useCustomInfiniteQuery<GetCommentsApiResponse, Comment, Error>(
['/archive', archiveId, 'comment'],
Expand Down
20 changes: 18 additions & 2 deletions src/features/auth/form.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ interface useProfileFormProps<T extends FormValues> {

export const useProfileForm = <T extends FormValues>({ formConfig }: useProfileFormProps<T>) => {
const [formStructure, setFormStructure] = useState([...formConfig.structure]);
const [isResetting, setIsResetting] = useState<boolean>(false);
const method = useForm({
resolver: yupResolver(formConfig.validation),
mode: 'onChange',
defaultValues: formConfig.defaultValues,
});

const majorJobGroup = method.watch('majorJobGroup');

/**
Expand All @@ -26,6 +28,11 @@ export const useProfileForm = <T extends FormValues>({ formConfig }: useProfileF
useEffect(() => {
if (!majorJobGroup) return;

if (isResetting) {
setIsResetting(false);
return;
}

setFormStructure(prev => {
const updatedStructure = [...prev];

Expand All @@ -41,7 +48,6 @@ export const useProfileForm = <T extends FormValues>({ formConfig }: useProfileF
break;
}
}

return updatedStructure;
});

Expand All @@ -50,9 +56,15 @@ export const useProfileForm = <T extends FormValues>({ formConfig }: useProfileF
});
}, [majorJobGroup, method]);

const handleReset = (data: Partial<T>) => {
setIsResetting(true);
method.reset(data);
};

return {
formStructure,
method,
handleReset,
};
};

Expand All @@ -71,7 +83,11 @@ export const usePortfolioInput = () => {
return false;
}

if (!(value.startsWith('https://') || value.startsWith('http://'))) {
if (
!value.match(
/^((ftp|http|https):\/\/)?(www.)?(?!.*(ftp|http|https|www.))[a-zA-Z0-9_-]+(\.[a-zA-Z]+)+((\/)[\w#]+)*(\/\w+\?[a-zA-Z0-9_]+=\w+(&[a-zA-Z0-9_]+=\w+)*)?$/gm,
)
) {
setError('URL 형식이 잘못됬습니다.');
return false;
}
Expand Down
22 changes: 17 additions & 5 deletions src/features/auth/form.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export type Option = {
label: string;
};

export type ImageField = {
url: string;
file: File | null;
};

export interface FormValues {
name: string;
briefIntro: string;
Expand All @@ -14,24 +19,29 @@ export interface FormValues {
jobTitle: string;
division: string;
url: { value: string }[];
imageUrl: string;
imageUrl: ImageField;
}

export interface PortfolioFormValues extends FormValues {
portfolioUrl: string;
portfolioLink: string;
email: string;
}

export type FormInputType = 'default' | 'radio' | 'select' | 'image' | 'textarea';

export type FormValuesName = keyof FormValues;
export type PortfolioFormValuesName = keyof PortfolioFormValues;

export interface InputFieldProps {
export interface CommonInputAttribute {
maxLength?: number;
disabled?: boolean;
placeholder?: string;
}

export interface InputFieldProps extends CommonInputAttribute {
name: FormValuesName | PortfolioFormValuesName;
type?: FormInputType;
placeholder?: string;
options?: Option[];
maxLength?: number;
}

interface InputInfo extends InputFieldProps {
Expand Down Expand Up @@ -129,6 +139,8 @@ export const JOB_CATEGORIES = [
},
];

export const JOB_SUB_CATEGORY = JOB_CATEGORIES.map(major => major.children).flat();

export const JOB_DIVISION: Option[] = [
{ value: 'student', label: '학생' },
{ value: 'worker', label: '회사' },
Expand Down
98 changes: 87 additions & 11 deletions src/features/auth/form.utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import * as yup from 'yup';

import type { FormConfigType, FormValues, PortfolioFormValues } from './form.types';
import type { FormConfigType, FormValues, ImageField, PortfolioFormValues } from './form.types';
import { JOB_CATEGORIES, JOB_DIVISION } from './form.types';
import { postImages } from '../image/image.api';

export const formValidation = yup.object({
name: yup.string().required('이름을 입력해주세요.'),
briefIntro: yup.string().defined().max(100, '100글자 이하로 소개 글을 작성해주세요.'),
briefIntro: yup
.string()
.required('자기소개를 입력해주세요.')
.max(100, '100글자 이하로 소개 글을 작성해주세요.'),
majorJobGroup: yup
.object()
.shape({
Expand All @@ -29,14 +33,22 @@ export const formValidation = yup.object({
yup.object().shape({
value: yup
.string()
.defined()
.required('URL을 입력해주세요.')
.url('URL 형식에 맞게 입력해주세요.'),
.matches(
/^((ftp|http|https):\/\/)?(www.)?(?!.*(ftp|http|https|www.))[a-zA-Z0-9_-]+(\.[a-zA-Z]+)+((\/)[\w#]+)*(\/\w+\?[a-zA-Z0-9_]+=\w+(&[a-zA-Z0-9_]+=\w+)*)?$/gm,
'URL 형식에 맞게 입력해주세요.',
),
}),
)
.max(5, 'URL은 최대 5개 까지 작성 가능합니다.')
.defined(),
imageUrl: yup.string().defined(), //.required('프로필 이미지를 등록해주세요.'),
imageUrl: yup
.object()
.shape({
url: yup.string().defined(),
file: yup.mixed().nullable(),
})
.defined(), //.required('프로필 이미지를 등록해주세요.'),
});

export const formConfig: FormConfigType<FormValues> = {
Expand Down Expand Up @@ -118,17 +130,57 @@ export const formConfig: FormConfigType<FormValues> = {
jobTitle: '',
division: 'student',
url: [],
imageUrl: '',
imageUrl: {
url: '',
file: null,
},
},
};

export const profileFormValidation = formValidation.shape({
portfolioUrl: yup.string().defined().url('URL 형식이 아닙니다.'),
email: yup.string().defined(),
portfolioLink: yup
.string()
.defined()
.matches(
/^\s*$|^((ftp|http|https):\/\/)?(www.)?(?!.*(ftp|http|https|www.))[a-zA-Z0-9_-]+(\.[a-zA-Z]+)+((\/)[\w#]+)*(\/\w+\?[a-zA-Z0-9_]+=\w+(&[a-zA-Z0-9_]+=\w+)*)?$/gm,
'URL 형식에 맞게 입력해주세요.',
),
});

export const profileFormConfig: FormConfigType<PortfolioFormValues> = {
structure: [
...formConfig.structure.slice(0, 2),
{
title: '기본 정보',
inputs: [
{
label: '프로필 사진',
type: 'image',
name: 'imageUrl',
},
{
label: '이메일',
type: 'default',
name: 'email',
disabled: true,
},
{
label: '이름',
type: 'default',
name: 'name',
required: true,
placeholder: '이름을 입력해주세요.',
},
{
label: '한 줄 소개',
type: 'textarea',
name: 'briefIntro',
maxLength: 100,
placeholder: '한 줄 소개를 입력해주세요.',
},
],
},
...formConfig.structure.slice(1, 2),
{
title: 'URL',
inputs: [
Expand All @@ -140,7 +192,7 @@ export const profileFormConfig: FormConfigType<PortfolioFormValues> = {
{
label: '포트폴리오 URL',
type: 'default',
name: 'portfolioUrl',
name: 'portfolioLink',
placeholder: 'https://',
},
],
Expand All @@ -155,7 +207,31 @@ export const profileFormConfig: FormConfigType<PortfolioFormValues> = {
jobTitle: '',
division: 'student',
url: [],
imageUrl: '',
portfolioUrl: '',
imageUrl: {
url: '',
file: null,
},
portfolioLink: '',
email: '[email protected]',
},
};

export const handleImageUpload = async (imageUrl: ImageField) => {
let profileImageUrl = imageUrl.url;

//이미지 업로드 처리
if (imageUrl.file) {
try {
const imageData = new FormData();
imageData.append('files', imageUrl.file);
const image = await postImages(imageData).then(res => res.data);
if (image && image.imgUrls[0]) {
profileImageUrl = image.imgUrls[0].imgUrl;
}
} catch {
console.error('Failed to upload image');
}

return profileImageUrl;
}
};
3 changes: 1 addition & 2 deletions src/features/auth/ui/FormField.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
}

& > div {
width: 100%;
width: 80%;
}

& > svg {
Expand All @@ -34,7 +34,6 @@
row-gap: 0.5rem;
align-items: flex-end;
width: 100%;
max-width: 426px;

& .iconBtn {
font-size: 1.5rem;
Expand Down
Loading

0 comments on commit 3cbba3f

Please sign in to comment.