diff --git a/src/components/card/Card.jsx b/src/components/card/Card.jsx index ce281805..dce51c29 100644 --- a/src/components/card/Card.jsx +++ b/src/components/card/Card.jsx @@ -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'; @@ -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: useEffect(() => { const interval = setInterval(() => { const newDaysLeft = getDaysRemaining(data.deadline); - setDaysLeft(newDaysLeft); // 갱신된 남은 일수로 상태 업데이트 - }, 55000); + setDaysLeft(newDaysLeft); + }, 60000); return () => clearInterval(interval); - }, [data.deadline]); + }, []); const handleClick = () => { setSelectedIndex(index); @@ -51,7 +51,7 @@ const Card = ({ data, setModalType, setSelectedIndex, index }) => {
- 크레딧 + 크레딧

{`${addCommas(data.receivedDonations)} / ${addCommas(data.targetDonation)}`}

{daysLeft > 1 ? `D-${daysLeft}` : daysLeft === 1 ? '오늘 마감' : '마감 완료'}

diff --git a/src/components/card/card.styles.js b/src/components/card/card.styles.js index 89f1f3f2..3948299e 100644 --- a/src/components/card/card.styles.js +++ b/src/components/card/card.styles.js @@ -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 { diff --git a/src/components/carousel/Carousel.jsx b/src/components/carousel/Carousel.jsx index c61545b3..efeff225 100644 --- a/src/components/carousel/Carousel.jsx +++ b/src/components/carousel/Carousel.jsx @@ -1,49 +1,46 @@ -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(3); + } else if (window.innerWidth < 1920) { + 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 ( @@ -54,15 +51,10 @@ const Carousel = ({ data, setModalType, setSelectedIndex }) => { direction="left" onButtonClick={handlePrev} disabled={currentIndex === 0} - styles={S.navigationButton} + styles={S.navigationButton(false)} /> -
-
+
+
{data?.list?.length > 0 ? ( data.list.map((item, index) => (
@@ -75,14 +67,15 @@ const Carousel = ({ data, setModalType, setSelectedIndex }) => {
)) ) : ( -
표시할 항목이 없습니다
+
현재 등록된 후원이 없습니다.
)}
= maxIndex} + disabled={currentIndex + itemsView >= itemsLength} + styles={S.navigationButton(true)} />
diff --git a/src/components/carousel/carousel.styles.js b/src/components/carousel/carousel.styles.js index a90a9865..b17cc1a9 100644 --- a/src/components/carousel/carousel.styles.js +++ b/src/components/carousel/carousel.styles.js @@ -4,62 +4,58 @@ 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'], @@ -67,8 +63,23 @@ export const carouselItem = css` })} `; -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; `; diff --git a/src/components/modals/creditChargeModal/index.js b/src/components/modals/creditChargeModal/index.js index b04c0315..8bfcaed5 100644 --- a/src/components/modals/creditChargeModal/index.js +++ b/src/components/modals/creditChargeModal/index.js @@ -1 +1 @@ -export { default } from './CreditChargeModal'; \ No newline at end of file +export { default } from './CreditChargeModal'; diff --git a/src/components/modals/donationModal/DonationModal.jsx b/src/components/modals/donationModal/DonationModal.jsx index 7fe40df4..4aae7027 100644 --- a/src/components/modals/donationModal/DonationModal.jsx +++ b/src/components/modals/donationModal/DonationModal.jsx @@ -1,10 +1,10 @@ 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'; 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 }) => { @@ -12,22 +12,10 @@ const DonationModal = ({ data, credit, updateCredit, onClose }) => { 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; @@ -51,7 +39,7 @@ const DonationModal = ({ data, credit, updateCredit, onClose }) => { }; const handleKeyDown = (e) => { - if (e.key === '.') { + if (e.key === '.' || e.key === ' ') { e.preventDefault(); } }; @@ -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); } @@ -94,8 +81,9 @@ const DonationModal = ({ data, credit, updateCredit, onClose }) => {
- 크레딧 + 크레딧 { > 후원하기 - {showAlert && }
); }; diff --git a/src/components/modals/donationModal/donationModal.styles.js b/src/components/modals/donationModal/donationModal.styles.js index 13373dab..178e62d1 100644 --- a/src/components/modals/donationModal/donationModal.styles.js +++ b/src/components/modals/donationModal/donationModal.styles.js @@ -8,46 +8,6 @@ const shakeTitle = keyframes` 100% { transform: translateX(0); } `; -/* -이스터 에그용 체험 하고 싶으시면 -animation: ${crazyAnimation} 1.5s ease-in-out infinite; 이걸로 요소 하나만 보시면 알아요 ^^7 -const crazyAnimation = keyframes` - 0% { - transform: rotate(0deg) translateX(0) scale(1); - } - 10% { - transform: rotate(45deg) translateX(-10px) scale(1.1); - } - 20% { - transform: rotate(-45deg) translateX(10px) scale(0.9); - } - 30% { - transform: rotate(90deg) translateX(-15px) scale(1.2); - } - 40% { - transform: rotate(-90deg) translateX(15px) scale(0.8); - } - 50% { - transform: rotate(180deg) translateX(-20px) scale(1); - } - 60% { - transform: rotate(-180deg) translateX(20px) scale(1.1); - } - 70% { - transform: rotate(360deg) translateX(-25px) scale(0.9); - } - 80% { - transform: rotate(-360deg) translateX(25px) scale(1.2); - } - 90% { - transform: rotate(540deg) translateX(-30px) scale(0.8); - } - 100% { - transform: rotate(0deg) translateX(0) scale(1); - } -`; -*/ - export const modalContent = css` display: flex; flex-direction: column; @@ -115,7 +75,7 @@ export const inputContent = (hasNomoney, isInvalidNumber) => css` input { width: 100%; height: 100%; - padding: 1.6rem 4rem 1.6rem 1.6rem; + padding: 1.6rem 1.6rem 1.6rem 6rem; border: 1px solid ${hasNomoney || isInvalidNumber ? 'var(--error-red)' : 'var(--white-full)'}; border-radius: 8px; font-size: 2rem; @@ -140,10 +100,10 @@ export const inputContent = (hasNomoney, isInvalidNumber) => css` img { position: absolute; top: 50%; - right: 5%; + left: 5%; z-index: 1; - width: 3.6rem; - height: 3.6rem; + width: 3rem; + height: 3rem; transform: translateY(-50%); } diff --git a/src/constants/carousel/carousel.js b/src/constants/carousel/carousel.js new file mode 100644 index 00000000..835ec465 --- /dev/null +++ b/src/constants/carousel/carousel.js @@ -0,0 +1,7 @@ +const CAROUSEL = { + DESKTOP_ITEM_WIDTH: 282, + ITEM_GAP: 12, + ITEMS_VIEW: 2, +}; + +export default CAROUSEL; diff --git a/src/constants/carousel/carouselConstants.js b/src/constants/carousel/carouselConstants.js deleted file mode 100644 index c7548f59..00000000 --- a/src/constants/carousel/carouselConstants.js +++ /dev/null @@ -1,10 +0,0 @@ -const CAROUSEL_CONSTANTS = { - MOBILE_BREAKPOINT: 768, - ITEM_DIMENSIONS: { - MOBILE_ITEM_WIDTH: 158, - DESKTOP_ITEM_WIDTH: 282, - ITEM_GAP: 12, - }, -}; - -export default CAROUSEL_CONSTANTS; diff --git a/src/constants/carousel/index.js b/src/constants/carousel/index.js index 458b00f2..41f65ab4 100644 --- a/src/constants/carousel/index.js +++ b/src/constants/carousel/index.js @@ -1 +1 @@ -export { default as CAROUSEL_CONSTANTS } from './carouselConstants'; +export { default as CAROUSEL } from './carousel';