diff --git a/package.json b/package.json index df32994..a5dc725 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@egjs/react-infinitegrid": "^4.12.0", "@mui/icons-material": "^6.4.4", "@tailwindcss/vite": "^4.0.6", "@tanstack/react-query": "^5.66.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e8ba04..329dd62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@egjs/react-infinitegrid': + specifier: ^4.12.0 + version: 4.12.0 '@mui/icons-material': specifier: ^6.4.4 version: 6.4.4(@mui/material@6.4.4(@types/react@19.0.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@19.0.8)(react@18.3.1) @@ -167,6 +170,33 @@ packages: resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==} engines: {node: '>=6.9.0'} + '@cfcs/core@0.0.24': + resolution: {integrity: sha512-feB38qu+eDk0Pggh/yR7gjaNmvUYA2uCxHP3Pz2MLE4LZ/9jPdtu8bzCSI47yTEhWyZCF5Pk698hdz8IN2mTjA==} + + '@cfcs/core@0.0.5': + resolution: {integrity: sha512-TZ/RXKV7MUrqFpiX+16uaaptv5jgChWNsuE6w8Rn3eJbO1PFd4Wpn25zeJbii7ch46ck8/ZIIYCcW3pHiYtm4Q==} + + '@egjs/children-differ@1.0.1': + resolution: {integrity: sha512-DRvyqMf+CPCOzAopQKHtW+X8iN6Hy6SFol+/7zCUiE5y4P/OB8JP8FtU4NxtZwtafvSL4faD5KoQYPj3JHzPFQ==} + + '@egjs/component@3.0.5': + resolution: {integrity: sha512-cLcGizTrrUNA2EYE3MBmEDt2tQv1joVP1Q3oDisZ5nw0MZDx2kcgEXM+/kZpfa/PAkFvYVhRUZwytIQWoN3V/w==} + + '@egjs/grid@1.16.0': + resolution: {integrity: sha512-w344hL2HNwmOHu379EG9I8ULkWF4A0cVf1XDn5nGlLXc+8c/rMvQadMBkGPOcdWMEKvFZpEiOPI7tWlXLOCloQ==} + + '@egjs/imready@1.4.1': + resolution: {integrity: sha512-JIOBs4lB7FYdsKi5uvz2j3SObX8eShtZjtqlOH41tm185aJOQZwiKBK8+V4MxzG4X6DqVhpdN8UcuVwBbElfsg==} + + '@egjs/infinitegrid@4.12.0': + resolution: {integrity: sha512-zoDz+mag7DAropcgmxW0Bx3JQ5ID19lk+AUN9rf9U7diwmO8IBfddX44jOVc9+ya5yZ+Goj95Fkm5S3GqCOrZQ==} + + '@egjs/list-differ@1.0.1': + resolution: {integrity: sha512-OTFTDQcWS+1ZREOdCWuk5hCBgYO4OsD30lXcOCyVOAjXMhgL5rBRDnt/otb6Nz8CzU0L/igdcaQBDLWc4t9gvg==} + + '@egjs/react-infinitegrid@4.12.0': + resolution: {integrity: sha512-V661KH1nka13wgy73N0IGBazZGe7LiECml1JDqB5DP6axTERUVCs8KRHadHu6oQihDeXwJIHe1YRhhMN9eAnUw==} + '@emotion/cache@11.14.0': resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} @@ -2371,6 +2401,45 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@cfcs/core@0.0.24': + dependencies: + '@egjs/component': 3.0.5 + + '@cfcs/core@0.0.5': + dependencies: + '@egjs/component': 3.0.5 + + '@egjs/children-differ@1.0.1': + dependencies: + '@egjs/list-differ': 1.0.1 + + '@egjs/component@3.0.5': {} + + '@egjs/grid@1.16.0': + dependencies: + '@egjs/children-differ': 1.0.1 + '@egjs/component': 3.0.5 + '@egjs/imready': 1.4.1 + + '@egjs/imready@1.4.1': + dependencies: + '@cfcs/core': 0.0.24 + '@egjs/component': 3.0.5 + + '@egjs/infinitegrid@4.12.0': + dependencies: + '@cfcs/core': 0.0.5 + '@egjs/children-differ': 1.0.1 + '@egjs/component': 3.0.5 + '@egjs/grid': 1.16.0 + '@egjs/list-differ': 1.0.1 + + '@egjs/list-differ@1.0.1': {} + + '@egjs/react-infinitegrid@4.12.0': + dependencies: + '@egjs/infinitegrid': 4.12.0 + '@emotion/cache@11.14.0': dependencies: '@emotion/memoize': 0.9.0 diff --git a/src/App.tsx b/src/App.tsx index 542309a..03cbaf9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,12 +31,15 @@ const App = () => { } /> - } /> + }> + } /> + } /> + } /> - } /> }> } /> + } /> } /> diff --git a/src/assets/.gitkeep b/src/assets/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/assets/icons/delete.svg b/src/assets/icons/delete.svg new file mode 100644 index 0000000..86a8132 --- /dev/null +++ b/src/assets/icons/delete.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index bd9733e..148e75f 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -1,8 +1,25 @@ import AlarmIcon from './alarm.svg?react'; import ArrowLeftIcon from './arrow-left.svg?react'; import BoardIcon from './board.svg?react'; +import DeleteIcon from './delete.svg?react'; import EnvelopeIcon from './envelope.svg?react'; +import LikeFilledIcon from './like-filled.svg?react'; +import LikeOutlinedIcon from './like-outlined.svg?react'; +import NoticeIcon from './notice.svg?react'; import PersonIcon from './person.svg?react'; -import SirenIcon from './siren.svg?react'; +import SirenFilledIcon from './siren-filled.svg?react'; +import SirenOutlinedIcon from './siren-outlined.svg?react'; -export { AlarmIcon, PersonIcon, ArrowLeftIcon, SirenIcon, EnvelopeIcon, BoardIcon }; +export { + AlarmIcon, + PersonIcon, + ArrowLeftIcon, + SirenFilledIcon, + SirenOutlinedIcon, + EnvelopeIcon, + BoardIcon, + NoticeIcon, + LikeFilledIcon, + LikeOutlinedIcon, + DeleteIcon, +}; diff --git a/src/assets/icons/like-filled.svg b/src/assets/icons/like-filled.svg new file mode 100644 index 0000000..6238ba6 --- /dev/null +++ b/src/assets/icons/like-filled.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/like-outlined.svg b/src/assets/icons/like-outlined.svg new file mode 100644 index 0000000..a210945 --- /dev/null +++ b/src/assets/icons/like-outlined.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/notice.svg b/src/assets/icons/notice.svg new file mode 100644 index 0000000..41d2136 --- /dev/null +++ b/src/assets/icons/notice.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/siren.svg b/src/assets/icons/siren-filled.svg similarity index 100% rename from src/assets/icons/siren.svg rename to src/assets/icons/siren-filled.svg diff --git a/src/assets/icons/siren-outlined.svg b/src/assets/icons/siren-outlined.svg new file mode 100644 index 0000000..e0555be --- /dev/null +++ b/src/assets/icons/siren-outlined.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/field-4.png b/src/assets/images/field-4.png new file mode 100644 index 0000000..8844d58 Binary files /dev/null and b/src/assets/images/field-4.png differ diff --git a/src/assets/images/memo-pink.png b/src/assets/images/memo-pink.png new file mode 100644 index 0000000..37b61b2 Binary files /dev/null and b/src/assets/images/memo-pink.png differ diff --git a/src/assets/images/memo-yellow.png b/src/assets/images/memo-yellow.png new file mode 100644 index 0000000..3705688 Binary files /dev/null and b/src/assets/images/memo-yellow.png differ diff --git a/src/assets/images/modal-pink.png b/src/assets/images/modal-pink.png new file mode 100644 index 0000000..746112f Binary files /dev/null and b/src/assets/images/modal-pink.png differ diff --git a/src/assets/images/yellow-modal.png b/src/assets/images/modal-yellow.png similarity index 100% rename from src/assets/images/yellow-modal.png rename to src/assets/images/modal-yellow.png diff --git a/src/components/BackgroundBottom.tsx b/src/components/BackgroundBottom.tsx new file mode 100644 index 0000000..1b8566a --- /dev/null +++ b/src/components/BackgroundBottom.tsx @@ -0,0 +1,12 @@ +import BgItem from '@/assets/images/field-4.png'; + +const BackgroundBottom = () => { + return ( +
+ ); +}; + +export default BackgroundBottom; diff --git a/src/components/ConfirmModal.tsx b/src/components/ConfirmModal.tsx index dd1336b..040f07b 100644 --- a/src/components/ConfirmModal.tsx +++ b/src/components/ConfirmModal.tsx @@ -1,4 +1,4 @@ -import ModalBg from '@/assets/images/yellow-modal.png'; +import ModalBg from '@/assets/images/modal-yellow.png'; import ModalOverlay from './ModalOverlay'; @@ -7,6 +7,7 @@ interface ConfirmModalProps { description: string; cancelText: string; confirmText: string; + confirmDisabled?: boolean; children?: React.ReactNode; onCancel: () => void; onConfirm: () => void; @@ -17,6 +18,7 @@ const ConfirmModal = ({ description, cancelText, confirmText, + confirmDisabled, children, onCancel, onConfirm, @@ -25,8 +27,10 @@ const ConfirmModal = ({ return (
-
- +

{title}

{description}

@@ -44,6 +48,7 @@ const ConfirmModal = ({ + +
+ + ); +}; + +export default MessageModal; diff --git a/src/components/ModalOverlay.tsx b/src/components/ModalOverlay.tsx index 2c637ab..95104ef 100644 --- a/src/components/ModalOverlay.tsx +++ b/src/components/ModalOverlay.tsx @@ -1,11 +1,44 @@ +import { useEffect } from 'react'; + +import { getScrollbarWidth } from '@/utils/getScrollbarWidth'; + interface ModalOverlayProps { - children: React.ReactElement; + closeOnOutsideClick?: boolean; + children: React.ReactNode; + onClose?: () => void; } -const ModalOverlay = ({ children }: ModalOverlayProps) => { +const ModalOverlay = ({ closeOnOutsideClick = false, children, onClose }: ModalOverlayProps) => { + useEffect(() => { + const scrollbarWidth = getScrollbarWidth(); + + document.documentElement.style.setProperty('--scrollbar-width', `${scrollbarWidth}px`); + document.body.classList.add('modal-open'); + + return () => { + document.documentElement.style.setProperty('--scrollbar-width', '0px'); + document.body.classList.remove('modal-open'); + }; + }, []); + + const handleClickOutside = () => { + if (closeOnOutsideClick && onClose) { + onClose(); + } + }; + + const handleClickInside = (event: React.MouseEvent) => { + event.stopPropagation(); + }; + return ( -
- {children} +
+
+ {children} +
); }; diff --git a/src/components/NoticeRolling.tsx b/src/components/NoticeRolling.tsx new file mode 100644 index 0000000..7390f75 --- /dev/null +++ b/src/components/NoticeRolling.tsx @@ -0,0 +1,26 @@ +import { Link } from 'react-router'; +import { twMerge } from 'tailwind-merge'; + +import { NoticeIcon } from '@/assets/icons'; + +const DUMMY_CONTENT = + '11월 15일은 수능! 고생하는 수험생들을 위해 응원의 11월 15일은 수능! 고생하는 수험 11월 15일은 수능! 고생하는 수험'; + +const NoticeRolling = () => { + return ( + +
+ +

{DUMMY_CONTENT}

+
+ + ); +}; + +export default NoticeRolling; diff --git a/src/components/PageTitle.tsx b/src/components/PageTitle.tsx new file mode 100644 index 0000000..e58cb9c --- /dev/null +++ b/src/components/PageTitle.tsx @@ -0,0 +1,16 @@ +import { twMerge } from 'tailwind-merge'; + +interface PageTitleProps { + className?: string; + children: React.ReactNode; +} + +const PageTitle = ({ className, children }: PageTitleProps) => { + return ( +

+ {children} +

+ ); +}; + +export default PageTitle; diff --git a/src/components/ReportModal.tsx b/src/components/ReportModal.tsx new file mode 100644 index 0000000..923ec42 --- /dev/null +++ b/src/components/ReportModal.tsx @@ -0,0 +1,60 @@ +import { useState } from 'react'; +import { twMerge } from 'tailwind-merge'; + +import ConfirmModal from './ConfirmModal'; +import TextareaField from './TextareaField'; + +interface ReportModalProps { + onClose: () => void; +} + +const REPORT_REASON = ['욕설', '비방', '폭언', '성희롱', '기타']; + +const ReportModal = ({ onClose }: ReportModalProps) => { + const [selected, setSelected] = useState(''); + const [additionalReason, setAdditionalReason] = useState(''); + + const handleReasonClick = (reason: string) => { + if (selected === reason) setSelected(''); + else setSelected(reason); + }; + + const handleSubmit = () => { + onClose(); + }; + + return ( + +
+ {REPORT_REASON.map((reason) => ( + + ))} +
+ setAdditionalReason(e.target.value)} + /> +
+ ); +}; + +export default ReportModal; diff --git a/src/components/TextareaField.tsx b/src/components/TextareaField.tsx new file mode 100644 index 0000000..deedc17 --- /dev/null +++ b/src/components/TextareaField.tsx @@ -0,0 +1,12 @@ +import { ComponentPropsWithoutRef } from 'react'; + +const TextareaField = ({ ...props }: ComponentPropsWithoutRef<'textarea'>) => { + return ( +