Skip to content

Commit 5284a85

Browse files
authored
Merge pull request #293 from DguFarmSystem/fix/#291
fix: 소식 목록 페이지 트러블 슈팅 우하하 디자인은 항상 어려워,,,
2 parents 9eec5b4 + 6b61f15 commit 5284a85

File tree

9 files changed

+231
-25
lines changed

9 files changed

+231
-25
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
# HomePage-FE🌱
2-
- 2025 동국대학교 Farm System 홈페이지 개발
1+
# FarmSystem-FE
2+
동국대학교 Farm System 웹 플랫폼의 프론트엔드 모노레포입니다.
3+
4+
이 저장소는 공식 홈페이지(Website), 파밍로그(FarmingLog), 관리자(Admin), 내부 문서(Docs)를 포함한 **통합 웹 플랫폼**의 코드베이스를 관리합니다.
5+
각 서비스는 **Turborepo 기반의 모노레포 구조**로 구성되어 있으며, 공통된 인증, API, 설정, 컴포넌트 등을 패키지로 분리하여 유지보수성과 확장성을 높였습니다.
36

47

58
## 📁 폴더 구조
@@ -154,4 +157,4 @@ pnpm install
154157
pnpm run dev
155158
```
156159

157-
---
160+
---

apps/farminglog/src/components/Popup/popup.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ const InfoLayout: React.FC<PopupProps> = () => {
265265
</S.HeaderContext>
266266

267267
<S.HeaderContext $isApp={isApp} $isMobile={isMobile}>
268-
<p>씨앗 모으기 퀘스트: 매일 00시 기준 초기화</p>
268+
<p>씨앗 모으기 퀘스트: 매일 자정 기준 초기화</p>
269269
<p>하루에 모을 수 있는 최대 씨앗 개수: 8개</p>
270270
</S.HeaderContext>
271271
</S.FarmingLogEditorContainerHeader>

apps/farminglog/src/pages/home/Cheer/cheer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export default function CheerPreview() {
3535
<S.BackArrow
3636
src={UpArrowImg}
3737
alt="작성하기"
38-
onClick={() => navigate("/cheer/write")}
38+
onClick={() => navigate("/cheer")}
3939
$isMobile={isMobile}
4040
/>
4141
</S.TitleBox>

apps/farminglog/src/pages/home/Ranking/ranking.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export default function RankingPreview() {
9595
</S.TitleBox>
9696

9797
<S.PhaseDesc $isMobile={isMobile}>
98-
· 랭킹은 씨앗을 기준으로 0시간마다 정렬돼요.
98+
· 랭킹은 씨앗을 기준으로 매일 자정마다 정렬돼요.
9999
<br />
100100
· 씨앗은 트랙별 우수활동자 심사에 반영돼요.
101101
<br />

apps/website/src/pages/News/NewsItem.styled.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import styled from 'styled-components';
22

33
interface MobileProps {
44
$isMobile?: boolean;
5+
$isTablet?: boolean;
6+
$isDesktop?: boolean;
57
}
68

79
export const NewsItem = styled.button<MobileProps>`
810
display: flex;
11+
flex-direction: ${({ $isMobile, $isTablet }) => ($isMobile ? 'column' : $isTablet ? 'row' : 'row')};
912
padding: ${({ $isMobile }) => ($isMobile ? '10px 15px' : '20px 30px')};
1013
align-items: center;
1114
gap: ${({ $isMobile }) => ($isMobile ? '15px' : '30px')};
@@ -18,7 +21,7 @@ export const NewsItem = styled.button<MobileProps>`
1821
`;
1922

2023
export const Thumbnail = styled.img<MobileProps>`
21-
width: ${({ $isMobile }) => ($isMobile ? '30%' : '311px')};
24+
width: ${({ $isMobile }) => ($isMobile ? 'auto' : '311px')};
2225
height: ${({ $isMobile }) => ($isMobile ? 'auto' : '200px')};
2326
flex-shrink: 0;
2427
aspect-ratio: ${({ $isMobile }) => ($isMobile ? '16/9' : '311/200')};
@@ -75,6 +78,23 @@ export const Content = styled.p<MobileProps>`
7578
text-overflow: ellipsis;
7679
`;
7780

81+
export const DateAndTagBox = styled.div<MobileProps>`
82+
display: flex;
83+
align-items: ${({ $isMobile }) => ($isMobile ? 'flex-start' : 'center')};
84+
justify-content: ${({ $isMobile }) => ($isMobile ? 'flex-start' : 'space-between')};
85+
width: 100%;
86+
flex-direction: ${({ $isMobile }) => ($isMobile ? 'column' : 'row')};
87+
`;
88+
89+
export const Date = styled.p<MobileProps>`
90+
color: var(--FarmSystem_Black, #191919);
91+
font-size: ${({ $isMobile }) => ($isMobile ? '14px' : '16px')};
92+
font-style: normal;
93+
font-weight: 400;
94+
line-height: 30px; /* 187.5% */
95+
letter-spacing: -0.24px;
96+
`;
97+
7898
export const TagBox = styled.div<MobileProps>`
7999
display: flex;
80100
align-items: center;

apps/website/src/pages/News/NewsItem.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import PlaceHolder from '@/assets/Images/news/PlaceHolder.png';
44
import Logger from '@/utils/Logger';
55
import { useNavigate } from 'react-router';
66
import useMediaQueries from '@/hooks/useMediaQueries';
7+
import { formatKoreanDateTimeNoHour } from '@/utils/formatKoreanDateTime';
78

89
export default function NewsItem({ newsListData }: { newsListData?: newsListData }) {
910
const navigate = useNavigate();
10-
const isMobile = useMediaQueries().isMobile;
11+
const { isMobile, isTablet } = useMediaQueries();
1112

1213
if (!newsListData) {
1314
return null;
@@ -27,17 +28,21 @@ export default function NewsItem({ newsListData }: { newsListData?: newsListData
2728
return (
2829
<S.NewsItem
2930
$isMobile={isMobile}
31+
$isTablet={isTablet}
3032
onClick={() => navigate(`/news/${newsListData.newsId}`)}
3133
>
3234
<S.Thumbnail src={thumbnailSrc} alt={title} $isMobile={isMobile} />
3335
<S.NewsContent $isMobile={isMobile}>
3436
<S.Title $isMobile={isMobile}>{title}</S.Title>
3537
<S.Content $isMobile={isMobile}>{truncatedContent}</S.Content>
36-
<S.TagBox $isMobile={isMobile}>
37-
{tags.map((tag, index) => (
38-
<S.Tag key={index} $isMobile={isMobile}>{tag}</S.Tag>
39-
))}
40-
</S.TagBox>
38+
<S.DateAndTagBox $isMobile={isMobile}>
39+
<S.Date $isMobile={isMobile}>게시일자: {formatKoreanDateTimeNoHour(newsListData.createdAt)}</S.Date>
40+
<S.TagBox $isMobile={isMobile}>
41+
{tags.map((tag, index) => (
42+
<S.Tag key={index} $isMobile={isMobile}>{tag}</S.Tag>
43+
))}
44+
</S.TagBox>
45+
</S.DateAndTagBox>
4146
</S.NewsContent>
4247
</S.NewsItem>
4348
);

apps/website/src/pages/News/index.styled.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,96 @@ export const Line = styled.hr`
8181
max-width: 1100px;
8282
margin: 30px 0;
8383
`;
84+
85+
86+
/** 페이지네이션 컨테이너 */
87+
export const PaginationContainer = styled.div`
88+
display: flex;
89+
justify-content: center;
90+
align-items: center;
91+
margin-top: 40px;
92+
margin-bottom: 40px;
93+
`;
94+
95+
/** 페이지네이션 버튼 컨테이너 */
96+
export const PaginationButton = styled.div`
97+
display: flex;
98+
gap: 10px;
99+
align-items: center;
100+
`;
101+
102+
/** 페이지네이션 버튼 텍스트 */
103+
export const PaginationButtonText = styled.span<{
104+
$active?: boolean;
105+
$disabled?: boolean;
106+
$isMobile?: boolean;
107+
$isTablet?: boolean;
108+
}>`
109+
border-radius: 6px;
110+
cursor: ${(props) => (props.$disabled ? 'not-allowed' : 'pointer')};
111+
font-size: 14px;
112+
font-weight: 500;
113+
transition: all 0.2s ease;
114+
115+
/* 사이즈 조절 */
116+
img[alt="nextArrow"]{
117+
width: ${(props) => (props.$isMobile ? '6px' : props.$isTablet ? '12px' : '15px')};
118+
height: ${(props) => (props.$isMobile ? '12px' : props.$isTablet ? '24px' : '30px')};
119+
margin-right: 10px;
120+
}
121+
122+
img[alt="jumpArrow"]{
123+
width: ${(props) => (props.$isMobile ? '24px' : props.$isTablet ? '48px' : '60px')};
124+
height: ${(props) => (props.$isMobile ? '24px' : props.$isTablet ? '48px' : '60px')};
125+
}
126+
127+
/* nextArrow 이미지 회전 */
128+
img[alt="nextArrow_right"] {
129+
width: ${(props) => (props.$isMobile ? '6px' : props.$isTablet ? '12px' : '15px')};
130+
height: ${(props) => (props.$isMobile ? '12px' : props.$isTablet ? '24px' : '30px')};
131+
transform: rotate(180deg);
132+
margin-left: 10px;
133+
}
134+
135+
img[alt="jumpArrow_right"] {
136+
width: ${(props) => (props.$isMobile ? '24px' : props.$isTablet ? '48px' : '60px')};
137+
height: ${(props) => (props.$isMobile ? '24px' : props.$isTablet ? '48px' : '60px')};
138+
transform: rotate(180deg);
139+
}
140+
141+
&:hover {
142+
${(props) => !props.$disabled && `
143+
background-color: ${props.$active ? 'var(--FarmSystem_Green06)' : '#f0f0f0'};
144+
transform: translateY(-1px);
145+
`}
146+
}
147+
148+
&:active {
149+
${(props) => !props.$disabled && `
150+
transform: translateY(0);
151+
`}
152+
}
153+
`;
154+
155+
export const PaginationPageButton = styled.span<{
156+
$active?: boolean;
157+
$disabled?: boolean;
158+
$isMobile?: boolean;
159+
$isTablet?: boolean;
160+
}>`
161+
width: ${(props) => (props.$isMobile ? '20px' : props.$isTablet ? '26px' : '40px')};
162+
height: ${(props) => (props.$isMobile ? '20px' : props.$isTablet ? '26px' : '40px')};
163+
display: flex;
164+
justify-content: center;
165+
align-items: center;
166+
border-radius: ${(props) => (props.$isMobile ? '10px' : props.$isTablet ? '13px' : '20px')};
167+
cursor: ${(props) => (props.$disabled ? 'not-allowed' : 'pointer')};
168+
169+
background-color: ${(props) => (props.$active ? 'var(--FarmSystem_Green06)' : 'var(--FarmSystem_DarkGrey)')};
170+
color: white;
171+
font-size: ${(props) => (props.$isMobile ? '8px' : props.$isTablet ? '12px' : '14px')};
172+
font-weight: 500;
173+
transition: all 0.2s ease;
174+
`;
175+
176+

apps/website/src/pages/News/index.tsx

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,60 @@
1+
import { useState, useEffect } from 'react';
12
import useMediaQueries from '@/hooks/useMediaQueries';
23
import { useNewsList } from '@/hooks/useNews';
34
// import Logger from '@/utils/Logger';
45
import * as S from './index.styled';
56
import NewsItem from './NewsItem';
67

8+
import jumpArrow_left from '@/assets/Icons/pagenation_1.png';
9+
import jumpArrow_right from '@/assets/Icons/pagenation_1.png';
10+
import nextArrow_left from '@/assets/Icons/pagenation_2.png';
11+
import nextArrow_right from '@/assets/Icons/pagenation_2.png';
12+
713

814
export default function News() {
15+
const [currentPage, setCurrentPage] = useState<number>(0);
16+
const pageSize = 12; // 12개씩 페이지네이션
917
const { isMobile } = useMediaQueries();
1018
const { data: newsData, loading: newsLoading, error: newsError } = useNewsList();
1119
const newsDataSorted = newsData?.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
12-
20+
const totalPages = Math.ceil(newsDataSorted?.length / pageSize);
21+
22+
const currentNews = newsDataSorted?.slice(
23+
currentPage * pageSize,
24+
(currentPage + 1) * pageSize
25+
);
26+
27+
const handlePreviousPage = () => {
28+
if (currentPage > 0) setCurrentPage(currentPage - 1);
29+
};
30+
31+
const handleNextPage = () => {
32+
if (currentPage < totalPages - 1) setCurrentPage(currentPage + 1);
33+
};
34+
35+
// 페이지 번호 배열 생성
36+
const generatePageNumbers = () => {
37+
const pages: number[] = [];
38+
const maxVisiblePages = 3;
39+
let startPage = Math.max(0, currentPage - Math.floor(maxVisiblePages / 2));
40+
const endPage = Math.min(totalPages - 1, startPage + maxVisiblePages - 1);
41+
42+
if (endPage - startPage < maxVisiblePages - 1) {
43+
startPage = Math.max(0, endPage - maxVisiblePages + 1);
44+
}
45+
46+
for (let i = startPage; i <= endPage; i++) {
47+
pages.push(i);
48+
}
49+
50+
return pages;
51+
};
52+
53+
// 페이지 전환시 스크롤을 맨위로 부드럽게 전환
54+
useEffect(() => {
55+
window.scrollTo({ top: 0, behavior: 'smooth' });
56+
}, [currentPage]);
57+
1358
if (newsLoading) {
1459
return (
1560
<S.Container>
@@ -27,23 +72,48 @@ export default function News() {
2772
</S.DescriptionContainer>
2873
</S.Container>
2974
);
30-
// Logger.error(newsError);
31-
// return (
32-
// <S.Container>
33-
// <S.Message $isMobile={isMobile}>뉴스를 불러오는 중 오류가 발생했습니다.</S.Message>
34-
// </S.Container>
35-
// );
3675
}
3776

3877
return (
3978
<S.Container>
4079
<S.NewsPageTitle>소식</S.NewsPageTitle>
4180
{newsDataSorted && newsDataSorted.length > 0 ? (
42-
<S.NewsContainer>
43-
{newsDataSorted.map((news, index) => (
44-
<NewsItem key={index} newsListData={news} />
45-
))}
46-
</S.NewsContainer>
81+
<>
82+
<S.NewsContainer>
83+
{currentNews?.map((news, index) => (
84+
<NewsItem key={index} newsListData={news} />
85+
))}
86+
</S.NewsContainer>
87+
{totalPages > 1 && (
88+
<S.PaginationContainer>
89+
<S.PaginationButton>
90+
<S.PaginationButtonText onClick={() => setCurrentPage(0)} $disabled={currentPage === 0}>
91+
<img src={jumpArrow_left} alt="jumpArrow" />
92+
</S.PaginationButtonText>
93+
<S.PaginationButtonText onClick={handlePreviousPage} $disabled={currentPage === 0}>
94+
<img src={nextArrow_left} alt="nextArrow" />
95+
</S.PaginationButtonText>
96+
97+
{generatePageNumbers().map((pageNum) => (
98+
<S.PaginationPageButton
99+
key={pageNum}
100+
onClick={() => setCurrentPage(pageNum)}
101+
$active={pageNum === currentPage}
102+
>
103+
{pageNum + 1}
104+
</S.PaginationPageButton>
105+
))}
106+
107+
<S.PaginationButtonText onClick={handleNextPage} $disabled={currentPage >= totalPages - 1}>
108+
<img src={nextArrow_right} alt="nextArrow_right" />
109+
</S.PaginationButtonText>
110+
<S.PaginationButtonText onClick={() => setCurrentPage(totalPages - 1)} $disabled={currentPage >= totalPages - 1}>
111+
<img src={jumpArrow_right} alt="jumpArrow_right" />
112+
</S.PaginationButtonText>
113+
</S.PaginationButton>
114+
</S.PaginationContainer>
115+
)}
116+
</>
47117
) : (
48118
<S.DescriptionContainer>
49119
<S.Message $isMobile={isMobile}>아직 등록된 소식이 없어요.</S.Message>

apps/website/src/utils/formatKoreanDateTime.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,18 @@ export function formatKoreanDateTime(isoString: string): string {
2424

2525
return `${year}${month}${day}${hours}:${minutes}`;
2626
}
27+
28+
export function formatKoreanDateTimeNoHour(isoString: string): string {
29+
const date = new Date(isoString);
30+
31+
if (isNaN(date.getTime())) {
32+
throw new Error("유효하지 않은 날짜 문자열입니다.");
33+
}
34+
35+
const year = date.getFullYear();
36+
const month = String(date.getMonth() + 1).padStart(2, '0');
37+
const day = String(date.getDate()).padStart(2, '0');
38+
39+
return `${year}${month}${day}일`;
40+
}
41+

0 commit comments

Comments
 (0)