diff --git a/src/App.tsx b/src/App.tsx index 4b7378a..6c7e433 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,19 +7,24 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; function App() { const helmetContext = {}; - const queryClient = new QueryClient(); + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + throwOnError: true, + }, + }, + }); return ( - - - - - - - - - + + + + + + + + ); } diff --git a/src/assets/icons/left.svg b/src/assets/icons/left.svg new file mode 100644 index 0000000..b7697cf --- /dev/null +++ b/src/assets/icons/left.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/Layout/WorkDetailHeaderLayout.tsx b/src/components/Layout/WorkDetailHeaderLayout.tsx new file mode 100644 index 0000000..d32e17e --- /dev/null +++ b/src/components/Layout/WorkDetailHeaderLayout.tsx @@ -0,0 +1,16 @@ +import WorkDetailHeader from '../../pages/Work/components/workDetail/WorkDetailHeader'; + +interface WorkDetailHeaderLayoutProps { + children: React.ReactNode; +} + +const WorkDetailHeaderLayout = ({ children }: WorkDetailHeaderLayoutProps) => { + return ( + <> + + {children} + + ); +}; + +export default WorkDetailHeaderLayout; diff --git a/src/constants/QueryKey.ts b/src/constants/QueryKey.ts index 7654318..67c6aea 100644 --- a/src/constants/QueryKey.ts +++ b/src/constants/QueryKey.ts @@ -2,7 +2,9 @@ const WORK_KEYS = { all: ['works'] as const, lists: () => [...WORK_KEYS.all, 'list'] as const, list: (category: string, currentPage: number) => - [...WORK_KEYS.lists(), category, currentPage] as const, + [...WORK_KEYS.lists(), 'infinite', category, currentPage] as const, + prefetchList: (category: string, currentPage: number) => + [...WORK_KEYS.lists(), 'prefetch', category, currentPage] as const, details: () => [...WORK_KEYS.all, 'detail'] as const, detail: (name: string, title: string) => [...WORK_KEYS.details(), name, title] as const, }; diff --git a/src/constants/constants.ts b/src/constants/constants.ts index a9f90aa..88421a7 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -1,7 +1,7 @@ export const NAVIGATION_ITEMS = [ { name: 'ABOUT', path: '/' }, - { name: 'WORKS', path: '/work' }, - { name: 'GUEST', path: '/guest' }, + { name: 'WORKS', path: 'work' }, + { name: 'GUEST', path: 'guest' }, ]; export const MOBILE_BREAKPOINT = 768; @@ -48,23 +48,4 @@ export const WORDS = [ ]; // CategoriesSection.tsx -export const CATEGORIES = ['ALL', 'UX', 'Brand', 'Graphic', 'Illust', 'Media'] as const; - -// 임시로 추가. 서버 연결 후 삭제 예정. -export const MOCK_DATA = [ - { 학생이름: '이가영', 작품이름: '스윗', 이미지: '' }, - { 학생이름: '이가영', 작품이름: '스윗', 이미지: '' }, - { 학생이름: '이가영', 작품이름: '스윗', 이미지: '' }, - { 학생이름: '이가영', 작품이름: '스윗', 이미지: '' }, - { 학생이름: '이가영', 작품이름: '스윗', 이미지: '' }, - { 학생이름: '이가영', 작품이름: '스윗', 이미지: '' }, - { 학생이름: '이가영', 작품이름: '스윗', 이미지: '' }, - { 학생이름: '이가영', 작품이름: '스윗', 이미지: '' }, - { 학생이름: '이가영', 작품이름: '스윗', 이미지: '' }, - { 학생이름: '이가영', 작품이름: '스윗', 이미지: '' }, - { 학생이름: '이가영', 작품이름: '스윗', 이미지: '' }, - { 학생이름: '이가영', 작품이름: '스윗', 이미지: '' }, - { 학생이름: '이가영', 작품이름: '스윗', 이미지: '' }, - { 학생이름: '이가영', 작품이름: '스윗', 이미지: '' }, - { 학생이름: '이가영', 작품이름: '스윗', 이미지: '' }, -]; +export const CATEGORIES = ['ALL', 'UX', 'BRAND', 'GRAPHIC', 'ILLUST', 'MEDIA'] as const; diff --git a/src/hooks/queries/useGetWorkList.tsx b/src/hooks/queries/useGetWorkList.tsx index 5465fa2..970e945 100644 --- a/src/hooks/queries/useGetWorkList.tsx +++ b/src/hooks/queries/useGetWorkList.tsx @@ -3,26 +3,46 @@ import { get } from '../../api/api'; import { WorkListRequestType, WorkListResponseType, WorkListType } from '../../types/types'; import { WORK_KEYS } from '../../constants/QueryKey'; +const getWorkList = async (category: string, currentPage: number) => { + const mappedCategory = (() => { + switch (category) { + case 'ILLUST': + return 'ILLUSTRATION'; + case 'UX': + return 'UXUI'; + case 'BRAND': + return 'BX'; + default: + return category; + } + })(); + + const res = await get( + `work?category=${mappedCategory}¤tPage=${currentPage}` + ); + console.log('API Response:', res); + return res.result.works as WorkListType[]; +}; + export const useGetWorkList = ({ category, currentPage }: WorkListRequestType) => { const { data, hasNextPage, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({ queryKey: WORK_KEYS.list(category, currentPage), queryFn: ({ pageParam = currentPage }) => getWorkList(category, pageParam), getNextPageParam: (lastPage, allPages) => { - return lastPage.length === 10 ? allPages.length + 1 : null; + if (lastPage && Array.isArray(lastPage) && lastPage?.length === 10) { + return allPages?.length ? allPages.length + 1 : 1; + } + return null; }, initialPageParam: 1, select: (data) => ({ - pages: data?.pages.flatMap((page) => page), - pageParams: data.pageParams, + pages: data?.pages.flatMap((page) => page) || [], + pageParams: data?.pageParams || [], }), + staleTime: 1000 * 60 * 5, + // refetchOnWindowFocus: false, + // refetchOnMount: false, }); return { data, hasNextPage, fetchNextPage, isFetchingNextPage }; }; - -const getWorkList = async (category: string, currentPage: number) => { - const res = await get( - `work?category=${category}¤tPage=${currentPage}` - ); - return res.result.works as WorkListType[]; -}; diff --git a/src/hooks/queries/usePrefetchWorkList.tsx b/src/hooks/queries/usePrefetchWorkList.tsx index 35537b8..4a9ff1c 100644 --- a/src/hooks/queries/usePrefetchWorkList.tsx +++ b/src/hooks/queries/usePrefetchWorkList.tsx @@ -8,7 +8,7 @@ export const usePrefetchWorkList = () => { const prefetchWorkList = async (category: string, page: number) => { await queryClient.prefetchQuery({ - queryKey: WORK_KEYS.list(category, page), + queryKey: WORK_KEYS.prefetchList(category, page), queryFn: () => getWorkList(category, page), }); }; diff --git a/src/pages/ErrorFallback.tsx b/src/pages/ErrorFallback.tsx deleted file mode 100644 index 7ea6d01..0000000 --- a/src/pages/ErrorFallback.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useNavigate } from 'react-router-dom'; -import DelayedComponent from './DelayedComponent'; - -const ErrorFallback = () => { - const navigate = useNavigate(); - - return ( - -
- 에러가 발생했습니다. - -
-
- ); -}; - -export default ErrorFallback; diff --git a/src/pages/Work/Work.tsx b/src/pages/Work/Work.tsx index f36f539..d2884d6 100644 --- a/src/pages/Work/Work.tsx +++ b/src/pages/Work/Work.tsx @@ -1,35 +1,52 @@ import { Helmet } from 'react-helmet-async'; -import CategoriesSection from './components/CategoriesSection'; -import ExhibitionSection from './components/ExhibitionSection'; +import CategoriesSection from './components/work/CategoriesSection'; +import ExhibitionSection from './components/work/ExhibitionSection'; import styled from 'styled-components'; import { useGetWorkList } from '../../hooks/queries/useGetWorkList'; import { useState } from 'react'; import { Category } from '../../types/types'; +import { useIsMobile } from '../../hooks/useIsMobile'; +import MobileHeader from './components/mobile/MobileHeader'; -function Work() { +const Work = () => { const [category, setCategory] = useState('ALL'); const { data, hasNextPage, fetchNextPage, isFetchingNextPage } = useGetWorkList({ category, currentPage: 1, }); + const isMobile = useIsMobile(); return ( Digging Club - Work -
- - -
+ {isMobile ? ( + + + + + ) : ( + + + + + )}
); -} +}; export default Work; @@ -38,6 +55,14 @@ const WorkPage = styled.div` display: flex; justify-content: center; +`; + +const PcDiv = styled.div` + padding: 6rem 0 10.4rem 0; +`; - padding: 80px 0 104px 0; +const MobileDiv = styled.div` + display: flex; + flex-direction: column; + align-items: center; `; diff --git a/src/pages/Work/WorkDetail.tsx b/src/pages/Work/WorkDetail.tsx index a9e9879..b1fe3e5 100644 --- a/src/pages/Work/WorkDetail.tsx +++ b/src/pages/Work/WorkDetail.tsx @@ -1,14 +1,18 @@ import styled from 'styled-components'; -import WorkInfoSection from './components/WorkInfoSection'; +import WorkInfoSection from './components/workDetail/WorkInfoSection'; import YouTube from 'react-youtube'; import { useGetWorkDetail } from '../../hooks/queries/useGetWorkDetail'; import { useParams } from 'react-router-dom'; +import { useIsMobile } from '../../hooks/useIsMobile'; +import MobileWorkDetail from './components/mobile/MobileWorkDetail'; const WorkDetail = () => { const params = useParams(); const name = params.name; const title = params.title; + const isMobile = useIsMobile(); + if (!name || !title) { throw new Error('[에러 발생]작품명이나 작가 이름값이 존재하는지 확인하세요.'); } @@ -18,23 +22,28 @@ const WorkDetail = () => { if (!result) { return ; } - - return ( + return isMobile ? ( + + ) : ( - + {result.videoUrl ? ( + + ) : ( + <> + )} @@ -44,23 +53,24 @@ const WorkDetail = () => { export default WorkDetail; const WorkDetailPage = styled.div` - padding-top: 80px; - padding-bottom: 104px; + margin-top: 6rem; + margin-bottom: 10.4rem; display: flex; justify-content: center; - column-gap: 40px; + column-gap: 4rem; `; const WorkDetailContent = styled.div` + margin-top: 3.3rem; display: flex; flex-direction: column; `; const WorkImg = styled.img` - width: 950px; - min-height: 838px; + width: 95rem; + min-height: 83.8rem; object-fit: cover; background-color: gray; diff --git a/src/pages/DelayRouteWrapper.tsx b/src/pages/Work/components/error/DelayRouteWrapper.tsx similarity index 87% rename from src/pages/DelayRouteWrapper.tsx rename to src/pages/Work/components/error/DelayRouteWrapper.tsx index 9c8c702..78f7df1 100644 --- a/src/pages/DelayRouteWrapper.tsx +++ b/src/pages/Work/components/error/DelayRouteWrapper.tsx @@ -1,8 +1,9 @@ import { Suspense } from 'react'; import { QueryErrorResetBoundary } from '@tanstack/react-query'; import { ErrorBoundary } from 'react-error-boundary'; -import DelayedComponent from '../pages/DelayedComponent'; -import ErrorFallback from '../pages/ErrorFallback'; +import ErrorFallback from './ErrorFallback'; +import DelayedComponent from './DelayedComponent'; + const DelayRouteWrapper = ({ children }: { children: React.ReactNode }) => { return ( diff --git a/src/pages/DelayedComponent.tsx b/src/pages/Work/components/error/DelayedComponent.tsx similarity index 88% rename from src/pages/DelayedComponent.tsx rename to src/pages/Work/components/error/DelayedComponent.tsx index 7d6c75d..73e44ef 100644 --- a/src/pages/DelayedComponent.tsx +++ b/src/pages/Work/components/error/DelayedComponent.tsx @@ -4,10 +4,9 @@ const DelayedComponent = ({ children }: PropsWithChildren<{}>) => { const [isDelayed, setIsDelayed] = useState(false); useEffect(() => { - console.log('타이머 시작'); const timeoutId = setTimeout(() => { setIsDelayed(true); - }, 2000); + }, 1000); return () => clearTimeout(timeoutId); }, []); diff --git a/src/pages/Work/components/error/ErrorBtn.tsx b/src/pages/Work/components/error/ErrorBtn.tsx new file mode 100644 index 0000000..546b907 --- /dev/null +++ b/src/pages/Work/components/error/ErrorBtn.tsx @@ -0,0 +1,25 @@ +import styled from 'styled-components'; + +interface ErrorBtnProps { + onClick: VoidFunction; + text: string; +} +const ErrorBtn = ({ onClick, text }: ErrorBtnProps) => { + return {text}; +}; + +export default ErrorBtn; + +const BtnWrapper = styled.div` + width: 10rem; + height: 3rem; + + display: flex; + justify-content: center; + align-items: center; + + font-size: 1.4rem; + border-radius: 1rem; + background-color: gray; + cursor: pointer; +`; diff --git a/src/pages/Work/components/error/ErrorFallback.tsx b/src/pages/Work/components/error/ErrorFallback.tsx new file mode 100644 index 0000000..4b6425d --- /dev/null +++ b/src/pages/Work/components/error/ErrorFallback.tsx @@ -0,0 +1,45 @@ +import { useNavigate } from 'react-router-dom'; +import DelayedComponent from './DelayedComponent'; +import { styled } from 'styled-components'; +import ErrorBtn from './ErrorBtn'; +import { FallbackProps } from 'react-error-boundary'; + +const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => { + const navigate = useNavigate(); + console.log('Error', error); + return ( + + + 에러가 발생했습니다 + + resetErrorBoundary()} /> + navigate('/')} /> + + + + ); +}; + +export default ErrorFallback; + +const ErrorWrapper = styled.div` + width: 100%; + height: 30dvh; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + row-gap: 3rem; +`; + +const ErrorSpan = styled.span` + font-size: 2rem; +`; + +const ErrorBtnWrapper = styled.div` + display: flex; + flex-direction: column; + row-gap: 1rem; +`; diff --git a/src/pages/Work/components/mobile/MobileCategoriesSection.tsx b/src/pages/Work/components/mobile/MobileCategoriesSection.tsx new file mode 100644 index 0000000..123799e --- /dev/null +++ b/src/pages/Work/components/mobile/MobileCategoriesSection.tsx @@ -0,0 +1,74 @@ +import { styled } from 'styled-components'; +import { Category } from '../../../../types/types'; +import { CATEGORIES } from '../../../../constants/constants'; +import { useQueryClient } from '@tanstack/react-query'; +import { usePrefetchWorkList } from '../../../../hooks/queries/usePrefetchWorkList'; +import { WORK_KEYS } from '../../../../constants/QueryKey'; + +interface MobileCategoriesSectionProps { + category: Category; + setCategory: React.Dispatch>; +} +const MobileCategoriesSection = ({ category, setCategory }: MobileCategoriesSectionProps) => { + const queryClient = useQueryClient(); + const { prefetchWorkList } = usePrefetchWorkList(); + + const handleClick = (name: Category) => { + queryClient.removeQueries({ queryKey: WORK_KEYS.all }); + prefetchWorkList(category, 1); + setCategory(name); + }; + + return ( + + {CATEGORIES.map((item) => ( + handleClick(item)} selected={category === item}> + {item} + + ))} + + ); +}; + +export default MobileCategoriesSection; + +const MobileCategoriesWrapper = styled.section` + min-width: 34.3rem; + + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr; + grid-gap: 0; + + background-color: #121212; +`; + +const CategoriesItem = styled.span<{ selected: boolean }>` + height: 4.4rem; + + display: flex; + justify-content: center; + align-items: center; + + font-size: 2rem; + line-height: 120%; + font-style: normal; + font-weight: 900; + color: ${({ theme, selected }) => (selected ? theme.colors.primary : 'white')}; + + cursor: pointer; + + border: 1px solid #fff; + + &:nth-child(2), + &:nth-child(5) { + border-left: none; + border-right: none; + } + + &:nth-child(1), + &:nth-child(2), + &:nth-child(3) { + border-bottom: none; + } +`; diff --git a/src/pages/Work/components/mobile/MobileHeader.tsx b/src/pages/Work/components/mobile/MobileHeader.tsx new file mode 100644 index 0000000..dcb105a --- /dev/null +++ b/src/pages/Work/components/mobile/MobileHeader.tsx @@ -0,0 +1,55 @@ +import { useState, useEffect } from 'react'; +import { styled } from 'styled-components'; +import { Category } from '../../../../types/types'; +import MobileCategoriesSection from './MobileCategoriesSection'; + +interface MobileHeaderProps { + category: Category; + setCategory: React.Dispatch>; +} + +const MobileHeader = ({ category, setCategory }: MobileHeaderProps) => { + const [showHeader, setShowHeader] = useState(true); + const [lastScrollY, setLastScrollY] = useState(0); + + const controlHeader = () => { + const currentScrollY = window.scrollY; + + if (currentScrollY > lastScrollY) { + setShowHeader(false); + } else { + setShowHeader(true); + } + + setLastScrollY(currentScrollY); + }; + + useEffect(() => { + window.addEventListener('scroll', controlHeader); + + return () => { + window.removeEventListener('scroll', controlHeader); + }; + }, [lastScrollY]); + + return ( + + + + ); +}; + +export default MobileHeader; + +const StyledHeader = styled.header<{ show: boolean }>` + width: 34.3rem; + position: fixed; + top: ${({ show }) => (show ? '8.4rem' : '-9rem')}; + width: 100%; + height: 8rem; + + display: flex; + justify-content: center; + align-items: center; + transition: top 0.1s ease-in-out; +`; diff --git a/src/pages/Work/components/mobile/MobileWorkDetail.tsx b/src/pages/Work/components/mobile/MobileWorkDetail.tsx new file mode 100644 index 0000000..19c6b8f --- /dev/null +++ b/src/pages/Work/components/mobile/MobileWorkDetail.tsx @@ -0,0 +1,81 @@ +import styled from 'styled-components'; +import YouTube from 'react-youtube'; +import { useGetWorkDetail } from '../../../../hooks/queries/useGetWorkDetail'; +import { useParams } from 'react-router-dom'; +import MobileWorkInfoSection from './MobileWorkInfoSection'; +import MobileWorkDetailContact from './MobileWorkDetailContact'; +import WorkDetailHeaderLayout from '../../../../components/Layout/WorkDetailHeaderLayout'; + +const MobileWorkDetail = () => { + const params = useParams(); + const name = params.name; + const title = params.title; + + if (!name || !title) { + throw new Error('[에러 발생]작품명이나 작가 이름값이 존재하는지 확인하세요.'); + } + + const { data } = useGetWorkDetail({ name, title }); + const result = data?.result; + if (!result) { + return ; + } + return ( + <> + + + + + {result.videoUrl ? ( + + ) : ( + <> + )} + + + + + + + ); +}; + +export default MobileWorkDetail; + +const MobileWorkDetailPage = styled.div` + display: flex; + flex-direction: column; + align-items: center; + + column-gap: 4rem; +`; + +const WorkDetailContent = styled.div` + display: flex; + flex-direction: column; + align-items: center; +`; + +const WorkImg = styled.img` + width: 34.3rem; + min-height: 60rem; + margin-bottom: 4rem; + + object-fit: cover; + background-color: gray; +`; diff --git a/src/pages/Work/components/mobile/MobileWorkDetailContact.tsx b/src/pages/Work/components/mobile/MobileWorkDetailContact.tsx new file mode 100644 index 0000000..96392f2 --- /dev/null +++ b/src/pages/Work/components/mobile/MobileWorkDetailContact.tsx @@ -0,0 +1,64 @@ +import { styled } from 'styled-components'; + +interface MobileContactProps { + name: string; + studentId: string; + contact: string; +} + +const MobileWorkDetailContact = ({ name, studentId, contact }: MobileContactProps) => { + return ( + + + {name} + {studentId} + + {contact} + + ); +}; + +export default MobileWorkDetailContact; + +const MobileFooterWrapper = styled.section` + width: 100%; + min-width: 37.5rem; + padding: 2rem 1.6rem 0 1.6rem; + margin-bottom: 10rem; + + display: flex; + justify-content: space-between; + border-top: 1px solid #fff; +`; + +const StudentInfoWrapper = styled.div` + display: flex; + column-gap: 1.44rem; +`; + +const NameSpan = styled.span` + color: #fff; + + font-size: 1.2rem; + font-style: normal; + font-weight: 700; + line-height: 100%; +`; + +const IdSpan = styled.span` + color: #6a7574; + + font-size: 1.2rem; + font-style: normal; + font-weight: 500; + line-height: 100%; +`; + +const ContactSpan = styled.span` + color: #fff; + + font-size: 1.2rem; + font-style: normal; + font-weight: 500; + line-height: 100%; +`; diff --git a/src/pages/Work/components/mobile/MobileWorkInfoSection.tsx b/src/pages/Work/components/mobile/MobileWorkInfoSection.tsx new file mode 100644 index 0000000..2145f23 --- /dev/null +++ b/src/pages/Work/components/mobile/MobileWorkInfoSection.tsx @@ -0,0 +1,114 @@ +import styled from 'styled-components'; +import { WorkDetailType } from '../../../../types/types'; +interface WorkInfoSectionProps { + data: WorkDetailType; +} + +const MobileWorkInfoSection = ({ data }: WorkInfoSectionProps) => { + return ( + + + {data.category} + + {data.studentName} + {data.studentId} + + + + 제목 : {data.title} + {data.subtitle} + {data.description} + + + ); +}; + +export default MobileWorkInfoSection; + +const WorkInfoWrapper = styled.section` + width: 100%; + min-width: 37.5rem; + + display: flex; + flex-direction: column; + align-items: center; +`; + +const MobileCategoryWrapper = styled.div` + width: 100%; + min-width: 37.5rem; + + height: 6.2rem; + margin-bottom: 4rem; + + display: flex; + justify-content: space-between; + align-items: center; + + border-bottom: 1px solid #fff; +`; + +const WorkCategory = styled.span` + margin-left: 1.6rem; + + font-size: 1.8rem; + line-height: 120%; + color: ${({ theme }) => theme.colors.primary}; +`; + +const WorkAuthorInfo = styled.div` + margin-right: 1.6rem; + + display: flex; + column-gap: 1.45rem; +`; + +const AuthorSpan = styled.span` + font-size: 1.2rem; + line-height: 100%; + + &.studentId { + color: #7ca2b0; + } +`; + +const InfoBodyWrapper = styled.div` + width: 37.5rem; + + display: flex; + flex-direction: column; + align-items: flex-start; + + padding: 0 1.6rem; +`; + +const WorkTitle = styled.span` + margin-bottom: 1.2rem; + + font: ${({ theme }) => theme.fonts.Primary}; + font-size: 2.4rem; + font-style: normal; + font-weight: 900; + line-height: 120%; +`; + +const WorkSubtitle = styled.span` + margin-bottom: 4rem; + + font-size: 1.2rem; + font-style: normal; + font-weight: 700; + line-height: 100%; +`; + +const WorkBody = styled.span` + width: 34.3rem; + margin-bottom: 4rem; + + font-size: 1.2rem; + font-style: normal; + font-weight: 400; + line-height: 140%; + + color: #6a7574; +`; diff --git a/src/pages/Work/components/CategoriesSection.tsx b/src/pages/Work/components/work/CategoriesSection.tsx similarity index 79% rename from src/pages/Work/components/CategoriesSection.tsx rename to src/pages/Work/components/work/CategoriesSection.tsx index 22d008d..acac500 100644 --- a/src/pages/Work/components/CategoriesSection.tsx +++ b/src/pages/Work/components/work/CategoriesSection.tsx @@ -1,9 +1,9 @@ import { styled } from 'styled-components'; -import { Category } from '../../../types/types'; -import { CATEGORIES } from '../../../constants/constants'; +import { Category } from '../../../../types/types'; +import { CATEGORIES } from '../../../../constants/constants'; import { useQueryClient } from '@tanstack/react-query'; -import { usePrefetchWorkList } from '../../../hooks/queries/usePrefetchWorkList'; -import { WORK_KEYS } from '../../../constants/QueryKey'; +import { usePrefetchWorkList } from '../../../../hooks/queries/usePrefetchWorkList'; +import { WORK_KEYS } from '../../../../constants/QueryKey'; interface CategoriesSectionProps { category: Category; @@ -42,7 +42,8 @@ const CategoriesWrapper = styled.section` `; const CategoriesItem = styled.span<{ selected: boolean }>` - font-size: 40px; + ${({ theme }) => theme.fonts.primary}; + font-size: 4rem; line-height: 120%; color: ${({ theme, selected }) => (selected ? theme.colors.primary : 'white')}; diff --git a/src/pages/Work/components/ExhibitionSection.tsx b/src/pages/Work/components/work/ExhibitionSection.tsx similarity index 56% rename from src/pages/Work/components/ExhibitionSection.tsx rename to src/pages/Work/components/work/ExhibitionSection.tsx index c339ff7..40ce59a 100644 --- a/src/pages/Work/components/ExhibitionSection.tsx +++ b/src/pages/Work/components/work/ExhibitionSection.tsx @@ -1,8 +1,8 @@ import { styled } from 'styled-components'; -import WorkCardItem from './WorkCardItem'; -import { WorkListType } from '../../../types/types'; -import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; +import { WorkListType } from '../../../../types/types'; +import { useIntersectionObserver } from '../../../../hooks/useIntersectionObserver'; import { InfiniteQueryObserverResult } from '@tanstack/react-query'; +import WorkCardItem from './WorkCardItem'; interface ExhibitionSectionProps { data: @@ -14,6 +14,7 @@ interface ExhibitionSectionProps { hasNextPage: boolean; fetchNextPage: () => Promise; isFetchingNextPage: boolean; + isMobile: boolean; } const ExhibitionSection = ({ @@ -21,9 +22,10 @@ const ExhibitionSection = ({ hasNextPage, fetchNextPage, isFetchingNextPage, + isMobile, }: ExhibitionSectionProps) => { if (!data?.pages && !isFetchingNextPage) { - return ; + return ; } // intersectionObserver 호출 @@ -32,9 +34,8 @@ const ExhibitionSection = ({ fetchNextPage, }); - console.log('data', data); return ( - + {data?.pages.map((work, index) => { const isLastItem = index === data.pages.length - 1; return ( @@ -53,12 +54,15 @@ const ExhibitionSection = ({ export default ExhibitionSection; -const ExhibitionWrapper = styled.div` - width: 950px; - margin-left: 33.85%; +const ExhibitionWrapper = styled.div<{ isMobile: boolean }>` + display: ${({ isMobile }) => (isMobile ? 'flex' : null)}; + justify-content: ${({ isMobile }) => (isMobile ? 'center' : null)}; + width: ${({ isMobile }) => (isMobile ? '34.3rem' : '95rem')}; + margin-left: ${({ isMobile }) => (isMobile ? null : '33.85%')}; + margin-top: ${({ isMobile }) => (isMobile ? '14.8rem' : null)}; display: grid; - grid-template-rows: repeat(5, 1fr); - grid-template-columns: repeat(2, 1fr); - gap: 20px; + grid-template-rows: ${({ isMobile }) => (isMobile ? 'repeat(10, 1fr)' : 'repeat(5, 1fr)')}; + grid-template-columns: ${({ isMobile }) => (isMobile ? 'repeat(1, 1fr)' : 'repeat(2, 1fr)')}; + gap: ${({ isMobile }) => (isMobile ? '1.6rem' : '1.4rem')}; `; diff --git a/src/pages/Work/components/WorkCardItem.tsx b/src/pages/Work/components/work/WorkCardItem.tsx similarity index 65% rename from src/pages/Work/components/WorkCardItem.tsx rename to src/pages/Work/components/work/WorkCardItem.tsx index fb21b7d..66c024b 100644 --- a/src/pages/Work/components/WorkCardItem.tsx +++ b/src/pages/Work/components/work/WorkCardItem.tsx @@ -1,7 +1,8 @@ import { useNavigate } from 'react-router-dom'; import { styled } from 'styled-components'; -import '../../../styles/animations.css'; -import { usePrefetchWorkDetail } from '../../../hooks/queries/usePrefetchWorkDetail'; +import '../../../../styles/animations.css'; +import { usePrefetchWorkDetail } from '../../../../hooks/queries/usePrefetchWorkDetail'; +import { useIsMobile } from '../../../../hooks/useIsMobile'; interface CardInfoProps { name: string; @@ -14,6 +15,7 @@ interface CardInfoProps { const WorkCardItem = ({ name, title, imgUrl, isLastItem, setTarget }: CardInfoProps) => { const navigate = useNavigate(); const { prefetchWorkDetail } = usePrefetchWorkDetail(); + const isMobile = useIsMobile(); const handleClick = () => { prefetchWorkDetail(name, title); @@ -24,31 +26,27 @@ const WorkCardItem = ({ name, title, imgUrl, isLastItem, setTarget }: CardInfoPr - {name} - {title} - + ref={isLastItem ? setTarget : null} + isMobile={isMobile} + /> ); }; export default WorkCardItem; -const WorkCardItemWrapper = styled.div<{ imgUrl?: string }>` - width: 466px; - height: 262px; +const WorkCardItemWrapper = styled.div<{ imgUrl?: string; isMobile: boolean }>` + width: ${({ isMobile }) => (isMobile ? '34.3rem' : '46.6rem')}; + height: ${({ isMobile }) => (isMobile ? '19rem' : '26.2rem')}; flex-shrink: 0; - padding: 27.85px 27.91px; display: flex; flex-direction: column; justify-content: end; - row-gap: 11.14px; - border: 2px solid white; + border: ${({ isMobile }) => (isMobile ? '1px' : '2px')} solid white; background-color: gray; - background-image: ${({ imgUrl }) => imgUrl || ''}; + background-image: ${({ imgUrl }) => (imgUrl ? `url(${imgUrl})` : '')}; background-size: cover; background-position: center; @@ -65,8 +63,3 @@ const WorkCardItemWrapper = styled.div<{ imgUrl?: string }>` animation-iteration-count: 1; } `; - -const ItemSpan = styled.span` - font-size: 16px; - line-height: 100%; -`; diff --git a/src/pages/Work/components/workDetail/WorkDetailHeader.tsx b/src/pages/Work/components/workDetail/WorkDetailHeader.tsx new file mode 100644 index 0000000..b468bbd --- /dev/null +++ b/src/pages/Work/components/workDetail/WorkDetailHeader.tsx @@ -0,0 +1,30 @@ +import { useNavigate } from 'react-router-dom'; +import IcLeft from '../../../../assets/icons/left.svg'; +import { styled } from 'styled-components'; + +const WorkDetailHeader = () => { + const navigate = useNavigate(); + const handleIcon = () => { + navigate(-1); + }; + + return ( + + + + ); +}; + +export default WorkDetailHeader; + +const WorkDetailHeaderWrapper = styled.div` + min-width: 37.5rem; + + display: flex; + justify-content: left; + padding: 1rem 1.6rem; +`; + +const Icon = styled.img` + cursor: pointer; +`; diff --git a/src/pages/Work/components/WorkInfoSection.tsx b/src/pages/Work/components/workDetail/WorkInfoSection.tsx similarity index 58% rename from src/pages/Work/components/WorkInfoSection.tsx rename to src/pages/Work/components/workDetail/WorkInfoSection.tsx index 21b5abf..95a7cee 100644 --- a/src/pages/Work/components/WorkInfoSection.tsx +++ b/src/pages/Work/components/workDetail/WorkInfoSection.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import { WorkDetailType } from '../../../types/types'; +import { WorkDetailType } from '../../../../types/types'; interface WorkInfoSectionProps { data: WorkDetailType; } @@ -8,13 +8,14 @@ const WorkInfoSection = ({ data }: WorkInfoSectionProps) => { return ( {data.category} + 제목 : {data.title} + {data.subtitle} {data.studentName} - {data.studentId} + {data.studentId} - 제목 : {data.title} - {data.subtitle} {data.description} + {data.contact} ); }; @@ -22,46 +23,67 @@ const WorkInfoSection = ({ data }: WorkInfoSectionProps) => { export default WorkInfoSection; const WorkInfoWrapper = styled.section` - width: 380px; + width: 29rem; display: flex; flex-direction: column; `; const WorkCategory = styled.span` - margin-bottom: 30px; + margin-bottom: 6rem; - font-size: 50px; + font-size: 5rem; line-height: 120%; color: ${({ theme }) => theme.colors.primary}; `; const WorkAuthorInfo = styled.div` - margin-bottom: 40px; + margin-bottom: 4rem; display: flex; - column-gap: 67px; + column-gap: 6.8rem; `; const AuthorSpan = styled.span` - font-size: 28px; + font-size: 1.6rem; line-height: 100%; + + &.studentId { + color: #7ca2b0; + } `; const WorkTitle = styled.span` - margin-bottom: 30px; + margin-bottom: 2rem; - font-size: 32px; + font-size: 3.2rem; line-height: 120%; `; + const WorkSubtitle = styled.span` - margin-bottom: 30px; + margin-bottom: 4rem; - font-size: 16px; + font-size: 1.6rem; line-height: 120%; `; const WorkBody = styled.span` - font-size: 16px; + width: 29rem; + margin-bottom: 23rem; + + font-size: 1.6rem; + font-style: normal; + font-weight: 400; line-height: 140%; + + color: #7ca2b0; +`; + +const WorkContact = styled.span` + color: ${({ theme }) => theme.colors.white}; + + font-size: 1.6rem; + font-style: normal; + font-weight: 500; + line-height: 100%; `; diff --git a/src/routes/Router.tsx b/src/routes/Router.tsx index 46225dc..421be6b 100644 --- a/src/routes/Router.tsx +++ b/src/routes/Router.tsx @@ -4,7 +4,7 @@ import Work from '../pages/Work/Work'; import Guest from '../pages/Guest/Guest'; import App from '../App'; import WorkDetail from '../pages/Work/WorkDetail'; -import DelayRouteWrapper from '../pages/DelayRouteWrapper'; +import DelayRouteWrapper from '../pages/Work/components/error/DelayRouteWrapper'; const router = createBrowserRouter([ { diff --git a/src/types/types.ts b/src/types/types.ts index df009c8..51eb20e 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -45,4 +45,5 @@ export type WorkDetailType = { description: string; detailArtUrl: string; thumbnailUrl: string; + videoUrl: string; };