diff --git a/.vscode/settings.json b/.vscode/settings.json index c1c068c..1dc93b2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,13 +14,9 @@ ], "eslint.workingDirectories": [ { - "mode": "auto", + "mode": "auto" } ], - "cSpell.words": [ - "kakao", - "signup", - "webp" - ], + "cSpell.words": ["kakao", "signup", "webp"], "editor.tabSize": 2 } diff --git a/apps/client/src/ModalPortal.ts b/apps/client/src/ModalPortal.ts index afd79b6..6c87cd0 100644 --- a/apps/client/src/ModalPortal.ts +++ b/apps/client/src/ModalPortal.ts @@ -6,9 +6,9 @@ interface ModalPortalProps { } export const ModalPortal: React.FC = ({ children }) => { - const el = document.getElementById('modal'); - if (!el) { + const modalElement = document.getElementById('modal'); + if (!modalElement) { return null; } - return ReactDOM.createPortal(children, el); + return ReactDOM.createPortal(children, modalElement); }; diff --git a/apps/client/src/apis/axios.ts b/apps/client/src/apis/axios.ts index 7844298..ba13f43 100644 --- a/apps/client/src/apis/axios.ts +++ b/apps/client/src/apis/axios.ts @@ -1,4 +1,4 @@ -import axios, { AxiosError } from 'axios'; +import axios, { AxiosError } from "axios"; const axiosInstance = axios.create({ baseURL: process.env.SERVER_URL, @@ -6,22 +6,29 @@ const axiosInstance = axios.create({ }); axiosInstance.interceptors.response.use( - response => { + (response) => { return response; }, - async error => { + async (error) => { const customError = error as AxiosError; const axiosError = customError.response?.status as number; - - // 401 에러 응답 - if (axiosError === 401) { + // 401 에러 응답 (인가 미들웨어에서 모든 토큰이 만료되었을때 401 리턴 -> 로그인 유도) + if (axiosError === 401 && window.location.pathname !== "/") { + if ( + /^\/cake\/[^/]+$/.test(window.location.pathname) || + /^\/letter\/choose\/[^/]+$/.test(window.location.pathname) || + /^\/letter\/create\/[^/]+$/.test(window.location.pathname) + ) { + return Promise.reject(error); + } + window.location.replace("/"); return Promise.reject(error); } // 500 에러 응답 if (axiosError === 500) { - console.log('서버 오류'); + console.log("서버 오류"); return Promise.reject(error); } @@ -30,10 +37,8 @@ axiosInstance.interceptors.response.use( return Promise.reject(error); } - return Promise.reject(error); - }, + } ); - export default axiosInstance; diff --git a/apps/client/src/apis/cake/useGetCakeLetters.tsx b/apps/client/src/apis/cake/useGetCakeLetters.tsx index 1462668..3af0ad3 100644 --- a/apps/client/src/apis/cake/useGetCakeLetters.tsx +++ b/apps/client/src/apis/cake/useGetCakeLetters.tsx @@ -13,5 +13,6 @@ export const useGetCakeLetters = (ownerId: string, year: string, page: number) = return useSuspenseQuery({ queryKey: ['cake-letters', ownerId, year, page], queryFn: () => fetchCakeLetters(ownerId, year, page), + staleTime: 1000 * 60, }); } diff --git a/apps/client/src/apis/cake/useGetYear.tsx b/apps/client/src/apis/cake/useGetYear.tsx index 9d184a4..a8572e8 100644 --- a/apps/client/src/apis/cake/useGetYear.tsx +++ b/apps/client/src/apis/cake/useGetYear.tsx @@ -10,6 +10,7 @@ async function fetchYear() { export const useGetYear = () => { return useSuspenseQuery({ queryKey: ['get-year'], - queryFn: () => fetchYear() + queryFn: () => fetchYear(), + staleTime: 1000 * 60 * 5, }) } diff --git a/apps/client/src/assets/icons/GridIcon.tsx b/apps/client/src/assets/icons/GridIcon.tsx index ab621d7..cdf8e1e 100644 --- a/apps/client/src/assets/icons/GridIcon.tsx +++ b/apps/client/src/assets/icons/GridIcon.tsx @@ -7,8 +7,8 @@ const GridIcon = (props: SVGProps) => { xmlns="http://www.w3.org/2000/svg" > diff --git a/apps/client/src/assets/icons/PrevIcon.tsx b/apps/client/src/assets/icons/PrevIcon.tsx index c6c9e76..9139974 100644 --- a/apps/client/src/assets/icons/PrevIcon.tsx +++ b/apps/client/src/assets/icons/PrevIcon.tsx @@ -5,13 +5,13 @@ const PrevIcon = (props: SVGProps) => { ); diff --git a/apps/client/src/components/cake/CakeHeader.tsx b/apps/client/src/components/cake/CakeHeader.tsx index e24ba89..1ff3aa6 100644 --- a/apps/client/src/components/cake/CakeHeader.tsx +++ b/apps/client/src/components/cake/CakeHeader.tsx @@ -1,61 +1,9 @@ import React from 'react'; -import styled from 'styled-components'; +import * as C from '#styles/CakeStyle.tsx'; import { useNavigate } from 'react-router-dom'; import { PersonIcon } from '#icons'; import useIsPC from '#hooks/useIsPc.tsx'; -const HeaderContainer = styled.div` - display: flex; - flex-direction: column; - justify-content: start; - align-items: start; - width: 100%; - padding-bottom: 20px; -`; - -const TitleContainer = styled.div` - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - width: 100%; -`; - -const Title = styled.h1` - font-weight: bold; -`; - -const SubTitle = styled.h2` - margin-top: 0.5rem; -`; - -const Nickname = styled.h1` - color: var(--orange-500); - display: inline; -`; - -const Phrase = styled.h1` - display: inline; -`; - -const MyPageButton = styled.button` - width: 2.6rem; - height: 2.6rem; - border-radius: 50%; - border-width: 0; - overflow: hidden; - cursor: pointer; - display: flex; - flex-direction: row; - justify-content: right; - align-items: center; - background-color: transparent; - - &:hover { - opacity: 0.8; - } -`; - const CakeHeader = ({ nickname, isMyCake, @@ -66,25 +14,25 @@ const CakeHeader = ({ const navigate = useNavigate(); const isPC = useIsPC(1024); return ( - - - - <Nickname>{nickname}</Nickname> - <Phrase>님의 케이크</Phrase> - + + + + {nickname} + 님의 케이크 + {isMyCake && ( - { navigate('/mypage'); }} > - {!isPC && ()} - + {!isPC && ()} + )} - - {isMyCake && 장식초를 눌러 편지를 확인해보세요✉️} - {!isMyCake && 친구의 케이크를 꾸며보세요❤️} - + + {isMyCake && 장식초를 눌러 편지를 확인해보세요✉️} + {!isMyCake && 친구의 케이크를 꾸며보세요❤️} + ); }; diff --git a/apps/client/src/components/cake/CakeInfo.tsx b/apps/client/src/components/cake/CakeInfo.tsx index 3f766f9..2f97461 100644 --- a/apps/client/src/components/cake/CakeInfo.tsx +++ b/apps/client/src/components/cake/CakeInfo.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; -import styled, { css } from 'styled-components'; import { useParams } from 'react-router-dom'; +import * as C from '#styles/CakeStyle.tsx'; + import { CakeColorType } from '@isttp/types/all'; import { getCakeDataRes, @@ -9,15 +10,17 @@ import { getLetterRes, getCakeNoDataRes, } from '@isttp/schemas/all'; + import Pagenation from '#components/cake/Pagenation.tsx'; import RenderCake from '#components/cake/RenderCake.tsx'; import ReadLetter from '#components/letter/ReadLetter.tsx'; -import { useGetCakeLetters } from '#apis/cake/useGetCakeLetters.tsx'; -import { useGetLetter } from '#apis/letter/useGetLetter.tsx'; -import { useQueryClient } from '@tanstack/react-query'; import Modal from '#components/modal/Modal.tsx'; import Button from '#components/common/Button.tsx'; +import { useQueryClient } from '@tanstack/react-query'; +import { useGetCakeLetters } from '#apis/cake/useGetCakeLetters.tsx'; +import { useGetLetter } from '#apis/letter/useGetLetter.tsx'; + interface CakeInfoProps { year: string; sheetColor: CakeColorType | null; @@ -25,6 +28,16 @@ interface CakeInfoProps { isMyCake?: boolean; } +const CandlePositions = [ + { top: 2, left: 30 }, + { top: 6, left: 50 }, + { top: 2, left: 70 }, + { top: 40, left: 20 }, + { top: 47, left: 40 }, + { top: 47, left: 60 }, + { top: 40, left: 80 }, +]; + const CakeInfo: React.FC = ({ year, sheetColor, @@ -41,13 +54,16 @@ const CakeInfo: React.FC = ({ totalPage: 1, }); const queryClient = useQueryClient(); - const { data: cakeLettersData } = useGetCakeLetters(ownerId!, year, pageData.currentPage); - + const { data: cakeLettersData } = useGetCakeLetters( + ownerId!, + year, + pageData.currentPage + ); const { data: letterData } = useGetLetter(selectedLetterId!); useEffect(() => { - const noDataResult = getCakeNoDataRes.safeParse(cakeLettersData); - if (noDataResult.success && noDataResult.data.noData) { + const checkNoData = getCakeNoDataRes.safeParse(cakeLettersData); + if (checkNoData.success && checkNoData.data.noData) { setCakeData([]); setPageData({ currentPage: 1, totalPage: 1 }); } else { @@ -55,7 +71,8 @@ const CakeInfo: React.FC = ({ setCakeData(result.data); setPageData({ currentPage: result.currentPage, - totalPage: result.totalPage === 0 ? result.totalPage + 1 : result.totalPage, + totalPage: + result.totalPage === 0 ? result.totalPage + 1 : result.totalPage, }); } }, [cakeLettersData]); @@ -84,32 +101,22 @@ const CakeInfo: React.FC = ({ }); }; - const candlePositions = [ - { top: 2, left: 30 }, - { top: 6, left: 50 }, - { top: 2, left: 70 }, - { top: 40, left: 20 }, - { top: 47, left: 40 }, - { top: 47, left: 60 }, - { top: 40, left: 80 }, - ]; - const candles = cakeData.map((cake, index) => ({ candleImageUrl: cake.candleImageUrl, nickname: cake.nickname, - position: candlePositions[index % candlePositions.length], + position: CandlePositions[index % CandlePositions.length], })); return ( - - + + - + = ({ keyword={selectedItem?.keyword ?? ''} /> )} - - 편지 내용은 생일 이후에 확인할 수 있어요!{'\n'}두근두근...👉👈 + setOpen(false)}> + + 편지 내용은 생일 이후에 확인할 수 있어요!{'\n'}두근두근...👉👈 + - + ); }; export default CakeInfo; - -const CakeContainer = styled.div` - margin-top: 50px; -`; - -const CakeInfoWrapper = styled.div<{ isMyCake?: boolean }>` - ${({ isMyCake }) => - isMyCake === false && - css` - pointer-events: none; - `} -`; diff --git a/apps/client/src/components/cake/ColorSelector.tsx b/apps/client/src/components/cake/ColorSelector.tsx index dbf12ba..ed9b73e 100644 --- a/apps/client/src/components/cake/ColorSelector.tsx +++ b/apps/client/src/components/cake/ColorSelector.tsx @@ -1,42 +1,7 @@ import React, { SetStateAction } from 'react'; -import styled from 'styled-components'; - +import * as C from '#styles/CakeStyle.tsx'; import { CakeColorType } from '@isttp/types/all'; -const ColorContainer = styled.div` - width: 100%; - display: flex; - justify-content: center; - flex-wrap: wrap; - background-color: rgba(222, 222, 222, 0.5); - padding: 0.6rem; - border-radius: 0.5rem; -`; - -const ColorOption = styled.div<{ $color: string; selected: boolean }>` - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - width: 2.5rem; - height: 2.5rem; - margin: 0.5rem; - border-radius: 50%; - background-color: var(--${(props) => props.$color === 'white' ? 'white' : `${props.$color}-100`}); - cursor: pointer; - transition: border 0.1s; - - &:hover { - border: 5px solid var(--${(props) => props.$color === 'white' ? 'gray-400' : `${props.$color}-200`}); - } - - @media (max-width: 429px) { - width: 2rem; - height: 2rem; - margin: 0.5rem; - } -`; - type ColorSelectorProps = { selectedColor: string | null; setSelectedColor: React.Dispatch>; @@ -56,18 +21,18 @@ const ColorSelector: React.FC = ({ ]; return ( - + {colors.map((color, index) => ( - setSelectedColor(color)} > {selectedColor === color && } - + ))} - + ); }; diff --git a/apps/client/src/components/cake/GridInfo.tsx b/apps/client/src/components/cake/GridInfo.tsx index 4afa3d6..cdcac11 100644 --- a/apps/client/src/components/cake/GridInfo.tsx +++ b/apps/client/src/components/cake/GridInfo.tsx @@ -1,4 +1,10 @@ -import React, { useState, CSSProperties, useRef, useEffect, useCallback } from 'react'; +import React, { + useState, + CSSProperties, + useRef, + useEffect, + useCallback, +} from 'react'; import { useParams } from 'react-router-dom'; import * as G from '#styles/GridStyle.tsx'; import { FixedSizeGrid as Grid } from 'react-window'; @@ -21,8 +27,10 @@ import Modal from '#components/modal/Modal.tsx'; const GRID_PAGE = 24; - -const GridInfo: React.FC<{ year: string, handleTotalChange?: (total: number) => void }> = ({ year: init, handleTotalChange }) => { +const GridInfo: React.FC<{ + year: string; + handleTotalChange?: (total: number) => void; +}> = ({ year: init, handleTotalChange }) => { const [year, setYear] = useState(init); const [cakeData, setCakeData] = useState([]); const [page, setPage] = useState(1); @@ -39,13 +47,15 @@ const GridInfo: React.FC<{ year: string, handleTotalChange?: (total: number) => const COL_WIDTH = isPC ? 155 : 130; const WIDTH = isPC ? 155 * 5 : 390; - const { data, isFetching } = year === 'all' - ? useGetMyLetters(page) - : useGetLetters(ownerId!, year, page); + const { data, isFetching } = + year === 'all' + ? useGetMyLetters(page) + : useGetLetters(ownerId!, year, page); - const { data: letterData } = year === 'all' - ? useGetAllLetter(selectedLetterId!) - : useGetLetter(selectedLetterId!); + const { data: letterData } = + year === 'all' + ? useGetAllLetter(selectedLetterId!) + : useGetLetter(selectedLetterId!); useEffect(() => { if (data) { @@ -59,7 +69,9 @@ const GridInfo: React.FC<{ year: string, handleTotalChange?: (total: number) => const result = getCakeLettersRes.parse(data); setNoData(false); setHasMore(result.data.length >= GRID_PAGE); - setCakeData((prev) => (page === 1 ? result.data : [...prev, ...result.data])); + setCakeData((prev) => + page === 1 ? result.data : [...prev, ...result.data] + ); handleTotalChange?.(result.totalPage); } } @@ -202,8 +214,10 @@ const GridInfo: React.FC<{ year: string, handleTotalChange?: (total: number) => candleImageUrl={selectedItem?.candleImageUrl ?? ''} keyword={selectedItem?.keyword ?? ''} /> - - 편지 내용은 생일 이후에 확인할 수 있어요! + setOpen(false)}> + + 편지 내용은 생일 이후에 확인할 수 있어요! + - - - - 편지를 작성하면 포인트를 얻을 수 있어요.{'\n'}로그인 하시겠어요? + + setOpen(false)}> + + 편지를 작성하면 포인트를 얻을 수 있어요.{'\n'}로그인 하시겠어요? + @@ -87,4 +87,4 @@ const ShareBox = styled.div` justify-content: center; align-items: center; gap: 24px; -` +`; diff --git a/apps/client/src/pages/ChooseCandle.tsx b/apps/client/src/pages/ChooseCandle.tsx index 395ee3c..f7cc477 100644 --- a/apps/client/src/pages/ChooseCandle.tsx +++ b/apps/client/src/pages/ChooseCandle.tsx @@ -48,7 +48,7 @@ const ChooseCandle = () => { try { // 선택한 장식초 정보 가져오기 const candleResponse = await axiosInstance.get( - `/candle/${candleId}`, + `/candle/${candleId}` ); if (candleResponse.status === 200) { const data = CandleType.parse(candleResponse.data); @@ -118,7 +118,7 @@ const ChooseCandle = () => { if (candle.point === 0) { // 무료 장식초: 편지 페이지로 이동 navigate( - `/letter/create/${ownerId}?candleId=${candle.candleId}`, + `/letter/create/${ownerId}?candleId=${candle.candleId}` ); } else { // 유료 장식초: 결제 혹은 로그인 유도 모달 띄우기 @@ -135,15 +135,15 @@ const ChooseCandle = () => { ))} - + {candle?.candleId.toString()} -

해당 장식초를 구매하시겠습니까?

+ 남은 포인트: {userPoint}P - 결제 포인트: {candle?.point}P + 결제 포인트: {candle?.point}P {!isEnoughPoint && ( 포인트가 부족합니다. )} @@ -155,11 +155,7 @@ const ChooseCandle = () => { gap: '1rem', }} > -
- + setOpenSuccess(false)}> {candle?.candleId.toString()} -

장식초 구매가 완료되었습니다.

- 남은 포인트: {userPoint}P + + 남은 포인트: {userPoint}P