Skip to content

feat: 상품 목록 페이지 구현#14

Open
Stephan1226 wants to merge 3 commits intomainfrom
feature/item-list
Open

feat: 상품 목록 페이지 구현#14
Stephan1226 wants to merge 3 commits intomainfrom
feature/item-list

Conversation

@Stephan1226
Copy link
Contributor

@Stephan1226 Stephan1226 commented Nov 17, 2025

개요

  • 상품 목록 페이지를 구현함

스크린샷

image

Summary by CodeRabbit

  • New Features
    • Added an item browsing page with category filter tabs, text search, and route access at /item-list.
    • Displays items in a responsive grid of product cards with image placeholders, name, formatted price, and NEW/HOT badges.
    • Includes loading and empty states and improved responsive styling for mobile/tablet/desktop.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 17, 2025

Walkthrough

New ItemList React component and styles added, fetching items from v2/item/ on mount (with 204/error fallback to mock data), providing category tabs and text search filtering, and rendering a responsive product grid. Router updated to expose the component at /item-list.

Changes

Cohort / File(s) Summary
ItemList Component
src/components/User/ItemList/index.tsx
New React functional component that fetches items from v2/item/ via axiosInstance on mount, handles 204/empty/error by loading mock data, maps API response to a local Item shape, and manages state for items, filteredItems, selectedCategory, searchTerm, and loading. Renders category tabs, search bar, loading/empty states, and a responsive product grid showing badges, name, price, and image placeholder.
ItemList Styling
src/components/User/ItemList/style.ts
New styled-components module exporting layout and UI primitives (Container, Title, FilterSection, CategoryTabBar, CategoryTab, SearchBar, SearchInput, SearchIcon, ProductGrid, ProductCard, Badge, ProductInfo, ProductTitle, ProductPrice, ProductImagePlaceholder, LoadingMessage, EmptyMessage) with responsive behavior, active/tab and badge props, hover effects, overlays, and placeholders.
Router Integration
src/routers/UserRouter.tsx
Imports ItemList and registers a new route /item-list under user routes to render the new component.

Sequence Diagram

sequenceDiagram
    participant User
    participant ItemList as ItemList Component
    participant API as API (v2/item/)
    participant Mock as Mock Data

    User->>ItemList: Mount
    activate ItemList
    ItemList->>API: GET /v2/item/
    activate API

    alt API returns 200 with data
        API-->>ItemList: items[]
        ItemList->>ItemList: map to Item, set items state
    else API returns 204 / error / empty
        API-->>ItemList: 204 / error / no data
        ItemList->>Mock: load mockItems
        Mock-->>ItemList: mockItems[]
        ItemList->>ItemList: set items state from mock
    end
    deactivate API

    User->>ItemList: select category / enter search
    ItemList->>ItemList: filter items by category + searchTerm
    ItemList-->>User: render filtered product grid
    deactivate ItemList
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Verify API error/204 handling and mapping defaults (category, isNew, isHot)
  • Check useEffect dependency arrays and potential re-render/fetch loops
  • Review styled-components props (CategoryTab.active, Badge.type) and responsive breakpoints
  • Confirm router integration and route naming

Suggested reviewers

  • sosojung

Poem

"I hopped through code with tiny paws,
categories, search and fetching laws.
A grid of goods, badges bright —
I nudged the route to show the sight.
— 🥕🐇"

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: 상품 목록 페이지 구현' (feat: Implement product list page) directly and concisely describes the main change - introducing a new ItemList component with product browsing, filtering, and search functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/item-list

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (7)
src/components/User/ItemList/index.tsx (3)

27-172: Consider extracting mock data to a separate file.

The 146-line mock dataset adds significant size to the component file. Moving it to a separate constants file (e.g., mockItems.ts) would improve maintainability and make the component logic easier to read.

Example structure:

// mockItems.ts
export const mockItems: Item[] = [
  // ... mock data
];

Then import it:

+import { mockItems } from './mockItems';
+
-// 목 데이터
-const mockItems: Item[] = [
-  // ... (remove inline data)
-];

207-239: Consider improvements to error handling and type safety.

A few refinements would strengthen this function:

  1. Console statements (lines 214, 232): Consider using a proper logging service or removing these before production deployment.
  2. Type safety (line 218): The any type for the mapping function parameter bypasses TypeScript's type checking. Consider defining an API response interface.
  3. User feedback: Errors silently fall back to mock data without informing users. Consider showing a toast notification or status message when the API call fails.

Example for type safety:

interface ApiItem {
  itemId: number;
  itemName: string;
  itemCode: string;
  itemPrice: number;
  category?: string;
  isNew?: boolean;
  isHot?: boolean;
}

// Then in the mapping:
const remappedData: Item[] = response.data.itemList.map((item: ApiItem) => ({
  // ...
}));

249-312: LGTM! Clean render logic with minor accessibility consideration.

The conditional rendering and grid layout are well-structured. Consider adding accessibility enhancements:

  • Add aria-label or associate a <label> with the SearchInput (line 267-271)
  • Ensure CategoryTab buttons are keyboard navigable (already using <button>, which is good)
src/components/User/ItemList/style.ts (4)

3-64: Consider using a theme system for colors and spacing.

The hardcoded color values (#111111, #666666, #F49E15, etc.) and spacing values appear throughout the file. Using a centralized theme or design tokens would improve maintainability and consistency across the application.

Example approach:

const theme = {
  colors: {
    primary: '#F49E15',
    textPrimary: '#111111',
    textSecondary: '#666666',
    // ...
  },
  // ...
};

// Then use: color: ${props => props.theme.colors.textPrimary};

179-188: Review the unusual positioning value.

The right: -0.5px positioning (line 181) is unusual and might indicate a workaround for border or gap rendering issues. Consider investigating whether the layout can be adjusted to avoid fractional pixel offsets, which can cause inconsistent rendering across browsers.


147-156: Consider overflow handling for long product names.

The max-width: 273px on ProductInfo (line 155) might cause long product names to overflow. Consider adding overflow: hidden and text-overflow: ellipsis to ProductTitle, or allowing the container to grow.


190-208: LGTM! Clear state messaging.

The loading and empty state components provide appropriate user feedback. As an optional improvement, these could share a common base component since they have identical styling.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68ddf34 and 3f8a900.

📒 Files selected for processing (3)
  • src/components/User/ItemList/index.tsx (1 hunks)
  • src/components/User/ItemList/style.ts (1 hunks)
  • src/routers/UserRouter.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/User/ItemList/index.tsx (1)
src/utils/Axios.ts (1)
  • axiosInstance (18-23)
🔇 Additional comments (4)
src/routers/UserRouter.tsx (1)

12-12: LGTM! Clean route integration.

The import and route registration follow the established patterns in this router.

Also applies to: 26-26

src/components/User/ItemList/index.tsx (3)

5-13: LGTM! Well-defined interface.

The Item interface clearly defines the data structure with appropriate optional fields.


181-197: LGTM! Efficient filtering logic.

The filtering implementation correctly handles both category and search term filters with case-insensitive matching. The useCallback dependencies are appropriate.


199-205: LGTM! Proper effect usage.

The effects are correctly structured: initial data fetch on mount and filtering when dependencies change. The filterItems dependency in the second effect is safe since it's memoized via useCallback.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/components/User/ItemList/index.tsx (1)

141-144: Consider clarifying badge priority logic.

When both isNew and isHot are true, only the NEW badge displays. If this priority is intentional, consider adding a comment to make it explicit. Alternatively, you could display both badges if that's desired behavior.

Current behavior (NEW takes priority):

{(item.isNew || item.isHot) && (
  <S.Badge type={item.isNew ? 'new' : 'hot'}>
    {item.isNew ? 'NEW' : 'HOT'}
  </S.Badge>
)}

If you want to show both badges:

{item.isNew && (
  <S.Badge type='new'>NEW</S.Badge>
)}
{item.isHot && !item.isNew && (
  <S.Badge type='hot'>HOT</S.Badge>
)}

Note: Regarding the past review comment asking how NEW/HOT are managed - the code shows these are provided by the backend API with fallback defaults to false (lines 74-75).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f8a900 and 5c43a69.

📒 Files selected for processing (1)
  • src/components/User/ItemList/index.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/User/ItemList/index.tsx (3)
src/components/Admin/Transaction/ManyCharge/index.jsx (2)
  • searchTerm (12-12)
  • handleSearchChange (39-41)
src/contexts/loadingContext.jsx (1)
  • setLoading (25-32)
src/utils/Axios.ts (1)
  • axiosInstance (18-23)
🔇 Additional comments (4)
src/components/User/ItemList/index.tsx (4)

1-13: LGTM!

The imports and interface definition are clean and appropriate for the component's needs.


33-49: LGTM!

The filtering logic is well-implemented with proper memoization and case-insensitive search. The sequential application of category and search filters works correctly.


51-57: LGTM!

The useEffect hooks are correctly structured. The mount effect handles initial data fetch, and the filter effect properly responds to changes in dependencies through the memoized filterItems callback.


95-157: LGTM!

The render logic is well-structured with proper conditional states (loading, empty, and populated). The price formatting with toLocaleString() provides good localization, and the category tabs with active state create a clear user interface.

Comment on lines +64 to +78
if (response.status === 204 || !response.data.itemList || response.data.itemList.length === 0) {
// API 데이터가 없으면 목 데이터 사용
alert("반환된 상품이 없습니다.");
} else {
const remappedData: Item[] = response.data.itemList.map((item: any) => ({
itemId: item.itemId,
itemName: item.itemName,
itemCode: item.itemCode,
itemPrice: item.itemPrice,
category: item.category || '기타',
isNew: item.isNew || false,
isHot: item.isHot || false,
}));
setItems(remappedData);
setFilteredItems(remappedData);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove redundant alert in favor of UI empty state.

The alert() call is redundant since the component already displays "상품이 없습니다." in the UI (line 136) when there are no items. Additionally, alert() blocks user interaction and provides poor UX.

Apply this diff to remove the alert:

       if (response.status === 204 || !response.data.itemList || response.data.itemList.length === 0) {
-        // API 데이터가 없으면 목 데이터 사용
-          alert("반환된 상품이 없습니다.");
+        // API returned no items - empty state will be shown in UI
       } else {
🤖 Prompt for AI Agents
In src/components/User/ItemList/index.tsx around lines 64 to 78, remove the
blocking alert call when the API returns no items and instead leave the items
state untouched (or explicitly set to an empty array) so the component's
existing UI empty state ("상품이 없습니다.") at line 136 can render; update the branch
that handles response.status === 204 || !response.data.itemList ||
response.data.itemList.length === 0 to omit alert(...) and ensure setItems([])
and setFilteredItems([]) are used (or no-op if already empty) so UX is
non-blocking and consistent with the component's empty-state rendering.

Comment on lines +80 to +83
} catch (error) {
console.error('Error fetching items:', error);
} finally {
setLoading(false);
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

Add user-facing error feedback for API failures.

The catch block silently handles errors with only a console log. Users cannot distinguish between "no items available" and "API error occurred". When the API fails, users see the misleading "상품이 없습니다." message instead of understanding that a system error prevented loading items.

Consider adding error state and user notification:

   const [loading, setLoading] = useState<boolean>(true);
+  const [error, setError] = useState<string | null>(null);

   const fetchItems = async () => {
     try {
       setLoading(true);
+      setError(null);
       const response = await axiosInstance.get('v2/item/');
       
       // ... existing code ...
     } catch (error) {
       console.error('Error fetching items:', error);
+      setError('상품 목록을 불러오는 중 오류가 발생했습니다. 다시 시도해 주세요.');
     } finally {
       setLoading(false);
     }
   };

Then update the render to show error state:

       {loading ? (
         <S.LoadingMessage>로딩 중...</S.LoadingMessage>
+      ) : error ? (
+        <S.EmptyMessage>{error}</S.EmptyMessage>
       ) : filteredItems.length === 0 ? (
         <S.EmptyMessage>상품이 없습니다.</S.EmptyMessage>
       ) : (

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/User/ItemList/index.tsx around lines 80-83, the catch block
only logs API errors so the UI shows the same "상품이 없습니다." message for both empty
results and failures; add a local error state (e.g., error string or boolean)
and set it in the catch block with a user-friendly message when the fetch fails,
ensure setLoading(false) remains in finally, and update the render logic to show
a distinct error UI (toast, alert banner, or inline message with retry) when
error is set instead of the "no items" empty state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants