Skip to content
Merged
3 changes: 3 additions & 0 deletions design-system/ui/textFields/DefaultTextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface DefaultTextFieldProps {
labelClassName?: string;
detailClassName?:string;
disabled?: boolean;
maxLength?: number;
}

const DefaultTextField = forwardRef<HTMLInputElement, DefaultTextFieldProps>(
Expand All @@ -38,6 +39,7 @@ const DefaultTextField = forwardRef<HTMLInputElement, DefaultTextFieldProps>(
errorMessage,
detailClassName ='',
disabled = false,
maxLength,
...rest
},
ref
Expand All @@ -57,6 +59,7 @@ const DefaultTextField = forwardRef<HTMLInputElement, DefaultTextFieldProps>(
onBlur={onBlur}
placeholder={placeholder}
disabled={disabled}
maxLength={maxLength}
{...rest}
className={`w-full border border-placeholderText rounded-[3px] px-2 py-1 outline-none placeholder:text-placeholderText text-xs font-light resize-none ${className} ${
errorMessage ? 'border-red-500' : ''
Expand Down
3 changes: 3 additions & 0 deletions src/features/host/hook/useHostHook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ export const useHostCreation = () => {
mutationFn: async (requestBody: HostCreationRequest) => {
return await createHost(requestBody);
},
onError: (error) => {
console.log("error", error.message);
},
});
};

Expand Down
2 changes: 1 addition & 1 deletion src/features/ticket/hooks/useOrderHook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const useOrderTicket = () => {
mutationFn: (data: OrderTicketRequest) => orderTickets(data),
onError: (error: any) => {
const errorCode = error?.code;
if (errorCode === 'TICKET4004') {
if (errorCode === 'TICKET4003') {
alert('해당 티켓은 현재 판매 기간이 아닙니다.');
} else {
alert('티켓 구매 중 오류가 발생했습니다.');
Expand Down
2 changes: 1 addition & 1 deletion src/features/ticket/model/Order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface OrderTicketRequest {
}

export interface OrderTicketResponse {
id: number;
orderId: number;
event: {
id: number;
bannerImageUrl: string;
Expand Down
3 changes: 3 additions & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import router from './app/routes/Router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import './index.css';
import { initializeAuth } from './shared/types/api/tokenValidator';

const queryClient = new QueryClient();

initializeAuth().catch(console.error);

createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
Expand Down
1 change: 1 addition & 0 deletions src/pages/dashboard/ui/ticket/TicketCreatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ const TicketCreatePage = () => {
detail="티켓을 잘 나타낼 수 있는 이름을 써보세요.(무료 입장권, VIP 입장권,얼리버드)"
className="h-12"
onChange={handleInputChange('ticketName')}
maxLength={17}
/>
</div>
{/*티켓 설명 입력란*/}
Expand Down
7 changes: 7 additions & 0 deletions src/pages/home/ui/MainPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import SecondaryButton from '../../../../design-system/ui/buttons/SecondaryButto
import SearchTextField from '../../../../design-system/ui/textFields/SearchTextField';
import searchIcon from '../../../../design-system/icons/Search.svg';
import VerticalCardButton from '../../../../design-system/ui/buttons/VerticalCardButton';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { AnimatePresence } from 'framer-motion';
import LoginModal from '../../../widgets/main/ui/LoginModal';
Expand Down Expand Up @@ -34,6 +35,12 @@ const MainPage = () => {
}
};

useEffect(() => {
if (!isLoggedIn) {
openModal();
}
}, [isLoggedIn, openModal]);

return (
<div className="flex flex-col items-center pb-24">
<Header
Expand Down
28 changes: 23 additions & 5 deletions src/pages/join/LogoutPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,34 @@ const LogoutPage = () => {
const handleLogout = async () => {
try {
await axiosClient.post('/oauth/logout');
console.log('이 에러는 토큰 만료로 인한 정상적인 동작입니다. 다시 로그인해주세요.');
logout();
navigate('/');
} catch (error) {
console.error('로그아웃 실패:', error);
alert('로그아웃에 실패했습니다. 다시 시도해주세요.');
navigate('/menu');
} catch (error: unknown) {
// 토큰 만료로 인한 자동 로그아웃인지 확인
if (
typeof error === 'object' &&
error !== null &&
'code' in error &&
(
(error as { code?: string }).code === 'TOKEN4001' ||
(error as { code?: string }).code === 'TOKEN4004')
) {
// 토큰 만료로 인한 자동 로그아웃이므로 조용히 처리
console.log('토큰 만료로 인한 자동 로그아웃', error);
alert('다시 로그인 해주세요.');
logout();
navigate('/');
} else {
// 실제 로그아웃 실패
console.error('로그아웃 실패:', error);
alert('로그아웃에 실패했습니다. 다시 시도해주세요.');
navigate('/menu');
}
}
};
handleLogout();
}, [navigate]);
}, [navigate, logout]);

return <div>로그아웃 중...</div>;
};
Expand Down
14 changes: 7 additions & 7 deletions src/pages/menu/ui/MyTicketPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const MyTicketPage = () => {
today.setHours(0, 0, 0, 0);

const invalidTickets = tickets.filter(
ticket => selectedIds.includes(ticket.id) && new Date(ticket.event.startDate) <= today
ticket => selectedIds.includes(ticket.orderId) && new Date(ticket.event.startDate) <= today
);

if (invalidTickets.length > 0) {
Expand All @@ -56,7 +56,7 @@ const MyTicketPage = () => {

const handleEventCardClick = (ticket: OrderTicketResponse) => {
if (isCancelMode) {
setSelectedIds(prev => (prev.includes(ticket.id) ? prev.filter(id => id !== ticket.id) : [...prev, ticket.id]));
setSelectedIds(prev => (prev.includes(ticket.orderId) ? prev.filter(id => id !== ticket.orderId) : [...prev, ticket.orderId]));
} else {
setSelectedTicket(ticket);
setIsModalOpen(true);
Expand All @@ -65,7 +65,7 @@ const MyTicketPage = () => {

useEffect(() => {
if (data?.result) {
setTickets(data.result);
setTickets(data.result);
}
}, [data]);

Expand Down Expand Up @@ -98,8 +98,8 @@ const MyTicketPage = () => {
) : tickets.length > 0 ? (
tickets.map(ticket => (
<EventCard
key={ticket.id}
id={ticket.id}
key={ticket.orderId}
id={ticket.orderId}
img={ticket.event.bannerImageUrl}
eventTitle={ticket.event.title}
dDay={ticket.event.remainDays}
Expand All @@ -108,7 +108,7 @@ const MyTicketPage = () => {
location={ticket.event.address}
hashtags={ticket.event.hashtags}
onClick={() => handleEventCardClick(ticket)}
className={`transition-transform duration-200 ${isCancelMode && selectedIds.includes(ticket.id) ? 'scale-95 border-2 border-pink-400' : ''
className={`transition-transform duration-200 ${isCancelMode && selectedIds.includes(ticket.orderId) ? 'scale-95 border-2 border-pink-400' : ''
}`}
aspectRatio='md:aspect-[3/4.7] sm:aspect-[1/2]'
>
Expand Down Expand Up @@ -164,7 +164,7 @@ const MyTicketPage = () => {
onClick={() => {
cancelTicket(selectedIds, {
onSuccess: () => {
setTickets(prev => prev.filter(ticket => !selectedIds.includes(ticket.id)));
setTickets(prev => prev.filter(ticket => !selectedIds.includes(ticket.orderId)));
setIsDeleteModalOpen(false);
setIsCancelMode(false);
setSelectedIds([]);
Expand Down
34 changes: 19 additions & 15 deletions src/shared/types/api/http-client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
import { ApiErrorResponse } from './apiResponse';
import Cookies from 'js-cookie';
import useAuthStore from '../../../app/provider/authStore';

// 로그아웃 처리 및 리다이렉트
function logoutAndRedirect(error: unknown) {
const authStore = useAuthStore.getState();
// Zustand persist가 자동으로 localStorage를 정리하므로 수동 정리 불필요
authStore.logout();
authStore.openModal();

console.log('logoutAndRedirect', error);
}

export const axiosClient = axios.create({
baseURL: `${import.meta.env.VITE_API_BASE_URL}/api/v1`,
timeout: 3000,
Expand Down Expand Up @@ -56,31 +65,26 @@ axiosClient.interceptors.response.use(
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };

// 401(토큰 만료)일 경우 로그아웃 처리 or 토큰 갱신 가능
if (errorInfo.code === 'TOKEN4001' && !originalRequest._retry) {
if (!originalRequest._retry && errorInfo.code === 'TOKEN4001') {
originalRequest._retry = true;

try {
await axios.post(
`${import.meta.env.VITE_API_BASE_URL}/api/v1/oauth/reissue`,
{},
{
withCredentials: true,
}
);
await axios.post(`${import.meta.env.VITE_API_BASE_URL}/api/v1/oauth/reissue`, {}, { withCredentials: true });
// 새 토큰이 쿠키에 재설정되었으므로 원래 요청 재시도
return axiosClient(originalRequest);
} catch (refreshError) {
} catch (refreshError: unknown) {
// 리프레시 실패 시 로그아웃 처리
Cookies.remove('access_token');
Cookies.remove('refresh_token');
const authStore = useAuthStore.getState();
authStore.logout();
authStore.openModal();
logoutAndRedirect(refreshError);

return Promise.reject(refreshError);
}
}

if (errorInfo.code === 'TOKEN4004' || errorInfo.status === 401) {
logoutAndRedirect(errorInfo);
return Promise.reject(errorInfo);
}

return Promise.reject(errorInfo);
}
);
42 changes: 42 additions & 0 deletions src/shared/types/api/tokenValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { axiosClient } from './http-client';
import useAuthStore from '../../../app/provider/authStore';

// 타입 가드 함수
function isTokenError(error: unknown): error is { code: string; status?: number } {
return error !== null && typeof error === 'object' && 'code' in error && typeof (error as any).code === 'string';
}

// 토큰 유효성 검증 함수 (httpOnly 쿠키 사용)
export const validateToken = async (): Promise<boolean> => {
try {
// 토큰 유효성 검증을 위한 API 호출 (사용자 정보 조회)
await axiosClient.get('/users');
return true;
} catch (error: unknown) {
// 토큰이 만료되었거나 유효하지 않은 경우
if (isTokenError(error) && (error.code === 'TOKEN4001' || error.code === 'TOKEN4004' || error.status === 401)) {
return false;
}
// 다른 에러의 경우 토큰이 유효하다고 간주
return true;
}
};

// 앱 시작 시 토큰 검증 및 자동 로그아웃
export const initializeAuth = async () => {
const authStore = useAuthStore.getState();

// 로그인 상태가 아닌 경우 검증하지 않음
if (!authStore.isLoggedIn) {
return;
}

const isValid = await validateToken();

if (!isValid) {
// 토큰이 유효하지 않으면 로그아웃 처리
// Zustand persist가 자동으로 localStorage를 정리하므로 수동 정리 불필요
authStore.logout();
authStore.openModal();
}
};
34 changes: 19 additions & 15 deletions src/widgets/event/ui/TicketInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,9 @@ const TicketInfo = ({ eventId }: { eventId: number }) => {
{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">
<div className="flex flex-col gap-2"> {/* w-[230px] */}
<div className="flex items-center gap-2 md:gap-4">
<span className="font-bold text-base md:text-lg">{ticket.ticketName}</span>
<span className="text-sm md:text-base">{ticket.ticketPrice}원</span>
<span className="font-bold text-base md:text-md">{ticket.ticketName}</span> {/* w-[170px] */}
</div>
<div className="flex gap-1 md:gap-2 text-xs md:text-sm text-gray-600">
<span>남은 티켓: {ticket.availableQuantity}장</span>
Expand All @@ -96,18 +95,23 @@ const TicketInfo = ({ eventId }: { eventId: number }) => {
</div>
</div>
<div className="flex items-center gap-2 md:gap-4">
<div className="flex gap-1 md:gap-2">
<TextButton
label="-"
onClick={() => handleDecrement(ticket.ticketId)}
className="flex justify-center items-center bg-white w-6 h-6 md:w-7 md:h-7"
/>
<span>{quantity[ticket.ticketId]}</span>
<TextButton
label="+"
onClick={() => handleIncrement(ticket.ticketId)}
className="flex justify-center items-center bg-white w-6 h-6 md:w-7 md:h-7"
/>
<div className="flex flex-col items-center gap-2">
<span className="text-sm md:text-base">
{(ticket.ticketPrice * quantity[ticket.ticketId]).toLocaleString('ko-KR')}원
</span>
<div className="flex gap-1 md:gap-2">
<TextButton
label="-"
onClick={() => handleDecrement(ticket.ticketId)}
className="flex justify-center items-center bg-white w-6 h-6 md:w-7 md:h-7"
/>
<span>{quantity[ticket.ticketId]}</span>
<TextButton
label="+"
onClick={() => handleIncrement(ticket.ticketId)}
className="flex justify-center items-center bg-white w-6 h-6 md:w-7 md:h-7"
/>
</div>
</div>
<TertiaryButton
label="구매하기"
Expand Down