Skip to content

Commit 392bebe

Browse files
authored
Merge pull request #281 from DguFarmSystem/fix/#280
fix: 소식 트러블 슈팅(이미지 크롭, 소식 정렬, 이미지 URLS 추가)
2 parents c2a58ff + 4ada7a9 commit 392bebe

File tree

6 files changed

+161
-6
lines changed

6 files changed

+161
-6
lines changed

apps/website/src/layouts/DetailLayout/DetailLayout.styled.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,11 @@ export const ImageContainer = styled.div<LayoutProps>`
124124
`;
125125

126126
export const Thumbnail = styled.img<LayoutProps>`
127-
width: 827.92px;
127+
width: 800px;
128+
border-radius: 4px;
128129
// height: 533px;
129130
flex-shrink: 0;
130-
aspect-ratio: 827.92/533.00;
131+
// aspect-ratio: 827.92/533.00;
131132
132133
border-top: 3px solid var(--FarmSystem_DarkGrey, #999);
133134
border-bottom: 1px solid var(--FarmSystem_DarkGrey, #999);
@@ -146,4 +147,26 @@ export const ContentBox = styled.p<LayoutProps>`
146147
font-weight: 400;
147148
line-height: 30px; /* 150% */
148149
letter-spacing: -0.24px;
149-
`;
150+
`;
151+
152+
////////////////////// 이미지 ////////////////////////
153+
export const ImageGallery = styled.div<{ $isMobile?: boolean; $isTablet?: boolean; $isDesktop?: boolean }>`
154+
width: 100%;
155+
max-width: 800px;
156+
display: grid;
157+
grid-template-columns: repeat(auto-fill, minmax(120px, 180px));
158+
gap: 10px;
159+
`;
160+
161+
export const Image = styled.img`
162+
width: 100%;
163+
aspect-ratio: 1 / 1;
164+
object-fit: cover;
165+
border-radius: 4px;
166+
cursor: pointer;
167+
transition: transform 0.2s;
168+
169+
&:hover {
170+
transform: scale(1.05);
171+
}
172+
`;

apps/website/src/layouts/DetailLayout/DetailLayout.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as S from "./DetailLayout.styled";
2+
import { useState } from "react";
23
import GoBackArrow from "@/assets/LeftArrow.png";
34
import useMediaQueries from "@/hooks/useMediaQueries";
5+
import ImageModal from './ImageModal';
46

57
interface DetailLayoutProps {
68
title?: string;
@@ -17,9 +19,10 @@ export default function DetailLayout({
1719
date = "(임시) 게시일자: 2025년 03월 13일",
1820
tag = "(임시) 태그",
1921
thumbnailUrl = "",
20-
// imageUrls = [],
22+
imageUrls = [],
2123
}: DetailLayoutProps) {
2224
const { isMobile, isTablet, isDesktop } = useMediaQueries();
25+
const [selectedImage, setSelectedImage] = useState<string | null>(null);
2326

2427
return (
2528
<S.DetailCard $isMobile={isMobile} $isTablet={isTablet} $isDesktop={isDesktop}>
@@ -56,6 +59,26 @@ export default function DetailLayout({
5659
alt={title}
5760
/>
5861
</S.ImageContainer>
62+
<S.ImageContainer $isMobile={isMobile} $isTablet={isTablet} $isDesktop={isDesktop}>
63+
{imageUrls && imageUrls.length > 0 && (
64+
<S.ImageGallery $isMobile={isMobile} $isTablet={isTablet} $isDesktop={isDesktop}>
65+
{imageUrls.map((url, index) => (
66+
<S.Image
67+
key={index}
68+
src={url}
69+
alt={`Image ${index + 1}`}
70+
onClick={() => setSelectedImage(url)}
71+
/>
72+
))}
73+
</S.ImageGallery>
74+
)}
75+
</S.ImageContainer>
76+
{selectedImage && (
77+
<ImageModal
78+
imageUrl={selectedImage}
79+
onClose={() => setSelectedImage(null)}
80+
/>
81+
)}
5982
<S.ContentBox $isMobile={isMobile} $isTablet={isTablet} $isDesktop={isDesktop}>
6083
{content}
6184
</S.ContentBox>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import styled from "styled-components";
2+
3+
export const ModalOverlay = styled.div`
4+
position: fixed;
5+
inset: 0;
6+
background: rgba(0, 0, 0, 0.7);
7+
z-index: 999;
8+
display: flex;
9+
justify-content: center;
10+
align-items: center;
11+
`;
12+
13+
export const ModalImage = styled.img`
14+
max-width: 90vw;
15+
max-height: 90vh;
16+
border-radius: 0px;
17+
object-fit: contain;
18+
`;
19+
20+
export const ModalContent = styled.div`
21+
position: relative;
22+
z-index: 1000;
23+
max-width: 90vw;
24+
max-height: 90vh;
25+
`;
26+
27+
export const ModalCloseArea = styled.div`
28+
position: fixed;
29+
inset: 0;
30+
cursor: zoom-out;
31+
`;
32+
33+
34+
export const CloseButton = styled.button`
35+
position: absolute;
36+
top: -20px;
37+
right: -20px;
38+
background: #fff;
39+
border: none;
40+
color: #000;
41+
font-size: 28px;
42+
font-weight: bold;
43+
border-radius: 50%;
44+
width: 40px;
45+
height: 40px;
46+
cursor: pointer;
47+
z-index: 1001;
48+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
49+
50+
&:hover {
51+
background: #eee;
52+
}
53+
`;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { useEffect, useRef } from 'react';
2+
import ReactDOM from 'react-dom';
3+
import * as S from './ImageModal.styled';
4+
5+
interface ImageModalProps {
6+
imageUrl: string;
7+
onClose: () => void;
8+
}
9+
10+
export default function ImageModal({ imageUrl, onClose }: ImageModalProps) {
11+
const modalRef = useRef<HTMLDivElement>(null);
12+
const closeButtonRef = useRef<HTMLButtonElement>(null);
13+
14+
useEffect(() => {
15+
const handleKeyDown = (e: KeyboardEvent) => {
16+
if (e.key === 'Escape') onClose();
17+
if (e.key === 'Tab') {
18+
e.preventDefault();
19+
closeButtonRef.current?.focus();
20+
}
21+
};
22+
23+
document.addEventListener('keydown', handleKeyDown);
24+
return () => document.removeEventListener('keydown', handleKeyDown);
25+
}, [onClose]);
26+
27+
useEffect(() => {
28+
const originalStyle = document.body.style.overflow;
29+
document.body.style.overflow = 'hidden';
30+
return () => {
31+
document.body.style.overflow = originalStyle;
32+
};
33+
}, []);
34+
35+
if (typeof window === 'undefined') return null;
36+
37+
return ReactDOM.createPortal(
38+
<S.ModalOverlay onClick={onClose} ref={modalRef}>
39+
<S.ModalCloseArea />
40+
<S.ModalContent onClick={(e) => e.stopPropagation()}>
41+
<S.CloseButton
42+
ref={closeButtonRef}
43+
type="button"
44+
onClick={onClose}
45+
aria-label="Close modal">
46+
&times;
47+
</S.CloseButton>
48+
<S.ModalImage src={imageUrl} alt="Modal image" />
49+
</S.ModalContent>
50+
</S.ModalOverlay>,
51+
document.body
52+
);
53+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export const Thumbnail = styled.img<MobileProps>`
2424
aspect-ratio: ${({ $isMobile }) => ($isMobile ? '16/9' : '311/200')};
2525
border-radius: 10px;
2626
// 피그마상 직선인거 아무거나 넣음
27+
object-fit: cover;
28+
object-position: center;
2729
`;
2830

2931
export const NewsContent = styled.div<MobileProps>`

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import NewsItem from './NewsItem';
88
export default function News() {
99
const { isMobile } = useMediaQueries();
1010
const { data: newsData, loading: newsLoading, error: newsError } = useNewsList();
11+
const newsDataSorted = newsData?.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
1112

1213
if (newsLoading) {
1314
return (
@@ -37,9 +38,9 @@ export default function News() {
3738
return (
3839
<S.Container>
3940
<S.NewsPageTitle>소식</S.NewsPageTitle>
40-
{newsData && newsData.length > 0 ? (
41+
{newsDataSorted && newsDataSorted.length > 0 ? (
4142
<S.NewsContainer>
42-
{newsData.map((news, index) => (
43+
{newsDataSorted.map((news, index) => (
4344
<NewsItem key={index} newsListData={news} />
4445
))}
4546
</S.NewsContainer>

0 commit comments

Comments
 (0)