diff --git a/apps/nowait-user/src/constants/departments.ts b/apps/nowait-user/src/constants/departments.ts new file mode 100644 index 00000000..ca672454 --- /dev/null +++ b/apps/nowait-user/src/constants/departments.ts @@ -0,0 +1,104 @@ +// 학과 정보 매핑 +export const DEPARTMENTS: { [key: number]: string } = { + 1: "자유전공", + 2: "한국학전공", + 3: "경영학부", + 4: "금융빅데이터학부", + 5: "미디어커뮤니케이션학과", + 6: "관광경영학과", + 7: "경제학과", + 8: "의료산업경영학과", + 9: "응용통계학과", + 10: "사회복지학과", + 11: "유아교육학과", + 12: "심리학과", + 13: "한국어문학과", + 14: "영미어문학과", + 15: "동양어문학과", + 16: "유럽어문학과", + 17: "법학과", + 18: "경찰행정학과", + 19: "행정학과", + 20: "경찰학연계전공", + 21: "도시계획학전공", + 22: "조경학전공", + 23: "실내건축학전공", + 24: "건축학전공", + 25: "건축공학전공", + 26: "설비•소방공학과", + 27: "화공생명공학과", + 28: "기계공학전공", + 29: "산업공학전공", + 30: "스마트팩토리전공", + 31: "토목환경공학과", + 32: "신소재공학과", + 33: "배터리공학과", + 34: "식품생명공학과", + 35: "식품영양학과", + 36: "바이오나노학과", + 37: "생명과학과", + 38: "물리학과", + 39: "화학과", + 40: "AI•소프트웨어학부", + 41: "컴퓨터공학전공", + 42: "스마트보안전공", + 43: "전자공학전공", + 44: "차세대반도체설계전공", + 45: "전기공학과", + 46: "스마트시티학과", + 47: "의공학과", + 48: "에너지IT학과", + 49: "클라우드공학과", + 50: "한의예과", + 51: "한의학과", + 52: "회화•조소전공", + 53: "시각디자인전공", + 54: "패션디자인전공", + 55: "산업디자인전공", + 56: "성악전공", + 57: "기악전공", + 58: "작곡전공", + 59: "체육전공", + 60: "태권도전공", + 61: "연기예술학과", + 62: "바이오의료기기학과", + 63: "게임•영상학과", + 64: "반도체•디스플레이학과", + 65: "반도체설계학과", + 66: "미래자동차학과", + 67: "IT융합대학", + 68: "가천리버럴아츠칼리지", + 69: "경영대학", + 70: "공과대학", + 71: "미래산업대학", + 72: "바이오나노대학", + 73: "법과대학", + 74: "사회과학대학", + 75: "예술•체육대학", + 76: "인문대학", + 77: "창업대학", + 78: "한의과대학", + 82: "반도체대학", + 83: "반도체공학전공", +}; + +// departmentId를 학과명으로 변환하는 함수 +export const getDepartmentName = (departmentId: number): string => { + return DEPARTMENTS[departmentId] || "알 수 없음"; +}; + +// 모든 학과 목록을 반환하는 함수 +export const getAllDepartments = () => { + return Object.entries(DEPARTMENTS).map(([id, name]) => ({ + id: parseInt(id), + name, + })); +}; + +// 학과명으로 ID를 찾는 함수 +export const getDepartmentId = (departmentName: string): number | null => { + const entry = Object.entries(DEPARTMENTS).find( + ([_, name]) => name === departmentName + ); + return entry ? parseInt(entry[0]) : null; +}; diff --git a/apps/nowait-user/src/hooks/useInfiniteStores.ts b/apps/nowait-user/src/hooks/useInfiniteStores.ts index c74e840c..8f4fc377 100644 --- a/apps/nowait-user/src/hooks/useInfiniteStores.ts +++ b/apps/nowait-user/src/hooks/useInfiniteStores.ts @@ -1,62 +1,119 @@ import { useEffect } from "react"; import { useInfiniteQuery } from "@tanstack/react-query"; +import axios from "axios"; -// Store 데이터 타입 정의 +// 실제 서버 API 응답 타입 (StoreCard에서 직접 사용) interface Store { - id: number; - storeName: string; - department: string; - status: "open" | "closed"; - waitingCount: number; - imageUrl?: string; + storeId: number; + departmentId: number; + name: string; + location: string; + description: string; + images: string[]; + isActive: boolean; + deleted: boolean; + createdAt: string; + waitingCount?: number; // 대기인원 API 연동 시 추가 예정 } -// 가상의 주점 데이터를 가져오는 함수 -const fetchStores = async ({ pageParam = 1 }): Promise => { - // 로딩 시뮬레이션 - await new Promise((resolve) => setTimeout(resolve, 300)); +// 실제 서버 응답 구조 +interface ServerResponse { + success: boolean; + response: { + hasNext: boolean; + storeReadDtos: Store[]; + }; +} - const stores: Store[] = Array.from({ length: 20 }, (_, index) => { - const globalIndex = (pageParam - 1) * 20 + index; - return { - id: globalIndex + 1, - storeName: `주점${globalIndex + 1}`, - department: globalIndex % 2 === 0 ? "컴퓨터공학과" : "경영학과", - status: globalIndex % 3 === 0 ? "closed" : "open", - waitingCount: Math.floor(Math.random() * 10) + 1, - }; - }); +// 서버에서 주점 데이터를 가져오는 함수 +const fetchStores = async ({ pageParam = 0 }): Promise => { + try { + const SERVER_URI = import.meta.env.VITE_SERVER_URI; + const response = await axios.get( + `${SERVER_URI}/admin/stores/all-stores`, + { + params: { + page: pageParam, + size: 20, + }, + } + ); + + console.log("서버 응답 전체:", response.data); + + // 서버 응답 구조에 맞게 데이터 추출 + if (response.data.success && response.data.response?.storeReadDtos) { + const storeArray = response.data.response.storeReadDtos; + console.log("추출된 주점 배열:", storeArray); + + // 삭제된 주점 필터링하고 실제 서버 데이터 그대로 반환 + const stores: Store[] = storeArray + .filter((store) => store && !store.deleted) + .map((store) => ({ + ...store, + waitingCount: undefined, // 대기인원 API 연동 시 추가 예정 + })); - return stores; + console.log("필터링된 주점 데이터:", stores); + return stores; + } else { + console.warn( + "서버 응답이 성공하지 못했거나 데이터가 없습니다:", + response.data + ); + return []; + } + } catch (error) { + console.error("주점 데이터 로딩 실패:", error); + if (axios.isAxiosError(error)) { + console.error("응답 상태:", error.response?.status); + console.error("응답 데이터:", error.response?.data); + } + return []; + } }; export const useInfiniteStores = () => { - // 무한 스크롤을 위한 useInfiniteQuery - const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = - useInfiniteQuery({ - queryKey: ["stores"], - queryFn: fetchStores, - initialPageParam: 1, - getNextPageParam: (_lastPage, allPages) => - allPages.length < 10 ? allPages.length + 1 : undefined, - }); + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + error, + isLoading, + } = useInfiniteQuery({ + queryKey: ["stores"], + queryFn: fetchStores, + initialPageParam: 0, + getNextPageParam: (lastPage, allPages) => { + // 서버에서 hasNext를 제공하지만, 일단 기존 로직 유지 + // 더 이상 데이터가 없으면 undefined 반환 + if (lastPage.length < 20) { + return undefined; + } + return allPages.length; + }, + retry: 3, // 실패 시 3번 재시도 + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), + }); // 모든 페이지의 stores를 하나의 배열로 합치기 const stores = data?.pages.flat() ?? []; - // 초기 로딩 시 자동으로 두 번째 페이지도 로딩 + // 에러 로깅 useEffect(() => { - if (stores.length === 20 && hasNextPage && !isFetchingNextPage) { - console.log("초기 로딩: 두 번째 페이지 자동 로딩"); - fetchNextPage(); + if (error) { + console.error("주점 데이터 로딩 에러:", error); } - }, [stores.length, hasNextPage, isFetchingNextPage, fetchNextPage]); + }, [error]); return { stores, fetchNextPage, hasNextPage, isFetchingNextPage, + isLoading, + error, }; }; diff --git a/apps/nowait-user/src/pages/home/components/InfiniteStoreList.tsx b/apps/nowait-user/src/pages/home/components/InfiniteStoreList.tsx index a0bb09f1..dc6536a6 100644 --- a/apps/nowait-user/src/pages/home/components/InfiniteStoreList.tsx +++ b/apps/nowait-user/src/pages/home/components/InfiniteStoreList.tsx @@ -1,14 +1,11 @@ import { useRef, useEffect } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; import StoreCard from "./StoreCard"; -import { - useInfiniteStores, - type Store, -} from "../../../hooks/useInfiniteStores"; +import { useInfiniteStores } from "../../../hooks/useInfiniteStores"; const InfiniteStoreList = () => { // 커스텀 훅에서 무한 스크롤 로직 가져오기 - const { stores, fetchNextPage, hasNextPage, isFetchingNextPage } = + const { stores, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useInfiniteStores(); // 가상 스크롤을 위한 ref @@ -85,66 +82,91 @@ const InfiniteStoreList = () => { 모든 주점 - {/* 가상 스크롤 컨테이너 */} -
+ {/* 로딩 중일 때 */} + {isLoading && ( +
+
+ 주점 정보를 불러오는 중... +
+
+ )} + + {/* 주점 데이터가 없을 때 */} + {!isLoading && stores.length === 0 && ( +
+
+ 주점이 아직 준비되지 않았어요. +
+
+ 곧 다양한 주점들이 추가될 예정입니다! +
+
+ )} + + {/* 주점 데이터가 있을 때만 가상 스크롤 컨테이너 렌더링 */} + {!isLoading && stores.length > 0 && (
- {/* 가상화된 아이템들 */} - {rowVirtualizer.getVirtualItems().map((virtualRow) => { - const store = stores[virtualRow.index]; - if (!store) return null; - - return ( -
- -
- ); - })} -
- - {/* 로딩 표시 */} - {isFetchingNextPage && ( -
-
로딩 중...
+
+ {/* 가상화된 아이템들 */} + {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const store = stores[virtualRow.index]; + if (!store) return null; + + return ( +
+ +
+ ); + })}
- )} - {/* 더 이상 데이터가 없을 때 */} - {!hasNextPage && stores.length > 0 && ( -
-
- 더 이상 주점이 없습니다 + {/* 로딩 표시 */} + {isFetchingNextPage && ( +
+
로딩 중...
-
- )} -
+ )} + + {/* 더 이상 데이터가 없을 때 */} + {!hasNextPage && stores.length > 0 && ( +
+
+ 더 이상 주점이 없습니다 +
+
+ )} +
+ )}
); }; diff --git a/apps/nowait-user/src/pages/home/components/StoreCard.tsx b/apps/nowait-user/src/pages/home/components/StoreCard.tsx index 88ae35c5..d3a9f208 100644 --- a/apps/nowait-user/src/pages/home/components/StoreCard.tsx +++ b/apps/nowait-user/src/pages/home/components/StoreCard.tsx @@ -1,33 +1,58 @@ import { NotOpenIcon, WaitingIcon } from "./HomeIcon"; +import { useNavigate } from "react-router-dom"; +import { getDepartmentName } from "../../../constants/departments"; interface StoreCardProps { - storeName: string; - department: string; - status: "open" | "closed"; - waitingCount?: number; - imageUrl?: string; + storeId: number; + name: string; + departmentId: number; + images: string[]; + isActive: boolean; + deleted: boolean; + waitingCount?: number; // 대기인원 API 연동 시 추가 예정 } const StoreCard = ({ - storeName, - department, - status, + storeId, + name, + departmentId, + images, + isActive, + deleted, waitingCount, - imageUrl, }: StoreCardProps) => { + const navigate = useNavigate(); + + // 삭제된 주점은 렌더링하지 않음 + if (deleted) { + return null; + } + + const departmentName = getDepartmentName(departmentId); + const mainImage = images.length > 0 ? images[0] : undefined; + const status = isActive ? "open" : "closed"; + + // 스토어 클릭 핸들러 + const handleStoreClick = () => { + navigate(`/store/${storeId}`); + }; + return ( -
+
store
- {storeName} + {name}
{status === "open" ? ( @@ -38,7 +63,7 @@ const StoreCard = ({
- {department} + {departmentName}
diff --git a/apps/nowait-user/src/pages/reserve/StoreDetailPage.tsx b/apps/nowait-user/src/pages/reserve/StoreDetailPage.tsx index 08053860..b1db6c56 100644 --- a/apps/nowait-user/src/pages/reserve/StoreDetailPage.tsx +++ b/apps/nowait-user/src/pages/reserve/StoreDetailPage.tsx @@ -1,9 +1,7 @@ -import React from 'react' +import React from "react"; const StoreDetailPage = () => { - return ( -
StoreDetailPage
- ) -} + return
StoreDetailPage
; +}; -export default StoreDetailPage \ No newline at end of file +export default StoreDetailPage; diff --git a/apps/nowait-user/src/routes/Router.tsx b/apps/nowait-user/src/routes/Router.tsx index c060d588..5ca1151a 100644 --- a/apps/nowait-user/src/routes/Router.tsx +++ b/apps/nowait-user/src/routes/Router.tsx @@ -14,43 +14,40 @@ import LoginPage from "../pages/login/LoginPage"; import KakaoRedirectHandler from "../pages/login/KakaoRedirectHandler"; import AuthGuard from "../components/AuthGuard"; +// AuthGuard로 래핑하는 헬퍼 함수 +const withAuth = (Component: React.ComponentType) => ( + + + +); + const Router = () => { return ( {/* 공개 라우트 - 인증 불필요 */} - {/* 로그인 페이지 */} } /> } /> - {/* QR 코드 접속 페이지 */} + {/* 보호된 라우트 - 인증 필요 (구체적인 경로 먼저) */} + + + + + + + {/* QR 코드 접속 페이지 - 인증 불필요 (일반적인 경로 나중에) */} } /> - } /> } /> - } /> } /> + } /> } /> - - {/* 보호된 라우트 - 인증 필요 */} - - - } /> - } /> - } /> - } - /> - } /> - - - } - /> + } /> ); };