-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 충전모달 디자인 #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 충전모달 디자인 #57
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| "use client"; | ||
|
|
||
| import React from "react"; | ||
| import { | ||
| Drawer, | ||
| DrawerClose, | ||
| DrawerContent, | ||
| DrawerHeader, | ||
| DrawerTitle, | ||
| DrawerTrigger, | ||
| } from "@/components/ui/drawer"; | ||
| 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 TAB_CONTENT = [ShopContent, ChargeHistoryContent, DepositorNameContent]; | ||
|
|
||
| /* ────────────────────────────────────── */ | ||
|
|
||
| export default function ChargeDrawer({ trigger }: ChargeDrawerProps) { | ||
| const [activeTab, setActiveTab] = React.useState(0); | ||
|
|
||
| const ActiveContent = TAB_CONTENT[activeTab]; | ||
|
Comment on lines
+29
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 탭 전환 시 상태 유지 문제가 발생할 수 있습니다. 현재 References
|
||
|
|
||
| return ( | ||
| <Drawer> | ||
| <DrawerTrigger asChild>{trigger}</DrawerTrigger> | ||
| <DrawerContent | ||
| className="rounded-t-[16px] bg-white outline-none" | ||
|
dasosann marked this conversation as resolved.
|
||
| showHandle={false} | ||
| > | ||
| <div className="flex max-h-[90dvh] flex-col overflow-hidden"> | ||
| {/* ── Header ── */} | ||
| <DrawerHeader className="shrink-0 gap-0 px-6 pt-6 pb-0"> | ||
| <div className="relative flex items-center justify-between"> | ||
| <div className="w-7" /> | ||
| <DrawerTitle className="typo-16-700 flex-1 text-center text-[#373737]"> | ||
| {TABS[activeTab].title} | ||
| </DrawerTitle> | ||
| <DrawerClose className="typo-16-500 text-color-gray-400 w-7 text-right"> | ||
| 닫기 | ||
| </DrawerClose> | ||
| </div> | ||
| </DrawerHeader> | ||
|
|
||
| {/* ── 탭 칩 ── */} | ||
| <ChargeTabs activeTab={activeTab} setActiveTab={setActiveTab} /> | ||
|
|
||
| {/* ── Scrollable Content ── */} | ||
| <div className="flex-1 overflow-y-auto px-6 pb-8"> | ||
| <ActiveContent /> | ||
| </div> | ||
| </div> | ||
| </DrawerContent> | ||
| </Drawer> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div className="flex flex-col gap-[23px] pt-8"> | ||
| {/* ── 보유현황 카드 ── */} | ||
| <ChargeInventoryCard ticketCounts={ticketCounts} /> | ||
|
|
||
| {/* ── 충전 내역 리스트 ── */} | ||
| <div className="flex flex-col"> | ||
| {CHARGE_HISTORY.map((item) => ( | ||
| <ChargeHistoryListItem key={item.id} {...item} /> | ||
| ))} | ||
| </div> | ||
|
|
||
| {/* ── 유의사항 ── */} | ||
| <div className="flex flex-col gap-2 pb-6"> | ||
| <span className="typo-10-600 text-color-gray-400">유의사항</span> | ||
| <div className="flex flex-col gap-2"> | ||
| <p className="typo-10-500 text-color-gray-400 leading-[180%]"> | ||
| {NOTICE_INFO} | ||
| </p> | ||
| <p className="typo-10-500 text-color-gray-400 leading-[140%]"> | ||
| {BUSINESS_INFO} | ||
| </p> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div className="border-color-gray-100 flex flex-col gap-2 border-b py-4"> | ||
| {/* 날짜 | 주문번호 */} | ||
| <span className={cn("typo-12-500", styles.dateColor)}> | ||
| {date} | {orderId} | ||
| </span> | ||
|
|
||
| {/* 포인트 · 결제금액 */} | ||
| <div className="flex items-center justify-between"> | ||
| <span className={cn("typo-16-600", styles.pointsColor)}>{points}</span> | ||
| <span | ||
| className={cn( | ||
| "typo-16-600", | ||
| styles.amountColor, | ||
| styles.amountStrike && "line-through", | ||
| )} | ||
| > | ||
| {paymentAmount} | ||
| </span> | ||
| </div> | ||
|
|
||
| {/* 상태 */} | ||
| <span className="typo-12-600 text-color-gray-400">{statusLabel}</span> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div className="bg-color-gray-50 flex flex-col gap-2 rounded-[16px] p-4"> | ||
| {/* 보유 수량 */} | ||
| <div className="border-color-gray-100 flex flex-col gap-2 border-b pb-3"> | ||
| {INVENTORY_ROWS.map((row) => ( | ||
| <div key={row.key} className="flex items-center gap-2"> | ||
| <Image | ||
| src={row.icon} | ||
| alt={row.alt} | ||
| width={ICON_SIZE.sm} | ||
| height={ICON_SIZE.sm} | ||
| /> | ||
| <div className="flex items-center gap-8"> | ||
| <span className="typo-10-600 text-color-gray-400"> | ||
| {row.label} | ||
| </span> | ||
| <span className="typo-16-700 text-color-text-black"> | ||
| {ticketCounts[row.key as keyof typeof ticketCounts]}개 | ||
| </span> | ||
| </div> | ||
| </div> | ||
| ))} | ||
| </div> | ||
|
|
||
| {/* 구매 가능 한도 */} | ||
| <div className="flex items-center justify-between py-1"> | ||
| <span className="typo-10-600 text-color-gray-400">구매 가능 한도</span> | ||
| <div className="flex items-center gap-6"> | ||
| {Object.values(PURCHASE_LIMITS).map((limit) => ( | ||
| <span key={limit.label} className="typo-10-600 text-color-gray-400"> | ||
| {limit.label} {limit.current}/{limit.max} | ||
| </span> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <ul className="flex shrink-0 items-start gap-2 px-6 pt-6"> | ||
| {TABS.map((tab, i) => ( | ||
| <li key={tab.label}> | ||
| <button | ||
| type="button" | ||
| onClick={() => setActiveTab(i)} | ||
| className={cn( | ||
| "typo-14-500 flex h-[33px] items-center justify-center rounded-full px-4", | ||
| activeTab === i | ||
| ? "bg-[#1A1A1A] text-white" | ||
| : "border border-[#E5E5E5] bg-[#F5F5F5] text-[#666666]", | ||
| )} | ||
| > | ||
| {tab.label} | ||
| </button> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| ); | ||
| }; | ||
|
|
||
| export default ChargeTabs; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| "use client"; | ||
|
|
||
| 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<HTMLInputElement>) => { | ||
| const value = e.target.value; | ||
| if (isValidDepositorName(value)) { | ||
| setName(value); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="flex flex-col gap-[23px] pt-8"> | ||
| {/* ── 입금자명 입력 ── */} | ||
| <div className="flex flex-col gap-2"> | ||
| <span className="typo-14-500 text-[#666666]">입금자명</span> | ||
| <div | ||
| className="flex h-[48px] items-center border-b border-[#B3B3B3] px-2" | ||
| style={{ | ||
| background: | ||
| "linear-gradient(180deg, rgba(245, 245, 245, 0.03) 0%, rgba(245, 245, 245, 0.24) 100%)", | ||
| }} | ||
| > | ||
| <input | ||
| type="text" | ||
| value={name} | ||
| onChange={handleNameChange} | ||
| placeholder="이름을 입력해주세요" | ||
| className="typo-16-500 w-full bg-transparent text-center text-[#1A1A1A] outline-none placeholder:text-[#B3B3B3]" | ||
| maxLength={6} | ||
|
Comment on lines
+21
to
+35
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 입금자명 입력 필드에 대한 접근성을 개선해야 합니다. References
|
||
| /> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* ── 예시 이미지 ── */} | ||
| <div className="flex flex-col gap-2"> | ||
| <span className="typo-14-500 text-[#666666]">예시</span> | ||
| <div className="bg-color-gray-50 flex h-[121px] w-full items-center justify-center rounded-[16px] border border-[#EFEFEF]"> | ||
| <span className="typo-12-500 text-color-gray-400"> | ||
| Toss 앱 입금자명 설정 예시 이미지 | ||
| </span> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* ── 안내 가이드 (Frame 2612440) ── */} | ||
| <div className="bg-color-gray-50 flex flex-col gap-6 rounded-[16px] p-4"> | ||
| {/* 유의사항 */} | ||
| <div className="flex flex-col gap-2"> | ||
| <span className="typo-14-600 text-[#808080]">유의사항</span> | ||
| <p className="typo-14-500 leading-[160%] text-[#999999]"> | ||
| 코매칭에 보내실 입금자명을 입력해 주세요. 입금자명은 한글이나 | ||
| 영문자로 6자 이내입니다. 특수문자는 사용할 수 없습니다. Toss 앱에 | ||
| 사전 설정된 입금자명을 입력하시면 간편합니다. 나중에 다시 수정할 수 | ||
| 있어요! | ||
| </p> | ||
| </div> | ||
|
|
||
| {/* FAQ */} | ||
| <div className="flex flex-col gap-2"> | ||
| <span className="typo-14-600 text-[#808080]"> | ||
| 왜 입금자명을 설정해야 하나요? | ||
| </span> | ||
| <p className="typo-14-500 leading-[140%] text-[#999999]"> | ||
| 모든 포인트 충전은 계좌이체로 진행되고 있어요. 충전 간 복잡함을 | ||
| 줄이기 위해, 사전에 입금자명을 설정하시면 더욱 편리하고 쾌적한 | ||
| 코매칭을 즐기실 수 있습니다. | ||
| </p> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.