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
65 changes: 65 additions & 0 deletions components/common/ChargeDrawer.tsx
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";

Check warning on line 12 in components/common/ChargeDrawer.tsx

View workflow job for this annotation

GitHub Actions / lint

'cn' is defined but never used
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) {
Comment thread
dasosann marked this conversation as resolved.
const [activeTab, setActiveTab] = React.useState(0);

const ActiveContent = TAB_CONTENT[activeTab];
Comment on lines +29 to +31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

탭 전환 시 상태 유지 문제가 발생할 수 있습니다. 현재 ActiveContentactiveTab에 따라 교체되면서 이전 탭의 컴포넌트가 언마운트됩니다. 이로 인해 DepositorNameContent 탭에서 입력 중이던 이름 정보가 다른 탭으로 이동했다가 돌아오면 초기화됩니다. 사용자 경험을 위해 입력 상태를 ChargeDrawer 부모 컴포넌트로 끌어올리거나, 전역 상태 관리 또는 display: none 방식을 사용하여 상태를 유지하는 것을 권장합니다. 이는 폼 입력 값을 유지하여 사용자 경험을 개선하라는 저장소 규칙과 일치합니다.

References
  1. 사용자 경험 향상을 위해 폼 제출 과정이나 화면 전환 시에도 사용자가 입력한 데이터가 유실되지 않도록 상태를 유지해야 합니다.


return (
<Drawer>
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
<DrawerContent
className="rounded-t-[16px] bg-white outline-none"
Comment thread
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>
);
}
11 changes: 8 additions & 3 deletions components/common/MyCoinSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -68,9 +69,13 @@ const MyCoinSection = ({ className }: MyCoinSectionProps) => {
{optionTicketCount}개
</span>
</div>
<button className="bg-milky-pink typo-11-700 h-6.5 w-14.5 rounded-full text-white">
구매하기
</button>
<ChargeDrawer
trigger={
<button className="bg-milky-pink typo-11-700 h-6.5 w-14.5 rounded-full text-white">
충전하기
</button>
}
/>
</section>
);
};
Expand Down
47 changes: 47 additions & 0 deletions components/common/charge/ChargeHistoryContent.tsx
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>
);
}
63 changes: 63 additions & 0 deletions components/common/charge/ChargeHistoryListItem.tsx
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>
);
}
58 changes: 58 additions & 0 deletions components/common/charge/ChargeInventoryCard.tsx
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>
);
}
35 changes: 35 additions & 0 deletions components/common/charge/ChargeTabs.tsx
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;
77 changes: 77 additions & 0 deletions components/common/charge/DepositorNameContent.tsx
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";

Check warning on line 4 in components/common/charge/DepositorNameContent.tsx

View workflow job for this annotation

GitHub Actions / lint

'cn' is defined but never used
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

입금자명 입력 필드에 대한 접근성을 개선해야 합니다. <span> 대신 <label>을 사용하고 input 요소의 id와 연결하여 스크린 리더 지원 및 클릭 영역 확대를 보장하세요. 또한, 배경 그라데이션과 같이 Tailwind로 표현하기 어려운 정밀한 스타일은 저장소 규칙에 따라 인라인 스타일을 사용합니다.

        <label htmlFor="depositor-name" className="typo-14-500 text-[#666666]">
          입금자명
        </label>
        <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
            id="depositor-name"
            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}
          />
        </div>
References
  1. 입력 필드는 label과 연결되어야 하며 웹 표준을 준수해야 함 (link)
  2. Tailwind utility 클래스로 정확하게 표현하기 어려운 복잡한 그라데이션이나 정밀한 수치는 인라인 스타일을 사용합니다.

/>
</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>
);
}
Loading
Loading