diff --git a/app/_components/LoginActionSection.tsx b/app/_components/LoginActionSection.tsx index 9f52b71..3672799 100644 --- a/app/_components/LoginActionSection.tsx +++ b/app/_components/LoginActionSection.tsx @@ -5,7 +5,11 @@ import Link from "next/link"; export default function ScreenLoginActionSection() { const handleKakaoLogin = () => { - window.location.href = `${process.env.NEXT_PUBLIC_API_URL}/oauth2/authorization/kakao`; + const url = `${process.env.NEXT_PUBLIC_API_URL}/oauth2/authorization/kakao`; + console.log("๐ŸŸก ์นด์นด์˜ค ๋กœ๊ทธ์ธ URL:", url); + // ์™ธ๋ถ€ OAuth ์—”๋“œํฌ์ธํŠธ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ (Next.js ๋‚ด๋ถ€ ๊ฒฝ๋กœ ์•„๋‹˜) + // eslint-disable-next-line @next/next/no-location-assign-relative-destination + window.location.href = url; }; return ( diff --git a/app/roulette/_components/Roulette.tsx b/app/roulette/_components/Roulette.tsx index bdcd2b9..2c431d8 100644 --- a/app/roulette/_components/Roulette.tsx +++ b/app/roulette/_components/Roulette.tsx @@ -9,7 +9,7 @@ import React, { import { cn } from "@/lib/utils"; export interface RouletteHandle { - spin: () => void; + spin: (targetItemName?: string) => void; reset: () => void; } @@ -108,7 +108,7 @@ const Roulette = forwardRef( } }; - const spin = () => { + const spin = (targetItemName?: string) => { if (isSpinning) return; setIsSpinning(true); setTransitionDuration(7000); @@ -116,8 +116,14 @@ const Roulette = forwardRef( triggerHapticFeedback(); - // ๋‹น์ฒจ ์•„์ดํ…œ ๋žœ๋ค ์„ ํƒ - const resultIndex = Math.floor(Math.random() * items.length); + // ๋‹น์ฒจ ์•„์ดํ…œ ๋žœ๋ค ์„ ํƒ (API์—์„œ ํƒ€๊ฒŸ์ด ์˜ค๋ฉด ํ•ด๋‹น ํƒ€๊ฒŸ ๋งค์นญ) + let resultIndex = Math.floor(Math.random() * items.length); + if (targetItemName) { + const foundIndex = items.findIndex((i) => i.label === targetItemName); + if (foundIndex !== -1) { + resultIndex = foundIndex; + } + } const resultItem = items[resultIndex]; // 1์นธ๋‹น ๊ฐ๋„ ๊ณ„์‚ฐ diff --git a/app/roulette/_components/RouletteCards.tsx b/app/roulette/_components/RouletteCards.tsx index 1d03a50..4ddc14a 100644 --- a/app/roulette/_components/RouletteCards.tsx +++ b/app/roulette/_components/RouletteCards.tsx @@ -1,7 +1,7 @@ "use client"; import React from "react"; -import Link from "next/link"; +import { useRouter } from "next/navigation"; import Image from "next/image"; import { Check } from "lucide-react"; @@ -13,10 +13,17 @@ type FreeRouletteCardProps = { }; export const FreeRouletteCard = ({ - remainingChances = 1, + remainingChances = 0, }: FreeRouletteCardProps) => { + const router = useRouter(); + const hasChances = remainingChances > 0; + return ( -
+
{/* Upper Content */}
{/* Left Info */} @@ -32,13 +39,30 @@ export const FreeRouletteCard = ({ {/* Status Checkbox */}
-
- +
+
์˜ค๋Š˜ ๋‚จ์€ ํšŸ์ˆ˜
- + {remainingChances}ํšŒ @@ -61,12 +85,24 @@ export const FreeRouletteCard = ({
{/* Button */} - router.push("/roulette/free")} + className={`flex h-14 w-full items-center justify-center rounded-[16px] 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]" + }`} > - ๋ฌด๋ฃŒ ๋ฃฐ๋ › ์ž…์žฅ - + + {hasChances ? "๋ฌด๋ฃŒ ๋ฃฐ๋ › ์ž…์žฅ" : "์˜ค๋Š˜์€ ์ด๋ฏธ ์ฐธ์—ฌํ–ˆ์–ด์š”"} + +
); }; @@ -77,14 +113,24 @@ export const FreeRouletteCard = ({ type SpecialRouletteCardProps = { currentAmount?: number; targetAmount?: number; + isSpecialParticipated?: boolean; }; export const SpecialRouletteCard = ({ - currentAmount = 2000, + currentAmount = 0, targetAmount = 3000, + isSpecialParticipated = false, }: SpecialRouletteCardProps) => { + const router = useRouter(); + const hasChances = !isSpecialParticipated && currentAmount >= targetAmount; + const isDisabled = !hasChances; + return ( -
+
{/* Upper Content */}
{/* Left Info */} @@ -100,13 +146,30 @@ export const SpecialRouletteCard = ({ {/* Status Checkbox */}
-
- +
+
ํ˜„์žฌ ๋ˆ„์ 
- + {currentAmount.toLocaleString()}์› {" "} / {targetAmount.toLocaleString()}์› @@ -130,12 +193,24 @@ export const SpecialRouletteCard = ({
{/* Button */} - router.push("/roulette/special")} + className={`flex h-14 w-full items-center justify-center rounded-[16px] 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]" + }`} > - ์ŠคํŽ˜์…œ ๋ฃฐ๋ › ์ž…์žฅ - + + {isDisabled ? "์˜ค๋Š˜์€ ์ด๋ฏธ ์ฐธ์—ฌํ–ˆ์–ด์š”" : "์ŠคํŽ˜์…œ ๋ฃฐ๋ › ์ž…์žฅ"} + +
); }; diff --git a/app/roulette/_components/RouletteHeader.tsx b/app/roulette/_components/RouletteHeader.tsx index 6236338..7466584 100644 --- a/app/roulette/_components/RouletteHeader.tsx +++ b/app/roulette/_components/RouletteHeader.tsx @@ -1,17 +1,40 @@ "use client"; import { BackButton } from "@/components/ui/BackButton"; +import { useRouter, usePathname } from "next/navigation"; +import { safeBack } from "@/lib/safeBack"; import React from "react"; type RouletteHeaderProps = { title?: React.ReactNode; sidebar?: React.ReactNode; + onBack?: () => void; }; -const RouletteHeader = ({ title, sidebar }: RouletteHeaderProps) => { +const RouletteHeader = ({ title, sidebar, onBack }: RouletteHeaderProps) => { + const router = useRouter(); + const pathname = usePathname(); + + const handleBack = () => { + if (onBack) { + onBack(); + return; + } + + if (pathname === "/roulette") { + // ๋ฉ”์ธ ํ—ˆ๋ธŒ ํ™”๋ฉด(/roulette)์˜ fallback์€ ํ™ˆ("/") + // ํ—ˆ๋ธŒ๋Š” ๊ธฐ๋Œ€๋˜๋Š” ๋‹จ์ผ ๋ถ€๋ชจ ๊ฒฝ๋กœ๊ฐ€ ์—†์œผ๋ฏ€๋กœ (์–ด๋””์„œ๋“  ์ง„์ž… ๊ฐ€๋Šฅ), expectedParentPath๋ฅผ ์ƒ๋žตํ•ฉ๋‹ˆ๋‹ค. + safeBack(router, "/"); + } else { + // ํ•˜์œ„ ๋ฃฐ๋ › ํ™”๋ฉด(/roulette/free ๋“ฑ)์˜ fallback์€ ๋ฃฐ๋ › ๋ฉ”์ธ("/roulette") + // ๋’ค๋กœ๊ฐ€๊ธฐ๋ฅผ ๋ˆŒ๋ €์„ ๋•Œ ์ง์ „ ๋ชฉ์ ์ง€๊ฐ€ "/roulette"์ผ ๋•Œ๋งŒ ๋ธŒ๋ผ์šฐ์ € back()์„ ์‹คํ–‰ํ•˜๊ณ , ์•„๋‹ˆ๋ฉด replace() ์‹œํ‚ต๋‹ˆ๋‹ค. + safeBack(router, "/roulette", "/roulette"); + } + }; + return (
- + {title && (
diff --git a/app/roulette/_components/RouletteResultModal.tsx b/app/roulette/_components/RouletteResultModal.tsx index 75839a3..0b4b8a9 100644 --- a/app/roulette/_components/RouletteResultModal.tsx +++ b/app/roulette/_components/RouletteResultModal.tsx @@ -118,6 +118,7 @@ export default function RouletteResultModal({ src={imageSrc} alt={item.label} fill + priority className="object-contain drop-shadow-[0px_6.25px_25px_rgba(0,0,0,0.14)]" />
diff --git a/app/roulette/_components/ScreenRouletteMain.tsx b/app/roulette/_components/ScreenRouletteMain.tsx index b5b0cb2..ee48c1f 100644 --- a/app/roulette/_components/ScreenRouletteMain.tsx +++ b/app/roulette/_components/ScreenRouletteMain.tsx @@ -4,11 +4,14 @@ import React from "react"; import RouletteHeader from "./RouletteHeader"; import RouletteProbabilityBottomSheet from "./RouletteProbabilityBottomSheet"; import { FreeRouletteCard, SpecialRouletteCard } from "./RouletteCards"; +import { useRouletteStatus } from "@/hooks/useRouletteStatus"; /** * ๋ฃฐ๋ › ๋ฉ”์ธ ํ™”๋ฉด (ํ—ˆ๋ธŒ) ์ปดํฌ๋„ŒํŠธ */ const ScreenRouletteMain = () => { + const { data: rouletteStatus } = useRouletteStatus(); + return (
{/* Roulette Header */} @@ -41,8 +44,16 @@ const ScreenRouletteMain = () => { {/* Cards */}
- - + +
diff --git a/app/roulette/free/_components/ScreenRouletteFree.tsx b/app/roulette/free/_components/ScreenRouletteFree.tsx index b29516e..1412f80 100644 --- a/app/roulette/free/_components/ScreenRouletteFree.tsx +++ b/app/roulette/free/_components/ScreenRouletteFree.tsx @@ -1,5 +1,6 @@ "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"; @@ -9,13 +10,16 @@ import Roulette, { } from "../../_components/Roulette"; import RouletteProbabilityBottomSheet from "../../_components/RouletteProbabilityBottomSheet"; import RouletteResultModal from "../../_components/RouletteResultModal"; - -// TODO: ์‹ค์ œ API ์—ฐ๋™ ์‹œ ๋Œ€์ฒด -const MOCK_REMAINING_CHANCES = 1; +import { useRouletteStatus } from "@/hooks/useRouletteStatus"; +import { useSpinRoulette } from "@/hooks/useSpinRoulette"; const ScreenRouletteFree = () => { - const remainingChances = MOCK_REMAINING_CHANCES; - const hasChances = remainingChances > 0; + const router = useRouter(); + const { data: rouletteStatus, isLoading } = useRouletteStatus(); + + // isFreeParticipated: true = ์•„์ง ์ฐธ์—ฌ ์•ˆ ํ•จ(1ํšŒ ๋‚จ์Œ), false = ์ด๋ฏธ ์ฐธ์—ฌํ•จ(0ํšŒ) + const hasChances = rouletteStatus?.isFreeParticipated ?? false; + const remainingChances = hasChances ? 1 : 0; const rouletteRef = useRef(null); const [isSpinning, setIsSpinning] = useState(false); @@ -23,9 +27,17 @@ const ScreenRouletteFree = () => { const [isModalOpen, setIsModalOpen] = useState(false); const [isProbabilityOpen, setIsProbabilityOpen] = useState(false); + const { mutate: spinRoulette, isPending } = useSpinRoulette("FREE"); + const handleSpin = () => { - if (!hasChances || isSpinning) return; - rouletteRef.current?.spin(); + if (!hasChances || isSpinning || isPending) return; + + spinRoulette(undefined, { + onSuccess: (res) => { + // ์„œ๋ฒ„์—์„œ ์„ฑ๊ณต ์‘๋‹ต์ด ์˜ค๋ฉด ๋ฐ˜ํ™˜๋œ rewardName์„ ํƒ€๊ฒŸ์œผ๋กœ ๋ฃฐ๋ › ํšŒ์ „ ์‹œ์ž‘ + rouletteRef.current?.spin(res.rewardName); + }, + }); }; return ( @@ -87,11 +99,17 @@ const ScreenRouletteFree = () => { {/* Bottom Group: Spin Button + Notice */}
{/* Participation notice */} diff --git a/app/roulette/free/page.tsx b/app/roulette/free/page.tsx index 495aa16..965a6c9 100644 --- a/app/roulette/free/page.tsx +++ b/app/roulette/free/page.tsx @@ -1,10 +1,41 @@ import ScreenRouletteFree from "./_components/ScreenRouletteFree"; +import { serverApi } from "@/lib/server-api"; +import { + dehydrate, + HydrationBoundary, + QueryClient, +} from "@tanstack/react-query"; +import { RoulettePageResponse } from "@/hooks/useRouletteStatus"; export const metadata = { title: "์ด๋ฒคํŠธ ๋ฃฐ๋ ›", description: "์ด๋ฒคํŠธ ๋ฃฐ๋ › ์ง„ํ–‰", }; -export default function RoulettePage() { - return ; +interface RoulettePageApiResponse { + code: string; + status: number; + message: string; + data: RoulettePageResponse; +} + +export default async function RoulettePage() { + const queryClient = new QueryClient(); + + await queryClient.prefetchQuery({ + queryKey: ["rouletteStatus"], + queryFn: async () => { + const res = await serverApi.get({ + path: "/api/items/roulette", + }); + console.log("๐ŸŽฐ [SSR-Free] GET /api/items/roulette ์‘๋‹ต:", res.data); + return res.data.data; + }, + }); + + return ( + + + + ); } diff --git a/app/roulette/page.tsx b/app/roulette/page.tsx index 4be33e4..26bdfbb 100644 --- a/app/roulette/page.tsx +++ b/app/roulette/page.tsx @@ -1,10 +1,41 @@ import ScreenRouletteMain from "./_components/ScreenRouletteMain"; +import { serverApi } from "@/lib/server-api"; +import { + dehydrate, + HydrationBoundary, + QueryClient, +} from "@tanstack/react-query"; +import { RoulettePageResponse } from "@/hooks/useRouletteStatus"; export const metadata = { title: "๋ฃฐ๋ ›", description: "๋ฃฐ๋ › ์ด๋ฒคํŠธ", }; -export default function RoulettePage() { - return ; +interface RoulettePageApiResponse { + code: string; + status: number; + message: string; + data: RoulettePageResponse; +} + +export default async function RoulettePage() { + const queryClient = new QueryClient(); + + await queryClient.prefetchQuery({ + queryKey: ["rouletteStatus"], + queryFn: async () => { + const res = await serverApi.get({ + path: "/api/items/roulette", + }); + console.log("๐ŸŽฐ [SSR-Main] GET /api/items/roulette ์‘๋‹ต:", res.data); + return res.data.data; + }, + }); + + return ( + + + + ); } diff --git a/app/roulette/special/[amount]/page.tsx b/app/roulette/special/[amount]/page.tsx index df747ed..e75a419 100644 --- a/app/roulette/special/[amount]/page.tsx +++ b/app/roulette/special/[amount]/page.tsx @@ -5,6 +5,7 @@ import Image from "next/image"; import { useRouter, redirect } from "next/navigation"; import { X, Info } from "lucide-react"; import Button from "@/components/ui/Button"; +import { safeBack } from "@/lib/safeBack"; export default function SpecialAmountPage({ params, @@ -40,23 +41,26 @@ export default function SpecialAmountPage({
{/* ๋‹ซ๊ธฐ ๋ฒ„ํŠผ */} -
- -
+
{/* ์ถ•ํ•˜ ๋ฉ”์‹œ์ง€ */} -

- ๋‹น์ฒจ์„ ์ถ•ํ•˜ํ•ด์š”! +

+ ํŠน๋ณ„ ๋ณด์ƒ์—
+ ๋‹น์ฒจ๋์–ด์š”!

{/* ๋‹น์ฒจ ๊ธˆ์•ก */} -

+

{displayAmount}์›

@@ -64,9 +68,7 @@ export default function SpecialAmountPage({
- - ์ƒํ’ˆ๊ถŒ - + ์ƒํ’ˆ๊ถŒ
@@ -86,36 +88,34 @@ export default function SpecialAmountPage({ {/* Way 1 */}
-
- - ๋ฐฉ๋ฒ• 1 +
+ + ์ˆ˜๋ น ๋ฐฉ๋ฒ• 1
- - ๋งˆ์ดํŽ˜์ด์ง€ ํ™•์ธ + + ๋ถ€์Šค์—์„œ ์ธ์ฆํ•˜๊ธฐ
-

- ๋งˆ์ดํŽ˜์ด์ง€ - ๋‚ด ์•„์ดํ…œ์—์„œ ์ƒํ’ˆ๊ถŒ์„ ํ™•์ธํ•˜์„ธ์š”. +

+ ์ฝ”๋งˆ ๋ถ€์Šค์— ๋ฐฉ๋ฌธํ•˜์—ฌ ํ™”๋ฉด๊ณผ ๋‹‰๋„ค์ž„์„ ๋ณด์—ฌ์ฃผ์„ธ์š”

{/* Way 2 */}
-
- - ๋ฐฉ๋ฒ• 2 +
+ + ์ˆ˜๋ น ๋ฐฉ๋ฒ• 2
- - ํ˜„์žฅ ๋ฐ์Šคํฌ ๊ตํ™˜ + + ์ธ์Šคํƒ€๊ทธ๋žจ ์ธ์ฆ
-

- { - "์ถ•์ œ ๊ธฐ๊ฐ„ ์ค‘ ์ดํ•™์ƒํšŒ ๋ถ€์Šค์— ๋ฐฉ๋ฌธํ•˜์—ฌ\n๋‹น์ฒจ ํ™”๋ฉด์„ ๋ณด์—ฌ์ฃผ๊ณ  ์‹ค๋ฌผ ์ƒํ’ˆ๊ถŒ์œผ๋กœ ๊ตํ™˜ํ•˜์„ธ์š”." - } +

+ {"์ฝ”๋งˆ ๊ณต์‹ ์ธ์Šคํƒ€๊ทธ๋žจ DM์œผ๋กœ \nํ™”๋ฉด๊ณผ ๋‹‰๋„ค์ž„์„ ๋ณด๋‚ด์ฃผ์„ธ์š”"}

@@ -125,11 +125,17 @@ export default function SpecialAmountPage({ {/* ํ•˜๋‹จ ์˜์—ญ (๋ฒ„ํŠผ + ์œ ์˜์‚ฌํ•ญ) */}
- +
- - - ๊ฒฐ์ œ ์ทจ์†Œ ์‹œ ์ง€๊ธ‰๋œ ๋ณด์ƒ์ด ํšŒ์ˆ˜๋  ์ˆ˜ ์žˆ์–ด์š” + + + ์ค€๋น„ ์ˆ˜๋Ÿ‰ ์†Œ์ง„ ์‹œ ๋™์ผ ๊ฐ€์น˜ ์ด์ƒ์˜ ์•„์ดํ…œ์œผ๋กœ ๋Œ€์ฒด๋  ์ˆ˜ ์žˆ์–ด์š”
diff --git a/app/roulette/special/_components/ScreenRouletteSpecial.tsx b/app/roulette/special/_components/ScreenRouletteSpecial.tsx index ded02f5..81dbf1a 100644 --- a/app/roulette/special/_components/ScreenRouletteSpecial.tsx +++ b/app/roulette/special/_components/ScreenRouletteSpecial.tsx @@ -11,14 +11,23 @@ import Roulette, { import RouletteProbabilityBottomSheet from "../../_components/RouletteProbabilityBottomSheet"; import SpecialRouletteChanceCard from "./SpecialRouletteChanceCard"; import RouletteResultModal from "../../_components/RouletteResultModal"; +import { useRouletteStatus } from "@/hooks/useRouletteStatus"; +import { useSpinRoulette } from "@/hooks/useSpinRoulette"; -// TODO: ์‹ค์ œ API ์—ฐ๋™ ์‹œ ๋Œ€์ฒด -const MOCK_REMAINING_CHANCES = 1; +// ์ŠคํŽ˜์…œ ๋ฃฐ๋ › ์ฐธ์—ฌ ๊ธฐ์ค€ ๊ธˆ์•ก (๋ˆ„์  ๊ฒฐ์ œ 3,000์› ์ด์ƒ) +const SPECIAL_TARGET_AMOUNT = 3000; const ScreenRouletteSpecial = () => { const router = useRouter(); - const remainingChances = MOCK_REMAINING_CHANCES; - const hasChances = remainingChances > 0; + const { data: rouletteStatus, isLoading } = useRouletteStatus(); + const { mutate: spinRoulette, isPending } = useSpinRoulette("SPECIAL"); + + // ์˜ค๋Š˜ ์ด๋ฏธ ์ฐธ์—ฌํ–ˆ์œผ๋ฉด(true) ๋” ์ด์ƒ ๋Œ๋ฆด ์ˆ˜ ์—†์Œ + // ์ŠคํŽ˜์…œ ๋ฃฐ๋ ›์€ isSpecialParticipated๊ฐ€ false์ด๊ณ  ๋ˆ„์  ๊ฒฐ์ œ๊ธˆ์ด ๊ธฐ์ค€ ์ด์ƒ์ด์–ด์•ผ ์ฐธ์—ฌ ๊ฐ€๋Šฅ + const totalPay = rouletteStatus?.totalPay ?? 0; + const isSpecialParticipated = rouletteStatus?.isSpecialParticipated ?? true; + const hasChances = + !isSpecialParticipated && totalPay >= SPECIAL_TARGET_AMOUNT; const rouletteRef = useRef(null); const [isSpinning, setIsSpinning] = useState(false); @@ -27,8 +36,14 @@ const ScreenRouletteSpecial = () => { const [isProbabilityOpen, setIsProbabilityOpen] = useState(false); const handleSpin = () => { - if (!hasChances || isSpinning) return; - rouletteRef.current?.spin(); + if (!hasChances || isSpinning || isPending) return; + + spinRoulette(undefined, { + onSuccess: (res) => { + // ์„œ๋ฒ„์—์„œ ์„ฑ๊ณต ์‘๋‹ต์ด ์˜ค๋ฉด ๋ฐ˜ํ™˜๋œ rewardName์„ ํƒ€๊ฒŸ์œผ๋กœ ๋ฃฐ๋ › ํšŒ์ „ ์‹œ์ž‘ + rouletteRef.current?.spin(res.rewardName); + }, + }); }; return ( @@ -56,7 +71,10 @@ const ScreenRouletteSpecial = () => { {/* Special Remaining chances card */}
- +
@@ -82,11 +100,19 @@ const ScreenRouletteSpecial = () => { {/* Bottom Group: Spin Button + Notice */}
{/* Participation notice */} diff --git a/app/roulette/special/page.tsx b/app/roulette/special/page.tsx index 8463166..d348b0e 100644 --- a/app/roulette/special/page.tsx +++ b/app/roulette/special/page.tsx @@ -1,10 +1,41 @@ import ScreenRouletteSpecial from "./_components/ScreenRouletteSpecial"; +import { serverApi } from "@/lib/server-api"; +import { + dehydrate, + HydrationBoundary, + QueryClient, +} from "@tanstack/react-query"; +import { RoulettePageResponse } from "@/hooks/useRouletteStatus"; export const metadata = { title: "์ŠคํŽ˜์…œ ๋ฃฐ๋ ›", description: "์ŠคํŽ˜์…œ ๋ฃฐ๋ › ์ง„ํ–‰", }; -export default function SpecialRoulettePage() { - return ; +interface RoulettePageApiResponse { + code: string; + status: number; + message: string; + data: RoulettePageResponse; +} + +export default async function SpecialRoulettePage() { + const queryClient = new QueryClient(); + + await queryClient.prefetchQuery({ + queryKey: ["rouletteStatus"], + queryFn: async () => { + const res = await serverApi.get({ + path: "/api/items/roulette", + }); + console.log("๐ŸŽฐ [SSR-Special] GET /api/items/roulette ์‘๋‹ต:", res.data); + return res.data.data; + }, + }); + + return ( + + + + ); } diff --git a/hooks/admin/useAdminOrders.ts b/hooks/admin/useAdminOrders.ts index 92ab6c3..3f79b3d 100644 --- a/hooks/admin/useAdminOrders.ts +++ b/hooks/admin/useAdminOrders.ts @@ -64,8 +64,11 @@ export const useApproveOrder = () => { return useMutation({ mutationFn: (requestId: number) => approveOrder(requestId), - onSuccess: () => { + onSuccess: (data) => { + console.log("โœ… [Admin] ๊ฒฐ์ œ ์Šน์ธ ์™„๋ฃŒ ์‘๋‹ต:", data); queryClient.invalidateQueries({ queryKey: ["adminOrders"] }); + // ๊ด€๋ฆฌ์ž๊ฐ€ ์Šน์ธํ–ˆ์„ ๋•Œ ๊ด€๋ จ ๋ฃฐ๋ › ์บ์‹œ(๋ˆ„์  ๊ฒฐ์ œ๊ธˆ์•ก ๋“ฑ)๋„ ์ดˆ๊ธฐํ™” + queryClient.invalidateQueries({ queryKey: ["rouletteStatus"] }); }, onError: (error: AxiosError<{ code: string; message: string }>) => { const errorData = error.response?.data; @@ -80,8 +83,11 @@ export const useRejectOrder = () => { return useMutation({ mutationFn: (requestId: number) => rejectOrder(requestId), - onSuccess: () => { + onSuccess: (data) => { + console.log("๐Ÿšซ [Admin] ๊ฒฐ์ œ ๊ฑฐ์ ˆ ์™„๋ฃŒ ์‘๋‹ต:", data); queryClient.invalidateQueries({ queryKey: ["adminOrders"] }); + // ๊ด€๋ฆฌ์ž๊ฐ€ ๊ฑฐ์ ˆ(๋˜๋Š” ์ทจ์†Œ)ํ–ˆ์„ ๋•Œ๋„ ๊ด€๋ จ ๋ฃฐ๋ › ์บ์‹œ ์ดˆ๊ธฐํ™” + queryClient.invalidateQueries({ queryKey: ["rouletteStatus"] }); }, onError: (error: AxiosError<{ code: string; message: string }>) => { const errorData = error.response?.data; diff --git a/hooks/useRouletteStatus.ts b/hooks/useRouletteStatus.ts new file mode 100644 index 0000000..6ba9b04 --- /dev/null +++ b/hooks/useRouletteStatus.ts @@ -0,0 +1,36 @@ +import { api } from "@/lib/axios"; +import { useQuery } from "@tanstack/react-query"; + +/* โ”€โ”€ ์‘๋‹ต ํƒ€์ž… โ”€โ”€ */ +export interface RoulettePageResponse { + isFreeParticipated: boolean; + isSpecialParticipated: boolean; + totalPay: number; +} + +interface RoulettePageApiResponse { + code: string; + status: number; + message: string; + data: RoulettePageResponse; +} + +/* โ”€โ”€ fetcher โ”€โ”€ */ +export const fetchRouletteStatus = async (): Promise => { + const { data } = await api.get( + "/api/items/roulette", + ); + console.log("๐ŸŽฐ [CSR] GET /api/items/roulette ์‘๋‹ต:", data); + return data.data; +}; + +/* โ”€โ”€ hook โ”€โ”€ */ +export const useRouletteStatus = () => { + return useQuery({ + queryKey: ["rouletteStatus"], + queryFn: fetchRouletteStatus, + // ์˜ค๋Š˜ ์ฐธ์—ฌ ์—ฌ๋ถ€๋Š” ํ•˜๋ฃจ ๋‹จ์œ„๋กœ ์ดˆ๊ธฐํ™”๋˜๋ฏ€๋กœ, ํŽ˜์ด์ง€ ์ง„์ž… ์‹œ ํ•ญ์ƒ ์ตœ์‹ ๊ฐ’์„ ๊ฐ€์ ธ์˜ด + staleTime: 0, + gcTime: 1000 * 60 * 5, // 5๋ถ„ + }); +}; diff --git a/hooks/useSpinRoulette.ts b/hooks/useSpinRoulette.ts new file mode 100644 index 0000000..42c2db8 --- /dev/null +++ b/hooks/useSpinRoulette.ts @@ -0,0 +1,62 @@ +import { api } from "@/lib/axios"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useToastStore } from "@/stores/toast-store"; +import { isAxiosError } from "axios"; + +export interface RouletteSpinResponse { + rewardName: string; +} + +export interface RouletteSpinApiResponse { + code: string; + status: number; + message: string; + data: RouletteSpinResponse; +} + +export type RouletteType = "FREE" | "SPECIAL"; + +export const useSpinRoulette = (rouletteType: RouletteType) => { + const queryClient = useQueryClient(); + const showToast = useToastStore((state) => state.showToast); + + return useMutation({ + mutationFn: async () => { + const { data } = await api.post( + `/api/items/roulette/${rouletteType}/spins`, + ); + return data.data; + }, + onSuccess: () => { + // ์ฐธ์—ฌ ์ด๋ ฅ ๋ฐ ๊ฒฐ์ œ ๊ธˆ์•ก ์ƒํƒœ ๋™๊ธฐํ™”๋ฅผ ์œ„ํ•ด ๋ฌดํšจํ™” + queryClient.invalidateQueries({ queryKey: ["rouletteStatus"] }); + }, + onError: (error) => { + if (isAxiosError(error) && error.response) { + const { code, message } = error.response.data; + if (code === "ITEM-007") { + showToast({ + title: "๋ฃฐ๋ › ์ฐธ์—ฌ ๋ถˆ๊ฐ€", + body: "์ด๋ฏธ ๋ฃฐ๋ ›์— ์ฐธ์—ฌํ•˜์…จ์Šต๋‹ˆ๋‹ค", + }); + } else if (code === "ITEM-008") { + showToast({ + title: "์ŠคํŽ˜์…œ ๋ฃฐ๋ › ์ฐธ์—ฌ ๋ถˆ๊ฐ€", + body: "์ŠคํŽ˜์…œ ๋ฃฐ๋ › ์ฐธ์—ฌ๋ฅผ ์œ„ํ•œ ์˜ค๋Š˜์˜ ๋ˆ„์  ๊ฒฐ์ œ ๊ธˆ์•ก์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.", + }); + } else { + showToast({ + title: "์˜ค๋ฅ˜", + body: + message || "๋ฃฐ๋ › ์‹คํ–‰์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•ด์ฃผ์„ธ์š”.", + }); + } + } else { + showToast({ + title: "์˜ค๋ฅ˜", + body: "์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } + }, + }); +}; diff --git a/lib/firebase.ts b/lib/firebase.ts index 0afb318..f44a460 100644 --- a/lib/firebase.ts +++ b/lib/firebase.ts @@ -20,10 +20,21 @@ export const firebaseConfig = { const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]; -// Analytics (๋ธŒ๋ผ์šฐ์ €์—์„œ๋งŒ ์‹คํ–‰) +// Analytics (๋ธŒ๋ผ์šฐ์ €์—์„œ๋งŒ ์‹คํ–‰, ์œ ํšจํ•œ ์„ค์ •์ด ์žˆ์„ ๋•Œ๋งŒ) let analytics; -if (typeof window !== "undefined") { - analytics = getAnalytics(app); +if ( + typeof window !== "undefined" && + firebaseConfig.projectId && + !firebaseConfig.projectId.includes("your_") +) { + try { + analytics = getAnalytics(app); + } catch (e) { + console.warn( + "[Firebase] Analytics ์ดˆ๊ธฐํ™” ์‹คํŒจ (๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฌด์‹œ๋ฉ๋‹ˆ๋‹ค):", + e, + ); + } } // ์„œ๋น„์Šค ์›Œ์ปค ๋“ฑ๋ก ๋ฐ FCM ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ (ํ† ํฐ๋งŒ ๋ฐ˜ํ™˜, ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก ์•ˆ ํ•จ) diff --git a/lib/safeBack.ts b/lib/safeBack.ts new file mode 100644 index 0000000..a4f4ac9 --- /dev/null +++ b/lib/safeBack.ts @@ -0,0 +1,108 @@ +import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime"; + +/** + * ๋ธŒ๋ผ์šฐ์ €์˜ ์•ž/๋’ค๋กœ๊ฐ€๊ธฐ ์ด๋ฒคํŠธ๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ์ถ”์ ํ•˜๊ธฐ ์œ„ํ•ด + * window.history.state ๋‚ด๋ถ€์— ๊ณ ์œ  ์ธ๋ฑ์Šค(__idx)๋ฅผ ์ฃผ์ž…ํ•˜๊ณ , + * ์„ธ์…˜ ์Šคํ† ๋ฆฌ์ง€์— ํ•ด๋‹น ์ธ๋ฑ์Šค๋ณ„ ๊ฒฝ๋กœ๋ฅผ ๋งคํ•‘ํ•ฉ๋‹ˆ๋‹ค. + */ +if (typeof window !== "undefined") { + // ์ดˆ๊ธฐ ์ธ๋ฑ์Šค ๋ถ€์—ฌ + const getIdx = () => window.history.state?.__idx ?? Date.now(); + + if (window.history.state && typeof window.history.state === "object") { + if (window.history.state.__idx === undefined) { + window.history.replaceState( + { ...window.history.state, __idx: getIdx() }, + "", + ); + } + } else { + window.history.replaceState({ __idx: getIdx() }, ""); + } + + const saveUrl = (idx: number, url: string) => { + try { + const parsed = new URL(url, window.location.origin); + sessionStorage.setItem(`hist_${idx}`, parsed.pathname); + } catch { + sessionStorage.setItem(`hist_${idx}`, url); + } + }; + + // ํ˜„์žฌ ๊ฒฝ๋กœ ์ €์žฅ + saveUrl(getIdx(), window.location.pathname); + + // pushState ์˜ค๋ฒ„๋ผ์ด๋“œ: ์ƒˆ ์ธ๋ฑ์Šค ๋ถ€์—ฌ ๋ฐ ์ €์žฅ + const originalPushState = window.history.pushState; + window.history.pushState = function (data, unused, url) { + const currentIdx = getIdx(); + const nextIdx = currentIdx + 1; + let newData = data; + if (typeof data === "object" && data !== null) { + newData = { ...data, __idx: nextIdx }; + } else if (data === undefined || data === null) { + newData = { __idx: nextIdx }; + } + + if (url) { + saveUrl(nextIdx, url.toString()); + } + return originalPushState.call(this, newData, unused, url); + }; + + // replaceState ์˜ค๋ฒ„๋ผ์ด๋“œ: ํ˜„์žฌ ์ธ๋ฑ์Šค ์œ ์ง€ํ•˜๋ฉฐ ๊ฒฝ๋กœ ๊ฐฑ์‹  + const originalReplaceState = window.history.replaceState; + window.history.replaceState = function (data, unused, url) { + const currentIdx = getIdx(); + let newData = data; + if (typeof data === "object" && data !== null) { + newData = { ...data, __idx: currentIdx }; + } else if (data === undefined || data === null) { + newData = { __idx: currentIdx }; + } + + if (url) { + saveUrl(currentIdx, url.toString()); + } + return originalReplaceState.call(this, newData, unused, url); + }; +} + +/** + * ์•ˆ์ „ํ•œ ๋’ค๋กœ๊ฐ€๊ธฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ + * + * ๋ธŒ๋ผ์šฐ์ € ํžˆ์Šคํ† ๋ฆฌ ์ƒํƒœ(__idx)๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ง์ „ ํŽ˜์ด์ง€์˜ ์ •ํ™•ํ•œ ๊ฒฝ๋กœ๋ฅผ ์•Œ์•„๋ƒ…๋‹ˆ๋‹ค. + * ์•ž์œผ๋กœ/๋’ค๋กœ๊ฐ€๊ธฐ ๋“ฑ ์–ด๋– ํ•œ ์กฐ์ž‘์—๋„ ์ธ๋ฑ์Šค๊ฐ€ ์œ ์ง€๋˜๋ฏ€๋กœ ์‹ค์ œ ๋ฐฉ๋ฌธํ•œ ๋ถ€๋ชจ ๊ฒฝ๋กœ๋ฅผ ์ •ํ™•ํžˆ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ +export function safeBack( + router: AppRouterInstance, + fallbackPath: string, + expectedParentPath?: string, +) { + if (typeof window === "undefined") return; + + const currentIdx = window.history.state?.__idx; + if (currentIdx === undefined) { + router.replace(fallbackPath); + return; + } + + // ์ง์ „ ์ธ๋ฑ์Šค์— ๋งคํ•‘๋œ ๊ฒฝ๋กœ ๊ฐ€์ ธ์˜ค๊ธฐ + const previousPath = sessionStorage.getItem(`hist_${currentIdx - 1}`); + + if (expectedParentPath) { + // ์ง์ „ ๊ฒฝ๋กœ๊ฐ€ ์šฐ๋ฆฌ๊ฐ€ ๊ธฐ๋Œ€ํ•˜๋Š” ๋ถ€๋ชจ ๊ฒฝ๋กœ์™€ ์ผ์น˜ํ•  ๋•Œ๋งŒ back() + if (previousPath && previousPath.includes(expectedParentPath)) { + router.back(); + } else { + router.replace(fallbackPath); + } + } else { + // ๊ธฐ๋Œ€ ๋ถ€๋ชจ๊ฐ€ ๋ช…์‹œ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ (์˜ˆ: ๋ฉ”์ธ ํ—ˆ๋ธŒ) + if (previousPath) { + router.back(); + } else { + router.replace(fallbackPath); + } + } +}