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: ProductItem 시간, style, Type 변경, 프로필 이미지 추가 #123

Merged
merged 13 commits into from
Oct 11, 2024
Merged
4 changes: 2 additions & 2 deletions src/@types/AuctionItem.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ declare module 'AuctionItem' {

export interface IUserAuctionWonItem extends IAuctionItemBase {
endDateTime: string;
winningBid: number;
winningAmount: number;
auctionId: number;
}
export interface IUserAuctionLostItem extends IAuctionItemBase {
endDateTime: string;
highestBid: number;
highestAmount: number;
auctionId: number;
}
export interface IUserAuctionHistoryItem extends Omit<IAuctionItem, 'isParticipated'> {}
Expand Down
4 changes: 4 additions & 0 deletions src/assets/icons/profile.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions src/assets/icons/profile_edit.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 7 additions & 7 deletions src/components/common/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ const Button = ({
ariaLabel = '',
loading = false,
}: ButtonProps) => {
const baseClasses = 'focus:outline-none rounded transition-colors active:bg-black active:text-white ';
const baseClasses = 'focus:outline-none rounded transition-colors active:bg-black active:text-white box-border';
const colorClasses = classNames({
'bg-black text-white': color === 'black',
'bg-white text-black border border-black': color === 'white',
'bg-gray text-white': color === 'gray',
'bg-gray2 text-white': color === 'gray2',
'bg-gray3 text-white': color === 'gray3',
'bg-cheeseYellow text-white': color === 'cheeseYellow',
'bg-black text-white border border-black': color === 'black',
'bg-white text-black border border-black': color === 'white', // 동일한 border 유지
'bg-gray text-white border border-gray': color === 'gray',
'bg-gray2 text-white border border-gray2': color === 'gray2',
'bg-gray3 text-white border border-gray3': color === 'gray3',
'bg-cheeseYellow text-white border border-cheeseYellow': color === 'cheeseYellow',
});
const sizeClasses = classNames({
'px-2 py-0.5 text-xs': size === 'xsmall',
Expand Down
19 changes: 5 additions & 14 deletions src/components/common/ProductItem.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import jordanBlackImage from '@/assets/images/jordan_black.jpeg';
import { getTimeColor } from '@/utils/getTimeColor';
import TimeLabel from './atomic/TimeLabel';

export interface ProductProps {
id: number;
name: string;
minPrice: number;
cdnPath?: string | null;
timeRemaining?: number;
participantCount?: number;
isParticipating?: boolean;
likeCount?: number;
isLiked?: boolean;
status?: string;
createdAt?: string;
imageUrl?: string;
}

const ProductItem = ({
Expand All @@ -22,26 +21,18 @@ const ProductItem = ({
product: ProductProps;
children: React.ReactNode;
}) => {
const remainHour = Math.floor((product.timeRemaining ?? 0) / 3600);
const timeColor = getTimeColor(remainHour);

return (
<div key={product.id} className="mb-4">
<div className="flex flex-col">
<div className="w-full h-auto mb-4">
<div className="relative">
<img
className="object-cover w-full h-[10rem] rounded-t"
src={`${product.cdnPath ? product.cdnPath : jordanBlackImage}`}
className="object-cover w-full h-[15rem] rounded-t"
src={product.imageUrl}
alt="Jordan Black Shoes"
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

alt 값도 수정이 필요해보입니다.

{remainHour && (
<div
className={`absolute bottom-0 w-full pt-1 text-center bg-white opacity-80 ${timeColor} border-b-2`}
>
{`${remainHour}시간 남음`}
</div>
)}
{product.timeRemaining && <TimeLabel time={product.timeRemaining} />}
</div>
</div>

Expand Down
5 changes: 2 additions & 3 deletions src/components/common/item/ProductItem.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import jordanBlackImage from '@/assets/images/jordan_black.jpeg';
import TimeLabel from '../atomic/TimeLabel';
import { ReactNode } from 'react';

export interface ProductProps {
auctionId?: number;
productName: string;
minPrice: number;
cdnPath?: string | null;
imageUrl?: string;
timeRemaining?: number;
participantCount?: number;
isParticipating?: boolean;
Expand All @@ -22,7 +21,7 @@ const ProductItem = ({ product, children }: { product: ProductProps; children: R
<div className='flex flex-col'>
<div className='w-full h-auto mb-4'>
<div className='relative'>
<img className='object-cover w-full h-[15rem] rounded-t' src={`${product.cdnPath ? product.cdnPath : jordanBlackImage}`} alt='Jordan Black Shoes' />
<img className='object-cover w-full h-[15rem] rounded-t' src={product.imageUrl} alt='제품 사진' />
{product.timeRemaining && <TimeLabel time={product.timeRemaining} />}
Copy link
Contributor

Choose a reason for hiding this comment

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

여기도 위와 마찬가지로 Default image필요없을 것 같습니다.

</div>
</div>
Expand Down
15 changes: 12 additions & 3 deletions src/components/login/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ export const postSignup = async (data: User) => {
};

export const logout = async () => {
await httpClient.post(API_END_POINT.LOGOUT, { withCredentials: true });

removeToken();
try {
await refreshToken();
await httpClient.post(API_END_POINT.LOGOUT, { withCredentials: true });
removeToken();
} catch (error) {
throw error;
}
};

export const refreshToken = async () => {
Expand All @@ -43,6 +47,11 @@ export const refreshToken = async () => {
}
};

export const nicknameCheck = async (nickname: string) => {
const response = await httpClient.get(`${API_END_POINT.NICKNAME_CHECK}/${nickname}`);
return response.data;
}

export const useRefreshTokenOnSuccess = () => {
const dispatch = useDispatch();
const queryParams = new URLSearchParams(window.location.search);
Expand Down
5 changes: 4 additions & 1 deletion src/components/order/OrderHistoryProduct.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@ import type { IUserAuctionHistoryItem } from 'AuctionItem';
import { IoPricetagsOutline } from 'react-icons/io5';
import { LuUsers } from 'react-icons/lu';
import ProductItem from '../common/item/ProductItem';
import { formatCurrencyWithWon } from '@/utils/formatCurrencyWithWon';

const OrderHistoryProduct = ({ product }: { product: IUserAuctionHistoryItem }) => {
const formattedPrice = formatCurrencyWithWon(product.minPrice);

return (
<ProductItem product={product}>
<div className='flex'>
<div className='flex gap-2'>
<IoPricetagsOutline className='text-gray-500' />
<p className='text-sm text-gray-500'>시작가</p>
</div>
<p className='ml-4 font-semibold'>{`${product.minPrice.toLocaleString()}원`}</p>
<p className='ml-4 font-semibold'>{formattedPrice}</p>
</div>
<div className='flex'>
<div className='flex gap-2'>
Expand Down
4 changes: 2 additions & 2 deletions src/components/order/OrderListTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ const OrderListTab = ({ activeTab, setActiveTab }: OrderListTabProps) => {
onClick={() => setActiveTab('AuctionHistory')}
>
{isWidthScreen ? (
'참여한 경매'
'입찰중인 경매'
) : (
<>
참여한
입찰중인
<br />
경매
</>
Expand Down
10 changes: 7 additions & 3 deletions src/components/order/OrderLostProduct.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import type { IUserAuctionLostItem } from 'AuctionItem';
import { IoPricetagsOutline } from 'react-icons/io5';
import { LuUsers } from 'react-icons/lu';
import ProductItem from '../common/item/ProductItem';
import { formatCurrencyWithWon } from '@/utils/formatCurrencyWithWon';

const OrderLostProduct = ({ product }: { product: IUserAuctionLostItem }) => {
const date = new Date(product.endDateTime);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');

const formattedDate = `${year}.${month}.${day}`;
const formattedDate = `${year}년 ${month}월 ${day}일`;
const formattedMinPrice = formatCurrencyWithWon(product.minPrice);
const formattedHighPrice = formatCurrencyWithWon(product.highestAmount);


return (
<ProductItem product={product}>
Expand All @@ -18,7 +22,7 @@ const OrderLostProduct = ({ product }: { product: IUserAuctionLostItem }) => {
<IoPricetagsOutline className='text-gray-500' />
<p className='text-sm text-gray-500'>시작가</p>
</div>
<p className='ml-4 font-semibold'>{`${product.minPrice.toLocaleString()}원`}</p>
<p className='ml-4 font-semibold'>{formattedMinPrice}</p>
</div>
<div className='flex'>
<div className='flex gap-2'>
Expand All @@ -32,7 +36,7 @@ const OrderLostProduct = ({ product }: { product: IUserAuctionLostItem }) => {
<LuUsers className='text-gray-500' />
<p className='text-sm text-gray-500'>가장 높은 금액</p>
</div>
<p className='ml-4 font-semibold'>{`${(product.highestBid).toLocaleString()}원`}</p>
<p className='ml-4 font-semibold'>{formattedHighPrice}</p>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

숫자를 원으로 바꾸는 함수를 util 함수에 작성해놨으니 그거 사용하시면 됩니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

넵!

</ProductItem>
);
Expand Down
9 changes: 6 additions & 3 deletions src/components/order/OrderWonProduct.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import type { IUserAuctionWonItem } from 'AuctionItem';
import { IoPricetagsOutline } from 'react-icons/io5';
import { LuUsers } from 'react-icons/lu';
import ProductItem from '../common/item/ProductItem';
import { formatCurrencyWithWon } from '@/utils/formatCurrencyWithWon';

const OrderWonProduct = ({ product }: { product: IUserAuctionWonItem }) => {
const date = new Date(product.endDateTime);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');

const formattedDate = `${year}.${month}.${day}`;
const formattedDate = `${year}년 ${month}월 ${day}일`;
const formattedMinPrice = formatCurrencyWithWon(product.minPrice);
const formattedWinningPrice = formatCurrencyWithWon(product.winningAmount);

return (
<ProductItem product={product}>
Expand All @@ -18,7 +21,7 @@ const OrderWonProduct = ({ product }: { product: IUserAuctionWonItem }) => {
<IoPricetagsOutline className='text-gray-500' />
<p className='text-sm text-gray-500'>시작가</p>
</div>
<p className='ml-4 font-semibold'>{`${product.minPrice.toLocaleString()}원`}</p>
<p className='ml-4 font-semibold'>{formattedMinPrice}</p>
</div>
<div className='flex'>
<div className='flex gap-2'>
Expand All @@ -32,7 +35,7 @@ const OrderWonProduct = ({ product }: { product: IUserAuctionWonItem }) => {
<LuUsers className='text-gray-500' />
<p className='text-sm text-gray-500'>최종 낙찰금액</p>
</div>
<p className='ml-4 font-semibold'>{`${(product.winningBid).toLocaleString()}원`}</p>
<p className='ml-4 font-semibold'>{formattedWinningPrice}</p>
</div>
</ProductItem>
);
Expand Down
5 changes: 4 additions & 1 deletion src/components/productList/OngoingProduct.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ import { IoPricetagsOutline } from 'react-icons/io5';
import { LuUsers } from 'react-icons/lu';
import ProductItem from '../common/item/ProductItem';
import { useNavigate } from 'react-router-dom';
import { formatCurrencyWithWon } from '@/utils/formatCurrencyWithWon';

const OngoingProduct = ({ product }: { product: IAuctionItem }) => {
const navigate = useNavigate();
const handleClick = () => navigate(`/auctions/bid/${product.auctionId}`);
const formattedPrice = formatCurrencyWithWon(product.minPrice);

return (
<ProductItem product={product}>
<div className='flex'>
<div className='flex gap-2'>
<IoPricetagsOutline className='text-gray-500' />
<p className='text-sm text-gray-500'>시작가</p>
</div>
<p className='ml-4 font-semibold'>{`${product.minPrice.toLocaleString()}원`}</p>
<p className='ml-4 font-semibold'>{formattedPrice}</p>
</div>
<div className='flex'>
<div className='flex gap-2'>
Expand Down
5 changes: 4 additions & 1 deletion src/components/productList/PreEnrollProduct.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@ import { FaHeart } from 'react-icons/fa';
import type { IPreAuctionItem } from 'AuctionItem';
import { IoPricetagsOutline } from 'react-icons/io5';
import ProductItem from '../common/item/ProductItem';
import { formatCurrencyWithWon } from '@/utils/formatCurrencyWithWon';

const PreEnrollProduct = ({ product }: { product: IPreAuctionItem }) => {
const formattedPrice = formatCurrencyWithWon(product.minPrice);

return (
<ProductItem product={product}>
<div className='flex'>
<div className='flex gap-2'>
<IoPricetagsOutline className='text-gray-500' />
<p className='text-sm text-gray-500'>시작가</p>
</div>
<p className='ml-4 font-semibold'>{`${product.minPrice.toLocaleString()}원`}</p>
<p className='ml-4 font-semibold'>{formattedPrice}</p>
</div>
<div className='flex'>
<div className='flex gap-2'>
Expand Down
50 changes: 50 additions & 0 deletions src/components/profile/ProfileImageUploader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Dispatch, SetStateAction } from 'react';
import { Input } from '../ui/input';
import { useProfileImageUploader } from '@/hooks/useProfileImageUploader';
import { Button } from '../ui/button';
import ProfileEdit from '@/assets/icons/profile_edit.svg'

interface ImageUploaderProps {
image: string | null;
setImage: Dispatch<SetStateAction<string | null>>;
file: File | null;
setFile: Dispatch<SetStateAction<File | null>>;
}

const ProfileImageUploader = ({ file, setFile, image, setImage }: ImageUploaderProps) => {
const { fileInputRef, deleteImage, handleImage, handleBoxClick } = useProfileImageUploader(image, setImage, file, setFile);

return (
<div className="flex flex-col items-center gap-4">
{image ? (
<div className="relative w-40 h-40">
<img src={image} alt="프로필 사진" className="object-cover w-full h-full rounded-full" />
<Button
type='button'
className="absolute top-[-5%] right-[-5%] bg-red-500 text-white rounded-full p-1 cursor-pointer"
onClick={deleteImage}
aria-label="프로필 사진 삭제"
>
삭제
</Button>
</div>
) : (
<div className="relative w-40 h-40" onClick={handleBoxClick}>
<img src={ProfileEdit} alt="프로필 사진" className="object-cover w-full h-full rounded-full" />
</div>
)}
<Input
ref={fileInputRef}
type="file"
id="사진"
className="hidden"
accept="image/*"
onChange={handleImage} // 다수의 파일을 받지 않음
aria-label="프로필 사진 업로드 인풋"
role="button"
/>
</div>
);
};

export default ProfileImageUploader;
12 changes: 8 additions & 4 deletions src/components/profile/queries.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { UserProfile } from '@/@types/user';
import { httpClient } from '@/api/axios';
import { API_END_POINT } from '@/constants/api';

export const getProfile = async () => {
const response = await httpClient.get(`${API_END_POINT.PROFILE}`);
const response = await httpClient.get(`${API_END_POINT.SIGNUP}`);
return response.data;
};

export const postEditProfile = async (data: UserProfile) => {
const response = await httpClient.post(`${API_END_POINT.PROFILE}/profile`,data);
export const postEditProfile = async (formData: FormData) => {
const response = await httpClient.post(`${API_END_POINT.PROFILE}`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});

return response.data;
};
Loading