Skip to content
Open
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: 1 addition & 1 deletion apps/web/src/app/(home)/_ui/UniversityList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const UniversityList = ({ allRegionsUniversityList }: UniversityListProps) => {
background: "white",
}}
/>
<UniversityCards colleges={previewUniversities} showCapacity={false} enableVirtualization={false} />
<UniversityCards colleges={previewUniversities} showCapacity={false} />
</div>
);
};
Expand Down
10 changes: 6 additions & 4 deletions apps/web/src/app/university/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { type SubmitHandler, useForm } from "react-hook-form";
import { z } from "zod";

Expand All @@ -27,6 +27,7 @@ interface SearchBarProps {
// --- 폼 로직을 관리하는 부모 컴포넌트 ---
const SearchForm = ({ initText }: SearchBarProps) => {
const router = useRouter();
const pathname = usePathname();

const {
register,
Expand All @@ -48,12 +49,13 @@ const SearchForm = ({ initText }: SearchBarProps) => {

const queryString = queryParams.toString();

router.push(`/university?${queryString}`);
// 현재 경로에서 쿼리 파라미터만 업데이트
router.push(`${pathname}?${queryString}`);
};

return (
<form onSubmit={handleSubmit(onSubmit)} className="w-full">
<div className="relative mb-2">
<form onSubmit={handleSubmit(onSubmit)} className="sticky top-14 z-10 w-full bg-white pb-2">
<div className="relative">
<input
type="text"
placeholder={"대학명을 검색해보세요..."}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const SearchResultsContentInner = ({ homeUniversityName }: SearchResultsContentI
}, [searchParams]);

return (
<div className="pt-4">
<div>
<SearchBar initText={searchText || undefined} />

{/* 지역 필터 */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@ const UniversityListPage = async ({ params }: PageProps) => {
return (
<>
<TopDetailNavigation title={`${universityName} 파견학교`} />
<div className="mt-14 w-full px-5">
<SearchResultsContent homeUniversityName={universityName} />
</div>
<SearchResultsContent homeUniversityName={universityName} />
</>
);
};
Expand Down
9 changes: 3 additions & 6 deletions apps/web/src/app/university/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,9 @@ const UniversityOnboardingPage = () => {
return (
<>
<TopDetailNavigation title="대학교 선택" />
<div className="mt-14 w-full px-5 py-6">
<h1 className="mb-2 text-k-800 typo-bold-1">파견 대학교를 선택해주세요</h1>
<p className="mb-6 text-k-500 typo-medium-4">
소속 대학교를 선택하면 해당 대학교의 교환학생 파견 정보를 확인할 수 있습니다.
</p>

<div className="mt-14 w-full px-5">
<h1 className="mb-2 text-k-800 typo-bold-1">출신 학교 정보를 선택해주세요</h1>
<p className="mb-6 text-k-500 typo-medium-4">해당 학교에서 제공되는 교환학생 파견 정보를 확인할 수 있습니다.</p>
<div className="flex flex-col gap-2.5">
{HOME_UNIVERSITIES.map((university) => (
<Link key={university.slug} href={`/university/list/${university.slug}`} className="block">
Expand Down
70 changes: 7 additions & 63 deletions apps/web/src/components/university/UniversityCards/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
"use client";

import { useVirtualizer } from "@tanstack/react-virtual";

import clsx from "clsx";
import { useRef } from "react";

import type { ListUniversity } from "@/types/university";
import UniversityCard from "../../ui/UniverSityCard";
Expand All @@ -13,69 +10,16 @@ type UniversityCardsProps = {
style?: React.CSSProperties;
className?: string;
showCapacity?: boolean;
enableVirtualization?: boolean;
};

const ITEM_HEIGHT = 101;

const UniversityCards = ({
colleges,
style,
className,
showCapacity = true,
enableVirtualization = true,
}: UniversityCardsProps) => {
// 훅은 항상 컴포넌트 상단에서 호출해야 함 (React Hooks 규칙)
const parentRef = useRef<HTMLDivElement>(null);

const virtualizer = useVirtualizer({
count: colleges.length,
getScrollElement: () => parentRef.current,
estimateSize: () => ITEM_HEIGHT,
overscan: 5,
});

// 가상화가 비활성화된 경우 일반 렌더링
if (!enableVirtualization) {
return (
<div className={clsx("flex flex-col gap-2.5", className)} style={style}>
{colleges.map((college) => (
<div key={college.id} className="pb-2.5">
<UniversityCard university={college} showCapacity={showCapacity} />
</div>
))}
</div>
);
}

// 가상화 사용
const UniversityCards = ({ colleges, style, className, showCapacity = true }: UniversityCardsProps) => {
return (
<div ref={parentRef} className={clsx("h-[calc(100vh-200px)] overflow-auto", className)} style={style}>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: "100%",
position: "relative",
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={colleges[virtualItem.index].id}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<div className="pb-2.5">
<UniversityCard university={colleges[virtualItem.index]} showCapacity={showCapacity} />
</div>
</div>
))}
</div>
<div className={clsx("flex flex-col gap-2.5", className)} style={style}>
{colleges.map((college) => (
<div key={college.id} className="pb-2.5">
<UniversityCard university={college} showCapacity={showCapacity} />
</div>
))}
</div>
);
};
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/lib/zustand/useAuthStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ const useAuthStore = create<AuthState>()(
accessToken: state.accessToken,
isAuthenticated: state.isAuthenticated,
}),
onRehydrateStorage: () => (state) => {
// hydration 완료 후 isInitialized를 true로 설정
if (state) {
state.isInitialized = true;
}
},
Comment on lines +66 to +71
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/web/src/lib/zustand/useAuthStore.ts

Repository: solid-connection/solid-connect-web

Length of output: 2452


🏁 Script executed:

# Check if there are any other stores or patterns in the codebase using onRehydrateStorage
rg "onRehydrateStorage" --type ts --type tsx -A 3

Repository: solid-connection/solid-connect-web

Length of output: 105


🏁 Script executed:

# Check if there are any other stores or patterns in the codebase using onRehydrateStorage
rg "onRehydrateStorage" -A 3

Repository: solid-connection/solid-connect-web

Length of output: 395


🏁 Script executed:

# Check zustand persist middleware usage patterns in the codebase
rg "persist\(" -B 2 -A 5

Repository: solid-connection/solid-connect-web

Length of output: 582


hydration 이후 직접 변이 대신 api.setState()를 사용하여 구독자에게 알려주세요.

현재 구현에서 onRehydrateStorage 콜백이 상태를 직접 변이시키면 zustand의 구독자들이 변경을 감지하지 못할 수 있어, UI에 isInitialized 업데이트가 반영되지 않을 수 있습니다. api.setState()를 통해 명시적으로 상태를 업데이트하면 모든 구독자에게 제대로 알림이 전달됩니다.

수정 사항:

  1. persist 콜백에 api 파라미터 추가

    • 세 번째 파라미터로 api를 받아서 onRehydrateStorage 클로저에서 접근할 수 있게 합니다.
  2. onRehydrateStorage에서 api.setState() 호출

    • 직접 state.isInitialized = true 대신 api.setState({ isInitialized: true })로 변경합니다.
✅ 수정 제안
-  persist(
-    (set) => ({
+  persist(
+    (set, _get, api) => ({
       accessToken: null,
       isAuthenticated: false,
       isLoading: false,
       isInitialized: false,
       refreshStatus: "idle",
       setAccessToken: (token) => {
         set({
           accessToken: token,
           isAuthenticated: true,
           isLoading: false,
           isInitialized: true,
           refreshStatus: "success",
         });
       },
       clearAccessToken: () => {
         set({
           accessToken: null,
           isAuthenticated: false,
           isLoading: false,
           isInitialized: true,
           refreshStatus: "idle",
         });
       },
       setLoading: (loading) => {
         set({ isLoading: loading });
       },
       setInitialized: (initialized) => {
         set({ isInitialized: initialized });
       },
       setRefreshStatus: (status) => {
         set({ refreshStatus: status });
       },
     }),
     {
       name: "auth-storage",
       partialize: (state) => ({
         accessToken: state.accessToken,
         isAuthenticated: state.isAuthenticated,
       }),
-      onRehydrateStorage: () => (state) => {
-        // hydration 완료 후 isInitialized를 true로 설정
-        if (state) {
-          state.isInitialized = true;
-        }
-      },
+      onRehydrateStorage: () => () => {
+        // hydration 완료 후 isInitialized를 true로 설정
+        api.setState({ isInitialized: true });
+      },
     },
   ),
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onRehydrateStorage: () => (state) => {
// hydration 완료 후 isInitialized를 true로 설정
if (state) {
state.isInitialized = true;
}
},
persist(
(set, _get, api) => ({
accessToken: null,
isAuthenticated: false,
isLoading: false,
isInitialized: false,
refreshStatus: "idle",
setAccessToken: (token) => {
set({
accessToken: token,
isAuthenticated: true,
isLoading: false,
isInitialized: true,
refreshStatus: "success",
});
},
clearAccessToken: () => {
set({
accessToken: null,
isAuthenticated: false,
isLoading: false,
isInitialized: true,
refreshStatus: "idle",
});
},
setLoading: (loading) => {
set({ isLoading: loading });
},
setInitialized: (initialized) => {
set({ isInitialized: initialized });
},
setRefreshStatus: (status) => {
set({ refreshStatus: status });
},
}),
{
name: "auth-storage",
partialize: (state) => ({
accessToken: state.accessToken,
isAuthenticated: state.isAuthenticated,
}),
onRehydrateStorage: () => () => {
// hydration 완료 후 isInitialized를 true로 설정
api.setState({ isInitialized: true });
},
},
),
🤖 Prompt for AI Agents
In `@apps/web/src/lib/zustand/useAuthStore.ts` around lines 66 - 71, The
onRehydrateStorage callback in your persist config mutates state directly
(state.isInitialized = true) which can bypass zustand subscribers; modify the
persist usage to accept the third parameter api (persist((set, get, api) =>
...)) and inside onRehydrateStorage call api.setState({ isInitialized: true })
instead of direct mutation so all subscribers are notified; update the persist
closure signature and replace the direct assignment in onRehydrateStorage
accordingly (referencing onRehydrateStorage and api.setState in
useAuthStore.ts).

},
),
);
Expand Down