From a529962cd9c8c4735a797c2e799db52c1bbe11b7 Mon Sep 17 00:00:00 2001 From: dasosann Date: Thu, 23 Apr 2026 16:12:59 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EA=B2=B0=EC=A0=9C=20=EB=91=90?= =?UTF-8?q?=EB=B2=88=EC=A7=B8=20=EB=AA=A8=EB=8B=AC=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../charge-confirm/ConfirmChargeDrawer.tsx | 201 ++++++++++++++++++ components/common/charge/QuickBundleCard.tsx | 20 +- components/common/charge/ShopContent.tsx | 57 ++--- components/common/charge/ShopRows.tsx | 76 +++++++ lib/constants/charge.ts | 82 ++++--- 5 files changed, 360 insertions(+), 76 deletions(-) create mode 100644 components/common/charge-confirm/ConfirmChargeDrawer.tsx create mode 100644 components/common/charge/ShopRows.tsx diff --git a/components/common/charge-confirm/ConfirmChargeDrawer.tsx b/components/common/charge-confirm/ConfirmChargeDrawer.tsx new file mode 100644 index 0000000..553f777 --- /dev/null +++ b/components/common/charge-confirm/ConfirmChargeDrawer.tsx @@ -0,0 +1,201 @@ +"use client"; + +import React from "react"; +import { cn } from "@/lib/utils"; +import { X, PencilLine, Check } from "lucide-react"; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer"; + +import Button from "@/components/ui/Button"; +import { BANK_INFO } from "@/lib/constants/charge"; + +/* ── Props ── */ +interface ConfirmChargeDrawerProps { + trigger: React.ReactNode; + /** 입금액 (원 단위 숫자) */ + amount: number; + /** 입금자명 (사전 설정된 값) */ + depositorName?: string; +} + +/* ────────────────────────────────────── */ + +export default function ConfirmChargeDrawer({ + trigger, + amount, + depositorName = "박승원", +}: ConfirmChargeDrawerProps) { + const [agreed, setAgreed] = React.useState(false); + const [name, setName] = React.useState(depositorName); + const [isEditingName, setIsEditingName] = React.useState(false); + const inputRef = React.useRef(null); + + /* 입금자명 수정 시 input focus */ + React.useEffect(() => { + if (isEditingName) inputRef.current?.focus(); + }, [isEditingName]); + + /* 계좌번호 복사 */ + const handleCopy = async () => { + try { + await navigator.clipboard.writeText( + `${BANK_INFO.bank} ${BANK_INFO.account}`, + ); + alert("계좌 정보가 복사되었습니다."); + } catch (err) { + console.error("Failed to copy: ", err); + } + }; + + /* 충전 완료 알림 */ + const handleConfirm = () => { + alert("충전 요청이 완료되었습니다!"); + }; + + return ( + setAgreed(false)}> + {trigger} + +
+
+ {/* ── Header ── */} + +
+ + 계좌이체 + + + + +
+
+ + {/* ── Content ── */} +
+ {/* ── 입금 정보 카드 ── */} +
+ {/* 입금계좌 라벨 + 복사 */} +
+
+ 입금계좌 + +
+ + {/* 계좌 정보 박스 */} +
+ + {BANK_INFO.bank} + + + {BANK_INFO.account} + + + 예금주 : {BANK_INFO.holder} + +
+
+ + {/* 입금자명 */} +
+ + 입금자명 + + {isEditingName ? ( +
+ setName(e.target.value)} + onBlur={() => setIsEditingName(false)} + onKeyDown={(e) => { + if (e.key === "Enter") setIsEditingName(false); + }} + className="typo-16-700 text-color-gray-900 h-[24px] w-[50px] bg-transparent text-center outline-none" + maxLength={6} + /> +
+ ) : ( + + )} +
+ + {/* 입금액 */} +
+ + 입금액 + +
+ + {amount.toLocaleString()} + + +
+
+
+ + {/* ── 동의 체크 ── */} +
+ + + 상기 계좌로 입금을 완료했습니다. + +
+
+ + {/* ── 하단 버튼 영역 ── */} +
+ {/* CTA 버튼 */} + + + {/* Toss 링크 */} + + 혹은 Toss로 계좌이체하기 + +
+
+
+
+
+ ); +} diff --git a/components/common/charge/QuickBundleCard.tsx b/components/common/charge/QuickBundleCard.tsx index b7f26c5..5dc8d3e 100644 --- a/components/common/charge/QuickBundleCard.tsx +++ b/components/common/charge/QuickBundleCard.tsx @@ -3,6 +3,7 @@ import React from "react"; import Image from "next/image"; import { ICON_SIZE } from "@/lib/constants/charge"; +import ConfirmChargeDrawer from "@/components/common/charge-confirm/ConfirmChargeDrawer"; interface QuickBundleCardProps { title: string; @@ -10,6 +11,7 @@ interface QuickBundleCardProps { description: string; bonus: string; price: string; + priceValue: number; } export default function QuickBundleCard({ @@ -18,6 +20,7 @@ export default function QuickBundleCard({ description, bonus, price, + priceValue, }: QuickBundleCardProps) { return (
@@ -40,12 +43,17 @@ export default function QuickBundleCard({
- + + {price} + + } + amount={priceValue} + /> ); } diff --git a/components/common/charge/ShopContent.tsx b/components/common/charge/ShopContent.tsx index eca3740..cba821b 100644 --- a/components/common/charge/ShopContent.tsx +++ b/components/common/charge/ShopContent.tsx @@ -4,10 +4,11 @@ import React from "react"; import { useItems } from "@/hooks/useItems"; import ChargeInventoryCard from "./ChargeInventoryCard"; import QuickBundleCard from "./QuickBundleCard"; +import { ShopItemRow } from "./ShopRows"; +import { ShopBundleRow } from "./ShopRows"; import { QUICK_BUNDLES, - INDIVIDUAL_ITEMS, - BUNDLE_ITEMS, + SHOP_ITEMS_API, USAGE_INFO, BUSINESS_INFO, } from "@/lib/constants/charge"; @@ -20,6 +21,14 @@ export default function ShopContent() { option: data?.data.optionTicketCount ?? 0, }; + // 개별 아이템: 리워드가 1개인 경우 + const individualItems = SHOP_ITEMS_API.filter( + (item) => item.rewards.length === 1, + ); + + // 번들 아이템: 리워드가 2개 이상인 경우 + const bundleItems = SHOP_ITEMS_API.filter((item) => item.rewards.length >= 2); + return (
{/* ── 보유현황 카드 ── */} @@ -39,21 +48,8 @@ export default function ShopContent() {
전체
- {INDIVIDUAL_ITEMS.map((item) => ( -
- - {item.label} - - -
+ {individualItems.map((item) => ( + ))}
@@ -62,31 +58,8 @@ export default function ShopContent() {
번들
- {BUNDLE_ITEMS.map((item) => ( -
-
-
- - {item.title} - - - {item.description} - -
- - {item.bonus} - -
- -
+ {bundleItems.map((item) => ( + ))}
diff --git a/components/common/charge/ShopRows.tsx b/components/common/charge/ShopRows.tsx new file mode 100644 index 0000000..fc09ad7 --- /dev/null +++ b/components/common/charge/ShopRows.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import ConfirmChargeDrawer from "@/components/common/charge-confirm/ConfirmChargeDrawer"; +import { ShopItemAPI } from "@/lib/constants/charge"; + +/* ── ShopItemRow ── */ + +export interface ShopItemRowProps { + item: ShopItemAPI; +} + +export function ShopItemRow({ item }: ShopItemRowProps) { + return ( +
+ {item.name} + + {item.price.toLocaleString()}원 + + } + amount={item.price} + /> +
+ ); +} + +/* ── ShopBundleRow ── */ + +export interface ShopBundleRowProps { + item: ShopItemAPI; +} + +export function ShopBundleRow({ item }: ShopBundleRowProps) { + // 리워드 목록에서 보너스 항목 제외한 메인 리워드 텍스트 생성 + const mainRewards = item.rewards.filter( + (r) => !r.itemName.includes("보너스"), + ); + const description = mainRewards + .map((r) => `${r.itemName} ${r.quantity}개`) + .join("+"); + + // 보너스 항목 찾기 + const bonusReward = item.rewards.find((r) => r.itemName.includes("보너스")); + + return ( +
+
+
+ {item.name} + + {description} + +
+ {bonusReward && ( + + {bonusReward.itemName} {bonusReward.quantity}개 무료 증정! + + )} +
+ + {item.price.toLocaleString()}원 + + } + amount={item.price} + /> +
+ ); +} diff --git a/lib/constants/charge.ts b/lib/constants/charge.ts index 9228882..9fe68f2 100644 --- a/lib/constants/charge.ts +++ b/lib/constants/charge.ts @@ -34,6 +34,7 @@ export const QUICK_BUNDLES = [ description: "뽑기권 5개+옵션권 2개", bonus: "옵션권 1개 무료 증정!", price: "5,000원", + priceValue: 5000, }, { title: "슈퍼 번들", @@ -41,48 +42,66 @@ export const QUICK_BUNDLES = [ description: "뽑기권 10개+옵션권 10개", bonus: "옵션권 5개 무료 증정!", price: "10,000원", + priceValue: 10000, }, ] as const; -/* ── 가격 상수 ── */ -export const ITEM_PRICES = { - matching: 1000, - option: 200, -} as const; +/* ── API 응답 데이터 타입 ── */ +export interface Reward { + itemType: "MATCHING_TICKET" | "OPTION_TICKET"; + itemName: string; + quantity: number; +} + +export interface ShopItemAPI { + id: number; + name: string; + price: number; + rewards: Reward[]; +} -/* ── 전체(개별) 아이템 ── */ -export const INDIVIDUAL_ITEMS = [ +/* ── 전체 아이템 (API 더미 데이터) ── */ +export const SHOP_ITEMS_API: ShopItemAPI[] = [ { - label: "뽑기권 1개", - price: `${ITEM_PRICES.matching.toLocaleString()}원`, + id: 1, + name: "뽑기권 1개", + price: 1000, + rewards: [{ itemType: "MATCHING_TICKET", itemName: "매칭권", quantity: 1 }], }, { - label: "옵션권 1개", - price: `${ITEM_PRICES.option.toLocaleString()}원`, + id: 2, + name: "옵션권 1개", + price: 200, + rewards: [{ itemType: "OPTION_TICKET", itemName: "옵션권", quantity: 1 }], }, -] as const; - -/* ── 번들 아이템 ── */ -export const BUNDLE_ITEMS = [ { - title: "미니 번들", - description: "옵션권 3개", - bonus: "옵션권 1개 무료 증정!", - price: "2,500원", + id: 10, + name: "미니 번들", + price: 2500, + rewards: [ + { itemType: "OPTION_TICKET", itemName: "옵션권", quantity: 3 }, + { itemType: "OPTION_TICKET", itemName: "보너스 옵션권", quantity: 1 }, + ], }, { - title: "실속 번들", - description: "뽑기권 5개+옵션권 2개", - bonus: "옵션권 1개 무료 증정!", - price: "5,000원", + id: 11, + name: "실속 번들", + price: 5000, + rewards: [ + { itemType: "MATCHING_TICKET", itemName: "매칭권", quantity: 5 }, + { itemType: "OPTION_TICKET", itemName: "옵션권", quantity: 2 }, + ], }, { - title: "슈퍼 번들", - description: "뽑기권 10개+옵션권 10개", - bonus: "옵션권 5개 무료 증정!", - price: "10,000원", + id: 12, + name: "슈퍼 번들", + price: 10000, + rewards: [ + { itemType: "MATCHING_TICKET", itemName: "매칭권", quantity: 10 }, + { itemType: "OPTION_TICKET", itemName: "옵션권", quantity: 10 }, + ], }, -] as const; +]; /* ── 이용안내 및 사업자 정보 ── */ export const USAGE_INFO = `충전된 포인트의 소멸시효 기한은 충전 후 5년입니다. @@ -134,3 +153,10 @@ export const TABS = [ { label: "충전내역", title: "충전내역" }, { label: "입금자명 설정", title: "입금자명 설정" }, ] as const; + +/* ── 입금 계좌 정보 (상수) ── */ +export const BANK_INFO = { + bank: "토스뱅크", + account: "1002-4809-1716", + holder: "천승환", +} as const; From 2066dbd7d29a943a3c304a7de786441c0fcf7954 Mon Sep 17 00:00:00 2001 From: dasosann Date: Fri, 24 Apr 2026 14:01:25 +0900 Subject: [PATCH 2/2] =?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 --- .../charge-confirm/ConfirmChargeDrawer.tsx | 6 +-- .../common/charge/DepositorNameContent.tsx | 9 ++++- lib/constants/charge.ts | 40 ++++++++++++++++++- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/components/common/charge-confirm/ConfirmChargeDrawer.tsx b/components/common/charge-confirm/ConfirmChargeDrawer.tsx index 553f777..b01e59e 100644 --- a/components/common/charge-confirm/ConfirmChargeDrawer.tsx +++ b/components/common/charge-confirm/ConfirmChargeDrawer.tsx @@ -29,7 +29,7 @@ interface ConfirmChargeDrawerProps { export default function ConfirmChargeDrawer({ trigger, amount, - depositorName = "박승원", + depositorName = "천승환", }: ConfirmChargeDrawerProps) { const [agreed, setAgreed] = React.useState(false); const [name, setName] = React.useState(depositorName); @@ -126,7 +126,7 @@ export default function ConfirmChargeDrawer({ onKeyDown={(e) => { if (e.key === "Enter") setIsEditingName(false); }} - className="typo-16-700 text-color-gray-900 h-[24px] w-[50px] bg-transparent text-center outline-none" + className="typo-16-700 text-color-gray-900 h-[24px] w-[90px] bg-transparent text-center outline-none" maxLength={6} />
@@ -185,7 +185,7 @@ export default function ConfirmChargeDrawer({
{/* CTA 버튼 */} {/* Toss 링크 */} diff --git a/components/common/charge/DepositorNameContent.tsx b/components/common/charge/DepositorNameContent.tsx index 91c747c..da06dbe 100644 --- a/components/common/charge/DepositorNameContent.tsx +++ b/components/common/charge/DepositorNameContent.tsx @@ -3,6 +3,7 @@ import React from "react"; import { cn } from "@/lib/utils"; import { isValidDepositorName } from "@/lib/validators"; +import Button from "@/components/ui/Button"; export default function DepositorNameContent() { const [name, setName] = React.useState(""); @@ -15,7 +16,7 @@ export default function DepositorNameContent() { }; return ( -
+
{/* ── 입금자명 입력 ── */}
입금자명 @@ -31,7 +32,7 @@ export default function DepositorNameContent() { value={name} onChange={handleNameChange} placeholder="이름을 입력해주세요" - className="typo-16-500 w-full bg-transparent text-center text-[#1A1A1A] outline-none placeholder:text-[#B3B3B3]" + className="typo-16-500 w-full bg-transparent text-left text-[#1A1A1A] outline-none placeholder:text-[#B3B3B3]" maxLength={6} />
@@ -72,6 +73,10 @@ export default function DepositorNameContent() {

+ +
); } diff --git a/lib/constants/charge.ts b/lib/constants/charge.ts index 9fe68f2..5c32990 100644 --- a/lib/constants/charge.ts +++ b/lib/constants/charge.ts @@ -120,6 +120,15 @@ export const BUSINESS_INFO = export const CHARGE_HISTORY = [ { id: 1, + date: "2024.04.12", + orderId: "20240412-0051", + points: "뽑기권 10개", + paymentAmount: "10,000원", + status: "success", + statusLabel: "결제완료", + }, + { + id: 2, date: "2024.03.20", orderId: "20240320-0001", points: "뽑기권 5개", @@ -128,7 +137,7 @@ export const CHARGE_HISTORY = [ statusLabel: "결제완료", }, { - id: 2, + id: 3, date: "2024.03.18", orderId: "20240318-0042", points: "옵션권 10개", @@ -137,7 +146,7 @@ export const CHARGE_HISTORY = [ statusLabel: "결제취소", }, { - id: 3, + id: 4, date: "2024.03.15", orderId: "20240315-0012", points: "뽑기권 1개 (이벤트)", @@ -145,6 +154,33 @@ export const CHARGE_HISTORY = [ status: "event", statusLabel: "이벤트 증정", }, + { + id: 5, + date: "2024.03.10", + orderId: "20240310-0089", + points: "옵션권 20개", + paymentAmount: "10,000원", + status: "success", + statusLabel: "결제완료", + }, + { + id: 6, + date: "2024.03.05", + orderId: "20240305-0022", + points: "뽑기권 30개", + paymentAmount: "30,000원", + status: "success", + statusLabel: "결제완료", + }, + { + id: 7, + date: "2024.02.28", + orderId: "20240228-0015", + points: "옵션권 5개", + paymentAmount: "2,500원", + status: "cancelled", + statusLabel: "결제취소", + }, ] as const; /* ── 탭 정의 ── */