diff --git a/public/assets/baekgyeong-speaker.png b/public/assets/baekgyeong-speaker.png deleted file mode 100644 index c8177981..00000000 Binary files a/public/assets/baekgyeong-speaker.png and /dev/null differ diff --git a/public/assets/baekgyeong-whalebe.png b/public/assets/baekgyeong-whalebe.png deleted file mode 100644 index d0a04d46..00000000 Binary files a/public/assets/baekgyeong-whalebe.png and /dev/null differ diff --git a/public/assets/pknu.png b/public/assets/pknu.png deleted file mode 100644 index 4b5a391a..00000000 Binary files a/public/assets/pknu.png and /dev/null differ diff --git a/public/assets/tipImages/png/baekgyeong_camera.png b/public/assets/tipImages/png/baekgyeong_camera.png new file mode 100644 index 00000000..6e5e1e4b Binary files /dev/null and b/public/assets/tipImages/png/baekgyeong_camera.png differ diff --git a/public/assets/tipImages/png/baekgyeong_guide.png b/public/assets/tipImages/png/baekgyeong_guide.png new file mode 100644 index 00000000..954263d9 Binary files /dev/null and b/public/assets/tipImages/png/baekgyeong_guide.png differ diff --git a/public/assets/tipImages/png/baekgyeong_hand_love.png b/public/assets/tipImages/png/baekgyeong_hand_love.png new file mode 100644 index 00000000..1c8dbe58 Binary files /dev/null and b/public/assets/tipImages/png/baekgyeong_hand_love.png differ diff --git a/public/assets/tipImages/png/baekgyeong_hi.png b/public/assets/tipImages/png/baekgyeong_hi.png new file mode 100644 index 00000000..81b4ee26 Binary files /dev/null and b/public/assets/tipImages/png/baekgyeong_hi.png differ diff --git a/public/assets/tipImages/png/baekgyeong_love.png b/public/assets/tipImages/png/baekgyeong_love.png new file mode 100644 index 00000000..cc711f1c Binary files /dev/null and b/public/assets/tipImages/png/baekgyeong_love.png differ diff --git a/public/assets/tipImages/png/baekgyeong_search.png b/public/assets/tipImages/png/baekgyeong_search.png new file mode 100644 index 00000000..b88c718b Binary files /dev/null and b/public/assets/tipImages/png/baekgyeong_search.png differ diff --git a/public/assets/tipImages/png/baekgyeong_suprised.png b/public/assets/tipImages/png/baekgyeong_suprised.png new file mode 100644 index 00000000..781114ef Binary files /dev/null and b/public/assets/tipImages/png/baekgyeong_suprised.png differ diff --git a/public/assets/tipImages/png/baekgyeong_teach.png b/public/assets/tipImages/png/baekgyeong_teach.png new file mode 100644 index 00000000..29cf218d Binary files /dev/null and b/public/assets/tipImages/png/baekgyeong_teach.png differ diff --git a/public/assets/tipImages/png/baekgyeong_underpin.png b/public/assets/tipImages/png/baekgyeong_underpin.png new file mode 100644 index 00000000..1ddc29fa Binary files /dev/null and b/public/assets/tipImages/png/baekgyeong_underpin.png differ diff --git a/public/assets/tipImages/png/pknu.png b/public/assets/tipImages/png/pknu.png new file mode 100644 index 00000000..1d357e80 Binary files /dev/null and b/public/assets/tipImages/png/pknu.png differ diff --git a/public/assets/tipImages/webp/baekgyeong_camera.webp b/public/assets/tipImages/webp/baekgyeong_camera.webp new file mode 100644 index 00000000..e888c7f3 Binary files /dev/null and b/public/assets/tipImages/webp/baekgyeong_camera.webp differ diff --git a/public/assets/tipImages/webp/baekgyeong_guide.webp b/public/assets/tipImages/webp/baekgyeong_guide.webp new file mode 100644 index 00000000..3d1a92ae Binary files /dev/null and b/public/assets/tipImages/webp/baekgyeong_guide.webp differ diff --git a/public/assets/tipImages/webp/baekgyeong_hand_love.webp b/public/assets/tipImages/webp/baekgyeong_hand_love.webp new file mode 100644 index 00000000..1fb9682b Binary files /dev/null and b/public/assets/tipImages/webp/baekgyeong_hand_love.webp differ diff --git a/public/assets/tipImages/webp/baekgyeong_hi.webp b/public/assets/tipImages/webp/baekgyeong_hi.webp new file mode 100644 index 00000000..866c6c70 Binary files /dev/null and b/public/assets/tipImages/webp/baekgyeong_hi.webp differ diff --git a/public/assets/tipImages/webp/baekgyeong_love.webp b/public/assets/tipImages/webp/baekgyeong_love.webp new file mode 100644 index 00000000..122f2697 Binary files /dev/null and b/public/assets/tipImages/webp/baekgyeong_love.webp differ diff --git a/public/assets/tipImages/webp/baekgyeong_search.webp b/public/assets/tipImages/webp/baekgyeong_search.webp new file mode 100644 index 00000000..31ae09a5 Binary files /dev/null and b/public/assets/tipImages/webp/baekgyeong_search.webp differ diff --git a/public/assets/tipImages/webp/baekgyeong_suprised.webp b/public/assets/tipImages/webp/baekgyeong_suprised.webp new file mode 100644 index 00000000..aa2881cc Binary files /dev/null and b/public/assets/tipImages/webp/baekgyeong_suprised.webp differ diff --git a/public/assets/tipImages/webp/baekgyeong_teach.webp b/public/assets/tipImages/webp/baekgyeong_teach.webp new file mode 100644 index 00000000..2d25ad14 Binary files /dev/null and b/public/assets/tipImages/webp/baekgyeong_teach.webp differ diff --git a/public/assets/tipImages/webp/baekgyeong_underpin.webp b/public/assets/tipImages/webp/baekgyeong_underpin.webp new file mode 100644 index 00000000..feb62201 Binary files /dev/null and b/public/assets/tipImages/webp/baekgyeong_underpin.webp differ diff --git a/public/assets/tipImages/webp/pknu.webp b/public/assets/tipImages/webp/pknu.webp new file mode 100644 index 00000000..8cc93f01 Binary files /dev/null and b/public/assets/tipImages/webp/pknu.webp differ diff --git a/src/@types/announcement.ts b/src/@types/announcement.ts index db26c25f..451930ee 100644 --- a/src/@types/announcement.ts +++ b/src/@types/announcement.ts @@ -1,3 +1,8 @@ +import { + ANNOUNCEMENT_CATEGORY, + ANNOUNCEMENT_TYPE, +} from '@constants/announcement'; + type AnnounceItemType = '고정' | '일반'; export interface AnnounceItem { @@ -11,3 +16,9 @@ export interface AnnounceItem { export type AnnounceItemList = { [key in AnnounceItemType]: AnnounceItem[]; }; + +export type AnnouncementCategory = + (typeof ANNOUNCEMENT_CATEGORY)[keyof typeof ANNOUNCEMENT_CATEGORY]; + +export type AnnouncementType = + (typeof ANNOUNCEMENT_TYPE)[keyof typeof ANNOUNCEMENT_TYPE]; diff --git a/src/@types/modals.ts b/src/@types/modals.ts index e68b92d5..c1866f80 100644 --- a/src/@types/modals.ts +++ b/src/@types/modals.ts @@ -1,8 +1,5 @@ -import { modals } from '@hooks/useModals'; - export type Modals = | Array<{ - Component: (typeof modals)[keyof typeof modals]; - props: object; + Component: React.ReactElement<{ chidren: React.ReactNode }>; }> | []; diff --git a/src/@types/styles/icon.ts b/src/@types/styles/icon.ts index 2fb79c48..364cad69 100644 --- a/src/@types/styles/icon.ts +++ b/src/@types/styles/icon.ts @@ -5,6 +5,9 @@ export type IconKind = | 'menu' | 'notification' | 'school' + | 'schoolBuilding' + | 'arrowRight' + | 'arrowDown' | 'arrowBack' | 'plus' | 'edit' @@ -22,4 +25,6 @@ export type IconKind = | 'checkedRadio' | 'uncheckedRadio' | 'location' - | 'warning'; + | 'warning' + | 'account' + | 'language'; diff --git a/src/App.tsx b/src/App.tsx index 18c1e4ed..01c9c664 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,34 +1,34 @@ +import BodyLayout from '@components/BodyLayout'; import FooterTab from '@components/FooterTab'; import Header from '@components/Header'; import Announcement from '@pages/Announcement'; -import BodyLayout from '@pages/BodyLayout'; +import FAQPage from '@pages/FAQ'; import Home from '@pages/Home'; import MajorDecision from '@pages/MajorDecision'; -import Map from '@pages/Map'; -import MapProvider from '@pages/Map/Provider'; +import MapPage from '@pages/Map'; import My from '@pages/My'; +import SuggestionPage from '@pages/Suggestion'; import Tip from '@pages/Tip'; import RouteChangeTracker from '@utils/routeChangeTracker'; -import { Routes, Route, useLocation } from 'react-router-dom'; +import { Routes, Route } from 'react-router-dom'; const App = () => { - const location = useLocation(); RouteChangeTracker(); return ( <> - {location.pathname !== '/map' &&
} +
}> } /> } /> } /> } /> - } /> - - }> - } /> + } /> + } /> + } /> + } /> diff --git a/src/apis/Suspense/fetch-announce-list.ts b/src/apis/Suspense/fetch-announce-list.ts index 3a2ec00e..d7a8f4a7 100644 --- a/src/apis/Suspense/fetch-announce-list.ts +++ b/src/apis/Suspense/fetch-announce-list.ts @@ -1,12 +1,11 @@ -import Major from '@type/major'; import { AxiosResponse } from 'axios'; import wrapPromise from './wrap-promise'; import http from '../http'; -const fetchAnnounceList = (major: Major) => { +const fetchAnnounceList = (endPoint: string) => { const promise: Promise> = http - .get(major ? `/api/announcement?major=${major}` : `/api/announcement`) + .get(`/api/announcement` + endPoint) .then((res) => res.data); return wrapPromise(promise); diff --git a/src/apis/suggestion/post-suggestion.ts b/src/apis/suggestion/post-suggestion.ts new file mode 100644 index 00000000..7905f71a --- /dev/null +++ b/src/apis/suggestion/post-suggestion.ts @@ -0,0 +1,18 @@ +import http from '@apis/http'; +import { SERVER_URL } from '@config/index'; + +const postSuggestion = async (value: string | undefined) => { + await http.post( + `${SERVER_URL}/api/suggestion`, + { + content: value, + }, + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ); +}; + +export default postSuggestion; diff --git a/src/pages/BodyLayout/index.tsx b/src/components/BodyLayout/index.tsx similarity index 100% rename from src/pages/BodyLayout/index.tsx rename to src/components/BodyLayout/index.tsx diff --git a/src/components/Card/AnnounceCard/AnnounceList/index.tsx b/src/components/Card/AnnounceCard/AnnounceList/index.tsx deleted file mode 100644 index d6f28700..00000000 --- a/src/components/Card/AnnounceCard/AnnounceList/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { AnnounceItemList } from '@type/announcement'; -import { AxiosError, AxiosResponse } from 'axios'; - -import AnnounceCard from '..'; - -type Resource = - | AxiosResponse - | AnnounceItemList - | AxiosError - | null; - -interface AnnounceListProps { - resource: { - read: () => Resource; - }; -} - -const AnnounceList = ({ resource }: AnnounceListProps) => { - const announceList: Resource = resource.read(); - - if (announceList === null || announceList instanceof Error) { - return null; - } - const { 고정: pinned, 일반: normal } = announceList as AnnounceItemList; - - return ( - <> - {pinned.map((announce, idx) => ( -
- -
- ))} - {normal.map((announce, idx) => ( -
- -
- ))} - - ); -}; - -export default AnnounceList; diff --git a/src/components/Card/AnnounceCard/index.test.tsx b/src/components/Card/AnnounceCard/index.test.tsx index 6558704d..aadaa992 100644 --- a/src/components/Card/AnnounceCard/index.test.tsx +++ b/src/components/Card/AnnounceCard/index.test.tsx @@ -1,4 +1,5 @@ import http from '@apis/http'; +import MajorProvider from '@components/Providers/MajorProvider'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { AnnounceItemList } from '@type/announcement'; @@ -34,7 +35,12 @@ describe('공지사항 카드 컴포넌트 테스트', () => { const { 고정, 일반 } = announceList; 일반.forEach(async (annouce) => { - render(, { wrapper: MemoryRouter }); + render( + + + , + { wrapper: MemoryRouter }, + ); }); const annouceCards = screen.getAllByTestId('card'); diff --git a/src/components/Card/AnnounceCard/index.tsx b/src/components/Card/AnnounceCard/index.tsx index 073e9c4e..42346fbd 100644 --- a/src/components/Card/AnnounceCard/index.tsx +++ b/src/components/Card/AnnounceCard/index.tsx @@ -1,39 +1,34 @@ -import Icon from '@components/Icon'; -import { css } from '@emotion/react'; import styled from '@emotion/styled'; +import useMajor from '@hooks/useMajor'; import { THEME } from '@styles/ThemeProvider/theme'; import { AnnounceItem } from '@type/announcement'; +import openLink from '@utils/router/openLink'; interface AnnounceCardProps extends AnnounceItem { - pinned?: boolean; + author?: string; } const AnnounceCard = ({ title, link, uploadDate, - pinned = false, + author, }: AnnounceCardProps) => { - const onClick = () => { - window.open(link, '_blank'); - }; + const { major } = useMajor(); uploadDate = uploadDate.slice(2); return ( - + openLink(link)} data-testid="card"> - {pinned && } - {title} - - {uploadDate} + {title} + + 20{uploadDate} + + {author ? author : major} + + ); }; @@ -41,53 +36,61 @@ const AnnounceCard = ({ export default AnnounceCard; const Card = styled.div` - height: 28px; - padding: 10px; + min-height: 50px; display: flex; flex-direction: column; justify-content: center; - color: ${THEME.TEXT.BLACK}; + + transition: 0.3s; + &:active { + transform: scale(0.95); + opacity: 0.6; + } `; const ContentContainer = styled.div` + padding: 20px 0 20px 0; display: flex; - align-items: center; + line-height: 1.5; + flex-direction: column; + + gap: 10px; `; -const AnnounceTitle = styled.span<{ pinned: boolean }>` +const AnnounceTitle = styled.span` + display: flex; + align-items: center; flex: 9; - font-size: 15px; - font-weight: ${({ pinned }) => (pinned ? 'bold' : 500)}; - margin-left: ${({ pinned }) => (pinned ? '' : '28px')}; - - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - &: hover { - cursor: pointer; - } - - transition: 0.3s; - &:active { - transform: scale(0.95); - opacity: 0.6; - } + font-size: 16px; + font-weight: 500; `; -const VertialSeparator = styled.div` +const VertialBoundaryLine = styled.div` border-left: 1px solid gray; height: 12px; margin: 0 5px; `; const AnnounceDate = styled.span` - flex: 1; - font-size: 10px; - font-weight: bold; - text-align: end; + font-size: 13px; white-space: nowrap; color: ${THEME.TEXT.GRAY}; + padding-right: 5px; +`; + +const HorizonBoundaryLine = styled.div` + border-bottom: 1px solid ${THEME.BACKGROUND}; +`; + +const SubContent = styled.div` + display: flex; + align-items: center; +`; + +const Source = styled.div` + font-size: 13px; + color: gray; + padding-left: 5px; `; diff --git a/src/components/Card/InformCard/index.test.tsx b/src/components/Card/InformCard/index.test.tsx index 0eda9a28..01dd8c0d 100644 --- a/src/components/Card/InformCard/index.test.tsx +++ b/src/components/Card/InformCard/index.test.tsx @@ -1,6 +1,6 @@ -import AlertModal from '@components/Modal/AlertModal'; -import ModalsProvider from '@components/ModalsProvider'; -import { MODAL_MESSAGE } from '@constants/modal-messages'; +import Modal from '@components/Common/Modal'; +import ModalsProvider from '@components/Providers/ModalsProvider'; +import { MODAL_BUTTON_MESSAGE, MODAL_MESSAGE } from '@constants/modal-messages'; import MajorContext from '@contexts/major'; import useModals from '@hooks/useModals'; import { render, screen } from '@testing-library/react'; @@ -9,8 +9,8 @@ import Major from '@type/major'; import { IconKind } from '@type/styles/icon'; import { act } from 'react-dom/test-utils'; import { MemoryRouter } from 'react-router-dom'; - import '@testing-library/jest-dom'; + import InformCard from './index'; type INFORM_CARD_TYPE = 'ANNOUNCEMENT' | 'GRADUATION'; @@ -18,7 +18,7 @@ type INFORM_CARD_TYPE = 'ANNOUNCEMENT' | 'GRADUATION'; type INFORM_CARD_DATA = { [key in INFORM_CARD_TYPE]: { title: string; - icon: IconKind & ('school' | 'notification'); + icon: IconKind & ('school' | 'schoolBuilding'); onClick: () => void; }; }; @@ -26,8 +26,8 @@ type INFORM_CARD_DATA = { const graduationLink = 'https://ce.pknu.ac.kr/ce/2889'; const INFORM_CARD: INFORM_CARD_DATA = { ANNOUNCEMENT: { - title: '공지사항', - icon: 'notification', + title: '학교 공지사항', + icon: 'schoolBuilding', onClick: () => mockRouterTo('/announcement'), }, GRADUATION: { @@ -43,12 +43,13 @@ const setMajorMock = (isRender: boolean) => { jest.mock('react', () => ({ ...jest.requireActual('react'), - useState: () => [mockMajor, mockSetMajor], + useState: () => [mockMajor, mockSetMajor, graduationLink], })); return { major: mockMajor, setMajor: mockSetMajor, + graduationLink, }; }; @@ -139,13 +140,16 @@ describe('InformCard 컴포넌트 테스트', () => { await userEvent.click(card); }); - expect(useModals().openModal).toHaveBeenCalledWith(AlertModal, { - message: MODAL_MESSAGE.ALERT.SET_MAJOR, - buttonMessage: '전공선택하러 가기', - iconKind: 'plus', - onClose: expect.any(Function), - routerTo: expect.any(Function), - }); + expect(useModals().openModal).toHaveBeenCalledWith( + + + + , + ); }); it('전역상태가 설정 됐을 경우, 졸업요건 클릭 시 페이지 이동 테스트', async () => { diff --git a/src/components/Card/InformCard/index.tsx b/src/components/Card/InformCard/index.tsx index 3261abaa..67b95b32 100644 --- a/src/components/Card/InformCard/index.tsx +++ b/src/components/Card/InformCard/index.tsx @@ -1,15 +1,15 @@ -import Icon from '@components/Icon'; +import Icon from '@components/Common/Icon'; +import Modal from '@components/Common/Modal'; import { MODAL_BUTTON_MESSAGE, MODAL_MESSAGE } from '@constants/modal-messages'; -import { css } from '@emotion/react'; import styled from '@emotion/styled'; import useMajor from '@hooks/useMajor'; -import useModals, { modals } from '@hooks/useModals'; +import useModals from '@hooks/useModals'; import useRouter from '@hooks/useRouter'; import { THEME } from '@styles/ThemeProvider/theme'; import { IconKind } from '@type/styles/icon'; interface InformCardProps { - icon: IconKind & ('school' | 'notification'); + icon: IconKind & ('school' | 'schoolBuilding' | 'speaker'); title: string; majorRequired: boolean; onClick: () => void; @@ -23,99 +23,76 @@ const InformCard = ({ }: InformCardProps) => { const { major } = useMajor(); const { routerTo } = useRouter(); - const routerToMajorDecision = () => routerTo('/major-decision'); - const { openModal, closeModal } = useModals(); + const { openModal } = useModals(); + + const routeToMajorDecisionPage = () => routerTo('/major-decision'); const handleMajorModal = () => { if (!majorRequired || major) { onClick(); return; } - openModal(modals.alert, { - message: MODAL_MESSAGE.ALERT.SET_MAJOR, - buttonMessage: MODAL_BUTTON_MESSAGE.SET_MAJOR, - iconKind: 'plus', - onClose: () => closeModal(modals.alert), - routerTo: () => { - closeModal(modals.alert); - routerToMajorDecision(); - }, - }); + + openModal( + + + + , + ); }; return ( - <> - - -
- -
-
- - - {title} - - - {title} 보러가기 - - -
- + + + + + + {title} + {title} 보러가기 + + ); }; export default InformCard; const Card = styled.div` - display: flex; - flex-direction: row; padding: 3% 1% 2% 0; - color: ${THEME.TEXT.GRAY}; - height: 70px; + height: 4rem; + display: flex; + align-items: center; - & > svg { - margin: 10px 0; + span:nth-of-type(1) { + font-size: 12px; + color: ${THEME.TEXT.GRAY}; } - cursor: pointer; - transition: all 0.2s ease-in-out; - - &:active { - transform: scale(0.95); - opacity: 0.6; + span:nth-of-type(2) { + font-size: 16px; + font-weight: bold; + color: ${THEME.TEXT.BLACK}; } + + transition: all 0.2s ease-in-out; `; -const Wrapper = styled.div` - &:first-of-type { - display: flex; - align-items: center; - } +const TextContainer = styled.div` + display: flex; + flex-direction: column; + gap: 5px; +`; - &:nth-of-type(2) { - display: flex; - flex-direction: column; - padding: 4% 0 3% 3%; - } +const IconContainer = styled.div` + height: 45px; + width: 45px; + display: flex; + justify-content: center; + align-items: center; + margin-right: 10px; + border-radius: 50%; + background-color: ${THEME.PRIMARY}; `; diff --git a/src/components/Card/TipCard/TipImage.tsx b/src/components/Card/TipCard/TipImage.tsx new file mode 100644 index 00000000..10db07ed --- /dev/null +++ b/src/components/Card/TipCard/TipImage.tsx @@ -0,0 +1,32 @@ +import Image from '@components/Common/Image'; +import { css } from '@emotion/react'; +import React from 'react'; + +interface TipImageProps { + title: string; + webpPath: string; + pngPath: string; +} + +const TipImage = ({ title, webpPath, pngPath }: TipImageProps) => { + return ( + + + {title} + + ); +}; + +export default TipImage; diff --git a/src/components/Card/TipCard/TipSubTitle.tsx b/src/components/Card/TipCard/TipSubTitle.tsx new file mode 100644 index 00000000..d851bbb3 --- /dev/null +++ b/src/components/Card/TipCard/TipSubTitle.tsx @@ -0,0 +1,25 @@ +import styled from '@emotion/styled'; +import React from 'react'; + +interface TipSubTitleProps { + subTitle: string; +} + +const TipSubTitle = ({ subTitle }: TipSubTitleProps) => { + const seperatedSubTitle = subTitle.split('\n'); + return ( + + {seperatedSubTitle.map((subTitle, index) => ( +

{subTitle}

+ ))} +
+ ); +}; + +export default TipSubTitle; + +const SubTitle = styled.span` + padding: 0 0 0 16px; + line-height: 1rem; + font-size: 0.8rem; +`; diff --git a/src/components/Card/TipCard/TipTitle.tsx b/src/components/Card/TipCard/TipTitle.tsx new file mode 100644 index 00000000..d3b749f3 --- /dev/null +++ b/src/components/Card/TipCard/TipTitle.tsx @@ -0,0 +1,26 @@ +import styled from '@emotion/styled'; +import React from 'react'; + +interface TipTitleProps { + title: string; +} + +const TipTitle = ({ title }: TipTitleProps) => { + const seperatedTitle = title.split('\n'); + return ( + + {seperatedTitle.map((titleItem, index) => ( + <p key={index}>{titleItem}</p> + ))} + + ); +}; + +export default TipTitle; + +const Title = styled.span` + padding: 20px 0px 10px 16px; + line-height: 1.2rem; + font-size: 1.2rem; + font-weight: bold; +`; diff --git a/src/components/Card/TipCard/domain/getTipCardSubElement.ts b/src/components/Card/TipCard/domain/getTipCardSubElement.ts new file mode 100644 index 00000000..083e0174 --- /dev/null +++ b/src/components/Card/TipCard/domain/getTipCardSubElement.ts @@ -0,0 +1,21 @@ +import { Children, isValidElement } from 'react'; + +import TipImage from '../TipImage'; +import TipSubTitle from '../TipSubTitle'; +import TipTitle from '../TipTitle'; + +type TipCardChildType = typeof TipTitle | typeof TipSubTitle | typeof TipImage; + +const getTipCardSubElement = ( + children: React.ReactNode, + childType: TipCardChildType, +) => { + const chidrenArray = Children.toArray(children); + const targetChild = chidrenArray + .filter((child) => isValidElement(child) && child.type === childType) + .slice(0, 2); + + return targetChild; +}; + +export default getTipCardSubElement; diff --git a/src/components/Card/TipCard/index.tsx b/src/components/Card/TipCard/index.tsx new file mode 100644 index 00000000..6cc301b3 --- /dev/null +++ b/src/components/Card/TipCard/index.tsx @@ -0,0 +1,47 @@ +import styled from '@emotion/styled'; +import { THEME } from '@styles/ThemeProvider/theme'; +import React from 'react'; + +import getTipCardSubElement from './domain/getTipCardSubElement'; +import TipImage from './TipImage'; +import TipSubTitle from './TipSubTitle'; +import TipTitle from './TipTitle'; + +type StrictPropsWithChildren = T & { + children: React.ReactNode; + onClick: () => void; +}; + +const TipCard = ({ children, onClick }: StrictPropsWithChildren) => { + const tipTitle = getTipCardSubElement(children, TipTitle); + const tipSubTitle = getTipCardSubElement(children, TipSubTitle); + const tipImage = getTipCardSubElement(children, TipImage); + + return ( + + {tipTitle} + {tipSubTitle} + {tipImage} + + ); +}; + +export default TipCard; + +TipCard.TipTitle = TipTitle; +TipCard.TipSubTitle = TipSubTitle; +TipCard.TipImage = TipImage; + +const Container = styled.div` + position: relative; + height: 10rem; + width: 10rem; + display: flex; + flex-direction: column; + background-color: ${THEME.PRIMARY}20; + border: 1px solid ${THEME.PRIMARY}30; + border-radius: 10px; + box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1); + gap: 5px; + z-index: 1; +`; diff --git a/src/components/Carousel/index.tsx b/src/components/Carousel/index.tsx index 58336a48..86fa4fd8 100644 --- a/src/components/Carousel/index.tsx +++ b/src/components/Carousel/index.tsx @@ -7,10 +7,11 @@ import Slider from 'react-slick'; import 'slick-carousel/slick/slick.css'; import 'slick-carousel/slick/slick-theme.css'; -interface WhalebeData { +export interface WhalebeData { title: string; - date: string; - imgUrl: string; + operating_period: string; + recruitment_period: string; + imgurl: string; link: string; } @@ -47,10 +48,11 @@ const Carousel = () => { onClick={() => window.open(data.link, '_blank')} > - + {data.title} - 모집기간: ~ {data.date} + 모집기간: {data.recruitment_period} + 운영기간: {data.operating_period} @@ -67,7 +69,7 @@ const CarouselContainer = styled.div` padding: 1rem 0 1rem; width: 100%; margin: 0 auto; - &: hover { + &:hover { cursor: pointer; } `; @@ -80,14 +82,14 @@ const SliderWrapper = styled.div` const Title = styled.div` font-weight: bold; font-size: 1rem; - height: 2rem; + height: 3rem; overflow: hidden; text-overflow: ellipsis; `; const Date = styled.div` color: ${THEME.TEXT.GRAY}; - margin-top: 2rem; + margin-top: 0.3rem; font-size: 0.8rem; `; @@ -100,7 +102,7 @@ const Button = styled.button` border-radius: 0.5rem; margin-top: 1rem; - &: hover { + &:hover { cursor: pointer; } `; diff --git a/src/components/Button/Toggle/index.tsx b/src/components/Common/Button/Toggle/index.tsx similarity index 100% rename from src/components/Button/Toggle/index.tsx rename to src/components/Common/Button/Toggle/index.tsx index 4b3199e7..1385f9a3 100644 --- a/src/components/Button/Toggle/index.tsx +++ b/src/components/Common/Button/Toggle/index.tsx @@ -8,11 +8,6 @@ interface Props { animation: boolean; } -interface Circle { - isOn: boolean; - animation: boolean; -} - const ToggleButton = (props: Props) => { const { isOn, changeState, animation } = props; @@ -25,6 +20,11 @@ const ToggleButton = (props: Props) => { export default ToggleButton; +interface Circle { + isOn: boolean; + animation: boolean; +} + const Button = styled.button` position: relative; border: none; diff --git a/src/components/Button/index.tsx b/src/components/Common/Button/index.tsx similarity index 100% rename from src/components/Button/index.tsx rename to src/components/Common/Button/index.tsx diff --git a/src/components/Icon/index.tsx b/src/components/Common/Icon/index.tsx similarity index 83% rename from src/components/Icon/index.tsx rename to src/components/Common/Icon/index.tsx index 7bb52d50..06c0a05e 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Common/Icon/index.tsx @@ -5,6 +5,7 @@ import { MdHome, MdAccountCircle, MdSchool, + MdOutlineLocationCity, MdNotifications, MdMenu, MdArrowBackIos, @@ -25,6 +26,10 @@ import { MdOutlineLightbulb, MdOutlineMyLocation, MdOutlineError, + MdOutlineKeyboardArrowRight, + MdOutlineKeyboardArrowDown, + MdAssignmentInd, + MdLanguage, } from 'react-icons/md'; const ICON: { [key in IconKind]: IconType } = { @@ -34,6 +39,9 @@ const ICON: { [key in IconKind]: IconType } = { menu: MdMenu, notification: MdNotifications, school: MdSchool, + schoolBuilding: MdOutlineLocationCity, + arrowRight: MdOutlineKeyboardArrowRight, + arrowDown: MdOutlineKeyboardArrowDown, arrowBack: MdArrowBackIos, plus: MdAddCircleOutline, edit: MdOutlineModeEdit, @@ -52,6 +60,8 @@ const ICON: { [key in IconKind]: IconType } = { checkedRadio: MdRadioButtonChecked, location: MdOutlineMyLocation, warning: MdOutlineError, + account: MdAssignmentInd, + language: MdLanguage, }; interface IconProps { diff --git a/src/components/Image/index.tsx b/src/components/Common/Image/index.tsx similarity index 97% rename from src/components/Image/index.tsx rename to src/components/Common/Image/index.tsx index 519e1231..c4572406 100644 --- a/src/components/Image/index.tsx +++ b/src/components/Common/Image/index.tsx @@ -7,7 +7,7 @@ const imageSize: ImageSize = { large: setSize(200), medium: setSize(150), small: setSize(100), - tiny: setSize(45), + tiny: setSize(80), }; const Image = ({ diff --git a/src/components/Common/Modal/ModalButton.tsx b/src/components/Common/Modal/ModalButton.tsx new file mode 100644 index 00000000..5edf2a1d --- /dev/null +++ b/src/components/Common/Modal/ModalButton.tsx @@ -0,0 +1,37 @@ +import { css } from '@emotion/react'; +import useModals from '@hooks/useModals'; +import { THEME } from '@styles/ThemeProvider/theme'; +import { IconKind } from '@type/styles/icon'; +import React from 'react'; + +import Button from '../Button'; +import Icon from '../Icon'; + +interface ModalButtonProps { + text: string; + iconKind?: IconKind & 'plus'; + onClick?: () => void; +} + +const ModalButton = ({ text, iconKind, onClick }: ModalButtonProps) => { + const { closeModal } = useModals(); + + const onButtonClick = () => { + closeModal(); + onClick && onClick(); + }; + + return ( + + ); +}; + +export default ModalButton; diff --git a/src/components/Common/Modal/ModalTitle.tsx b/src/components/Common/Modal/ModalTitle.tsx new file mode 100644 index 00000000..1502ad57 --- /dev/null +++ b/src/components/Common/Modal/ModalTitle.tsx @@ -0,0 +1,22 @@ +import styled from '@emotion/styled'; +import { THEME } from '@styles/ThemeProvider/theme'; +import React from 'react'; + +interface ModalTitleProps { + title: string; +} + +const ModalTitle = ({ title }: ModalTitleProps) => { + return {title}; +}; + +export default ModalTitle; + +const Title = styled.span` + padding: 20px 0 20px 0; + text-align: center; + line-height: 1.3; + font-weight: bold; + white-space: pre-line; + color: ${THEME.TEXT.GRAY}; +`; diff --git a/src/components/Modal/Modals/index.tsx b/src/components/Common/Modal/Modals/index.tsx similarity index 69% rename from src/components/Modal/Modals/index.tsx rename to src/components/Common/Modal/Modals/index.tsx index 3e47c916..491a2f7e 100644 --- a/src/components/Modal/Modals/index.tsx +++ b/src/components/Common/Modal/Modals/index.tsx @@ -7,8 +7,8 @@ const Modals = () => { return ( <> {modals && - modals.map(({ Component, props }, idx) => { - return ; + modals.map(({ Component }, idx) => { + return {Component}; })} ); diff --git a/src/components/Common/Modal/domain/getModalSubElement.ts b/src/components/Common/Modal/domain/getModalSubElement.ts new file mode 100644 index 00000000..c4d7068d --- /dev/null +++ b/src/components/Common/Modal/domain/getModalSubElement.ts @@ -0,0 +1,20 @@ +import { Children, isValidElement } from 'react'; + +import ModalButton from '../ModalButton'; +import ModalTitle from '../ModalTitle'; + +type ModalChildType = typeof ModalTitle | typeof ModalButton; + +const getModalSubElement = ( + children: React.ReactNode, + childType: ModalChildType, +) => { + const childrenToArray = Children.toArray(children); + const targetChild = childrenToArray + .filter((child) => isValidElement(child) && child.type === childType) + .slice(0, 2); + + return targetChild; +}; + +export default getModalSubElement; diff --git a/src/components/Common/Modal/index.tsx b/src/components/Common/Modal/index.tsx new file mode 100644 index 00000000..949a666e --- /dev/null +++ b/src/components/Common/Modal/index.tsx @@ -0,0 +1,85 @@ +import { keyframes } from '@emotion/react'; +import styled from '@emotion/styled'; +import useModals from '@hooks/useModals'; +import { THEME } from '@styles/ThemeProvider/theme'; +import React from 'react'; + +import getModalSubElement from './domain/getModalSubElement'; +import ModalButton from './ModalButton'; +import ModalTitle from './ModalTitle'; + +type StaticPropsWithChidren = T & { children: React.ReactNode }; + +const Modal = ({ children }: StaticPropsWithChidren) => { + const { closeModal } = useModals(); + + const modalTitle = getModalSubElement(children, ModalTitle); + const modalButtons = getModalSubElement(children, ModalButton); + const hasModalButtons = modalButtons.length !== 0; + + const onBackgroundClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + closeModal(); + } + }; + + return ( + + + {modalTitle} + {hasModalButtons && ( + {modalButtons} + )} + + + ); +}; + +export default Modal; + +Modal.ModalTitle = ModalTitle; +Modal.ModalButton = ModalButton; + +const ModalBackground = styled.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + background-color: rgba(0, 0, 0, 0.6); + z-index: 9999; +`; + +const modalIn = keyframes` + from{ + opacity: 0; + transform: translateY(-30px); + } + to{ + opacity: 1; + transform: translateY(0); + } +`; + +const ModalContent = styled.div` + max-height: 70vh; + max-width: 480px; + width: 80%; + display: flex; + flex-direction: column; + padding: 0 30px 0 30px; + overflow: auto; + border-radius: 15px; + background-color: ${THEME.TEXT.WHITE}; + animation: ${modalIn} 0.3s ease-out; +`; + +const ModalButtonsContainer = styled.div` + padding: 0 0 20px 0; + display: flex; + justify-content: center; + gap: 0.5rem; +`; diff --git a/src/components/Toast/Toasts/index.tsx b/src/components/Common/Toast/Toasts/index.tsx similarity index 100% rename from src/components/Toast/Toasts/index.tsx rename to src/components/Common/Toast/Toasts/index.tsx diff --git a/src/components/Toast/index.tsx b/src/components/Common/Toast/index.tsx similarity index 100% rename from src/components/Toast/index.tsx rename to src/components/Common/Toast/index.tsx diff --git a/src/components/FAQBox/index.tsx b/src/components/FAQBox/index.tsx new file mode 100644 index 00000000..0678b66c --- /dev/null +++ b/src/components/FAQBox/index.tsx @@ -0,0 +1,103 @@ +import Icon from '@components/Common/Icon'; +import { FAQ_CONSTANTS } from '@constants/FAQ'; +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; +import { THEME } from '@styles/ThemeProvider/theme'; +import openLink from '@utils/router/openLink'; +import React, { useState } from 'react'; + +interface FAQBoxProps { + readonly question: string; + readonly answer: { + readonly text: string; + readonly link?: string; + }; +} + +const FAQBox = ({ question, answer }: FAQBoxProps) => { + const [showAnswer, setShowAnswer] = useState(false); + const toggleAnswer = () => setShowAnswer((prevState) => !prevState); + + const seperatedAnswerText = answer.text.split(FAQ_CONSTANTS.LINE_SEPERATOR); + const moveToLink = () => { + if (!answer.link) return; + openLink(answer.link); + }; + const hasAnswerLink = () => !!answer.link; + + return ( + <> + + {FAQ_CONSTANTS.QUESTION_MARK} + {question} + + + + + {showAnswer && ( + + {seperatedAnswerText.map((line, index) => ( +

{line}

+ ))} + {hasAnswerLink() && ( + {FAQ_CONSTANTS.LINK} + )} +
+ )} + + + ); +}; + +export default FAQBox; + +const QuestionContainer = styled.div<{ showAnswer: boolean }>` + position: relative; + padding: 10px 0px 10px 0px; + display: flex; + align-items: center; + + ${({ showAnswer }) => css` + & > span { + color: ${showAnswer && THEME.PRIMARY}; + } + & > div > svg { + transform: ${showAnswer ? 'rotate(-180deg)' : 'rotate(0deg)'}; + transition: all ease 0.3s; + } + `} +`; + +const QuestionMark = styled.span` + font-weight: bold; +`; + +const QuestionText = styled.span` + text-indent: 1rem; +`; + +const IconContainer = styled.div` + position: absolute; + right: 0; + display: flex; +`; + +const AnswerContainer = styled.div` + background-color: #7a9dd31a; + color: ${THEME.TEXT.BLACK}; + line-height: 1.8; + padding: 10px 20px 10px 20px; + border-radius: 10px; + margin-bottom: 10px; +`; + +const StyledLink = styled.span` + color: ${THEME.PRIMARY}; + border-bottom: 1px solid ${THEME.PRIMARY}; +`; + +const BoundaryLine = styled.hr` + height: 1px; + background-color: #ededed; + border: none; +`; diff --git a/src/components/FooterTab/index.tsx b/src/components/FooterTab/index.tsx index f25fc173..545fcd6d 100644 --- a/src/components/FooterTab/index.tsx +++ b/src/components/FooterTab/index.tsx @@ -1,9 +1,9 @@ -import Icon from '@components/Icon'; +import Icon from '@components/Common/Icon'; import styled from '@emotion/styled'; import useRouter from '@hooks/useRouter'; import { THEME } from '@styles/ThemeProvider/theme'; -const footerTabs = [ +const FOOTER_TABS = [ { kind: 'map', label: '지도', path: '/map' }, { kind: 'home', label: '홈', path: '/' }, { kind: 'accountCircle', label: '마이', path: '/my' }, @@ -16,7 +16,7 @@ const FooterTab = () => { return (