Skip to content

Commit c24f9a4

Browse files
authored
Merge pull request #288 from DguFarmSystem/fix/#287
[Feat] 블로그/프로젝트 페이지네이션 및 반응형 style 구현
2 parents fcaa2e6 + 0a58bf6 commit c24f9a4

File tree

17 files changed

+821
-89
lines changed

17 files changed

+821
-89
lines changed
619 Bytes
Loading
351 Bytes
Loading

apps/website/src/components/Header/Header.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ export default function Header() {
5454
<S.NavItem
5555
$isTablet={isTablet}
5656
$isMobile={isMobile}
57-
onClick={() => navigate('/blog')}
58-
isActive={location.pathname === '/blog'}
57+
onClick={() => navigate('/project')}
58+
isActive={location.pathname === '/project'}
5959
>
6060
블로그 / 프로젝트
6161
</S.NavItem>

apps/website/src/hooks/useBlog.ts

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
// useBlog.tsx
1+
// useBlog.ts
22
import { useState, useEffect } from "react";
3-
import { BlogGETResponse, BlogPOSTRequest, BlogPOSTResponse } from "@/models/blog";
4-
import { getBlogList, postBlog, getApprovedBlogList } from "@/services/blog";
3+
import { BlogGETResponse, BlogPOSTRequest, BlogPOSTResponse, BlogPage, PageableMeta, SortMeta } from "@/models/blog";
4+
import { getBlogList, postBlog, getApprovedBlogList, getBlogPageList } from "@/services/blog";
55
import { handleApiError } from "@/utils/handleApiError";
66

77
/**
@@ -72,11 +72,94 @@ export const useApprovedBlogList = () => {
7272
.then((response) => {
7373
setData(response);
7474
})
75-
.catch((err) => { setError(handleApiError(err)); })
75+
.catch((err) => setError(handleApiError(err)))
7676
.finally(() => {
7777
setLoading(false);
7878
});
7979
}, []);
8080

8181
return { data, loading, error };
8282
};
83+
84+
/**
85+
* 페이지 단위 블로그 조회를 위한 커스텀 훅 (페이지네이션 지원)
86+
* @param page 페이지 번호 (0부터 시작)
87+
* @param size 페이지 크기
88+
*/
89+
interface BlogPageQuery {
90+
page: number;
91+
size: number;
92+
}
93+
94+
// 페이지 정보 인터페이스
95+
interface BlogPageInfo {
96+
// 기본 페이지 정보
97+
currentPage: number;
98+
totalPages: number;
99+
totalElements: number;
100+
hasNextPage: boolean;
101+
hasPreviousPage: boolean;
102+
pageSize: number;
103+
numberOfElements: number;
104+
105+
// PageableMeta 정보
106+
pageable: PageableMeta;
107+
108+
// SortMeta 정보
109+
sort: SortMeta;
110+
111+
// 추가 플래그
112+
isFirst: boolean;
113+
isLast: boolean;
114+
isEmpty: boolean;
115+
}
116+
117+
export const useBlogPage = ({ page, size }: BlogPageQuery) => {
118+
const [data, setData] = useState<BlogPage | null>(null);
119+
const [pageInfo, setPageInfo] = useState<BlogPageInfo | null>(null);
120+
const [loading, setLoading] = useState(false);
121+
const [error, setError] = useState<Error | null>(null);
122+
123+
useEffect(() => {
124+
const fetchBlogPage = async () => {
125+
setLoading(true);
126+
try {
127+
const response = await getBlogPageList(page, size);
128+
if (response) {
129+
setData(response);
130+
131+
// PageableMeta와 SortMeta를 포함한 완전한 페이지 정보 추출
132+
setPageInfo({
133+
// 기본 페이지 정보
134+
currentPage: response.number,
135+
totalPages: response.totalPages,
136+
totalElements: response.totalElements,
137+
hasNextPage: !response.last,
138+
hasPreviousPage: !response.first,
139+
pageSize: response.size,
140+
numberOfElements: response.numberOfElements,
141+
142+
// PageableMeta 정보
143+
pageable: response.pageable,
144+
145+
// SortMeta 정보
146+
sort: response.sort,
147+
148+
// 추가 플래그
149+
isFirst: response.first,
150+
isLast: response.last,
151+
isEmpty: response.empty
152+
});
153+
}
154+
} catch (err) {
155+
setError(handleApiError(err));
156+
} finally {
157+
setLoading(false);
158+
}
159+
};
160+
161+
fetchBlogPage();
162+
}, [page, size]);
163+
164+
return { data, pageInfo, loading, error };
165+
};

apps/website/src/hooks/useLinkPreview.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const isValidResponse = (res: APIResponse | null): boolean => {
2626
// 프록시 서버 URL (CORS 헤더가 추가되어야 합니다)
2727
// corsproxy.io 는 요청 URL을 쿼리스트링으로 전달합니다.
2828
// const proxyUrl = 'https://corsproxy.io/?key=****&url=';
29-
const proxyUrl = 'https://corsproxy.io/'; // localhost용 프록시
29+
const proxyUrl = 'https://corsproxy.io/?url='; // localhost용 프록시
3030

3131
/**
3232
* extractMetaContent

apps/website/src/hooks/useProject.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const useProjectList = (
99
generation?: number,
1010
track?: Track,
1111
page: number = 0,
12-
size: number = 10
12+
size: number = 12
1313
) => {
1414
const [data, setData] = useState<Project[]>([]);
1515
const [pageInfo, setPageInfo] = useState<ProjectFilterResponse['pageInfo'] | null>(null);
@@ -21,6 +21,7 @@ export const useProjectList = (
2121
setLoading(true);
2222
try {
2323
const response: ProjectFilterApiResponse = await getFilteredProjects(generation, track, page, size);
24+
console.log("pageInfo", response.data?.pageInfo);
2425
if (response.data) {
2526
setData(response.data.content);
2627
setPageInfo(response.data.pageInfo);

apps/website/src/models/blog.ts

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ enum ApiErrorMessages {
55
}
66

77
export enum Track {
8+
ALL = "",
89
UNION = "UNION",
910
GAMING_VIDEO = "GAMING_VIDEO",
1011
AI = "AI",
@@ -13,6 +14,22 @@ enum ApiErrorMessages {
1314
BIGDATA = "BIGDATA",
1415
}
1516

17+
export interface SortMeta {
18+
unsorted: boolean;
19+
sorted: boolean;
20+
empty: boolean;
21+
}
22+
23+
export interface PageableMeta {
24+
pageNumber: number;
25+
pageSize: number;
26+
sort: SortMeta;
27+
offset: number;
28+
unpaged: boolean;
29+
paged: boolean;
30+
}
31+
32+
1633
interface ApiResponse<T = unknown> {
1734
status: number;
1835
message: ApiErrorMessages | "요청이 성공했습니다." | string;
@@ -27,13 +44,65 @@ enum ApiErrorMessages {
2744
link : string;
2845
}
2946

30-
interface BlogpageResquest {
31-
track : string;
32-
generation : number;
33-
page : number; // 페이징
47+
// 블로그 목록 조회 응답 데이터
48+
export interface BlogItem {
49+
blogId: number;
50+
link: string;
51+
category: string[];
52+
approvalStatus: string;
53+
}
54+
55+
interface BlogListResponse {
56+
content: BlogItem[];
57+
pageInfo: {
58+
pageSize: number;
59+
totalElements: number;
60+
currentPageElements: number;
61+
totalPages: number;
62+
currentPage: number;
63+
sortBy: string;
64+
hasNextPage: boolean;
65+
hasPreviousPage: boolean;
66+
};
67+
}
68+
69+
export interface BlogPage {
70+
content: BlogItem[];
71+
pageable: PageableMeta;
72+
73+
last: boolean;
74+
totalElements: number;
75+
totalPages: number;
76+
77+
sort: SortMeta;
78+
79+
first: boolean;
80+
number: number;
81+
numberOfElements: number;
82+
size: number;
83+
empty: boolean;
84+
}
85+
86+
export interface BlogPageInfo {
87+
currentPage: number;
88+
totalPages: number;
89+
totalElements: number;
90+
hasNextPage: boolean;
91+
hasPreviousPage: boolean;
92+
93+
pageSize: number;
94+
numberOfElements: number;
95+
96+
pageable: PageableMeta;
97+
sort: SortMeta;
98+
99+
isFirst: boolean;
100+
isLast: boolean;
101+
isEmpty: boolean;
34102
}
35103

36104
// blog에 POST 요청, POST 응답, GET요청
37105
export type BlogPOSTRequest = ApiRequest;
38-
export type BlogGETResponse = BlogpageResquest;
39-
export type BlogPOSTResponse = ApiResponse<{ blogId: number }>;
106+
export type BlogGETResponse = ApiResponse<BlogListResponse>;
107+
export type BlogPOSTResponse = ApiResponse<{ blogId: number }>;
108+
export type BlogPageResponse = ApiResponse<BlogPage>;

apps/website/src/pages/Blog/Blog/BlogItem.styles.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import styled from 'styled-components';
22

33
// 스타일 컴포넌트 (블로그랑 같지만, 다를 수 있으니께)
44
// 요긴 대충 짜서 해결
5-
export const Card = styled.a`
6-
width: 300px;
7-
height: 335px;
5+
export const Card = styled.a<{$isMobile: boolean; $isTablet: boolean;}>`
6+
width: ${(props) => (props.$isMobile ? '130px' : props.$isTablet ? '240px' : '300px')};
7+
height: ${(props) => (props.$isMobile ? '205px' : props.$isTablet ? '260px' : '335px')};
88
9-
border-radius: 8px;
9+
border-radius: ${(props) => (props.$isMobile ? '10px' : '8px')};
1010
overflow: hidden;
1111
1212
display: flex;
@@ -17,17 +17,16 @@ export const Card = styled.a`
1717
text-decoration: none; // 링크 밑줄 제거 등
1818
`;
1919

20-
export const Image = styled.div`
20+
export const Image = styled.div<{$isMobile: boolean; $isTablet: boolean;}>`
2121
width: 100%;
2222
2323
overflow: hidden;
2424
border-radius: 8px;
25-
height: 200px;
25+
height: ${(props) => (props.$isMobile ? '87px' : props.$isTablet ? '150px' : '200px')};
2626
background-color: var(--FarmSystem_LightGrey);
2727
2828
img{
29-
width: 100%;
30-
height: 200px;
29+
height: ${(props) => (props.$isMobile ? '87px' : props.$isTablet ? '150px' : '200px')};
3130
object-fit: cover;
3231
}
3332
`;
@@ -36,9 +35,9 @@ export const Content = styled.div`
3635
padding: 0px;
3736
`;
3837

39-
export const Title = styled.h3`
38+
export const Title = styled.h3<{$isMobile: boolean; $isTablet: boolean;}>`
4039
margin: 0px;
41-
font-size: 24px;
40+
font-size: ${(props) => (props.$isMobile ? '12px' : props.$isTablet ? '16px' : '24px')};
4241
font-weight: 700;
4342
4443
overflow: hidden;
@@ -47,8 +46,8 @@ export const Title = styled.h3`
4746
4847
`;
4948

50-
export const Description = styled.p`
51-
font-size: 15px;
49+
export const Description = styled.p<{$isMobile: boolean; $isTablet: boolean;}>`
50+
font-size: ${(props) => (props.$isMobile ? '10px' : props.$isTablet ? '15px' : '15px')};
5251
line-height: 20px;
5352
font-weight: 300;
5453
@@ -57,6 +56,8 @@ export const Description = styled.p`
5756
display: -webkit-box;
5857
-webkit-line-clamp: 2;
5958
-webkit-box-orient: vertical;
59+
60+
height: ${(props) => (props.$isMobile ? '60px' : props.$isTablet ? '40px' : '40px')}; /* line-height * 2 */
6061
`;
6162

6263
export const TagContainer = styled.div`
@@ -65,13 +66,14 @@ export const TagContainer = styled.div`
6566
gap: 10px;
6667
`;
6768

68-
export const Tag = styled.span`
69+
export const Tag = styled.span<{$isMobile: boolean; $isTablet: boolean;}>`
6970
background-color: var(--FarmSystem_Green06);
7071
color: var(--FarmSystem_White);
7172
7273
padding: 5px 10px;
7374
border-radius: 15px;
74-
font-size: 14px;
75+
font-size: ${(props) => (props.$isMobile ? '10px' : props.$isTablet ? '12px' : '14px')};
76+
7577
7678
font-weight: 300;
7779
line-height: 1.2;

apps/website/src/pages/Blog/Blog/BlogItem.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import * as S from './BlogItem.styles';
33
import { useLinkPreview } from '@/hooks/useLinkPreview';
4+
import useMediaQueries from '@/hooks/useMediaQueries';
45
import BlankImg from '../../../assets/Images/Blog_Project/blank_img.svg';
56

67
export enum BlogCategory {
@@ -19,7 +20,7 @@ export interface BlogTag {
1920

2021
export interface BlogItemProps {
2122
blogUrl: string;
22-
tags: BlogTag[];
23+
tags: BlogCategory[];
2324
}
2425

2526
// 카테고리 enum을 텍스트로 매핑
@@ -47,6 +48,7 @@ const getCategoryName = (category: BlogCategory): string => {
4748
const BlogItem: React.FC<BlogItemProps> = ({ blogUrl, tags }) => {
4849
// blogUrl을 기반으로 메타데이터를 fetching
4950
const { metadata, loading} = useLinkPreview(blogUrl);
51+
const { isMobile, isTablet } = useMediaQueries();
5052

5153

5254
// 메타데이터가 없는 경우 대비 기본값 설정
@@ -57,22 +59,22 @@ const BlogItem: React.FC<BlogItemProps> = ({ blogUrl, tags }) => {
5759
? metadata.image
5860
: BlankImg;
5961
return (
60-
<S.Card href={blogUrl} target="_blank">
61-
<S.Image>
62+
<S.Card href={blogUrl} target="_blank" $isMobile={isMobile} $isTablet={isTablet}>
63+
<S.Image $isMobile={isMobile} $isTablet={isTablet}>
6264
{loading ? (
6365
<img src={BlankImg} alt="로딩중..." />
6466
) : (
6567
<img src={previewImage} alt={title} />
6668
)}
6769
</S.Image>
6870
<S.Content>
69-
<S.Title>{loading ? '로딩중...' : title}</S.Title>
70-
<S.Description>
71+
<S.Title $isMobile={isMobile} $isTablet={isTablet}>{loading ? '로딩중...' : title}</S.Title>
72+
<S.Description $isMobile={isMobile} $isTablet={isTablet}>
7173
{loading ? '설명을 불러오는 중입니다...' : description}
7274
</S.Description>
7375
<S.TagContainer>
74-
{tags.map((tag, index) => (
75-
<S.Tag key={index}>{getCategoryName(tag.category)}</S.Tag>
76+
{tags.map((category, index) => (
77+
<S.Tag key={index} $isMobile={isMobile} $isTablet={isTablet}>{getCategoryName(category)}</S.Tag>
7678
))}
7779
</S.TagContainer>
7880
</S.Content>

0 commit comments

Comments
 (0)