diff --git a/src/App.tsx b/src/App.tsx index 5a958a3..40ac487 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,10 +27,22 @@ import RandomLettersPage from './pages/RandomLetters'; import RollingPaperPage from './pages/RollingPaper'; import WritePage from './pages/Write'; import ShareApprovalPage from './pages/Share'; +import useThemeStore from './stores/themeStore'; const App = () => { + const theme = useThemeStore((state) => state.theme); useViewport(); + const initializeTheme = () => { + if (theme === 'dark') { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + }; + + initializeTheme(); + return ( }> diff --git a/src/apis/admin.ts b/src/apis/admin.ts index f768098..8508d49 100644 --- a/src/apis/admin.ts +++ b/src/apis/admin.ts @@ -41,11 +41,12 @@ const patchReport = async (reportId: number, patchReportRequest: PatchReportRequ }; // badwords -const getBadWords = async (setBadWords: React.Dispatch>) => { +const getBadWords = async () => { try { const res = await client.get('/api/bad-words'); - setBadWords(res.data.data); + if (!res) throw new Error('금칙어 조회 도중 에러가 발생했습니다.'); console.log(res); + return res; } catch (error) { console.error(error); } @@ -63,14 +64,34 @@ const postBadWords = async (badWordsRequest: BadWords) => { }; // 내 상상대로 만든 필터링 단어 취소 버튼 -const patchBadWords = async (badWordId: number) => { +const patchBadWordsUsed = async (badWordId: string) => { try { const res = await client.patch(`/api/bad-words/${badWordId}/status`, { isUsed: false }); if (!res) throw new Error('검열 단어 삭제 도중 에러가 발생했습니다.'); console.log(res); + return res; + } catch (error) { + console.error(error); + } +}; + +const patchBadWords = async (badWordId: string, word: string) => { + try { + const res = await client.patch(`/api/bad-words/${badWordId}`, { word: word }); + if (!res) throw new Error('검열 단어 삭제 도중 에러가 발생했습니다.'); + console.log(res); + return res; } catch (error) { console.error(error); } }; -export { postReports, getReports, patchReport, getBadWords, postBadWords, patchBadWords }; +export { + postReports, + getReports, + patchReport, + getBadWords, + postBadWords, + patchBadWordsUsed, + patchBadWords, +}; diff --git a/src/apis/share.ts b/src/apis/share.ts index 37a3087..6018376 100644 --- a/src/apis/share.ts +++ b/src/apis/share.ts @@ -163,7 +163,6 @@ export const postSharePostLike = async (sharePostId: string) => { return response.data; } catch (error) { console.error('❌ 편지 좋아요 중 에러가 발생했습니다', error); - throw new Error('편지 좋아요 실패'); } }; @@ -175,6 +174,15 @@ export const getSharePostLikeCount = async (sharePostId: string) => { return response.data; } catch (error) { console.error('❌ 편지 좋아요 갯수 조회 중 에러가 발생했습니다', error); - throw new Error('편지 좋아요 갯수 조회 실패'); + } +}; + +export const deleteSharePost = async (sharePostId: string) => { + try { + const response = await client.delete(`/api/share-posts/${sharePostId}`); + if (!response) throw new Error('error while deleting post'); + return response; + } catch (error) { + console.error('❌ 편지 삭제 중 에러가 발생했습니다', error); } }; diff --git a/src/apis/write.ts b/src/apis/write.ts index 2a93094..012478f 100644 --- a/src/apis/write.ts +++ b/src/apis/write.ts @@ -7,7 +7,9 @@ const postLetter = async (data: LetterRequest) => { if (!res) throw new Error('편지 전송과정에서 오류가 발생했습니다.'); return res; } catch (error) { + const errorWithStatus = error as unknown as { status: number }; console.error(error); + return errorWithStatus; } }; @@ -42,4 +44,17 @@ const postTemporarySave = async (data: TemporaryRequest) => { } }; -export { postLetter, postFirstReply, getPrevLetter, postTemporarySave }; +const postTemporaryLetter = async (data: TemporaryRequest) => { + console.log('Temporary request', data); + try { + const res = await client.post('/api/letters', data); + if (!res) throw new Error('편지 전송과정에서 오류가 발생했습니다.'); + return res; + } catch (error) { + const errorWithStatus = error as unknown as { status: number }; + console.error(error); + return errorWithStatus; + } +}; + +export { postLetter, postFirstReply, getPrevLetter, postTemporarySave, postTemporaryLetter }; diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index 42da171..1f952b5 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -25,6 +25,8 @@ import SnowIcon from './snow.svg?react'; import StampIcon from './stamp.svg?react'; import ThermostatIcon from './thermostat.svg?react'; import WarmIcon from './warm.svg?react'; +import ToggleOff from './toggle-off.svg?react'; +import ToggleOn from './toggle-on.svg?react'; export { AddIcon, @@ -54,4 +56,6 @@ export { DeleteIcon, CancelIcon, PencilIcon, + ToggleOff, + ToggleOn, }; diff --git a/src/assets/icons/toggle-off.svg b/src/assets/icons/toggle-off.svg new file mode 100644 index 0000000..e75f80e --- /dev/null +++ b/src/assets/icons/toggle-off.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/toggle-on.svg b/src/assets/icons/toggle-on.svg new file mode 100644 index 0000000..e896baa --- /dev/null +++ b/src/assets/icons/toggle-on.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/images/background-dark.png b/src/assets/images/background-dark.png new file mode 100644 index 0000000..ed737c5 Binary files /dev/null and b/src/assets/images/background-dark.png differ diff --git a/src/assets/images/field-4-dark.png b/src/assets/images/field-4-dark.png new file mode 100644 index 0000000..0f6ab5b Binary files /dev/null and b/src/assets/images/field-4-dark.png differ diff --git a/src/assets/images/field-theme-asset-bird-dark.png b/src/assets/images/field-theme-asset-bird-dark.png new file mode 100644 index 0000000..ffaac1c Binary files /dev/null and b/src/assets/images/field-theme-asset-bird-dark.png differ diff --git a/src/assets/images/home-left-mountain-dark.png b/src/assets/images/home-left-mountain-dark.png new file mode 100644 index 0000000..f49e739 Binary files /dev/null and b/src/assets/images/home-left-mountain-dark.png differ diff --git a/src/assets/images/home-right-mountain-bottom-dark.png b/src/assets/images/home-right-mountain-bottom-dark.png new file mode 100644 index 0000000..efde328 Binary files /dev/null and b/src/assets/images/home-right-mountain-bottom-dark.png differ diff --git a/src/assets/images/home-right-mountain-top-dark.png b/src/assets/images/home-right-mountain-top-dark.png new file mode 100644 index 0000000..ca10f5f Binary files /dev/null and b/src/assets/images/home-right-mountain-top-dark.png differ diff --git a/src/assets/images/landing-dark.png b/src/assets/images/landing-dark.png new file mode 100644 index 0000000..14ebe02 Binary files /dev/null and b/src/assets/images/landing-dark.png differ diff --git a/src/components/BackgroundBottom.tsx b/src/components/BackgroundBottom.tsx index 6f8e830..6310b4b 100644 --- a/src/components/BackgroundBottom.tsx +++ b/src/components/BackgroundBottom.tsx @@ -1,13 +1,17 @@ import BgItem from '@/assets/images/field-4.png'; +import BgItemDark from '@/assets/images/field-4-dark.png'; import BackgroundImageWrapper from './BackgroundImageWrapper'; +import useThemeStore from '@/stores/themeStore'; const BackgroundBottom = () => { + const theme = useThemeStore((state) => state.theme); + return ( ); }; diff --git a/src/components/MenuButton.tsx b/src/components/MenuButton.tsx index 681e59a..d911338 100644 --- a/src/components/MenuButton.tsx +++ b/src/components/MenuButton.tsx @@ -16,44 +16,48 @@ export default function MenuButton() { setIsOpen(false)} > - setIsOpen(false)} /> + setIsOpen(false)} > - setIsOpen(false)} /> + setIsOpen(false)} > - setIsOpen(false)} /> +
setIsOpen((state) => !state)} > - setIsOpen((state) => !state)} /> +
diff --git a/src/components/NoticeRollingPaper.tsx b/src/components/NoticeRollingPaper.tsx index 7c80fe3..fb1252b 100644 --- a/src/components/NoticeRollingPaper.tsx +++ b/src/components/NoticeRollingPaper.tsx @@ -46,12 +46,12 @@ const NoticeRollingPaper = () => {
- +

{ return ( -

+

{children}

); diff --git a/src/components/ToastItem.tsx b/src/components/ToastItem.tsx index e9499c2..4d4e506 100644 --- a/src/components/ToastItem.tsx +++ b/src/components/ToastItem.tsx @@ -21,7 +21,7 @@ export default function ToastItem({ toastObj, index }: { toastObj: ToastObj; ind const TOAST_POSITION = { Top: 'top-20', - Bottom: 'bottom-5', + Bottom: 'bottom-20', }; const animation = `toast-blink ${toastObj.time}s ease-in-out forwards`; diff --git a/src/hooks/useServerSentEvents.tsx b/src/hooks/useServerSentEvents.tsx index c24a84b..ac9c5ab 100644 --- a/src/hooks/useServerSentEvents.tsx +++ b/src/hooks/useServerSentEvents.tsx @@ -18,7 +18,6 @@ export const useServerSentEvents = () => { const navigate = useNavigate(); // const recallCountRef = useRef(1); - const accessToken = useAuthStore((state) => state.accessToken); const setAccessToken = useAuthStore((state) => state.setAccessToken); const sourceRef = useRef(null); @@ -93,7 +92,6 @@ export const useServerSentEvents = () => { }; } catch (error) { console.log('catch문에서 에러 발생', error); - } }; diff --git a/src/layouts/Header.tsx b/src/layouts/Header.tsx index 5d91ef6..c199a1f 100644 --- a/src/layouts/Header.tsx +++ b/src/layouts/Header.tsx @@ -2,8 +2,14 @@ import { Link, useNavigate } from 'react-router'; import { ArrowLeftIcon, PersonIcon } from '@/assets/icons'; import NotificationButton from '@/components/NotificationButton'; +import FlareRoundedIcon from '@mui/icons-material/FlareRounded'; +import DarkModeOutlinedIcon from '@mui/icons-material/DarkModeOutlined'; +import useThemeStore from '@/stores/themeStore'; const Header = () => { + const theme = useThemeStore((state) => state.theme); + const toggleTheme = useThemeStore((state) => state.toggleTheme); + const navigate = useNavigate(); return ( @@ -12,6 +18,11 @@ const Header = () => {
+ {theme === 'light' ? ( + + ) : ( + + )} diff --git a/src/layouts/MobileLayout.tsx b/src/layouts/MobileLayout.tsx index 3229467..d4d7dd6 100644 --- a/src/layouts/MobileLayout.tsx +++ b/src/layouts/MobileLayout.tsx @@ -3,7 +3,7 @@ import { Outlet } from 'react-router'; const MobileLayout = () => { return (
-
+
diff --git a/src/main.tsx b/src/main.tsx index 4df6fa6..e5b67d3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { StrictMode } from 'react'; +// import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { BrowserRouter } from 'react-router'; @@ -15,11 +15,11 @@ queryClient.setDefaultOptions({ }); createRoot(document.getElementById('root')!).render( - - - - - - - , + // + + + + + , + // , ); diff --git a/src/pages/Admin/Filtering.tsx b/src/pages/Admin/Filtering.tsx index 38ec96e..aad0f86 100644 --- a/src/pages/Admin/Filtering.tsx +++ b/src/pages/Admin/Filtering.tsx @@ -1,19 +1,29 @@ import { useEffect, useState } from 'react'; import { getBadWords } from '@/apis/admin'; -import { AddIcon, AlarmIcon, CancelIcon } from '@/assets/icons'; +import { AddIcon, AlarmIcon } from '@/assets/icons'; import AddInputButton from './components/AddInputButton'; import AdminPageTitle from './components/AdminPageTitle'; import WrapperFrame from './components/WrapperFrame'; import WrapperTitle from './components/WrapperTitle'; +import FilterTextItem from './components/FilterTextItem'; export default function FilteringManage() { - const [badWords, setBadWords] = useState([]); + const [badWords, setBadWords] = useState([]); const [addInputShow, setAddInputShow] = useState(false); + const handleGetBadWords = async () => { + const res = await getBadWords(); + if (res?.status === 200) { + setBadWords(res.data.data); + } else { + console.log('검열 조회 오류 발생'); + } + }; + useEffect(() => { - getBadWords(setBadWords); + handleGetBadWords(); }, []); return ( <> @@ -21,23 +31,13 @@ export default function FilteringManage() {
- {badWords.map((badWord, idx) => { - return ( - - {badWord.word} - - - ); + {badWords.map((badWord) => { + return ; })} {addInputShow ? ( ) : ( - + 추가하기 + + + + + + ); +} diff --git a/src/pages/Admin/components/PatchInput.tsx b/src/pages/Admin/components/PatchInput.tsx new file mode 100644 index 0000000..f48cd9c --- /dev/null +++ b/src/pages/Admin/components/PatchInput.tsx @@ -0,0 +1,75 @@ +import { useEffect, useRef, useState } from 'react'; + +import { patchBadWords } from '@/apis/admin'; +import { AddIcon } from '@/assets/icons'; + +export default function PatchInput({ + badWordId, + setPatchInputShow, + setBadWords, +}: { + badWordId: string; + setPatchInputShow: React.Dispatch>; + setBadWords: React.Dispatch>; +}) { + const [inputText, setInputText] = useState({ word: '' }); + const inputRef = useRef(null); + + const handleInputWidth = (event: React.FormEvent) => { + const target = event.target as HTMLInputElement; + target.style.width = '50px'; + target.style.width = `${target.scrollWidth}px`; + }; + + const handlePatchBadWords = async () => { + if (inputText.word === '') return setPatchInputShow(false); + const res = await patchBadWords(badWordId, inputText.word); + if (res?.status === 200) { + setBadWords((cur) => + cur.map((e) => { + if (e.id === badWordId) { + return { ...e, word: inputText.word }; + } + return e; + }), + ); + console.log('일단 수정했음 api 새로 업데이트되면 바인딩'); + setPatchInputShow(false); + } + }; + + useEffect(() => { + const inputElement = inputRef.current; + if (inputElement) { + inputElement.focus(); + } + }, []); + + return ( + <> + { + handleInputWidth(e); + }} + onChange={(e) => { + setInputText(() => ({ word: e.target.value })); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handlePatchBadWords(); + } + }} + /> + + + ); +} diff --git a/src/pages/Admin/components/ReportListItem.tsx b/src/pages/Admin/components/ReportListItem.tsx index fec8aab..51877d2 100644 --- a/src/pages/Admin/components/ReportListItem.tsx +++ b/src/pages/Admin/components/ReportListItem.tsx @@ -64,14 +64,17 @@ export default function ReportListItem({ {`${formattedDate} ${formattedTime}`} {reasonList[report.reasonType]}
- + {report.status === 'PENDING' && ( + + )} + {modalOpen && }
); diff --git a/src/pages/Home/components/FloatingLetters.tsx b/src/pages/Home/components/FloatingLetters.tsx index 3403883..2941464 100644 --- a/src/pages/Home/components/FloatingLetters.tsx +++ b/src/pages/Home/components/FloatingLetters.tsx @@ -16,7 +16,7 @@ const FloatingLetters = () => { lettersRef.current.forEach((letter, index) => { gsap.to(letter, { // x: Math.random() * 50 - 40, - y: Math.random() * 20 - 40 + 'vh', // 위아래 이동 + y: Math.random() * 20 - 30 + 'vh', // 위아래 이동 rotation: Math.random() * 50 - 25, // 회전 duration: Math.random() * 3 + 2, // 지속 시간 repeat: -1, // 무한 반복 @@ -35,7 +35,7 @@ const FloatingLetters = () => { ref={(el) => { if (el) lettersRef.current[index] = el; }} - className="absolute w-20 opacity-90" + className="absolute w-20 opacity-90 md:w-24" style={{ left: `${index * 30 + 30}px`, // 편지지 간격 top: '60vh', diff --git a/src/pages/Home/components/GoToLetterBoard.tsx b/src/pages/Home/components/GoToLetterBoard.tsx index 2f86778..fbdc6d9 100644 --- a/src/pages/Home/components/GoToLetterBoard.tsx +++ b/src/pages/Home/components/GoToLetterBoard.tsx @@ -4,11 +4,15 @@ import goToLetterBoard from '@/assets/images/go-to-letter-board.png'; const GoToLetterBoard = () => { return ( -
+
-

게시판

+

게시판

- go to letter board + go to letter board
diff --git a/src/pages/Home/components/GoToLetterBox.tsx b/src/pages/Home/components/GoToLetterBox.tsx index ca8b3fe..f14ab4d 100644 --- a/src/pages/Home/components/GoToLetterBox.tsx +++ b/src/pages/Home/components/GoToLetterBox.tsx @@ -4,9 +4,11 @@ import { Link } from 'react-router'; import { getUnreadLettersCount } from '@/apis/unreadLetters'; import goToLetterBoxNewLetters from '@/assets/images/go-to-letter-box-new-letters.png'; import goToLetterBox from '@/assets/images/go-to-letter-box.png'; +import useToastStore from '@/stores/toastStore'; const GoToLetterBox = () => { const [arrivedCount, setArrivedCount] = useState(0); + const setToastActive = useToastStore((state) => state.setToastActive); useEffect(() => { const fetchUnreadCount = async () => { @@ -15,20 +17,25 @@ const GoToLetterBox = () => { setArrivedCount(result.data); } catch (error) { console.error('❌ 안 읽은 편지 개수를 불러오는 데 실패했습니다:', error); + setToastActive({ + toastType: 'Error', + title: '서버 오류로 안 읽은 편지 개수를 불러오는 데에 실패했습니다.', + time: 5, + }); } }; fetchUnreadCount(); }, []); return ( -
+
-

내 편지함

+

내 편지함

go to letter box
diff --git a/src/pages/Home/components/GoToRandomLetter.tsx b/src/pages/Home/components/GoToRandomLetter.tsx index 2154e7e..0ca8e06 100644 --- a/src/pages/Home/components/GoToRandomLetter.tsx +++ b/src/pages/Home/components/GoToRandomLetter.tsx @@ -6,7 +6,9 @@ const GoToRandomLetter = () => { return ( <>
-

고민편지 보러가기

+

+ 고민편지 보러가기 +

go to random letter diff --git a/src/pages/Home/components/GoToWrite.tsx b/src/pages/Home/components/GoToWrite.tsx index 9c4d589..cdfc9c6 100644 --- a/src/pages/Home/components/GoToWrite.tsx +++ b/src/pages/Home/components/GoToWrite.tsx @@ -5,7 +5,9 @@ import goToWrite from '@/assets/images/postoffice-letter.png'; const GoToWrite = () => { return (
-

속마음 나누기

+

+ 속마음 나누기 +

go to write diff --git a/src/pages/Home/components/HomeBackgroundLeft.tsx b/src/pages/Home/components/HomeBackgroundLeft.tsx index 43b9f91..eb53aa6 100644 --- a/src/pages/Home/components/HomeBackgroundLeft.tsx +++ b/src/pages/Home/components/HomeBackgroundLeft.tsx @@ -1,12 +1,17 @@ import homeLeftMountain from '@/assets/images/home-left-mountain.png'; +import homeLeftMountainDark from '@/assets/images/home-left-mountain-dark.png'; + import BackgroundImageWrapper from '@/components/BackgroundImageWrapper'; +import useThemeStore from '@/stores/themeStore'; const HomeBackgroundLeft = () => { + const theme = useThemeStore((state) => state.theme); + return ( ); }; diff --git a/src/pages/Home/components/HomeBackgroundRightBottom.tsx b/src/pages/Home/components/HomeBackgroundRightBottom.tsx index 6b55d0c..4a15d96 100644 --- a/src/pages/Home/components/HomeBackgroundRightBottom.tsx +++ b/src/pages/Home/components/HomeBackgroundRightBottom.tsx @@ -1,12 +1,16 @@ import homeRightMountainBottom from '@/assets/images/home-right-mountain-bottom.png'; +import homeRightMountainBottomDark from '@/assets/images/home-right-mountain-bottom-dark.png'; import BackgroundImageWrapper from '@/components/BackgroundImageWrapper'; +import useThemeStore from '@/stores/themeStore'; const HomeBackgroundRightBottom = () => { + const theme = useThemeStore((state) => state.theme); + return ( ); }; diff --git a/src/pages/Home/components/HomeBackgroundRightTop.tsx b/src/pages/Home/components/HomeBackgroundRightTop.tsx index 966424e..e37560b 100644 --- a/src/pages/Home/components/HomeBackgroundRightTop.tsx +++ b/src/pages/Home/components/HomeBackgroundRightTop.tsx @@ -1,11 +1,17 @@ import homeRightMountainTop from '@/assets/images/home-right-mountain-top.png'; +import homeRightMountainTopDark from '@/assets/images/home-right-mountain-top-dark.png'; + import BackgroundImageWrapper from '@/components/BackgroundImageWrapper'; +import useThemeStore from '@/stores/themeStore'; + const HomeBackgroundRightTop = () => { + const theme = useThemeStore((state) => state.theme); + return ( ); }; diff --git a/src/pages/Home/components/HomeHeader.tsx b/src/pages/Home/components/HomeHeader.tsx index a1ea28f..567dfa3 100644 --- a/src/pages/Home/components/HomeHeader.tsx +++ b/src/pages/Home/components/HomeHeader.tsx @@ -1,12 +1,23 @@ import { Link } from 'react-router'; import { PersonIcon } from '@/assets/icons'; +import FlareRoundedIcon from '@mui/icons-material/FlareRounded'; +import DarkModeOutlinedIcon from '@mui/icons-material/DarkModeOutlined'; +import useThemeStore from '@/stores/themeStore'; import NotificationButton from '@/components/NotificationButton'; const HomeHeader = () => { + const theme = useThemeStore((state) => state.theme); + const toggleTheme = useThemeStore((state) => state.toggleTheme); + return (
+ {theme === 'light' ? ( + + ) : ( + + )} diff --git a/src/pages/Home/components/HomeRight.tsx b/src/pages/Home/components/HomeRight.tsx index 8182517..fb75c01 100644 --- a/src/pages/Home/components/HomeRight.tsx +++ b/src/pages/Home/components/HomeRight.tsx @@ -7,9 +7,13 @@ import GoToLetterBoard from './GoToLetterBoard'; import GoToLetterBox from './GoToLetterBox'; import UnreadLetterModal from './UnreadLetterModal'; +import useToastStore from '@/stores/toastStore'; + const HomeRight = () => { const [arrivedCount, setArrivedCount] = useState(0); + const setToastActive = useToastStore((state) => state.setToastActive); + useEffect(() => { const fetchUnreadCount = async () => { try { @@ -17,6 +21,11 @@ const HomeRight = () => { setArrivedCount(result.data); } catch (error) { console.error('❌ 안 읽은 편지 개수를 불러오는 데 실패했습니다:', error); + setToastActive({ + toastType: 'Error', + title: '서버 오류로 안 읽은 편지 개수를 불러오는 데에 실패했습니다.', + time: 5, + }); } }; fetchUnreadCount(); diff --git a/src/pages/Home/components/LetterActions.tsx b/src/pages/Home/components/LetterActions.tsx index a7259cd..45444de 100644 --- a/src/pages/Home/components/LetterActions.tsx +++ b/src/pages/Home/components/LetterActions.tsx @@ -34,7 +34,7 @@ const LetterActions = () => { diff --git a/src/pages/Home/components/RandomCheer.tsx b/src/pages/Home/components/RandomCheer.tsx index 5d3ac5a..f257844 100644 --- a/src/pages/Home/components/RandomCheer.tsx +++ b/src/pages/Home/components/RandomCheer.tsx @@ -1,10 +1,13 @@ import { useState } from 'react'; import randomCheerBird from '@/assets/images/field-theme-asset-bird.png'; +import randomCheerBirdDark from '@/assets/images/field-theme-asset-bird-dark.png'; import { RANDOM_CHEER_LIST } from '../constants'; +import useThemeStore from '@/stores/themeStore'; const RandomCheer = () => { + const theme = useThemeStore((state) => state.theme); const getRandomCheer = (): string => { const randomIndex = Math.floor(Math.random() * RANDOM_CHEER_LIST.length); return RANDOM_CHEER_LIST[randomIndex]; @@ -22,7 +25,7 @@ const RandomCheer = () => {
random cheer bird setRandomCheer(getRandomCheer())} diff --git a/src/pages/Home/components/ShowDraftModal.tsx b/src/pages/Home/components/ShowDraftModal.tsx index 4764837..b2f1d59 100644 --- a/src/pages/Home/components/ShowDraftModal.tsx +++ b/src/pages/Home/components/ShowDraftModal.tsx @@ -6,6 +6,8 @@ import { DraftLetter, getDraftLetters, deleteDraftLetters } from '@/apis/draftLe import ModalBackgroundWrapper from '@/components/ModalBackgroundWrapper'; import ModalOverlay from '@/components/ModalOverlay'; +import useToastStore from '@/stores/toastStore'; + interface ShowDraftModalProps { children?: React.ReactNode; onClose: () => void; @@ -16,6 +18,8 @@ const ShowDraftModal = ({ onClose }: ShowDraftModalProps) => { const navigate = useNavigate(); + const setToastActive = useToastStore((state) => state.setToastActive); + const handleNavigation = (draft: DraftLetter) => { navigate(`/letter/write/?letterId=${draft.parentLetterId}`, { state: { draft, isDraft: true }, @@ -29,17 +33,26 @@ const ShowDraftModal = ({ onClose }: ShowDraftModalProps) => { }) .catch((error) => { console.error('❌ 임시저장된 편지를 불러오는데 실패했습니다', error); + setToastActive({ + toastType: 'Error', + title: '서버 오류로 임시저장된 편지를 불러오는 데에 실패했습니다.', + time: 5, + }); }); }; const handleDeleteDraftLetters = async (letterId: number) => { - //TODO: 정말로 삭제하시겠습니까? 모달창 try { await deleteDraftLetters(letterId); setDraftLetters((prev) => prev.filter((letter) => letter.letterId !== letterId)); console.log(`letterId는 `, letterId); } catch (error) { console.error(`❌임시저장된 편지를 삭제하던 중 에러가 발생했습니다.`, error); + setToastActive({ + toastType: 'Error', + title: '서버 오류로 임시저장 된 편지를 삭제하던 중 에러가 발생했습니다.', + time: 5, + }); } }; diff --git a/src/pages/Home/components/ShowShareAccessModal.tsx b/src/pages/Home/components/ShowShareAccessModal.tsx index e9c1dfa..15a952c 100644 --- a/src/pages/Home/components/ShowShareAccessModal.tsx +++ b/src/pages/Home/components/ShowShareAccessModal.tsx @@ -8,6 +8,8 @@ import { ShareProposal } from '@/apis/share'; import ModalBackgroundWrapper from '@/components/ModalBackgroundWrapper'; import ModalOverlay from '@/components/ModalOverlay'; +import useToastStore from '@/stores/toastStore'; + interface ShowShareAccessModalProps { children?: React.ReactNode; onClose: () => void; @@ -18,6 +20,8 @@ const ShowShareAccessModal = ({ onClose }: ShowShareAccessModalProps) => { const [shareProposals, setShareProposals] = useState([]); + const setToastActive = useToastStore((state) => state.setToastActive); + useEffect(() => { getShareProposalList() .then((data) => { @@ -25,6 +29,11 @@ const ShowShareAccessModal = ({ onClose }: ShowShareAccessModalProps) => { }) .catch((error) => { console.error('❌ 공유 요청 목록을 불러오는 데 실패했습니다.', error); + setToastActive({ + toastType: 'Error', + title: '서버 오류로 공유 요청 목록을 불러오는 데에 실패했습니다.', + time: 5, + }); }); }, []); @@ -36,6 +45,11 @@ const ShowShareAccessModal = ({ onClose }: ShowShareAccessModalProps) => { }); } catch (error) { console.error('❌ 게시글 상세 페이지로 이동하는 데에 실패했습니다.', error); + setToastActive({ + toastType: 'Error', + title: '서버 오류로 게시글 상세 페이지로 이동하는 데에 실패했습니다.', + time: 5, + }); } }; diff --git a/src/pages/Home/components/UnreadLetterModal.tsx b/src/pages/Home/components/UnreadLetterModal.tsx index c3491a3..5ce5522 100644 --- a/src/pages/Home/components/UnreadLetterModal.tsx +++ b/src/pages/Home/components/UnreadLetterModal.tsx @@ -1,9 +1,11 @@ import { useEffect, useState } from 'react'; import { getUnreadLettersCount } from '@/apis/unreadLetters'; +import useToastStore from '@/stores/toastStore'; const UnreadLetterModal = () => { const [arrivedCount, setArrivedCount] = useState(0); + const setToastActive = useToastStore((state) => state.setToastActive); useEffect(() => { const fetchUnreadCount = async () => { @@ -12,6 +14,11 @@ const UnreadLetterModal = () => { setArrivedCount(result.data); } catch (error) { console.error('❌ 안 읽은 편지 개수를 불러오는 데 실패했습니다:', error); + setToastActive({ + toastType: 'Error', + title: '서버 오류로 안 읽은 편지 개수를 불러오는 데에 실패했습니다.', + time: 5, + }); } }; fetchUnreadCount(); diff --git a/src/pages/Landing/index.tsx b/src/pages/Landing/index.tsx index b89bfda..2b2fb90 100644 --- a/src/pages/Landing/index.tsx +++ b/src/pages/Landing/index.tsx @@ -3,7 +3,9 @@ import { Navigate, useNavigate } from 'react-router'; import { twMerge } from 'tailwind-merge'; import LandingImg from '@/assets/images/landing.png'; +import LandingImgDark from '@/assets/images/landing-dark.png'; import useAuthStore from '@/stores/authStore'; +import useThemeStore from '@/stores/themeStore'; import { STYLE_CLASS } from './constants'; @@ -11,6 +13,7 @@ const Landing = () => { const [step, setStep] = useState(0); const isLoggedIn = useAuthStore((state) => state.isLoggedIn); const navigate = useNavigate(); + const theme = useThemeStore((state) => state.theme); useEffect(() => { if (isLoggedIn) navigate('/'); @@ -21,7 +24,7 @@ const Landing = () => { return (
setStep((prev) => prev + 1)}> 서비스 소개 이미지 { <> 게시판 -

+

따숨이에게 힘이 되었던 다양한 편지들을 모아두었어요

diff --git a/src/pages/LetterBoardDetail/components/Header.tsx b/src/pages/LetterBoardDetail/components/Header.tsx index 26c306e..437ef61 100644 --- a/src/pages/LetterBoardDetail/components/Header.tsx +++ b/src/pages/LetterBoardDetail/components/Header.tsx @@ -14,6 +14,7 @@ interface HeaderProps { isWriter: boolean; onToggleLike: () => void; onOpenReportModal: () => void; + onDeleteLetter: () => void; isShareLetterPreview?: boolean; } @@ -23,10 +24,10 @@ const Header = ({ isWriter, onToggleLike, onOpenReportModal, + onDeleteLetter, isShareLetterPreview = false, }: HeaderProps) => { const navigate = useNavigate(); - return (
@@ -46,8 +47,7 @@ const Header = ({

{likeCount}

{isWriter ? ( - // TODO: 게시물 삭제 - + ) : (
{ setShareComment(''); setToastActive({ toastType: 'Success', - title: '공유 완료 되었습니다.', + title: '공유 요청이 완료 되었습니다.', }); }, onError: (error) => { setToastActive({ toastType: 'Error', - title: '공유가 실패했습니다. 잠시 후에 다시 시도해주세요.', + title: '공유 요청이 실패했습니다. 잠시 후에 다시 시도해주세요.', }); console.error(error); }, @@ -171,7 +171,7 @@ const LetterBoxDetailPage = () => { ? '게시판에 올릴 편지를 선택해주세요' : `${userInfo.zipCode}님과 주고 받은 편지`} -
+

주고 받은 편지 {mailLists.length}

{!userInfo.isClosed && ( @@ -207,7 +207,7 @@ const LetterBoxDetailPage = () => { {!isShareMode && !userInfo.isClosed && !isLoading && (
)} - + {!isShareMode && } ); }; diff --git a/src/pages/Login/components/Background.tsx b/src/pages/Login/components/Background.tsx index 0c6caf0..5ec92ca 100644 --- a/src/pages/Login/components/Background.tsx +++ b/src/pages/Login/components/Background.tsx @@ -1,19 +1,24 @@ import { Link } from 'react-router'; import FieldImg from '@/assets/images/home-left-mountain.png'; +import FieldImgDark from '@/assets/images/home-left-mountain-dark.png'; + import BlurImg from '@/assets/images/landing-blur.png'; import EnvelopeImg from '@/assets/images/postoffice-letter.png'; import PostofficeImg from '@/assets/images/postoffice.png'; import BackgroundImageWrapper from '@/components/BackgroundImageWrapper'; +import useThemeStore from '@/stores/themeStore'; const Background = () => { + const theme = useThemeStore((state) => state.theme); + return ( <>
-

+

36.5 설명 보기

{ className="absolute bottom-[93px] left-1/2 z-1 h-[184.5px] w-full -translate-x-1/2 object-contain object-[calc(50%-78px)]" /> 언덕 이미지 diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index 2922331..506d823 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -24,9 +24,13 @@ const LoginPage = () => {
-

마음이 맞닿는 온도

-

36.5

-

+

+ 마음이 맞닿는 온도 +

+

+ 36.5 +

+

모르는 사람과 편지를 주고 받으며
마음의 위안을 얻어보세요. diff --git a/src/pages/MyPage/components/MyBoardPage.tsx b/src/pages/MyPage/components/MyBoardPage.tsx index 03e5588..70e9c20 100644 --- a/src/pages/MyPage/components/MyBoardPage.tsx +++ b/src/pages/MyPage/components/MyBoardPage.tsx @@ -48,7 +48,7 @@ const MyBoardPage = () => {

내가 올린 게시물 {isLoading ? ( -

로딩 중 입니다.

+

loading

) : postLists && postLists?.length > 0 ? (
{postLists?.map((item, index) => ( @@ -62,7 +62,7 @@ const MyBoardPage = () => { ))}
) : ( -

게시글이 없습니다.

+

게시글이 없습니다.

)}
diff --git a/src/pages/MyPage/index.tsx b/src/pages/MyPage/index.tsx index de1bc97..e091935 100644 --- a/src/pages/MyPage/index.tsx +++ b/src/pages/MyPage/index.tsx @@ -59,7 +59,11 @@ const MyPage = () => { setIsOpenModal(false); if (response?.status === 200) { logout(); - alert('탈퇴가 완료 되었습니다.'); + setToastActive({ + toastType: 'Success', + title: '탈퇴가 완료되었습니다.', + time: 5, + }); } }} /> @@ -70,6 +74,11 @@ const MyPage = () => {
+

경고 안내

+

+ 따사로운 서비스 이용을 위해, 부적절하다고 판단되는 편지는 반려하고 있어요. 서로를 + 존중하는 따뜻한 공간을 만들기 위해 협조 부탁드립니다. +

경고 규칙

3회 경고: 서비스 이용 불가능

@@ -90,8 +99,8 @@ const MyPage = () => {

-

{description}

-

{data.temperature}도

+

{description}

+

{data.temperature}도

{
-

활동

+

활동

-

내가 올린 게시물

+

내가 올린 게시물

-

고객 센터

+

고객 센터

운영자에게 문의하기
-

계정

+

계정

-

로그인 정보

-

+

로그인 정보

+

{data.social} {data.email}

@@ -132,14 +141,14 @@ const MyPage = () => { setIsOpenWarningModal(true); }} > -

경고 횟수

-

+

경고 횟수

+

{data.warningCount}회

+
+ + + + ); +} diff --git a/src/pages/Notifications/index.tsx b/src/pages/Notifications/index.tsx index 257453e..445e31e 100644 --- a/src/pages/Notifications/index.tsx +++ b/src/pages/Notifications/index.tsx @@ -8,6 +8,7 @@ import NotificationItem from './components/NotificationItem'; import WarningModal from './components/WarningModal'; import SendingModal from './components/SendingModal'; import useNotificationStore from '@/stores/notificationStore'; +import ShareModal from './components/ShareModal'; const NotificationsPage = () => { const navigate = useNavigate(); @@ -19,6 +20,7 @@ const NotificationsPage = () => { const [isOpenWarningModal, setIsOpenWarningModal] = useState(false); const [isOpenSendingModal, setIsOpenSendingModal] = useState(false); + const [isOpenShareModal, setIsOpenShareModal] = useState(false); const [reportContent, setReportContent] = useState(''); @@ -35,7 +37,7 @@ const NotificationsPage = () => { if (typeof content === 'string') setReportContent(content); } if (alarmType === 'SHARE') { - navigate(`/board/letter/${content}`, { state: { isShareLetterPreview: true } }); + setIsOpenShareModal(true); } if (alarmType === 'POSTED') { navigate(`/board/letter/${content}`); @@ -99,11 +101,12 @@ const NotificationsPage = () => { isOpenSendingModal={isOpenSendingModal} setIsOpenSendingModal={setIsOpenSendingModal} /> +
알림 diff --git a/src/pages/RandomLetters/index.tsx b/src/pages/RandomLetters/index.tsx index c1b12d9..d71c3d1 100644 --- a/src/pages/RandomLetters/index.tsx +++ b/src/pages/RandomLetters/index.tsx @@ -75,6 +75,14 @@ const RandomLettersPage = () => { }, []); return ( <> + {openSelectModal && ( + + )} {openSelectedDetailModal ? ( ) : ( @@ -103,15 +111,6 @@ const RandomLettersPage = () => { setOpenSelectedDetailModal={setOpenSelectedDetailModal} /> )} - - {openSelectModal && ( - - )}
diff --git a/src/pages/RollingPaper/index.tsx b/src/pages/RollingPaper/index.tsx index 28fc0f6..7fc638f 100644 --- a/src/pages/RollingPaper/index.tsx +++ b/src/pages/RollingPaper/index.tsx @@ -102,7 +102,9 @@ const RollingPaperPage = () => {
{title} -

등록된 편지 {totalComments}

+

+ 등록된 편지 {totalComments} +

{isSuccess && @@ -129,6 +131,7 @@ const RollingPaperPage = () => { )}
+
diff --git a/src/pages/Share/index.tsx b/src/pages/Share/index.tsx index 8a2fddf..2ecb81f 100644 --- a/src/pages/Share/index.tsx +++ b/src/pages/Share/index.tsx @@ -4,6 +4,8 @@ import { useNavigate, useParams } from 'react-router'; import { getShareProposalDetail } from '@/apis/share'; import { postShareProposalApproval, ShareProposalDetail } from '@/apis/share'; +import useToastStore from '@/stores/toastStore'; + import { twMerge } from 'tailwind-merge'; import Letter from '../LetterBoardDetail/components/Letter'; @@ -12,17 +14,36 @@ import BlurImg from '@/assets/images/landing-blur.png'; const ShareApprovalPage = () => { const navigate = useNavigate(); const { shareProposalId } = useParams(); - console.log(shareProposalId); const [proposalDetail, setProposalDetail] = useState(); + const setToastActive = useToastStore((state) => state.setToastActive); + const handleProposalApproval = async (action: 'approve' | 'reject') => { try { const result = await postShareProposalApproval(Number(shareProposalId), action); console.log(`✅ 편지 공유 ${action === 'approve' ? '수락' : '거절'}됨:`, result); + if (action === 'approve') { + setToastActive({ + toastType: 'Success', + title: '편지를 성공적으로 공유하였습니다. 게시판에서 확인해보세요!', + time: 5, + }); + } else { + setToastActive({ + toastType: 'Info', + title: '공유 요청을 성공적으로 거부하였습니다.', + time: 5, + }); + } navigate('/'); } catch (error) { console.error('❌공유 요청 처리 중 에러 발생', error); + setToastActive({ + toastType: 'Error', + title: '서버 오류로 편지를 공유하는 데에 실패했습니다.', + time: 5, + }); } }; useEffect(() => { @@ -32,6 +53,11 @@ const ShareApprovalPage = () => { setProposalDetail(data); } catch (error) { console.error('❌ 공유 요청 상세 조회에 실패했습니다.', error); + setToastActive({ + toastType: 'Error', + title: '서버 오류로 공유 요청을 조회하는 데에 실패했습니다.', + time: 5, + }); throw error; } }; diff --git a/src/pages/Write/CategorySelect.tsx b/src/pages/Write/CategorySelect.tsx index 89b9331..7da2dee 100644 --- a/src/pages/Write/CategorySelect.tsx +++ b/src/pages/Write/CategorySelect.tsx @@ -29,10 +29,13 @@ export default function CategorySelect({ if (res?.status === 200) { console.log(letterRequest); setSend(true); - } else { + setToastActive({ title: '편지 전송을 완료했습니다.', toastType: 'Success' }); + } else if (res?.status === 400) { // 일단 에러 발생하면 무조건 검열단어라고 토스트를 띄웠는데 후에 에러 처리 수정해야함 - setToastActive({ title: '편지에 검열 단어가 포함되어있습니다. ', toastType: 'Error' }); + setToastActive({ title: '편지에 검열 단어가 포함되어있습니다.', toastType: 'Error' }); setStep('edit'); + } else { + setToastActive({ title: '편지 전송과정에 오류가 발생했습니다.', toastType: 'Error' }); } }; @@ -73,7 +76,7 @@ export default function CategorySelect({ {send && !isReply && (
- + 두근두근! 답장이 언제 올까요?
diff --git a/src/pages/Write/LetterEditor.tsx b/src/pages/Write/LetterEditor.tsx index 1668801..23ef8b8 100644 --- a/src/pages/Write/LetterEditor.tsx +++ b/src/pages/Write/LetterEditor.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router'; import { twMerge } from 'tailwind-merge'; -import { postFirstReply, postLetter, postTemporarySave } from '@/apis/write'; +import { postFirstReply, postLetter, postTemporaryLetter, postTemporarySave } from '@/apis/write'; import BackButton from '@/components/BackButton'; import ConfirmModal from '@/components/ConfirmModal'; import WritePageButton from '@/pages/Write/components/WritePageButton'; @@ -42,6 +42,9 @@ export default function LetterEditor({ if (res?.status === 200) { setSend(true); setStep('category'); + setToastActive({ title: '편지 전송을 완료했습니다.', toastType: 'Success' }); + } else if (res?.status === 400) { + setToastActive({ title: '편지에 검열단어가 포함되어있습니다.', toastType: 'Error' }); } else { setToastActive({ title: '전송중 오류가 발생했습니다.', toastType: 'Error' }); } @@ -54,6 +57,37 @@ export default function LetterEditor({ console.log(prevLetter); setSend(true); setStep('category'); + setToastActive({ title: '편지 전송을 완료했습니다.', toastType: 'Success' }); + } else if (res?.status === 400) { + setToastActive({ title: '편지에 검열단어가 포함되어있습니다.', toastType: 'Error' }); + } else { + setToastActive({ title: '전송중 오류가 발생했습니다.', toastType: 'Error' }); + } + }; + + const handlePostTemporarySave = async () => { + if (!letterId) return alert('임시저장중 오류 발생'); + const requestLetterId = location.state?.draft.letterId || null; + // MEMO : 임시저장 전송 방식 : 최초임시저장은 letterId : null, 임시저장 업데이트는 letterId : location state로 받아오는 임시저장편지의 letterId값 + const temporaryRequest: TemporaryRequest = { ...letterRequest, letterId: requestLetterId }; + const res = await postTemporarySave(temporaryRequest); + if (res?.status === 200) { + console.log(res); + setToastActive({ title: '임시저장을 완료했습니다.', toastType: 'Success' }); + navigate('/'); + } else { + setToastActive({ title: '임시저장에 실패했습니다.', toastType: 'Error' }); + } + }; + + const handleTemporaryLetter = async (temporaryRequest: TemporaryRequest) => { + const res = await postTemporaryLetter(temporaryRequest); + if (res?.status === 200) { + setSend(true); + setStep('category'); + setToastActive({ title: '편지 전송을 완료했습니다.', toastType: 'Success' }); + } else if (res?.status === 400) { + setToastActive({ title: '편지에 검열단어가 포함되어있습니다.', toastType: 'Error' }); } else { setToastActive({ title: '전송중 오류가 발생했습니다.', toastType: 'Error' }); } @@ -77,21 +111,6 @@ export default function LetterEditor({ } }, [prevLetter, setLetterRequest, isReply]); - const handlePostTemporarySave = async () => { - if (!letterId) return alert('임시저장중 오류 발생'); - const requestLetterId = location.state?.draft.letterId || null; - // MEMO : 임시저장 전송 방식 : 최초임시저장은 letterId : null, 임시저장 업데이트는 letterId : location state로 받아오는 임시저장편지의 letterId값 - const temporaryRequest: TemporaryRequest = { ...letterRequest, letterId: requestLetterId }; - const res = await postTemporarySave(temporaryRequest); - if (res?.status === 200) { - console.log(res); - setToastActive({ title: '임시저장을 완료했습니다.', toastType: 'Success' }); - navigate('/'); - } else { - setToastActive({ title: '임시저장에 실패했습니다.', toastType: 'Error' }); - } - }; - return (
{isTemporaryConfirmModal && ( @@ -128,7 +147,15 @@ export default function LetterEditor({ console.log(firstReplyRequest); handlePostFirstReply(firstReplyRequest); } else { - handlePostReply(letterRequest); + if (location.state?.isDraft) { + const temporaryRequest: TemporaryRequest = { + ...letterRequest, + letterId: location.state.draft.letterId, + }; + handleTemporaryLetter(temporaryRequest); + } else { + handlePostReply(letterRequest); + } } queryClient.invalidateQueries({ queryKey: ['mailBox'] }); queryClient.invalidateQueries({ queryKey: ['mailBoxDetail'] }); diff --git a/src/pages/Write/components/CategoryList.tsx b/src/pages/Write/components/CategoryList.tsx index 78cde3c..5e70229 100644 --- a/src/pages/Write/components/CategoryList.tsx +++ b/src/pages/Write/components/CategoryList.tsx @@ -9,7 +9,9 @@ function CategoryList() { return (
-

{stamp.title}

+

+ {stamp.title} +

); })} diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts index a0b56c1..ff69b02 100644 --- a/src/stores/authStore.ts +++ b/src/stores/authStore.ts @@ -1,7 +1,6 @@ import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; - -// import { postLogout } from '@/apis/auth'; +import useThemeStore from './themeStore'; interface AuthStore { isLoggedIn: boolean; @@ -21,6 +20,12 @@ const useAuthStore = create( zipCode: '', login: () => set({ isLoggedIn: true }), logout: async () => { + const theme = useThemeStore.getState().theme; + const toggleTheme = useThemeStore.getState().toggleTheme; + + if (theme === 'dark') { + toggleTheme(); + } set({ isLoggedIn: false, zipCode: '', accessToken: '' }); // location.reload(); // try { diff --git a/src/stores/themeStore.ts b/src/stores/themeStore.ts new file mode 100644 index 0000000..42c597e --- /dev/null +++ b/src/stores/themeStore.ts @@ -0,0 +1,33 @@ +import { create } from 'zustand'; +import { persist, createJSONStorage } from 'zustand/middleware'; + +interface ThemeStore { + theme: 'light' | 'dark'; + toggleTheme: () => void; +} + +const useThemeStore = create( + persist( + (set, get) => ({ + theme: get()?.theme ?? 'light', + toggleTheme: () => + set((state) => { + const newTheme = state.theme === 'light' ? 'dark' : 'light'; + + if (newTheme === 'dark') { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + + return { theme: newTheme }; + }), + }), + { + name: 'theme', + storage: createJSONStorage(() => sessionStorage), + }, + ), +); + +export default useThemeStore; diff --git a/src/styles/animations.css b/src/styles/animations.css index 293b084..d8d5398 100644 --- a/src/styles/animations.css +++ b/src/styles/animations.css @@ -105,7 +105,7 @@ transform-origin: bottom; } 100% { - transform: translateY(100%); + transform: translateY(160%); transform-origin: bottom; } } @@ -140,7 +140,7 @@ /* envelope out animation*/ @keyframes envelopeOut { 0% { - transform: translateY(100%); + transform: translateY(160%); } 100% { transform: translateY(2000%); diff --git a/src/styles/index.css b/src/styles/index.css index 910bae5..e2950ba 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -6,3 +6,5 @@ @import './utilities.css'; @import './fonts.css'; @import './animations.css'; + +@custom-variant dark (&:where(.dark, .dark *)); diff --git a/src/styles/preflight.css b/src/styles/preflight.css index c23d658..dbc3ff5 100644 --- a/src/styles/preflight.css +++ b/src/styles/preflight.css @@ -64,6 +64,32 @@ #f2f2f2; background-blend-mode: overlay, normal, normal; } + .dark .mobile-bg { + position: relative; /* ::before의 위치 기준 */ + background: + linear-gradient( + 180deg, + rgba(0, 0, 0, 1) 0%, + rgba(7, 0, 146, 0.5) 40%, + rgba(150, 150, 150, 0.5) 80% + ) + fixed, + rgba(109, 109, 109, 0.3); + } + + .dark .mobile-bg::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 50%; + background: url('/src/assets/images/background-dark.png') repeat center top; + background-size: cover; + opacity: 1; + z-index: 1; + } + .mobile-layout { display: flex; flex-direction: column; diff --git a/src/types/admin.d.ts b/src/types/admin.d.ts index 4ef77be..4efa604 100644 --- a/src/types/admin.d.ts +++ b/src/types/admin.d.ts @@ -73,6 +73,10 @@ interface BadWords { word: string; } +interface BadWordsData extends BadWords { + id: string; +} + // interface ModalContents { title: string;