Skip to content

Commit ad8282c

Browse files
authored
Merge pull request #274 from DguFarmSystem/feat/#273
feat: 프로젝트 API 연결 완료
2 parents 469372c + 2e6c8d7 commit ad8282c

File tree

13 files changed

+518
-85
lines changed

13 files changed

+518
-85
lines changed

apps/website/src/Router.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import NewsDetail from '@/pages/News/NewsDetail';
99
import FAQ from '@/pages/FAQ';
1010
import MaintainPage from '@/pages/MaintainPage';
1111
import RedirectRoute from '@/components/RedirectRoute';
12+
import ProjectDetail from './pages/Blog/Project/ProjectDetail';
1213

1314
const IS_MAINTENANCE = false; // 유지보수 모드 ON/OFF 설정은 여기서 해주시면 됩니다.
1415
const IS_RECRUIT = false; // 모집 모드 ON/OFF 설정은 여기서 해주시면 됩니다.
@@ -25,7 +26,9 @@ export default function Router() {
2526
children: [
2627
{ path: '/', element: <Main /> },
2728
{ path: '/recruit', element: <RedirectRoute boolean={IS_RECRUIT}><Recruit /></RedirectRoute>},
28-
{ path: '/blog', element: <Blog /> },
29+
{ path: '/project', element: <Blog/> },
30+
{ path: '/project/:projectId', element: <ProjectDetail /> },
31+
{ path: '/blog', element: <Blog/> },
2932
{ path: '/news', element: <News /> },
3033
{ path: '/news/:newsId', element: <NewsDetail /> },
3134
{ path: '/FAQ', element: <FAQ /> },
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { useState, useEffect } from 'react';
2+
import { handleApiError } from '@/utils/handleApiError';
3+
import { getFilteredProjects, getProjectById } from '@/services/project';
4+
import { Project, ProjectFilterResponse, ProjectFilterApiResponse, ProjectDetailResponse } from '@/models/project';
5+
import { Track } from '@/models/blog';
6+
7+
/** 필터링된 프로젝트 목록 불러오기 */
8+
export const useProjectList = (
9+
generation?: number,
10+
track?: Track,
11+
page: number = 0,
12+
size: number = 10
13+
) => {
14+
const [data, setData] = useState<Project[]>([]);
15+
const [pageInfo, setPageInfo] = useState<ProjectFilterResponse['pageInfo'] | null>(null);
16+
const [loading, setLoading] = useState<boolean>(true);
17+
const [error, setError] = useState<Error | null>(null);
18+
19+
useEffect(() => {
20+
const fetchProjects = async () => {
21+
setLoading(true);
22+
try {
23+
const response: ProjectFilterApiResponse = await getFilteredProjects(generation, track, page, size);
24+
if (response.data) {
25+
setData(response.data.content);
26+
setPageInfo(response.data.pageInfo);
27+
}
28+
} catch (err) {
29+
const errorObj = handleApiError(err);
30+
setError(errorObj);
31+
} finally {
32+
setLoading(false);
33+
}
34+
};
35+
36+
fetchProjects();
37+
}, [generation, track, page, size]);
38+
39+
return { data, pageInfo, loading, error };
40+
};
41+
42+
/** 프로젝트 상세 불러오기 */
43+
export const useProjectDetail = (projectId: number) => {
44+
const [data, setData] = useState<ProjectDetailResponse['data'] | null>(null);
45+
const [loading, setLoading] = useState<boolean>(true);
46+
const [error, setError] = useState<Error | null>(null);
47+
48+
useEffect(() => {
49+
const fetchProjectDetail = async () => {
50+
setLoading(true);
51+
try {
52+
const response = await getProjectById(projectId);
53+
if (response.data) {
54+
setData(response.data);
55+
}
56+
} catch (err) {
57+
const errorObj = handleApiError(err);
58+
setError(errorObj);
59+
} finally {
60+
setLoading(false);
61+
}
62+
};
63+
64+
fetchProjectDetail();
65+
}, [projectId]);
66+
67+
return { data, loading, error };
68+
};

apps/website/src/models/blog.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ enum ApiErrorMessages {
44
ALREADY_SUBMITTED = "이미 승인 처리된 블로그입니다.",
55
}
66

7-
enum Track {
7+
export enum Track {
88
UNION = "UNION",
99
GAMING_VIDEO = "GAMING_VIDEO",
1010
AI = "AI",

apps/website/src/models/project.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Track } from './blog';
2+
3+
export interface Project {
4+
projectId: number;
5+
title: string;
6+
introduction: string;
7+
thumbnailImageUrl: string;
8+
track: Track;
9+
generation: number;
10+
}
11+
12+
export interface ProjectFilterResponse {
13+
content: Project[];
14+
pageInfo: {
15+
pageSize: number;
16+
totalElements: number;
17+
currentPageElements: number;
18+
totalPages: number;
19+
currentPage: number;
20+
sortBy: string;
21+
hasNextPage: boolean;
22+
hasPreviousPage: boolean;
23+
};
24+
}
25+
26+
export interface DetailedProjectResponse {
27+
projectId: number;
28+
title: string;
29+
introduction: string;
30+
content: string;
31+
thumbnailImageUrl: string;
32+
bodyImageUrl: string;
33+
githubLink: string;
34+
deploymentLink: string;
35+
resourceLink: string;
36+
participants: string[];
37+
approvalStatus: string;
38+
track: Track;
39+
}
40+
41+
interface ApiResponse<T = unknown> {
42+
status: number;
43+
message: "요청이 성공했습니다." | string;
44+
data?: T;
45+
}
46+
47+
//GET요청
48+
export type ProjectDetailResponse = ApiResponse<DetailedProjectResponse>;
49+
export type ProjectFilterApiResponse = ApiResponse<ProjectFilterResponse>;
50+

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const BlogList: React.FC = () => {
4949
</S.SubDescription>
5050
</S.TableContainer>
5151

52-
{blogData.length <= 5 ? (
52+
{blogData.length <= 7 ? (
5353
<S.TextContainer $isMobile={isMobile}>
5454
아직 등록된 글이 없어요.
5555
<a href="/create">파밍로그를 통해 글을 작성해보세요!</a>
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import styled from 'styled-components';
2+
3+
interface ContainerProps {
4+
$isMobile: boolean;
5+
$isTablet: boolean;
6+
$isDesktop: boolean;
7+
}
8+
9+
export const Container = styled.div<ContainerProps>`
10+
padding: ${({ $isMobile }) => ($isMobile ? "100px 8px": "100px 20px")};
11+
display: flex;
12+
flex-direction: column;
13+
justify-content: start;
14+
align-items: center;
15+
16+
width: 100%;
17+
min-height: 100vh;
18+
position: relative;
19+
overflow-y: auto;
20+
`;
21+
22+
export const ProjectPageTitle = styled.h2<ContainerProps>`
23+
display: flex;
24+
justify-content: flex-start;
25+
align-items: flex-start;
26+
color: var(--FarmSystem_Green01, #28723F);
27+
font-size: ${({ $isMobile }) => ($isMobile ? "32px": "40px")};
28+
font-style: normal;
29+
font-weight: 700;
30+
line-height: 40px; /* 100% */
31+
32+
padding: 10px 0;
33+
width: 100%;
34+
max-width: 1000px;
35+
margin-top: ${({ $isMobile }) => ($isMobile ? "0": "20px")};;
36+
margin-bottom: ${({ $isMobile }) => ($isMobile ? "30px": "70px")};
37+
`;
38+
39+
export const LoadingContainer = styled.div`
40+
display: flex;
41+
justify-content: center;
42+
align-items: center;
43+
min-height: 400px;
44+
font-size: 18px;
45+
color: var(--FarmSystem_Black);
46+
`;
47+
48+
export const ErrorContainer = styled.div`
49+
display: flex;
50+
justify-content: center;
51+
align-items: center;
52+
min-height: 400px;
53+
font-size: 18px;
54+
color: var(--FarmSystem_Red);
55+
`;
56+
57+
export const Section = styled.section`
58+
margin-bottom: 40px;
59+
`;
60+
61+
export const SectionTitle = styled.h2`
62+
font-size: 24px;
63+
font-weight: 600;
64+
color: var(--FarmSystem_Black);
65+
margin-bottom: 16px;
66+
`;
67+
68+
export const SectionContent = styled.div`
69+
font-size: 16px;
70+
line-height: 1.6;
71+
color: var(--FarmSystem_Black);
72+
white-space: pre-wrap;
73+
`;
74+
75+
export const SectionImage = styled.img`
76+
width: 100%;
77+
max-width: 800px;
78+
height: auto;
79+
border-radius: 8px;
80+
margin: 20px 0;
81+
`;
82+
83+
export const ParticipantsList = styled.div`
84+
display: flex;
85+
flex-wrap: wrap;
86+
gap: 12px;
87+
`;
88+
89+
export const Participant = styled.span`
90+
background-color: var(--FarmSystem_Green06);
91+
color: var(--FarmSystem_White);
92+
padding: 8px 16px;
93+
border-radius: 20px;
94+
font-size: 14px;
95+
`;
96+
97+
export const LinkSection = styled.div`
98+
display: flex;
99+
gap: 16px;
100+
margin-top: 40px;
101+
`;
102+
103+
export const LinkButton = styled.a`
104+
display: inline-block;
105+
padding: 12px 24px;
106+
background-color: var(--FarmSystem_Green01);
107+
color: var(--FarmSystem_White);
108+
border-radius: 8px;
109+
text-decoration: none;
110+
font-weight: 500;
111+
transition: background-color 0.2s;
112+
113+
&:hover {
114+
background-color: var(--FarmSystem_Green02);
115+
}
116+
`;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { useParams } from 'react-router';
2+
import useMediaQueries from '@/hooks/useMediaQueries';
3+
import { getTrackName } from './ProjectItem';
4+
import * as S from './ProjectDetail.styles';
5+
import { useEffect, useState } from 'react';
6+
import { DetailedProjectResponse } from '@/models/project';
7+
import { getProjectById } from '@/services/project';
8+
import DetailLayout from '@/layouts/DetailLayout/DetailLayout';
9+
import Logger from '@/utils/Logger';
10+
11+
const ProjectDetail: React.FC = () => {
12+
const { projectId } = useParams<{ projectId: string }>();
13+
const { isMobile, isTablet, isDesktop } = useMediaQueries();
14+
const [project, setProject] = useState<DetailedProjectResponse | null>(null);
15+
const [loading, setLoading] = useState(true);
16+
const [error, setError] = useState<Error | null>(null);
17+
18+
useEffect(() => {
19+
const fetchProject = async () => {
20+
try {
21+
if (!projectId) return;
22+
const response = await getProjectById(parseInt(projectId));
23+
console.log('Fetched Project Data:', response);
24+
25+
if (response.data) {
26+
const projectData = response.data;
27+
setProject(projectData);
28+
}
29+
} catch (err) {
30+
console.error('Error fetching project:', err);
31+
setError(err instanceof Error ? err : new Error('프로젝트를 불러오는데 실패했습니다.'));
32+
Logger.error(err);
33+
} finally {
34+
setLoading(false);
35+
}
36+
};
37+
38+
fetchProject();
39+
}, [projectId]);
40+
41+
if (loading) {
42+
return (
43+
<S.Container $isMobile={isMobile} $isTablet={isTablet} $isDesktop={isDesktop}>
44+
로딩 중...
45+
</S.Container>
46+
);
47+
}
48+
49+
if (error || !project) {
50+
return (
51+
<S.Container $isMobile={isMobile} $isTablet={isTablet} $isDesktop={isDesktop}>
52+
에러가 발생했습니다: {error?.message}
53+
</S.Container>
54+
);
55+
}
56+
57+
return (
58+
<S.Container $isMobile={isMobile} $isTablet={isTablet} $isDesktop={isDesktop}>
59+
<S.ProjectPageTitle $isMobile={isMobile} $isTablet={isTablet} $isDesktop={isDesktop}>
60+
프로젝트
61+
</S.ProjectPageTitle>
62+
<DetailLayout
63+
title={project.title}
64+
content={`${project.introduction}\n\n${project.content}`}
65+
tag={getTrackName(project.track)}
66+
thumbnailUrl={project.thumbnailImageUrl}
67+
imageUrls={project.bodyImageUrl ? [project.bodyImageUrl] : []}
68+
date={project.githubLink}
69+
/>
70+
</S.Container>
71+
);
72+
};
73+
74+
export default ProjectDetail;

apps/website/src/pages/Blog/Project/ProjectItem.style.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ export const Description = styled.p`
4646
font-size: 15px;
4747
line-height: 20px;
4848
font-weight: 300;
49+
50+
display: -webkit-box;
51+
-webkit-line-clamp: 2;
52+
-webkit-box-orient: vertical;
53+
overflow: hidden;
54+
text-overflow: ellipsis;
55+
height: 40px; /* line-height * 2 */
4956
`;
5057

5158
export const TagContainer = styled.div`

0 commit comments

Comments
 (0)