Skip to content

Commit aebd82f

Browse files
authored
Merge pull request #192 from Moaguide-develop/feat/articleDetail
feat: 아티클 페이지 리뉴얼 작업
2 parents 582f3f2 + 4b5236d commit aebd82f

12 files changed

+285
-36
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import ArticleDetailClientWrapper from '@/components/learning/article/ArticleDetailClientWrapper';
2+
3+
interface PageProps {
4+
params: { articleId: string };
5+
}
6+
7+
export default async function ArticleDetailPage({ params }: PageProps) {
8+
const articleId = params.articleId;
9+
10+
return (
11+
<ArticleDetailClientWrapper
12+
articleId={Number(articleId)}
13+
/>
14+
);
15+
}

src/components/learning/FilteredContents.tsx

+28-13
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { useRouter } from 'next/navigation';
12
import Image from 'next/image';
23
import defaultImage from '../../../public/images/learning/learning_img.svg';
4+
import { FilteredResponse } from '@/types/filterArticle';
35

46
interface FilteredContentsProps {
5-
contents: any[];
7+
contents: FilteredResponse['content'];
68
total: number;
79
page: number;
810
size: number;
@@ -16,39 +18,53 @@ const FilteredContents = ({
1618
size,
1719
onPageChange,
1820
}: FilteredContentsProps) => {
21+
const router = useRouter();
1922
const totalPages = Math.ceil(total / size);
2023

2124
const formatDate = (dateString: string) => {
2225
const date = new Date(dateString);
23-
return date.toISOString().split('T')[0];
26+
return date.toISOString().split('T')[0];
2427
};
2528

29+
const getValidImageSrc = (imgLink: string | null) => {
30+
if (!imgLink || imgLink === '테스트') {
31+
return defaultImage;
32+
}
33+
if (imgLink.startsWith('http://') || imgLink.startsWith('https://')) {
34+
return imgLink;
35+
}
36+
return defaultImage;
37+
};
2638
return (
27-
<div className='mt-10'>
39+
<div className="mt-10">
2840
<div className="space-y-10">
2941
{contents.length > 0 ? (
3042
contents.map((item) => (
31-
<div key={item.contentId} className="flex items-center gap-4">
43+
<div
44+
key={item.article.articleId}
45+
className="flex items-center gap-4 cursor-pointer"
46+
onClick={() => router.push(`/learning/detail/${item.article.articleId}`)}
47+
>
3248
<div className="w-64 h-40 flex-shrink-0 overflow-hidden rounded-md">
33-
<Image
34-
src={item.img_link || defaultImage}
35-
alt={item.title}
49+
<Image
50+
src={getValidImageSrc(item.article.img_link)}
51+
alt={item.article.title}
3652
width={128}
3753
height={80}
3854
className="object-cover w-full h-full"
3955
/>
4056
</div>
4157
<div className="h-full flex-1">
4258
<h3 className="text-lg font-bold text-gray-800 mb-4 line-clamp-1">
43-
{item.title}
59+
{item.article.title}
4460
</h3>
4561
<p className="text-sm text-gray-600 line-clamp-2">
46-
{item.description}
62+
{item.article.description || '설명 없음'}
4763
</p>
4864
<div className="justify-end text-xs text-gray-500 mt-4 flex items-center gap-4">
49-
<span>{formatDate(item.date)}</span>
50-
<span>{item.likes}</span>
51-
<span>👁 {item.views}</span>
65+
<span>{formatDate(item.article.date)}</span>
66+
<span>{item.article.likes}</span>
67+
<span>👁 {item.article.views}</span>
5268
</div>
5369
</div>
5470
</div>
@@ -58,7 +74,6 @@ const FilteredContents = ({
5874
)}
5975
</div>
6076

61-
{/* 페이지네이션 */}
6277
{totalPages > 1 && (
6378
<div className="flex justify-center mt-8">
6479
<ul className="flex items-center space-x-1">

src/components/learning/LatestNewsClipping.tsx

+6-4
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import React, { useEffect, useState } from 'react';
44
import Image from 'next/image';
55
import defaultImage from '../../../public/images/learning/learning_img.svg';
66
import LatestNewsClippingSkeleton from '../skeleton/LatestNewsClippingSkeleton';
7+
import { useRouter } from 'next/navigation';
78

89
const LatestNewsClipping = ({ contents }: { contents: any[] }) => {
10+
const router = useRouter();
911
const [isMobile, setIsMobile] = useState<boolean | null>(null);
1012

1113
useEffect(() => {
@@ -34,7 +36,7 @@ const LatestNewsClipping = ({ contents }: { contents: any[] }) => {
3436
// 모바일 레이아웃
3537
<div className="sm:hidden">
3638
{contents[0] && (
37-
<div className="w-full overflow-hidden mb-4">
39+
<div className="w-full overflow-hidden mb-4 cursor-pointer" onClick={() => router.push(`/learning/detail/${contents[0].articleId}`)}>
3840
<Image
3941
src={contents[0].img_link || defaultImage}
4042
alt={contents[0].title}
@@ -61,7 +63,7 @@ const LatestNewsClipping = ({ contents }: { contents: any[] }) => {
6163
)}
6264
<div className="flex flex-col gap-4">
6365
{contents.slice(1, 5).map((content, index) => (
64-
<div key={index} className="flex gap-4 w-[90%] sm:w-[100%] mx-auto">
66+
<div key={index} className="flex gap-4 w-[90%] sm:w-[100%] mx-auto cursor-pointer" onClick={() => router.push(`/learning/detail/${content.articleId}`)}>
6567
<div className="w-20 h-20 flex-shrink-0 rounded-lg overflow-hidden bg-gray-200">
6668
<Image
6769
src={content.img_link || defaultImage}
@@ -99,7 +101,7 @@ const LatestNewsClipping = ({ contents }: { contents: any[] }) => {
99101
// 데스크톱 레이아웃
100102
<div className="hidden sm:flex gap-6">
101103
{contents[0] && (
102-
<div className="flex-1 shadow-sm overflow-hidden rounded-lg border">
104+
<div className="flex-1 shadow-sm overflow-hidden rounded-lg border cursor-pointer" onClick={() => router.push(`/learning/detail/${contents[0].articleId}`)}>
103105
<div className="bg-gray-200 rounded-t-lg overflow-hidden">
104106
<Image
105107
src={contents[0].img_link || defaultImage}
@@ -117,7 +119,7 @@ const LatestNewsClipping = ({ contents }: { contents: any[] }) => {
117119
)}
118120
<div className="flex flex-col gap-6 w-1/2">
119121
{contents.slice(1, 5).map((content, index) => (
120-
<div key={index} className="flex items-stretch gap-4">
122+
<div key={index} className="flex items-stretch gap-4 cursor-pointer" onClick={() => router.push(`/learning/detail/${content.articleId}`)}>
121123
<div className="w-32 flex-shrink-0 rounded-lg overflow-hidden bg-gray-200">
122124
<Image
123125
src={content.img_link || defaultImage}

src/components/learning/LearningPageClient.tsx

+25-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useState } from 'react';
3+
import React, { useState } from 'react';
44
import { useQuery } from '@tanstack/react-query';
55
import Image from 'next/image';
66
import PopularContents from '@/components/learning/PopularContents';
@@ -12,21 +12,24 @@ import ArrowIcon from '../../../public/images/learning/bottom_arrow_button.svg';
1212
import BackgroundImage from '../../../public/images/learning/learning_background.png';
1313
import SubscriptionBanner from './SubscriptionBanner';
1414
import { dropdownOptions } from '@/utils/dropdownOptions';
15+
import { OverviewResponse, Content } from '@/types/learning';
16+
import { FilteredContent, FilteredResponse } from '@/types/filterArticle';
1517

16-
const LearningPageClient = ({ initialData }: { initialData: any }) => {
18+
const LearningPageClient = ({ initialData }: { initialData: OverviewResponse }) => {
1719
const [selectedType, setSelectedType] = useState<string>('');
1820
const [selectedCategory, setSelectedCategory] = useState<string>('');
1921
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
2022
const [page, setPage] = useState<number>(1);
2123

22-
const fetchContentsWithPage = async () => {
24+
const fetchContentsWithPage = async (): Promise<FilteredResponse> => {
2325
const type = selectedType || 'all';
2426
const category = selectedCategory || 'all';
2527
const endpoint = `http://43.200.90.72/contents/list?type=${type}&category=${category}&page=${page}`;
2628
const response = await fetch(endpoint);
2729
if (!response.ok) throw new Error('API 호출 실패');
2830
return response.json();
2931
};
32+
3033

3134
const { data, isLoading } = useQuery({
3235
queryKey: ['contents', selectedType, selectedCategory, page],
@@ -57,6 +60,14 @@ const LearningPageClient = ({ initialData }: { initialData: any }) => {
5760
setActiveDropdown(null);
5861
};
5962

63+
const extractContents = (contents: Content[] | undefined) =>
64+
Array.isArray(contents)
65+
? contents.map((item) => ({
66+
...item.article,
67+
likedByMe: item.likedByMe,
68+
}))
69+
: [];
70+
6071
return (
6172
<div>
6273
<div className="relative w-full h-[300px] lg:h-[400px]">
@@ -67,8 +78,8 @@ const LearningPageClient = ({ initialData }: { initialData: any }) => {
6778
objectFit="cover"
6879
className="w-full"
6980
/>
70-
<div className="absolute bottom-0 left-0 right-0 flex justify-end items-center border-b shadow-sm z-50 bg-[#fffffc]/50">
71-
<button
81+
<div className="absolute bottom-0 left-0 right-0 flex justify-end items-center border-b shadow-sm z-50 bg-[#fffffc]/50">
82+
<button
7283
onClick={resetFilters}
7384
className="flex items-center gap-2 px-6 py-4 text-lg font-semibold text-gray-800"
7485
>
@@ -154,22 +165,22 @@ const LearningPageClient = ({ initialData }: { initialData: any }) => {
154165
<div className="max-w-[360px] mx-auto desk:max-w-[1000px] w-full sm:w-[90%] lg:w-[100%] mt-8">
155166
{!selectedType && !selectedCategory ? (
156167
<>
157-
<PopularContents contents={initialData.popularContents} />
158-
<RecentContents contents={initialData.recentContents} />
168+
<PopularContents contents={extractContents(initialData.popularContents)} />
169+
<RecentContents contents={extractContents(initialData.recentContents)} />
159170
<div className="hidden sm:flex w-full">
160-
<SubscriptionBanner/>
171+
<SubscriptionBanner />
161172
</div>
162-
<LatestNewsClipping contents={initialData.latestNewsClipping} />
173+
<LatestNewsClipping contents={extractContents(initialData.newsContents)} />
163174
</>
164175
) : isLoading ? (
165176
null
166177
) : (
167178
<FilteredContents
168-
contents={data?.content || []}
169-
total={data?.total || 0}
170-
page={page}
171-
size={data?.size || 5}
172-
onPageChange={(newPage) => setPage(newPage)}
179+
contents={data?.content || []}
180+
total={data?.total || 0}
181+
page={page}
182+
size={data?.size || 5}
183+
onPageChange={(newPage) => setPage(newPage)}
173184
/>
174185
)}
175186
</div>

src/components/learning/PopularContents.tsx

+10-3
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@ import defaultImage from '../../../public/images/learning/learning_img.svg';
88
import 'swiper/css';
99
import 'swiper/css/pagination';
1010
import PopularContentsSkeleton from '../skeleton/PopularContentsSkeleton';
11+
import { useRouter } from 'next/navigation';
1112

13+
/*
14+
todo: any 타입 변경
15+
*/
1216
const PopularContents = ({ contents }: { contents: any[] }) => {
17+
const router = useRouter();
1318
const [isMobile, setIsMobile] = useState<boolean | null>(null);
14-
19+
1520
useEffect(() => {
1621
const handleResize = () => {
1722
setIsMobile(window.innerWidth < 640);
@@ -45,13 +50,14 @@ const PopularContents = ({ contents }: { contents: any[] }) => {
4550
{contents.map((content, index) => (
4651
<SwiperSlide key={index}>
4752
<div
48-
className="relative w-full h-[500px] bg-cover bg-center"
53+
className="relative w-full h-[500px] bg-cover bg-center cursor-pointer"
4954
style={{
5055
backgroundImage: `url('${content.img_link || defaultImage.src}')`,
5156
backgroundSize: 'cover',
5257
backgroundPosition: 'center',
5358
backgroundRepeat: 'no-repeat',
5459
}}
60+
onClick={() => router.push(`/learning/detail/${content.articleId}`)}
5561
>
5662
<div className="absolute inset-0 bg-black bg-opacity-50 filter blur-lg"></div>
5763
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[270px] h-[300px]">
@@ -90,7 +96,8 @@ const PopularContents = ({ contents }: { contents: any[] }) => {
9096
{contents.map((content, index) => (
9197
<div
9298
key={index}
93-
className="border rounded-lg shadow-sm overflow-hidden flex flex-col bg-white"
99+
className="border rounded-lg shadow-sm overflow-hidden flex flex-col bg-white cursor-pointer"
100+
onClick={() => router.push(`/learning/detail/${content.articleId}`)}
94101
>
95102
<div className="relative w-full h-[180px]">
96103
<Image

src/components/learning/RecentContents.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import defaultImage from '../../../public/images/learning/learning_img.svg';
88
import 'swiper/css';
99
import 'swiper/css/pagination';
1010
import RecentContentsSkeleton from '../skeleton/RecentContentsSkeleton';
11+
import { useRouter } from 'next/navigation';
1112

1213
const RecentContents = ({ contents }: { contents: any[] }) => {
14+
const router = useRouter();
1315
const [isMobile, setIsMobile] = useState<boolean | null>(null);
1416

1517
useEffect(() => {
@@ -45,7 +47,7 @@ const RecentContents = ({ contents }: { contents: any[] }) => {
4547
>
4648
{contents.map((content, index) => (
4749
<SwiperSlide key={index} className="relative">
48-
<div className="relative w-full h-[350px] bg-white shadow-lg rounded-lg overflow-hidden">
50+
<div className="relative w-full h-[350px] bg-white shadow-lg rounded-lg overflow-hidden cursor-pointer" onClick={() => router.push(`/learning/detail/${content.articleId}`)}>
4951
<Image
5052
src={content.img_link || defaultImage}
5153
alt={content.title}
@@ -72,7 +74,8 @@ const RecentContents = ({ contents }: { contents: any[] }) => {
7274
{contents.map((content, index) => (
7375
<div
7476
key={index}
75-
className="border rounded-lg shadow-sm overflow-hidden flex flex-col bg-white"
77+
className="border rounded-lg shadow-sm overflow-hidden flex flex-col bg-white cursor-pointer"
78+
onClick={() => router.push(`/learning/detail/${contents[0].articleId}`)}
7679
>
7780
<div className="relative w-full h-[180px]">
7881
<Image
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
import { useRouter } from 'next/navigation';
5+
import ArticleDetailHeader from '@/components/learning/article/ArticleDetailHeader';
6+
import ArticleDetailContent from '@/components/learning/article/ArticleDetailContent';
7+
import { getArticleDetail } from '@/factory/Article/GetArticle';
8+
import { ArticleDetail } from '@/types/learning';
9+
import { useAuthStore } from '@/store/userAuth.store';
10+
11+
interface ArticleDetailClientWrapperProps {
12+
articleId: number;
13+
}
14+
15+
const ArticleDetailClientWrapper = ({ articleId }: ArticleDetailClientWrapperProps) => {
16+
const { isLoggedIn } = useAuthStore();
17+
const router = useRouter();
18+
const [data, setData] = useState<ArticleDetail | null>(null);
19+
20+
useEffect(() => {
21+
if (!isLoggedIn) {
22+
alert('로그인이 필요한 서비스입니다.');
23+
router.push('/sign');
24+
return;
25+
}
26+
27+
const fetchData = async () => {
28+
try {
29+
const result = await getArticleDetail(articleId);
30+
setData(result);
31+
} catch (error) {
32+
console.error('데이터를 가져오는 중 오류 발생:', error);
33+
}
34+
};
35+
36+
fetchData();
37+
}, [isLoggedIn, articleId, router]);
38+
39+
if (!data) {
40+
return null;
41+
}
42+
43+
return (
44+
<div>
45+
<ArticleDetailHeader
46+
categoryName={data.categoryName}
47+
title={data.title}
48+
createdAt={data.createdAt}
49+
authorName={data.authorName}
50+
imgLink={data.imgLink}
51+
/>
52+
<div className="max-w-[1000px] mx-auto my-6 text-sm text-gray-600">
53+
학습하기 &gt; 아티클 &gt; {data.categoryName}
54+
</div>
55+
<ArticleDetailContent
56+
text={data.text}
57+
createdAt={data.createdAt}
58+
authorName={data.authorName}
59+
/>
60+
</div>
61+
);
62+
};
63+
64+
export default ArticleDetailClientWrapper;

0 commit comments

Comments
 (0)