Skip to content

Commit 09efaa3

Browse files
authored
[이수지] sprint11 (#132)
* fix: hasImage prop 오류로 인한 라이브러리 설지 및 수정 * fix: react, react dom 라이브러리 버전 변경(18 -> 18.2.0) React의 18버전에서 `fetchPriority` prop이 DOM 요소에서 인식되지 않는 버그로 인함 * refactor: 이미지 업로드 관련 함수들 커스텀훅으로 분리 * fix: svg를 컴포넌트화해서 사용하는 부분에 오류가 있어, Image 컴포넌트로 수정 * feat: 로그인 기능 구현 * feat: 토큰 유무에 따라 프로필 이미지 렌더링 처리 --------- Co-authored-by: Suzy-Lee <>
1 parent bf77f21 commit 09efaa3

19 files changed

+360
-139
lines changed

api/itemApi.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { ProductListFetcherParams } from '@/types/productTypes';
2+
3+
export async function getProducts({
4+
orderBy,
5+
pageSize,
6+
page = 1,
7+
}: ProductListFetcherParams) {
8+
const params = new URLSearchParams({
9+
orderBy,
10+
pageSize: String(pageSize),
11+
page: String(page),
12+
});
13+
14+
try {
15+
const response = await fetch(
16+
`http://panda-market-api.vercel.app/products?${params}`
17+
);
18+
if (!response.ok) {
19+
throw new Error(`HTTP error: ${response.status}`);
20+
}
21+
22+
const body = await response.json();
23+
return body;
24+
} catch (error) {
25+
console.error('Failed to fetch products:', error);
26+
throw error;
27+
}
28+
}
29+
30+
export async function getProductDetail(productId: number) {
31+
if (!productId) {
32+
throw new Error('상품 아이디가 유효가지 않습니다');
33+
}
34+
35+
try {
36+
const response = await fetch(
37+
`https://panda-market-api.vercel.app/products/${productId}`
38+
);
39+
if (!response.ok) {
40+
throw new Error(`HTTP error: ${response.status}`);
41+
}
42+
43+
const body = await response.json();
44+
return body;
45+
} catch (error) {
46+
console.error('Failed to fetch product detail:', error);
47+
throw error;
48+
}
49+
}
50+
51+
export async function getProductComments({
52+
productId,
53+
limit = 10,
54+
}: {
55+
productId: number;
56+
limit?: number;
57+
}) {
58+
const params = new URLSearchParams({
59+
limit: String(limit),
60+
productId: String(productId),
61+
});
62+
63+
try {
64+
const query = new URLSearchParams(params).toString();
65+
const response = await fetch(
66+
`https://panda-market-api.vercel.app/products/${productId}/comments?${query}`
67+
);
68+
if (!response.ok) {
69+
throw new Error(`HTTP error: ${response.status}`);
70+
}
71+
72+
const body = await response.json();
73+
return body;
74+
} catch (error) {
75+
console.error('Failed to fetch product comments:', error);
76+
throw error;
77+
}
78+
}

components/boards/BestArticlesSection.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ const BestArticleCard = ({ article }: { article: Article }) => {
4949
return (
5050
<CardContainer href={`/boards/${article.id}`}>
5151
<BestSticker>
52-
<MedalIcon alt="베스트 게시글" />
52+
{/* <MedalIcon alt="베스트 게시글" /> */}
53+
<Image src={MedalIcon} alt="베스트 게시글" />
5354
Best
5455
</BestSticker>
5556

components/items/itemPage/CommentThread.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
ProductCommentListResponse,
1111
} from "@/types/commentTypes";
1212
import EmptyState from "@/components/ui/EmptyState";
13+
import Image from "next/image";
1314

1415
const CommentContainer = styled.div`
1516
padding: 24px 0;
@@ -64,7 +65,8 @@ const CommentItem: React.FC<CommentItemProps> = ({ item }) => {
6465
<CommentContainer>
6566
{/* 참고: 더보기 버튼 기능은 추후 요구사항에 따라 추가 예정 */}
6667
<SeeMoreButton>
67-
<SeeMoreIcon />
68+
{/* <SeeMoreIcon /> */}
69+
<Image src={SeeMoreIcon} alt="더보기" />
6870
</SeeMoreButton>
6971

7072
<CommentContent>{item.content}</CommentContent>

components/items/itemPage/ItemProfileSection.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ const ItemProfileSection: React.FC<ItemProfileSectionProps> = ({ product }) => {
126126
<MainDetails>
127127
{/* 참고: 더보기 버튼 기능은 추후 요구사항에 따라 추가 예정 */}
128128
<SeeMoreButton>
129-
<SeeMoreIcon />
129+
{/* <SeeMoreIcon /> */}
130+
<Image src={SeeMoreIcon} alt="더보기" />
130131
</SeeMoreButton>
131132

132133
<div>

components/layout/Header.tsx

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import Logo from "@/public/images/logo/logo.svg";
2-
import Link from "next/link";
3-
import { useRouter } from "next/router";
4-
import styled from "styled-components";
5-
import { StyledLink } from "@/styles/CommonStyles";
1+
import Logo from '@/public/images/logo/logo.svg';
2+
import Link from 'next/link';
3+
import { useRouter } from 'next/router';
4+
import styled from 'styled-components';
5+
import { StyledLink } from '@/styles/CommonStyles';
6+
import Image from 'next/image';
7+
import { useEffect, useState } from 'react';
8+
import DefaultProfile from '@/public/images/ui/ic_profile.svg';
69

710
const GlobalHeader = styled.header`
811
display: flex;
@@ -15,8 +18,7 @@ const HeaderLeft = styled.div`
1518
align-items: center;
1619
`;
1720

18-
// Next.js에서는 next/link의 Link 컴포넌트를 사용해 주세요.
19-
// 참고: Next.js 버전 13부터는 Link 자체가 anchor 태그의 역할을 해요. Link 요소 내에 <a> 태그가 중첩되어 있으면 hydration 오류가 발생하니 주의해 주세요.
21+
2022
const HeaderLogo = styled(Link)`
2123
margin-right: 16px;
2224
@@ -52,31 +54,38 @@ const NavItem = styled.li`
5254
const LoginLink = styled(StyledLink)``;
5355

5456
function getLinkStyle(isActive: boolean) {
55-
return { color: isActive ? "var(--blue)" : undefined };
57+
return { color: isActive ? 'var(--blue)' : undefined };
5658
}
5759

5860
const Header: React.FC = () => {
5961
const { pathname } = useRouter();
62+
const [isLoggedIn, setIsLoggedIn] = useState(false);
63+
64+
useEffect(() => {
65+
// 컴포넌트가 마운트될 때 sessionStorage에서 토큰을 확인
66+
const token = sessionStorage.getItem('token');
67+
setIsLoggedIn(!!token); // 토큰이 있으면 true, 없으면 false 설정
68+
}, []);
6069

6170
return (
6271
<GlobalHeader>
6372
<HeaderLeft>
64-
<HeaderLogo href="/" aria-label="홈으로 이동">
65-
<Logo alt="판다마켓 로고" width="153" />
73+
<HeaderLogo href='/' aria-label='홈으로 이동'>
74+
<Image src={Logo} alt='leftBtn' width={153} height={51} />
6675
</HeaderLogo>
6776

6877
<nav>
6978
<NavList>
7079
<NavItem>
71-
<Link href="/boards" style={getLinkStyle(pathname === "/boards")}>
80+
<Link href='/boards' style={getLinkStyle(pathname === '/boards')}>
7281
자유게시판
7382
</Link>
7483
</NavItem>
7584
<NavItem>
7685
<Link
77-
href="/items"
86+
href='/items'
7887
style={getLinkStyle(
79-
pathname.includes("/items") || pathname === "/additem"
88+
pathname.includes('/items') || pathname === '/additem'
8089
)}
8190
>
8291
중고마켓
@@ -85,8 +94,12 @@ const Header: React.FC = () => {
8594
</NavList>
8695
</nav>
8796
</HeaderLeft>
88-
89-
<LoginLink href="/login">로그인</LoginLink>
97+
{isLoggedIn ? (
98+
// 로그인이 되어 있으면 판다 이미지 렌더링
99+
<Image src={DefaultProfile} alt='판다마켓 기본 프로필' width={40} height={40} />
100+
) : (
101+
<LoginLink href='/login'>로그인</LoginLink>
102+
)}
90103
</GlobalHeader>
91104
);
92105
};

components/ui/DropdownMenu.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState } from "react";
22
import styled from "styled-components";
33
import SortIcon from "@/public/images/icons/ic_sort.svg";
4+
import Image from "next/image";
45

56
const SortButtonWrapper = styled.div`
67
position: relative;
@@ -54,7 +55,8 @@ const DropdownMenu: React.FC<DropdownMenuProps> = ({
5455
return (
5556
<SortButtonWrapper>
5657
<SortDropdownTriggerButton onClick={toggleDropdown}>
57-
<SortIcon alt="정렬" />
58+
{/* <SortIcon alt="정렬" /> */}
59+
<Image src={SortIcon} alt="정렬" />
5860
</SortDropdownTriggerButton>
5961

6062
{isDropdownVisible && (

components/ui/Icon.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import styled from "styled-components";
1+
import Image from 'next/image';
2+
import styled from 'styled-components';
23

34
// SVG 이미지 형식을 이용하는 큰 장점 중 하나는 비슷한 아이콘을 색상별로 저장할 필요 없이 하나의 파일로 유연하게 변경할 수 있다는 거예요.
45
// SVG 파일 내에 정의된 크기 또는 선 및 배경색을 동적으로 변경할 수 있는 컴포넌트를 만들어 볼게요.
@@ -15,21 +16,21 @@ const IconWrapper = styled.div<IconWrapperProps>`
1516
justify-content: center;
1617
1718
svg {
18-
fill: ${({ $fillColor }) => $fillColor || "current"}; // 색 채움
19-
width: ${({ $size }) => ($size ? `${$size}px` : "auto")};
20-
height: ${({ $size }) => ($size ? `${$size}px` : "auto")};
19+
fill: ${({ $fillColor }) => $fillColor || 'current'}; // 색 채움
20+
width: ${({ $size }) => ($size ? `${$size}px` : 'auto')};
21+
height: ${({ $size }) => ($size ? `${$size}px` : 'auto')};
2122
}
2223
2324
/* 선(stroke)의 색상 변경은 svg 내의 path 요소에 넣어줘야 적용돼요 */
2425
/* - 채움색이 있을 때는 아웃라인 색상도 함께 바꿔주는 것이 일반적이기 때문에 선에 대한 속성이지만 fillColor를 outlineColor보다 우선적으로 적용하도록 했어요 */
2526
svg path {
2627
stroke: ${({ $fillColor, $outlineColor }) =>
27-
$fillColor || $outlineColor || "currentColor"};
28+
$fillColor || $outlineColor || 'currentColor'};
2829
}
2930
`;
3031

3132
interface IconProps {
32-
iconComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
33+
iconComponent: string;
3334
size?: number;
3435
fillColor?: string;
3536
outlineColor?: string;
@@ -40,11 +41,12 @@ const Icon: React.FC<IconProps> = ({
4041
// - 통일성을 위해 prop 이름에는 camelCase를 사용했지만, SVG를 ReactComponent 형태로 전달하고 있기 때문에 PascalCase으로 바꿔 사용
4142
iconComponent: IconComponent,
4243
size, // Optional props이며, prop이 생략될 경우 SVG 파일의 기본 크기를 사용함
43-
fillColor = "currentColor",
44-
outlineColor = "currentColor",
44+
fillColor = 'currentColor',
45+
outlineColor = 'currentColor',
4546
}) => (
4647
<IconWrapper $size={size} $fillColor={fillColor} $outlineColor={outlineColor}>
47-
<IconComponent />
48+
{/* <IconComponent /> */}
49+
<Image src={IconComponent} alt='더보기' />
4850
</IconWrapper>
4951
);
5052

components/ui/LikeCountDisplay.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from "react";
22
import styled from "styled-components";
33
import HeartIcon from "@/public/images/icons/ic_heart.svg";
44
import { FlexRowCentered } from "@/styles/CommonStyles";
5+
import Image from "next/image";
56

67
const LikeCountWrapper = styled(FlexRowCentered)<{
78
$fontSize: number;
@@ -37,7 +38,8 @@ const LikeCountDisplay: React.FC<LikeCountDisplayProps> = ({
3738

3839
return (
3940
<LikeCountWrapper className={className} $fontSize={fontSize} $gap={gap}>
40-
<HeartIcon width={iconWidth} alt="좋아요 아이콘" />
41+
{/* <HeartIcon width={iconWidth} alt="좋아요 아이콘" /> */}
42+
<Image src={HeartIcon} width={iconWidth} alt="좋아요 아이콘" />
4143
{displayCount}
4244
</LikeCountWrapper>
4345
);

components/ui/PaginationBar.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import styled from "styled-components";
22
import LeftArrow from "@/public/images/icons/arrow_left.svg";
33
import RightArrow from "@/public/images/icons/arrow_right.svg";
4+
import Image from "next/image";
45

56
const PaginationBarContainer = styled.div`
67
display: flex;
@@ -59,7 +60,8 @@ const PaginationBar: React.FC<PaginationBarProps> = ({
5960
onClick={() => onPageChange(activePageNum - 1)}
6061
aria-label="이전 페이지로 이동 버튼"
6162
>
62-
<LeftArrow />
63+
{/* <LeftArrow /> */}
64+
<Image src={LeftArrow} alt='이전 페이지로 이동 버튼 이미지' />
6365
</PaginationButton>
6466
{pages.map((page) => (
6567
<PaginationButton
@@ -75,7 +77,8 @@ const PaginationBar: React.FC<PaginationBarProps> = ({
7577
onClick={() => onPageChange(activePageNum + 1)}
7678
aria-label="다음 페이지로 이동 버튼"
7779
>
78-
<RightArrow />
80+
{/* <RightArrow /> */}
81+
<Image src={RightArrow} alt="다음페이지로 이동버튼 이미지" />
7982
</PaginationButton>
8083
</PaginationBarContainer>
8184
);

components/ui/SearchBar.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { FlexRowCentered } from "@/styles/CommonStyles";
33
import SearchIcon from "@/public/images/icons/ic_search.svg";
44
import { useEffect, useState } from "react";
55
import { useRouter } from "next/router";
6+
import Image from "next/image";
67

78
const Container = styled(FlexRowCentered)`
89
background-color: var(--gray-100);
@@ -57,7 +58,8 @@ const SearchBar: React.FC<SearchBarProps> = ({
5758

5859
return (
5960
<Container>
60-
<SearchIcon alt="검색" />
61+
{/* <SearchIcon alt="검색" /> */}
62+
<Image src={SearchIcon} alt="검색" />
6163
<SearchBarInput
6264
value={keyword}
6365
onChange={handleInputChange}

0 commit comments

Comments
 (0)