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
19 changes: 12 additions & 7 deletions design-system/ui/modals/QrModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,37 @@ import qr_check from '../../icons/QrCheck.svg';
interface QrModalProps {
isChecked: boolean; // QR 상태
iconPath1?: React.ReactElement; // 모달 배경 이미지
iconPath2?: React.ReactElement; // QR 이미지
ticketQrCode: string;
className?: string;
title: string; // 이벤트명
hostName: string; // 주최명
date: string; // 이벤트 (시작)날짜
location: string; // 이벤트 장소
ticketName: string; // 티켓 이름
price: number; // 티켓 가격
isApproved: boolean; // 티켓 승인 여부
orderStatus: string; // 티켓 승인 여부
isCheckIn: boolean; // 참가자 체크인 여부
isCountdownChecked: boolean;
remainDays: string; //d-day
onClick: () => void;
}

const QrModal = ({
isChecked,
iconPath1,
iconPath2,
ticketQrCode,
className,
title,
hostName,
date,
location,
ticketName,
price,
isApproved,
orderStatus,
isCheckIn,
isCountdownChecked,
remainDays,

onClick,
}: QrModalProps) => {
const formattedPrice = price.toLocaleString();
Expand All @@ -53,11 +56,13 @@ const QrModal = ({
<div className={`${flexColumnCenter} h-screen ${className}`}>
<div>
{iconPath1 && <div className="w-full">{iconPath1}</div>}
{iconPath2 && <div className={`ml-[8%] -mt-[176%] ${isChecked ? '' : 'opacity-50'}`}>{iconPath2}</div>}
<div className={`ml-[5%] -mt-[180%] ${isChecked ? '' : 'opacity-50'}`}>
<img src={`data:image/png;base64,${ticketQrCode}`} alt="QR Code" className="w-60 h-60" />
</div>
<div className={`${flexColumn} justify-start px-6 ${isChecked ? '' : 'opacity-50'}`}>
<div className={`${flexRowSpaceBetweenCenter} w-full mt-[22%]`}>
<h1 className="truncate max-w-48 mr-2 font-semibold">{title}</h1>
<Countdown children={`${isCountdownChecked ? 'D-5' : '완료'}`} isChecked={isCountdownChecked} />
<Countdown children={remainDays} isChecked={isCountdownChecked} />
</div>
<h2 className="text-deDayTextDark text-xs font-medium mb-2">{hostName}</h2>
<div className="space-y-1 text-deDayTextDark">
Expand All @@ -84,7 +89,7 @@ const QrModal = ({
<IconText
size="xSmall"
iconPath={<img src={qr_check} alt="qr_check" />}
children={isApproved ? '승인 됨' : '승인 안됨'}
children={orderStatus === 'COMPLETED' ? '승인됨' : '대기 중'}
className="text-11"
></IconText>
<IconText
Expand Down
11 changes: 11 additions & 0 deletions public/assets/menu/completed.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/assets/menu/pending.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/features/dashboard/ui/ResponseFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import IconButton from '../../../../design-system/ui/buttons/IconButton';
import { responsesData } from '../../../shared/types/responseType';
import DropDown from '../../../shared/ui/DropDown';
import { createFieldMappings } from '../../lib/createFieldMappings';
import { createFieldMappings } from '../../../shared/lib/createFieldMappings';
import rightButton from '../../../../public/assets/main/RightButton.svg';
import leftButton from '../../../../public/assets/main/LeftButton.svg';

Expand Down
2 changes: 1 addition & 1 deletion src/features/dashboard/ui/ResponsesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useResponseStore } from '../model/ResponseStore';
import { responsesInfo } from '../../../shared/types/responseType';
import ResponseFilter from './ResponseFilter';
import SelectedResponseList from './SelectedResponseList';
import { createFieldMappings } from '../../lib/createFieldMappings';
import { createFieldMappings } from '../../../shared/lib/createFieldMappings';
import { useEffect } from 'react';

interface ResponsesListProps {
Expand Down
14 changes: 14 additions & 0 deletions src/features/ticket/api/order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { axiosClient } from "../../../shared/types/api/http-client"
export const readMyTickets = {
get: async (page: number = 0, size: number = 10) => {
try {
const response = await axiosClient.get("/orders", {
params: { page, size },
});
return response.data;
} catch (error) {
console.error("티켓 조회 중 오류 발생:", error);
throw error;
}
},
}
97 changes: 74 additions & 23 deletions src/pages/menu/ui/MyTicketPage.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,101 @@
import TicketHostLayout from '../../../shared/ui/backgrounds/TicketHostLayout';
import TicketLogo from '../../../../public/assets/menu/TicketLogo.svg';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import QrModal from '../../../../design-system/ui/modals/QrModal';
import QRbackground from '../../../../design-system/icons/QRbackground.svg';
import QRcode from '../../../../design-system/icons/QrCode.svg';
import { trendingEvents } from '../../../shared/types/eventCardType';
import EventCard from '../../../shared/ui/EventCard';
import { readMyTickets } from '../../../features/ticket/api/order';
import completedImg from '../../../../public/assets/menu/Completed.svg';
import pendingImg from '../../../../public/assets/menu/Pending.svg';
import ticketImg from '../../../../public/assets/menu/Ticket.svg';

type Ticket = {
id: number;
eventId: number;
bannerImageUrl: string;
title: string;
hostChannelName: string;
address: string;
startDate: string;
ticketName: string;
orderStatus: "COMPLETED" | "PENDING" | "CANCELED";
remainDays: string;
ticketPrice: number;
ticketQrCode: string;
checkIn: boolean;
hashtags: [];
};

const MyTicketPage = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [myTickets, setMyTickets] = useState<Ticket[]>([]);
const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null);

useEffect(() => {
const fetchMyTickets = async () => {
try {
const response = await readMyTickets.get(0, 10);
setMyTickets(response.result || []);
} catch (error) {
console.error("티켓 목록 불러오기 실패:", error);
}
};
fetchMyTickets();
Comment on lines +35 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

API 에러 처리 개선 필요

현재 코드는 API 오류를 콘솔에만 기록하고 있습니다. 사용자 경험을 향상시키기 위해 오류 메시지를 표시하고 재시도 기능을 제공하는 것이 좋습니다.

API가 실패할 때 어떤 종류의 에러를 반환하는지 확인하고, 각 에러 유형에 맞는 사용자 친화적인 메시지를 표시하는 것이 좋습니다. 다음과 같은 쉘 스크립트로 API 응답을 확인해볼 수 있습니다:


🏁 Script executed:

#!/bin/bash
# API 오류 응답 형식 확인

# API 호출 예시 코드 검색
rg -A 3 -B 3 "catch \(error\)" --glob "src/features/ticket/api/*.ts"

# 오류 처리 관련 다른 참조 검색
rg "axiosClient" --glob "src/**/*.ts" | grep -i "error"

Length of output: 546


API 에러 처리 개선 요청

현재 MyTicketPage.tsxfetchMyTickets 함수에서 API 호출 실패 시 단순히 콘솔에 에러만 기록하고 있습니다.
이와 유사하게 src/features/ticket/api/order.ts에서도 에러를 콘솔에 기록한 후 에러를 재던지는 방식으로 처리되고 있음을 확인했습니다.

다음 사항들을 개선해주시기 바랍니다:

  • API 오류 발생 시 사용자에게 친절한 메시지(예: “티켓 목록을 불러오지 못했습니다. 다시 시도해주세요.”)를 UI에 표시
  • 재시도 기능 또는 오류 복구 기능 구현
  • API로부터 반환되는 구체적인 오류 코드나 메시지에 따라 사용자에게 적절히 안내하도록 에러 핸들링 로직 개선

이와 같이 에러 처리를 개선하여 사용자 경험을 높일 필요가 있습니다.

}, []);
Comment on lines +34 to +44
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

티켓 데이터 로딩 로직 개선 필요

useEffect를 사용한 데이터 로딩이 기본적으로 잘 구현되었습니다. 하지만 몇 가지 개선할 점이 있습니다:

  1. 로딩 상태 표시 기능이 없습니다.
  2. 데이터가 없는 경우에 대한 처리가 없습니다.
  3. 오류 발생 시 사용자에게 알림을 주는 기능이 없습니다.

다음과 같이 개선해 보세요:

+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchMyTickets = async () => {
+     setIsLoading(true);
+     setError(null);
      try {
        const response = await readMyTickets.get(0, 10);
        setMyTickets(response.result || []);
      } catch (error) {
        console.error("티켓 목록 불러오기 실패:", error);
+       setError("티켓 정보를 불러오는 중 오류가 발생했습니다.");
      } finally {
+       setIsLoading(false);
      }
    };
    fetchMyTickets();
  }, []);


return (
<TicketHostLayout image={TicketLogo} centerContent="내 티켓" showText={true}>
{/* 이벤트 카드 목록 */}
<div className="grid grid-cols-2 gap-4 mx-6 mt-28 md:grid-cols-2 lg:grid-cols-2 pb-4">
{trendingEvents.map((event, index) => (
{myTickets.map((ticket, index) => (
<EventCard
key={index}
img={event.img}
eventTitle={event.eventTitle}
dDay={event.dDay}
host={event.host}
eventDate={event.eventDate}
location={event.location}
hashtags={event.hashtags}
onClick={() => setIsModalOpen(true)}
/>
img={ticket.bannerImageUrl}
eventTitle={ticket.title}
dDay={ticket.remainDays}
host={ticket.hostChannelName}
eventDate={ticket.startDate}
location={ticket.address}
hashtags={ticket.hashtags}
onClick={() => {
setSelectedTicket(ticket);
setIsModalOpen(true);
}}
>
<div className="flex items-center text-xs text-gray-500">
<img src={ticketImg} alt="날짜" className="w-3 h-3 mr-1" />
{ticket.ticketName}
</div>
<div className="flex items-center text-xs text-gray-500">
<img
src={ticket.orderStatus === 'COMPLETED' ? completedImg : pendingImg}
alt={ticket.orderStatus === 'COMPLETED' ? '승인됨' : '대기 중'}
className="w-3 h-3 mr-1"
/>
{ticket.orderStatus === 'COMPLETED' ? '승인됨' : '대기 중'}
</div>

</EventCard>
))}
</div>

{isModalOpen && (
{isModalOpen && selectedTicket &&(
<div className="fixed top-0 left-0 w-full h-full z-20">
<div className="relative mx-auto w-full max-w-lg bg-black bg-opacity-30">
<QrModal
isChecked={true}
iconPath1={<img src={QRbackground} alt="QRbackground" />}
iconPath2={<img src={QRcode} alt="QRcode" />}
title="이벤트명"
hostName="주최명"
date="2025-01-01"
location="이벤트 장소"
ticketName="티켓 이름"
price={50000}
isApproved={true}
isCheckIn={false}
ticketQrCode = {selectedTicket.ticketQrCode}
title={selectedTicket.title}
hostName={selectedTicket.hostChannelName}
date={selectedTicket.startDate}
location={selectedTicket.address}
ticketName={selectedTicket.ticketName}
price={selectedTicket.ticketPrice}
orderStatus={selectedTicket.orderStatus}
isCheckIn={selectedTicket.checkIn}
isCountdownChecked={true}
remainDays={selectedTicket.remainDays}
onClick={() => setIsModalOpen(false)}
/>
</div>
Expand Down
20 changes: 17 additions & 3 deletions src/shared/ui/EventCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useLocation, useNavigate } from 'react-router-dom';
import TertiaryButton from '../../../design-system/ui/buttons/TertiaryButton';
import Countdown from '../../../design-system/ui/texts/Countdown';
import dateImg from '../../../public/assets/event-manage/details/Date.svg';
import locationImg from '../../../public/assets/event-manage/details/Location.svg'

interface EventCardProps {
img: string;
Expand All @@ -11,9 +13,10 @@ interface EventCardProps {
location: string;
hashtags: string[];
onClick?: () => void;
children?: React.ReactNode;
}

const EventCard = ({ img, eventTitle, dDay, host, eventDate, location, hashtags, onClick }: EventCardProps) => {
const EventCard = ({ img, eventTitle, dDay, host, eventDate, location, hashtags, onClick, children }: EventCardProps) => {
const navigate = useNavigate();
const { pathname } = useLocation();

Expand All @@ -32,8 +35,19 @@ const EventCard = ({ img, eventTitle, dDay, host, eventDate, location, hashtags,
</div>

<p className="text-xs text-gray-500">{host}</p>
<p className="text-xs text-gray-500">{eventDate}</p>
<p className="text-xs text-gray-500">{location}</p>

<div className="flex items-center text-xs text-gray-500">
<img src={dateImg} alt="날짜" className="w-3 h-3 mr-1" />
{eventDate}
</div>

<div className="flex items-center text-xs text-gray-500">
<img src={locationImg} alt="위치" className="w-3 h-3 mr-1" />
{location}
</div>

{/* 승인 여부 표시 */}
{children}
{/* 해시태그 */}
<div className="flex flex-wrap w-full h-6 mt-2 overflow-hidden text-xs font-semibold text-gray-700 whitespace-nowrap">
{hashtags.map((tag, index) => (
Expand Down