From 478b35a0b70f7cce77a7b25798eed5efb1a18f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=A3=BC?= <145267904+ParkSungju01@users.noreply.github.com> Date: Thu, 28 May 2026 19:03:15 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EB=93=B1=EB=A1=9D=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80=20(#322)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 동아리 정보 등록 페이지 추가 * fix: coderabbit 수정사항 수정 * fix: 코드 중복 수정 --- apps/web/src/App.tsx | 2 + apps/web/src/assets/edit-club-detail.png | Bin 0 -> 825 bytes apps/web/src/assets/new-club.png | Bin 0 -> 2263 bytes apps/web/src/assets/register-club.png | Bin 0 -> 1411 bytes apps/web/src/layout/Header/index.tsx | 5 ++ apps/web/src/pages/RegisterClub/index.tsx | 56 ++++++++++++++++++++++ 6 files changed, 63 insertions(+) create mode 100644 apps/web/src/assets/edit-club-detail.png create mode 100644 apps/web/src/assets/new-club.png create mode 100644 apps/web/src/assets/register-club.png create mode 100644 apps/web/src/pages/RegisterClub/index.tsx diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 12f62bb..a1fbda7 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -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); @@ -15,6 +16,7 @@ function App() { }> } /> } /> + } /> } /> diff --git a/apps/web/src/assets/edit-club-detail.png b/apps/web/src/assets/edit-club-detail.png new file mode 100644 index 0000000000000000000000000000000000000000..25187582c3f353f6ce264d949fe4d1c8f0c0627d GIT binary patch literal 825 zcmeAS@N?(olHy`uVBq!ia0vp^Q6S901|%(3I5Gh#&H|6fVg?3oVGw3ym^DX&fr06@ zr;B4q#hkZy9J7TTMGkzt+FW(PKSA&#_lxNQFApwu7qmIEkJIfVvxkF!9gA(XfPKUC zS9=r|eOVC7;l@y4Sr9E( zDLY}G{FmpCizOQ0n>O-#ERJG-`#6Pl!hQ92N6RNa4vBw%Vs)dTWcf9(YaMTx3tf6< zgl5KbLl{&GVd1vLDD<1o7Pn=C!-u8*}+Y{$MpSJPs`Wnp4 z`@dfGz1-}&+|P5WGEVSXx@Vjav^38+A!@0faYE8kJmZ9{C40sTrlQ*m7iyo#ZEMq0 zJpOI>j73Lx-kDI`Qf8_2ob7Fq>p9L_PkZKYZ^@Rp$-XZ-qrHsjgwp3+<1NBYVZ3iy zCSG;2PE474L632hRfc*2_w<~7xs7E`-%qYuIQPNfj;IXvKS!1e-BPVs6UO;=qTE%d zxJR*btrjxxof>sAYax5{&5Kg<4iU!z|cEB~GwE^pFXzCr#-E(=rati$Kh+@);So4p8;+!Da1JH?SZ z^@0;I%#+Vx!fnT++3>to&8*y?uFe1)@(* hOO1ph*WJH)Gp4U|zyCQ`1(+8YJYD@<);T3K0RYLTeAoa0 literal 0 HcmV?d00001 diff --git a/apps/web/src/assets/new-club.png b/apps/web/src/assets/new-club.png new file mode 100644 index 0000000000000000000000000000000000000000..92b2d8379293cebeda98d856dbd28956dfb28247 GIT binary patch literal 2263 zcmV;|2q^c7P)b~7G-80kEQ+xYM&ck%q&#AAgPFK}A)uU968Z~Ovs8ORv4GX%%XB*({ z2Kh)Jz}w}Touio5jr@{9_HB`Ctu|dSZuvEoQZ3x~HVu3*N%E@Dq3a&n|Msf& zbRHV{XW1l20{ru;^_&@KJaCqV?&UJnuiWatK2ILg;WG{L%Q=XCrPW^kXr3~qZqXss zC0`yV&|F+O%^^v=Q+PVBb z(Eb7>M?IVNn>KwlS)SA}NDp~Pb32w! zRIWk;U7d#@4ZNa}@6o{b4u0|F6DcDoD=c#*S01EqD_3EVw~R}gnVxJ>uEHSSIRgax zIpt2}Dh$wFGeDr9OMvcDu0rf&Nj5gwz^%&7h>&TJuQkZG z8{n;xY}ddKI%bpoAtKI&-3I9o{dOX6k=1VL4N+ z*Mn%_gLz$Yg(0q>svBr#M1%`f76QN9V3*Lk5DoM<8R4o-i2tqXhMKC0M(+|;I(&b_ zI=9u!G3RfYaA`x|OkTE4h2(%}q(8=n63~AQ(BVMBAjBcc($RkUFwg2e`}z>kK+lDF z_?38hbTqAA4fJZFjMsAMC*}9`qjKlxpDL-zC9^QzIJL zmkqDpbqRVjNdH?~FW(%i7x?BB-CmP&duiry>HTXSM8~^Om5CrsDy4j|W8>Bti zV#VeG|29$P?Fk)UOztCJxpm(g*ElH>1-1iyd-}$w`1uY zW04#U|7=@3glK_8YHHwrludMKr&oJe1Nn@^{hQK>rXSkrHZ68PX^-N3b2$Z85^83bzSKiz+ zZwC0EX+bnaIEeD!qlzNm!XuODJ2dc~3-HYDG`gH+uMD&yWbnWF2$u;|kMds9W(xyXR5~IEhZKA{IAC5UOlhdKe zy$plKb`rWkZ`zlfi8MP0KHN{2$X7T(6X=-C`wJ5tlI@+J=cvPw9_@>>p!f1g!z788 zQ(QlI;F-7f%CP-wZi}%OX&S#+DOwt zzszAO9xWxVBT#n1JINsZbyT7?&~w-C>V7aU(S!aDl;IGFq{ClSRrm}glGHr7 zd`e#=0(0Qdqg&(Yt$X(MA<~>r@+Y>`R+8(-I41jw?o&= zxC9}=z`LWi_1d|)3Egi=C|Ri_=XnTnVBP6M_rU)#$hz`F_jw?nVsMdcEhNM8r<6;O zo?SI@rl3fChjI;p8F9&G10Am1_KpgPWpjvVRIb7$2Z081gl=h+SK_A_cy^CZ}k8_Z(X&K<1ML#_s#Ww#XKhd;j zP_Uu(>G@=I?ER&05KR*eUUMvWRZYSa)0{tuLJ7ZA{E=VAZ=002ovPDHLkV1mt!W3>PP literal 0 HcmV?d00001 diff --git a/apps/web/src/assets/register-club.png b/apps/web/src/assets/register-club.png new file mode 100644 index 0000000000000000000000000000000000000000..4dff290b141838c5cd7059abaafc2bc41d0cb723 GIT binary patch literal 1411 zcma)+do&XY0LGWcluSu7!$suXsk|lHJhv7%^2qzG71Fu$PMVg|)htb3BfHnGB&>N% z@+dcM4a;nnN9J`A#%O0A+r-s5_pkfc{m%Km-#_2?@AvSMhZ{&9A`bunKnS?A*B%3Y zct3D2ZK`2XdpK|t?vDll4l4ifK0s+17yyv9L^#8|Q_EJ$2k_o5hd)TB4z+`Aiw*U6 za$r9L?_s`89op8iA;3q5?Hf(LY4bqSHCcxdNAcqpN|w$!r!TKRmN_$Bl=G97osYSO z>^&L9dgQUo=53`PdEpa62`52J^lm6APiL=}jHQ3)tsAQx{g22oc?|oStO@qVtClQH zYz8H-DxVL3q?BCMl9@D@@2KNe_i_L{i;Q+U?cyZ&#*nI7NGI$ctpzi)9nAuJT^73{ zMoYNy^NM5IyFtmMsyJz}Q3?~XmAsf0F{;LBLCeKU)~1Nm422OHW>9RXfe8HS0IBtJ zr-$H7m@*Vyce{Kx@|`|rK0tDozR^X9F~-aT#)dvhp%h4~9J&;$)^_3l&okAy9Qaod z3ntB7Q4)N)Y+yG~%I@H_DQO)cROrrrb?*;^!0i+SI-rbEz)4xAO*f{{!VCHC)3(j# z?CWoRH@t9h-V?@};3}FdeSDt&3E68PD(C`ky(*BItGpnWQwe4u<9!_ZuooQ~ltl;T z+T!;4jt+}w9s2_n)Z$-bPT#JPII?cdR_WoD zih!cG{hg-P(@k-1cYVqRyL961rf#mt;)Gp8FZk>TAZj?w@p{lm(?BQ_ilq75EE?D) zO=FU|swT`@>4lzZcV4?pU48&9xmD4%)F3^UFA;bMKfi7+>D)Zhl*QlZwt^*8@|w{P z{f?sLU4&a>P)+nJd7?QR!_mTfd$7xreY^B&(5as!}~0?iaq^f1iKt) z=2Dj$<4&4%`pZ9ZE#w!-o}s>wXyJki3jL;K?6<`Jnlf_!`C6aFV%kMqUb^|w5&xLv z^8~qd+X`a%wGY%Dx`{DF^u@G3A=f%LHkM3bV_h8rfeS#EBJ$RT_fHl~{tyYox@e?^ z%P@Rl-+-+ybXuGH`_ZXF>N)cAN~?b`Gcj`6DN;hD=H3T2McH{>Y3N91lTDr^AQUN9 z(5v!oFe&ThzAGv+pr!&IjcP{D%4>rnmc%Eu;QS|GHy`d0G-Pt}pmpX3BhL~{Eqc@w zGoI$c@dTpYIstd=suAX3Ds~?46Wk0+d#V9i4WI+a69XH_lzIwNW}Yq@Ih|{v>MVlRmwE2Fk|0C=T@_RKt7WO1r%>3x9SO93&Q|6!Xzb zVt-_I5W#^yHz*&f(tNFaIXf^7<$!(u3;r{$qAWY=R2gh1V@)GmeAX1L+N|#TQsV^L z((JYoF)(T}yY{Z>sa(3Y3(X+N`rX@Ja!0${hjQznoh0`8NcidybgZ&&?^OW^7Z2xp I$FRHq0DXzI)c^nh literal 0 HcmV?d00001 diff --git a/apps/web/src/layout/Header/index.tsx b/apps/web/src/layout/Header/index.tsx index 0a0611e..4938e4b 100644 --- a/apps/web/src/layout/Header/index.tsx +++ b/apps/web/src/layout/Header/index.tsx @@ -1,6 +1,8 @@ +import { useNavigate } from 'react-router-dom'; import Logo from '@/assets/image/Logo.png'; function Header() { + const navigate = useNavigate(); return (
@@ -13,6 +15,9 @@ function Header() { diff --git a/apps/web/src/pages/RegisterClub/index.tsx b/apps/web/src/pages/RegisterClub/index.tsx new file mode 100644 index 0000000..48b5051 --- /dev/null +++ b/apps/web/src/pages/RegisterClub/index.tsx @@ -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 ( +
+
+
+
+ + 작성자 유형에 따라 필요한 항목이 다릅니다. 현재 상황에 맞는 항목을 선택해주세요. + +
+
+ {registerClubCards.map((card) => ( +
+ {card.imageAlt} +
+

{card.title}

+ {card.description} + {card.target} +
+
+ ))} +
+
+ ); +} From 5a1d6390560279b84407e5cc28d7a1bc54fd5c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Thu, 28 May 2026 21:02:03 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[feat]=20=EC=9B=B9=20=ED=99=88=20=EB=B0=8F?= =?UTF-8?q?=20=EB=8C=80=ED=95=99=EB=B3=84=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=ED=83=90=EC=83=89=20=EA=B0=9C=EC=84=A0=20(#324)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [배포] 이미지 전처리, 광고 기능, 하단바 리디자인, 인앱 알림 페이지 및 토스트 프로덕션 배포 (#230) * 205 feat 이미지 전처리 기능 구현 (#206) * feat: 전처리 로직 및 WebWorker 구현 * feat: 전처리 적용 및 preview 동시성 제어 로직 추가 * refactor: 리뷰 반영 * [hotfix] 하단바 너비 수정 (#208) * hotfix: 하단바 너비 수정 * chore: 불필요한 값 제거 * refactor: 고정 gap 제거 * Reapply "[feat] 광고 배너 추가 (#200)" This reverts commit c51ec851fd4f7a404fca567e383847c4c9cf6b1c. * [feat] 하단바 리디자인 (#213) * chore: asset 추가 * feat: 하단바 리디자인 반영 및 레이아웃 수정 * [refactor] 광고 카드 레이아웃 밀림 수정 (#215) * refactor: 광고 개수 측정 시기 변경 및 기본값 제거 * feat: 스켈레톤 UI 추가 * feat: 인앱 알림 페이지 및 토스트 구현 (#217) * feat: 알림 API 및 스트림 기반 추가 * feat: 인앱 알림 레이어 추가 * feat: 알림 페이지 및 헤더 진입 구현 * fix: 알림 스트림 401 재시도 조건 정리 * fix: 알림 목록 이동 차단 제거 * refactor: 알림 공용 훅 위치 정리 * fix: 알림 재연결 캐시 동기화 추가 * fix: 알림 목록 토스트 큐 누적 방지 * fix: 알림 읽음 카운트 감소 조건 보강 * [refactor] 도메인별 TanStack Query 훅 정리 (#219) * chore: pwa용 이미지 제거 * refactor: auth 도메인 쿼리와 뮤테이션 정리 * refactor: council과 schedule 조회 훅 정리 * refactor: chat과 notification 캐시 처리 정리 * refactor: club 조회와 지원 플로우 정리 * refactor: manager 도메인 캐시 처리 정리 * refactor: studyTime 도메인 쿼리와 뮤테이션 정리 * refactor: 광고와 업로드 도메인 훅 정리 * [refactor] mutaton query 및 hook 추가 수정 (#221) * refactor: auth와 user myInfo 훅 정리 * refactor: club과 schedule 조회 훅 정리 * refactor: chat과 notification 훅 구조 정리 * refactor: club 지원 뮤테이션 훅 정리 * refactor: manager 뮤테이션 훅 구조 정리 * refactor: mutation 훅 cache 정리 * refactor: 컨벤션 통일 * refactor: isRead 조건 정리 * fix: 채팅 스크롤 문제 수정 * refactor: 불필요한 코드 제거 * [fix] 모바일 환경 입력창과 키보드 간의 간격이 큰 문제 수정 (#223) * chore: 가공용 safeArea 변수 선언 * refactor: 고정 패딩 값 수정 및 safeArea 적용 변경 * feat: 키보드 활성화 감지 및 safeArea 적용 여부 기능 추가 * refactor: 매직넘버 상수화 및 가로모드 처리 * [fix] 키보드 활성화 시 화면 흔들림 문제 수정 (#225) * refactor: 채팅 viewport 훅 네이밍 정리 * refactor: viewport 높이 잠금 훅 적용 시점 조정 * [fix] 키보드 활성화 시 채팅 화면 전체가 흔들리는 문제 수정 (#227) * refactor: 채팅 viewport 훅 네이밍 정리 * refactor: viewport 높이 잠금 훅 적용 시점 조정 * fix: 채팅 화면 스크롤 잠금으로 키보드 흔들림 완화 * fix: 입력 포커스 중 viewport offset 고정 (#229) * fix: 문서 루트 스크롤 잠금으로 빈 공간 잔류 방지 (#232) * [fix] 키보드 활성화 시 채팅 화면 상단 고정이 깨지고 빈 공간이 남는 문제 수정 (#234) * fix: 채팅 화면 상단 고정 깨짐과 빈 공간 잔류 수정 * refactor: 라우트 조건 수정 * fix: 문서 스크롤 위치 감지 보강 * refactor: 입력 요소 판별 유틸과 스크롤 주석 정리 * [fix] 키보드 활성화 시 채팅 화면에서 문서 스크롤이 발생하는 문제 수정 (#236) * fix: 채팅 문서 스크롤 제스처 차단 * fix: 입력 요소 터치 동작 예외 처리 * [fix] 키보드 활성화 시 채팅방이 마지막 메시지 위치를 유지하지 못하는 문제 수정 (#238) * fix: 키보드 활성화 시 채팅 하단 정렬 유지 * refactor: 채팅 리사이즈 관찰 안정화 * fix: mypage 연계 약관 페이지 뒤로가기 수정 (#240) * refactor: alias import 경로 정리 * fix: query 설정과 suspense 분기 정리 * refactor: 관리자 화면 스타일 유틸 정리 * fix: 이미지 전처리 예외 처리 보강 * fix: 헤더와 회비 화면 동작 정리 * fix: 공통 유틸 안정성 개선 * fix: 이미지 전처리 실패 처리를 보정 * fix: 모집 공고 저장 후 설정 반영 순서 조정 * fix: 부원 직책 변경 실패 처리를 보강 * fix: 약관 링크 접근성을 개선 * fix: 공통 쿼리와 유틸 안정성을 보완 * [feat] 동적 버전 정보 표시 구현 (#211) * feat: 동적 버전 정보 표시 구현 * refactor: 버전 정보 미 존재시 v 표시 제거 * [feat] 메인화면 동아리 카드 디자인 수정 반영 (#242) * feat: 메인화면 동아리 카드 디자인 수정 * chore: 하단바 아이콘 수정 * refactor: 코드래빗 리뷰 반영 * refactor: and 연산자로 변경 * apiClient 코드 중복 제거 및 네이티브 브릿지 인증 동기화 중앙화 (#244) * refactor: apiClient 코드 중복 제거 및 네이티브 브릿지 인증 동기화 중앙화 * refactor: body 직렬화 가드를 plain object/array로 한정 * fix: body 읽기 중 AbortError가 ParseError로 오분류되는 문제 수정 * [refactor] 에러 처리 유틸 및 utils 구조 정리 (#246) * refactor: 에러 처리 유틸 및 공통 토스트 흐름 정리 * refactor: utils 폴더 구조를 역할별로 정리 * refactor: 코드래빗 리뷰 반영 * refactor: 코드래빗 리뷰 반영 * Update src/pages/Home/components/HomeClubSection.tsx * fix: 인증 세션 복구 흐름 정리 * fix: 홈 동아리 카드 레이아웃 정리 * [feat] 총동아리 페이지 리디자인 및 하단 오버레이 정리 (#249) * refactor: 하단 오버레이 처리 공통화 * feat: 총동아리 페이지와 헤더 리디자인 반영 * fix: 채팅 하단 여백과 외부 링크 속성 수정 * refactor: 총동아리 헤더 설정 정리 * fix: 총동아리 상세 접근성과 스타일 보완 * [feat] 마이페이지 관리자 카드 분리 및 채팅 미확인 배지 반영 (#251) * feat: 하단 채팅 배지 표시 및 조회 조건 보완 * refactor: 관리자 정보 카드 컴포넌트 분리 * feat: 채팅 페이지 리디자인 (#252) * feat: 채팅 페이지 리디자인 * fix:tailwind 문법 수정 * fix: 코드 수정 * fix: 폰트 색상 및 위치 수정 * fix: 채팅방 사람수 정렬 * fix: 오타 수정 * chore: conflict 해결 중 누락된 부분 수정 * [refactor] 광고 렌더링 조건 수정 (#254) * refactor: 광고 렌더링 조건 수정 * docs: 문서명 변경 --------- Co-authored-by: 박성주 <145267904+ParkSungju01@users.noreply.github.com> * refactor: 가이드 페이지 이미지 변경 및 구조 개선 (#260) * hotfix: 행사 배너 제거 * feat: 웹 동아리 상세 진입 시 스크롤 초기화 * feat: 대학별 동아리 목록 탐색 흐름 개선 * feat: 웹 홈 최근 본 동아리와 지역 필터 개선 * style: 웹 탐색 화면 여백 조정 --------- Co-authored-by: 박성주 <145267904+ParkSungju01@users.noreply.github.com> --- apps/web/src/apis/clubDetail/entity.ts | 6 +- apps/web/src/apis/universityClub/entity.ts | 1 + .../src/components/RecentClubList/index.tsx | 8 ++ .../UniversityClubSidebar/index.tsx | 2 +- apps/web/src/layout/Header/index.tsx | 2 +- apps/web/src/pages/ClubDetail/index.tsx | 4 + apps/web/src/pages/Home/index.tsx | 90 ++++++++++++++----- .../src/pages/UniversityClubList/index.tsx | 59 +++++++----- apps/web/src/utils/hooks/useResetScroll.ts | 9 ++ 9 files changed, 130 insertions(+), 51 deletions(-) create mode 100644 apps/web/src/utils/hooks/useResetScroll.ts diff --git a/apps/web/src/apis/clubDetail/entity.ts b/apps/web/src/apis/clubDetail/entity.ts index fc61399..9adc1ac 100644 --- a/apps/web/src/apis/clubDetail/entity.ts +++ b/apps/web/src/apis/clubDetail/entity.ts @@ -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; @@ -16,5 +12,5 @@ export interface ClubDetailResponse { description: string; introduce: string; location: string; - university: ClubDetailUniversitySummary; + university: UniversitySummary; } diff --git a/apps/web/src/apis/universityClub/entity.ts b/apps/web/src/apis/universityClub/entity.ts index b625af1..d375fb1 100644 --- a/apps/web/src/apis/universityClub/entity.ts +++ b/apps/web/src/apis/universityClub/entity.ts @@ -15,6 +15,7 @@ export interface UniversitySummary { region: Region; regionName: string; imageUrl: string; + clubCount: number; } export interface ClubCategorySummary { diff --git a/apps/web/src/components/RecentClubList/index.tsx b/apps/web/src/components/RecentClubList/index.tsx index f780a1a..8574d73 100644 --- a/apps/web/src/components/RecentClubList/index.tsx +++ b/apps/web/src/components/RecentClubList/index.tsx @@ -10,9 +10,17 @@ interface RecentClubListProps { emptyClassName?: string; } +interface RecentClubListByIdsProps extends RecentClubListProps { + recentClubIds: number[]; +} + function RecentClubList({ className, emptyClassName }: RecentClubListProps) { const recentClubIds = useRecentClubIds(); + return ; +} + +export function RecentClubListByIds({ className, emptyClassName, recentClubIds }: RecentClubListByIdsProps) { if (recentClubIds.length === 0) { return ; } diff --git a/apps/web/src/components/UniversityClubSidebar/index.tsx b/apps/web/src/components/UniversityClubSidebar/index.tsx index 28e568f..0cf57bc 100644 --- a/apps/web/src/components/UniversityClubSidebar/index.tsx +++ b/apps/web/src/components/UniversityClubSidebar/index.tsx @@ -24,7 +24,7 @@ function UniversityClubSidebar({ university, clubCount }: UniversityClubSidebarP
-
+

최근에 본 동아리

diff --git a/apps/web/src/layout/Header/index.tsx b/apps/web/src/layout/Header/index.tsx index 4938e4b..d1f1711 100644 --- a/apps/web/src/layout/Header/index.tsx +++ b/apps/web/src/layout/Header/index.tsx @@ -5,7 +5,7 @@ function Header() { const navigate = useNavigate(); return (
-
+
로고 diff --git a/apps/web/src/pages/ClubDetail/index.tsx b/apps/web/src/pages/ClubDetail/index.tsx index 6ab1f55..e5873a1 100644 --- a/apps/web/src/pages/ClubDetail/index.tsx +++ b/apps/web/src/pages/ClubDetail/index.tsx @@ -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 = [ @@ -55,6 +56,9 @@ function NoneIntroduce() { export default function ClubDetail() { const { clubId } = useParams(); + + useResetScroll(clubId); + const { data: clubDetail } = useSuspenseQuery(clubDetailQueries.detail(Number(clubId))); useEffect(() => { diff --git a/apps/web/src/pages/Home/index.tsx b/apps/web/src/pages/Home/index.tsx index fc98c8f..b49cbca 100644 --- a/apps/web/src/pages/Home/index.tsx +++ b/apps/web/src/pages/Home/index.tsx @@ -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(); + const [searchParams, setSearchParams] = useSearchParams(); const [searchKeyword, setSearchKeyword] = useState(''); const [searchQuery, setSearchQuery] = useState(''); + const universityListRef = useRef(null); + const selectedRegion = getRegionParam(searchParams.get(REGION_PARAM)); + const targetSection = searchParams.get(SECTION_PARAM); const updateSearchQuery = useDebouncedCallback((value: string) => { setSearchQuery(value.trim()); @@ -37,7 +45,9 @@ 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) => { const value = event.target.value; @@ -45,6 +55,28 @@ function Home() { 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 (
@@ -87,21 +119,27 @@ function Home() {
-
-
- - -
-
+ {hasRecentClubs ? ( +
+
+ + +
+
+ ) : null} -
+
@@ -110,15 +148,15 @@ function Home() { return ( @@ -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; diff --git a/apps/web/src/pages/UniversityClubList/index.tsx b/apps/web/src/pages/UniversityClubList/index.tsx index cb12ca5..5e5cc95 100644 --- a/apps/web/src/pages/UniversityClubList/index.tsx +++ b/apps/web/src/pages/UniversityClubList/index.tsx @@ -5,14 +5,17 @@ import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; import { Link, Navigate, useParams, useSearchParams } from 'react-router-dom'; import { isClubCategory, type ClubCategory } from '@/apis/common/club'; +import type { Region } from '@/apis/home/entity'; import type { UniversityClub, UniversityClubListRequestParams } from '@/apis/universityClub/entity'; import { universityClubQueries } from '@/apis/universityClub/queries'; import SearchIcon from '@/assets/svg/search-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'; const PAGE_LIMIT = 12; +const HOME_UNIVERSITY_SECTION = 'universities'; function UniversityClubList() { const { universityId } = useParams(); @@ -32,6 +35,8 @@ function UniversityClubListContent({ universityId }: { universityId: number }) { const [searchKeyword, setSearchKeyword] = useState(query); const observerRef = useRef(null); + useResetScroll(universityId); + const updateSearchQuery = useDebouncedCallback((value: string) => { updateListSearchParams(setSearchParams, { query: value.trim() }); }); @@ -49,7 +54,7 @@ function UniversityClubListContent({ universityId }: { universityId: number }) { const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useSuspenseInfiniteQuery( universityClubQueries.infiniteList(universityId, requestParams) ); - const [{ university, totalCount, categories }] = data.pages; + const [{ university, categories }] = data.pages; const clubs = data.pages.flatMap((pageData) => pageData.clubs); const handleSearchKeywordChange = (event: ChangeEvent) => { @@ -88,10 +93,16 @@ function UniversityClubListContent({ universityId }: { universityId: number }) { return (
- +
- +
@@ -106,29 +117,27 @@ function UniversityClubListContent({ universityId }: { universityId: number }) { -
-
+
+ handleCategoryChange()} + /> + {categories.map((category) => ( handleCategoryChange()} + key={category.category} + label={category.categoryName} + count={category.count} + isSelected={category.category === selectedCategory} + onClick={() => handleCategoryChange(category.category)} /> - {categories.map((category) => ( - handleCategoryChange(category.category)} - /> - ))} -
+ ))}
-

+

선택한 대학의 동아리를 확인해보세요. 관심 있는 동아리의 소개를 살펴볼 수 있어요.

@@ -224,6 +233,16 @@ function getCategoryParam(value: string | null): ClubCategory | undefined { return undefined; } +function getHomeUniversitySectionPath(region: Region) { + const params = new URLSearchParams({ section: HOME_UNIVERSITY_SECTION }); + + if (region !== 'UNKNOWN') { + params.set('region', region); + } + + return `/?${params.toString()}`; +} + function updateListSearchParams( setSearchParams: ReturnType[1], next: { query?: string; category?: ClubCategory } diff --git a/apps/web/src/utils/hooks/useResetScroll.ts b/apps/web/src/utils/hooks/useResetScroll.ts new file mode 100644 index 0000000..304d1b3 --- /dev/null +++ b/apps/web/src/utils/hooks/useResetScroll.ts @@ -0,0 +1,9 @@ +import { useLayoutEffect } from 'react'; + +export function useResetScroll(resetKey?: unknown) { + useLayoutEffect(() => { + window.scrollTo({ top: 0, left: 0 }); + }, [resetKey]); +} + +export default useResetScroll;