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
59 changes: 47 additions & 12 deletions frontend/src/pages/MainPage/MainPage.styles.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import styled from 'styled-components';
import {media} from '@/styles/mediaQuery';

export const PageContainer = styled.div`
padding: 0 40px;
max-width: 1180px;
margin: 0 auto;

@media (max-width: 500px) {
${media.mobile} {
padding: 0 20px;
}

@media (max-width: 375px) {
${media.mini_mobile} {
padding: 0 10px;
}
`;
Expand All @@ -18,37 +19,71 @@ export const ContentWrapper = styled.div`
width: 100%;
`;

export const SectionTabs = styled.nav`
display: flex;
Comment on lines +22 to +23
Copy link
Member

@oesnuj oesnuj Sep 23, 2025

Choose a reason for hiding this comment

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

nav semantic 태그 사용해주신거 좋네요~
접근성까지 잘 챙기신 것 같아요 👏

gap: 18px;
margin: 60px 8px 24px;

${media.mobile} {
gap: 16px;
margin: 32px 4px 16px;
}
Comment on lines +29 to +30
Copy link
Member

Choose a reason for hiding this comment

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

저번에 말씀드린 단축 속성 잘 활용하셨네요

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

덕분입니당!

`;

export const Tab = styled.button<{$active?: boolean}>`
display: flex;
position: relative;
font-size: 24px;
font-weight: bold;
color: ${({$active}) => $active ? '#787878' : '#DCDCDC'};
border: none;
background: none;
cursor: pointer;

&::after {
content: '';
position: absolute;
left: 0;
bottom: -4px;
width: 100%;
height: 1.5px;
background: #787878;
border-radius: 1.5px;
transform: ${({$active}) => $active ? 'scaleX(1)' : 'scaleX(0)'};
transform-origin: center;
transition: transform 0.2s ease;
}

${media.mobile} {
font-size: 14px
}
`;

export const CardList = styled.div`
display: grid;
width: 100%;
max-width: 100%;
gap: 35px;
margin-top: 50px;
transition:
gap 0.5s ease,
grid-template-columns 0.5s ease;

grid-template-columns: repeat(3, 1fr);

@media (max-width: 1280px) {
${media.laptop} {
grid-template-columns: repeat(2, 1fr);
}

@media (max-width: 750px) {
grid-template-columns: repeat(1, 1fr);
}
@media (max-width: 500px) {

${media.mobile} {
gap: 16px;
margin-top: 16px;
}
`;

export const FilterWrapper = styled.div`
display: flex;
justify-content: right;
margin: 20px 0;
`;

export const EmptyResult = styled.div`
padding: 80px 20px;
text-align: center;
Expand All @@ -57,7 +92,7 @@ export const EmptyResult = styled.div`
line-height: 1.6;
white-space: pre-line;

@media (max-width: 500px) {
${media.mobile} {
font-size: 0.95rem;
}
`;
21 changes: 14 additions & 7 deletions frontend/src/pages/MainPage/MainPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import useTrackPageView from '@/hooks/useTrackPageView';
import { useGetCardList } from '@/hooks/queries/club/useGetCardList';
import CategoryButtonList from '@/pages/MainPage/components/CategoryButtonList/CategoryButtonList';
import ClubCard from '@/pages/MainPage/components/ClubCard/ClubCard';
import StatusRadioButton from '@/pages/MainPage/components/StatusRadioButton/StatusRadioButton';
import Footer from '@/components/common/Footer/Footer';
import Header from '@/components/common/Header/Header';
import Banner from '@/pages/MainPage/components/Banner/Banner';
Expand All @@ -18,15 +17,18 @@ import * as Styled from './MainPage.styles';
const MainPage = () => {
useTrackPageView('MainPage');

const [isFilterActive, setIsFilterActive] = useState(false);
const { selectedCategory } = useSelectedCategory();

const { keyword } = useSearchKeyword();
const { isSearching } = useSearchIsSearching();
const recruitmentStatus = isFilterActive ? 'OPEN' : 'all';
const recruitmentStatus = 'all';
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

🧩 Analysis chain

recruitmentStatus 상수화에 따른 훅 시그니처/쿼리키 영향 점검

UI에서 모집 상태 필터를 제거했으므로 훅 인자 유지가 불필요할 수 있습니다. useGetCardList의 쿼리 키/파라미터에서 recruitmentStatus를 완전히 제거 가능 여부를 확인해주세요(중복 캐시 방지 차원).


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "잔존 컴포넌트/스타일 사용 여부 확인 (StatusRadioButton/FilterWrapper)"
rg -n -C2 --type=ts --type=tsx "\bStatusRadioButton\b|FilterWrapper\b" frontend || true

echo "recruitmentStatus 사용처 확인"
rg -n -C2 --type=ts --type=tsx "\brecruitmentStatus\b" frontend || true

echo "useGetCardList 정의/파라미터 확인"
rg -n -C3 --type=ts --type=tsx "(export\s+(const|function)\s+useGetCardList\b|function\s+useGetCardList\s*\()" frontend || true

Length of output: 189


rg의 --type=tsx 옵션 때문에 검색이 실패했습니다. 아래 스크립트로 타입 필터 없이 재검색해 주세요:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "잔존 컴포넌트/스타일 사용 여부 확인 (StatusRadioButton / FilterWrapper)"
rg -n -C2 "\bStatusRadioButton\b|\bFilterWrapper\b" frontend || true

echo "recruitmentStatus 사용처 확인"
rg -n -C2 "\brecruitmentStatus\b" frontend || true

echo "useGetCardList 정의/파라미터(함수/const export) 및 호출 확인"
rg -n -C3 "export\s+(const|function)\s+useGetCardList\b|function\s+useGetCardList\s*\(|\buseGetCardList\s*\(" frontend || true

Length of output: 8889


recruitmentStatus 제거 및 useGetCardList 시그니처·쿼리키 정리 필요

검증: useGetCardList 정의는 frontend/src/hooks/queries/club/useGetCardList.ts이고 호출지는 frontend/src/pages/MainPage/MainPage.tsx(현재 recruitmentStatus = 'all' 상수 전달) 한 곳뿐입니다. 조치(요약):

  • useGetCardList에서 recruitmentStatus 파라미터 제거 및 queryKey에서 삭제(또는 훅 내부에서 고정값 'all' 사용). 파일: frontend/src/hooks/queries/club/useGetCardList.ts.
  • 훅 내부의 파라미터 명/순서(category ↔ division)가 혼동을 유발하므로 queryKey와 getClubList 호출 인자 순서를 명확히 일치시키고 시그니처를 정리할 것.
  • 호출부(MainPage.tsx)를 훅 시그니처에 맞춰 수정(인자 재정렬/제거). 파일: frontend/src/pages/MainPage/MainPage.tsx.
  • 참고: getClubList(frontend/src/apis/getClubList.ts)는 recruitmentStatus를 URL 파라미터로 사용하므로, 훅에서 제거할 경우 getClubList에 기본값('all')이 전달되도록 호출하도록 변경할 것.

const division = 'all';
const searchCategory = isSearching ? 'all' : selectedCategory;

const tabs = ['중앙동아리'] as const;
Copy link
Contributor

Choose a reason for hiding this comment

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

as const를 사용하신 이유가뭔가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

as const를 사용하지 않아도 큰 문제는 없지만, "중앙동아리"처럼 탭은 값이 정해져 있는 집합이기 때문에 읽기 전용으로 두는게 좋을 것 같다고 생각했습니다ㅏ

  const tabs = ['중앙동아리', '가동아리', '과동아리'] as const;
  const [active, setActive] = useState<typeof tabs[number]>('중앙동아리');

위의 코드처럼 tabs의 값들로부터 타입을 바로 추론해 active를 관리하면, 항상 tabs 안의 값만 선택할 수 있게 됩니당
이렇게 해주면 "중동아리"처럼 오타나 잘못된 값이 들어가도 컴파일 단계에서 바로 막아줄 수 있고, tabs의 배열이 타입을 바로 추론해주기 때문에 별도의 타입 선언도 필요없습니다

간결하게 작성할 수도 있고, 타입 안정성을 확보할 수 있어서 as const를 사용했어요~

Copy link
Member

@oesnuj oesnuj Sep 23, 2025

Choose a reason for hiding this comment

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

tab에 들어갈 동아리 타입을 types/club.ts에서 관리하는 방식도 좋아 보입니다!

조금 멀리 보면 API 응답에서 division 값으로 중동/과동 등이 분류될 것 같아서
아래처럼 관리하면 기존 API 타입을 확장하면서, 앞으로 필터링이나 카테고리 기반 기능을 추가할 때도 유용할 것 같아요.

// 예시
// types/club.ts
export type DivisionKey = '중동' | '가동' | '과동';

export interface Club {
  id: string;
  name: string;
  logo: string;
  tags: string[];
  recruitmentStatus: string;
  division: DivisionKey;   // ← 문자열 대신 타입으로 관리
  category: string;
  introduction: string;
}

export const DivisionLabel: Record<DivisionKey, string> = {
  중동: '중앙동아리',
  가동: '가동아리',
  과동: '과동아리',
};

// 사용할 때
const tabs: DivisionKey[] = ['중동', '과동'];

다만 현재 API가 "중동"처럼 태그 형태로 내려주다 보니 매핑 로직이 꼭 필요하겠네요.

수현님이 사용하신 as const 방식도 타입 안정성을 챙기려는 의도가 잘 드러나서 좋았습니다~ 🙂

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

오 좋은 조언 감사합니다ㅏ
그런데 당장은 중동을 제외하고는 어떻게 될지 몰라서 지금 타입을 정의하는 것보다는 나중에 확정되었을 때, 추가하면 좋을 것 같아요!

추후 확장성을 고려해서 준서님께서 제시해주신 부분을 참고할 수 있도록 //TODO 주석으로 남겨뒀습니다ㅏ 감사합니당☺️

const [active, setActive] = useState<typeof tabs[number]>('중앙동아리');
// TODO: 추후 확정되면 DivisionKey(중동/가동/과동) 같은 타입을
// types/club.ts에 정의해서 tabs 관리하도록 리팩터링하기

const {
data: clubs,
error,
Expand All @@ -53,9 +55,14 @@ const MainPage = () => {
/>
<Styled.PageContainer>
<CategoryButtonList />
<Styled.FilterWrapper>
<StatusRadioButton onChange={setIsFilterActive} />
</Styled.FilterWrapper>
<Styled.SectionTabs>
{tabs
.map((tab) =>(
<Styled.Tab key={tab} $active={active===tab} onClick={() => setActive(tab)}>
{tab}
</Styled.Tab>
))}
</Styled.SectionTabs>
Comment on lines +58 to +65
Copy link
Contributor

Choose a reason for hiding this comment

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

list로 탭관리하는 부분 좋네요

<Styled.ContentWrapper>
{isLoading ? (
<Spinner />
Expand Down
11 changes: 8 additions & 3 deletions frontend/src/styles/mediaQuery.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
export const BREAKPOINT = {
mobile: 500,
tablet: 900,
mini_mobile: 375, // ≤ 375
mobile: 500, // ≤ 500
tablet: 700, // ≤ 700
laptop: 1280, // ≤ 1280
};

export const media = {
mini_mobile: `@media (max-width: ${BREAKPOINT.mini_mobile}px)`,
mobile: `@media (max-width: ${BREAKPOINT.mobile}px)`,
tablet: `@media (max-width: ${BREAKPOINT.tablet}px)`,
};
laptop: `@media (max-width: ${BREAKPOINT.laptop}px)`,
// 1281px 이상은 기본 스타일 (desktop)
};