diff --git a/umc-master/src/apis/policyApi.ts b/umc-master/src/apis/policyApi.ts index 72ba0d3..9078863 100644 --- a/umc-master/src/apis/policyApi.ts +++ b/umc-master/src/apis/policyApi.ts @@ -1,5 +1,5 @@ import axiosInstance from '@apis/axios-instance'; -//TODO: api 연결시 구조 바뀔 수도 있음 +import { PolicyData } from '@pages/magazine/components/cardGrid'; interface GetPoliciesParams { locationId: number; } @@ -11,15 +11,13 @@ export interface Policy { created_at: string; updated_at: string; policy_url: string; - magazine_image_url_list: { - image_name: string; - image_url: string; - }[]; + image_url_list: []; magazine_likes: number; magazine_bookmarks: number; organization: { id: number; name: string; + image: string; }; location: { id: number; @@ -45,9 +43,15 @@ export interface PolicyMutationParams { data: CreatePolicyParams; } -export const getPolicies = async ({ locationId }: GetPoliciesParams): Promise => { +export interface Hashtag { + hashtag_id: number; + name: string; + popularity: number; +} + +export const getPolicies = async ({ locationId }: GetPoliciesParams): Promise => { const { data } = await axiosInstance.get(`/policies?location_id=${locationId}`); - return data.result; + return data.result.policy_list; }; export const getPolicyGuide = async ({ policyId }: { policyId: number }): Promise => { @@ -69,3 +73,9 @@ export const deletePolicy = async ({ policyId }: { policyId: number }): Promise< const response = await axiosInstance.delete(`/policies/${policyId}`); return response.data.isSuccess; }; + +// 인기 관심사 조회 (기본값 6개) +export const getPopularHashtags = async ({ limit }: { limit: number }): Promise => { + const response = await axiosInstance.get(`/hashtags/popular?limit=${limit}`); + return response.data.result; +}; diff --git a/umc-master/src/apis/queries/usePolicyQueries.ts b/umc-master/src/apis/queries/usePolicyQueries.ts index e455009..979e87f 100644 --- a/umc-master/src/apis/queries/usePolicyQueries.ts +++ b/umc-master/src/apis/queries/usePolicyQueries.ts @@ -1,4 +1,4 @@ -import { getPolicies, getPolicyGuide } from '@apis/policyApi'; +import { getPolicies, getPolicyGuide, getPopularHashtags } from '@apis/policyApi'; import { keepPreviousData, useQuery } from '@tanstack/react-query'; interface PolicyListParams { @@ -24,3 +24,10 @@ export const usePolicyGuide = ({ policyId }: PolicyGuideParams) => { placeholderData: keepPreviousData, }); }; + +export const usePopularHashtags = ({ limit }: { limit: number }) => { + return useQuery({ + queryKey: ['hashtag'], + queryFn: () => getPopularHashtags({ limit }), + }); +}; diff --git a/umc-master/src/assets/character/magazine.png b/umc-master/src/assets/character/magazine.png new file mode 100644 index 0000000..3871073 Binary files /dev/null and b/umc-master/src/assets/character/magazine.png differ diff --git a/umc-master/src/components/Modal/image.tsx b/umc-master/src/components/Modal/image.tsx new file mode 100644 index 0000000..e372ae8 --- /dev/null +++ b/umc-master/src/components/Modal/image.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import styled from 'styled-components'; + +interface ImageModalProps { + imageUrl: string; + onClose: () => void; +} + +const ImageModal: React.FC = ({ imageUrl, onClose }) => { + return ( + + + 확대된 이미지 + + + ); +}; + +export default ImageModal; + +const Overlay = styled.div` + position: fixed; + top: 30px; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +`; + +const ModalContent = styled.div` + position: relative; + max-width: 70vh; + max-height: 70vh; + display: flex; + align-items: center; + justify-content: center; +`; + +const Image = styled.img` + max-width: 100%; + max-height: 100%; + border-radius: 8px; + object-fit: contain; +`; diff --git a/umc-master/src/pages/magazine/MagazineDetailPage.tsx b/umc-master/src/pages/magazine/MagazineDetailPage.tsx index 4f8587f..3d2a36d 100644 --- a/umc-master/src/pages/magazine/MagazineDetailPage.tsx +++ b/umc-master/src/pages/magazine/MagazineDetailPage.tsx @@ -1,87 +1,78 @@ -import { useEffect } from 'react'; +/* eslint-disable @typescript-eslint/ban-ts-comment */ +// @ts-nocheck +import { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; import Typography from '@components/common/typography'; import Tag from '@components/Tag/Tag'; - -interface MagazineDetail { - id: string; - image: string; - title: string; - author: string; - date: string; - tags: string[]; - description: string; - externalLink: string; -} -const dummyDetails: MagazineDetail[] = [ - { - id: '1', - image: 'https://i.ibb.co/SXSyhmX6/image-11.png', - title: '서리풀 보디가드 [주거안전] - 홈방범 시스템', - author: '서초1인가구지원센터', - date: '2024.12.30', - tags: ['보안', '도어가드', '홈케어'], - description: `홈 방범 시스템 - -- 도어가드벨, 몰카안심 존 등 1인세 설치 -- 설치비: 무료 -- 연이용료: 9,900원 -- 대상: 인터넷(유선) 설치가 되어있는 서초구 거주 1인가구 -- 무인경비서비스 설치 주택 적용 불가 (신규 아파트, 신규 오피스텔, 청년주택 등) - -문의: 서초1인가구지원센터`, - externalLink: 'https://example.com', - }, -]; +import { usePolicyGuide } from '@apis/queries/usePolicyQueries'; +import ImageModal from '@components/Modal/image'; const MagazineDetailPage: React.FC = () => { const { magazineId } = useParams<{ magazineId: string }>(); + const { data, isLoading } = usePolicyGuide({ policyId: Number(magazineId) }); + console.log('매거진', data); + + const formattedDescription = data?.description + .replace(/ {5,}/g, '\n\n') // 공백 5칸 이상 -> \n\n + .replace(/ {3,4}/g, '\n'); // 공백 3~4칸 -> \n + + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedImage, setSelectedImage] = useState(null); useEffect(() => { window.scrollTo(0, 0); }, []); - // TODO: 추후 API 연동 시, 실제 데이터 불러오도록 수정 - const detail = dummyDetails.find((item) => item.id === magazineId); + const handleImageClick = (imageUrl: string) => { + setSelectedImage(imageUrl); + setIsModalOpen(true); + }; - if (!detail) { - return 해당 매거진을 찾을 수 없습니다.; + if (isLoading) { + return; } return ( - {detail.title} + {data?.title} data?.image_url_list && handleImageClick(data?.image_url_list)} + /> - <Typography variant="titleMedium">{detail.title}</Typography> + <Typography variant="titleMedium">{data?.title}</Typography>
- + - {detail.author} + {data?.organization.name}
- {detail.date} + {data?.updated_at.slice(0, 10)}
- - {detail.tags.map((tag) => ( - - ))} - + {data?.hashtag?.map((tag) => ) ?? []} - {detail.description} + {formattedDescription} - + {isModalOpen && selectedImage && setIsModalOpen(false)} />}
); }; +export default MagazineDetailPage; + const Container = styled.div` max-width: 1280px; margin: 0 auto; @@ -90,10 +81,11 @@ const Container = styled.div` const Image = styled.img` width: 100%; - height: 200px; + height: 400px; object-fit: cover; border-radius: 20px; margin-bottom: 32px; + cursor: pointer; `; const Title = styled.div` @@ -110,11 +102,13 @@ const AuthorContainer = styled.div` margin-bottom: 32px; `; -const ProfileImage = styled.div` +const ProfileImage = styled.img<{ hasImage: boolean }>` width: 60px; height: 60px; border-radius: 50%; - background-color: ${({ theme }) => theme.colors.text.lightGray}; + object-fit: cover; + background-color: ${({ hasImage, theme }) => (hasImage ? theme.colors.text.white : theme.colors.text.lightGray)}; + box-shadow: ${({ hasImage }) => (hasImage ? '0px 4px 10px rgba(0, 0, 0, 0.15)' : 'none')}; `; const Author = styled.div` @@ -162,5 +156,3 @@ const Button = styled.a` background-color: ${({ theme }) => theme.colors.primary[600]}; } `; - -export default MagazineDetailPage; diff --git a/umc-master/src/pages/magazine/MagazinePage.tsx b/umc-master/src/pages/magazine/MagazinePage.tsx index f45b3ca..3f4983b 100644 --- a/umc-master/src/pages/magazine/MagazinePage.tsx +++ b/umc-master/src/pages/magazine/MagazinePage.tsx @@ -1,9 +1,12 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +// @ts-nocheck import { useEffect } from 'react'; import styled from 'styled-components'; import Typography from '@components/common/typography'; import MindMap from './components/mindMap'; import CardGrid, { CardGridData } from './components/cardGrid'; import dummyImg from '@assets/dummyImage/dummy.jpeg'; +import { usePolicies } from '@apis/queries/usePolicyQueries'; const generateDummyData = (): CardGridData[] => { return Array.from({ length: 9 }, (_, index) => ({ @@ -17,7 +20,7 @@ const generateDummyData = (): CardGridData[] => { }; const MagazinePage = () => { - const programData = generateDummyData(); + const { data: policiesData } = usePolicies({ locationId: 17 }); const influencerData = generateDummyData(); useEffect(() => { @@ -31,9 +34,9 @@ const MagazinePage = () => { - <Typography variant="headingXxSmall">서초구 지원 프로그램</Typography> + <Typography variant="headingXxSmall">종로구 지원 프로그램</Typography> - + <Typography variant="headingXxSmall">인플루언서 꿀팁</Typography> diff --git a/umc-master/src/pages/magazine/components/cardGrid.tsx b/umc-master/src/pages/magazine/components/cardGrid.tsx index 98d9de7..18c53e9 100644 --- a/umc-master/src/pages/magazine/components/cardGrid.tsx +++ b/umc-master/src/pages/magazine/components/cardGrid.tsx @@ -2,6 +2,16 @@ import React from 'react'; import styled from 'styled-components'; import { useNavigate } from 'react-router-dom'; import CardInfo from '@components/Card/CardInfo'; +import dummyImg from '@assets/dummyImage/dummy.jpeg'; + +export interface PolicyData { + id: number; + title: string; + imageUrl: string; + likeCount: number; + bookmarkCount: number; + createAt: string; +} export interface CardGridData { id: string; @@ -13,13 +23,25 @@ export interface CardGridData { } interface CardGridProps { - cards: CardGridData[]; + cards: PolicyData[] | undefined; } interface ProcessedCardData extends CardGridData { columnSpan: number; } +const transformPolicies = (policies: PolicyData[] | undefined): CardGridData[] => { + if (!policies) return []; + return policies.map((policy) => ({ + id: policy.id.toString(), + image: policy.imageUrl || dummyImg, // 이미지 없을 경우 기본값 + text: policy.title, + likes: policy.likeCount ?? 0, // undefined 방지 + bookmarks: policy.bookmarkCount ?? 0, // undefined 방지 + date: new Date(policy.createAt).toLocaleDateString('ko-KR'), + })); +}; + const generatePattern = (): number[] => { const patterns = [ [2, 1], // 큰 카드 - 작은 카드 @@ -46,7 +68,9 @@ const applyPatternToCards = (cards: CardGridData[]): ProcessedCardData[] => { const CardGrid: React.FC = ({ cards }) => { const navigate = useNavigate(); - const updatedCards = applyPatternToCards(cards); + + const transformedCards = transformPolicies(cards); + const updatedCards = applyPatternToCards(transformedCards); const handleClick = (id: string) => { navigate(`/magazine/${id}`); @@ -78,7 +102,7 @@ const GridContainer = styled.div` display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; - align-items: start; // 카드가 위쪽부터 정렬되도록 유지 + align-items: start; `; const GridItem = styled.div<{ columnSpan: number }>` diff --git a/umc-master/src/pages/magazine/components/mindMap.tsx b/umc-master/src/pages/magazine/components/mindMap.tsx index 50b38e5..2ca30fd 100644 --- a/umc-master/src/pages/magazine/components/mindMap.tsx +++ b/umc-master/src/pages/magazine/components/mindMap.tsx @@ -1,8 +1,9 @@ import styled from 'styled-components'; import { motion } from 'framer-motion'; -import Img from '@assets/dummyImage/dummy.jpeg'; +import Img from '@assets/character/magazine.png'; import theme from '@styles/theme'; import Typography from '@components/common/typography'; +import { usePopularHashtags } from '@apis/queries/usePolicyQueries'; const nodes = [ { label: '#재활용1', x: '70%', y: '15%', color: theme.colors.primary[400] }, @@ -14,6 +15,8 @@ const nodes = [ ]; const MindMap = () => { + const { data } = usePopularHashtags({ limit: 6 }); + console.log('인기 관심사 로그', data); return ( @@ -52,17 +55,17 @@ const MindMap = () => { transition={{ duration: 0.8 }} /> - {nodes.map((node, index) => ( + {data?.slice(0, nodes.length).map((hashtag, index) => ( - {node.label} + #{hashtag.name} ))}