diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index da82ce0..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[javascriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[json]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[css]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "css.lint.unknownAtRules": "ignore" -} diff --git a/app/roulette/free/_components/MyTicketSection.tsx b/app/roulette/_components/MyTicketSection.tsx similarity index 100% rename from app/roulette/free/_components/MyTicketSection.tsx rename to app/roulette/_components/MyTicketSection.tsx diff --git a/app/roulette/free/_components/Roulette.tsx b/app/roulette/_components/Roulette.tsx similarity index 94% rename from app/roulette/free/_components/Roulette.tsx rename to app/roulette/_components/Roulette.tsx index bf5660a..bcaac29 100644 --- a/app/roulette/free/_components/Roulette.tsx +++ b/app/roulette/_components/Roulette.tsx @@ -149,9 +149,9 @@ const Roulette = forwardRef( {/* 고정된 그림자: 회전하지 않음 */}
- {/* 룰렛 이미지만 회전 */} + {/* 룰렛 이미지만 회전 (rounded-full과 will-change 추가로 회전 시 네 모서리 돌출 방지) */}
( priority fill sizes="(max-width: 356px) 100vw, 356px" - className="block object-contain" + className="block rounded-full object-contain" />
diff --git a/app/roulette/_components/RouletteCards.tsx b/app/roulette/_components/RouletteCards.tsx new file mode 100644 index 0000000..1d03a50 --- /dev/null +++ b/app/roulette/_components/RouletteCards.tsx @@ -0,0 +1,141 @@ +"use client"; + +import React from "react"; +import Link from "next/link"; +import Image from "next/image"; +import { Check } from "lucide-react"; + +// ========================================== +// 1. 무료 룰렛 카드 +// ========================================== +type FreeRouletteCardProps = { + remainingChances?: number; +}; + +export const FreeRouletteCard = ({ + remainingChances = 1, +}: FreeRouletteCardProps) => { + return ( +
+ {/* Upper Content */} +
+ {/* Left Info */} +
+
+

무료 룰렛

+

+ 매일 1회 참여 가능 +
+ 행운을 확인해 보세요! +

+
+ + {/* Status Checkbox */} +
+
+ +
+ + 오늘 남은 횟수 +
+ + {remainingChances}회 + +
+
+
+ + {/* Right Image Area */} +
+ {/* Shadow Ellipse */} +
+ {/* Free Roulette Graphic */} + 무료 룰렛 +
+
+ + {/* Button */} + + 무료 룰렛 입장 + +
+ ); +}; + +// ========================================== +// 2. 스페셜 룰렛 카드 +// ========================================== +type SpecialRouletteCardProps = { + currentAmount?: number; + targetAmount?: number; +}; + +export const SpecialRouletteCard = ({ + currentAmount = 2000, + targetAmount = 3000, +}: SpecialRouletteCardProps) => { + return ( +
+ {/* Upper Content */} +
+ {/* Left Info */} +
+
+

스페셜 룰렛

+

+ 누적 {targetAmount.toLocaleString()}원 이상 +
+ 결제 시 참여 가능 +

+
+ + {/* Status Checkbox */} +
+
+ +
+ + 현재 누적 +
+ + {currentAmount.toLocaleString()}원 + {" "} + / {targetAmount.toLocaleString()}원 +
+
+
+ + {/* Right Image Area */} +
+ {/* Shadow Ellipse */} +
+ {/* Special Roulette Graphic */} + 스페셜 룰렛 +
+
+ + {/* Button */} + + 스페셜 룰렛 입장 + +
+ ); +}; diff --git a/components/common/PageHeader.tsx b/app/roulette/_components/RouletteHeader.tsx similarity index 82% rename from components/common/PageHeader.tsx rename to app/roulette/_components/RouletteHeader.tsx index 81313a0..6236338 100644 --- a/components/common/PageHeader.tsx +++ b/app/roulette/_components/RouletteHeader.tsx @@ -1,14 +1,14 @@ -"use client"; +"use client"; import { BackButton } from "@/components/ui/BackButton"; import React from "react"; -type PageHeaderProps = { +type RouletteHeaderProps = { title?: React.ReactNode; sidebar?: React.ReactNode; }; -const PageHeader = ({ title, sidebar }: PageHeaderProps) => { +const RouletteHeader = ({ title, sidebar }: RouletteHeaderProps) => { return (
@@ -30,4 +30,4 @@ const PageHeader = ({ title, sidebar }: PageHeaderProps) => { ); }; -export default PageHeader; +export default RouletteHeader; diff --git a/app/roulette/free/_components/RouletteParticipationInfo.tsx b/app/roulette/_components/RouletteParticipationInfo.tsx similarity index 100% rename from app/roulette/free/_components/RouletteParticipationInfo.tsx rename to app/roulette/_components/RouletteParticipationInfo.tsx diff --git a/app/roulette/_components/RouletteProbabilityBottomSheet.tsx b/app/roulette/_components/RouletteProbabilityBottomSheet.tsx new file mode 100644 index 0000000..6cad52b --- /dev/null +++ b/app/roulette/_components/RouletteProbabilityBottomSheet.tsx @@ -0,0 +1,145 @@ +"use client"; + +import React, { useState } from "react"; +import { X } from "lucide-react"; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer"; +import { cn } from "@/lib/utils"; + +// 컴포넌트 내부에 직접 데이터 정의 +const FREE_ROULETTE_ITEMS = [ + { reward: "옵션권 2장", probability: "39%" }, + { reward: "옵션권 5장", probability: "25%" }, + { reward: "풀세트", probability: "18%" }, + { reward: "뽑기권 5장", probability: "10%" }, +]; + +const SPECIAL_ROULETTE_ITEMS = [ + { reward: "스타벅스 쿠폰", probability: "5%" }, + { reward: "10,000 코인", probability: "15%" }, + { reward: "5,000 코인", probability: "30%" }, + { reward: "1,000 코인", probability: "50%" }, +]; + +export interface RouletteProbabilityBottomSheetProps { + /** 바텀시트를 열 트리거 엘리먼트 */ + trigger: React.ReactNode; + /** 처음에 보여줄 기본 탭 (기본값: "free") */ + defaultTab?: "free" | "special"; + /** DrawerContent 추가 클래스명 */ + className?: string; +} + +export default function RouletteProbabilityBottomSheet({ + trigger, + defaultTab = "free", + className, +}: RouletteProbabilityBottomSheetProps) { + const [activeTab, setActiveTab] = useState<"free" | "special">(defaultTab); + + const currentItems = + activeTab === "free" ? FREE_ROULETTE_ITEMS : SPECIAL_ROULETTE_ITEMS; + + return ( + + {trigger} + + + {/* Header Section (DirectChargeDrawer 계좌이체 모달 헤더 구조 + 중앙 정렬) */} + +
+
+ + 당첨 확률 + + + + +
+ + 룰렛 보상 및 당첨 확률 안내 바텀시트입니다. + + + + {/* Content Section */} +
+ {/* Tabs (Chips) - Frame 2612797 */} +
+ + +
+ + {/* List Container - Frame 1171276922 */} +
+ + {activeTab === "free" ? "무료 룰렛" : "스페셜 룰렛"} + + + {/* Frame 1171276921 */} +
+ {/* Header - Frame 1171276915 */} +
+ 보상 + + 확률 + +
+ + {/* Items List - Frame 1171276920 */} +
+ {currentItems.map((item, index) => ( +
+ + {item.reward} + + + {item.probability} + +
+ ))} +
+
+
+
+ + + ); +} + +export { RouletteProbabilityBottomSheet as RouletteProbabilityDrawer }; diff --git a/app/roulette/_components/ScreenRouletteMain.tsx b/app/roulette/_components/ScreenRouletteMain.tsx new file mode 100644 index 0000000..b5b0cb2 --- /dev/null +++ b/app/roulette/_components/ScreenRouletteMain.tsx @@ -0,0 +1,52 @@ +"use client"; + +import React from "react"; +import RouletteHeader from "./RouletteHeader"; +import RouletteProbabilityBottomSheet from "./RouletteProbabilityBottomSheet"; +import { FreeRouletteCard, SpecialRouletteCard } from "./RouletteCards"; + +/** + * 룰렛 메인 화면 (허브) 컴포넌트 + */ +const ScreenRouletteMain = () => { + return ( +
+ {/* Roulette Header */} +
+ + 확률 안내 + + } + /> + } + /> +
+ + {/* Content */} +
+ {/* Title */} +

+ 오늘의 행운을 확인해보세요! +
+ 매일 1회 무료로 돌릴 수 있어요 +

+ + {/* Cards */} +
+ + +
+
+
+ ); +}; + +export default ScreenRouletteMain; diff --git a/app/roulette/free/_components/RouletteChanceBadge.tsx b/app/roulette/free/_components/RouletteChanceBadge.tsx deleted file mode 100644 index 8b1a983..0000000 --- a/app/roulette/free/_components/RouletteChanceBadge.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { cn } from "@/lib/utils"; -import React from "react"; - -interface RouletteChanceBadgeProps { - remainingCount?: number; - className?: string; -} - -const RouletteChanceBadge = ({ - remainingCount = 2, - className, -}: RouletteChanceBadgeProps) => { - return ( -
- - 남은 기회 {remainingCount}개 - -
- ); -}; - -export default RouletteChanceBadge; diff --git a/app/roulette/free/_components/RouletteStatusBar.tsx b/app/roulette/free/_components/RouletteStatusBar.tsx deleted file mode 100644 index f83087f..0000000 --- a/app/roulette/free/_components/RouletteStatusBar.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react"; - -const RouletteStatusBar = () => { - return ( -
-
- - 9:41 - -
- -
-
- - - - -
-
-
-
-
-
-
-
- ); -}; - -export default RouletteStatusBar; diff --git a/app/roulette/free/_components/ScreenRouletteMain.tsx b/app/roulette/free/_components/ScreenRouletteFree.tsx similarity index 71% rename from app/roulette/free/_components/ScreenRouletteMain.tsx rename to app/roulette/free/_components/ScreenRouletteFree.tsx index 354bf37..891eab9 100644 --- a/app/roulette/free/_components/ScreenRouletteMain.tsx +++ b/app/roulette/free/_components/ScreenRouletteFree.tsx @@ -1,14 +1,15 @@ "use client"; import React, { useRef, useState } from "react"; import { CircleAlert } from "lucide-react"; -import PageHeader from "@/components/common/PageHeader"; +import RouletteHeader from "../../_components/RouletteHeader"; import Button from "@/components/ui/Button"; -import Roulette, { RouletteHandle } from "./Roulette"; +import Roulette, { RouletteHandle } from "../../_components/Roulette"; +import RouletteProbabilityBottomSheet from "../../_components/RouletteProbabilityBottomSheet"; // TODO: 실제 API 연동 시 대체 const MOCK_REMAINING_CHANCES = 1; -const ScreenRouletteMain = () => { +const ScreenRouletteFree = () => { const remainingChances = MOCK_REMAINING_CHANCES; const hasChances = remainingChances > 0; @@ -21,15 +22,22 @@ const ScreenRouletteMain = () => { }; return ( -
+
{/* Top Group: Header, Badge, Tagline */}
- - 확률 안내 - + + 확률 안내 + + } + /> } /> @@ -55,13 +63,11 @@ const ScreenRouletteMain = () => {
{/* Center: Roulette Wheel */} -
- console.log("당첨 인덱스:", idx)} - /> -
+ console.log("당첨 인덱스:", idx)} + /> {/* Bottom Group: Spin Button + Notice */}
@@ -83,4 +89,4 @@ const ScreenRouletteMain = () => { ); }; -export default ScreenRouletteMain; +export default ScreenRouletteFree; diff --git a/app/roulette/free/page.tsx b/app/roulette/free/page.tsx index b0dfbbc..495aa16 100644 --- a/app/roulette/free/page.tsx +++ b/app/roulette/free/page.tsx @@ -1,4 +1,4 @@ -import ScreenRouletteMain from "./_components/ScreenRouletteMain"; +import ScreenRouletteFree from "./_components/ScreenRouletteFree"; export const metadata = { title: "이벤트 룰렛", @@ -6,5 +6,5 @@ export const metadata = { }; export default function RoulettePage() { - return ; + return ; } diff --git a/app/roulette/page.tsx b/app/roulette/page.tsx new file mode 100644 index 0000000..4be33e4 --- /dev/null +++ b/app/roulette/page.tsx @@ -0,0 +1,10 @@ +import ScreenRouletteMain from "./_components/ScreenRouletteMain"; + +export const metadata = { + title: "룰렛", + description: "룰렛 이벤트", +}; + +export default function RoulettePage() { + return ; +} diff --git a/app/roulette/special/_components/ScreenRouletteSpecial.tsx b/app/roulette/special/_components/ScreenRouletteSpecial.tsx new file mode 100644 index 0000000..e6ed206 --- /dev/null +++ b/app/roulette/special/_components/ScreenRouletteSpecial.tsx @@ -0,0 +1,79 @@ +"use client"; +import React, { useRef, useState } from "react"; +import { CircleAlert } from "lucide-react"; +import RouletteHeader from "../../_components/RouletteHeader"; +import Button from "@/components/ui/Button"; +import Roulette, { RouletteHandle } from "../../_components/Roulette"; +import RouletteProbabilityBottomSheet from "../../_components/RouletteProbabilityBottomSheet"; +import SpecialRouletteChanceCard from "./SpecialRouletteChanceCard"; + +// TODO: 실제 API 연동 시 대체 +const MOCK_REMAINING_CHANCES = 1; + +const ScreenRouletteSpecial = () => { + const remainingChances = MOCK_REMAINING_CHANCES; + const hasChances = remainingChances > 0; + + const rouletteRef = useRef(null); + const [isSpinning, setIsSpinning] = useState(false); + + const handleSpin = () => { + if (!hasChances || isSpinning) return; + rouletteRef.current?.spin(); + }; + + return ( +
+ {/* Top Group: Header, Badge, Tagline */} +
+ + 확률 안내 + + } + /> + } + /> + + {/* Special Remaining chances card */} +
+ +
+
+ + {/* Center: Roulette Wheel */} + console.log("당첨 인덱스:", idx)} + /> + + {/* Bottom Group: Spin Button + Notice */} +
+ + + {/* Participation notice */} +
+ + 결제 취소 시 지급된 보상이 회수될 수 있어요 +
+
+
+ ); +}; + +export default ScreenRouletteSpecial; diff --git a/app/roulette/special/_components/SpecialRouletteChanceCard.tsx b/app/roulette/special/_components/SpecialRouletteChanceCard.tsx new file mode 100644 index 0000000..5d0e7bb --- /dev/null +++ b/app/roulette/special/_components/SpecialRouletteChanceCard.tsx @@ -0,0 +1,99 @@ +"use client"; + +import React from "react"; +import Image from "next/image"; +import { cn } from "@/lib/utils"; + +export interface SpecialRouletteChanceCardProps { + /** 현재 결제 누적 금액 (기본값: 1000) */ + currentAmount?: number; + /** 다음 룰렛 기회까지의 목표/토탈 금액 (기본값: 3000) */ + targetAmount?: number; + /** 상단 누적 결제 정보 텍스트 (옵션, 기본값: "누적 결제 {currentAmount}원") */ + accumulatedText?: string; + /** 상단 추가 결제 안내 텍스트 (옵션, 기본값: "{남은금액}원 추가 결제 시 1회") */ + additionalText?: string; + /** 우측 하단 목표 금액 텍스트 (옵션, 기본값: "{targetAmount}원") */ + targetText?: string; + className?: string; +} + +export default function SpecialRouletteChanceCard({ + currentAmount = 1000, + targetAmount = 3000, + accumulatedText, + additionalText, + targetText, + className, +}: SpecialRouletteChanceCardProps) { + const diff = Math.max(0, targetAmount - currentAmount); + const percent = Math.min( + 100, + Math.max(0, Math.round((currentAmount / targetAmount) * 100)), + ); + + const displayAccumulated = + accumulatedText ?? `누적 결제 ${currentAmount.toLocaleString()}원`; + const displayAdditional = + additionalText ?? `${diff.toLocaleString()}원 추가 결제 시 1회`; + const displayTarget = targetText ?? `${targetAmount.toLocaleString()}원`; + + return ( +
+ {/* Chances container (상단 티켓 아이콘 + 결제 정보) */} +
+ {/* Ticket Image */} +
+ ticket +
+ + {/* Info container */} +
+ + {displayAccumulated} + + + {displayAdditional} + +
+
+ + {/* Progress bar container (하단 진행도 바 + 목표 금액) */} +
+ {/* Progress bar background */} +
+ {/* Progress bar fill */} +
+ {/* Progress indicator bar */} + {percent > 0 && percent < 100 && ( +
+ )} +
+
+ + {/* Amount text */} + + {displayTarget} + +
+
+ ); +} + +// 편의를 위한 별칭 export +export { SpecialRouletteChanceCard as RemainingChancesContainer }; diff --git a/app/roulette/special/page.tsx b/app/roulette/special/page.tsx new file mode 100644 index 0000000..8463166 --- /dev/null +++ b/app/roulette/special/page.tsx @@ -0,0 +1,10 @@ +import ScreenRouletteSpecial from "./_components/ScreenRouletteSpecial"; + +export const metadata = { + title: "스페셜 룰렛", + description: "스페셜 룰렛 진행", +}; + +export default function SpecialRoulettePage() { + return ; +} diff --git a/public/roulette/free.png b/public/roulette/free.png new file mode 100644 index 0000000..cf9e343 Binary files /dev/null and b/public/roulette/free.png differ diff --git a/public/roulette/special.png b/public/roulette/special.png new file mode 100644 index 0000000..8cb8dbc Binary files /dev/null and b/public/roulette/special.png differ diff --git a/public/roulette/ticket.png b/public/roulette/ticket.png new file mode 100644 index 0000000..8caaa2a Binary files /dev/null and b/public/roulette/ticket.png differ