From 9a9d7043f156548deebfd04dcd9599226ac9ca45 Mon Sep 17 00:00:00 2001 From: HyeonSik Choi Date: Tue, 27 Aug 2024 16:11:56 +0900 Subject: [PATCH] =?UTF-8?q?=08front:=20=EC=9E=98=EB=AA=BB=EB=90=9C=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=ED=91=9C=ED=98=84=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EA=B2=BD=ED=97=98=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20(#320)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 경매 가격 경신 로직 수정 * fix: 경매 가격 하락 로직 수정 * feat: 재고 수량을 폴링하는 로직 추가 (시연용) * feat: 정말로 로그아웃할까요? 문구 추가 * feat: 경매 입찰 성공을 시각적으로 표시. 및 따닥 방지 * style: list 미리보기 이미지 수정 * style: 목록에서는 현재가격을 추정할 수 없으므로 '시작 가격'으로 문구 변경 * feat: 타이머를 통해 다음 가격 변동 시점을 노출 * feat: 회원가입 시 아이디, 패스워드 규격을 사용자에게 안내한다. * feat: API 요청 실패 시, 일부 케이스에서 서버가 전달한 메세지를 표시 * feat: 활성항목을 리스트에 표시 * refactor: 진행 중인 경매에 색상을 표시한다. * feat: 회원가입 성공시 성공 멘트 표시 * refactor: 거래내역에서 생성일과 수정일을 표시하지 않습니다. (서버에서 생성하지 않음) * refactor: 판매자 가입 기능 제거 * fix: 경매 상세페이지에서 가격을 갱신하는 타이머 오류 수정 * fix: 종료된 경매에서 입찰버튼이 활성화되던 문제 수정 --------- Co-authored-by: MinSeok Oh --- front/package-lock.json | 22 +++ front/package.json | 1 + front/src/api/auction/api.ts | 14 +- front/src/api/payments/api.ts | 7 +- front/src/api/receipt/api.ts | 7 +- front/src/api/user/api.ts | 28 +-- front/src/pages/ChargePoint.tsx | 4 +- front/src/pages/Footer.tsx | 9 +- front/src/pages/Login.tsx | 4 +- front/src/pages/SignUp.tsx | 89 +++++++-- .../pages/auction/detail/AuctionDetail.tsx | 179 ++++++++++++++++-- .../auction/detail/PricePolicyElement.tsx | 106 +++++++---- .../pages/auction/list/AuctionListElement.tsx | 18 +- .../pages/receipt/detail/ReceiptDetail.tsx | 29 +-- front/src/util/DateUtil.ts | 24 ++- 15 files changed, 415 insertions(+), 126 deletions(-) diff --git a/front/package-lock.json b/front/package-lock.json index a8f055ef..71910bc9 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -16,6 +16,7 @@ "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", "react": "^18.3.1", + "react-confetti": "^6.1.0", "react-dom": "^18.3.1", "react-scripts": "5.0.1", "typescript": "^4.9.5", @@ -14053,6 +14054,21 @@ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "license": "MIT" }, + "node_modules/react-confetti": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.1.0.tgz", + "integrity": "sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==", + "license": "MIT", + "dependencies": { + "tween-functions": "^1.2.0" + }, + "engines": { + "node": ">=10.18" + }, + "peerDependencies": { + "react": "^16.3.0 || ^17.0.1 || ^18.0.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -16494,6 +16510,12 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD" }, + "node_modules/tween-functions": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz", + "integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==", + "license": "BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/front/package.json b/front/package.json index cc6f7625..73dc6310 100644 --- a/front/package.json +++ b/front/package.json @@ -11,6 +11,7 @@ "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", "react": "^18.3.1", + "react-confetti": "^6.1.0", "react-dom": "^18.3.1", "react-scripts": "5.0.1", "typescript": "^4.9.5", diff --git a/front/src/api/auction/api.ts b/front/src/api/auction/api.ts index 303119a1..128e8737 100644 --- a/front/src/api/auction/api.ts +++ b/front/src/api/auction/api.ts @@ -30,7 +30,7 @@ async function requestAuctionDetail( baseUrl: string, auctionId: number, onSuccess: (auctionDetail: AuctionDetailItem) => void, - onFailure: () => void + onFailure: (message: string) => void ) { try { const response = await fetch(`${baseUrl}/auctions/${auctionId}`, { @@ -45,11 +45,12 @@ async function requestAuctionDetail( const auctionDetail: AuctionDetailItem = await response.json(); onSuccess(auctionDetail); } else { - onFailure(); + const errorMessage = await response.text(); + onFailure(errorMessage); } } catch (error) { console.error('Failed to fetch auction detail.', error); - onFailure(); + onFailure("REQUEST AUCTION DETAIL."); } } @@ -58,7 +59,7 @@ async function requestAuctionBid( auctionId: number, request: AuctionPurchaseRequest, onSuccess: () => void, - onFailure: () => void + onFailure: (message: string) => void ) { try { const response = await fetch(`${baseUrl}/auctions/${auctionId}/purchase`, { @@ -78,12 +79,13 @@ async function requestAuctionBid( if (response.ok) { onSuccess(); } else { - onFailure(); + const errorMessage = await response.text(); + onFailure(errorMessage); } } catch (error) { console.error('Failed to bid auction.', error); - onFailure(); + onFailure("BID REQUEST FAILED"); } } diff --git a/front/src/api/payments/api.ts b/front/src/api/payments/api.ts index 3097272d..d4a0f1ce 100644 --- a/front/src/api/payments/api.ts +++ b/front/src/api/payments/api.ts @@ -4,7 +4,7 @@ async function chargePointsApi( baseUrl: string, data: ChargePointsRequest, onSuccess: () => void, - onFailure: () => void + onFailure: (message: string) => void ) { try { const response = await fetch(`${baseUrl}/payments/points/charge`, { @@ -21,10 +21,11 @@ async function chargePointsApi( if (response.ok) { onSuccess(); } else { - onFailure(); + const errorMessage = await response.text(); + onFailure(errorMessage); } } catch (error) { - onFailure(); + onFailure("CHARGE POINT ERROR."); } } diff --git a/front/src/api/receipt/api.ts b/front/src/api/receipt/api.ts index 627ed5eb..618a6281 100644 --- a/front/src/api/receipt/api.ts +++ b/front/src/api/receipt/api.ts @@ -61,7 +61,7 @@ async function requestRefund( baseUrl: string, receiptId: number, onSuccess: () => void, - onFailure: () => void + onFailure: (message: string) => void ) { try { const response = await fetch(`${baseUrl}/receipts/${receiptId}/refund`, { @@ -76,10 +76,11 @@ async function requestRefund( if (response.ok) { onSuccess(); } else { - onFailure(); + const errorMessage = await response.text(); + onFailure(errorMessage); } } catch (error) { - onFailure(); + onFailure("REQUEST REFUND ERROR"); } } diff --git a/front/src/api/user/api.ts b/front/src/api/user/api.ts index 86b5fe59..723fd00b 100644 --- a/front/src/api/user/api.ts +++ b/front/src/api/user/api.ts @@ -1,10 +1,10 @@ -import {SignInRequest, SignUpRequest} from "./type"; +import { SignInRequest, SignUpRequest } from "./type"; async function signUpApi( baseUrl: string, data: SignUpRequest, onSuccess: () => void, - onFailure: () => void + onFailure: (message: string) => void ) { try { const response = await fetch(`${baseUrl}/auth/signup`, { @@ -18,10 +18,12 @@ async function signUpApi( if (response.ok) { onSuccess(); } else { - onFailure(); + const errorMessage = await response.text(); + onFailure(`Sign up failed: ${errorMessage}`); } } catch (error) { - onFailure(); + console.error(error); + onFailure("An unexpected error occurred during sign-up."); } } @@ -29,7 +31,7 @@ async function signInApi( baseUrl: string, data: SignInRequest, onSuccess: () => void, - onFailure: () => void + onFailure: (message: string) => void ) { try { const response = await fetch(`${baseUrl}/auth/signin`, { @@ -41,21 +43,23 @@ async function signInApi( }, body: JSON.stringify(data), }); + if (response.ok) { onSuccess(); } else { - onFailure(); + const errorMessage = await response.text(); + onFailure(`Sign in failed: ${errorMessage}`); } } catch (error) { console.error(error); - onFailure(); + onFailure("An unexpected error occurred during sign-in."); } } async function signOut( baseUrl: string, onSuccess: () => void, - onFailure: () => void + onFailure: (message: string) => void ) { try { const response = await fetch(`${baseUrl}/auth/signout`, { @@ -70,11 +74,13 @@ async function signOut( if (response.ok) { onSuccess(); } else { - onFailure(); + const errorMessage = await response.text(); + onFailure(`Sign out failed: ${errorMessage}`); } } catch (error) { - onFailure(); + console.error(error); + onFailure("An unexpected error occurred during sign-out."); } } -export {signUpApi, signInApi, signOut}; +export { signUpApi, signInApi, signOut }; diff --git a/front/src/pages/ChargePoint.tsx b/front/src/pages/ChargePoint.tsx index 816b930f..9da793ea 100644 --- a/front/src/pages/ChargePoint.tsx +++ b/front/src/pages/ChargePoint.tsx @@ -20,8 +20,8 @@ function ChargePointPage() { showAlert("포인트 충전 성공!") setRequest({amount: 0}); }, - () => { - showAlert("포인트 충전 실패!") + (message) => { + showAlert("포인트 충전 실패! " + message) } ); } diff --git a/front/src/pages/Footer.tsx b/front/src/pages/Footer.tsx index 50cf6d23..c9979a49 100644 --- a/front/src/pages/Footer.tsx +++ b/front/src/pages/Footer.tsx @@ -7,8 +7,11 @@ import creditCardIcon from '../img/credit-card.svg'; import listIcon from '../img/list.svg'; import logInIcon from '../img/log-in.svg'; import logOutIcon from '../img/log-out.svg'; +import useAlert from "../hooks/useAlert"; function Footer() { + const {showAlert} = useAlert(); + const { currentPage, setPage } = usePageStore(); const { isLogin, setIsLogin } = useLoginStore(); const baseUrl = process.env.REACT_APP_API_URL || ''; @@ -18,14 +21,18 @@ function Footer() { }; const logout = () => { + if(!window.confirm("정말로 로그아웃할까요?")) { + return; + } signOut( baseUrl, () => { setIsLogin(false); setPage('home'); }, - () => { + (message) => { console.log('Failed to sign out.'); + showAlert(message); } ); }; diff --git a/front/src/pages/Login.tsx b/front/src/pages/Login.tsx index c3160516..dcce8424 100644 --- a/front/src/pages/Login.tsx +++ b/front/src/pages/Login.tsx @@ -29,9 +29,9 @@ function LoginPage() { setIsLogin(true); showAlert('로그인 성공하였습니다.'); }, - () => { + (response) => { setIsLogin(false); - showAlert('로그인에 실패하였습니다.'); + showAlert(response); setRequest({signInId: '', password: ''}); } ); diff --git a/front/src/pages/SignUp.tsx b/front/src/pages/SignUp.tsx index 2f74238e..3975ea87 100644 --- a/front/src/pages/SignUp.tsx +++ b/front/src/pages/SignUp.tsx @@ -15,17 +15,28 @@ function SignUpPage() { password: '', userRole: 'BUYER', }); + const [idError, setIdError] = useState(" "); + const [passwordError, setPasswordError] = useState(" "); + const [isFormValid, setIsFormValid] = useState(false); const handleUserTypeChange = (e: React.ChangeEvent) => { setRequest({...request, userRole: e.target.value,}); }; const handleNameChange = (e: React.ChangeEvent) => { - setRequest({...request, signUpId: e.target.value,}); + const newSignUpId = e.target.value; + setRequest({...request, signUpId: newSignUpId}); + const idError = validateSignUpId(newSignUpId); + setIdError(idError); + validateForm(idError, passwordError); }; const handlePasswordChange = (e: React.ChangeEvent) => { - setRequest({...request, password: e.target.value,}); + const newPassword = e.target.value; + setRequest({...request, password: newPassword}); + const passwordError = validatePassword(newPassword); + setPasswordError(passwordError); + validateForm(idError, passwordError); }; const requestSignUp = () => { @@ -33,14 +44,46 @@ function SignUpPage() { baseUrl, request, () => { + showAlert("회원가입에 성공했습니다!"); setPage('login'); }, - () => { - showAlert('회원가입에 실패했습니다.'); + (message) => { + showAlert(message); } ); } + const validateForm = (idError: string, passwordError: string) => { + if (idError === "" && passwordError === "") { + setIsFormValid(true); + } else { + setIsFormValid(false); + } + }; + + const validateSignUpId = (signUpId: string): string => { + if (signUpId.length < 2 || signUpId.length > 20) { + return "아이디는 2자 이상, 20자 이하로 입력해야 합니다."; + } + return ""; + }; + + const validatePassword = (password: string): string => { + if (password.length < 8 || password.length > 20) { + return "비밀번호는 8자 이상, 20자 이하로 입력해야 합니다."; + } + if (!/[0-9]/.test(password)) { + return "비밀번호에는 최소한 하나의 숫자가 포함되어야 합니다."; + } + if (!/[a-z]/.test(password)) { + return "비밀번호에는 최소한 하나의 소문자가 포함되어야 합니다."; + } + if (!/^[a-zA-Z0-9]*$/.test(password)) { + return "비밀번호는 영문 대소문자와 숫자만 사용할 수 있습니다."; + } + return ""; + }; + return (
@@ -61,33 +104,36 @@ function SignUpPage() { /> 구매자 - + + {/**/} +
- + + {idError &&

{idError}

}
@@ -102,16 +148,21 @@ function SignUpPage() { onChange={handlePasswordChange} required /> + {passwordError &&

{passwordError}

}
+
+ diff --git a/front/src/pages/auction/detail/AuctionDetail.tsx b/front/src/pages/auction/detail/AuctionDetail.tsx index ed811224..9c0fe3fc 100644 --- a/front/src/pages/auction/detail/AuctionDetail.tsx +++ b/front/src/pages/auction/detail/AuctionDetail.tsx @@ -1,12 +1,18 @@ -import {formatVariationDuration, getKrDateFormat} from "../../../util/DateUtil"; +import { + formatVariationDuration, + getAuctionStatus, + getKrDateFormat, + getMsFromIso8601Duration, getTimeDifferenceInMs +} from "../../../util/DateUtil"; import {useEffect, useState} from "react"; -import {AuctionDetailInfo} from "./type"; +import {AuctionDetailInfo, ConstantPricePolicy, PercentagePricePolicy} from "./type"; import PricePolicyElement from "./PricePolicyElement"; import {requestAuctionBid, requestAuctionDetail} from "../../../api/auction/api"; import {usePageStore} from "../../../store/PageStore"; import useAlert from "../../../hooks/useAlert"; import {getAuctionProgress} from "../../../util/NumberUtil" import arrowLeftIcon from '../../../img/arrow-left.svg'; +import Confetti from 'react-confetti'; function AuctionDetail({auctionId}: { auctionId?: number }) { @@ -16,6 +22,12 @@ function AuctionDetail({auctionId}: { auctionId?: number }) { const {showAlert} = useAlert(); const [auction, setAuction] = useState(null); const [quantity, setQuantity] = useState(1); + const [leftInfo, setLeftInfo] = useState("불러오는 중..."); + + const [isNotRunning, setIsNotRunning] = useState(true); + const [isButtonDisabled, setIsButtonDisabled] = useState(false); + const [countdown, setCountdown] = useState(0); + const [showConfetti, setShowConfetti] = useState(false); const increaseQuantity = (maximum: number) => { if (quantity >= maximum) { @@ -63,22 +75,142 @@ function AuctionDetail({auctionId}: { auctionId?: number }) { showAlert("상품 정보를 가져오는데 실패했습니다."); } ); + }, []); + useEffect(() => { + if (auctionId === undefined) { + return; + } + + // 재고 갱신 + const intervalId = setInterval(() => { + requestAuctionDetail(baseUrl, auctionId, + (auctionDetailItem) => { + setAuction(prevAuction => + prevAuction ? {...prevAuction, currentStock: auctionDetailItem.currentStock} : null + ); + console.log("현재 재고: " + auctionDetailItem.currentStock); + }, + () => {console.log("현재 재고량을 가져오는데 실패하였습니다.")} + ); + }, 1000); + + return () => clearInterval(intervalId); + }, []); + + useEffect(() => { + if (!auction) { + return; + } + + // 남은 시간 갱신 타이머 + // setIsNotRunning(true); + const {status, timeInfo} = getAuctionStatus(auction.startedAt, auction.finishedAt); + if (status === "종료") { + let krDateFormat = "경매 종료 (" + getKrDateFormat(auction.finishedAt) + ")"; + setLeftInfo(krDateFormat); + } + else if (status === "진행 예정") { + setLeftInfo(status + " (" + timeInfo + ")"); + } + else if (status === "곧 시작") { + setLeftInfo(status + " (" + timeInfo + ")"); + } else { + setLeftInfo(status + " (" + timeInfo + ")"); + setIsNotRunning(false); // 입찰 버튼 활성화 + } + }, [auction]); + + useEffect(() => { + if (isButtonDisabled) { + const timer = setInterval(() => { + setCountdown((prevCount) => { + if (prevCount <= 1) { + clearInterval(timer); + setIsButtonDisabled(false); + return 0; + } + return prevCount - 1; + }); + }, 1000); + + return () => clearInterval(timer); + } + }, [isButtonDisabled]); + + useEffect(() => { + if (showConfetti) { + const timer = setTimeout(() => setShowConfetti(false), 5000); // 5초 후에 confetti 효과 종료 + return () => clearTimeout(timer); + } + }, [showConfetti]); + + function getCurrentPrice(): number { + if (!auction) { + return 1; + } + + // 가격 하락 정책이 종료되었는지 체크 + const now = new Date(); + let isLastTime = false; + if (now >= auction.finishedAt) { + isLastTime = true; + } + + // 현재 가격 계산 로직 + const durationMs = getMsFromIso8601Duration(auction.variationDuration); + const diffMsBetweenStartedAndNow = getTimeDifferenceInMs( + auction.startedAt, isLastTime ? auction.finishedAt : now + ); + const times = Math.floor(diffMsBetweenStartedAndNow / durationMs); + + let currentPrice = auction.originPrice; + for (let i = 0; i < times; i++) { // times번 만큼 할인된 가격을 구하는 로직 + const calculatePrice = calculateNextPrice(currentPrice); + currentPrice = calculatePrice; + } + + return currentPrice; + } + + function calculateNextPrice(currentPrice: number): number { + if (!auction) { + return 1; + } + + if (auction.pricePolicy.type === "CONSTANT") { + return currentPrice - (auction.pricePolicy as ConstantPricePolicy).variationWidth; + } else if (auction.pricePolicy.type === "PERCENTAGE") { + return currentPrice - (currentPrice * (auction.pricePolicy as PercentagePricePolicy).discountRate / 100); + } else { + return -1; + } + } + const onClickBidButton = () => { + const currentPrice = getCurrentPrice(); + requestAuctionBid( baseUrl, auction?.auctionId!, - {quantity: quantity, price: auction!.currentPrice}, + {quantity: quantity, price: currentPrice}, () => { - setPage('home'); + setShowConfetti(true); // 성공 시 confetti 효과 시작 + setIsButtonDisabled(true); // 버튼 비활성화 + setCountdown(5); // 5초 카운트다운 시작 }, - () => { - showAlert("입찰에 실패했습니다."); + (message) => { + showAlert(message); } ); } + const getButtonText = () => { + if (isButtonDisabled) return `${countdown}초 남음`; + return '입찰하기'; + } + const onClickBackButton = () => { setPage('home'); } @@ -95,6 +227,14 @@ function AuctionDetail({auctionId}: { auctionId?: number }) { return ( <> + {showConfetti && ( + + )}
-
-

변동 주기

-

{formatVariationDuration(auction.variationDuration)}

-
+ {/*
*/} + {/*

변동 주기

*/} + {/*

{formatVariationDuration(auction.variationDuration)}

*/} + {/*
*/}
@@ -174,6 +314,13 @@ function AuctionDetail({auctionId}: { auctionId?: number }) { max={auction.maximumPurchaseLimitCount} value={quantity} className="input input-bordered text-center mx-2" + disabled + style={{ + backgroundColor: 'white', + color: 'black', + opacity: 1, + cursor: 'default' + }} />
- - + diff --git a/front/src/pages/auction/detail/PricePolicyElement.tsx b/front/src/pages/auction/detail/PricePolicyElement.tsx index 6704c2cf..ab1e33f6 100644 --- a/front/src/pages/auction/detail/PricePolicyElement.tsx +++ b/front/src/pages/auction/detail/PricePolicyElement.tsx @@ -1,7 +1,12 @@ import {AuctionDetailInfo, ConstantPricePolicy, PercentagePricePolicy} from "./type"; import {getPriceFormatted} from "../../../util/NumberUtil"; -import {formatVariationDuration, getMsFromIso8601Duration, getTimeDifferenceInMs} from "../../../util/DateUtil"; -import {useEffect} from "react"; +import { getAuctionStatus } from "../../../util/DateUtil"; +import { + formatVariationDuration, + getMsFromIso8601Duration, + getTimeDifferenceInMs +} from "../../../util/DateUtil"; +import {useEffect, useState} from "react"; interface PricePolicyElementProps { priceLimit: number; @@ -16,44 +21,69 @@ function PricePolicyElement( setAuction }: PricePolicyElementProps) { - useEffect(() => { - - const durationMs = getMsFromIso8601Duration(auction.variationDuration); - const diffMsBetweenStartedAndNow = getTimeDifferenceInMs(auction.startedAt, new Date()); - const diffMs = getTimeDifferenceInMs(auction.startedAt, auction.finishedAt); + const [nowPrice, setNowPrice] = useState(0); + const [isLastPrice, setIsLastPrice] = useState(false); + const [timeUntilNextChange, setTimeUntilNextChange] = useState({ minutes: 0, seconds: 0 }); + const [isStarted, setIsStarted] = useState(false); - const times = Math.floor(diffMsBetweenStartedAndNow / durationMs); + useEffect(() => { + // 시작 가격을 설정한다. + setNowPrice(auction.originPrice); - let currentPrice = auction.originPrice; - for (let i = 0; i < times; i++) { - const nextPrice = calculateNextPrice(); - if (priceLimit <= nextPrice) { - currentPrice = nextPrice; - } else { - currentPrice = priceLimit; + // 현재 가격 갱신 타이머 + const intervalId = setInterval(() => { + // 가격 하락 정책이 종료되었는지 체크 + const now = new Date(); + let isLastTime = false; + if (now >= auction.finishedAt) { + isLastTime = true; + setIsLastPrice(true); } - } - setAuction({...auction, currentPrice: currentPrice}); - const intervalId = setInterval(() => { + // 현재 가격 계산 로직 + const durationMs = getMsFromIso8601Duration(auction.variationDuration); + const diffMsBetweenStartedAndNow = getTimeDifferenceInMs( + auction.startedAt, isLastTime ? auction.finishedAt : now + ); + const times = Math.floor(diffMsBetweenStartedAndNow / durationMs); - if (diffMsBetweenStartedAndNow % durationMs === 0) { - const nextPrice = calculateNextPrice(); - if (priceLimit <= nextPrice) { - setAuction({...auction, currentPrice: nextPrice}); + let currentPrice = auction.originPrice; + for (let i = 0; i < times; i++) { // times번 만큼 할인된 가격을 구하는 로직 + const calculatePrice = calculateNextPrice(currentPrice); + if (priceLimit <= calculatePrice) { + currentPrice = calculatePrice; + } else { + currentPrice = priceLimit; } } + // 가격 설정 + setNowPrice(currentPrice); + + // 다음 가격 변동까지 남은 시간 계산 + const msUntilNextChange = durationMs - (diffMsBetweenStartedAndNow % durationMs); + const minutesUntilNextChange = Math.floor(msUntilNextChange / 60000); + const secondsUntilNextChange = Math.floor((msUntilNextChange % 60000) / 1000); + + setTimeUntilNextChange({ + minutes: minutesUntilNextChange, + seconds: secondsUntilNextChange + }); + + // 경매 시작했는지 확인하는 로직 + if (now >= auction.startedAt) { + setIsStarted(true); + } }, 1000); return () => clearInterval(intervalId); }, []); - function calculateNextPrice(): number { + function calculateNextPrice(currentPrice: number): number { if (auction.pricePolicy.type === "CONSTANT") { - return auction.currentPrice - (auction.pricePolicy as ConstantPricePolicy).variationWidth; + return currentPrice - (auction.pricePolicy as ConstantPricePolicy).variationWidth; } else if (auction.pricePolicy.type === "PERCENTAGE") { - return auction.currentPrice - (auction.currentPrice * (auction.pricePolicy as PercentagePricePolicy).discountRate / 100); + return currentPrice - (currentPrice * (auction.pricePolicy as PercentagePricePolicy).discountRate / 100); } else { return -1; } @@ -64,14 +94,14 @@ function PricePolicyElement( case "CONSTANT": return (
- {formatVariationDuration(auction.variationDuration)}후 {auction.pricePolicy.variationWidth}원 할인이 적용됩니다.
) case "PERCENTAGE": return (
- {formatVariationDuration(auction.variationDuration)}후 ${auction.pricePolicy.discountRate}% 할인이 적용됩니다.
) @@ -88,18 +118,30 @@ function PricePolicyElement(

할인 정책

+
{component()}

현재 가격

-

{getPriceFormatted(auction.currentPrice)}

-
-
-

다음 가격

-

{getPriceFormatted(calculateNextPrice())}

+

{getPriceFormatted(nowPrice)}

+ { + isLastPrice + ?
+

최종 가격

+

{getPriceFormatted(nowPrice)}

+
+ :
+ { + isStarted + ?

{timeUntilNextChange.minutes}분 {timeUntilNextChange.seconds}초 뒤

+ :

첫 할인가!

+ } +

{getPriceFormatted(calculateNextPrice(nowPrice))}

+
+ }
diff --git a/front/src/pages/auction/list/AuctionListElement.tsx b/front/src/pages/auction/list/AuctionListElement.tsx index 86d56c4d..f61a7120 100644 --- a/front/src/pages/auction/list/AuctionListElement.tsx +++ b/front/src/pages/auction/list/AuctionListElement.tsx @@ -24,12 +24,14 @@ const AuctionListElement: React.FC = ({ const [status, setStatus] = useState(''); const [timeInfo, setTimeInfo] = useState(''); + const [tagColor, setTagColor] = useState('bg-gray-500'); useEffect(() => { const updateStatus = () => { - const { status, timeInfo } = getAuctionStatus(startedAt, endedAt); + const { status, timeInfo, color } = getAuctionStatus(startedAt, endedAt); setStatus(status); setTimeInfo(timeInfo); + setTagColor(color); }; updateStatus(); @@ -44,7 +46,8 @@ const AuctionListElement: React.FC = ({ } // 상태에 따라 현재가의 색상 설정 - const priceColor = status === '종료' ? 'text-gray-400' : 'text-[#62CBC6]'; + const priceColor = status !== '진행 중' ? 'text-gray-400' : 'text-[#62CBC6]'; + const greyScale = status !== '진행 중' ? 'grayscale' : ''; return (
= ({
{/* 이미지 영역 */} Auction Item - {/* 상태 태그 */} - + {status}
@@ -67,8 +69,8 @@ const AuctionListElement: React.FC = ({

{title}

시작 시간: {getKrDateFormat(startedAt)}

종료 시간: {getKrDateFormat(endedAt)}

-

현재가: {getPriceFormatted(price)}

-

{timeInfo}

+

시작 가격: {getPriceFormatted(price)}

+

{status}! {timeInfo}

); diff --git a/front/src/pages/receipt/detail/ReceiptDetail.tsx b/front/src/pages/receipt/detail/ReceiptDetail.tsx index 8d2b0be7..d6805af8 100644 --- a/front/src/pages/receipt/detail/ReceiptDetail.tsx +++ b/front/src/pages/receipt/detail/ReceiptDetail.tsx @@ -5,8 +5,10 @@ import {ReceiptDetailItem} from "../../../api/receipt/type"; import {getPriceFormatted} from "../../../util/NumberUtil"; import {getKrDateFormat} from "../../../util/DateUtil"; import {usePageStore} from "../../../store/PageStore"; +import useAlert from "../../../hooks/useAlert"; function ReceiptDetailPage() { + const {showAlert} = useAlert(); const {receiptId, setReceiptId} = useReceiptStore(); const {currentPage, setPage} = usePageStore(); @@ -50,8 +52,9 @@ function ReceiptDetailPage() { console.log('Refund success.'); setPage('home'); }, - () => { - console.log('Refund failed.'); + (message) => { + console.log('Refund failed. ' + message); + showAlert(message); } ) } @@ -102,17 +105,17 @@ function ReceiptDetailPage() {

{receiptDetail?.buyerId}

-
- -

{getKrDateFormat(new Date(receiptDetail!.createdAt))}

-
- -
- -

- {getKrDateFormat(new Date(receiptDetail!.updatedAt))} -

-
+ {/*
*/} + {/* */} + {/*

{getKrDateFormat(new Date(receiptDetail!.createdAt))}

*/} + {/*
*/} + + {/*
*/} + {/* */} + {/*

*/} + {/* {getKrDateFormat(new Date(receiptDetail!.updatedAt))}*/} + {/*

*/} + {/*
*/} { receiptDetail?.receiptStatus === 'REFUND' ? '' diff --git a/front/src/util/DateUtil.ts b/front/src/util/DateUtil.ts index 407a9c76..f4709e72 100644 --- a/front/src/util/DateUtil.ts +++ b/front/src/util/DateUtil.ts @@ -88,14 +88,14 @@ const getAuctionStatus = (startedAt: Date, endedAt: Date): { status: string; tim return { status: '곧 시작', timeInfo: formatTime(timeUntilStart), - color: 'text-red-500' + color: 'bg-red-500' }; } else { // 5분 이상 return { status: '진행 예정', timeInfo: formatTime(timeUntilStart), - color: 'text-blue-500' + color: 'bg-blue-500' }; } } else if (now >= startedAt && now <= endedAt) { @@ -104,7 +104,7 @@ const getAuctionStatus = (startedAt: Date, endedAt: Date): { status: string; tim return { status: '진행 중', timeInfo: `남은 시간: ${formatTime(timeRemaining)}`, - color: 'text-red-500' + color: 'bg-red-500' }; } else { // 경매 종료 @@ -112,7 +112,7 @@ const getAuctionStatus = (startedAt: Date, endedAt: Date): { status: string; tim return { status: '종료', timeInfo: formatEndTime(timeElapsed), - color: 'text-gray-500' + color: 'bg-gray-500' }; } }; @@ -142,14 +142,18 @@ function getMsFromIso8601Duration(duration: string): number { } function getTimeDifferenceInMs(date1: Date, date2: Date): number { - const timeDifference = date2.getTime() - date1.getTime(); - if (timeDifference < 0) { - return -timeDifference; - } else { - return timeDifference; - } + return date2.getTime() - date1.getTime(); } +// function getTimeDifferenceInMs(date1: Date, date2: Date): number { +// const timeDifference = date2.getTime() - date1.getTime(); +// if (timeDifference < 0) { +// return -timeDifference; +// } else { +// return timeDifference; +// } +// } + export { getKrDateFormat, getRelativeTime,