Repository: Moadong/moadong
Pull request #326: [refactor] 검색 구조 변경 및 스크롤훅 리팩토링
Author: login: seongwon030
Labels: name: 🔨 Refactor
description: 코드 리팩토링
name: 💻 FE
description: Frontend
name: 🛠Fix
description: 기능이 의도한 대로 동작하지 않는 버그를 수정
Created: 2025-04-15T08:05:35Z | Merged: 2025-04-27T11:18:36Z

## #️⃣연관된 이슈

> ex) #325 

## 📝작업 내용
## 기존의 검색어 관리
### keyword를 전역으로 관리
```typescript
  const { keyword, setKeyword } = useSearch();
```
```useSearch```는 검색어를 전역으로 관리하는 context api인데요, 이것을 ```onChange```로 전달하고 있었기에
입력할 때마다 검색어로 필터링되었습니다. 

이는 성능 상의 문제도 있고, ui 적으로 자주 바뀌는 불편함을 주어 검색어를 ```SearchBox.tsx```에서만 관리하고
그것을 전역적으로 뿌려주는 방법으로 바꾸었습니다. 

```typescript
  const [inputValue, setInputValue] = useState('');
  const { setKeyword } = useSearch();
```
```keyword```는 없애고, 지역적인 ```inputValue```상태를 추가했습니다. [e7e26f5](https://github.com/Moadong/moadong/pull/326/commits/e7e26f50e03bf7f18aae9c598e34d6894c35de7b) <hr/>

**@변경사항 (4.25)** 카테고리 버튼 클릭 시 기존 ```inputValue```초기화가 되어야 한다고 생각했습니다. 
사용자가 카테고리 버튼을 클릭하는 의도는 기존 검색어를 더이상 보지 않고 다른 동아리를 더 보고 싶다는 의미가 담겨 있을 것입니다.
그래서 ```setInputValue```를 ```categoryButtonList```에서도 쓸 수 있도록 ```SearchContext```에 전역으로 관리하도록 변경했습니다. 5f86d6823f8124a624fd7d4005a1dc0b26c5d9d6

홈 버튼 클릭 시에도 검색어가 초기화되도록 했습니다. 7d50296c614317052bfcfbf67616fbdf8538a224

https://github.com/user-attachments/assets/3685f694-0465-468c-a92e-8e7c315c9d5d


## 검색방식 
### 1. form태그 안에서 enter키를 누르면 검색이 된다.
```typescript
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    handleSearch();
  };
```
- submit 시 새고고침을 없애기 위해 ```e.preventDefault();```를 추가했습니다.
### 2. button 클릭 시 검색이 된다. (기존 방식)
### 3. 공백이 들어간 경우에는 분류 기준을 없애고 동아리 전체가 나온다.
- 기존 ```빈 칸 일 때 return```조건을 없애 의도대로 동작합니다. [cf833f8](https://github.com/Moadong/moadong/pull/326/commits/cf833f8a16728a196a954d1e05c318b1a1a41fbc)

### 시선 이동 최소화 [0fcad35](https://github.com/Moadong/moadong/pull/326/commits/0fcad35f90ce84279d6e34a305c2400a62c70c7a)
```redirectToMainIfSearchTriggeredOutside```함수를 위로 올려 top-down으로 읽히도록 했습니다.
## 중점적으로 리뷰받고 싶은 부분(선택)
> 리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요
>
> ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?


## 논의하고 싶은 부분(선택)
현재 검색어가 있을 시에 분과태그를 선택하면 필터링이 안 됩니다. 
이렇게 동작해도 괜찮을까요? 

## 🫡 참고사항

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **문서화**
  - 프론트엔드 코드 품질, 가독성, 예측 가능성, 응집도, 결합도 향상을 위한 상세 가이드라인 문서가 추가되었습니다.

- **신규 기능**
  - 검색 입력값과 실제 검색어를 분리하여 입력값 상태를 별도로 관리하도록 개선되었습니다.
  - 검색창에서 Enter 키로 검색이 가능해졌으며, 검색 후 입력창 포커스가 해제됩니다.

- **버그 수정**
  - 카테고리 버튼, 홈 버튼 클릭 시 기존 검색어 및 입력값이 초기화되어 이전 검색 상태가 남지 않도록 개선되었습니다.

- **리팩터**
  - 자동 스크롤 훅이 섹션 개수와 오프셋을 동적으로 받을 수 있도록 개선되었습니다.
  - 불필요한 디버깅 콘솔 로그가 제거되었습니다.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/frontend/.cursorrules b/frontend/.cursorrules
new file mode 100644
index 000000000..2e92f8e9b
--- /dev/null
+++ b/frontend/.cursorrules
@@ -0,0 +1,687 @@
+# Frontend Design Guideline
+
+This document summarizes key frontend design principles and rules, showcasing
+recommended patterns. Follow these guidelines when writing frontend code.
+
+# Readability
+
+Improving the clarity and ease of understanding code.
+
+## Naming Magic Numbers
+
+**Rule:** Replace magic numbers with named constants for clarity.
+
+**Reasoning:**
+
+- Improves clarity by giving semantic meaning to unexplained values.
+- Enhances maintainability.
+
+#### Recommended Pattern:
+
+```typescript
+const ANIMATION_DELAY_MS = 300;
+
+async function onLikeClick() {
+  await postLike(url);
+  await delay(ANIMATION_DELAY_MS); // Clearly indicates waiting for animation
+  await refetchPostLike();
+}
+```
+
+## Abstracting Implementation Details
+
+**Rule:** Abstract complex logic/interactions into dedicated components/HOCs.
+
+**Reasoning:**
+
+- Reduces cognitive load by separating concerns.
+- Improves readability, testability, and maintainability of components.
+
+#### Recommended Pattern 1: Auth Guard
+
+(Login check abstracted to a wrapper/guard component)
+
+```tsx
+// App structure
+function App() {
+  return (
+    <AuthGuard>
+      {' '}
+      {/* Wrapper handles auth check */}
+      <LoginStartPage />
+    </AuthGuard>
+  );
+}
+
+// AuthGuard component encapsulates the check/redirect logic
+function AuthGuard({ children }) {
+  const status = useCheckLoginStatus();
+  useEffect(() => {
+    if (status === 'LOGGED_IN') {
+      location.href = '/home';
+    }
+  }, [status]);
+
+  // Render children only if not logged in, otherwise render null (or loading)
+  return status !== 'LOGGED_IN' ? children : null;
+}
+
+// LoginStartPage is now simpler, focused only on login UI/logic
+function LoginStartPage() {
+  // ... login related logic ONLY ...
+  return <>{/* ... login related components ... */}</>;
+}
+```
+
+#### Recommended Pattern 2: Dedicated Interaction Component
+
+(Dialog logic abstracted into a dedicated `InviteButton` component)
+
+```tsx
+export function FriendInvitation() {
+  const { data } = useQuery(/* ... */);
+
+  return (
+    <>
+      {/* Use the dedicated button component */}
+      <InviteButton name={data.name} />
+      {/* ... other UI ... */}
+    </>
+  );
+}
+
+// InviteButton handles the confirmation flow internally
+function InviteButton({ name }) {
+  const handleClick = async () => {
+    const canInvite = await overlay.openAsync(({ isOpen, close }) => (
+      <ConfirmDialog
+        title={`Share with ${name}`}
+        // ... dialog setup ...
+      />
+    ));
+
+    if (canInvite) {
+      await sendPush();
+    }
+  };
+
+  return <Button onClick={handleClick}>Invite</Button>;
+}
+```
+
+## Separating Code Paths for Conditional Rendering
+
+**Rule:** Separate significantly different conditional UI/logic into distinct
+components.
+
+**Reasoning:**
+
+- Improves readability by avoiding complex conditionals within one component.
+- Ensures each specialized component has a clear, single responsibility.
+
+#### Recommended Pattern:
+
+(Separate components for each role)
+
+```tsx
+function SubmitButton() {
+  const isViewer = useRole() === 'viewer';
+
+  // Delegate rendering to specialized components
+  return isViewer ? <ViewerSubmitButton /> : <AdminSubmitButton />;
+}
+
+// Component specifically for the 'viewer' role
+function ViewerSubmitButton() {
+  return <TextButton disabled>Submit</TextButton>;
+}
+
+// Component specifically for the 'admin' (or non-viewer) role
+function AdminSubmitButton() {
+  useEffect(() => {
+    showAnimation(); // Animation logic isolated here
+  }, []);
+
+  return <Button type='submit'>Submit</Button>;
+}
+```
+
+## Simplifying Complex Ternary Operators
+
+**Rule:** Replace complex/nested ternaries with `if`/`else` or IIFEs for
+readability.
+
+**Reasoning:**
+
+- Makes conditional logic easier to follow quickly.
+- Improves overall code maintainability.
+
+#### Recommended Pattern:
+
+(Using an IIFE with `if` statements)
+
+```typescript
+const status = (() => {
+  if (ACondition && BCondition) return 'BOTH';
+  if (ACondition) return 'A';
+  if (BCondition) return 'B';
+  return 'NONE';
+})();
+```
+
+## Reducing Eye Movement (Colocating Simple Logic)
+
+**Rule:** Colocate simple, localized logic or use inline definitions to reduce
+context switching.
+
+**Reasoning:**
+
+- Allows top-to-bottom reading and faster comprehension.
+- Reduces cognitive load from context switching (eye movement).
+
+#### Recommended Pattern A: Inline `switch`
+
+```tsx
+function Page() {
+  const user = useUser();
+
+  // Logic is directly visible here
+  switch (user.role) {
+    case 'admin':
+      return (
+        <div>
+          <Button disabled={false}>Invite</Button>
+          <Button disabled={false}>View</Button>
+        </div>
+      );
+    case 'viewer':
+      return (
+        <div>
+          <Button disabled={true}>Invite</Button> {/* Example for viewer */}
+          <Button disabled={false}>View</Button>
+        </div>
+      );
+    default:
+      return null;
+  }
+}
+```
+
+#### Recommended Pattern B: Colocated simple policy object
+
+```tsx
+function Page() {
+  const user = useUser();
+  // Simple policy defined right here, easy to see
+  const policy = {
+    admin: { canInvite: true, canView: true },
+    viewer: { canInvite: false, canView: true },
+  }[user.role];
+
+  // Ensure policy exists before accessing properties if role might not match
+  if (!policy) return null;
+
+  return (
+    <div>
+      <Button disabled={!policy.canInvite}>Invite</Button>
+      <Button disabled={!policy.canView}>View</Button>
+    </div>
+  );
+}
+```
+
+## Naming Complex Conditions
+
+**Rule:** Assign complex boolean conditions to named variables.
+
+**Reasoning:**
+
+- Makes the _meaning_ of the condition explicit.
+- Improves readability and self-documentation by reducing cognitive load.
+
+#### Recommended Pattern:
+
+(Conditions assigned to named variables)
+
+```typescript
+const matchedProducts = products.filter((product) => {
+  // Check if product belongs to the target category
+  const isSameCategory = product.categories.some(
+    (category) => category.id === targetCategory.id,
+  );
+
+  // Check if any product price falls within the desired range
+  const isPriceInRange = product.prices.some(
+    (price) => price >= minPrice && price <= maxPrice,
+  );
+
+  // The overall condition is now much clearer
+  return isSameCategory && isPriceInRange;
+});
+```
+
+**Guidance:** Name conditions when the logic is complex, reused, or needs unit
+testing. Avoid naming very simple, single-use conditions.
+
+# Predictability
+
+Ensuring code behaves as expected based on its name, parameters, and context.
+
+## Standardizing Return Types
+
+**Rule:** Use consistent return types for similar functions/hooks.
+
+**Reasoning:**
+
+- Improves code predictability; developers can anticipate return value shapes.
+- Reduces confusion and potential errors from inconsistent types.
+
+#### Recommended Pattern 1: API Hooks (React Query)
+
+```typescript
+// Always return the Query object
+import { useQuery, UseQueryResult } from '@tanstack/react-query';
+
+// Assuming fetchUser returns Promise<UserType>
+function useUser(): UseQueryResult<UserType, Error> {
+  const query = useQuery({ queryKey: ['user'], queryFn: fetchUser });
+  return query;
+}
+
+// Assuming fetchServerTime returns Promise<Date>
+function useServerTime(): UseQueryResult<Date, Error> {
+  const query = useQuery({
+    queryKey: ['serverTime'],
+    queryFn: fetchServerTime,
+  });
+  return query;
+}
+```
+
+#### Recommended Pattern 2: Validation Functions
+
+(Using a consistent type, ideally a Discriminated Union)
+
+```typescript
+type ValidationResult = { ok: true } | { ok: false; reason: string };
+
+function checkIsNameValid(name: string): ValidationResult {
+  if (name.length === 0) return { ok: false, reason: 'Name cannot be empty.' };
+  if (name.length >= 20)
+    return { ok: false, reason: 'Name cannot be longer than 20 characters.' };
+  return { ok: true };
+}
+
+function checkIsAgeValid(age: number): ValidationResult {
+  if (!Number.isInteger(age))
+    return { ok: false, reason: 'Age must be an integer.' };
+  if (age < 18) return { ok: false, reason: 'Age must be 18 or older.' };
+  if (age > 99) return { ok: false, reason: 'Age must be 99 or younger.' };
+  return { ok: true };
+}
+
+// Usage allows safe access to 'reason' only when ok is false
+const nameValidation = checkIsNameValid(name);
+if (!nameValidation.ok) {
+  console.error(nameValidation.reason);
+}
+```
+
+## Revealing Hidden Logic (Single Responsibility)
+
+**Rule:** Avoid hidden side effects; functions should only perform actions
+implied by their signature (SRP).
+
+**Reasoning:**
+
+- Leads to predictable behavior without unintended side effects.
+- Creates more robust, testable code through separation of concerns (SRP).
+
+#### Recommended Pattern:
+
+```typescript
+// Function *only* fetches balance
+async function fetchBalance(): Promise<number> {
+  const balance = await http.get<number>('...');
+  return balance;
+}
+
+// Caller explicitly performs logging where needed
+async function handleUpdateClick() {
+  const balance = await fetchBalance(); // Fetch
+  logging.log('balance_fetched'); // Log (explicit action)
+  await syncBalance(balance); // Another action
+}
+```
+
+## Using Unique and Descriptive Names (Avoiding Ambiguity)
+
+**Rule:** Use unique, descriptive names for custom wrappers/functions to avoid
+ambiguity.
+
+**Reasoning:**
+
+- Avoids ambiguity and enhances predictability.
+- Allows developers to understand specific actions (e.g., adding auth) directly
+  from the name.
+
+#### Recommended Pattern:
+
+```typescript
+// In httpService.ts - Clearer module name
+import { http as httpLibrary } from '@some-library/http';
+
+export const httpService = {
+  // Unique module name
+  async getWithAuth(url: string) {
+    // Descriptive function name
+    const token = await fetchToken();
+    return httpLibrary.get(url, {
+      headers: { Authorization: `Bearer ${token}` },
+    });
+  },
+};
+
+// In fetchUser.ts - Usage clearly indicates auth
+import { httpService } from './httpService';
+export async function fetchUser() {
+  // Name 'getWithAuth' makes the behavior explicit
+  return await httpService.getWithAuth('...');
+}
+```
+
+# Cohesion
+
+Keeping related code together and ensuring modules have a well-defined, single
+purpose.
+
+## Considering Form Cohesion
+
+**Rule:** Choose field-level or form-level cohesion based on form requirements.
+
+**Reasoning:**
+
+- Balances field independence (field-level) vs. form unity (form-level).
+- Ensures related form logic is appropriately grouped based on requirements.
+
+#### Recommended Pattern (Field-Level Example):
+
+```tsx
+// Each field uses its own `validate` function
+import { useForm } from 'react-hook-form';
+
+export function Form() {
+  const {
+    register,
+    formState: { errors },
+    handleSubmit,
+  } = useForm({
+    /* defaultValues etc. */
+  });
+
+  const onSubmit = handleSubmit((formData) => {
+    console.log('Form submitted:', formData);
+  });
+
+  return (
+    <form onSubmit={onSubmit}>
+      <div>
+        <input
+          {...register('name', {
+            validate: (value) =>
+              value.trim() === '' ? 'Please enter your name.' : true, // Example validation
+          })}
+          placeholder='Name'
+        />
+        {errors.name && <p>{errors.name.message}</p>}
+      </div>
+      <div>
+        <input
+          {...register('email', {
+            validate: (value) =>
+              /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)
+                ? true
+                : 'Invalid email address.', // Example validation
+          })}
+          placeholder='Email'
+        />
+        {errors.email && <p>{errors.email.message}</p>}
+      </div>
+      <button type='submit'>Submit</button>
+    </form>
+  );
+}
+```
+
+#### Recommended Pattern (Form-Level Example):
+
+```tsx
+// A single schema defines validation for the whole form
+import * as z from 'zod';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+
+const schema = z.object({
+  name: z.string().min(1, 'Please enter your name.'),
+  email: z.string().min(1, 'Please enter your email.').email('Invalid email.'),
+});
+
+export function Form() {
+  const {
+    register,
+    formState: { errors },
+    handleSubmit,
+  } = useForm({
+    resolver: zodResolver(schema),
+    defaultValues: { name: '', email: '' },
+  });
+
+  const onSubmit = handleSubmit((formData) => {
+    console.log('Form submitted:', formData);
+  });
+
+  return (
+    <form onSubmit={onSubmit}>
+      <div>
+        <input {...register('name')} placeholder='Name' />
+        {errors.name && <p>{errors.name.message}</p>}
+      </div>
+      <div>
+        <input {...register('email')} placeholder='Email' />
+        {errors.email && <p>{errors.email.message}</p>}
+      </div>
+      <button type='submit'>Submit</button>
+    </form>
+  );
+}
+```
+
+**Guidance:** Choose **field-level** for independent validation, async checks,
+or reusable fields. Choose **form-level** for related fields, wizard forms, or
+interdependent validation.
+
+## Organizing Code by Feature/Domain
+
+**Rule:** Organize directories by feature/domain, not just by code type.
+
+**Reasoning:**
+
+- Increases cohesion by keeping related files together.
+- Simplifies feature understanding, development, maintenance, and deletion.
+
+#### Recommended Pattern:
+
+(Organized by feature/domain)
+
+```
+src/
+├── components/ # Shared/common components
+├── hooks/      # Shared/common hooks
+├── utils/      # Shared/common utils
+├── domains/
+│   ├── user/
+│   │   ├── components/
+│   │   │   └── UserProfileCard.tsx
+│   │   ├── hooks/
+│   │   │   └── useUser.ts
+│   │   └── index.ts # Optional barrel file
+│   ├── product/
+│   │   ├── components/
+│   │   │   └── ProductList.tsx
+│   │   ├── hooks/
+│   │   │   └── useProducts.ts
+│   │   └── ...
+│   └── order/
+│       ├── components/
+│       │   └── OrderSummary.tsx
+│       ├── hooks/
+│       │   └── useOrder.ts
+│       └── ...
+└── App.tsx
+```
+
+## Relating Magic Numbers to Logic
+
+**Rule:** Define constants near related logic or ensure names link them clearly.
+
+**Reasoning:**
+
+- Improves cohesion by linking constants to the logic they represent.
+- Prevents silent failures caused by updating logic without updating related
+  constants.
+
+#### Recommended Pattern:
+
+```typescript
+// Constant clearly named and potentially defined near animation logic
+const ANIMATION_DELAY_MS = 300;
+
+async function onLikeClick() {
+  await postLike(url);
+  // Delay uses the constant, maintaining the link to the animation
+  await delay(ANIMATION_DELAY_MS);
+  await refetchPostLike();
+}
+```
+
+_Ensure constants are maintained alongside the logic they depend on or clearly
+named to show the relationship._
+
+# Coupling
+
+Minimizing dependencies between different parts of the codebase.
+
+## Balancing Abstraction and Coupling (Avoiding Premature Abstraction)
+
+**Rule:** Avoid premature abstraction of duplicates if use cases might diverge;
+prefer lower coupling.
+
+**Reasoning:**
+
+- Avoids tight coupling from forcing potentially diverging logic into one
+  abstraction.
+- Allowing some duplication can improve decoupling and maintainability when
+  future needs are uncertain.
+
+#### Guidance:
+
+Before abstracting, consider if the logic is truly identical and likely to
+_stay_ identical across all use cases. If divergence is possible (e.g.,
+different pages needing slightly different behavior from a shared hook like
+`useOpenMaintenanceBottomSheet`), keeping the logic separate initially (allowing
+duplication) can lead to more maintainable, decoupled code. Discuss trade-offs
+with the team. _[No specific 'good' code example here, as the recommendation is
+situational awareness rather than a single pattern]._
+
+## Scoping State Management (Avoiding Overly Broad Hooks)
+
+**Rule:** Break down broad state management into smaller, focused
+hooks/contexts.
+
+**Reasoning:**
+
+- Reduces coupling by ensuring components only depend on necessary state slices.
+- Improves performance by preventing unnecessary re-renders from unrelated state
+  changes.
+
+#### Recommended Pattern:
+
+(Focused hooks, low coupling)
+
+```typescript
+// Hook specifically for cardId query param
+import { useQueryParam, NumberParam } from 'use-query-params';
+import { useCallback } from 'react';
+
+export function useCardIdQueryParam() {
+  // Assuming 'query' provides the raw param value
+  const [cardIdParam, setCardIdParam] = useQueryParam('cardId', NumberParam);
+
+  const setCardId = useCallback(
+    (newCardId: number | undefined) => {
+      setCardIdParam(newCardId, 'replaceIn'); // Or 'push' depending on desired history behavior
+    },
+    [setCardIdParam],
+  );
+
+  // Provide a stable return tuple
+  return [cardIdParam ?? undefined, setCardId] as const;
+}
+
+// Separate hook for date range, etc.
+// export function useDateRangeQueryParam() { /* ... */ }
+```
+
+Components now only import and use `useCardIdQueryParam` if they need `cardId`,
+decoupling them from date range state, etc.
+
+## Eliminating Props Drilling with Composition
+
+**Rule:** Use Component Composition instead of Props Drilling.
+
+**Reasoning:**
+
+- Significantly reduces coupling by eliminating unnecessary intermediate
+  dependencies.
+- Makes refactoring easier and clarifies data flow in flatter component trees.
+
+#### Recommended Pattern:
+
+```tsx
+import React, { useState } from 'react';
+
+// Assume Modal, Input, Button, ItemEditList components exist
+
+function ItemEditModal({ open, items, recommendedItems, onConfirm, onClose }) {
+  const [keyword, setKeyword] = useState('');
+
+  // Render children directly within Modal, passing props only where needed
+  return (
+    <Modal open={open} onClose={onClose}>
+      {/* Input and Button rendered directly */}
+      <div
+        style={{
+          display: 'flex',
+          justifyContent: 'space-between',
+          marginBottom: '1rem',
+        }}>
+        <Input
+          value={keyword}
+          onChange={(e) => setKeyword(e.target.value)} // State managed here
+          placeholder='Search items...'
+        />
+        <Button onClick={onClose}>Close</Button>
+      </div>
+      {/* ItemEditList rendered directly, gets props it needs */}
+      <ItemEditList
+        keyword={keyword} // Passed directly
+        items={items} // Passed directly
+        recommendedItems={recommendedItems} // Passed directly
+        onConfirm={onConfirm} // Passed directly
+      />
+    </Modal>
+  );
+}
+
+// The intermediate ItemEditBody component is eliminated, reducing coupling.
+```
diff --git a/frontend/src/components/common/Header/Header.tsx b/frontend/src/components/common/Header/Header.tsx
index 3cfd69d53..7889bc05d 100644
--- a/frontend/src/components/common/Header/Header.tsx
+++ b/frontend/src/components/common/Header/Header.tsx
@@ -1,24 +1,28 @@
 import React from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
 import useMixpanelTrack from '@/hooks/useMixpanelTrack';
 import * as Styled from './Header.styles';
 import SearchBox from '@/components/common/SearchBox/SearchBox';
 import MainIcon from '@/assets/images/moadong_name_logo.svg';
-import { useNavigate, useLocation } from 'react-router-dom';
+import { useSearch } from '@/context/SearchContext';
 
 const Header = () => {
+  const { setKeyword, setInputValue } = useSearch();
   const navigate = useNavigate();
-  const location = useLocation(); // 현재 경로 가져오기
+  const location = useLocation();
   const trackEvent = useMixpanelTrack();
 
   const handleHomeClick = () => {
-    trackEvent('Home Button Clicked');
     navigate('/');
+    setKeyword('');
+    setInputValue('');
+    trackEvent('Home Button Clicked');
   };
 
   const handleIntroduceClick = () => {
-    trackEvent('Introduce Button Clicked');
     window.location.href =
       'https://valiant-schooner-12c.notion.site/1a64ac84bab3805287e0cef50b563370';
+    trackEvent('Introduce Button Clicked');
   };
 
   const isAdminPage = location.pathname.startsWith('/admin');
diff --git a/frontend/src/components/common/SearchBox/SearchBox.tsx b/frontend/src/components/common/SearchBox/SearchBox.tsx
index 77579ee6d..c1c5808b6 100644
--- a/frontend/src/components/common/SearchBox/SearchBox.tsx
+++ b/frontend/src/components/common/SearchBox/SearchBox.tsx
@@ -1,50 +1,58 @@
-import React, { useState } from 'react';
+import React, { useRef, useState } from 'react';
 import { useSearch } from '@/context/SearchContext';
 import useMixpanelTrack from '@/hooks/useMixpanelTrack';
 import * as Styled from './SearchBox.styles';
 import SearchIcon from '@/assets/images/icons/search_button_icon.svg';
-import { useNavigate } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router-dom';
 
 const SearchBox = () => {
   const [isSearchBoxClicked, setIsSearchBoxClicked] = useState(false);
-  const { keyword, setKeyword } = useSearch();
+  const { setKeyword, inputValue, setInputValue } = useSearch();
   const trackEvent = useMixpanelTrack();
   const navigate = useNavigate();
+  const location = useLocation();
 
-  const handleSearchClick = () => {
-    redirectToMainIfSearchTriggeredOutside();
+  const inputRef = useRef<HTMLInputElement>(null);
 
-    if (!keyword.trim()) {
-      return;
+  const redirectToHome = () => {
+    if (location.pathname !== '/') {
+      navigate('/');
     }
+  };
+
+  const handleSearch = () => {
+    redirectToHome();
+    setKeyword(inputValue);
+
+    inputRef.current?.blur();
 
     trackEvent('Search Executed', {
-      keyword,
+      inputValue: inputValue,
       page: window.location.pathname,
     });
-
-    setKeyword(keyword);
   };
 
-  const redirectToMainIfSearchTriggeredOutside = () => {
-    if (window.location.pathname !== '/') {
-      navigate('/');
-    }
+  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
+    e.preventDefault();
+    handleSearch();
   };
 
   return (
-    <Styled.SearchBoxContainer isFocused={isSearchBoxClicked}>
+    <Styled.SearchBoxContainer
+      isFocused={isSearchBoxClicked}
+      onSubmit={handleSubmit}>
       <Styled.SearchInputStyles
+        ref={inputRef}
+        type='text'
         placeholder='어떤 동아리를 찾으세요?'
-        value={keyword}
-        onChange={(e) => setKeyword(e.target.value)}
+        value={inputValue}
+        onChange={(e) => setInputValue(e.target.value)}
         onFocus={() => setIsSearchBoxClicked(true)}
         onBlur={() => setIsSearchBoxClicked(false)}
         aria-label='동아리 검색창'
       />
       <Styled.SearchButton
-        type='button'
-        onClick={handleSearchClick}
+        type='submit'
         isFocused={isSearchBoxClicked}
         aria-label='검색'>
         <img src={SearchIcon} alt='Search Button' />
diff --git a/frontend/src/context/SearchContext.tsx b/frontend/src/context/SearchContext.tsx
index 1e14cf943..e8431a2f9 100644
--- a/frontend/src/context/SearchContext.tsx
+++ b/frontend/src/context/SearchContext.tsx
@@ -3,6 +3,8 @@ import React, { createContext, useState, useContext, ReactNode } from 'react';
 interface SearchContextType {
   keyword: string;
   setKeyword: (keyword: string) => void;
+  inputValue: string;
+  setInputValue: (value: string) => void;
 }
 
 interface SearchProviderProps {
@@ -13,9 +15,16 @@ const SearchContext = createContext<SearchContextType | undefined>(undefined);
 
 export const SearchProvider = ({ children }: SearchProviderProps) => {
   const [keyword, setKeyword] = useState<string>('');
+  const [inputValue, setInputValue] = useState('');
 
   return (
-    <SearchContext.Provider value={{ keyword, setKeyword }}>
+    <SearchContext.Provider
+      value={{
+        keyword,
+        setKeyword,
+        inputValue,
+        setInputValue,
+      }}>
       {children}
     </SearchContext.Provider>
   );
diff --git a/frontend/src/hooks/InfoTabs/useAutoScroll.ts b/frontend/src/hooks/InfoTabs/useAutoScroll.ts
index 1889e9f9d..4e40bed6e 100644
--- a/frontend/src/hooks/InfoTabs/useAutoScroll.ts
+++ b/frontend/src/hooks/InfoTabs/useAutoScroll.ts
@@ -1,21 +1,46 @@
-import { useRef } from 'react';
+import { useRef, useCallback } from 'react';
 
-const useAutoScroll = () => {
+/**
+ * 섹션 자동 스크롤을 위한 커스텀 훅
+ * @param sectionCount - 섹션의 총 개수
+ * @param yOffset - 스크롤 시 상단 여백 (기본값: -110)
+ * @returns {Object} sectionRefs와 scrollToSection 함수를 포함한 객체
+ */
+const useAutoScroll = (sectionCount: number = 4, yOffset: number = -110) => {
   const sectionRefs = useRef<(HTMLDivElement | null)[]>(
-    new Array(4).fill(null),
+    new Array(sectionCount).fill(null),
   );
 
-  const scrollToSection = (index: number) => {
-    if (sectionRefs.current[index]) {
+  /**
+   * 지정된 인덱스의 섹션으로 스크롤
+   * @param index - 스크롤할 섹션의 인덱스
+   * @returns {boolean} 스크롤 성공 여부
+   */
+  const scrollToSection = useCallback(
+    (index: number): boolean => {
+      if (index < 0 || index >= sectionCount) {
+        return false;
+      }
+
       const element = sectionRefs.current[index];
-      const yOffset = -110;
 
-      window.scrollTo({
-        top: element.getBoundingClientRect().top + window.scrollY + yOffset,
-        behavior: 'smooth',
-      });
-    }
-  };
+      if (!element) {
+        return false;
+      }
+
+      try {
+        window.scrollTo({
+          top: element.getBoundingClientRect().top + window.scrollY + yOffset,
+          behavior: 'smooth',
+        });
+        return true;
+      } catch (error) {
+        console.error('Error scrolling to section:', error);
+        return false;
+      }
+    },
+    [sectionCount, yOffset],
+  );
 
   return { sectionRefs, scrollToSection };
 };
diff --git a/frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx b/frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx
index b47d7bfe2..b6176ddfc 100644
--- a/frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx
+++ b/frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx
@@ -8,6 +8,7 @@ import iconHobby from '@/assets/images/icons/category_button/category_hobby_butt
 import iconStudy from '@/assets/images/icons/category_button/category_study_button_icon.svg';
 import iconSport from '@/assets/images/icons/category_button/category_sport_button_icon.svg';
 import iconPerformance from '@/assets/images/icons/category_button/category_performance_button_icon.svg';
+import { useSearch } from '@/context/SearchContext';
 
 interface Category {
   id: string;
@@ -61,6 +62,8 @@ const clubCategories: Category[] = [
 ];
 
 const CategoryButtonList = ({ onCategorySelect }: CategoryButtonListProps) => {
+  const { setKeyword, setInputValue } = useSearch();
+
   const handleCategoryClick = (category: Category) => {
     mixpanel.track(category.eventName, {
       category_id: category.id,
@@ -69,6 +72,9 @@ const CategoryButtonList = ({ onCategorySelect }: CategoryButtonListProps) => {
       url: window.location.href,
     });
 
+    setKeyword('');
+    setInputValue('');
+
     onCategorySelect(category.id);
   };
 
diff --git a/frontend/src/pages/MainPage/components/MobileHeader/MobileHeader.tsx b/frontend/src/pages/MainPage/components/MobileHeader/MobileHeader.tsx
index f0537df77..b8d830b2f 100644
--- a/frontend/src/pages/MainPage/components/MobileHeader/MobileHeader.tsx
+++ b/frontend/src/pages/MainPage/components/MobileHeader/MobileHeader.tsx
@@ -1,19 +1,22 @@
 import React from 'react';
+import { useNavigate } from 'react-router-dom';
 import SearchBox from '@/components/common/SearchBox/SearchBox';
 import useMixpanelTrack from '@/hooks/useMixpanelTrack';
 import * as Styled from './MobileHeader.styles';
 import MainIcon from '@/assets/images/logos/moadong_mobile_logo.png';
 import MenuBar from '@/assets/images/icons/menu_button_icon.svg';
-
-import { useNavigate } from 'react-router-dom';
+import { useSearch } from '@/context/SearchContext';
 
 const MainMobileHeader = () => {
+  const { setKeyword, setInputValue } = useSearch();
   const navigate = useNavigate();
   const trackEvent = useMixpanelTrack();
 
   const handleHomeClick = () => {
-    trackEvent('Mobile Home Button Clicked');
     navigate('/');
+    setKeyword('');
+    setInputValue('');
+    trackEvent('Mobile Home Button Clicked');
   };
 
   const handleMenuClick = () => {
diff --git a/frontend/src/utils/getDeadLineText.ts b/frontend/src/utils/getDeadLineText.ts
index 30aa8aad2..a1603d4e5 100644
--- a/frontend/src/utils/getDeadLineText.ts
+++ b/frontend/src/utils/getDeadLineText.ts
@@ -5,7 +5,6 @@ const getDeadlineText = (
   recruitmentEnd: Date | null,
   today: Date = new Date(),
 ): string => {
-  console.log(recruitmentEnd, recruitmentStart);
   if (!recruitmentStart || !recruitmentEnd) return '모집 마감';
   if (isBefore(today, recruitmentStart)) return '모집 전';
   if (isAfter(today, recruitmentEnd)) return '모집 마감';


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-15T08:05:40Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request refactors the search functionality by moving keyword management from a global context to a local state within the SearchBox component and updating the search trigger events.  
- Removed unnecessary console logging in the deadline helper function.  
- Updated SearchBox to use a local input state, added form submission handling, and repositioned the navigation redirection for improved UX.

### Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

| File                                                         | Description                                                                          |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| frontend/src/utils/getDeadLineText.ts                        | Removed an extraneous console.log to clean up debugging output.                      |
| frontend/src/components/common/SearchBox/SearchBox.tsx       | Refactored state management and search triggering mechanisms with onSubmit handling. |


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**frontend/src/components/common/SearchBox/SearchBox.tsx:59**
* Having an onClick handler on a button of type 'submit' that is also within a form with an onSubmit handler may result in duplicate search execution. Consider removing the onClick handler and relying solely on the form's submit event to trigger the search.
```
onClick={handleSearchClick}
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T02:32:38Z
user: login: oesnuj
body: 검색어 Enter 처리로 변경된 것 확인했습니다!
수고 많으셨습니다 :)
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-27T07:48:33Z
user: login: seongwon030
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-27T08:22:53Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>frontend/src/hooks/InfoTabs/useAutoScroll.ts (1)</summary><blockquote>

`3-8`: **JSDoc 주석이 명확하게 작성되었습니다.**

훅의 목적, 매개변수, 반환값에 대한 문서화가 잘 되어있어 코드의 가독성과 유지보수성이 향상되었습니다. 사용자가 이 훅을 사용할 때 필요한 정보를 쉽게 파악할 수 있습니다.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>frontend/src/components/common/SearchBox/SearchBox.tsx (1)</summary><blockquote>

`16-20`: **리다이렉트 함수 위치 개선 및 구현**

리다이렉트 함수를 코드 상단으로 이동한 것은 가독성을 높이는 좋은 변경입니다. 하지만 이전 리뷰 코멘트에서 언급된 대로 `window.location` 대신 `useLocation` 훅을 사용하는 것이 일관성 측면에서 더 좋을 것 같습니다.

```diff
- const redirectToMainIfSearchTriggeredOutside = () => {
-   if (window.location.pathname !== '/') {
-     navigate('/');
-   }
- };
+ const location = useLocation();
+ 
+ const redirectToMainIfSearchTriggeredOutside = () => {
+   if (location.pathname !== '/') {
+     navigate('/');
+   }
+ };
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 93edd7b5393196c92b9a2a3492539117e61d5479 and cc095c5f5f603fdbabb2dfbdf5e3ff6be5ed3dae.

</details>

<details>
<summary>📒 Files selected for processing (8)</summary>

* `frontend/.cursorrules` (1 hunks)
* `frontend/src/components/common/Header/Header.tsx` (1 hunks)
* `frontend/src/components/common/SearchBox/SearchBox.tsx` (2 hunks)
* `frontend/src/context/SearchContext.tsx` (2 hunks)
* `frontend/src/hooks/InfoTabs/useAutoScroll.ts` (1 hunks)
* `frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx` (3 hunks)
* `frontend/src/pages/MainPage/components/MobileHeader/MobileHeader.tsx` (1 hunks)
* `frontend/src/utils/getDeadLineText.ts` (0 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* frontend/src/utils/getDeadLineText.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (4)</summary>

<details>
<summary>frontend/src/components/common/Header/Header.tsx (1)</summary><blockquote>

<details>
<summary>frontend/src/context/SearchContext.tsx (1)</summary>

* `useSearch` (33-39)

</details>

</blockquote></details>
<details>
<summary>frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx (1)</summary><blockquote>

<details>
<summary>frontend/src/context/SearchContext.tsx (1)</summary>

* `useSearch` (33-39)

</details>

</blockquote></details>
<details>
<summary>frontend/src/pages/MainPage/components/MobileHeader/MobileHeader.tsx (1)</summary><blockquote>

<details>
<summary>frontend/src/context/SearchContext.tsx (1)</summary>

* `useSearch` (33-39)

</details>

</blockquote></details>
<details>
<summary>frontend/src/components/common/SearchBox/SearchBox.tsx (1)</summary><blockquote>

<details>
<summary>frontend/src/context/SearchContext.tsx (1)</summary>

* `useSearch` (33-39)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (25)</summary><blockquote>

<details>
<summary>frontend/.cursorrules (1)</summary>

`1-688`: **프론트엔드 설계 가이드라인 문서가 잘 정리되어 있습니다.**

이 문서는 프론트엔드 코드 작성을 위한 명확한 지침과 패턴을 제시하고 있으며, 가독성, 예측 가능성, 응집성, 결합도 측면에서 좋은 프렉티스를 잘 설명하고 있습니다. 특히 실제 코드 예제를 통해 개념을 명확히 설명한 점이 좋습니다.

다음과 같은 특징이 잘 구성되어 있습니다:
- 매직 넘버 네이밍과 추상화에 관한 명확한 지침
- 조건부 렌더링 및 복잡한 삼항 연산자 간소화 방법
- 유형 표준화 및 유효성 검사에 대한 패턴
- 결합도 감소 및 응집성 향상을 위한 기법

이 가이드라인은 프로젝트 전반에 걸쳐 일관된 코드 스타일과 설계 결정을 내리는 데 큰 도움이 될 것입니다.

</details>
<details>
<summary>frontend/src/context/SearchContext.tsx (3)</summary>

`6-7`: **검색 컨텍스트에 입력값 상태가 적절하게 추가되었습니다.**

SearchContextType 인터페이스에 `inputValue`와 `setInputValue`를 추가한 것은 검색 입력값과 실제 검색 키워드를 분리하여 관리하는 좋은 접근 방식입니다. 이로 인해 입력 중인 값과 실제 검색에 사용되는 키워드를 구분할 수 있게 되었습니다.

---

`18-18`: **입력값 상태가 적절하게 초기화되었습니다.**

`inputValue` 상태를 빈 문자열로 초기화한 것은 적절합니다. 이는 검색 입력 필드가 처음에는 비어있게 표시되게 합니다.

---

`21-27`: **컨텍스트 값이 올바르게 업데이트되었습니다.**

Provider의 value 객체에 새로운 `inputValue`와 `setInputValue`를 추가하여 컨텍스트 소비자 컴포넌트들이 이 값에 접근할 수 있도록 했습니다. 이는 PR 목표에 부합하는 변경 사항입니다.

</details>
<details>
<summary>frontend/src/hooks/InfoTabs/useAutoScroll.ts (2)</summary>

`9-12`: **훅이 파라미터화되어 재사용성이 향상되었습니다.**

`sectionCount`와 `yOffset` 매개변수를 도입하여 하드코딩된 값 대신 사용자가 필요에 따라 설정할 수 있도록 한 것은 매우 좋은 개선점입니다. 기본값도 적절하게 설정되어 있어 기존 코드와의 호환성도 유지됩니다.

---

`14-43`: **scrollToSection 함수가 안전하고 예측 가능하게 개선되었습니다.**

함수에 다음과 같은 개선 사항이 있습니다:
1. 경계 검사를 통한 유효한 인덱스 확인
2. 요소 존재 여부 확인
3. 오류 처리를 위한 try-catch 블록 추가
4. 성공/실패 여부를 나타내는 불리언 반환값 제공
5. useCallback 의존성 배열에 `sectionCount`와 `yOffset` 추가

이러한 변경 사항은 코드의 안정성과 예측 가능성을 크게 향상시킵니다. 특히 예외 처리와 경계 검사는 런타임 오류 방지에 큰 도움이 됩니다.

</details>
<details>
<summary>frontend/src/pages/MainPage/components/MobileHeader/MobileHeader.tsx (3)</summary>

`8-8`: **useSearch 훅 임포트가 적절하게 추가되었습니다.**

SearchContext에서 useSearch 훅을 가져와 MobileHeader 컴포넌트에서 검색 상태에 접근할 수 있도록 했습니다. 이는 검색 기능 리팩토링의 일부로 필요한 변경 사항입니다.

---

`11-11`: **검색 컨텍스트에서 필요한 상태와 함수가 적절하게 추출되었습니다.**

useSearch에서 필요한 `setKeyword`와 `setInputValue` 함수를 구조 분해 할당으로 추출하여 컴포넌트 내에서 사용할 수 있도록 했습니다. 이는 검색 관련 상태를 효과적으로 관리하는 좋은 방법입니다.

---

`17-19`: **홈 버튼 클릭 시 검색 상태가 적절하게 초기화됩니다.**

홈으로 이동할 때 `setKeyword`와 `setInputValue`를 모두 빈 문자열로 설정하여 검색 입력과 검색 키워드를 초기화하는 것은 사용자 경험 측면에서 좋은 개선입니다. 이를 통해 홈 화면으로 돌아갔을 때 이전 검색 상태가 남아있지 않게 됩니다.

</details>
<details>
<summary>frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx (3)</summary>

`11-11`: **SearchContext 훅 추가로 검색 상태 관리 개선**

여기서 `useSearch` 훅을 가져오는 것은 카테고리 버튼 클릭 시 검색 상태를 리셋하기 위한 좋은 접근 방식입니다. 이를 통해 카테고리 버튼과 검색 기능 간의 상호작용이 명확해집니다.

---

`65-66`: **SearchContext에서 필요한 상태 함수 추출**

검색 키워드와 입력값을 동시에 관리할 수 있도록 `setKeyword`와 `setInputValue`를 가져왔습니다. 이 구조는 검색 상태 관리를 일관되게 유지하는 데 도움이 됩니다.

---

`75-77`: **카테고리 버튼 클릭 시 검색 상태 초기화**

카테고리 선택 시 검색 키워드와 입력값을 모두 초기화하는 방식은 사용자 경험 측면에서 매우 적절합니다. 사용자가 카테고리를 선택하면 이전 검색어에 의한 필터링이 해제되어 해당 카테고리의 모든 항목을 볼 수 있게 됩니다.

</details>
<details>
<summary>frontend/src/components/common/Header/Header.tsx (4)</summary>

`2-7`: **적절한 import 순서 정리 및 SearchContext 추가**

import 순서를 체계적으로 정리하고 `useSearch` 훅을 추가한 것은 코드의 가독성과 일관성을 높이는 좋은 변경입니다. 특히 이제 Header 컴포넌트에서도 검색 관련 상태를 접근할 수 있게 되었습니다.

---

`10-13`: **검색 상태 관리 및 위치 정보 활용**

`useSearch` 훅을 통해 검색 상태 관리 함수들을 가져오고, `useLocation`을 통해 현재 위치 정보를 활용하는 방식은 적절합니다. 이를 통해 홈 버튼 클릭 시 검색 상태를 초기화하는 일관된 사용자 경험을 제공할 수 있습니다.

---

`16-20`: **홈 버튼 클릭 시 검색 상태 초기화**

홈 버튼 클릭 시 검색 키워드와 입력값을 모두 초기화하는 방식은 사용자가 새로운 검색을 시작할 수 있도록 하는 적절한 접근입니다. 이는 PR 목표인 "상태 공유 및 리셋" 부분과 일치합니다.

---

`22-26`: **이벤트 트래킹 위치 개선**

이벤트 트래킹 코드를 `window.location.href` 설정 이후로 이동한 것은 좋은 변경입니다. 이렇게 하면 페이지 이동이 완료된 후에 트래킹 이벤트가 발생하므로 더 정확한 사용자 행동 데이터를 수집할 수 있습니다.

</details>
<details>
<summary>frontend/src/components/common/SearchBox/SearchBox.tsx (9)</summary>

`1-1`: **useRef 추가로 DOM 접근 기능 향상**

`useRef`를 추가한 것은 입력 요소에 직접 접근하여 검색 후 포커스를 제거하는 기능을 구현하기 위한 좋은 접근법입니다. 이는 모바일 환경에서 키보드가 자동으로 닫히도록 하여 사용자 경험을 개선합니다.

---

`10-10`: **검색 입력값과 키워드 상태 분리**

`inputValue`와 `setInputValue`를 `useSearch` 훅에서 가져와 사용하는 방식은 PR 목표인 "검색 키워드 관리" 개선과 일치합니다. 이제 입력값은 실시간으로 변경되지만, 실제 검색 키워드는 검색이 트리거될 때만 업데이트됩니다.

---

`14-14`: **입력 요소 참조 추가**

입력 요소에 대한 참조를 추가하여 `blur()` 메서드를 호출할 수 있게 한 것은 모바일 환경에서 사용자 경험을 개선하는 좋은 방법입니다. 검색 후 키보드가 자동으로 닫힙니다.

---

`22-32`: **검색 실행 로직 개선**

검색 로직을 `handleSearch` 함수로 분리하고, 리다이렉트, 키워드 설정, 입력 포커스 제거 등의 작업을 순차적으로 수행하도록 한 것은 매우 좋은 구조입니다. 이는 PR 목표인 "검색 동작 개선"과 일치합니다.

또한 트래킹 이벤트에 `inputValue`를 사용하는 것은 사용자가 입력한 실제 검색어를 정확히 캡처할 수 있게 해줍니다.

---

`34-37`: **폼 제출 핸들러 추가**

폼 제출을 처리하는 핸들러를 추가하고 기본 동작을 방지(`e.preventDefault()`)하는 것은 PR 목표와 완벽하게 일치합니다. 이제 사용자는 Enter 키를 눌러 검색을 실행할 수 있으며, 페이지 새로고침 없이 검색 결과를 볼 수 있습니다.

---

`40-42`: **폼 태그 활용 및 제출 이벤트 연결**

`SearchBoxContainer`를 폼 요소로 활용하고 `onSubmit` 이벤트를 연결한 것은 좋은 접근법입니다. 이를 통해 사용자가 Enter 키를 눌러 검색을 실행할 수 있으며, 웹 접근성 측면에서도 개선되었습니다.

---

`44-45`: **입력 요소에 참조 연결**

입력 요소에 `ref`를 연결하여 DOM에 직접 접근할 수 있게 한 것은 검색 후 포커스를 제거하는 기능을 구현하기 위해 필요한 변경입니다. 이는 사용자 경험을 향상시킵니다.

---

`47-48`: **입력값 상태 관리 방식 개선**

이전에는 모든 입력 변경이 전역 키워드 상태를 업데이트했지만, 이제 `inputValue`와 `onChange` 이벤트 핸들러를 통해 입력값을 로컬에서 관리하다가 검색이 트리거될 때만 전역 키워드 상태를 업데이트하는 방식으로 변경되었습니다. 이는 PR 목표인 "검색 키워드 관리" 개선과 일치하며, 성능 문제와 UI 깜빡임을 해결합니다.

---

`54-54`: **버튼 타입을 submit으로 변경**

검색 버튼의 타입을 `button`에서 `submit`으로 변경한 것은 폼 제출 방식을 활용하기 위한 적절한 변경입니다. 이제 버튼 클릭 시 폼이 제출되어 검색이 실행됩니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T08:26:13Z
user: login: seongwon030
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-27T08:27:41Z
user: login: seongwon030
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-27T08:30:04Z

Review comments:
diff_hunk: @@ -7,43 +7,55 @@ import { useNavigate } from 'react-router-dom';
 
 const SearchBox = () => {
   const [isSearchBoxClicked, setIsSearchBoxClicked] = useState(false);
-  const { keyword, setKeyword } = useSearch();
+  const { setKeyword, inputValue, setInputValue } = useSearch();
   const trackEvent = useMixpanelTrack();
   const navigate = useNavigate();
 
-  const handleSearchClick = () => {
-    redirectToMainIfSearchTriggeredOutside();
+  const inputRef = useRef<HTMLInputElement>(null);
 
-    if (!keyword.trim()) {
-      return;
+  const redirectToMainIfSearchTriggeredOutside = () => {
+    if (window.location.pathname !== '/') {
+      navigate('/');
     }
+  };
+
+  const handleSearch = () => {
+    redirectToMainIfSearchTriggeredOutside();
+    setKeyword(inputValue);
+
+    inputRef.current?.blur();
 
     trackEvent('Search Executed', {
-      keyword,
+      inputValue: inputValue,
       page: window.location.pathname,
     });
+  };
 
-    setKeyword(keyword);
+  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
+    e.preventDefault();
+    handleSearch();
   };
 
-  const redirectToMainIfSearchTriggeredOutside = () => {
-    if (window.location.pathname !== '/') {
-      navigate('/');
-    }
+  const handleSearchClick = () => {
+    handleSearch();
   };
 
   return (
-    <Styled.SearchBoxContainer isFocused={isSearchBoxClicked}>
+    <Styled.SearchBoxContainer
+      isFocused={isSearchBoxClicked}
+      onSubmit={handleSubmit}>
       <Styled.SearchInputStyles
+        ref={inputRef}
+        type='text'
         placeholder='어떤 동아리를 찾으세요?'
-        value={keyword}
-        onChange={(e) => setKeyword(e.target.value)}
+        value={inputValue}
+        onChange={(e) => setInputValue(e.target.value)}
         onFocus={() => setIsSearchBoxClicked(true)}
         onBlur={() => setIsSearchBoxClicked(false)}
         aria-label='동아리 검색창'
       />
       <Styled.SearchButton
-        type='button'
+        type='submit'
         onClick={handleSearchClick}
path: frontend/src/components/common/SearchBox/SearchBox.tsx
user: login: oesnuj
body: 현재 버튼 클릭 시 onClick과 form의 onSubmit이 함께 동작하는 구조인데요
form에 이미 `onSubmit`이 걸려있기 때문에, SearchButton 안에는 별도로 `onClick`을 달지 않아도 정상적으로 폼 제출이 될 것 같습니다.
버튼에는 type="submit"로 잘 설정되어있으니 핸들러는 handleSubmit 하나로 통일하면 더 깔끔할 것 같아요 😁

created_at: 2025-04-27T07:22:40Z
author_association: MEMBER
diff_hunk: @@ -7,43 +7,55 @@ import { useNavigate } from 'react-router-dom';
 
 const SearchBox = () => {
   const [isSearchBoxClicked, setIsSearchBoxClicked] = useState(false);
-  const { keyword, setKeyword } = useSearch();
+  const { setKeyword, inputValue, setInputValue } = useSearch();
   const trackEvent = useMixpanelTrack();
   const navigate = useNavigate();
 
-  const handleSearchClick = () => {
-    redirectToMainIfSearchTriggeredOutside();
+  const inputRef = useRef<HTMLInputElement>(null);
 
-    if (!keyword.trim()) {
-      return;
+  const redirectToMainIfSearchTriggeredOutside = () => {
+    if (window.location.pathname !== '/') {
+      navigate('/');
     }
+  };
path: frontend/src/components/common/SearchBox/SearchBox.tsx
user: login: oesnuj
body: `redirectToMainIfSearchTriggeredOutside `함수에서 현재 위치를 파악하는 부분은 `window.location` 대신 `useLocation `훅을 사용하는 것이 더 좋을 것 같습니다!
Header.tsx에서도 이미 useLocation을 사용하고 있어서, 통일성과 유지보수 측면에서도 맞추는 쪽이 더 좋을 것 같아요 :)
또한  jest 테스트나 믹스패널 트래킹 처리 시 코드가 더 간결해지는 장점도 있습니다.
created_at: 2025-04-27T07:31:02Z
author_association: MEMBER
diff_hunk: @@ -7,43 +7,55 @@ import { useNavigate } from 'react-router-dom';
 
 const SearchBox = () => {
   const [isSearchBoxClicked, setIsSearchBoxClicked] = useState(false);
-  const { keyword, setKeyword } = useSearch();
+  const { setKeyword, inputValue, setInputValue } = useSearch();
   const trackEvent = useMixpanelTrack();
   const navigate = useNavigate();
 
-  const handleSearchClick = () => {
-    redirectToMainIfSearchTriggeredOutside();
+  const inputRef = useRef<HTMLInputElement>(null);
 
-    if (!keyword.trim()) {
-      return;
+  const redirectToMainIfSearchTriggeredOutside = () => {
+    if (window.location.pathname !== '/') {
+      navigate('/');
     }
+  };
path: frontend/src/components/common/SearchBox/SearchBox.tsx
user: login: oesnuj
body: 추가로 함수명을 조금 더 간결하게 다듬어보는 것도 괜찮을 것 같아 가볍게 제안드립니다 ㅎㅎ
created_at: 2025-04-27T07:36:48Z
author_association: MEMBER
diff_hunk: @@ -1,21 +1,46 @@
-import { useRef } from 'react';
+import { useRef, useCallback } from 'react';
 
-const useAutoScroll = () => {
+/**
+ * 섹션 자동 스크롤을 위한 커스텀 훅
+ * @param sectionCount - 섹션의 총 개수
+ * @param yOffset - 스크롤 시 상단 여백 (기본값: -110)
+ * @returns {Object} sectionRefs와 scrollToSection 함수를 포함한 객체
+ */
+const useAutoScroll = (sectionCount: number = 4, yOffset: number = -110) => {
   const sectionRefs = useRef<(HTMLDivElement | null)[]>(
-    new Array(4).fill(null),
+    new Array(sectionCount).fill(null),
   );
path: frontend/src/hooks/InfoTabs/useAutoScroll.ts
user: login: oesnuj
body: JSdoc 주석이 잘 정리되어 있어서 리뷰할 때 큰 도움이 되었습니다!
`sectionCount`와 `yOffset `파라미터 처리, 인덱스 예외처리까지 깔끔하게 작성해주셔서 전체적인 코드 안정성이 높아진 것 같습니다.
created_at: 2025-04-27T07:42:39Z
author_association: MEMBER
diff_hunk: @@ -7,43 +7,55 @@ import { useNavigate } from 'react-router-dom';
 
 const SearchBox = () => {
   const [isSearchBoxClicked, setIsSearchBoxClicked] = useState(false);
-  const { keyword, setKeyword } = useSearch();
+  const { setKeyword, inputValue, setInputValue } = useSearch();
   const trackEvent = useMixpanelTrack();
   const navigate = useNavigate();
 
-  const handleSearchClick = () => {
-    redirectToMainIfSearchTriggeredOutside();
+  const inputRef = useRef<HTMLInputElement>(null);
 
-    if (!keyword.trim()) {
-      return;
+  const redirectToMainIfSearchTriggeredOutside = () => {
+    if (window.location.pathname !== '/') {
+      navigate('/');
     }
+  };
+
+  const handleSearch = () => {
+    redirectToMainIfSearchTriggeredOutside();
+    setKeyword(inputValue);
+
+    inputRef.current?.blur();
 
     trackEvent('Search Executed', {
-      keyword,
+      inputValue: inputValue,
       page: window.location.pathname,
     });
+  };
 
-    setKeyword(keyword);
+  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
+    e.preventDefault();
+    handleSearch();
   };
 
-  const redirectToMainIfSearchTriggeredOutside = () => {
-    if (window.location.pathname !== '/') {
-      navigate('/');
-    }
+  const handleSearchClick = () => {
+    handleSearch();
   };
 
   return (
-    <Styled.SearchBoxContainer isFocused={isSearchBoxClicked}>
+    <Styled.SearchBoxContainer
+      isFocused={isSearchBoxClicked}
+      onSubmit={handleSubmit}>
       <Styled.SearchInputStyles
+        ref={inputRef}
+        type='text'
         placeholder='어떤 동아리를 찾으세요?'
-        value={keyword}
-        onChange={(e) => setKeyword(e.target.value)}
+        value={inputValue}
+        onChange={(e) => setInputValue(e.target.value)}
         onFocus={() => setIsSearchBoxClicked(true)}
         onBlur={() => setIsSearchBoxClicked(false)}
         aria-label='동아리 검색창'
       />
       <Styled.SearchButton
-        type='button'
+        type='submit'
         onClick={handleSearchClick}
path: frontend/src/components/common/SearchBox/SearchBox.tsx
user: login: seongwon030
body: type="submit"을 설정하면 버튼이 form 태그 안에 있을 때, 버튼을 클릭하면 자동으로 form 이 submit 되네요?
하나로 통일하겠습니다 ㅎㅎ
created_at: 2025-04-27T08:22:53Z
author_association: MEMBER
diff_hunk: @@ -7,43 +7,55 @@ import { useNavigate } from 'react-router-dom';
 
 const SearchBox = () => {
   const [isSearchBoxClicked, setIsSearchBoxClicked] = useState(false);
-  const { keyword, setKeyword } = useSearch();
+  const { setKeyword, inputValue, setInputValue } = useSearch();
   const trackEvent = useMixpanelTrack();
   const navigate = useNavigate();
 
-  const handleSearchClick = () => {
-    redirectToMainIfSearchTriggeredOutside();
+  const inputRef = useRef<HTMLInputElement>(null);
 
-    if (!keyword.trim()) {
-      return;
+  const redirectToMainIfSearchTriggeredOutside = () => {
+    if (window.location.pathname !== '/') {
+      navigate('/');
     }
+  };
path: frontend/src/components/common/SearchBox/SearchBox.tsx
user: login: seongwon030
body: 일관성 위해서 ```useLocation```으로 수정했습미다~
created_at: 2025-04-27T08:27:41Z
author_association: MEMBER
diff_hunk: @@ -7,43 +7,55 @@ import { useNavigate } from 'react-router-dom';
 
 const SearchBox = () => {
   const [isSearchBoxClicked, setIsSearchBoxClicked] = useState(false);
-  const { keyword, setKeyword } = useSearch();
+  const { setKeyword, inputValue, setInputValue } = useSearch();
   const trackEvent = useMixpanelTrack();
   const navigate = useNavigate();
 
-  const handleSearchClick = () => {
-    redirectToMainIfSearchTriggeredOutside();
+  const inputRef = useRef<HTMLInputElement>(null);
 
-    if (!keyword.trim()) {
-      return;
+  const redirectToMainIfSearchTriggeredOutside = () => {
+    if (window.location.pathname !== '/') {
+      navigate('/');
     }
+  };
path: frontend/src/components/common/SearchBox/SearchBox.tsx
user: login: seongwon030
body: 의도를 잘 드러내는 표현을 찾다가 함수이름이 너무 길어졌네요. 너무 길어도 가독성이 떨어지네요.
```redirectToHome``` 으로 변경했습니다! 

created_at: 2025-04-27T08:30:04Z
author_association: MEMBER

PR comments:
user: login: netlify[bot]
created_at: 2025-04-15T08:05:51Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *moadong* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 02f4df82ba0a4342cf3dd0c66dac5597f0f954db |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/moadong/deploys/680dfe5dd323e7000838b947 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-326--moadong.netlify.app](https://deploy-preview-326--moadong.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTMyNi0tbW9hZG9uZy5uZXRsaWZ5LmFwcCJ9.Aq2kB8WOxNqnRSfBUK93tWjC0dUt_dsWDprd5o4DufQ)<br /><br />_Use your smartphone camera to open QR code link._</details> |
|<span aria-hidden="true">![Lighthouse](https://app.netlify.com/public/external-usage/git-comments/lighthouse-sm.png)</span><br />Lighthouse | 1 paths audited<br />**Performance**: 35<br />**Accessibility**: 87<br />**Best Practices**: 92<br />**SEO**: 85<br />**PWA**: -<br />[View the detailed breakdown and full score reports](https://app.netlify.com/sites/moadong/deploys/680dfe5dd323e7000838b947) |
---
<!-- [moadong Preview](https://deploy-preview-326--moadong.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/moadong/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-04-25T04:59:35Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
> [!TIP]
> For best results, initiate chat on the files or code changes.

<details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Fetch PR details
gh pr view 326 --json title,body,commits,files,baseRefName,headRefName,author

# Check if there's a .coderabbit.yaml file
if [ -f .coderabbit.yaml ]; then
    echo "CodeRabbit configuration found:"
    cat .coderabbit.yaml
else
    echo "No .coderabbit.yaml file found"
fi

# Get the list of files changed in this PR
echo "Files changed in this PR:"
gh pr view 326 --json files --jq '.files[].path'
```

Length of output: 8339

---

🏁 Script executed:
```
#!/bin/bash
# Let's examine the key files that have been changed

# Check SearchBox.tsx
echo "=== SearchBox.tsx DIFF ==="
gh pr view 326 --json files --jq '.files[] | select(.path=="frontend/src/components/common/SearchBox/SearchBox.tsx") | .patch'

echo -e "\n=== SearchContext.tsx DIFF ==="
gh pr view 326 --json files --jq '.files[] | select(.path=="frontend/src/context/SearchContext.tsx") | .patch'

echo -e "\n=== CategoryButtonList.tsx DIFF ==="
gh pr view 326 --json files --jq '.files[] | select(.path=="frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx") | .patch'

echo -e "\n=== Header.tsx DIFF ==="
gh pr view 326 --json files --jq '.files[] | select(.path=="frontend/src/components/common/Header/Header.tsx") | .patch'

echo -e "\n=== useAutoScroll.ts DIFF ==="
gh pr view 326 --json files --jq '.files[] | select(.path=="frontend/src/hooks/InfoTabs/useAutoScroll.ts") | .patch'
```

Length of output: 1008

---

🏁 Script executed:
```
#!/bin/bash
# Let's examine the key files by getting their current content

echo "=== SearchBox.tsx CONTENT ==="
gh pr view 326 --json files --jq '.files[] | select(.path=="frontend/src/components/common/SearchBox/SearchBox.tsx")' | jq -r '.additions, .deletions'

# Let's try to get the files directly
echo -e "\n=== Getting files from the PR branch ==="
git fetch origin pull/326/head:pr-326
git checkout pr-326

# Now let's view the files directly
echo -e "\n=== SearchBox.tsx CONTENT ==="
if [ -f "frontend/src/components/common/SearchBox/SearchBox.tsx" ]; then
    cat "frontend/src/components/common/SearchBox/SearchBox.tsx"
else
    echo "File not found"
fi

echo -e "\n=== SearchContext.tsx CONTENT ==="
if [ -f "frontend/src/context/SearchContext.tsx" ]; then
    cat "frontend/src/context/SearchContext.tsx"
else
    echo "File not found"
fi

echo -e "\n=== CategoryButtonList.tsx CONTENT ==="
if [ -f "frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx" ]; then
    cat "frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx"
else
    echo "File not found"
fi
```

Length of output: 6711

---



</details>

@seongwon030 안녕하세요! 리뷰를 수행하겠습니다. PR #326을 검토했습니다.

## 검색 구조 변경 리뷰

PR에서 변경된 주요 내용:

1. **검색 방식 개선**:
   - onChange에서 onClick/submit 방식으로 변경하여 사용자 경험 향상
   - form 태그 활용으로 엔터키를 통한 검색 기능 추가
   - 검색 후 모바일 키보드 자동 숨김 처리 (blur 사용)

2. **상태 관리 최적화**:
   - 이전: 전역 keyword가 입력 시마다 업데이트되어 성능 이슈 발생
   - 변경: inputValue 상태로 로컬에서 관리하다가 검색 실행 시만 전역 keyword 업데이트
   - 추가 변경: inputValue도 전역으로 관리하여 컴포넌트 간 공유 가능

3. **사용자 경험 개선**:
   - 카테고리 버튼 클릭 시 검색어 초기화
   - 홈 버튼 클릭 시 검색어 초기화
   - 빈 검색어 처리로 전체 항목 표시

이 변경사항은 사용자 경험과 애플리케이션 성능을 모두 개선하는 좋은 리팩토링입니다. 특히 입력할 때마다 필터링되던 기존 방식에서 사용자가 의도적으로 검색을 실행할 때만 필터링하도록 변경한 점이 효과적입니다.

`useAutoScroll` 훅 관련 리팩토링도 매직넘버를 분리하고 방어 코드를 추가하여 견고성을 높였습니다.

코드 구조가 전반적으로 개선되었으며, 사용자 경험 측면에서도 훨씬 직관적이 되었습니다. 👍


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Fetch PR details
gh pr view 326 --json title,body,commits,files,baseRefName,headRefName,author

# Check if there's a .coderabbit.yaml file
if [ -f .coderabbit.yaml ]; then
    echo "CodeRabbit configuration found:"
    cat .coderabbit.yaml
else
    echo "No .coderabbit.yaml file found"
fi

# Get the list of files changed in this PR
echo "Files changed in this PR:"
gh pr view 326 --json files --jq '.files[].path'
```

Length of output: 8339

---

🏁 Script executed:
```
#!/bin/bash
# Let's examine the key files that have been changed

# Check SearchBox.tsx
echo "=== SearchBox.tsx DIFF ==="
gh pr view 326 --json files --jq '.files[] | select(.path=="frontend/src/components/common/SearchBox/SearchBox.tsx") | .patch'

echo -e "\n=== SearchContext.tsx DIFF ==="
gh pr view 326 --json files --jq '.files[] | select(.path=="frontend/src/context/SearchContext.tsx") | .patch'

echo -e "\n=== CategoryButtonList.tsx DIFF ==="
gh pr view 326 --json files --jq '.files[] | select(.path=="frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx") | .patch'

echo -e "\n=== Header.tsx DIFF ==="
gh pr view 326 --json files --jq '.files[] | select(.path=="frontend/src/components/common/Header/Header.tsx") | .patch'

echo -e "\n=== useAutoScroll.ts DIFF ==="
gh pr view 326 --json files --jq '.files[] | select(.path=="frontend/src/hooks/InfoTabs/useAutoScroll.ts") | .patch'
```

Length of output: 1008

---

🏁 Script executed:
```
#!/bin/bash
# Let's examine the key files by getting their current content

echo "=== SearchBox.tsx CONTENT ==="
gh pr view 326 --json files --jq '.files[] | select(.path=="frontend/src/components/common/SearchBox/SearchBox.tsx")' | jq -r '.additions, .deletions'

# Let's try to get the files directly
echo -e "\n=== Getting files from the PR branch ==="
git fetch origin pull/326/head:pr-326
git checkout pr-326

# Now let's view the files directly
echo -e "\n=== SearchBox.tsx CONTENT ==="
if [ -f "frontend/src/components/common/SearchBox/SearchBox.tsx" ]; then
    cat "frontend/src/components/common/SearchBox/SearchBox.tsx"
else
    echo "File not found"
fi

echo -e "\n=== SearchContext.tsx CONTENT ==="
if [ -f "frontend/src/context/SearchContext.tsx" ]; then
    cat "frontend/src/context/SearchContext.tsx"
else
    echo "File not found"
fi

echo -e "\n=== CategoryButtonList.tsx CONTENT ==="
if [ -f "frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx" ]; then
    cat "frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx"
else
    echo "File not found"
fi
```

Length of output: 6711

---



</details>

@seongwon030 안녕하세요! 리뷰를 수행하겠습니다. PR #326을 검토했습니다.

## 검색 구조 변경 리뷰

PR에서 변경된 주요 내용:

1. **검색 방식 개선**:
   - onChange에서 onClick/submit 방식으로 변경하여 사용자 경험 향상
   - form 태그 활용으로 엔터키를 통한 검색 기능 추가
   - 검색 후 모바일 키보드 자동 숨김 처리 (blur 사용)

2. **상태 관리 최적화**:
   - 이전: 전역 keyword가 입력 시마다 업데이트되어 성능 이슈 발생
   - 변경: inputValue 상태로 로컬에서 관리하다가 검색 실행 시만 전역 keyword 업데이트
   - 추가 변경: inputValue도 전역으로 관리하여 컴포넌트 간 공유 가능

3. **사용자 경험 개선**:
   - 카테고리 버튼 클릭 시 검색어 초기화
   - 홈 버튼 클릭 시 검색어 초기화
   - 빈 검색어 처리로 전체 항목 표시

이 변경사항은 사용자 경험과 애플리케이션 성능을 모두 개선하는 좋은 리팩토링입니다. 특히 입력할 때마다 필터링되던 기존 방식에서 사용자가 의도적으로 검색을 실행할 때만 필터링하도록 변경한 점이 효과적입니다.

`useAutoScroll` 훅 관련 리팩토링도 매직넘버를 분리하고 방어 코드를 추가하여 견고성을 높였습니다.

코드 구조가 전반적으로 개선되었으며, 사용자 경험 측면에서도 훨씬 직관적이 되었습니다. 👍</answer></rawResChunk> -->
author_association: CONTRIBUTOR