[개인 포트폴리오] [클라이밍 커뮤니티 SNS] 2. 코드 분석 <timeline.js>
2024. 12. 20. 11:10ㆍ웹개발 포트폴리오
728x90
반응형
import styled from "styled-components";
import {
collection,
getDoc,
limit,
onSnapshot,
orderBy,
query,
} from "firebase/firestore";
import React, { useState, useEffect } from "react";
import Modal from "react-modal";
import Post from "./post";
import { auth, db } from "../firebase";
Modal.setAppElement("#root");
const Wrapper = styled.div`
@media (max-width: 600px) {
flex-direction: column; /* 세로 방향 배치 */
padding: 10px; /* 여백 축소 */
}
display: flex;
flex-direction: column;
width: 100%;
overflow-y: auto;
margin-left: 20vw;
`;
const PostGrid = styled.div`
display: grid;
grid-template-columns: repeat(3, 1fr); /* 3열 그리드 */
gap: 20px;
width: 80vw; /* 전체 페이지의 80% 너비를 차지 */
padding: 20px;
box-sizing: border-box;
@media (max-width: 600px) {
grid-template-columns: repeat(1, 1fr); /* 3열 그리드 */
}
`;
const PostCard = styled.div`
background-color: #fff;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
padding: 20px;
display: flex;
flex-direction: column;
text-align: center;
height: 110vh;
position: relative;
@media (max-width: 600px) {
height: 70vh;
}
`;
const PostThumbnail = styled.img`
width: 100%;
height: 150px;
object-fit: cover;
border-radius: 8px;
margin-bottom: 10px;
cursor: pointer;
@media (max-width: 600px) {
height: 200px;
}
`;
const ProfileContainer = styled.div`
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
`;
const CloseButton = styled.button`
background-color: none;
color: #8f89a1;
border: none;
cursor: pointer;
position: absolute;
top: 10px;
right: 10px;
`;
const ModalImage = styled.img`
width: 100vw;
height: 70vh;
max-width: 100%;
max-height: 100%;
object-fit: contain;
`;
const ModalNavigation = styled.div`
display: flex;
justify-content: space-between;
margin-top: 10px;
`;
const ModalButton = styled.button`
background-color: #333;
color: white;
border: none;
padding: 10px;
border-radius: 5px;
cursor: pointer;
`;
const Timeline = () => {
const [posts, setPosts] = useState([]);
const [, setCurrentUserId] = useState(null);
// 상태 값을 사용하지 않고 업데이트 함수(setCurrentUserId)만 사용
const [modalIsOpen, setIsOpen] = useState(false);
const [selectedPhotos, setSelectedPhotos] = useState([]);
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
useEffect(() => {
const unsubscribeAuth = auth.onAuthStateChanged((user) => {
if (user) {
setCurrentUserId(user.uid);
} else {
setCurrentUserId(null);
}
});
const fetchPosts = () => {
const postsQuery = query(
collection(db, "posts"),
orderBy("createdAt", "desc"),
limit(25)
);
const unsubscribe = onSnapshot(postsQuery, async (snapshot) => {
// onSnapshot은 Firestore에서 실시간 데이터를 수신하기 위한 리스너(listener)를 설정하는 메서드
const postsData = await Promise.all(
snapshot.docs.map(async (doc) => {
const data = doc.data();
const userId = data.userId;
// 기본값 설정
let displayName = "익명";
let profileImage = "/guest.png";
let photos = Array.isArray(data.photos) && data.photos.length > 0 ? data.photos : ["/empty.jpg"];
try {
// users 컬렉션에서 userId 기반으로 displayName과 photoURL 가져오기
const userDoc = await getDoc(doc(db, "users", userId));
if (userDoc.exists()) {
const userData = userDoc.data();
displayName = userData.displayName || "익명";
profileImage = userData.photoURL || "/guest.png";
}
} catch (error) {
console.error("Error loading user profile:", error);
}
return {
id: doc.id,
createdAt: data.createdAt ? data.createdAt.toDate() : null,
likes: data.likes || 0,
post: data.post || "",
userId,
username: displayName,
profileImage, // users 컬렉션의 photoURL로 설정한 프로필 이미지
photos,
};
})
);
setPosts(postsData);
});
return unsubscribe; // Firestore 구독 해제를 위한 unsubscribe 반환
};
const unsubscribePosts = fetchPosts(); // fetchPosts의 반환 값 저장
return () => {
unsubscribeAuth(); // Firebase Auth 구독 해제
if (unsubscribePosts) unsubscribePosts(); // Firestore 구독 해제
};
}, []);
//사진 확대 보기
const openModal = (photos) => {
setSelectedPhotos(photos);
setCurrentPhotoIndex(0);
setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
setSelectedPhotos([]);
setCurrentPhotoIndex(0);
};
const showNextPhoto = () => {
setCurrentPhotoIndex((prevIndex) =>
prevIndex === selectedPhotos.length - 1 ? 0 : prevIndex + 1
);
};
const showPreviousPhoto = () => {
setCurrentPhotoIndex((prevIndex) =>
prevIndex === 0 ? selectedPhotos.length - 1 : prevIndex - 1
);
};
return (
<Wrapper>
<PostGrid>
{posts.map((post) => (
<PostCard key={post.id}>
{/* 첫 번째 사진만 썸네일로 표시 */}
{post.photos.length > 0 && (
<PostThumbnail
src={post.photos[0]} // 첫 번째 사진만 표시
alt="Post Thumbnail" // 접근성을 위한 alt 텍스트
onClick={() => openModal(post.photos)} // 전체 사진 배열 전달
/>
)}
<ProfileContainer>
<Post
key={post.id}
id={post.id}
userId={post.userId}
post={post.post}
photos={post.photos}
profileImage={auth.currentUser.photoURL} // 프로필 이미지 전달
username={post.username}
/>
</ProfileContainer>
</PostCard>
))}
</PostGrid>
<Modal isOpen={modalIsOpen} onRequestClose={closeModal} style={{ width: "100vw", height: "100vh", overlay: { zIndex: 1000 }, display: "flex", // 플렉스 박스 활성화
justifyContent: "center", alignItems: "center"}} >
<CloseButton onClick={closeModal}>X</CloseButton>
{selectedPhotos.length > 0 && (
<>
<ModalImage src={selectedPhotos[currentPhotoIndex]} />
<ModalNavigation>
<ModalButton onClick={showPreviousPhoto}>이전</ModalButton>
<ModalButton onClick={showNextPhoto}>다음</ModalButton>
</ModalNavigation>
</>
)}
</Modal>
</Wrapper>
);
};
export default Timeline;
728x90
반응형
'웹개발 포트폴리오' 카테고리의 다른 글
[개인 포트폴리오] [클라이밍 커뮤니티 SNS] 2. 코드 분석 <App.css & index.css> (0) | 2024.12.20 |
---|---|
[개인 포트폴리오] [클라이밍 커뮤니티 SNS] 2. 코드 분석 <posting.js> (0) | 2024.12.20 |
[개인 포트폴리오] [클라이밍 커뮤니티 SNS] 2. 코드 분석 <post.js> (0) | 2024.12.20 |
[개인 포트폴리오] [클라이밍 커뮤니티 SNS] 2. 코드 분석 <profile.js> (0) | 2024.12.20 |
[개인 포트폴리오] [클라이밍 커뮤니티 SNS] 2. 코드 분석 <loading-screen.js> (0) | 2024.12.20 |