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
2 changes: 2 additions & 0 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Layout from './layout';
import ClubDetail from './pages/ClubDetail';
import Home from './pages/Home';
import RegisterClub from './pages/RegisterClub';
import UniversityClubList from './pages/UniversityClubList';

const SentryRoutes = Sentry.withSentryReactRouterV7Routing(Routes);
Expand All @@ -15,6 +16,7 @@ function App() {
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="/universities/:universityId/clubs" element={<UniversityClubList />} />
<Route path="/clubs/register" element={<RegisterClub />} />
<Route path="/clubs/:clubId" element={<ClubDetail />} />
</Route>
</SentryRoutes>
Expand Down
6 changes: 1 addition & 5 deletions apps/web/src/apis/clubDetail/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ import type { ClubCategory } from '@/apis/common/club';

import type { UniversitySummary } from '../universityClub/entity';

export interface ClubDetailUniversitySummary extends UniversitySummary {
clubCount: number;
}

export interface ClubDetailResponse {
id: number;
name: string;
Expand All @@ -16,5 +12,5 @@ export interface ClubDetailResponse {
description: string;
introduce: string;
location: string;
university: ClubDetailUniversitySummary;
university: UniversitySummary;
}
1 change: 1 addition & 0 deletions apps/web/src/apis/universityClub/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface UniversitySummary {
region: Region;
regionName: string;
imageUrl: string;
clubCount: number;
}

export interface ClubCategorySummary {
Expand Down
Binary file added apps/web/src/assets/edit-club-detail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/src/assets/new-club.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/src/assets/register-club.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions apps/web/src/components/RecentClubList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,17 @@ interface RecentClubListProps {
emptyClassName?: string;
}

interface RecentClubListByIdsProps extends RecentClubListProps {
recentClubIds: number[];
}

function RecentClubList({ className, emptyClassName }: RecentClubListProps) {
const recentClubIds = useRecentClubIds();

return <RecentClubListByIds className={className} emptyClassName={emptyClassName} recentClubIds={recentClubIds} />;
}

export function RecentClubListByIds({ className, emptyClassName, recentClubIds }: RecentClubListByIdsProps) {
if (recentClubIds.length === 0) {
return <RecentClubListMessage className={emptyClassName} message="최근에 본 동아리가 없어요." />;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/UniversityClubSidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function UniversityClubSidebar({ university, clubCount }: UniversityClubSidebarP
</div>
</section>

<section className="border-text-100 rounded-4xl border bg-white px-3 py-7 sm:rounded-[40px] sm:px-6 sm:py-11">
<section className="border-text-100 rounded-4xl border bg-white px-3 py-7 sm:rounded-[40px] sm:px-7.5 sm:py-6">
<h2 className="text-text-600 text-[20px] leading-10 font-medium">최근에 본 동아리</h2>
<RecentClubList className="mt-5 flex flex-col gap-5" />
</section>
Expand Down
7 changes: 6 additions & 1 deletion apps/web/src/layout/Header/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { useNavigate } from 'react-router-dom';
import Logo from '@/assets/image/Logo.png';

function Header() {
const navigate = useNavigate();
return (
<header className="border-text-100 border-b bg-white">
<div className="mx-auto flex h-15 w-full max-w-279 items-center justify-between px-2 lg:h-21 lg:px-10">
<div className="mx-auto flex h-15 w-full max-w-279 items-center justify-between px-2 lg:h-21 lg:px-0">
<a className="flex h-full items-center" href="/" aria-label="Konect 홈">
<img className="aspect-11/8 h-10 object-center mix-blend-multiply sm:h-10" src={Logo} alt="로고" />
<span className="text-primary-500 [font-family:var(--font-cal-sans)] text-[28px] leading-9 font-normal sm:text-[32px] sm:leading-10">
Expand All @@ -13,6 +15,9 @@ function Header() {
<button
className="border-text-200 text-text-600 hover:border-primary-500 hover:text-primary-700 focus-visible:outline-primary-500 hidden h-10 shrink-0 items-center rounded-full border px-5 transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 sm:flex"
type="button"
onClick={() => {
navigate('/clubs/register');
}}
>
동아리 등록하기
</button>
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/pages/ClubDetail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import AddPhotoIcon from '@/assets/svg/add-photo-icon.svg';
import Breadcrumb from '@/components/Breadcrumb';
import UniversityClubSidebar from '@/components/UniversityClubSidebar';
import { CATEGORY_TEXT_COLORS } from '@/constants/club';
import useResetScroll from '@/utils/hooks/useResetScroll';
import { saveRecentClubId } from '@/utils/recentClubStorage';

const INTRODUCE_MEDIA_ITEMS = [
Expand Down Expand Up @@ -55,6 +56,9 @@ function NoneIntroduce() {

export default function ClubDetail() {
const { clubId } = useParams();

useResetScroll(clubId);

const { data: clubDetail } = useSuspenseQuery(clubDetailQueries.detail(Number(clubId)));

useEffect(() => {
Expand Down
90 changes: 66 additions & 24 deletions apps/web/src/pages/Home/index.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
import { useState, type ChangeEvent } from 'react';
import { useEffect, useRef, 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 { Link, useSearchParams } from 'react-router-dom';

import type { Region, HomeRequestParams, University } from '@/apis/home/entity';
import { homeQueries } from '@/apis/home/queries';
import heroCatBook from '@/assets/hero-cat-book.png';
import SearchIcon from '@/assets/svg/search-icon.svg';
import RecentClubList from '@/components/RecentClubList';
import { RecentClubListByIds } from '@/components/RecentClubList';
import { useRecentClubIds } from '@/utils/recentClubStorage';

const REGION_OPTIONS: { label: string; value?: Region }[] = [
{ label: '전체' },
{ label: '서울', value: 'SEOUL' },
{ label: '경기도', value: 'GYEONGGI' },
{ label: '강원도', value: 'GANGWON' },
{ label: '충청도', value: 'CHUNGCHEONG' },
{ label: '전라도', value: 'JEOLLA' },
{ label: '경상도', value: 'GYEONGSANG' },
{ label: '강원도', value: 'GANGWON' },
{ label: '전라도', value: 'JEOLLA' },
{ label: '제주도', value: 'JEJU' },
];

const HOME_UNIVERSITY_SECTION = 'universities';
const REGION_PARAM = 'region';
const SECTION_PARAM = 'section';

function Home() {
const [selectedRegion, setSelectedRegion] = useState<Region>();
const [searchParams, setSearchParams] = useSearchParams();
const [searchKeyword, setSearchKeyword] = useState('');
const [searchQuery, setSearchQuery] = useState('');
const universityListRef = useRef<HTMLElement>(null);
const selectedRegion = getRegionParam(searchParams.get(REGION_PARAM));
const targetSection = searchParams.get(SECTION_PARAM);

const updateSearchQuery = useDebouncedCallback((value: string) => {
setSearchQuery(value.trim());
Expand All @@ -37,14 +45,38 @@ function Home() {
const { data: homeData } = useSuspenseQuery(homeQueries.detail(homeParams));
const universities = homeData.universities ?? [];
const totalUniversityCount = homeData.totalUniversityCount;
const recentClubIds = useRecentClubIds();
const isSearching = searchKeyword.trim().length > 0 || searchQuery.length > 0;
const hasRecentClubs = recentClubIds.length > 0;

const handleSearchKeywordChange = (event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setSearchKeyword(value);
updateSearchQuery(value);
};

const handleRegionChange = (region?: Region) => {
setSearchParams((prev) => {
const params = new URLSearchParams(prev);

if (region) params.set(REGION_PARAM, region);
else params.delete(REGION_PARAM);

params.delete(SECTION_PARAM);
return params;
});
};

useEffect(() => {
if (targetSection !== HOME_UNIVERSITY_SECTION) return;

const animationFrameId = requestAnimationFrame(() => {
universityListRef.current?.scrollIntoView({ block: 'start' });
});

return () => cancelAnimationFrame(animationFrameId);
}, [selectedRegion, targetSection]);

return (
<div className="min-h-screen text-black">
<main className="mx-auto flex w-full max-w-264 flex-col items-center pt-16 pb-24 sm:pt-24 lg:pt-34">
Expand Down Expand Up @@ -87,21 +119,27 @@ function Home() {
</label>
</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="관심있게 봤던 동아리를 다시 확인해보세요." />
<RecentClubList
className="mt-4 grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"
emptyClassName="mt-4"
/>
</section>
</div>
{hasRecentClubs ? (
<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-10 grid-rows-[1fr] opacity-100 sm:mt-24'
}`}
>
<section
className={`min-h-0 ${isSearching ? 'overflow-hidden' : 'overflow-visible'}`}
aria-hidden={isSearching}
>
<SectionTitle title="최근에 본 동아리" description="관심있게 봤던 동아리를 다시 확인해보세요." />
<RecentClubListByIds
className="mt-4 grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"
emptyClassName="mt-4"
recentClubIds={recentClubIds}
/>
</section>
</div>
) : null}

<section className="mt-10 w-full sm:mt-31.75">
<section ref={universityListRef} className="mt-10 w-full scroll-mt-5 sm:mt-31.75">
<SectionTitle title="전체 대학" description="학교별 동아리를 자유롭게 탐색해보세요." />
<div className="mt-4 grid grid-cols-[minmax(0,1fr)_auto] items-start gap-x-4 gap-y-2">
<div className="flex min-w-0 flex-wrap gap-2">
Expand All @@ -110,15 +148,15 @@ function Home() {

return (
<button
className={`shrink-0 rounded-[26px] px-3 transition-colors sm:px-5 ${
className={`shrink-0 rounded-[26px] border px-3 font-medium transition-colors sm:px-5 ${
isSelected
? 'bg-primary-900 text-indigo-5 font-medium'
: 'border-text-300 text-text-300 hover:border-primary-500 hover:text-primary-700 border bg-transparent'
? 'border-primary-900 bg-primary-900 text-indigo-5'
: 'border-text-300 text-text-300 hover:border-primary-500 hover:text-primary-700 bg-transparent'
}`}
key={region.label}
type="button"
aria-pressed={isSelected}
onClick={() => setSelectedRegion(region.value)}
onClick={() => handleRegionChange(region.value)}
>
<span className="leading-7 sm:text-[20px] sm:leading-10">{region.label}</span>
</button>
Expand Down Expand Up @@ -188,4 +226,8 @@ function UniversityCard({ university }: { university: University }) {
);
}

function getRegionParam(value: string | null): Region | undefined {
return REGION_OPTIONS.some((region) => region.value === value) ? (value as Region) : undefined;
}

export default Home;
56 changes: 56 additions & 0 deletions apps/web/src/pages/RegisterClub/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import EditClub from '@/assets/edit-club-detail.png';
import NewClub from '@/assets/new-club.png';
import Register from '@/assets/register-club.png';
export default function RegisterClub() {
const registerClubCards = [
{
image: EditClub,
imageAlt: '동아리 정보 수정',
title: '동아리 정보 수정',
description: '이미 KONECT에 등록된 동아리의 소개, 사진, 상세정보를 추가하거나 수정할 수 있어요',
target: '대상 : 동아리 회장, 임원진',
},
{
image: NewClub,
imageAlt: '신규 동아리 등록',
title: '신규 동아리 등록',
description: '아직 KONECT에 등록되지 않은 동아리의 기본 정보와 소개 정보를 제출할 수 있어요.',
target: '대상 : 미등록된 동아리의 관계자',
},
{
image: Register,
imageAlt: '학교 동아리 목록 등록',
title: '학교 동아리 목록 등록',
description: '총동 / 학생회 담당자가 학교 단위의 동아리 목록과 기본 정보를 한 번에 전달할 수 있어요.',
target: '대상 : 총동아리 연합회 / 학생회 담당자',
},
];
return (
<main className="flex min-h-screen flex-col items-center gap-10 py-11.5">
<section className="flex flex-col items-center gap-5">
<div className="bg-primary-100 border-primary-400 text-primary-500 flex h-16 w-74.25 items-center justify-center gap-2.5 rounded-[30px] border-2 px-7.5 py-3 text-[24px] leading-10 font-semibold">

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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

시맨틱 타이포그래피 유틸리티 사용을 권장합니다.

text-[24px], text-[20px], text-[14px] 대신 @konect/design-tokens에서 제공하는 시맨틱 유틸리티(예: text-h2, text-body1)를 사용하면 디자인 일관성과 유지보수성이 향상됩니다.

가이드라인 근거: Prioritize semantic typography utilities (e.g., text-h1, text-body1) over generic utilities

Also applies to: 35-35, 47-49

🤖 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/RegisterClub/index.tsx` at line 31, The JSX uses raw
size-based utilities (e.g., text-[24px], text-[20px], text-[14px]) in the
RegisterClub component's className (the div with className containing
text-[24px] and other locations noted), so replace those with the semantic
typography utilities from `@konect/design-tokens` (for example text-h2,
text-body1, text-caption or whichever maps to 24/20/14px in your design system)
to ensure consistency; update each affected className occurrence (the main
rounded div and the other occurrences referenced) swapping the raw text-* tokens
for the corresponding semantic tokens and run a quick visual check to confirm
spacing/line-height remains correct.

<span className="bg-primary-500 size-2 rounded-full" aria-hidden="true" />
동아리 정보 등록/수정
</div>
<span className="text-text-400 text-[20px]">
작성자 유형에 따라 필요한 항목이 다릅니다. 현재 상황에 맞는 항목을 선택해주세요.
</span>
</section>
<section className="flex gap-5">
{registerClubCards.map((card) => (
<div
key={card.title}
className="border-text-100 flex h-92.75 w-82.75 flex-col items-center gap-10 rounded-[20px] border bg-[#ffffff] px-7.5 py-10"
>
<img src={card.image} alt={card.imageAlt} />
<div className="flex flex-col items-center">
<h2 className="text-text-700 text-[24px] font-semibold">{card.title}</h2>
<span className="text-text-600 mt-10 w-65 text-center text-[14px] leading-4">{card.description}</span>
<span className="text-text-600 pt-5 text-[14px]">{card.target}</span>
</div>
</div>
))}
</section>
Comment on lines +39 to +53

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.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

반응형 레이아웃을 추가하세요.

카드 3개가 flex gap-5로 가로 배치되어 있어 모바일에서 레이아웃이 깨질 수 있습니다. 반응형 처리를 권장합니다.

반응형 레이아웃 예시
-      <section className="flex gap-5">
+      <section className="flex flex-col gap-5 md:flex-row">
         {registerClubCards.map((card) => (
           <div
             key={card.title}
-            className="border-text-100 flex h-92.75 w-82.75 flex-col items-center gap-10 rounded-[20px] border bg-[`#ffffff`] px-7.5 py-10"
+            className="border-text-100 flex w-full flex-col items-center gap-10 rounded-[20px] border bg-white px-7.5 py-10 md:h-92.75 md:w-82.75"
           >
🤖 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/RegisterClub/index.tsx` around lines 39 - 53, The
three-card layout is not responsive; update the section and card container
classes around the registerClubCards mapping so cards stack on small screens and
lay out horizontally on larger screens (e.g., change the section's container
from "flex gap-5" to a responsive layout such as "flex flex-col gap-5
md:flex-row md:gap-5" or use "grid grid-cols-1 md:grid-cols-3 gap-5"), and make
each card's sizing responsive by replacing fixed widths/heights on the card div
(the element keyed by card.title inside the map) with responsive classes like
"w-full md:w-82.75" and "h-auto md:h-92.75" (or equivalent Tailwind utilities)
so images and text wrap correctly on mobile while preserving the desktop look.

</main>
);
}
Loading
Loading