Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/_components/LoginActionSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
14 changes: 10 additions & 4 deletions app/roulette/_components/Roulette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import React, {
import { cn } from "@/lib/utils";

export interface RouletteHandle {
spin: () => void;
spin: (targetItemName?: string) => void;
reset: () => void;
}

Expand Down Expand Up @@ -108,16 +108,22 @@ const Roulette = forwardRef<RouletteHandle, RouletteProps>(
}
};

const spin = () => {
const spin = (targetItemName?: string) => {
if (isSpinning) return;
setIsSpinning(true);
setTransitionDuration(7000);
onSpinChange?.(true);

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칸당 각도 계산
Expand Down
117 changes: 96 additions & 21 deletions app/roulette/_components/RouletteCards.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -13,10 +13,17 @@ type FreeRouletteCardProps = {
};

export const FreeRouletteCard = ({
remainingChances = 1,
remainingChances = 0,
}: FreeRouletteCardProps) => {
const router = useRouter();
const hasChances = remainingChances > 0;

return (
<div className="relative flex w-full flex-col justify-between gap-4 rounded-[24px] border border-white/30 bg-white/80 p-6 shadow-sm backdrop-blur-[15px]">
<div
className={`relative flex w-full flex-col justify-between gap-4 rounded-[24px] border border-white/30 bg-white/80 p-6 shadow-sm backdrop-blur-[15px] transition-opacity ${
!hasChances ? "opacity-50" : ""
}`}
>
{/* Upper Content */}
<div className="flex w-full items-center justify-between gap-2">
{/* Left Info */}
Expand All @@ -32,13 +39,30 @@ export const FreeRouletteCard = ({

{/* Status Checkbox */}
<div className="flex items-center gap-2">
<div className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-[#FF775E] via-[#FF4D61] to-[#E83ABC]">
<Check size={12} className="stroke-[3] text-white" />
<div
className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full ${
hasChances
? "from-color-brand-primary-orange via-color-brand-primary-flame to-color-brand-primary-pink bg-gradient-to-br"
: "bg-[#E5E5E5]"
}`}
>
<Check
size={12}
className={`stroke-[2] ${
hasChances ? "text-white" : "text-[#B3B3B3]"
}`}
/>
</div>
<span className="typo-14-500 leading-[20px] text-[#858585]">
오늘 남은 횟수
<br />
<span className="text-color-brand-primary-flame">
<span
className={
hasChances
? "text-color-brand-primary-flame"
: "text-[#858585]"
}
>
{remainingChances}회
</span>
</span>
Expand All @@ -61,12 +85,24 @@ export const FreeRouletteCard = ({
</div>

{/* Button */}
<Link
href="/roulette/free"
className="flex h-14 w-full items-center justify-center rounded-[16px] border border-white/30 bg-[#FF4D61] transition-transform hover:opacity-95 active:scale-[0.98]"
<button
type="button"
disabled={!hasChances}
onClick={() => 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]"
}`}
>
<span className="typo-20-600 text-white">무료 룰렛 입장</span>
</Link>
<span
className={`typo-20-600 ${
hasChances ? "text-white" : "text-[#B3B3B3]"
}`}
>
{hasChances ? "무료 룰렛 입장" : "오늘은 이미 참여했어요"}
</span>
</button>
</div>
);
};
Expand All @@ -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 (
<div className="relative flex w-full flex-col justify-between gap-4 rounded-[24px] border border-white/30 bg-white/80 p-6 shadow-sm backdrop-blur-[15px]">
<div
className={`relative flex w-full flex-col justify-between gap-4 rounded-[24px] border border-white/30 bg-white/80 p-6 shadow-sm backdrop-blur-[15px] transition-opacity ${
isDisabled ? "opacity-50" : ""
}`}
>
{/* Upper Content */}
<div className="flex w-full items-center justify-between gap-2">
{/* Left Info */}
Expand All @@ -100,13 +146,30 @@ export const SpecialRouletteCard = ({

{/* Status Checkbox */}
<div className="flex items-center gap-2">
<div className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-[#E5E5E5]">
<Check size={12} className="stroke-[3] text-[#B3B3B3]" />
<div
className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full ${
hasChances
? "from-color-brand-primary-orange via-color-brand-primary-flame to-color-brand-primary-pink bg-gradient-to-br"
: "bg-[#E5E5E5]"
}`}
>
<Check
size={12}
className={`stroke-[2] ${
hasChances ? "text-white" : "text-[#B3B3B3]"
}`}
/>
</div>
<span className="typo-14-500 leading-[20px] text-[#858585]">
현재 누적
<br />
<span className="text-color-brand-primary-flame">
<span
className={
hasChances
? "text-color-brand-primary-flame"
: "text-[#858585]"
}
>
{currentAmount.toLocaleString()}원
</span>{" "}
/ {targetAmount.toLocaleString()}원
Expand All @@ -130,12 +193,24 @@ export const SpecialRouletteCard = ({
</div>

{/* Button */}
<Link
href="/roulette/special"
className="flex h-14 w-full items-center justify-center rounded-[16px] border border-white/30 bg-gradient-to-r from-[#FB5E53] to-[#E53BAE] transition-transform hover:opacity-95 active:scale-[0.98]"
<button
type="button"
disabled={isDisabled}
onClick={() => 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]"
}`}
>
<span className="typo-20-600 text-white">스페셜 룰렛 입장</span>
</Link>
<span
className={`typo-20-600 ${
isDisabled ? "text-[#B3B3B3]" : "text-white"
}`}
>
{isDisabled ? "오늘은 이미 참여했어요" : "스페셜 룰렛 입장"}
</span>
</button>
</div>
);
};
27 changes: 25 additions & 2 deletions app/roulette/_components/RouletteHeader.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<header className="flex h-[64px] w-full items-center justify-between py-2">
<BackButton className="shrink-0" />
<BackButton className="shrink-0" onClick={handleBack} />

{title && (
<div className="flex flex-1 justify-center text-center">
Expand Down
1 change: 1 addition & 0 deletions app/roulette/_components/RouletteResultModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)]"
/>
</div>
Expand Down
15 changes: 13 additions & 2 deletions app/roulette/_components/ScreenRouletteMain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="relative flex min-h-screen w-full flex-col overflow-hidden px-4 pt-3 pb-8">
{/* Roulette Header */}
Expand Down Expand Up @@ -41,8 +44,16 @@ const ScreenRouletteMain = () => {

{/* Cards */}
<div className="mt-6 flex w-full flex-col items-center gap-4">
<FreeRouletteCard />
<SpecialRouletteCard />
<FreeRouletteCard
remainingChances={rouletteStatus?.isFreeParticipated ? 1 : 0}
/>
<SpecialRouletteCard
currentAmount={rouletteStatus?.totalPay ?? 0}
targetAmount={3000}
isSpecialParticipated={
rouletteStatus?.isSpecialParticipated ?? false
}
/>
</div>
</div>
</div>
Expand Down
36 changes: 27 additions & 9 deletions app/roulette/free/_components/ScreenRouletteFree.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -9,23 +10,34 @@ 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<RouletteHandle>(null);
const [isSpinning, setIsSpinning] = useState(false);
const [resultItem, setResultItem] = useState<RouletteItem | null>(null);
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 (
Expand Down Expand Up @@ -87,11 +99,17 @@ const ScreenRouletteFree = () => {
{/* Bottom Group: Spin Button + Notice */}
<div className="flex w-full flex-col items-center gap-3">
<Button
disabled={!hasChances || isSpinning}
disabled={isLoading || !hasChances || isSpinning || isPending}
onClick={handleSpin}
className="typo-20-600 bg-button-primary w-full py-4"
>
{isSpinning ? "돌아가는 중..." : "무료로 룰렛 돌리기"}
{isLoading || isPending
? "확인 중..."
: isSpinning
? "돌아가는 중..."
: hasChances
? "무료로 룰렛 돌리기"
: "오늘은 이미 참여했어요"}
</Button>

{/* Participation notice */}
Expand Down
Loading
Loading