diff --git a/apps/web/src/apis/clubDetail/entity.ts b/apps/web/src/apis/clubDetail/entity.ts index 2d24ddb..fc61399 100644 --- a/apps/web/src/apis/clubDetail/entity.ts +++ b/apps/web/src/apis/clubDetail/entity.ts @@ -1,5 +1,11 @@ +import type { ClubCategory } from '@/apis/common/club'; + import type { UniversitySummary } from '../universityClub/entity'; -export type ClubCategory = 'ACADEMIC' | 'SPORTS' | 'HOBBY' | 'RELIGION' | 'PERFORMANCE' | 'JUNIOR'; + +export interface ClubDetailUniversitySummary extends UniversitySummary { + clubCount: number; +} + export interface ClubDetailResponse { id: number; name: string; @@ -10,5 +16,5 @@ export interface ClubDetailResponse { description: string; introduce: string; location: string; - university: UniversitySummary; + university: ClubDetailUniversitySummary; } diff --git a/apps/web/src/apis/common/club.ts b/apps/web/src/apis/common/club.ts new file mode 100644 index 0000000..d9256d8 --- /dev/null +++ b/apps/web/src/apis/common/club.ts @@ -0,0 +1,16 @@ +export const CLUB_CATEGORY = { + ACADEMIC: 'ACADEMIC', + SPORTS: 'SPORTS', + HOBBY: 'HOBBY', + RELIGION: 'RELIGION', + PERFORMANCE: 'PERFORMANCE', + JUNIOR: 'JUNIOR', +} as const; + +export type ClubCategory = (typeof CLUB_CATEGORY)[keyof typeof CLUB_CATEGORY]; + +export const CLUB_CATEGORY_VALUES = Object.values(CLUB_CATEGORY); + +export function isClubCategory(value: string | null | undefined): value is ClubCategory { + return typeof value === 'string' && CLUB_CATEGORY_VALUES.includes(value as ClubCategory); +} diff --git a/apps/web/src/apis/recentClub/entity.ts b/apps/web/src/apis/recentClub/entity.ts new file mode 100644 index 0000000..047400a --- /dev/null +++ b/apps/web/src/apis/recentClub/entity.ts @@ -0,0 +1,19 @@ +import type { ClubCategory } from '@/apis/common/club'; + +export interface RecentClubRequestParams { + clubIds: number[]; +} + +export interface RecentClub { + id: number; + name: string; + imageUrl: string; + category: ClubCategory; + categoryName: string; + topic: string; + description: string; +} + +export interface RecentClubResponse { + clubs: RecentClub[]; +} diff --git a/apps/web/src/apis/recentClub/index.ts b/apps/web/src/apis/recentClub/index.ts new file mode 100644 index 0000000..3b3878c --- /dev/null +++ b/apps/web/src/apis/recentClub/index.ts @@ -0,0 +1,9 @@ +import { apiClient } from '../client'; +import type { RecentClubRequestParams, RecentClubResponse } from './entity'; + +export const getRecentClubs = async (clubIds: number[]) => { + const response = await apiClient.get('konect/clubs/recent', { + params: { clubIds }, + }); + return response; +}; diff --git a/apps/web/src/apis/recentClub/queries.ts b/apps/web/src/apis/recentClub/queries.ts new file mode 100644 index 0000000..38881f1 --- /dev/null +++ b/apps/web/src/apis/recentClub/queries.ts @@ -0,0 +1,16 @@ +import { queryOptions } from '@tanstack/react-query'; + +import { getRecentClubs } from '.'; + +export const recentClubQueryKeys = { + all: ['recentClub'] as const, + list: (clubIds: number[]) => [...recentClubQueryKeys.all, clubIds] as const, +}; + +export const recentClubQueries = { + list: (clubIds: number[]) => + queryOptions({ + queryKey: recentClubQueryKeys.list(clubIds), + queryFn: () => getRecentClubs(clubIds), + }), +}; diff --git a/apps/web/src/apis/universityClub/entity.ts b/apps/web/src/apis/universityClub/entity.ts index 2019cb8..b625af1 100644 --- a/apps/web/src/apis/universityClub/entity.ts +++ b/apps/web/src/apis/universityClub/entity.ts @@ -1,7 +1,6 @@ +import type { ClubCategory } from '@/apis/common/club'; import type { Region } from '@/apis/home/entity'; -export type ClubCategory = 'ACADEMIC' | 'SPORTS' | 'HOBBY' | 'RELIGION' | 'PERFORMANCE' | 'JUNIOR'; - export interface UniversityClubListRequestParams { page?: number; limit?: number; @@ -16,7 +15,6 @@ export interface UniversitySummary { region: Region; regionName: string; imageUrl: string; - clubCount?: number; } export interface ClubCategorySummary { @@ -31,8 +29,8 @@ export interface UniversityClub { imageUrl: string; category: ClubCategory; categoryName: string; + topic: string; description: string; - memberCount: number; } export interface UniversityClubListResponse { diff --git a/apps/web/src/components/Breadcrumb/index.tsx b/apps/web/src/components/Breadcrumb/index.tsx new file mode 100644 index 0000000..7187ea4 --- /dev/null +++ b/apps/web/src/components/Breadcrumb/index.tsx @@ -0,0 +1,49 @@ +import { Fragment } from 'react'; +import { cn } from '@konect/utils/cn'; +import { Link } from 'react-router-dom'; + +interface BreadcrumbItem { + label: string; + to?: string; +} + +interface BreadcrumbProps { + items: BreadcrumbItem[]; +} + +function Breadcrumb({ items }: BreadcrumbProps) { + return ( + + ); +} + +export default Breadcrumb; diff --git a/apps/web/src/components/RecentClubCard/index.tsx b/apps/web/src/components/RecentClubCard/index.tsx new file mode 100644 index 0000000..327b8a2 --- /dev/null +++ b/apps/web/src/components/RecentClubCard/index.tsx @@ -0,0 +1,30 @@ +import { cn } from '@konect/utils/cn'; +import { Link } from 'react-router-dom'; + +import type { RecentClub } from '@/apis/recentClub/entity'; +import { CATEGORY_TEXT_COLORS } from '@/constants/club'; + +interface RecentClubCardProps { + club: RecentClub; +} + +function RecentClubCard({ club }: RecentClubCardProps) { + return ( + + + + {club.name} + + {club.categoryName} + + + + ); +} + +export default RecentClubCard; diff --git a/apps/web/src/components/RecentClubList/index.tsx b/apps/web/src/components/RecentClubList/index.tsx new file mode 100644 index 0000000..f780a1a --- /dev/null +++ b/apps/web/src/components/RecentClubList/index.tsx @@ -0,0 +1,57 @@ +import { cn } from '@konect/utils/cn'; +import { useSuspenseQuery } from '@tanstack/react-query'; + +import { recentClubQueries } from '@/apis/recentClub/queries'; +import RecentClubCard from '@/components/RecentClubCard'; +import { useRecentClubIds } from '@/utils/recentClubStorage'; + +interface RecentClubListProps { + className: string; + emptyClassName?: string; +} + +function RecentClubList({ className, emptyClassName }: RecentClubListProps) { + const recentClubIds = useRecentClubIds(); + + if (recentClubIds.length === 0) { + return ; + } + + return ; +} + +function RecentClubListContent({ + className, + emptyClassName, + recentClubIds, +}: RecentClubListProps & { recentClubIds: number[] }) { + const { data } = useSuspenseQuery(recentClubQueries.list(recentClubIds)); + const recentClubs = data.clubs; + + if (recentClubs.length === 0) { + return ; + } + + return ( +
+ {recentClubs.map((club) => ( + + ))} +
+ ); +} + +function RecentClubListMessage({ className, message }: { className?: string; message: string }) { + return ( +

+ {message} +

+ ); +} + +export default RecentClubList; diff --git a/apps/web/src/constants/club.ts b/apps/web/src/constants/club.ts index 9929fc6..122cd14 100644 --- a/apps/web/src/constants/club.ts +++ b/apps/web/src/constants/club.ts @@ -1,10 +1,10 @@ -import type { ClubCategory } from '@/apis/universityClub/entity'; +import { CLUB_CATEGORY, type ClubCategory } from '@/apis/common/club'; export const CATEGORY_TEXT_COLORS: Record = { - ACADEMIC: 'text-primary-500', - SPORTS: 'text-info-600', - HOBBY: 'text-danger-600', - RELIGION: 'text-warning-700', - PERFORMANCE: 'text-[#cd3bf6]', - JUNIOR: 'text-success-700', + [CLUB_CATEGORY.ACADEMIC]: 'text-primary-500', + [CLUB_CATEGORY.SPORTS]: 'text-info-600', + [CLUB_CATEGORY.HOBBY]: 'text-danger-600', + [CLUB_CATEGORY.RELIGION]: 'text-warning-700', + [CLUB_CATEGORY.PERFORMANCE]: 'text-[#cd3bf6]', + [CLUB_CATEGORY.JUNIOR]: 'text-success-700', }; diff --git a/apps/web/src/pages/ClubDetail/index.tsx b/apps/web/src/pages/ClubDetail/index.tsx index bc63722..bbea8b8 100644 --- a/apps/web/src/pages/ClubDetail/index.tsx +++ b/apps/web/src/pages/ClubDetail/index.tsx @@ -1,12 +1,16 @@ +import { useEffect } from 'react'; import { cn } from '@konect/utils/cn'; import { useSuspenseQuery } from '@tanstack/react-query'; -import { Link, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { clubDetailQueries } from '@/apis/clubDetail/queries'; import AddMov from '@/assets/add-mov.svg'; import AddPhoto from '@/assets/add-photo.svg'; import NoneImage from '@/assets/None-image.png'; +import Breadcrumb from '@/components/Breadcrumb'; +import RecentClubList from '@/components/RecentClubList'; import { CATEGORY_TEXT_COLORS } from '@/constants/club'; +import { saveRecentClubId } from '@/utils/recentClubStorage'; function Introduce({ introduce }: { introduce: string }) { return ( @@ -48,16 +52,20 @@ export default function ClubDetail() { const { clubId } = useParams(); const { data: clubDetail } = useSuspenseQuery(clubDetailQueries.detail(Number(clubId))); + useEffect(() => { + saveRecentClubId(clubDetail.id); + }, [clubDetail.id]); + return (
- +
diff --git a/apps/web/src/pages/Home/index.tsx b/apps/web/src/pages/Home/index.tsx index e353954..fc98c8f 100644 --- a/apps/web/src/pages/Home/index.tsx +++ b/apps/web/src/pages/Home/index.tsx @@ -5,10 +5,9 @@ import { Link } from 'react-router-dom'; import type { Region, HomeRequestParams, University } from '@/apis/home/entity'; import { homeQueries } from '@/apis/home/queries'; -import clubBadgeBlue from '@/assets/club-badge-blue.png'; -import clubBadgeRed from '@/assets/club-badge-red.png'; import heroCatBook from '@/assets/hero-cat-book.png'; import SearchIcon from '@/assets/svg/search-icon.svg'; +import RecentClubList from '@/components/RecentClubList'; const REGION_OPTIONS: { label: string; value?: Region }[] = [ { label: '전체' }, @@ -21,45 +20,6 @@ const REGION_OPTIONS: { label: string; value?: Region }[] = [ { label: '제주도', value: 'JEJU' }, ]; -type RecentClub = { - id: number; - name: string; - category: string; - keyword: string; - logo: string; -}; - -const recentClubs: RecentClub[] = [ - { - id: 1, - name: '경영전략연구회', - category: '학술', - keyword: '경영', - logo: clubBadgeBlue, - }, - { - id: 2, - name: '경영전략연구회', - category: '학술', - keyword: '경영', - logo: clubBadgeRed, - }, - { - id: 3, - name: '경영전략연구회', - category: '학술', - keyword: '경영', - logo: clubBadgeBlue, - }, - { - id: 4, - name: '경영전략연구회', - category: '학술', - keyword: '경영', - logo: clubBadgeBlue, - }, -]; - function Home() { const [selectedRegion, setSelectedRegion] = useState(); const [searchKeyword, setSearchKeyword] = useState(''); @@ -134,11 +94,10 @@ function Home() { >
-
- {recentClubs.map((club) => ( - - ))} -
+
@@ -193,25 +152,6 @@ function SectionTitle({ title, description }: { title: string; description: stri ); } -function RecentClubCard({ club }: { club: RecentClub }) { - return ( - - ); -} - // function UniversityCardSkeletonList() { // return Array.from({ length: 8 }, (_, index) => ( //
sum + category.count, 0); const allClubCount = categoryTotalCount || totalCount; - const recentClubs = clubs.slice(0, 4); + + const handleSearchKeywordChange = (event: ChangeEvent) => { + const value = event.target.value; + setSearchKeyword(value); + updateSearchQuery(value); + }; + + const handleCategoryChange = (category?: ClubCategory) => { + updateListSearchParams(setSearchParams, { category }); + }; useEffect(() => { setSearchKeyword(query); @@ -77,38 +89,16 @@ function UniversityClubListContent({ universityId }: { universityId: number }) { return () => observer.disconnect(); }, [fetchNextPage, hasNextPage, isFetchingNextPage]); - const handleSearchKeywordChange = (event: ChangeEvent) => { - const value = event.target.value; - setSearchKeyword(value); - updateSearchQuery(value); - }; - - const handleCategoryChange = (category?: ClubCategory) => { - updateListSearchParams(setSearchParams, { category }); - }; - return ( -
-
- - -
+
+
+ + +
-