Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
12 changes: 6 additions & 6 deletions src/components/card/Card.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { CustomButton } from '@/components/button';
import creditImg from '@/assets/images/credit.png';
import starImg from '@/assets/images/logo.png';
import { addCommas, getDaysRemaining, getDonationPercentage } from '@/utils/format';
import * as S from './card.styles';

Expand All @@ -9,15 +9,15 @@ const Card = ({ data, setModalType, setSelectedIndex, index }) => {
const percent = `${getDonationPercentage(data.targetDonation, data.receivedDonations)}%`;
const isDonationAvailable = daysLeft > 0;

// 실시간으로 남은 일수를 갱신하기 위해 setInterval 사용
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
const interval = setInterval(() => {
const newDaysLeft = getDaysRemaining(data.deadline);
setDaysLeft(newDaysLeft); // 갱신된 남은 일수로 상태 업데이트
}, 55000);
setDaysLeft(newDaysLeft);
}, 60000);

return () => clearInterval(interval);
}, [data.deadline]);
}, []);

const handleClick = () => {
setSelectedIndex(index);
Expand Down Expand Up @@ -51,7 +51,7 @@ const Card = ({ data, setModalType, setSelectedIndex, index }) => {
<div css={S.info}>
<div css={S.statusBar}>
<div css={S.statusLeft}>
<img src={creditImg} alt="크레딧" css={S.icon} />
<img src={starImg} alt="크레딧" css={S.icon} />
<p>{`${addCommas(data.receivedDonations)} / ${addCommas(data.targetDonation)}`}</p>
</div>
<p>{daysLeft > 1 ? `D-${daysLeft}` : daysLeft === 1 ? '오늘 마감' : '마감 완료'}</p>
Expand Down
7 changes: 6 additions & 1 deletion src/components/card/card.styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,16 @@ export const info = css`
export const statusBar = css`
display: flex;
justify-content: space-between;
align-items: center;

p {
font-size: 1.2rem;
font-size: 0.9rem;
font-weight: 400;
color: var(--white);

${media({
fontSize: ['0.9rem', '0.9rem', '1rem', '1.1rem', '1.2rem'],
})}
}

& > div > p {
Expand Down
71 changes: 33 additions & 38 deletions src/components/carousel/Carousel.jsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,48 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useState, useEffect } from 'react';
import { ArrowButton } from '@/components/button';
import { Card } from '@/components/card';
import { getItemMetrics } from '@/utils/carousel';
import { CAROUSEL } from '@/constants/carousel';
import * as S from './carousel.styles';

const Carousel = ({ data, setModalType, setSelectedIndex }) => {
const [currentIndex, setCurrentIndex] = useState(0);
const [itemsPerView, setItemsPerView] = useState(4);
const containerRef = useRef(null);

// data.list의 길이를 사용
const [itemsView, setItemsView] = useState(CAROUSEL.ITEMS_VIEW);
const itemsLength = data?.list?.length || 0;

const updateItemsPerView = useCallback(() => {
if (!containerRef.current) return;

const viewportWidth = containerRef.current.offsetWidth;
const { itemWidth, gap } = getItemMetrics();
const totalItemWidth = itemWidth + gap;
const calculatedItemsPerView = Math.floor(viewportWidth / totalItemWidth);

setItemsPerView(calculatedItemsPerView);
const newMaxIndex = Math.max(0, itemsLength - calculatedItemsPerView);
setCurrentIndex((prev) => Math.min(prev, newMaxIndex));
}, [itemsLength]);

useEffect(() => {
updateItemsPerView();
window.addEventListener('resize', updateItemsPerView);
return () => window.removeEventListener('resize', updateItemsPerView);
}, [updateItemsPerView]);
const updateItemsView = () => {
if (window.innerWidth < 1000) {
setItemsView(CAROUSEL.ITEMS_VIEW);
} else if (window.innerWidth < 1200) {
setItemsView(2);
} else if (window.innerWidth < 1920) {
setItemsView(3);
} else {
setItemsView(4);
}
};

updateItemsView();
window.addEventListener('resize', updateItemsView);

const maxIndex = Math.max(0, itemsLength - itemsPerView);
return () => {
window.removeEventListener('resize', updateItemsView);
};
}, []);

const handleNext = () => {
setCurrentIndex((prev) => Math.min(prev + 1, maxIndex));
setCurrentIndex((prev) => {
const nextIndex = prev + itemsView;
return nextIndex >= itemsLength ? prev : nextIndex;
});
};

const handlePrev = () => {
setCurrentIndex((prev) => Math.max(prev - 1, 0));
setCurrentIndex((prev) => Math.max(prev - itemsView, 0));
};

const getSlideOffset = () => {
const { itemWidth, gap } = getItemMetrics();
return currentIndex * (itemWidth + gap);
return currentIndex * (CAROUSEL.DESKTOP_ITEM_WIDTH + CAROUSEL.ITEM_GAP);
};

return (
Expand All @@ -54,15 +53,10 @@ const Carousel = ({ data, setModalType, setSelectedIndex }) => {
direction="left"
onButtonClick={handlePrev}
disabled={currentIndex === 0}
styles={S.navigationButton}
styles={S.navigationButton(false)}
/>
<div ref={containerRef} css={S.carouselContainer}>
<div
css={S.carouselTrack}
style={{
'--slide-offset': `-${getSlideOffset()}px`,
}}
>
<div css={S.carouselContainer}>
<div css={S.carouselTrack(getSlideOffset())}>
{data?.list?.length > 0 ? (
data.list.map((item, index) => (
<div key={item.id} css={S.carouselItem}>
Expand All @@ -75,14 +69,15 @@ const Carousel = ({ data, setModalType, setSelectedIndex }) => {
</div>
))
) : (
<div>표시할 항목이 없습니다</div>
<div css={S.notthingTitle}>현재 등록된 후원이 없습니다.</div>
)}
</div>
</div>
<ArrowButton
direction="right"
onButtonClick={handleNext}
disabled={currentIndex >= maxIndex}
disabled={currentIndex + itemsView >= itemsLength}
styles={S.navigationButton(true)}
/>
</div>
</div>
Expand Down
59 changes: 35 additions & 24 deletions src/components/carousel/carousel.styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,71 +4,82 @@ import media from '@/styles/responsive';
export const wrapper = css`
width: 100%;
margin-block: 4rem;

${media({
marginBlock: ['4rem', '4rem', '4rem 6rem', '4rem 6rem', '4rem 6rem'],
})}
`;

export const carouselTitle = css`
margin-inline: 2rem;
margin-bottom: 1.6rem;
font-size: 2.4rem;
font-weight: 700;
color: var(--white);

${media({
fontSize: ['2rem', '2rem', '2.4rem', '2.4rem'],
marginInline: ['2rem', '2rem', '8rem', '8rem'],
marginBottom: ['1.6rem', '1.6rem', '2.4rem', '3.2rem'],
})}
`;

export const viewportArea = css`
display: flex;
align-items: center;
gap: 0;
padding-inline: 2rem;

${media({
gap: ['0', '0', '2rem', '2rem'],
})}
position: relative;
`;

export const carouselContainer = css`
overflow-x: scroll;
width: 100%;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
width: 100%;


&::-webkit-scrollbar {
display: none;
}
`;

export const carouselTrack = css`
export const carouselTrack = (slide) => css`
display: flex;
gap: 1.2rem;
transition: transform 0.3s ease-in-out;
transition: transform 0.4s ease-in-out;

${media({
transform: [
'none',
'none',
'translateX(var(--slide-offset))',
'translateX(var(--slide-offset))',
transition: [
'transform 0.4s ease-in-out',
'transform 0.6s ease-in-out',
'transform 0.7s ease-in-out',
'transform 0.9s ease-in-out',
],
transform: ['none', 'none', `translateX(-${slide}px)`, `translateX(-${slide}px)`],
})}
`;

export const carouselItem = css`
flex-basis: 15.8rem;
min-width: 15.8rem;
scroll-snap-align: start;

${media({
flexBasis: ['15.8rem', '15.8rem', '28.2rem', '28.2rem'],
minWidth: ['15.8rem', '15.8rem', '28.2rem', '28.2rem'],
})}
`;

export const navigationButton = css`
${media({
display: ['none', 'none', 'flex', 'flex'],
})}
export const navigationButton = (isRight) => css`
position: absolute;
top: 50%;
${isRight ? 'right: 0.5%' : 'left: 0.5%'};
z-index: 1;
width: 3rem;
opacity: 0.5;
transition: opacity 0.3s ease-in-out; /* 버튼에 부드러운 비활성화 효과 추가 */
transform: translateY(-125%);
`;

export const notthingTitle = css`
display: flex;
justify-content: center;
align-items: center;
font-size: 2rem;
font-weight: 700;
text-align: center;
margin-block: 7rem;
`;
2 changes: 1 addition & 1 deletion src/components/modals/creditChargeModal/index.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { default } from './CreditChargeModal';
export { default } from './CreditChargeModal';
27 changes: 7 additions & 20 deletions src/components/modals/donationModal/DonationModal.jsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,21 @@
import { useRef, useState } from 'react';
import { useRevalidator } from 'react-router-dom';
import { CustomButton } from '@/components/button';
import { Alert } from '@/components/alert';
import { showAlert } from '@/utils/alert/alertController';
import { ENDPOINTS } from '@/constants/api';
import { requestPut } from '@/utils/api';
import creditImg from '@/assets/images/credit.png';
import starImg from '@/assets/images/logo.png';
import * as S from './donationModal.styles';

const DonationModal = ({ data, credit, updateCredit, onClose }) => {
const [donateAmount, setDonateAmount] = useState('');
const [hasNoMoney, setHasNoMoney] = useState(false);
const [isDonating, setIsDonating] = useState(false);
const [isInvalidNumber, setIsInvalidNumber] = useState(false);
const [showAlert, setShowAlert] = useState(false);
const [alertContent, setAlertContent] = useState('');
const [alertType, setAlertType] = useState('warning');
const inputRef = useRef(null);
const prevCredit = credit;
const revalidator = useRevalidator();

const triggerAlert = (message, type = 'warning') => {
setAlertContent(message);
setAlertType(type);
setShowAlert(true);
setTimeout(() => {
setShowAlert(false);
}, 2000);
};

const handleChangeAmount = (e) => {
const amount = e.target.value;

Expand All @@ -51,7 +39,7 @@ const DonationModal = ({ data, credit, updateCredit, onClose }) => {
};

const handleKeyDown = (e) => {
if (e.key === '.') {
if (e.key === '.' || e.key === ' ') {
e.preventDefault();
}
};
Expand All @@ -67,15 +55,14 @@ const DonationModal = ({ data, credit, updateCredit, onClose }) => {
const total = prevCredit - donateAmountNum;
localStorage.setItem('selectedCredit', total);
updateCredit(total);

triggerAlert('후원에 성공했습니다', 'success');
showAlert('투표에 성공했습니다', 'success');
setTimeout(() => {
onClose();
revalidator.revalidate();
}, 700);
} catch (e) {
showAlert('투표에 실패했습니다.', 'warning');
console.error('후원 처리 중 오류 발생', e);
triggerAlert('후원에 실패했습니다', 'warning');
} finally {
setIsDonating(false);
}
Expand All @@ -94,8 +81,9 @@ const DonationModal = ({ data, credit, updateCredit, onClose }) => {
</div>
</div>
<div css={S.inputContent(hasNoMoney, isInvalidNumber)}>
<img src={creditImg} alt="크레딧" />
<img src={starImg} alt="크레딧" />
<input
type="text"
placeholder="크레딧 입력"
value={donateAmount}
onChange={handleChangeAmount}
Expand All @@ -117,7 +105,6 @@ const DonationModal = ({ data, credit, updateCredit, onClose }) => {
>
후원하기
</CustomButton>
{showAlert && <Alert content={alertContent} type={alertType} isSmall />}
</div>
);
};
Expand Down
Loading