Skip to content
Merged
3 changes: 3 additions & 0 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as Sentry from '@sentry/react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';

import Layout from './layout';
import Home from './pages/Home';
import UniversityClubList from './pages/UniversityClubList';

const SentryRoutes = Sentry.withSentryReactRouterV7Routing(Routes);

Expand All @@ -11,6 +13,7 @@ function App() {
<SentryRoutes>
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="/universities/:universityId/clubs" element={<UniversityClubList />} />
</Route>
</SentryRoutes>
</BrowserRouter>
Expand Down
44 changes: 44 additions & 0 deletions apps/web/src/apis/universityClub/entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Region } from '@/apis/home/entity';

export type ClubCategory = 'ACADEMIC' | 'SPORTS' | 'HOBBY' | 'RELIGION' | 'PERFORMANCE' | 'JUNIOR';

export interface UniversityClubListRequestParams {
page?: number;
limit?: number;
query?: string;
category?: ClubCategory;
}

export interface UniversitySummary {
id: number;
name: string;
campusName: string;
region: Region;
regionName: string;
imageUrl: string;
}

export interface ClubCategorySummary {
category: ClubCategory;
categoryName: string;
count: number;
}

export interface UniversityClub {
id: number;
name: string;
imageUrl: string;
category: ClubCategory;
categoryName: string;
description: string;
memberCount: number;
}

export interface UniversityClubListResponse {
university: UniversitySummary;
totalCount: number;
totalPage: number;
currentPage: number;
categories: ClubCategorySummary[];
clubs: UniversityClub[];
}
10 changes: 10 additions & 0 deletions apps/web/src/apis/universityClub/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { apiClient } from '../client';
import type { UniversityClubListRequestParams, UniversityClubListResponse } from './entity';

export const getUniversityClubs = async (universityId: number, params?: UniversityClubListRequestParams) => {
const response = await apiClient.get<UniversityClubListResponse, UniversityClubListRequestParams>(
`konect/universities/${universityId}/clubs`,
{ params }
);
return response;
};
52 changes: 52 additions & 0 deletions apps/web/src/apis/universityClub/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { infiniteQueryOptions, queryOptions } from '@tanstack/react-query';

import type { UniversityClubListRequestParams, UniversityClubListResponse } from './entity';
import { getUniversityClubs } from '.';

type UniversityClubInfiniteListParams = Omit<UniversityClubListRequestParams, 'page'>;

export const universityClubQueryKeys = {
all: ['universityClub'] as const,
list: (universityId: number, params: UniversityClubListRequestParams) =>
[
...universityClubQueryKeys.all,
'list',
universityId,
params.page ?? 1,
params.limit ?? 12,
params.query ?? '',
params.category ?? '',
] as const,
infinite: {
all: () => [...universityClubQueryKeys.all, 'infinite'] as const,
list: (universityId: number, params: UniversityClubInfiniteListParams) =>
[
...universityClubQueryKeys.infinite.all(),
universityId,
params.limit ?? 12,
params.query ?? '',
params.category ?? '',
] as const,
},
};

export const universityClubQueries = {
list: (universityId: number, params: UniversityClubListRequestParams) =>
queryOptions({
queryKey: universityClubQueryKeys.list(universityId, params),
queryFn: () => getUniversityClubs(universityId, params),
}),
infiniteList: (universityId: number, params: UniversityClubInfiniteListParams) =>
infiniteQueryOptions({
queryKey: universityClubQueryKeys.infinite.list(universityId, params),
queryFn: ({ pageParam }) => getUniversityClubs(universityId, { ...params, page: pageParam }),
initialPageParam: 1,
getNextPageParam: (lastPage: UniversityClubListResponse) => {
if (lastPage.currentPage < lastPage.totalPage) {
return lastPage.currentPage + 1;
}

return undefined;
},
}),
};
33 changes: 21 additions & 12 deletions apps/web/src/pages/Home/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useState, type ChangeEvent } from 'react';
import { useDebouncedCallback } from '@konect/utils/use-debounced-callback';
import { useSuspenseQuery } from '@tanstack/react-query';
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';
Expand Down Expand Up @@ -75,6 +77,7 @@ function Home() {
const { data: homeData } = useSuspenseQuery(homeQueries.detail(homeParams));
const universities = homeData.universities ?? [];
const totalUniversityCount = homeData.totalUniversityCount;
const isSearching = searchKeyword.trim().length > 0 || searchQuery.length > 0;

const handleSearchKeywordChange = (event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
Expand Down Expand Up @@ -124,14 +127,20 @@ function Home() {
</label>
</section>

<section className="mt-20 w-full sm:mt-24">
<SectionTitle title="최근에 본 동아리" description="관심있게 봤던 동아리를 다시 확인해보세요." />
<div className="xl: mt-7 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{recentClubs.map((club) => (
<RecentClubCard key={club.id} club={club} />
))}
</div>
</section>
<div
className={`grid w-full transition-[grid-template-rows,opacity,margin-top] duration-300 ease-out ${
isSearching ? 'mt-0 grid-rows-[0fr] opacity-0' : 'mt-20 grid-rows-[1fr] opacity-100 sm:mt-24'
}`}
>
<section className="min-h-0 overflow-hidden" aria-hidden={isSearching}>
<SectionTitle title="최근에 본 동아리" description="관심있게 봤던 동아리를 다시 확인해보세요." />
<div className="xl: mt-7 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Tailwind 클래스 오타를 수정해주세요 (xl:).

Line 137의 classNamexl: mt-7가 있어 xl:가 단독 토큰으로 남습니다. xl:mt-7로 붙여야 의도한 반응형 마진이 적용됩니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/pages/Home/index.tsx` at line 137, The Tailwind class string in
the JSX div's className contains a typo "xl: mt-7" which leaves "xl:" isolated;
update the class value used in the Home page component (the div with className
"xl: mt-7 grid ...") by removing the space so the token reads "xl:mt-7" to
enable the intended responsive margin.

{recentClubs.map((club) => (
<RecentClubCard key={club.id} club={club} />
))}
</div>
</section>
</div>

<section className="mt-10 w-full sm:mt-20">
<SectionTitle title="전체 대학" description="학교별 동아리를 자유롭게 탐색해보세요." />
Expand Down Expand Up @@ -187,7 +196,7 @@ function SectionTitle({ title, description }: { title: string; description: stri
function RecentClubCard({ club }: { club: RecentClub }) {
return (
<button
className="border-text-100 hover:border-primary-500 focus-visible:outline-primary-500 flex h-35 items-center justify-center gap-7 rounded-[20px] border bg-white py-8 transition-colors hover:shadow-[0_0_30px_0_rgba(105,191,223,0.30)] focus-visible:outline-2 focus-visible:outline-offset-2"
className="border-text-100 hover:border-primary-500 focus-visible:outline-primary-500 flex h-35 items-center gap-7 rounded-[20px] border bg-white px-5.5 py-8 transition-colors hover:shadow-[0_0_30px_0_rgba(105,191,223,0.30)] focus-visible:outline-2 focus-visible:outline-offset-2"
type="button"
>
<img className="size-12.5 shrink-0 rounded-full object-cover" src={club.logo} alt="" />
Expand Down Expand Up @@ -228,14 +237,14 @@ function UniversityCard({ university }: { university: University }) {
const universityLabel = university.campusName ? `${university.name} ${university.campusName}` : university.name;

return (
<button
<Link
to={`/universities/${university.id}/clubs`}
className="border-text-100 hover:border-primary-500 focus-visible:outline-primary-500 flex h-45 flex-col items-center justify-center rounded-[20px] border bg-white py-7 text-center transition-colors hover:shadow-[0_0_30px_0_rgba(105,191,223,0.30)] focus-visible:outline-2 focus-visible:outline-offset-2"
type="button"
>
<img className="size-12.5 object-contain" src={university.imageUrl} alt="" />
<span className="mt-3 block truncate text-[20px] leading-10 font-semibold text-black">{universityLabel}</span>
<span className="text-text-600 text-[14px] leading-6 font-medium">{university.clubCount}개 동아리</span>
</button>
</Link>
);
}

Expand Down
Loading
Loading