Skip to content

Commit 501e53c

Browse files
authored
feat: 뉴스 페이지 Ui 변경 & 컴포넌트 분리 (DASOMFE-32)
* feat: 뉴스 페이지 Ui 변경 & 컴포넌트 분리 (DASOMFE-32) * fix: 컴포넌트 폰트 적용 (DASOMFE-32)
1 parent 4989239 commit 501e53c

File tree

11 files changed

+280
-169
lines changed

11 files changed

+280
-169
lines changed

src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ function AppContent() {
7272
<Route path='/' element={<Main />} />
7373
<Route path='/login' element={<Login />} />
7474
<Route path='/usermain' element={<UserMain />} />
75-
<Route path='/news' element={<News />} />
76-
<Route path='/news/:no' element={<NewsInfo />} />
75+
<Route path='/activities/news' element={<News />} />
76+
<Route path='/activities/news/:no' element={<NewsInfo />} />
7777
<Route path='/coremember' element={<CoreMembers />} />
7878
<Route path='/faq' element={<FAQ />} />
7979
<Route path='/recruit' element={<Recruit />} />

src/assets/images/dasombanner.png

2.57 MB
Loading

src/components/UI/NewsCarousel.tsx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import React, { useState } from 'react'
2+
import { NewsCarouselProps } from './types'
3+
4+
const NewsCarousel: React.FC<NewsCarouselProps> = ({ imageUrls }) => {
5+
const [currentSlide, setCurrentSlide] = useState(0)
6+
7+
const nextSlide = () => {
8+
setCurrentSlide(prev => (prev + 1) % imageUrls.length)
9+
}
10+
11+
const prevSlide = () => {
12+
setCurrentSlide(prev => (prev - 1 + imageUrls.length) % imageUrls.length)
13+
}
14+
15+
const goToSlide = (index: number) => {
16+
setCurrentSlide(index)
17+
}
18+
19+
if (imageUrls.length === 0) return null
20+
21+
return (
22+
<header className='flex flex-col w-full'>
23+
<div className='w-full px-2 sm:px-4 py-4 sm:py-8'>
24+
<div className='relative w-full max-w-6xl mx-auto mb-4 sm:mb-6'>
25+
<div className='relative overflow-hidden rounded-lg sm:rounded-xl shadow-xl sm:shadow-2xl'>
26+
<img
27+
src={imageUrls[currentSlide]}
28+
alt={`뉴스 이미지 ${currentSlide + 1}`}
29+
className='w-full h-48 sm:h-64 md:h-80 lg:h-96 object-cover object-center transform scale-100 transition-transform duration-300 hover:scale-105'
30+
style={{
31+
imageRendering: 'crisp-edges',
32+
}}
33+
onError={e => {
34+
e.currentTarget.style.display = 'none'
35+
}}
36+
/>
37+
</div>
38+
39+
{imageUrls.length > 1 && (
40+
<>
41+
<button
42+
onClick={prevSlide}
43+
className='absolute left-1 sm:left-2 top-1/2 transform -translate-y-1/2 bg-white/20 hover:bg-white/30 text-white text-lg sm:text-2xl w-8 h-8 sm:w-10 sm:h-10 rounded-full flex items-center justify-center transition-colors backdrop-blur-sm'
44+
>
45+
46+
</button>
47+
<button
48+
onClick={nextSlide}
49+
className='absolute right-1 sm:right-2 top-1/2 transform -translate-y-1/2 bg-white/20 hover:bg-white/30 text-white text-lg sm:text-2xl w-8 h-8 sm:w-10 sm:h-10 rounded-full flex items-center justify-center transition-colors backdrop-blur-sm'
50+
>
51+
52+
</button>
53+
</>
54+
)}
55+
</div>
56+
57+
{imageUrls.length > 1 && (
58+
<div className='w-full max-w-6xl mx-auto'>
59+
<div className='grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-1 sm:gap-2 md:gap-4'>
60+
{imageUrls.map((imageUrl, index) => (
61+
<button
62+
key={index}
63+
onClick={() => goToSlide(index)}
64+
className={`relative overflow-hidden rounded-md sm:rounded-lg transition-all duration-300 shadow-md sm:shadow-lg ${
65+
currentSlide === index
66+
? 'ring-2 ring-teal-400 scale-105 shadow-lg sm:shadow-xl'
67+
: 'hover:scale-105 hover:shadow-lg sm:hover:shadow-xl'
68+
}`}
69+
>
70+
<img
71+
src={imageUrl}
72+
alt={`썸네일 ${index + 1}`}
73+
className='w-full h-12 sm:h-16 md:h-20 lg:h-24 object-cover object-center'
74+
style={{
75+
imageRendering: 'crisp-edges',
76+
}}
77+
onError={e => {
78+
e.currentTarget.style.display = 'none'
79+
}}
80+
/>
81+
</button>
82+
))}
83+
</div>
84+
</div>
85+
)}
86+
</div>
87+
</header>
88+
)
89+
}
90+
91+
export default NewsCarousel

src/components/UI/NewsContent.tsx

Lines changed: 24 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,15 @@
11
import React, { useState, useMemo } from 'react'
2-
import { Swiper, SwiperSlide } from 'swiper/react'
3-
import { Pagination, Autoplay } from 'swiper/modules'
4-
import 'swiper/css'
5-
import 'swiper/css/pagination'
62
import { NewsProps } from './types'
73

84
const NewsContent: React.FC<NewsProps> = React.memo(
9-
({ id, title, image, images, createdAt, onClick, isDetail = false }) => {
5+
({ title, image, createdAt, content, onClick }) => {
106
const [loading, setLoading] = useState(true)
117

128
const formattedDate = useMemo(
139
() => new Date(createdAt).toISOString().split('T')[0].replace(/-/g, '.'),
1410
[createdAt]
1511
)
1612

17-
// Base64 → 이미지 URL 변환 (useMemo로 캐싱)
18-
const imageUrls = useMemo(() => {
19-
if (!images || images.length === 0) return null
20-
return images.map(img =>
21-
img?.encodedData
22-
? `data:${img.fileFormat};base64,${img.encodedData}`
23-
: null
24-
)
25-
}, [images])
26-
2713
return (
2814
<div
2915
className='flex flex-col items-start mb-5 w-full cursor-pointer'
@@ -33,48 +19,30 @@ const NewsContent: React.FC<NewsProps> = React.memo(
3319
<div className='w-full h-[140px] bg-gray-700 animate-pulse rounded-lg'></div>
3420
)}
3521

36-
{isDetail && imageUrls ? (
37-
<Swiper
38-
key={`swiper-${id}-${imageUrls.length}`}
39-
spaceBetween={10}
40-
pagination={{ clickable: true }}
41-
autoplay={{ delay: 3000, disableOnInteraction: false }}
42-
loop={true}
43-
observer={true}
44-
observeParents={true}
45-
modules={[Pagination, Autoplay]}
46-
className='w-full'
47-
>
48-
{imageUrls.map(
49-
(imageUrl, index) =>
50-
imageUrl && (
51-
<SwiperSlide key={index}>
52-
<img
53-
src={imageUrl}
54-
className='w-full h-[140px] rounded-lg transition-opacity duration-500'
55-
alt={`뉴스 이미지 ${index + 1}`}
56-
onLoad={() => setLoading(false)}
57-
loading='lazy'
58-
/>
59-
</SwiperSlide>
60-
)
61-
)}
62-
</Swiper>
63-
) : (
64-
image && (
65-
<img
66-
src={image}
67-
className='w-full h-[140px] rounded-lg transition-opacity duration-500'
68-
alt='뉴스 대표 이미지'
69-
onLoad={() => setLoading(false)}
70-
loading='lazy'
22+
<img
23+
src={image || ''}
24+
className='w-full h-[140px] rounded-t-lg transition-opacity duration-500'
25+
alt='뉴스 대표 이미지'
26+
onLoad={() => setLoading(false)}
27+
loading='lazy'
28+
/>
29+
30+
<div className='flex flex-col bg-[#26262D] rounded-b-lg w-full px-3 py-3'>
31+
<h3 className='font-pretendardBold text-white mb-2 truncate'>
32+
{title}
33+
</h3>
34+
35+
{content && (
36+
<p
37+
className='font-pretendardRegular text-subGrey mb-3 line-clamp-2 leading-relaxed'
38+
dangerouslySetInnerHTML={{ __html: content }}
7139
/>
72-
)
73-
)}
74-
<p className='font-pretendardBold text-white text-[16px] mt-2'>
75-
{title}
76-
</p>
77-
<p className='text-[12px] text-subGrey2'>작성일: {formattedDate}</p>
40+
)}
41+
42+
<time className='font-pretendardRegular text-subGrey2 self-start'>
43+
작성일: {formattedDate}
44+
</time>
45+
</div>
7846
</div>
7947
)
8048
}

src/components/UI/NewsNotice.tsx

Lines changed: 0 additions & 15 deletions
This file was deleted.

src/components/UI/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,12 @@ export interface NewsProps {
6363
title: string
6464
image?: string | null
6565
images?: { encodedData: string; fileFormat: string }[] | null
66+
content?: string
6667
createdAt: string
6768
onClick: () => void
6869
isDetail?: boolean
6970
}
71+
72+
export interface NewsCarouselProps {
73+
imageUrls: string[]
74+
}

src/components/sections/ActivitiesSection.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useEffect, useMemo, useRef, useState } from 'react'
22
import { Link } from 'react-router-dom'
33
import { NewsItem } from '../../pages/news/Newstype'
4-
import { NewsService } from '../../pages/news/NewsService'
4+
import { getNewsList } from '../../pages/news/NewsService'
55
import { convertToBase64Url } from '../../utils/imageUtils'
66
import Reveal from '../UI/Reveal'
77

@@ -13,7 +13,7 @@ const ActivitiesSection: React.FC = () => {
1313
if (fetched.current) return
1414
const load = async () => {
1515
try {
16-
const list = await NewsService.getNewsList()
16+
const list = await getNewsList()
1717
setNews(list)
1818
} catch (e) {
1919
console.error('최근 소식 불러오기 실패:', e)

src/pages/news/News.tsx

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
import React, { useEffect, useState, useRef, useMemo } from 'react'
2-
import MobileLayout from '../../components/layout/MobileLayout'
3-
import dasomLogo from '../../assets/images/dasomLogo.svg'
42
import NewsContent from '../../components/UI/NewsContent'
3+
import dasombanner from '../../assets/images/dasombanner.png'
54
import { useNavigate } from 'react-router-dom'
65
import { NewsItem } from './Newstype'
76
import { convertToBase64Url } from '../../utils/imageUtils'
8-
import { NewsService } from './NewsService'
7+
import { getNewsList } from './NewsService'
98

109
const News: React.FC = () => {
1110
const navigate = useNavigate()
1211
const [newsList, setNewsList] = useState<NewsItem[]>([])
12+
const [currentPage, setCurrentPage] = useState(1)
13+
const [itemsPerPage] = useState(9)
1314
const isFetched = useRef(false)
1415

1516
const fetchNews = async () => {
1617
try {
17-
const data = await NewsService.getNewsList()
18+
const data = await getNewsList()
1819
setNewsList(data)
1920
} catch (error) {
20-
console.error('뉴스 데이터를 불러오는 중 오류 발생:', error)
21+
console.error('뉴스 데이터 오류 발생:', error)
2122
}
2223
}
2324

@@ -28,7 +29,6 @@ const News: React.FC = () => {
2829
}
2930
}, [])
3031

31-
// `useMemo`로 이미지 변환 최적화 (불필요한 연산 방지)
3232
const formattedNewsList = useMemo(
3333
() =>
3434
newsList.map(news => ({
@@ -38,32 +38,63 @@ const News: React.FC = () => {
3838
[newsList]
3939
)
4040

41+
const totalPages = Math.ceil(formattedNewsList.length / itemsPerPage)
42+
const startIndex = (currentPage - 1) * itemsPerPage
43+
const endIndex = startIndex + itemsPerPage
44+
const currentNewsList = formattedNewsList.slice(startIndex, endIndex)
45+
46+
const handlePageChange = (page: number) => {
47+
setCurrentPage(page)
48+
}
49+
4150
return (
42-
<MobileLayout>
43-
<div className='mt-[65px] mb-2 px-[12px] flex'>
51+
<main className='w-full bg-[#17171B] flex flex-col items-center pb-20 min-h-screen'>
52+
<header className='flex flex-col w-full'>
4453
<img
45-
className='w-[21px] h-[24px] cursor-pointer'
46-
alt='logo'
47-
src={dasomLogo}
54+
src={dasombanner}
55+
alt='dasombanner'
56+
className='w-full h-full object-contain'
4857
/>
49-
<div className='font-pretendardSemiBold text-white text-[16px] ml-[9px]'>
50-
다솜 소식
58+
</header>
59+
60+
<section className='w-full flex justify-center'>
61+
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-8 mx-[12px] w-full max-w-6xl mt-[60px] md:mt-[100px] mb-10'>
62+
{currentNewsList.map(news => (
63+
<NewsContent
64+
key={news.id}
65+
id={news.id}
66+
title={news.title}
67+
image={news.imageUrl}
68+
content={news.content}
69+
createdAt={news.createdAt}
70+
onClick={() => navigate(`/activities/news/${news.id}`)}
71+
/>
72+
))}
5173
</div>
52-
</div>
74+
</section>
75+
76+
{totalPages > 1 && (
77+
<div className='w-full max-w-screen-xl mx-auto flex flex-col items-center px-4 md:px-12'>
78+
<div className='w-full h-px bg-gray-600 mb-4 md:mb-6'></div>
5379

54-
<div className='flex flex-col mx-[12px] w-auto mb-40'>
55-
{formattedNewsList.map(news => (
56-
<NewsContent
57-
key={news.id}
58-
id={news.id}
59-
title={news.title}
60-
image={news.imageUrl}
61-
createdAt={news.createdAt}
62-
onClick={() => navigate(`/news/${news.id}`)}
63-
/>
64-
))}
65-
</div>
66-
</MobileLayout>
80+
<div className='flex items-center justify-center space-x-1 md:space-x-2 mb-4 md:mb-6'>
81+
{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
82+
<button
83+
key={page}
84+
onClick={() => handlePageChange(page)}
85+
className={`w-8 h-8 md:w-10 md:h-10 rounded-lg text-white font-pretendardRegular transition-colors duration-200 text-sm md:text-base ${
86+
currentPage === page
87+
? 'bg-mainColor text-white'
88+
: 'bg-[#26262D] text-white hover:bg-gray-600'
89+
}`}
90+
>
91+
{page}
92+
</button>
93+
))}
94+
</div>
95+
</div>
96+
)}
97+
</main>
6798
)
6899
}
69100

0 commit comments

Comments
 (0)