From 0c278461978ba3643e0db932641a38938b6a5e40 Mon Sep 17 00:00:00 2001 From: dasosann Date: Tue, 21 Apr 2026 17:26:23 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EC=B6=A9=EC=A0=84=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EB=94=94=EC=9E=90=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/common/ChargeDrawer.tsx | 318 ++++++++++++++++++++++++++++ components/common/MyCoinSection.tsx | 11 +- components/ui/drawer.tsx | 2 +- 3 files changed, 327 insertions(+), 4 deletions(-) create mode 100644 components/common/ChargeDrawer.tsx diff --git a/components/common/ChargeDrawer.tsx b/components/common/ChargeDrawer.tsx new file mode 100644 index 0000000..fc25e5f --- /dev/null +++ b/components/common/ChargeDrawer.tsx @@ -0,0 +1,318 @@ +"use client"; + +import React from "react"; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer"; +import Image from "next/image"; +import { cn } from "@/lib/utils"; +import { useItems } from "@/hooks/useItems"; + +interface ChargeDrawerProps { + trigger: React.ReactNode; +} + +/* ── 카테고리 칩 ── */ +const CATEGORIES = ["전체", "뽑기권", "번들+옵션권"] as const; + +/* ── 아이콘 사이즈 ── */ +const ICON_SIZE = { sm: 24, lg: 80 } as const; + +/* ── 보유현황 아이콘·라벨 ── */ +const INVENTORY_ROWS = [ + { + icon: "/main/coin.png", + alt: "coin", + label: "보유 뽑기권", + key: "matching", + }, + { + icon: "/main/elec-bulb.png", + alt: "bulb", + label: "보유 아이템", + key: "option", + }, +] as const; + +/* ── 구매 가능 한도 (임시) ── */ +const PURCHASE_LIMITS = { + matching: { current: 18, max: 30, label: "뽑기권" }, + option: { current: 42, max: 60, label: "아이템" }, +} as const; + +/* ── 빠른 구매 번들 카드 ── */ +const QUICK_BUNDLES = [ + { + title: "실속 번들", + icon: "/main/coin.png", + description: "뽑기권 5개+옵션권 2개", + bonus: "옵션권 1개 무료 증정!", + price: "5,000원", + }, + { + title: "슈퍼 번들", + icon: "/main/coin.png", + description: "뽑기권 10개+옵션권 10개", + bonus: "옵션권 5개 무료 증정!", + price: "10,000원", + }, +] as const; + +/* ── 전체(개별) 아이템 ── */ +const INDIVIDUAL_ITEMS = [ + { label: "뽑기권 1개", price: "1,000원" }, + { label: "옵션권 1개", price: "500원" }, +] as const; + +/* ── 번들 아이템 ── */ +const BUNDLE_ITEMS = [ + { + title: "미니 번들", + description: "옵션권 3개", + bonus: "옵션권 1개 무료 증정!", + price: "2,500원", + }, + { + title: "실속 번들", + description: "뽑기권 5개+옵션권 2개", + bonus: "옵션권 1개 무료 증정!", + price: "5,000원", + }, + { + title: "슈퍼 번들", + description: "뽑기권 10개+옵션권 10개", + bonus: "옵션권 5개 무료 증정!", + price: "10,000원", + }, +] as const; + +/* ── 이용안내 텍스트 ── */ +const USAGE_INFO = `충전된 포인트의 소멸시효 기한은 충전 후 5년입니다. +1 포인트는 1원입니다. +충전한 포인트로 서비스를 이용할 수 있습니다. +포인트는 이벤트 포인트 먼저 사용되고, 유상 포인트가 사용됩니다. +이벤트 포인트는 유효기한이 임박한 순으로 먼저 사용됩니다. +유효기간은 포인트 충전내역을 통해 확인하실 수 있습니다.`; + +const BUSINESS_INFO = + "대표이사 천승환 | 호스팅서비스사업자 코매칭 | 사업자 등록번호 843-27-01742 | 통신판매신고 2024-부천원미-2812 | 대표전화 010-3039-7387 | 경기도 부천시 조마루로 366번길 27, 401호"; + +/* ────────────────────────────────────── */ + +export default function ChargeDrawer({ trigger }: ChargeDrawerProps) { + const [activeCategory, setActiveCategory] = React.useState(0); + const { data } = useItems(); + + const ticketCounts: Record = { + matching: data?.data.matchingTicketCount ?? 0, + option: data?.data.optionTicketCount ?? 0, + }; + + return ( + + {trigger} + +
+ {/* ── Header ── */} + +
+
+ + 상점 + + + 닫기 + +
+ + + {/* ── Scrollable Content ── */} +
+
+ {/* ── 카테고리 칩 ── */} +
+ {CATEGORIES.map((cat, i) => ( + + ))} +
+ + {/* ── 보유현황 카드 ── */} +
+ {/* 보유 수량 */} +
+ {INVENTORY_ROWS.map((row) => ( +
+ {row.alt} +
+ + {row.label} + + + {ticketCounts[row.key]}개 + +
+
+ ))} +
+ + {/* 구매 가능 한도 */} +
+ + 구매 가능 한도 + +
+ {Object.values(PURCHASE_LIMITS).map((limit) => ( + + {limit.label} {limit.current}/{limit.max} + + ))} +
+
+
+ + {/* ── 빠른 구매 ── */} +
+ 빠른 구매 +
+ {QUICK_BUNDLES.map((bundle) => ( +
+
+ coin +
+ + {bundle.title} + +
+ + {bundle.description} + + + {bundle.bonus} + +
+
+
+ +
+ ))} +
+
+ + {/* ── 전체 ── */} +
+ 전체 +
+ {INDIVIDUAL_ITEMS.map((item) => ( +
+ + {item.label} + + +
+ ))} +
+
+ + {/* ── 번들 ── */} +
+ 번들 +
+ {BUNDLE_ITEMS.map((item) => ( +
+
+
+ + {item.title} + + + {item.description} + +
+ + {item.bonus} + +
+ +
+ ))} +
+
+ + {/* ── 이용안내 ── */} +
+ + 이용안내 + +
+

+ {USAGE_INFO} +

+

+ {BUSINESS_INFO} +

+
+
+
+
+
+ + + ); +} diff --git a/components/common/MyCoinSection.tsx b/components/common/MyCoinSection.tsx index b9265d7..17909e6 100644 --- a/components/common/MyCoinSection.tsx +++ b/components/common/MyCoinSection.tsx @@ -2,6 +2,7 @@ import { useItems } from "@/hooks/useItems"; import { cn } from "@/lib/utils"; import Image from "next/image"; import React from "react"; +import ChargeDrawer from "./ChargeDrawer"; interface MyCoinSectionProps { className?: string; @@ -68,9 +69,13 @@ const MyCoinSection = ({ className }: MyCoinSectionProps) => { {optionTicketCount}개
- + + 충전하기 + + } + /> ); }; diff --git a/components/ui/drawer.tsx b/components/ui/drawer.tsx index 0e50b52..ae1eaa4 100644 --- a/components/ui/drawer.tsx +++ b/components/ui/drawer.tsx @@ -65,7 +65,7 @@ function DrawerContent({ className={cn( "group/drawer-content bg-background fixed z-50 flex h-auto flex-col", "data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:max-h-[96vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b", - "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:max-h-[96vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t", + "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mx-auto data-[vaul-drawer-direction=bottom]:max-h-[96vh] data-[vaul-drawer-direction=bottom]:max-w-[430px] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t", "data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm", "data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm", "after:hidden", From e17662628c2cfccb88f84e8f5a13c1d404bf9cc3 Mon Sep 17 00:00:00 2001 From: dasosann Date: Wed, 22 Apr 2026 17:04:58 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EA=B2=B0=EC=A0=9C=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/common/ChargeDrawer.tsx | 298 ++---------------- .../common/charge/ChargeHistoryContent.tsx | 47 +++ .../common/charge/ChargeHistoryListItem.tsx | 63 ++++ .../common/charge/ChargeInventoryCard.tsx | 58 ++++ .../common/charge/DepositorNameContent.tsx | 69 ++++ components/common/charge/QuickBundleCard.tsx | 51 +++ components/common/charge/ShopContent.tsx | 108 +++++++ lib/constants/charge.ts | 129 ++++++++ 8 files changed, 558 insertions(+), 265 deletions(-) create mode 100644 components/common/charge/ChargeHistoryContent.tsx create mode 100644 components/common/charge/ChargeHistoryListItem.tsx create mode 100644 components/common/charge/ChargeInventoryCard.tsx create mode 100644 components/common/charge/DepositorNameContent.tsx create mode 100644 components/common/charge/QuickBundleCard.tsx create mode 100644 components/common/charge/ShopContent.tsx create mode 100644 lib/constants/charge.ts diff --git a/components/common/ChargeDrawer.tsx b/components/common/ChargeDrawer.tsx index fc25e5f..9039bdd 100644 --- a/components/common/ChargeDrawer.tsx +++ b/components/common/ChargeDrawer.tsx @@ -9,109 +9,31 @@ import { DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer"; -import Image from "next/image"; import { cn } from "@/lib/utils"; -import { useItems } from "@/hooks/useItems"; +import ShopContent from "@/components/common/charge/ShopContent"; +import ChargeHistoryContent from "@/components/common/charge/ChargeHistoryContent"; +import DepositorNameContent from "@/components/common/charge/DepositorNameContent"; interface ChargeDrawerProps { trigger: React.ReactNode; } -/* ── 카테고리 칩 ── */ -const CATEGORIES = ["전체", "뽑기권", "번들+옵션권"] as const; - -/* ── 아이콘 사이즈 ── */ -const ICON_SIZE = { sm: 24, lg: 80 } as const; - -/* ── 보유현황 아이콘·라벨 ── */ -const INVENTORY_ROWS = [ - { - icon: "/main/coin.png", - alt: "coin", - label: "보유 뽑기권", - key: "matching", - }, - { - icon: "/main/elec-bulb.png", - alt: "bulb", - label: "보유 아이템", - key: "option", - }, -] as const; - -/* ── 구매 가능 한도 (임시) ── */ -const PURCHASE_LIMITS = { - matching: { current: 18, max: 30, label: "뽑기권" }, - option: { current: 42, max: 60, label: "아이템" }, -} as const; - -/* ── 빠른 구매 번들 카드 ── */ -const QUICK_BUNDLES = [ - { - title: "실속 번들", - icon: "/main/coin.png", - description: "뽑기권 5개+옵션권 2개", - bonus: "옵션권 1개 무료 증정!", - price: "5,000원", - }, - { - title: "슈퍼 번들", - icon: "/main/coin.png", - description: "뽑기권 10개+옵션권 10개", - bonus: "옵션권 5개 무료 증정!", - price: "10,000원", - }, +/* ── 탭 정의 ── */ +const TABS = [ + { label: "상점", title: "상점" }, + { label: "충전내역", title: "충전내역" }, + { label: "입금자명 설정", title: "입금자명 설정" }, ] as const; -/* ── 전체(개별) 아이템 ── */ -const INDIVIDUAL_ITEMS = [ - { label: "뽑기권 1개", price: "1,000원" }, - { label: "옵션권 1개", price: "500원" }, -] as const; - -/* ── 번들 아이템 ── */ -const BUNDLE_ITEMS = [ - { - title: "미니 번들", - description: "옵션권 3개", - bonus: "옵션권 1개 무료 증정!", - price: "2,500원", - }, - { - title: "실속 번들", - description: "뽑기권 5개+옵션권 2개", - bonus: "옵션권 1개 무료 증정!", - price: "5,000원", - }, - { - title: "슈퍼 번들", - description: "뽑기권 10개+옵션권 10개", - bonus: "옵션권 5개 무료 증정!", - price: "10,000원", - }, -] as const; - -/* ── 이용안내 텍스트 ── */ -const USAGE_INFO = `충전된 포인트의 소멸시효 기한은 충전 후 5년입니다. -1 포인트는 1원입니다. -충전한 포인트로 서비스를 이용할 수 있습니다. -포인트는 이벤트 포인트 먼저 사용되고, 유상 포인트가 사용됩니다. -이벤트 포인트는 유효기한이 임박한 순으로 먼저 사용됩니다. -유효기간은 포인트 충전내역을 통해 확인하실 수 있습니다.`; - -const BUSINESS_INFO = - "대표이사 천승환 | 호스팅서비스사업자 코매칭 | 사업자 등록번호 843-27-01742 | 통신판매신고 2024-부천원미-2812 | 대표전화 010-3039-7387 | 경기도 부천시 조마루로 366번길 27, 401호"; +/* ── 탭별 콘텐츠 ── */ +const TAB_CONTENT = [ShopContent, ChargeHistoryContent, DepositorNameContent]; /* ────────────────────────────────────── */ export default function ChargeDrawer({ trigger }: ChargeDrawerProps) { - const [activeCategory, setActiveCategory] = React.useState(0); - const { data } = useItems(); + const [activeTab, setActiveTab] = React.useState(0); - const ticketCounts: Record = { - matching: data?.data.matchingTicketCount ?? 0, - option: data?.data.optionTicketCount ?? 0, - }; + const ActiveContent = TAB_CONTENT[activeTab]; return ( @@ -126,7 +48,7 @@ export default function ChargeDrawer({ trigger }: ChargeDrawerProps) {
- 상점 + {TABS[activeTab].title} 닫기 @@ -134,182 +56,28 @@ export default function ChargeDrawer({ trigger }: ChargeDrawerProps) {
+ {/* ── 탭 칩 ── */} +
+ {TABS.map((tab, i) => ( + + ))} +
+ {/* ── Scrollable Content ── */}
-
- {/* ── 카테고리 칩 ── */} -
- {CATEGORIES.map((cat, i) => ( - - ))} -
- - {/* ── 보유현황 카드 ── */} -
- {/* 보유 수량 */} -
- {INVENTORY_ROWS.map((row) => ( -
- {row.alt} -
- - {row.label} - - - {ticketCounts[row.key]}개 - -
-
- ))} -
- - {/* 구매 가능 한도 */} -
- - 구매 가능 한도 - -
- {Object.values(PURCHASE_LIMITS).map((limit) => ( - - {limit.label} {limit.current}/{limit.max} - - ))} -
-
-
- - {/* ── 빠른 구매 ── */} -
- 빠른 구매 -
- {QUICK_BUNDLES.map((bundle) => ( -
-
- coin -
- - {bundle.title} - -
- - {bundle.description} - - - {bundle.bonus} - -
-
-
- -
- ))} -
-
- - {/* ── 전체 ── */} -
- 전체 -
- {INDIVIDUAL_ITEMS.map((item) => ( -
- - {item.label} - - -
- ))} -
-
- - {/* ── 번들 ── */} -
- 번들 -
- {BUNDLE_ITEMS.map((item) => ( -
-
-
- - {item.title} - - - {item.description} - -
- - {item.bonus} - -
- -
- ))} -
-
- - {/* ── 이용안내 ── */} -
- - 이용안내 - -
-

- {USAGE_INFO} -

-

- {BUSINESS_INFO} -

-
-
-
+
diff --git a/components/common/charge/ChargeHistoryContent.tsx b/components/common/charge/ChargeHistoryContent.tsx new file mode 100644 index 0000000..821fb89 --- /dev/null +++ b/components/common/charge/ChargeHistoryContent.tsx @@ -0,0 +1,47 @@ +"use client"; + +import React from "react"; +import { useItems } from "@/hooks/useItems"; +import ChargeInventoryCard from "./ChargeInventoryCard"; +import ChargeHistoryListItem from "./ChargeHistoryListItem"; +import { + BUSINESS_INFO, + NOTICE_INFO, + CHARGE_HISTORY, +} from "@/lib/constants/charge"; + +export default function ChargeHistoryContent() { + const { data } = useItems(); + + const ticketCounts = { + matching: data?.data.matchingTicketCount ?? 0, + option: data?.data.optionTicketCount ?? 0, + }; + + return ( +
+ {/* ── 보유현황 카드 ── */} + + + {/* ── 충전 내역 리스트 ── */} +
+ {CHARGE_HISTORY.map((item) => ( + + ))} +
+ + {/* ── 유의사항 ── */} +
+ 유의사항 +
+

+ {NOTICE_INFO} +

+

+ {BUSINESS_INFO} +

+
+
+
+ ); +} diff --git a/components/common/charge/ChargeHistoryListItem.tsx b/components/common/charge/ChargeHistoryListItem.tsx new file mode 100644 index 0000000..ef5b1bc --- /dev/null +++ b/components/common/charge/ChargeHistoryListItem.tsx @@ -0,0 +1,63 @@ +"use client"; + +import React from "react"; +import { cn } from "@/lib/utils"; + +export type ChargeStatus = "success" | "failed" | "cancelled" | "event"; + +export interface ChargeHistoryItemProps { + date: string; + orderId: string; + points: string; + paymentAmount: string; + status: ChargeStatus; + statusLabel: string; +} + +/* ── 상태별 스타일 헬퍼 ── */ +function getStatusStyles(status: ChargeStatus) { + const isInactive = status === "failed" || status === "cancelled"; + return { + dateColor: isInactive ? "text-color-gray-400" : "text-color-text-black", + pointsColor: isInactive ? "text-color-gray-400" : "text-color-text-black", + amountColor: isInactive ? "text-color-gray-400" : "text-color-text-black", + amountStrike: isInactive, + }; +} + +export default function ChargeHistoryListItem({ + date, + orderId, + points, + paymentAmount, + status, + statusLabel, +}: ChargeHistoryItemProps) { + const styles = getStatusStyles(status); + + return ( +
+ {/* 날짜 | 주문번호 */} + + {date} | {orderId} + + + {/* 포인트 · 결제금액 */} +
+ {points} + + {paymentAmount} + +
+ + {/* 상태 */} + {statusLabel} +
+ ); +} diff --git a/components/common/charge/ChargeInventoryCard.tsx b/components/common/charge/ChargeInventoryCard.tsx new file mode 100644 index 0000000..3597472 --- /dev/null +++ b/components/common/charge/ChargeInventoryCard.tsx @@ -0,0 +1,58 @@ +"use client"; + +import React from "react"; +import Image from "next/image"; +import { + ICON_SIZE, + INVENTORY_ROWS, + PURCHASE_LIMITS, +} from "@/lib/constants/charge"; + +interface ChargeInventoryCardProps { + ticketCounts: { + matching: number; + option: number; + }; +} + +export default function ChargeInventoryCard({ + ticketCounts, +}: ChargeInventoryCardProps) { + return ( +
+ {/* 보유 수량 */} +
+ {INVENTORY_ROWS.map((row) => ( +
+ {row.alt} +
+ + {row.label} + + + {ticketCounts[row.key as keyof typeof ticketCounts]}개 + +
+
+ ))} +
+ + {/* 구매 가능 한도 */} +
+ 구매 가능 한도 +
+ {Object.values(PURCHASE_LIMITS).map((limit) => ( + + {limit.label} {limit.current}/{limit.max} + + ))} +
+
+
+ ); +} diff --git a/components/common/charge/DepositorNameContent.tsx b/components/common/charge/DepositorNameContent.tsx new file mode 100644 index 0000000..b0c9df4 --- /dev/null +++ b/components/common/charge/DepositorNameContent.tsx @@ -0,0 +1,69 @@ +"use client"; + +import React from "react"; +import { cn } from "@/lib/utils"; + +export default function DepositorNameContent() { + const [name, setName] = React.useState(""); + + return ( +
+ {/* ── 입금자명 입력 ── */} +
+ 입금자명 +
+ setName(e.target.value)} + placeholder="이름을 입력해주세요" + className="typo-16-500 w-full bg-transparent text-center text-[#1A1A1A] outline-none placeholder:text-[#B3B3B3]" + maxLength={6} + /> +
+
+ + {/* ── 예시 이미지 ── */} +
+ 예시 +
+ + Toss 앱 입금자명 설정 예시 이미지 + +
+
+ + {/* ── 안내 가이드 (Frame 2612440) ── */} +
+ {/* 유의사항 */} +
+ 유의사항 +

+ 코매칭에 보내실 입금자명을 입력해 주세요. 입금자명은 한글이나 + 영문자로 6자 이내입니다. 특수문자는 사용할 수 없습니다. Toss 앱에 + 사전 설정된 입금자명을 입력하시면 간편합니다. 나중에 다시 수정할 수 + 있어요! +

+
+ + {/* FAQ */} +
+ + 왜 입금자명을 설정해야 하나요? + +

+ 모든 포인트 충전은 계좌이체로 진행되고 있어요. 충전 간 복잡함을 + 줄이기 위해, 사전에 입금자명을 설정하시면 더욱 편리하고 쾌적한 + 코매칭을 즐기실 수 있습니다. +

+
+
+
+ ); +} diff --git a/components/common/charge/QuickBundleCard.tsx b/components/common/charge/QuickBundleCard.tsx new file mode 100644 index 0000000..b7f26c5 --- /dev/null +++ b/components/common/charge/QuickBundleCard.tsx @@ -0,0 +1,51 @@ +"use client"; + +import React from "react"; +import Image from "next/image"; +import { ICON_SIZE } from "@/lib/constants/charge"; + +interface QuickBundleCardProps { + title: string; + icon: string; + description: string; + bonus: string; + price: string; +} + +export default function QuickBundleCard({ + title, + icon, + description, + bonus, + price, +}: QuickBundleCardProps) { + return ( +
+
+ {title} +
+ {title} +
+ + {description} + + + {bonus} + +
+
+
+ +
+ ); +} diff --git a/components/common/charge/ShopContent.tsx b/components/common/charge/ShopContent.tsx new file mode 100644 index 0000000..eca3740 --- /dev/null +++ b/components/common/charge/ShopContent.tsx @@ -0,0 +1,108 @@ +"use client"; + +import React from "react"; +import { useItems } from "@/hooks/useItems"; +import ChargeInventoryCard from "./ChargeInventoryCard"; +import QuickBundleCard from "./QuickBundleCard"; +import { + QUICK_BUNDLES, + INDIVIDUAL_ITEMS, + BUNDLE_ITEMS, + USAGE_INFO, + BUSINESS_INFO, +} from "@/lib/constants/charge"; + +export default function ShopContent() { + const { data } = useItems(); + + const ticketCounts = { + matching: data?.data.matchingTicketCount ?? 0, + option: data?.data.optionTicketCount ?? 0, + }; + + return ( +
+ {/* ── 보유현황 카드 ── */} + + + {/* ── 빠른 구매 ── */} +
+ 빠른 구매 +
+ {QUICK_BUNDLES.map((bundle) => ( + + ))} +
+
+ + {/* ── 전체 ── */} +
+ 전체 +
+ {INDIVIDUAL_ITEMS.map((item) => ( +
+ + {item.label} + + +
+ ))} +
+
+ + {/* ── 번들 ── */} +
+ 번들 +
+ {BUNDLE_ITEMS.map((item) => ( +
+
+
+ + {item.title} + + + {item.description} + +
+ + {item.bonus} + +
+ +
+ ))} +
+
+ + {/* ── 이용안내 ── */} +
+ 이용안내 +
+

+ {USAGE_INFO} +

+

+ {BUSINESS_INFO} +

+
+
+
+ ); +} diff --git a/lib/constants/charge.ts b/lib/constants/charge.ts new file mode 100644 index 0000000..31a9069 --- /dev/null +++ b/lib/constants/charge.ts @@ -0,0 +1,129 @@ +/* ── 아이콘 사이즈 ── */ +export const ICON_SIZE = { + sm: 24, + lg: 80, +} as const; + +/* ── 보유현황 아이콘·라벨 ── */ +export const INVENTORY_ROWS = [ + { + icon: "/main/coin.png", + alt: "coin", + label: "보유 뽑기권", + key: "matching", + }, + { + icon: "/main/elec-bulb.png", + alt: "bulb", + label: "보유 아이템", + key: "option", + }, +] as const; + +/* ── 구매 가능 한도 (임시) ── */ +export const PURCHASE_LIMITS = { + matching: { current: 18, max: 30, label: "뽑기권" }, + option: { current: 42, max: 60, label: "아이템" }, +} as const; + +/* ── 빠른 구매 번들 카드 ── */ +export const QUICK_BUNDLES = [ + { + title: "실속 번들", + icon: "/main/coin.png", + description: "뽑기권 5개+옵션권 2개", + bonus: "옵션권 1개 무료 증정!", + price: "5,000원", + }, + { + title: "슈퍼 번들", + icon: "/main/coin.png", + description: "뽑기권 10개+옵션권 10개", + bonus: "옵션권 5개 무료 증정!", + price: "10,000원", + }, +] as const; + +/* ── 가격 상수 ── */ +export const ITEM_PRICES = { + matching: 1000, + option: 200, +} as const; + +/* ── 전체(개별) 아이템 ── */ +export const INDIVIDUAL_ITEMS = [ + { + label: "뽑기권 1개", + price: `${ITEM_PRICES.matching.toLocaleString()}원`, + }, + { + label: "옵션권 1개", + price: `${ITEM_PRICES.option.toLocaleString()}원`, + }, +] as const; + +/* ── 번들 아이템 ── */ +export const BUNDLE_ITEMS = [ + { + title: "미니 번들", + description: "옵션권 3개", + bonus: "옵션권 1개 무료 증정!", + price: "2,500원", + }, + { + title: "실속 번들", + description: "뽑기권 5개+옵션권 2개", + bonus: "옵션권 1개 무료 증정!", + price: "5,000원", + }, + { + title: "슈퍼 번들", + description: "뽑기권 10개+옵션권 10개", + bonus: "옵션권 5개 무료 증정!", + price: "10,000원", + }, +] as const; + +/* ── 이용안내 및 사업자 정보 ── */ +export const USAGE_INFO = `충전된 포인트의 소멸시효 기한은 충전 후 5년입니다. +1 포인트는 1원입니다. +충전한 포인트로 서비스를 이용할 수 있습니다. +포인트는 이벤트 포인트 먼저 사용되고, 유상 포인트가 사용됩니다. +이벤트 포인트는 유효기한이 임박한 순으로 먼저 사용됩니다. +유효기간은 포인트 충전내역을 통해 확인하실 수 있습니다.`; + +export const NOTICE_INFO = "결제 혹은 환불에 필요한 유의사항을 적습니다."; + +export const BUSINESS_INFO = + "대표이사 천승환 | 호스팅서비스사업자 코매칭 | 사업자 등록번호 843-27-01742 | 통신판매신고 2024-부천원미-2812 | 대표전화 010-3039-7387 | 경기도 부천시 조마루로 366번길 27, 401호"; + +/* ── 충전 내역 (임시) ── */ +export const CHARGE_HISTORY = [ + { + id: 1, + date: "2024.03.20", + orderId: "20240320-0001", + points: "뽑기권 5개", + paymentAmount: "5,000원", + status: "success", + statusLabel: "결제완료", + }, + { + id: 2, + date: "2024.03.18", + orderId: "20240318-0042", + points: "옵션권 10개", + paymentAmount: "5,000원", + status: "cancelled", + statusLabel: "결제취소", + }, + { + id: 3, + date: "2024.03.15", + orderId: "20240315-0012", + points: "뽑기권 1개 (이벤트)", + paymentAmount: "0원", + status: "event", + statusLabel: "이벤트 증정", + }, +] as const; From 25543003682129767b4978b02980bd7c997354a5 Mon Sep 17 00:00:00 2001 From: dasosann Date: Wed, 22 Apr 2026 17:08:17 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=201=EC=B0=A8=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/common/ChargeDrawer.tsx | 31 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/components/common/ChargeDrawer.tsx b/components/common/ChargeDrawer.tsx index 9039bdd..5362aba 100644 --- a/components/common/ChargeDrawer.tsx +++ b/components/common/ChargeDrawer.tsx @@ -57,23 +57,24 @@ export default function ChargeDrawer({ trigger }: ChargeDrawerProps) { {/* ── 탭 칩 ── */} -
+
    {TABS.map((tab, i) => ( - +
  • + +
  • ))} -
+ {/* ── Scrollable Content ── */}
From e426860e11f5a89108d4442411064a7e1551820d Mon Sep 17 00:00:00 2001 From: dasosann Date: Thu, 23 Apr 2026 11:28:38 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/common/ChargeDrawer.tsx | 28 ++------------- components/common/charge/ChargeTabs.tsx | 35 +++++++++++++++++++ .../common/charge/DepositorNameContent.tsx | 10 +++++- lib/constants/charge.ts | 7 ++++ lib/validators.ts | 5 +++ 5 files changed, 59 insertions(+), 26 deletions(-) create mode 100644 components/common/charge/ChargeTabs.tsx diff --git a/components/common/ChargeDrawer.tsx b/components/common/ChargeDrawer.tsx index 5362aba..b6d8bd1 100644 --- a/components/common/ChargeDrawer.tsx +++ b/components/common/ChargeDrawer.tsx @@ -13,18 +13,13 @@ import { cn } from "@/lib/utils"; import ShopContent from "@/components/common/charge/ShopContent"; import ChargeHistoryContent from "@/components/common/charge/ChargeHistoryContent"; import DepositorNameContent from "@/components/common/charge/DepositorNameContent"; +import ChargeTabs from "@/components/common/charge/ChargeTabs"; +import { TABS } from "@/lib/constants/charge"; interface ChargeDrawerProps { trigger: React.ReactNode; } -/* ── 탭 정의 ── */ -const TABS = [ - { label: "상점", title: "상점" }, - { label: "충전내역", title: "충전내역" }, - { label: "입금자명 설정", title: "입금자명 설정" }, -] as const; - /* ── 탭별 콘텐츠 ── */ const TAB_CONTENT = [ShopContent, ChargeHistoryContent, DepositorNameContent]; @@ -57,24 +52,7 @@ export default function ChargeDrawer({ trigger }: ChargeDrawerProps) { {/* ── 탭 칩 ── */} -
    - {TABS.map((tab, i) => ( -
  • - -
  • - ))} -
+ {/* ── Scrollable Content ── */}
diff --git a/components/common/charge/ChargeTabs.tsx b/components/common/charge/ChargeTabs.tsx new file mode 100644 index 0000000..a6436b8 --- /dev/null +++ b/components/common/charge/ChargeTabs.tsx @@ -0,0 +1,35 @@ +"use client"; + +import React from "react"; +import { cn } from "@/lib/utils"; +import { TABS } from "@/lib/constants/charge"; + +interface ChargeTabsProps { + activeTab: number; + setActiveTab: (index: number) => void; +} + +const ChargeTabs = ({ activeTab, setActiveTab }: ChargeTabsProps) => { + return ( +
    + {TABS.map((tab, i) => ( +
  • + +
  • + ))} +
+ ); +}; + +export default ChargeTabs; diff --git a/components/common/charge/DepositorNameContent.tsx b/components/common/charge/DepositorNameContent.tsx index b0c9df4..91c747c 100644 --- a/components/common/charge/DepositorNameContent.tsx +++ b/components/common/charge/DepositorNameContent.tsx @@ -2,10 +2,18 @@ import React from "react"; import { cn } from "@/lib/utils"; +import { isValidDepositorName } from "@/lib/validators"; export default function DepositorNameContent() { const [name, setName] = React.useState(""); + const handleNameChange = (e: React.ChangeEvent) => { + const value = e.target.value; + if (isValidDepositorName(value)) { + setName(value); + } + }; + return (
{/* ── 입금자명 입력 ── */} @@ -21,7 +29,7 @@ export default function DepositorNameContent() { setName(e.target.value)} + onChange={handleNameChange} placeholder="이름을 입력해주세요" className="typo-16-500 w-full bg-transparent text-center text-[#1A1A1A] outline-none placeholder:text-[#B3B3B3]" maxLength={6} diff --git a/lib/constants/charge.ts b/lib/constants/charge.ts index 31a9069..9228882 100644 --- a/lib/constants/charge.ts +++ b/lib/constants/charge.ts @@ -127,3 +127,10 @@ export const CHARGE_HISTORY = [ statusLabel: "이벤트 증정", }, ] as const; + +/* ── 탭 정의 ── */ +export const TABS = [ + { label: "상점", title: "상점" }, + { label: "충전내역", title: "충전내역" }, + { label: "입금자명 설정", title: "입금자명 설정" }, +] as const; diff --git a/lib/validators.ts b/lib/validators.ts index aabcecf..daeca7a 100644 --- a/lib/validators.ts +++ b/lib/validators.ts @@ -14,3 +14,8 @@ export const validatePasswordPattern = (password: string): boolean => { /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]+$/; return passwordRegex.test(password); }; + +export const isValidDepositorName = (text: string): boolean => { + const regex = /^[a-zA-Z가-힣ㄱ-ㅎㅏ-ㅣ]*$/; + return regex.test(text); +};