Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 상품 추가 모달 구현 #147

Merged
merged 6 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/app/@modal/(.)modal/home/productAdd/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ProductModal } from '@/components/@common/modal/ProductModal';
import { Modal } from '@/shared/ui/Modal';
import { cookies } from 'next/headers';

const ProductAddModal = () => {
const accessToken = cookies().get('accessToken')?.value ?? '';
return (
<Modal size="medium" closeIcon>
<ProductModal accessToken={accessToken} title="상품 추가" />
</Modal>
);
};

export default ProductAddModal;
14 changes: 14 additions & 0 deletions src/app/modal/home/productAdd/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ProductModal } from '@/components/@common/modal/ProductModal';
import { Modal } from '@/shared/ui/Modal';
import { cookies } from 'next/headers';

const ProductAddModal = () => {
const accessToken = cookies().get('accessToken')?.value ?? '';
return (
<Modal size="medium" closeIcon>
<ProductModal accessToken={accessToken} title="상품 추가" />
</Modal>
);
};

export default ProductAddModal;
99 changes: 80 additions & 19 deletions src/components/@common/modal/ProductModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,98 @@ import { FormValues } from '@/shared/@common/types/input';
import { handleDeleteButton, handleImageChange } from '@/shared/@common/utils';
import { Button, ButtonKind } from '@/shared/ui/Button/Button';
import { Dropdown, DropdownKind } from '@/shared/ui/Dropdown/Dropdown';
import { Option } from '@/shared/ui/Dropdown/Sort';
import { Input } from '@/shared/ui/Input';
import HelperText from '@/shared/ui/Input/HelperText';
import { TextBoxInput } from '@/shared/ui/Input/TextBox';
import { ImageInput } from '@/shared/ui/Input/image';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';

const dropdownOptions: Option[] = [
{ value: 'option1', label: '옵션 1' },
{ value: 'option2', label: '옵션 2' },
{ value: 'option3', label: '옵션 3' },
{ value: 'option4', label: '옵션 4' },
];
import { useProductAddMutation } from '@/components/Home/hooks/useProductAddMutation';
// import { useQueryClient } from '@tanstack/react-query';
import { CATEGORY_DROPDOWN_OPTIONS } from './constants/CATEGORY_DROPDOWN_OPTIONS';

interface ProductModalProps {
accessToken: string;
title: string;
}

export const ProductModal = ({ accessToken, title }: ProductModalProps) => {
const { register, watch, handleSubmit } = useForm<FormValues>({
const {
register,
watch,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
mode: 'onChange',
});

const [file, setFile] = useState<File | null>(null);
const [preview, setPreview] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [options, setOptions] = useState<{ value: string; label: string }[]>(
[],
);
const [category, setCategory] = useState('');

// 카테고리 선택 드롭다운 옵션 API 연동하여 불러오기
useEffect(() => {
const loadOptions = async () => {
const fetchedOptions = await CATEGORY_DROPDOWN_OPTIONS();
setOptions(fetchedOptions);
};

loadOptions();
}, []);

const text = watch('textarea', '');
const nameInput = watch('productName', '');

// const queryClient = useQueryClient();

const imageMutation = useImageMutation({ accessToken, setErrorMessage });

const handleProduct = (product: string) => {
console.log(product);
// 카테고리 선택 핸들러
const handleProductCategory = (productCategory: string) => {
setCategory(productCategory);
// console.log(typeof category); // 데이터 받을땐 String으로 받아야함
// console.log(Number(category)); // 데이터 보낼땐 Number로 변환해서 보내야함
};

const onSubmit: SubmitHandler<FormValues> = () => {
// const categoryId = Number(category);
// console.log(categoryId);

// useEffect(() => {
// console.log(Number(category)); // 상태가 업데이트된 후에 로그 출력
// }, [category]);

// const productCategoryId = Number(category);
const addProductMutation = useProductAddMutation({
accessToken,
setErrorMessage,
// productCategoryId,
});

// 저장하기 버튼 클릭 시
const onSubmit: SubmitHandler<FormValues> = (productData) => {
if (!file) return;
console.log(productData);

imageMutation.mutate(file, {
onSuccess: (data) => {
console.log(data.url);
// console.log(data);
// 이 부분이 세 곳이 다 다르게 작동,,
if (title === '상품 추가') {
try {
addProductMutation.mutate({
categoryId: Number(category),
image: data.url,
description: text,
name: nameInput,
});
} catch (error) {
console.error('상품 추가에 실패했습니다.');
}
}
},
});
};
Expand All @@ -66,11 +114,25 @@ export const ProductModal = ({ accessToken, title }: ProductModalProps) => {
<div className="flex flex-col gap-[10px] md:gap-[15px] lg:gap-5">
<Input
inputSize="small"
type="text"
placeholder="상품명 (상품 등록 여부를 확인해 주세요)"
{...register('productName', {
required: '상품명을 입력해주세요',
maxLength: {
value: 20,
message: '상품명은 20자 이하로 작성해주세요',
},
})}
isError={!!errors.productName}
/>
<HelperText type={errors.productName ? 'error' : 'basic'}>
{errors.productName
? errors.productName.message
: '최대 20자 가능'}
</HelperText>
<Dropdown
options={dropdownOptions}
onSelect={handleProduct}
options={options}
onSelect={handleProductCategory}
placeholder="카테고리 선택"
kind={DropdownKind.modal}
/>
Expand All @@ -82,17 +144,16 @@ export const ProductModal = ({ accessToken, title }: ProductModalProps) => {
handleImageChange({ event, setFile, setPreview })
}
/>
{errorMessage && (
<HelperText type="error">이미지를 다시 선택해주세요</HelperText>
)}
</div>
<TextBoxInput
register={register}
text={text}
placeholder="상품 설명을 입력해 주세요"
/>
{errorMessage && <HelperText type="error">{errorMessage}</HelperText>}
</div>
<Button
type="submit"
kind={ButtonKind.primary}
customSize="lg:text-lg w-[295px] md:w-[510px] lg:w-[540px] h-[50px] md:h-[55px] lg:h-[65px]"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { getCategory } from '@/shared/@common/apis';
import DEFAULT_CATEGORIES from '@/shared/ui/Menu/SideMenu/constants/DEFAULT_CATEGORIES';
import { Category } from '@/shared/ui/Menu/SideMenu/types/categoryType';

async function fetchCategories(): Promise<Category[]> {
try {
const response = await getCategory();

if (!response.ok) {
console.error('카테고리 데이터를 불러오는데 실패했습니다.');
return DEFAULT_CATEGORIES;
}
return await response.json();
} catch (error) {
console.error('카테고리 데이터를 불러오는데 실패했습니다.');
return DEFAULT_CATEGORIES;
}
}

export const CATEGORY_DROPDOWN_OPTIONS = async () => {
const categories = await fetchCategories();
const categoryList: Category[] = categories ?? DEFAULT_CATEGORIES;
console.log(categoryList);

// 카테고리 선택 드롭다운 옵션
return categoryList.map((category) => ({
value: String(category.id),
label: category.name,
}));
};
28 changes: 15 additions & 13 deletions src/components/Home/RankingCard/RankingCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,30 @@ export const RankingCard = ({ ranking }: RankingCardProps) => {

return (
// 프로필 페이지 주소로 이동하는 <Link> 태그
<Link href={`/profile?userId=${ranking.id}`}>
<div className="flex-none w-47 flex items-center gap-[10px] mb-7">
<div className="flex-none w-47 flex items-center gap-[10px] mb-7">
<Link href={`/profile?userId=${ranking.id}`}>
<ImageComponent
type="profile"
// 임시로 설정해둔 기본 프로필 이미지 경로 (추후 변경 예정)
src={ranking?.image ?? '/icon/profile.png'}
alt={ranking.nickname}
className="w-12 h-12 rounded-full"
/>
<div className="flex flex-col items-start gap-1 lg:gap-2">
<div className="flex items-center gap-[5px]">
<RankingChip rankNumber={ranking.rank} color={ranking.color} />
<span className="text-gray-F1 text-[16px] font-normal text-ellipsis whitespace-nowrap inline-block overflow-hidden text-overflow-ellipsis max-w-[80px]">
</Link>
<div className="flex flex-col items-start gap-1 lg:gap-2">
<div className="flex items-center gap-[5px]">
<RankingChip rankNumber={ranking.rank} color={ranking.color} />
<span className="text-gray-F1 text-[16px] font-normal text-ellipsis whitespace-nowrap inline-block overflow-hidden text-overflow-ellipsis max-w-[80px]">
<Link href={`/profile?userId=${ranking.id}`}>
{ranking.nickname}
</span>
</div>
<div className="flex gap-3 text-[12px] font-light text-gray-6E">
<span>팔로워 {formatted1000toK(ranking.followersCount)}</span>
<span>리뷰 {formatted1000toK(ranking.reviewCount)}</span>
</div>
</Link>
</span>
</div>
<div className="flex gap-3 text-[12px] font-light text-gray-6E">
<span>팔로워 {formatted1000toK(ranking.followersCount)}</span>
<span>리뷰 {formatted1000toK(ranking.reviewCount)}</span>
</div>
</div>
</Link>
</div>
);
};
45 changes: 45 additions & 0 deletions src/components/Home/hooks/useProductAddMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// import { homeProductKeys } from '@/app/[category]/homeQueryKeyFactories';
import { ProductProps, postCreateProduct } from '@/shared/@common/apis/product';
import { useMutation } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
// import { convertIdToCategory } from '@/shared/@common/utils';

interface ProductAddProps {
accessToken: string;
setErrorMessage: (message: string) => void;
// productCategoryId: number;
// queryClient: QueryClient;
}

export const useProductAddMutation = ({
accessToken,
setErrorMessage,
// productCategoryId,
// queryClient,
}: ProductAddProps) => {
const router = useRouter();

return useMutation({
mutationFn: async (data: ProductProps) => {
const response = await postCreateProduct(data, accessToken);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message);
}
return response.json();
},
onSuccess: () => {
router.back();
// -> 데이터가 바로 반영되지는 않지만 모달이 바로 꺼짐
// router.push(`/${convertIdToCategory(productCategoryId)}`);
// -> 데이터가 바로 반영되지만 모달이 안 꺼짐
//
// queryClient.invalidateQueries({
// queryKey: homeProductKeys.all(),
// });
},
onError: (error) => {
setErrorMessage(error.message);
},
});
};
2 changes: 1 addition & 1 deletion src/shared/@common/apis/product.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { API_PRODUCT } from './constants/API';

interface ProductProps {
export interface ProductProps {
categoryId: number;
image: string;
description: string;
Expand Down
1 change: 1 addition & 0 deletions src/shared/@common/types/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface FormValues {
passwordConfirmation: string;
text: string;
textarea: string;
productName: string;
}

export interface AuthInputProps {
Expand Down
13 changes: 6 additions & 7 deletions src/shared/ui/Button/Floating/Floating.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
'use client';

import { Icon } from '@/shared/ui/Icon';
import { useRouter } from 'next/navigation';

export const Floating = () => {
const makingAlertNow = () => {
alert('Floating이 클릭되었습니다!');
const router = useRouter();
const openModal = () => {
// alert('Floating이 클릭되었습니다!');
router.push(`/modal/home/productAdd`, { scroll: false });
};

/*
onClick으로 상품 등록 Modal Open 해야 함.
*/

return (
<button
type="button"
Expand All @@ -19,7 +18,7 @@ export const Floating = () => {
<Icon
name="AddIcon"
className={`w-[40px] text-white `}
onClick={makingAlertNow}
onClick={openModal}
/>
</button>
);
Expand Down