+
-
+
나이 구간 설정
diff --git a/app/matching/_components/ImportantOptionDrawer.tsx b/app/matching/_components/ImportantOptionDrawer.tsx
index 2cd5508..bca34b8 100644
--- a/app/matching/_components/ImportantOptionDrawer.tsx
+++ b/app/matching/_components/ImportantOptionDrawer.tsx
@@ -208,11 +208,11 @@ export default function ImportantOptionDrawer({
@@ -233,7 +233,7 @@ export default function ImportantOptionDrawer({
)}
>
{!selectedOption ? (
-
+
{typingStep >= 1 && (
-
+
= 2 && (
@@ -299,7 +299,7 @@ export default function ImportantOptionDrawer({
-
@@ -331,10 +331,10 @@ export default function ImportantOptionDrawer({
}
-
diff --git a/app/matching/_components/MatchingAgeOption.tsx b/app/matching/_components/MatchingAgeOption.tsx
index bdf5bb0..f6245bc 100644
--- a/app/matching/_components/MatchingAgeOption.tsx
+++ b/app/matching/_components/MatchingAgeOption.tsx
@@ -89,7 +89,7 @@ export default function MatchingAgeOption({
)}
>
@@ -99,10 +99,7 @@ export default function MatchingAgeOption({
!showCheck ? "opacity-100" : "opacity-0",
)}
>
-
+
diff --git a/app/matching/_components/MatchingImportantOptionSection.tsx b/app/matching/_components/MatchingImportantOptionSection.tsx
index 969ab47..0d76122 100644
--- a/app/matching/_components/MatchingImportantOptionSection.tsx
+++ b/app/matching/_components/MatchingImportantOptionSection.tsx
@@ -79,7 +79,7 @@ export default function MatchingImportantOptionSection({
)}
>
@@ -92,7 +92,7 @@ export default function MatchingImportantOptionSection({
)}
>
diff --git a/app/matching/_components/MatchingOptionCard.tsx b/app/matching/_components/MatchingOptionCard.tsx
index b26b700..d473a3f 100644
--- a/app/matching/_components/MatchingOptionCard.tsx
+++ b/app/matching/_components/MatchingOptionCard.tsx
@@ -44,10 +44,10 @@ export default function MatchingOptionCard({
{/* 드래그 핸들 아이콘 (Hamburger 형태) */}
-
-
-
-
+
);
diff --git a/app/matching/_components/MatchingSameMajorSection.tsx b/app/matching/_components/MatchingSameMajorSection.tsx
index d09ba39..5e3ba84 100644
--- a/app/matching/_components/MatchingSameMajorSection.tsx
+++ b/app/matching/_components/MatchingSameMajorSection.tsx
@@ -79,7 +79,7 @@ export default function MatchingSameMajorSection({
)}
>
@@ -91,10 +91,7 @@ export default function MatchingSameMajorSection({
!showCheck ? "opacity-100" : "opacity-0",
)}
>
-
+
) : (
diff --git a/app/mypage/_components/ScreenMyPage.tsx b/app/mypage/_components/ScreenMyPage.tsx
index 7d0e52f..7ee8eeb 100644
--- a/app/mypage/_components/ScreenMyPage.tsx
+++ b/app/mypage/_components/ScreenMyPage.tsx
@@ -691,7 +691,7 @@ const ScreenMyPage = ({ initialProfile }: ScreenMyPageProps) => {
setEditableBirthYear(val);
}}
placeholder="출생연도"
- className="typo-16-600 w-[64px] bg-transparent text-right text-[#999999] underline outline-none placeholder:text-[#B3B3B3]"
+ className="typo-16-600 w-16 bg-transparent text-right text-[#999999] underline outline-none placeholder:text-[#B3B3B3]"
/>
{editableBirthYear && (
년생
diff --git a/app/profile-image/_components/IntroSection.tsx b/app/profile-image/_components/IntroSection.tsx
index 9f0bd32..a03f5ae 100644
--- a/app/profile-image/_components/IntroSection.tsx
+++ b/app/profile-image/_components/IntroSection.tsx
@@ -15,7 +15,7 @@ const IntroSection = ({ intro, onIntroChange }: IntroSectionProps) => {
자기소개
-
-
+
diff --git a/app/register/_components/VerificationStep.tsx b/app/register/_components/VerificationStep.tsx
index 60302f3..bbc4770 100644
--- a/app/register/_components/VerificationStep.tsx
+++ b/app/register/_components/VerificationStep.tsx
@@ -125,7 +125,7 @@ export const VerificationStep = ({
type="button"
onClick={handleResend}
disabled={resendCooldown > 0 || isSending}
- className="bg-bubble-background-white text-color-text-caption1 typo-18-600 h-[48px] w-[120px] shrink-0"
+ className="bg-bubble-background-white text-color-text-caption1 typo-18-600 h-12 w-[120px] shrink-0"
shadow={false}
>
{isSending
@@ -143,7 +143,7 @@ export const VerificationStep = ({
-
+
인증번호가 오지 않았나요?
이전 단계에서 이메일을 재확인하거나, 스팸함을 확인해
diff --git a/app/reset/_components/InfoCard.tsx b/app/reset/_components/InfoCard.tsx
index 0ce9201..f24f17a 100644
--- a/app/reset/_components/InfoCard.tsx
+++ b/app/reset/_components/InfoCard.tsx
@@ -20,13 +20,13 @@ const InfoCard = ({
const CardContent = (
-
+
{
onChange={(e) => setEmail(e.target.value)}
required
/>
-
+
{message.text ? `*${message.text}` : ""}
diff --git a/app/reset/password/verification/_components/ScreenVerificationPage.tsx b/app/reset/password/verification/_components/ScreenVerificationPage.tsx
index 5334254..592e9b4 100644
--- a/app/reset/password/verification/_components/ScreenVerificationPage.tsx
+++ b/app/reset/password/verification/_components/ScreenVerificationPage.tsx
@@ -207,7 +207,7 @@ const ScreenVerificationPage = () => {
-
+
인증번호가 오지 않았나요?
이전 단계에서 이메일을 재확인하거나, 스팸함을 확인해
diff --git a/app/roulette/_components/Roulette.tsx b/app/roulette/_components/Roulette.tsx
index 2c431d8..9fb4c75 100644
--- a/app/roulette/_components/Roulette.tsx
+++ b/app/roulette/_components/Roulette.tsx
@@ -28,7 +28,7 @@ const ROULETTE_CONFIG: Record<
{ imageSrc: string; items: RouletteItem[] }
> = {
free: {
- imageSrc: "/roulette/roulette3.png",
+ imageSrc: "/roulette/free_roulette.png",
items: [
{ id: 1, label: "옵션권 1장" },
{ id: 2, label: "옵션권 2장" },
diff --git a/app/roulette/_components/RouletteCards.tsx b/app/roulette/_components/RouletteCards.tsx
index 4ddc14a..5aabf0c 100644
--- a/app/roulette/_components/RouletteCards.tsx
+++ b/app/roulette/_components/RouletteCards.tsx
@@ -20,7 +20,7 @@ export const FreeRouletteCard = ({
return (
@@ -40,11 +40,15 @@ export const FreeRouletteCard = ({
{/* Status Checkbox */}
{/* Shadow Ellipse */}
-
+
{/* Free Roulette Graphic */}
router.push("/roulette/free")}
- className={`flex h-14 w-full items-center justify-center rounded-[16px] border border-white/30 transition-transform ${
+ className={`flex h-14 w-full items-center justify-center rounded-2xl border border-white/30 transition-transform ${
hasChances
? "bg-color-flame-700 hover:opacity-95 active:scale-[0.98]"
: "cursor-not-allowed bg-[rgba(179,179,179,0.4)] backdrop-blur-[15px]"
@@ -122,12 +126,13 @@ export const SpecialRouletteCard = ({
isSpecialParticipated = false,
}: SpecialRouletteCardProps) => {
const router = useRouter();
- const hasChances = !isSpecialParticipated && currentAmount >= targetAmount;
+ const isAmountSatisfied = currentAmount >= targetAmount;
+ const hasChances = !isSpecialParticipated && isAmountSatisfied;
const isDisabled = !hasChances;
return (
@@ -148,7 +153,7 @@ export const SpecialRouletteCard = ({
@@ -165,7 +170,7 @@ export const SpecialRouletteCard = ({
{/* Shadow Ellipse */}
-
+
{/* Special Roulette Graphic */}
router.push("/roulette/special")}
- className={`flex h-14 w-full items-center justify-center rounded-[16px] border border-white/30 transition-transform ${
+ className={`flex h-14 w-full items-center justify-center rounded-2xl border border-white/30 transition-transform ${
isDisabled
? "cursor-not-allowed bg-[rgba(179,179,179,0.4)] backdrop-blur-[15px]"
: "bg-button-primary hover:opacity-95 active:scale-[0.98]"
@@ -208,7 +213,11 @@ export const SpecialRouletteCard = ({
isDisabled ? "text-[#B3B3B3]" : "text-white"
}`}
>
- {isDisabled ? "오늘은 이미 참여했어요" : "스페셜 룰렛 입장"}
+ {isSpecialParticipated
+ ? "오늘은 이미 참여했어요"
+ : !hasChances
+ ? `${(targetAmount - currentAmount).toLocaleString()}원 더 결제 시 입장`
+ : "스페셜 룰렛 입장"}
diff --git a/app/roulette/_components/RouletteHeader.tsx b/app/roulette/_components/RouletteHeader.tsx
index 7466584..645f693 100644
--- a/app/roulette/_components/RouletteHeader.tsx
+++ b/app/roulette/_components/RouletteHeader.tsx
@@ -33,7 +33,7 @@ const RouletteHeader = ({ title, sidebar, onBack }: RouletteHeaderProps) => {
};
return (
-
+
{title && (
diff --git a/app/roulette/_components/RouletteProbabilityBottomSheet.tsx b/app/roulette/_components/RouletteProbabilityBottomSheet.tsx
index 42d4b3e..b55a01b 100644
--- a/app/roulette/_components/RouletteProbabilityBottomSheet.tsx
+++ b/app/roulette/_components/RouletteProbabilityBottomSheet.tsx
@@ -124,7 +124,7 @@ export default function RouletteProbabilityBottomSheet({
{/* Header - Frame 1171276915 */}
보상
-
+
확률
@@ -139,7 +139,7 @@ export default function RouletteProbabilityBottomSheet({
{item.reward}
-
+
{item.probability}
diff --git a/app/roulette/_components/RouletteResultModal.tsx b/app/roulette/_components/RouletteResultModal.tsx
index 0b4b8a9..f17523d 100644
--- a/app/roulette/_components/RouletteResultModal.tsx
+++ b/app/roulette/_components/RouletteResultModal.tsx
@@ -47,10 +47,10 @@ const ITEM_DESCRIPTION_MAP: Record
= {
const ConfettiLeft = () => (
-
+
-
+
@@ -60,9 +60,9 @@ const ConfettiLeft = () => (
const ConfettiRight = () => (
@@ -99,7 +99,7 @@ export default function RouletteResultModal({
{/* 찐 모달 본체 */}
-
+
{/* 컨페티 (장식용이므로 얘네만 absolute 유지) */}
@@ -123,7 +123,7 @@ export default function RouletteResultModal({
/>
{/* 바닥 그림자 */}
-
+
{/* 하단 텍스트 영역 */}
@@ -137,7 +137,7 @@ export default function RouletteResultModal({
{/* 하단 버튼 영역 */}
-
+
확인
{item.label !== "꽝" && (
{
{/* Content */}
{/* Title */}
-
+
오늘의 행운을 확인해보세요!
매일 1회 무료로 돌릴 수 있어요
@@ -45,13 +45,18 @@ const ScreenRouletteMain = () => {
{/* Cards */}
diff --git a/app/roulette/free/_components/ScreenRouletteFree.tsx b/app/roulette/free/_components/ScreenRouletteFree.tsx
index 1412f80..903f56f 100644
--- a/app/roulette/free/_components/ScreenRouletteFree.tsx
+++ b/app/roulette/free/_components/ScreenRouletteFree.tsx
@@ -1,6 +1,5 @@
"use client";
import React, { useRef, useState } from "react";
-import { useRouter } from "next/navigation";
import { CircleAlert } from "lucide-react";
import RouletteHeader from "../../_components/RouletteHeader";
import Button from "@/components/ui/Button";
@@ -14,11 +13,11 @@ import { useRouletteStatus } from "@/hooks/useRouletteStatus";
import { useSpinRoulette } from "@/hooks/useSpinRoulette";
const ScreenRouletteFree = () => {
- const router = useRouter();
const { data: rouletteStatus, isLoading } = useRouletteStatus();
- // isFreeParticipated: true = 아직 참여 안 함(1회 남음), false = 이미 참여함(0회)
- const hasChances = rouletteStatus?.isFreeParticipated ?? false;
+ // isFreeParticipated: false = 아직 참여 안 함(1회 남음), true = 이미 참여함(0회)
+ const isFreeParticipated = rouletteStatus?.isFreeParticipated ?? true;
+ const hasChances = !isFreeParticipated;
const remainingChances = hasChances ? 1 : 0;
const rouletteRef = useRef
(null);
diff --git a/app/roulette/free/page.tsx b/app/roulette/free/page.tsx
index 965a6c9..ddda8bf 100644
--- a/app/roulette/free/page.tsx
+++ b/app/roulette/free/page.tsx
@@ -12,6 +12,10 @@ export const metadata = {
description: "이벤트 룰렛 진행",
};
+// 페이지 이동 시마다 서버에서 항상 최신 데이터를 가져와 HydrationBoundary로 주입
+// useEffect + refetch 없이도 신선한 상태를 보장
+export const dynamic = "force-dynamic";
+
interface RoulettePageApiResponse {
code: string;
status: number;
diff --git a/app/roulette/special/[amount]/page.tsx b/app/roulette/special/[amount]/page.tsx
index e75a419..7fb6998 100644
--- a/app/roulette/special/[amount]/page.tsx
+++ b/app/roulette/special/[amount]/page.tsx
@@ -32,12 +32,12 @@ export default function SpecialAmountPage({
>
{/* Decorative Vectors (별/빛 모양 등) */}
{/* 닫기 버튼 */}
@@ -60,21 +60,21 @@ export default function SpecialAmountPage({
{/* 당첨 금액 */}
-
+
{displayAmount}원
{/* 바우처 라벨 구분선 (Voucher container) */}
-
+
-
{/* 바우처 이미지 */}
-
+
{/* 사용 방법 안내 (Redemption instructions container) */}
-
+
{/* Way 1 */}
-
-
-
+
+
+
수령 방법 1
@@ -103,9 +103,9 @@ export default function SpecialAmountPage({
{/* Way 2 */}
-
-
-
+
+
+
수령 방법 2
diff --git a/app/roulette/special/_components/SpecialRouletteChanceCard.tsx b/app/roulette/special/_components/SpecialRouletteChanceCard.tsx
index 5d0e7bb..d9acb16 100644
--- a/app/roulette/special/_components/SpecialRouletteChanceCard.tsx
+++ b/app/roulette/special/_components/SpecialRouletteChanceCard.tsx
@@ -41,7 +41,7 @@ export default function SpecialRouletteChanceCard({
return (
@@ -81,7 +81,7 @@ export default function SpecialRouletteChanceCard({
>
{/* Progress indicator bar */}
{percent > 0 && percent < 100 && (
-
+
)}
diff --git a/app/roulette/special/page.tsx b/app/roulette/special/page.tsx
index d348b0e..9cff2a4 100644
--- a/app/roulette/special/page.tsx
+++ b/app/roulette/special/page.tsx
@@ -9,9 +9,12 @@ import { RoulettePageResponse } from "@/hooks/useRouletteStatus";
export const metadata = {
title: "스페셜 룰렛",
- description: "스페셜 룰렛 진행",
+ description: "스페셜 이벤트 룰렛 진행",
};
+// 페이지 이동 시마다 서버에서 항상 최신 데이터를 가져와 HydrationBoundary로 주입
+export const dynamic = "force-dynamic";
+
interface RoulettePageApiResponse {
code: string;
status: number;
diff --git a/components/common/ToastContainer.tsx b/components/common/ToastContainer.tsx
index 1782467..b952286 100644
--- a/components/common/ToastContainer.tsx
+++ b/components/common/ToastContainer.tsx
@@ -25,7 +25,7 @@ export default function ToastContainer() {
>
{/* 알림 아이콘: COMAtching 그라데이션 브랜드 테두리 적용 */}
-
+
-
+
{toast.title}
diff --git a/components/common/charge-confirm/DirectChargeDrawer.tsx b/components/common/charge-confirm/DirectChargeDrawer.tsx
index 2b2e47a..55508d3 100644
--- a/components/common/charge-confirm/DirectChargeDrawer.tsx
+++ b/components/common/charge-confirm/DirectChargeDrawer.tsx
@@ -46,6 +46,7 @@ export default function DirectChargeDrawer({
depositorName || realNameData?.data?.realName || "",
);
const [agreed, setAgreed] = React.useState(false);
+ const [quantity, setQuantity] = React.useState(1);
// 실명이 로드되면 이름 업데이트
React.useEffect(() => {
@@ -55,10 +56,11 @@ export default function DirectChargeDrawer({
}
}, [realNameData, depositorName, name]);
- // Drawer가 닫힐 때 동의 상태 리셋
+ // Drawer가 닫힐 때 상태 리셋
React.useEffect(() => {
if (!open) {
setAgreed(false);
+ setQuantity(1);
}
}, [open]);
@@ -75,7 +77,7 @@ export default function DirectChargeDrawer({
};
/* 충전 완료 알림 */
- const handleConfirmAction = (customAmount: number) => {
+ const handleConfirmAction = () => {
if (!name) {
alert("먼저 입금자명을 설정해 주세요.");
onOpenChange(false);
@@ -83,40 +85,47 @@ export default function DirectChargeDrawer({
return;
}
- purchase(productId, {
- onSuccess: async () => {
- try {
- await navigator.clipboard.writeText(BANK_INFO.account);
- } catch (err) {
- console.error("Failed to copy account number: ", err);
- }
- alert("계좌번호가 복사되었습니다.");
- onOpenChange(false);
- queryClient.invalidateQueries({ queryKey: ["myProfile"] });
- },
- onError: (error: AxiosError<{ code?: string; message?: string }>) => {
- const errorData = error.response?.data;
- if (errorData?.code === "PAY-003") {
- alert("이미 입금 확인 대기 중인 주문이 존재합니다.");
- onOpenChange(false);
- } else if (
- errorData?.code === "PAY-004" ||
- errorData?.message?.includes("사용자명") ||
- errorData?.message?.includes("입금자명")
- ) {
- alert(errorData?.message || "먼저 입금자명을 설정해 주세요.");
+ purchase(
+ { productId, quantity },
+ {
+ onSuccess: async () => {
+ try {
+ await navigator.clipboard.writeText(BANK_INFO.account);
+ } catch (err) {
+ console.error("Failed to copy account number: ", err);
+ }
+ alert("계좌번호가 복사되었습니다.");
onOpenChange(false);
- drawerContext?.setActiveTab(2);
- } else {
- alert(
- errorData?.message ||
- "충전 요청 중 오류가 발생했습니다. 다시 시도해 주세요.",
- );
- }
+ queryClient.invalidateQueries({ queryKey: ["myProfile"] });
+ },
+ onError: (error: AxiosError<{ code?: string; message?: string }>) => {
+ const errorData = error.response?.data;
+ if (errorData?.code === "PAY-003") {
+ alert("이미 입금 확인 대기 중인 주문이 존재합니다.");
+ onOpenChange(false);
+ } else if (
+ errorData?.code === "PAY-004" ||
+ errorData?.message?.includes("사용자명") ||
+ errorData?.message?.includes("입금자명")
+ ) {
+ alert(errorData?.message || "먼저 입금자명을 설정해 주세요.");
+ onOpenChange(false);
+ drawerContext?.setActiveTab(2);
+ } else {
+ alert(
+ errorData?.message ||
+ "충전 요청 중 오류가 발생했습니다. 다시 시도해 주세요.",
+ );
+ }
+ },
},
- });
+ );
};
+ const isQuantityItem =
+ productName?.includes("옵션권") || productName?.includes("뽑기권");
+ const totalAmount = amount * quantity;
+
return (
{/* ── 입금 정보 카드 ── */}
-
+
{/* 입금계좌 라벨 + 복사 */}
-
+
입금계좌
{/* 계좌 정보 박스 */}
-
+
{BANK_INFO.bank}
@@ -168,28 +177,62 @@ export default function DirectChargeDrawer({
- {/* 입금자명 */}
-
-
- 입금자명
-
-
-
- {name || "미지정"}
+ {/* 구매 수량 (조건부) */}
+ {isQuantityItem && (
+
+
+ 구매 수량
+
+ setQuantity((q) => Math.max(1, q - 1))}
+ disabled={quantity <= 1}
+ className="flex h-6 w-6 items-center justify-center text-[#777777] disabled:opacity-30"
+ >
+ -
+
+
+ {quantity}
+
+ setQuantity((q) => Math.min(3, q + 1))}
+ disabled={quantity >= 3}
+ className="flex h-6 w-6 items-center justify-center text-[#777777] disabled:opacity-30"
+ >
+ +
+
+
+
+ )}
+
+
+ {/* 입금자명 */}
+
+
+ 입금자명
+
+
+
+ {name || "미지정"}
+
+
-
- {/* 입금액 */}
-
-
- 입금액
-
-
-
- {amount.toLocaleString()}
+ {/* 입금액 */}
+
+
+ 총 입금액
-
원
+
+
+ {totalAmount.toLocaleString()}
+
+
+ 원
+
+
@@ -220,7 +263,7 @@ export default function DirectChargeDrawer({
handleConfirmAction(amount)}
+ onClick={handleConfirmAction}
>
{isPending ? "요청 중..." : "충전 확인 요청하기"}
@@ -228,7 +271,7 @@ export default function DirectChargeDrawer({
혹은 Toss로 계좌이체하기
diff --git a/components/common/charge-confirm/TossChargeDrawer.tsx b/components/common/charge-confirm/TossChargeDrawer.tsx
index 0fa87fc..6fb23c9 100644
--- a/components/common/charge-confirm/TossChargeDrawer.tsx
+++ b/components/common/charge-confirm/TossChargeDrawer.tsx
@@ -49,6 +49,7 @@ export default function TossChargeDrawer({
const [isEditingName, setIsEditingName] = React.useState(false);
const [nameInput, setNameInput] = React.useState("");
+ const [quantity, setQuantity] = React.useState(1);
// 실명이 로드되면 이름 업데이트
React.useEffect(() => {
@@ -63,6 +64,7 @@ export default function TossChargeDrawer({
if (open) {
setIsEditingName(false);
setNameInput(name || realNameData?.data?.realName || "");
+ setQuantity(1);
}
}, [open, name, realNameData]);
@@ -125,40 +127,47 @@ export default function TossChargeDrawer({
return;
}
- purchase(productId, {
- onSuccess: () => {
- alert(
- "충전 요청이 완료되었습니다! 토스가 설치되어 있다면 토스로 바로 이동합니다.",
- );
- onOpenChange(false);
- queryClient.invalidateQueries({ queryKey: ["myProfile"] });
-
- // 토스 송금 앱으로 즉시 자동 연동
- handleTossTransfer(customAmount);
- },
- onError: (error: AxiosError<{ code?: string; message?: string }>) => {
- const errorData = error.response?.data;
- if (errorData?.code === "PAY-003") {
- alert("이미 입금 확인 대기 중인 주문이 존재합니다.");
- onOpenChange(false);
- } else if (
- errorData?.code === "PAY-004" ||
- errorData?.message?.includes("사용자명") ||
- errorData?.message?.includes("입금자명")
- ) {
- alert(errorData?.message || "먼저 입금자명을 설정해 주세요.");
- onOpenChange(false);
- drawerContext?.setActiveTab(2);
- } else {
+ purchase(
+ { productId, quantity },
+ {
+ onSuccess: () => {
alert(
- errorData?.message ||
- "충전 요청 중 오류가 발생했습니다. 다시 시도해 주세요.",
+ "충전 요청이 완료되었습니다! 토스가 설치되어 있다면 토스로 바로 이동합니다.",
);
- }
+ onOpenChange(false);
+ queryClient.invalidateQueries({ queryKey: ["myProfile"] });
+
+ // 토스 송금 앱으로 즉시 자동 연동
+ handleTossTransfer(customAmount);
+ },
+ onError: (error: AxiosError<{ code?: string; message?: string }>) => {
+ const errorData = error.response?.data;
+ if (errorData?.code === "PAY-003") {
+ alert("이미 입금 확인 대기 중인 주문이 존재합니다.");
+ onOpenChange(false);
+ } else if (
+ errorData?.code === "PAY-004" ||
+ errorData?.message?.includes("사용자명") ||
+ errorData?.message?.includes("입금자명")
+ ) {
+ alert(errorData?.message || "먼저 입금자명을 설정해 주세요.");
+ onOpenChange(false);
+ drawerContext?.setActiveTab(2);
+ } else {
+ alert(
+ errorData?.message ||
+ "충전 요청 중 오류가 발생했습니다. 다시 시도해 주세요.",
+ );
+ }
+ },
},
- });
+ );
};
+ const isQuantityItem =
+ productName?.includes("옵션권") || productName?.includes("뽑기권");
+ const totalAmount = amount * quantity;
+
return (
{/* ── Header (CONTENTS) ── */}
-
+
결제하기
{/* ── Product Card (Frame 2612294) ── */}
-
+
{/* Row 1 (Product Name & Price) */}
-
+
{productName || "뽑기권"}
-
+
{amount.toLocaleString()}원
{/* Row 2 (Pricing/Depositor) */}
+ {/* 구매 수량 (조건부) */}
+ {isQuantityItem && (
+
+
+ 구매 수량
+
+
+ setQuantity((q) => Math.max(1, q - 1))}
+ disabled={quantity <= 1}
+ className="flex h-6 w-6 items-center justify-center text-[#777777] disabled:opacity-30"
+ >
+ -
+
+
+ {quantity}
+
+ setQuantity((q) => Math.min(3, q + 1))}
+ disabled={quantity >= 3}
+ className="flex h-6 w-6 items-center justify-center text-[#777777] disabled:opacity-30"
+ >
+ +
+
+
+
+ )}
+
{/* Depositor Name (Frame 2612281) */}
-
+
입금자명
) : (
<>
-
+
{name || "미지정"}
@@ -235,14 +274,14 @@ export default function TossChargeDrawer({
{/* Total Price (Frame 2612282) */}
-
- 결제 금액
+
+ 총 결제 금액
-
- {amount.toLocaleString()}
+
+ {totalAmount.toLocaleString()}
-
+
원
@@ -257,8 +296,8 @@ export default function TossChargeDrawer({
handleConfirmAction(amount)}
- className="flex h-12 w-full max-w-[345px] flex-row items-center justify-center gap-[10px] rounded-[8px] bg-[#1A1A1A] px-[18px] py-[3px] transition-colors hover:bg-black"
+ onClick={() => handleConfirmAction(totalAmount)}
+ className="flex h-12 w-full max-w-[345px] flex-row items-center justify-center gap-2.5 rounded-lg bg-[#1A1A1A] px-[18px] py-[3px] transition-colors hover:bg-black"
>
-
+
{isPending
? "요청 중..."
- : `Toss로 ${amount.toLocaleString()}원 결제하기`}
+ : `Toss로 ${totalAmount.toLocaleString()}원 결제하기`}
{/* Manual Transfer Link */}
-
+
토스앱을 설치하지 않으셨나요?
수동으로 계좌이체하기
diff --git a/components/common/charge/ChargeInventoryCard.tsx b/components/common/charge/ChargeInventoryCard.tsx
index 912e9e4..0ff3a01 100644
--- a/components/common/charge/ChargeInventoryCard.tsx
+++ b/components/common/charge/ChargeInventoryCard.tsx
@@ -25,7 +25,7 @@ export default function ChargeInventoryCard() {
};
return (
-
+
{/* 보유 수량 */}
{INVENTORY_ROWS.map((row) => {
diff --git a/components/common/charge/DepositorNameContent.tsx b/components/common/charge/DepositorNameContent.tsx
index a3c93bc..e0b1347 100644
--- a/components/common/charge/DepositorNameContent.tsx
+++ b/components/common/charge/DepositorNameContent.tsx
@@ -47,7 +47,7 @@ export default function DepositorNameContent() {
입금자명
{/* ── 안내 가이드 (Frame 2612440) ── */}
-
+
{/* 유의사항 */}
유의사항
diff --git a/components/common/charge/QuickBundleCard.tsx b/components/common/charge/QuickBundleCard.tsx
index 061dce1..ff728ec 100644
--- a/components/common/charge/QuickBundleCard.tsx
+++ b/components/common/charge/QuickBundleCard.tsx
@@ -21,7 +21,7 @@ export default function QuickBundleCard({
.join("+");
return (
-
+
{product.price.toLocaleString()}원
@@ -67,7 +67,7 @@ export function ShopBundleRow({ product }: ShopBundleRowProps) {
{/* 두 번째 줄: 리워드 목록 상세 */}
-
+
{description}
@@ -85,7 +85,7 @@ export function ShopBundleRow({ product }: ShopBundleRowProps) {
-
+
Close
diff --git a/hooks/admin/useAdminOrderSocket.ts b/hooks/admin/useAdminOrderSocket.ts
index 0c61f35..6f72203 100644
--- a/hooks/admin/useAdminOrderSocket.ts
+++ b/hooks/admin/useAdminOrderSocket.ts
@@ -58,17 +58,17 @@ export function useAdminOrderSocket() {
const handleOrderCreated = useCallback(
(payload: OrderCreatedPayload) => {
- queryClient.setQueryData(["adminOrders"], (old) => {
+ queryClient.setQueryData(["adminOrders"], (old) => {
if (!old) return old;
- const exists = old.data.some(
- (order) => order.requestId === payload.orderId,
- );
+ const exists = old.some((order) => order.requestId === payload.orderId);
if (exists) return old;
const newOrder: AdminOrder = {
requestId: payload.orderId,
memberId: payload.memberId,
+ productId: -1, // Web Socket 페이로드에 없는 경우 기본값
+ productCode: "UNKNOWN",
requestedItemName: payload.requestedItemName,
requesterRealName: payload.requesterRealName,
requesterUsername: payload.requesterUsername,
@@ -81,10 +81,7 @@ export function useAdminOrderSocket() {
expiresAt: payload.expiresAt,
};
- return {
- ...old,
- data: [newOrder, ...old.data],
- };
+ return [newOrder, ...old];
});
},
[queryClient],
@@ -92,20 +89,17 @@ export function useAdminOrderSocket() {
const handleOrderStatusChanged = useCallback(
(payload: OrderStatusChangedPayload) => {
- queryClient.setQueryData(["adminOrders"], (old) => {
+ queryClient.setQueryData(["adminOrders"], (old) => {
if (!old) return old;
- return {
- ...old,
- data: old.data.map((order) =>
- order.requestId === payload.orderId
- ? {
- ...order,
- status: payload.toStatus as AdminOrder["status"],
- }
- : order,
- ),
- };
+ return old.map((order) =>
+ order.requestId === payload.orderId
+ ? {
+ ...order,
+ status: payload.toStatus as AdminOrder["status"],
+ }
+ : order,
+ );
});
},
[queryClient],
diff --git a/hooks/admin/useAdminOrders.ts b/hooks/admin/useAdminOrders.ts
index 3f79b3d..b209e4c 100644
--- a/hooks/admin/useAdminOrders.ts
+++ b/hooks/admin/useAdminOrders.ts
@@ -6,6 +6,8 @@ import { AxiosError } from "axios";
export interface AdminOrder {
requestId: number;
memberId: number;
+ productId: number;
+ productCode: string;
requestedItemName: string;
requesterRealName: string;
requesterUsername: string;
@@ -18,7 +20,18 @@ export interface AdminOrder {
expiresAt: string;
}
-interface ApiResponse {
+/* ── 페이징 응답 구조 ── */
+export interface PaginatedResponse {
+ content: T[];
+ currentPage: number;
+ size: number;
+ totalElements: number;
+ totalPages: number;
+ hasNext: boolean;
+ hasPrevious: boolean;
+}
+
+export interface ApiResponse {
code: string;
status: number;
message: string;
@@ -26,11 +39,22 @@ interface ApiResponse {
}
/* ── 대기 주문 목록 조회 ── */
-const fetchAdminOrders = async (): Promise> => {
- const { data } = await api.get>(
- "/api/v1/admin/payment/requests",
- );
- return data;
+const fetchAdminOrders = async (): Promise => {
+ let allOrders: AdminOrder[] = [];
+ let page = 0;
+ let hasNext = true;
+
+ while (hasNext) {
+ const { data } = await api.get>>(
+ "/api/v1/admin/payment/requests",
+ { params: { page, size: 100, sort: "requestedAt,desc" } },
+ );
+ allOrders = [...allOrders, ...data.data.content];
+ hasNext = data.data.hasNext;
+ page += 1;
+ }
+
+ return allOrders;
};
/* ── 승인 ── */
diff --git a/hooks/usePurchaseProduct.ts b/hooks/usePurchaseProduct.ts
index b15a10b..a4745f5 100644
--- a/hooks/usePurchaseProduct.ts
+++ b/hooks/usePurchaseProduct.ts
@@ -12,11 +12,15 @@ interface PurchaseResponse {
/**
* 상품 구매(주문 생성) API 호출 함수
*/
-export const postPurchaseProduct = async (
- productId: number,
-): Promise => {
+export const postPurchaseProduct = async ({
+ productId,
+ quantity = 1,
+}: {
+ productId: number;
+ quantity?: number;
+}): Promise => {
const { data } = await api.post(
- `/api/v1/shop/purchase/${productId}`,
+ `/api/v1/shop/purchase/${productId}?quantity=${quantity}`,
{},
);
return data;
@@ -29,7 +33,13 @@ export const usePurchaseProduct = () => {
const queryClient = useQueryClient();
return useMutation({
- mutationFn: (productId: number) => postPurchaseProduct(productId),
+ mutationFn: ({
+ productId,
+ quantity,
+ }: {
+ productId: number;
+ quantity?: number;
+ }) => postPurchaseProduct({ productId, quantity }),
onSuccess: (data) => {
console.log("✅ 주문 생성 성공:", data);
queryClient.invalidateQueries({ queryKey: ["requestStatus"] });
diff --git a/lib/axios.ts b/lib/axios.ts
index da49f9f..d1e6605 100644
--- a/lib/axios.ts
+++ b/lib/axios.ts
@@ -1,8 +1,11 @@
import axios from "axios";
// 1. 공통 설정 (URL, 헤더 등)
+// 개발 환경에서는 프록시(rewrites)를 타도록 빈 문자열 지정 (브라우저가 알아서 localhost:3000으로 보냄)
const API_URL =
- process.env.NEXT_PUBLIC_API_URL || "https://srv.comatching.site";
+ process.env.NODE_ENV === "development"
+ ? ""
+ : process.env.NEXT_PUBLIC_API_URL || "https://srv.comatching.site";
const axiosConfig = {
baseURL: API_URL,
diff --git a/public/main/main-roulette.png b/public/main/main-roulette.png
deleted file mode 100644
index b18d3c3..0000000
Binary files a/public/main/main-roulette.png and /dev/null differ
diff --git a/public/roulette/roulette3.png b/public/roulette/free_roulette.png
similarity index 100%
rename from public/roulette/roulette3.png
rename to public/roulette/free_roulette.png
diff --git a/public/sw.js b/public/sw.js
index 9a323d1..febf4b8 100644
--- a/public/sw.js
+++ b/public/sw.js
@@ -34,18 +34,46 @@ self.addEventListener("fetch", (event) => {
// Do not intercept FCM route or API calls
if (
event.request.url.includes("/api/") ||
- event.request.url.includes("firebase")
+ event.request.url.includes("firebase") ||
+ event.request.url.includes("/ws/")
) {
return;
}
+ // HTML 문서 요청(Navigation)이거나 루트("/") 요청일 경우 Network First 전략 사용
+ // 이유: 캐시된 예전 HTML이 새 빌드의 CSS/JS 청크를 참조하여 404가 발생하는 것을 방지
+ if (
+ event.request.mode === "navigate" ||
+ new URL(event.request.url).pathname === "/"
+ ) {
+ event.respondWith(
+ fetch(event.request)
+ .then((networkResponse) => {
+ // 2xx 성공 응답일 때만 캐시 갱신
+ // 4xx·5xx 오류 응답으로 정상 캐시를 덮어쓰지 않도록 방지
+ if (networkResponse.ok) {
+ const clonedResponse = networkResponse.clone();
+ caches.open(CACHE_NAME).then((cache) => {
+ cache.put(event.request, clonedResponse);
+ });
+ }
+ return networkResponse;
+ })
+ .catch(() => {
+ // 네트워크 실패 시 오프라인 캐시 반환
+ return caches.match(event.request);
+ }),
+ );
+ return;
+ }
+
+ // 그 외 정적 에셋은 기존처럼 Cache First 전략
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request).catch(() => {
- // 네트워크 연결 실패 시 브라우저가 에러를 뿜지 않도록 503 에러 Response 반환
return new Response("Network Error", {
status: 503,
statusText: "Network Error",