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
45 changes: 24 additions & 21 deletions apps/web/src/app/search/PopularSearchHistory.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { ChartNoAxesCombined } from 'lucide-react';
import { ChartNoAxesCombined, Loader2 } from 'lucide-react';

import { useSearchLogQuery } from '@/queries/searchLogQuery';

Expand All @@ -9,33 +9,36 @@ interface PopularSearchHistoryProps {
}

export default function PopularSearchHistory({ onHistoryClick }: PopularSearchHistoryProps) {
const { data: searchLogs } = useSearchLogQuery();

if (!searchLogs || searchLogs.length === 0) return null;
const { data: searchLogs, isPending } = useSearchLogQuery();

return (
<div>
<div className="h-30 overflow-hidden">
<div className="flex items-center gap-2">
<ChartNoAxesCombined className="h-4 w-4" />
<p className="m-2">인기 검색어</p>
</div>
<div className="flex flex-wrap gap-2 pb-4">
{searchLogs.slice(0, 10).map((log, index) => (
<div
key={`${log.text}-${index}`}
className="bg-background flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm"
>
<span className="text-xs">{index + 1}</span>
<span
className="hover:text-primary max-w-30 cursor-pointer truncate text-left"
onClick={() => onHistoryClick(log.text)}
{isPending ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-5 w-5 animate-spin" />
</div>
) : (
<div className="flex flex-wrap gap-2 pb-4">
{searchLogs?.slice(0, 10).map((log, index) => (
<div
key={`${log.text}-${index}`}
className="bg-background flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm"
>
{log.text}
</span>
<span className="text-muted-foreground text-xs">{log.count}</span>
</div>
))}
</div>
<span
className="hover:text-primary max-w-30 cursor-pointer truncate text-left"
onClick={() => onHistoryClick(log.text)}
>
{log.text}
</span>
<span className="text-muted-foreground text-xs">{log.count}</span>
</div>
))}
</div>
)}
</div>
);
}
60 changes: 36 additions & 24 deletions apps/web/src/app/search/SearchHistory.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Clock, X } from 'lucide-react';
import { Clock, Loader2, X } from 'lucide-react';
import { useEffect, useState } from 'react';

import useSearchHistoryStore from '@/stores/useSearchHistoryStore';

Expand All @@ -8,37 +9,48 @@ interface SearchHistoryProps {

export default function SearchHistory({ onHistoryClick }: SearchHistoryProps) {
const { searchHistory, removeFromHistory } = useSearchHistoryStore();
const [isHydrated, setIsHydrated] = useState(false);

if (searchHistory.length === 0) return null;
useEffect(() => {
if (searchHistory.length > 0) {
setIsHydrated(true);
}
}, [searchHistory]);
Comment on lines +14 to +18
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. ishydrated tied to history 📎 Requirement gap ☼ Reliability

SearchHistory sets isHydrated to true only when searchHistory.length > 0, so users with no
stored history will see the loading spinner forever even after Zustand persist hydration completes.
This makes the hydration transition incorrect/unclear and violates the requirement to show the
spinner only until hydration finishes.
Agent Prompt
## Issue description
`SearchHistory` shows a loading spinner until `isHydrated` becomes true, but `isHydrated` is currently set based on `searchHistory.length > 0` instead of Zustand persist hydration completion. If the persisted history is empty, hydration can finish while the spinner never disappears.

## Issue Context
`useSearchHistoryStore` uses `zustand/middleware` `persist`, so hydration completion is independent of whether the hydrated `searchHistory` array has items.

## Fix Focus Areas
- apps/web/src/app/search/SearchHistory.tsx[14-33]
- apps/web/src/stores/useSearchHistoryStore.ts[18-44]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


return (
<div>
<div className="h-30 overflow-hidden">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4" />
<p className="m-2">최근 검색어</p>
</div>
<div className="flex flex-wrap gap-2 pb-4">
{searchHistory.slice(10).map((term, index) => (
<div
key={`${term}-${index}`}
className="bg-background flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm"
>
<span
className="hover:text-primary max-w-30 cursor-pointer truncate text-left"
onClick={() => onHistoryClick(term)}
{!isHydrated ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-5 w-5 animate-spin" />
</div>
) : (
Comment on lines +12 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. ishydrated tied to history length 📎 Requirement gap ≡ Correctness

SearchHistory sets isHydrated to true only when searchHistory.length > 0, so if the persisted
store hydrates to an empty array the component can remain stuck showing the spinner forever. This
fails the requirement to transition from the pre-hydration spinner to the hydrated UI once Zustand
persist hydration completes.
Agent Prompt
## Issue description
`SearchHistory` determines hydration via `searchHistory.length > 0`, which can leave the UI stuck on the loading spinner when hydration completes with an empty list.

## Issue Context
Zustand `persist` provides hydration lifecycle helpers (e.g., `useStore.persist.hasHydrated()` / `useStore.persist.onFinishHydration(...)`) that should be used to track hydration completion independently of the stored data length.

## Fix Focus Areas
- apps/web/src/app/search/SearchHistory.tsx[12-30]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

<div className="flex flex-wrap gap-2 pb-4">
{searchHistory.slice(0, 10).map((term, index) => (
<div
key={`${term}-${index}`}
className="bg-background flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm"
>
{term}
</span>
<span
className="hover:text-destructive cursor-pointer"
onClick={() => removeFromHistory(term)}
title="검색 기록 삭제"
>
<X className="h-4 w-4" />
</span>
</div>
))}
</div>
<span
className="hover:text-primary max-w-30 cursor-pointer truncate text-left"
onClick={() => onHistoryClick(term)}
>
{term}
</span>
<span
className="hover:text-destructive cursor-pointer"
onClick={() => removeFromHistory(term)}
title="검색 기록 삭제"
>
<X className="h-4 w-4" />
</span>
</div>
))}
</div>
)}
</div>
);
}