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
22 changes: 9 additions & 13 deletions src/features/ticket/api/ticket.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import { axiosClient } from "../../../shared/types/api/http-client";
import { CreateTicketRequest } from "../model/ticketCreation";
import { CreateTicketRequest, ReadTicketResponse } from "../model/ticketInformation";

export const createTicket = async (data: CreateTicketRequest) => {
const response = await axiosClient.post('/tickets', data);
return response.data;
}

export const readTicket = {
getAll: async (eventId: number) => {
const response = await axiosClient.get("/tickets", {
params: { eventId },
});
return response.data;
},
export const readTicket = async (eventId: number): Promise<{ isSuccess: boolean; result: ReadTicketResponse[] }> => {
const response = await axiosClient.get("/tickets", {
params: { eventId },
});
return response.data;
}

export const deleteTicket = {
remove: async (ticketId: number) => {
const response = await axiosClient.delete(`/tickets/${ticketId}`);
return response.data;
},
export const deleteTicket = async (ticketId: number) => {
const response = await axiosClient.delete(`/tickets/${ticketId}`);
return response.data;
}
37 changes: 37 additions & 0 deletions src/features/ticket/hooks/useTicketHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { createTicket, deleteTicket, readTicket } from "../api/ticket";
import { CreateTicketRequest, ReadTicketResponse, TicketResponse } from "../model/ticketInformation";

export const useTickets = (eventId: number) => {
return useQuery<{ isSuccess: boolean; result: ReadTicketResponse[] }>({
queryKey: ['tickets', eventId],
queryFn: () => readTicket(eventId),
enabled: !!eventId,
});
};

export const useCreateTicket = () => {
return useMutation<TicketResponse, Error, CreateTicketRequest>({
mutationFn: createTicket,
onSuccess: () => {
alert('티켓이 성공적으로 저장되었습니다.');
window.location.reload();
},
onError: () => {
alert('티켓 저장에 실패했습니다. 다시 시도해주세요.');
},
});
};
Comment on lines +13 to +24
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

useCreateTicket 훅에 개선이 필요합니다.

티켓 생성을 위한 훅이 잘 구현되었지만, 페이지 리로드 방식에 개선이 필요합니다.

현재 방식은 window.location.reload()를 사용하여 전체 페이지를 새로고침하고 있습니다. 이는 사용자 경험을 저하시키고, React Query의 장점을 충분히 활용하지 못합니다. 다음과 같이 개선해보세요:

export const useCreateTicket = () => {
    return useMutation<TicketResponse, Error, CreateTicketRequest>({
        mutationFn: createTicket,
        onSuccess: () => {
            alert('티켓이 성공적으로 저장되었습니다.');
-           window.location.reload();
+           // 쿼리 무효화를 통해 데이터 갱신
+           // queryClient.invalidateQueries(['tickets']);
        },
        onError: () => {
            alert('티켓 저장에 실패했습니다. 다시 시도해주세요.');
        },
    });
};

이를 위해서는 QueryClient 인스턴스에 대한 접근이 필요합니다. 다음과 같이 모듈 최상단에 useQueryClient를 추가하고 훅 내부에서 사용할 수 있습니다:

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";

// 훅 내부
export const useCreateTicket = () => {
    const queryClient = useQueryClient();
    return useMutation<TicketResponse, Error, CreateTicketRequest>({
        // ...
        onSuccess: (data, variables) => {
            alert('티켓이 성공적으로 저장되었습니다.');
            queryClient.invalidateQueries(['tickets', variables.eventId]);
        },
        // ...
    });
};


export const useDeleteTicket = () => {
return useMutation<TicketResponse, Error, number>({
mutationFn: deleteTicket,
onSuccess: () => {
alert("티켓이 삭제되었습니다.");
window.location.reload();
},
onError: () => {
alert('티켓 삭제 중 오류가 발생했습니다.');
}
});
};
Comment on lines +26 to +37
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

useDeleteTicket 훅에도 동일한 개선이 필요합니다.

삭제 훅도 마찬가지로 페이지 리로드 대신 쿼리 무효화를 통한 데이터 갱신 방식으로 개선이 필요합니다.

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

export const useDeleteTicket = () => {
+   const queryClient = useQueryClient();
    return useMutation<TicketResponse, Error, number>({
        mutationFn: deleteTicket,
        onSuccess: () => {
            alert("티켓이 삭제되었습니다.");
-           window.location.reload();
+           // 모든 티켓 쿼리 무효화
+           queryClient.invalidateQueries({ queryKey: ['tickets'] });
        },
        onError: () => {
            alert('티켓 삭제 중 오류가 발생했습니다.');
        }
    });
};

또한 삭제 함수에 이벤트 ID를 함께 전달하여 특정 이벤트의 티켓 쿼리만 무효화하는 방식으로도 구현할 수 있습니다:

export const useDeleteTicket = () => {
    const queryClient = useQueryClient();
    return useMutation<TicketResponse, Error, { ticketId: number, eventId: number }>({
        mutationFn: ({ ticketId }) => deleteTicket(ticketId),
        onSuccess: (_, { eventId }) => {
            alert("티켓이 삭제되었습니다.");
            queryClient.invalidateQueries(['tickets', eventId]);
        },
        // ...
    });
};

이 방식을 사용하면 특정 이벤트의 티켓 목록만 갱신할 수 있어 더 효율적입니다.

2 changes: 1 addition & 1 deletion src/features/ticket/model/TicketContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createContext, ReactNode, useContext, useState } from 'react';
import { CreateTicketRequest } from './ticketCreation';
import { CreateTicketRequest } from './ticketInformation';

export interface TicketState {
ticketState: CreateTicketRequest;
Expand Down
12 changes: 0 additions & 12 deletions src/features/ticket/model/ticketCreation.ts

This file was deleted.

27 changes: 27 additions & 0 deletions src/features/ticket/model/ticketInformation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export interface CreateTicketRequest {
eventId: number;
ticketType: string;
ticketName: string;
ticketDescription: string;
ticketPrice: number;
availableQuantity: number;
startDate: string;
endDate: string;
startTime: string;
endTime: string;
}

export interface TicketResponse {
isSuccess: boolean;
code: string;
message: string;
result: string; // "ticketId: 2"
}

export interface ReadTicketResponse {
ticketId: number;
ticketName: string;
ticketDescription: string;
ticketPrice: number;
availableQuantity: number;
}
22 changes: 11 additions & 11 deletions src/pages/dashboard/ui/ticket/TicketCreatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ import ChoiceChip from '../../../../../design-system/ui/ChoiceChip';
import DefaultTextField from '../../../../../design-system/ui/textFields/DefaultTextField';
import Button from '../../../../../design-system/ui/Button';
import TicketDatePicker from '../../../../features/ticket/model/TicketDatePicker';
import { createTicket } from '../../../../features/ticket/api/ticket';
import { CreateTicketRequest } from '../../../../features/ticket/model/ticketCreation';
import { CreateTicketRequest } from '../../../../features/ticket/model/ticketInformation';
import { useCreateTicket } from '../../../../features/ticket/hooks/useTicketHook';
import { useNavigate, useParams } from 'react-router-dom';

const TicketCreatePage = () => {
const navigate = useNavigate();
const { mutate: createTicket } = useCreateTicket();
const { id } = useParams();
const eventId = Number(id);

const [ticketData, setTicketData] = useState<CreateTicketRequest>({
eventId: 1,
eventId: eventId,
ticketType: 'FIRST_COME',
ticketName: '',
ticketDescription: '',
Expand Down Expand Up @@ -76,14 +82,8 @@ const TicketCreatePage = () => {
alert('모든 필수 입력 항목을 작성해주세요.');
return;
}
try {
const response = await createTicket(ticketData);
console.log('티켓 저장 성공:', response);
alert('티켓이 성공적으로 저장되었습니다.');
} catch (err) {
console.error('티켓 저장에 실패했습니다.', err);
alert('티켓 저장에 실패했습니다. 다시 시도해주세요.');
}
createTicket(ticketData);
navigate(`/dashboard/${id}/ticket`);
Comment on lines +85 to +86
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

티켓 생성 로직이 React Query를 사용하여 개선되었습니다.

기존의 직접적인 API 호출 대신 React Query 기반 훅을 사용하여 코드가 간결해졌습니다. 하지만 다음 두 가지 개선점이 있습니다:

  1. 현재 방식은 API 호출 결과를 기다리지 않고 바로 페이지를 이동합니다.
  2. useCreateTicket 훅은 내부적으로 window.location.reload()를 호출하는데, 페이지 이동과 충돌할 수 있습니다.

성공 여부를 확인한 후 페이지를 이동하는 방식이 더 안정적일 것입니다:

-    createTicket(ticketData);
-    navigate(`/dashboard/${id}/ticket`);
+    createTicket(ticketData, {
+      onSuccess: () => {
+        navigate(`/dashboard/${id}/ticket`);
+      }
+    });

이 변경을 위해서는 useTicketHook.ts에서 useCreateTicket 훅도 함께 수정해야 합니다:

// useTicketHook.ts에서 수정 필요
export const useCreateTicket = () => {
    return useMutation<TicketResponse, Error, CreateTicketRequest>({
        mutationFn: createTicket,
        onSuccess: () => {
            alert('티켓이 성공적으로 저장되었습니다.');
            // window.location.reload(); // 제거
        },
        onError: () => {
            alert('티켓 저장에 실패했습니다. 다시 시도해주세요.');
        },
    });
};

};

return (
Expand Down
44 changes: 10 additions & 34 deletions src/pages/dashboard/ui/ticket/TicketListPage.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,24 @@
import DashboardLayout from '../../../../shared/ui/backgrounds/DashboardLayout';
import Ticket from '../../../../../public/assets/dashboard/ticket/Ticket(horizon).svg';
import TicketItem from '../../../../widgets/dashboard/ui/TicketItem';
import { useNavigate } from 'react-router-dom';
import { DASHBOARD_ROUTES } from '../../../../app/routes/routes';
import { useNavigate, useParams } from 'react-router-dom';
import HorizontalCardButton from '../../../../../design-system/ui/buttons/HorizontalCardButton';
import AddButton from '../../../../../public/assets/dashboard/ticket/AddButton.svg';
import { useEffect, useState } from 'react';
import { readTicket } from '../../../../features/ticket/api/ticket';

export interface ReadTicket {
ticketId: number;
ticketName: string;
ticketDescription: string;
ticketPrice: number;
availableQuantity: number;
}
import { useTickets } from '../../../../features/ticket/hooks/useTicketHook';

const TicketListPage = () => {
const navigate = useNavigate();

const navigateToTicketCreate = () => {
navigate(DASHBOARD_ROUTES.ticketCreate);
navigate(`/dashboard/${id}/ticket/create`);
};
const { id } = useParams();
const eventId = id ? parseInt(id) : 1;
const { data, isLoading, isError } = useTickets(eventId);

const [tickets, setTickets] = useState<ReadTicket[]>([]);
const eventId = 1; //수정 필요
if (isLoading) return <div>로딩 중...</div>;
if (isError) return <div>티켓 정보를 불러오는 중 오류가 발생했습니다.</div>;

useEffect(() => {
const fetchTickets = async () => {
try {
const data = await readTicket.getAll(eventId);
if (data.isSuccess && Array.isArray(data.result)) {
setTickets(data.result);
} else {
setTickets([]);
}
} catch (error) {
console.error("티켓 데이터를 불러오는 중 오류 발생:", error);
setTickets([]);
}
};
fetchTickets();
}, [eventId]);

return (
<DashboardLayout centerContent="WOOACON 2024">
<div className="mt-8 px-7">
Expand All @@ -69,8 +45,8 @@ const TicketListPage = () => {
<img src={Ticket} />
<p className="font-bold text-base md:text-lg">티켓</p>
</div>
{tickets.length > 0 ? (
tickets.map(value => <TicketItem key={value.ticketId} ticket={value}/>)
{data?.isSuccess && data?.result.length > 0 ? (
data.result.map(value => <TicketItem key={value.ticketId} ticket={value} />)
) : (
<div className="text-gray5 font-thin">현재 등록된 티켓이 없습니다.</div>
)}
Expand Down
20 changes: 5 additions & 15 deletions src/widgets/dashboard/ui/TicketItem.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
import AvailableTicket from '../../../../public/assets/dashboard/ticket/Ticket(gray).svg';
import PersonIcon from '../../../../public/assets/dashboard/ticket/PersonIcon.svg';
import { ReadTicket } from '../../../pages/dashboard/ui/ticket/TicketListPage';
import { motion } from "framer-motion";
import { useState } from 'react';
import { deleteTicket } from '../../../features/ticket/api/ticket';
import { ReadTicketResponse } from '../../../features/ticket/model/ticketInformation';
import { useDeleteTicket } from '../../../features/ticket/hooks/useTicketHook';

const TicketItem = ({ ticket }: { ticket: ReadTicket }) => {
const TicketItem = ({ ticket }: { ticket: ReadTicketResponse }) => {
const [isDragging, setIsDragging] = useState(false);
const handleDelete = async () => {
const isConfirmed = window.confirm("티켓을 삭제하시겠습니까?");
if (!isConfirmed) return;
try {
await deleteTicket.remove(ticket.ticketId);
alert("티켓이 삭제되었습니다.");
window.location.reload();
} catch (error) {
console.error("티켓 삭제 중 오류 발생:", error);
}
};
const { mutate: handleDelete} = useDeleteTicket();
return (
<div className="relative overflow-hidden w-full mb-4">
<motion.div
Expand Down Expand Up @@ -54,7 +44,7 @@ const TicketItem = ({ ticket }: { ticket: ReadTicket }) => {
initial={{ x: '100%' }}
animate={{ x: isDragging ? 0 : 'calc(100% + 1px)' }}
transition={{ duration: 0 }}
onClick={handleDelete}
onClick={() => handleDelete(ticket.ticketId)}
>
삭제
</motion.button>
Expand Down
44 changes: 14 additions & 30 deletions src/widgets/event/ui/TicketInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,26 @@
import { useEffect, useState } from 'react';
import TertiaryButton from '../../../../design-system/ui/buttons/TertiaryButton';
import TextButton from '../../../../design-system/ui/buttons/TextButton';
import { readTicket } from '../../../features/ticket/api/ticket';
import { ReadTicket } from '../../../pages/dashboard/ui/ticket/TicketListPage';
import { OrderTicketRequest } from '../../../features/ticket/model/OrderCreation';
import { orderTickets } from '../../../features/ticket/api/order';
import { useNavigate } from 'react-router-dom';
import { useTickets } from '../../../features/ticket/hooks/useTicketHook';

const TicketInfo = ({ eventId }: { eventId: number }) => {
const limitNum = 4;
const [tickets, setTickets] = useState<ReadTicket[]>([]);
const { data, isError, isLoading } = useTickets(eventId);
const [quantity, setQuantity] = useState<{ [key: number]: number }>({});
const navigate = useNavigate();

useEffect(() => {
const fetchTickets = async () => {
try {
const data = await readTicket.getAll(eventId);
if (data.isSuccess && Array.isArray(data.result)) {
setTickets(data.result);
} else {
setTickets([]);
}
} catch (error) {
console.error("티켓 데이터를 불러오는 중 오류 발생:", error);
setTickets([]);
}
};
fetchTickets();
}, [eventId]);
useEffect(() => {
if (tickets.length > 0) {
if (data && data.isSuccess) {
const initialQuantity: { [key: number]: number } = {};
tickets.forEach(ticket => {
data.result.forEach(ticket => {
initialQuantity[ticket.ticketId] = 1;
});
setQuantity(initialQuantity);
}
}, [tickets]);
}, [data]);

const handleIncrement = (ticketId: number) => {
setQuantity(prev => ({
...prev,
Expand All @@ -60,28 +43,29 @@ const TicketInfo = ({ eventId }: { eventId: number }) => {
eventId,
ticketCnt,
};

const response = await orderTickets(requestData);

console.log("API 응답:", response);

if (response.isSuccess && Array.isArray(response.result)) {
const orderIds = response.result;

navigate('/payment/ticket-confirm', { state: { orderIds, ticketId, eventId } });
} else {
alert("주문 정보를 불러올 수 없습니다.");
}

} catch (error) {
alert("티켓 구매 중 오류가 발생했습니다.");
}
};

if (isLoading) return <div>Loading...</div>;
if (isError || !data || !data.isSuccess) return <div>티켓 정보를 불러올 수 없습니다.</div>;

return (
<div className="w-full h-full">
{tickets.map(ticket => (
{data.result.map(ticket => (
<div key={ticket.ticketId} className="bg-gray1 px-3 py-3 md:px-6 md:py-4 rounded-[10px] mb-3">
<div className="flex justify-between items-center">
<div className="flex flex-col gap-2">
Expand Down