Skip to content
4 changes: 1 addition & 3 deletions src/components/common/Filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ export const Filter = ({ filters, selectedFilter, setSelectedFilter }: FilterPro
};

const handleModalClick = () => {
if (!isOpenModal) {
setIsOpenModal(true);
}
setIsOpenModal(!isOpenModal);
};

return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const NavBar = ({ src, path }: FabProps) => {
</NavItem>
);
})}
<Fab src={src} path={path} />
{path && <Fab src={src} path={path} />}
</NavContainer>
</NavWrapper>
);
Expand Down
132 changes: 132 additions & 0 deletions src/components/search/BookSearchResult.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import type { SearchedBook } from '@/pages/search/Search';
import styled from '@emotion/styled';
import { colors, typography } from '@/styles/global/global';
import { useNavigate } from 'react-router-dom';

interface BookSearchResultProps {
type: 'searching' | 'searched';
searchedBookList: SearchedBook[];
}

export function BookSearchResult({ type, searchedBookList }: BookSearchResultProps) {
const navigate = useNavigate();
const isEmptySearchedBookList = () => {
if (searchedBookList.length === 0) return true;
else return false;
};

const handleApplyBook = () => {
navigate('/search/applybook');
};
return (
<Wrapper>
<List>
{type === 'searching' ? <></> : <ResultHeader>전체 {searchedBookList.length}</ResultHeader>}

{isEmptySearchedBookList() ? (
<EmptyWrapper>
<MainText>현재 등록된 책이 없어요.</MainText>
<SubText>원하는 책을 신청해주세요!</SubText>
<RequestButton onClick={handleApplyBook}>책 신청하기</RequestButton>
</EmptyWrapper>
) : (
searchedBookList.map(book => (
<BookItem key={book.id}>
<Cover src={book.coverUrl} alt={`${book.title} 커버`} />
<BookInfo>
<Title>{book.title}</Title>
<Subtitle>
{book.author} 저 · {book.publisher}
</Subtitle>
</BookInfo>
</BookItem>
))
)}
</List>
</Wrapper>
);
}

const Wrapper = styled.div`
display: flex;
flex-direction: column;
padding: 0 20px;
margin-bottom: 72px;
`;

const List = styled.div`
display: flex;
flex-direction: column;
`;

const BookItem = styled.div`
display: flex;
border-bottom: 1px solid ${colors.darkgrey.dark};
padding: 12px 0;
`;

const ResultHeader = styled.div`
font-size: ${typography.fontSize.sm};
font-weight: ${typography.fontWeight.medium};
color: ${colors.white};
padding-bottom: 8px;
border-bottom: 1px solid ${colors.darkgrey.dark};
`;

const Cover = styled.img`
width: 80px;
height: 107px;
object-fit: cover;
`;

const BookInfo = styled.div`
display: flex;
flex-direction: column;
margin-left: 12px;
`;

const Title = styled.h3`
font-size: ${typography.fontSize.base};
font-weight: ${typography.fontWeight.semibold};
color: ${colors.white};
`;

const Subtitle = styled.span`
font-size: ${typography.fontSize.xs};
font-weight: ${typography.fontWeight.medium};
color: ${colors.grey[200]};
margin-top: 8px;
`;

const EmptyWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 60vh;
`;

const MainText = styled.p`
color: ${colors.white};
font-size: ${typography.fontSize.lg};
font-weight: ${typography.fontWeight.semibold};
margin-bottom: 8px;
`;

const SubText = styled.p`
color: ${colors.grey[100]};
font-size: ${typography.fontSize.sm};
font-weight: ${typography.fontWeight.regular};
margin-bottom: 20px;
`;

const RequestButton = styled.button`
background-color: ${colors.purple.main};
color: ${colors.white};
padding: 10px 12px;
font-size: ${typography.fontSize.base};
border: none;
border-radius: 12px;
font-weight: ${typography.fontWeight.semibold};
cursor: pointer;
`;
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const dummyMyGroups: Group[] = [
},
];

const SearchResult = () => {
const GroupSearchResult = () => {
const [selected, setSelected] = useState<string>('');
const [showGroup] = useState<Group[]>(dummyMyGroups);
const [selectedFilter, setSelectedFilter] = useState<string>('마감임박순');
Expand Down Expand Up @@ -139,7 +139,7 @@ const SearchResult = () => {
);
};

export default SearchResult;
export default GroupSearchResult;

const TabContainer = styled.div`
display: flex;
Expand Down
153 changes: 153 additions & 0 deletions src/components/search/MostSearchedBooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { colors, typography } from '@/styles/global/global';
import styled from '@emotion/styled';

interface Book {
id: number;
title: string;
coverUrl: string;
}

const dummyBooks: Book[] = [
{
id: 1,
title: '토마토 컵라면',
coverUrl: 'https://cdn.imweb.me/upload/S20230204e049098f5e744/e6fd3d849546d.jpg',
},
{
id: 2,
title: '사슴',
coverUrl: 'https://cdn.imweb.me/upload/S20230204e049098f5e744/e6fd3d849546d.jpg',
},
{
id: 3,
title: '호르몬 체인지지',
coverUrl: 'https://cdn.imweb.me/upload/S20230204e049098f5e744/e6fd3d849546d.jpg',
},
{
id: 4,
title: '호르몬 체인지지',
coverUrl: 'https://cdn.imweb.me/upload/S20230204e049098f5e744/e6fd3d849546d.jpg',
},
{
id: 5,
title: '호르몬 체인지지',
coverUrl: 'https://cdn.imweb.me/upload/S20230204e049098f5e744/e6fd3d849546d.jpg',
},
{
id: 6,
title: '호르몬 체인지지',
coverUrl: 'https://cdn.imweb.me/upload/S20230204e049098f5e744/e6fd3d849546d.jpg',
},
];
Comment on lines +10 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

더미 데이터에서 보안 및 데이터 품질 문제가 있습니다.

  1. 외부 URL 사용으로 인한 보안 및 가용성 위험
  2. 3번부터 6번까지 동일한 책 제목으로 복사-붙여넣기 오류 의심
  3. 모든 책이 동일한 외부 이미지 URL 사용

다음과 같이 개선하는 것을 권장합니다:

const dummyBooks: Book[] = [
  {
    id: 1,
    title: '토마토 컵라면',
-    coverUrl: 'https://cdn.imweb.me/upload/S20230204e049098f5e744/e6fd3d849546d.jpg',
+    coverUrl: '/images/book-placeholder.jpg',
  },
  {
    id: 2,
-    title: '사슴',
-    coverUrl: 'https://cdn.imweb.me/upload/S20230204e049098f5e744/e6fd3d849546d.jpg',
+    title: '어린 왕자',
+    coverUrl: '/images/book-placeholder-2.jpg',
  },
  {
    id: 3,
-    title: '호르몬 체인지지',
-    coverUrl: 'https://cdn.imweb.me/upload/S20230204e049098f5e744/e6fd3d849546d.jpg',
+    title: '1984',
+    coverUrl: '/images/book-placeholder-3.jpg',
  },
  // 각각 다른 책 정보로 수정
];

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

🤖 Prompt for AI Agents
In src/components/search/MostSearchedBooks.tsx between lines 10 and 41, the
dummyBooks array has security and data quality issues: it uses external image
URLs which pose security and availability risks, and books from id 3 to 6 have
duplicated titles and identical cover URLs likely due to copy-paste errors. To
fix this, replace the external image URLs with local or trusted internal assets,
ensure each book has a unique and meaningful title, and assign distinct cover
images for each book to improve data quality and security.


export default function MostSearchedBooks() {
return (
<Container>
<Header>
<Title>가장 많이 검색된 책</Title>
{/* 서버 응답 포맷을 모르기에 우선 하드 코딩 */}
<DateText>01.12. 기준</DateText>
Comment on lines +48 to +49

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

하드코딩된 날짜를 동적으로 처리하도록 개선이 필요합니다.

현재 하드코딩된 날짜는 임시 구현으로 보입니다. 프로덕션에서는 서버에서 받은 데이터를 기반으로 동적으로 표시되어야 합니다.

다음과 같이 개선하는 것을 권장합니다:

- {/* 서버 응답 포맷을 모르기에 우선 하드 코딩 */}
- <DateText>01.12. 기준</DateText>
+ <DateText>{formatDate(lastUpdated)} 기준</DateText>

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

🤖 Prompt for AI Agents
In src/components/search/MostSearchedBooks.tsx around lines 48 to 49, the date
"01.12." is hardcoded in the DateText component. Replace this hardcoded date
with a dynamic value sourced from the server response or component props. Ensure
the date is formatted correctly before rendering, so it updates based on actual
data rather than a fixed string.

</Header>
{dummyBooks.length === 0 ? (
<EmptyMessage>
<MainText>아직 순위가 집계되지 않았어요.</MainText>
<SubText>조금만 기다려주세요!</SubText>
</EmptyMessage>
) : (
<BookList>
{dummyBooks.map((book, index) => (
<BookItem key={book.id}>
<Rank>{index + 1}.</Rank>
<Cover src={book.coverUrl} alt={`${book.title} 커버`} />
<BookTitle>{book.title}</BookTitle>
</BookItem>
))}
</BookList>
)}
</Container>
);
}

const Container = styled.div`
display: flex;
flex-direction: column;
padding: 36px 20px 0 20px;
background-color: var(--color-black-main);
margin-bottom: 72px;
`;

const Header = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
`;

const Title = styled.h2`
font-size: ${typography.fontSize.lg};
color: ${colors.white};
font-weight: ${typography.fontWeight.semibold};
`;

const DateText = styled.span`
font-size: ${typography.fontSize.xs};
color: ${colors.grey[300]};
font-weight: ${typography.fontWeight.regular};
`;

const BookList = styled.ul`
display: flex;
flex-direction: column;
margin-top: 4px;
`;

const BookItem = styled.li`
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid ${colors.darkgrey.dark};
`;

const Rank = styled.span`
font-size: ${typography.fontSize.base};
color: ${colors.white};
font-weight: ${typography.fontWeight.medium};
width: 24px;
`;

const Cover = styled.img`
width: 45px;
height: 60px;
object-fit: cover;
`;
Comment on lines +118 to +122

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

이미지 로딩 실패 처리를 추가해야 합니다.

외부 이미지 URL 사용 시 로딩 실패에 대한 fallback 처리가 필요합니다.

다음과 같이 개선하는 것을 권장합니다:

const Cover = styled.img`
  width: 45px;
  height: 60px;
  object-fit: cover;
+ background-color: ${colors.grey[400]};
`;

그리고 컴포넌트에서 onError 핸들러를 추가:

<Cover 
  src={book.coverUrl} 
  alt={`${book.title} 커버`}
  onError={(e) => {
    e.currentTarget.src = '/images/book-placeholder.jpg';
  }}
/>
🤖 Prompt for AI Agents
In src/components/search/MostSearchedBooks.tsx around lines 118 to 122, the
Cover styled image component lacks error handling for failed image loads. To fix
this, add an onError event handler to the Cover component usage that sets the
image source to a local fallback image (e.g., '/images/book-placeholder.jpg')
when the original image fails to load. This ensures a graceful fallback for
broken external image URLs.


const BookTitle = styled.span`
font-size: ${typography.fontSize.sm};
color: ${colors.white};
font-weight: ${typography.fontWeight.regular};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-left: 8px;
`;

const EmptyMessage = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
height: 60vh;
text-align: center;
`;

const MainText = styled.div`
font-size: ${typography.fontSize.lg};
color: ${colors.white};
font-weight: ${typography.fontWeight.semibold};
margin-bottom: 8px;
`;

const SubText = styled.div`
font-size: ${typography.fontSize.sm};
color: ${colors.grey[100]};
font-weight: ${typography.fontWeight.regular};
`;
15 changes: 12 additions & 3 deletions src/components/search/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import styled from '@emotion/styled';
import searchIcon from '../../assets/searchBar/search.svg';

import deleteIcon from '../../assets/searchBar/delete.svg';
import { IconButton } from '../common/IconButton';

Expand All @@ -10,9 +9,17 @@ interface SearchBarProps {
onChange?: (v: string) => void;
onClick?: () => void;
onSearch?: () => void;
isSearched?: boolean;
}

const SearchBar = ({ placeholder, value, onChange, onClick, onSearch }: SearchBarProps) => {
const SearchBar = ({
placeholder,
value,
onChange,
onClick,
onSearch,
isSearched,
}: SearchBarProps) => {
return (
<SearchBarWrapper onClick={onClick}>
<Input
Expand All @@ -26,7 +33,9 @@ const SearchBar = ({ placeholder, value, onChange, onClick, onSearch }: SearchBa
}}
/>
<IconButtonContainer>
{value && <IconButton src={deleteIcon} alt="입력 지우기" onClick={() => onChange?.('')} />}
{value && !isSearched && (
<IconButton src={deleteIcon} alt="입력 지우기" onClick={() => onChange?.('')} />
)}
<IconButton src={searchIcon} alt="검색" onClick={() => onSearch?.()} />
</IconButtonContainer>
</SearchBarWrapper>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/group/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ const Group = () => {
const closeCompletedGroupModal = () => setIsCompletedGroupModalOpen(false);

const handleSearchBarClick = () => {
navigate('/groupSearch');
navigate('/groupsearch');
};
return (
<Wrapper>
Expand Down
4 changes: 2 additions & 2 deletions src/pages/groupSearch/GroupSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom';
import SearchBar from '@/components/search/SearchBar';
import { useState } from 'react';
import RecentSearchTabs from '@/components/search/RecentSearchTabs';
import SearchResult from '@/components/search/SearchResult';
import GroupSearchResult from '@/components/search/GroupSearchResult';

const GroupSearch = () => {
const navigate = useNavigate();
Expand Down Expand Up @@ -57,7 +57,7 @@ const GroupSearch = () => {
}}
/>
{isSearching ? (
<SearchResult></SearchResult>
<GroupSearchResult></GroupSearchResult>
) : (
<RecentSearchTabs
recentSearches={recentSearches}
Expand Down
Loading