From 0dd654b7c2efa71a168ca472f7e77e952f06f148 Mon Sep 17 00:00:00 2001 From: Cho-heejung <66050038+he2e2@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:37:01 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[Feat]=20=EC=95=8C=EB=A6=BC=20UI=20?= =?UTF-8?q?=ED=8D=BC=EB=B8=94=EB=A6=AC=EC=8B=B1=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: header에 알림 표시 추가 * feat: NoticeItem, NoticeContainer * feat: 알림 창 열리는 작업 진행 중 * feat: Noti UI * design: SearchBar, NotificationContainer z-index 수정 * design: 모달 창 배경 블러 추가 * feat: 아카이브 순서 변경 기능 인덱스 기준 변경 --- src/features/notification/index.ts | 1 + .../notification/notification.type.ts | 38 +++++++++++ .../notification/ui/NoticeItem.module.scss | 59 +++++++++++++++++ src/features/notification/ui/NoticeItem.tsx | 29 +++++++++ src/features/notification/ui/index.ts | 1 + src/features/search/ui/SearchBar.module.scss | 7 +++ src/shared/ui/Modal/Modal.module.scss | 1 + .../Layout/ui/Header/Header.module.scss | 11 ++++ src/widgets/Layout/ui/Header/Header.tsx | 63 ++++++++++--------- src/widgets/MenuModal/MenuModal.tsx | 18 +++++- .../NoticeContainer.module.scss | 29 +++++++++ .../NoticeContainer/NoticeContainer.tsx | 16 +++++ src/widgets/SettingUser/SetArchive.tsx | 2 +- src/widgets/index.ts | 1 + 14 files changed, 243 insertions(+), 33 deletions(-) create mode 100644 src/features/notification/index.ts create mode 100644 src/features/notification/notification.type.ts create mode 100644 src/features/notification/ui/NoticeItem.module.scss create mode 100644 src/features/notification/ui/NoticeItem.tsx create mode 100644 src/features/notification/ui/index.ts create mode 100644 src/widgets/NoticeContainer/NoticeContainer.module.scss create mode 100644 src/widgets/NoticeContainer/NoticeContainer.tsx diff --git a/src/features/notification/index.ts b/src/features/notification/index.ts new file mode 100644 index 0000000..5ecdd1f --- /dev/null +++ b/src/features/notification/index.ts @@ -0,0 +1 @@ +export * from './ui'; diff --git a/src/features/notification/notification.type.ts b/src/features/notification/notification.type.ts new file mode 100644 index 0000000..3195613 --- /dev/null +++ b/src/features/notification/notification.type.ts @@ -0,0 +1,38 @@ +import type { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { faCoffee, faComment, faHeart, faUsers } from '@fortawesome/free-solid-svg-icons'; + +export type NotificationType = 'LIKE' | 'FEEDBACK' | 'GATHERING' | 'COFFEE_CHAT'; + +export interface NotificationItem { + icon: IconProp; + label: string; + title: string; + description: string; +} + +export const NotificationMap: { [key in NotificationType]: NotificationItem } = { + LIKE: { + icon: faHeart, + label: 'like', + title: '좋아요 알림', + description: '좋아요를 받았습니다.', + }, + FEEDBACK: { + icon: faComment, + label: 'feedback', + title: '피드백 알림', + description: '피드백을 요청 받았습니다.', + }, + GATHERING: { + icon: faUsers, + label: 'gathering', + title: '모임 알림', + description: '게더링 초대를 받았습니다.', + }, + COFFEE_CHAT: { + icon: faCoffee, + label: 'coffee', + title: '커피챗 알림', + description: '커피챗 요청을 받았습니다.', + }, +}; diff --git a/src/features/notification/ui/NoticeItem.module.scss b/src/features/notification/ui/NoticeItem.module.scss new file mode 100644 index 0000000..1a68cd3 --- /dev/null +++ b/src/features/notification/ui/NoticeItem.module.scss @@ -0,0 +1,59 @@ +.container { + display: flex; + flex: 1; + gap: 1.3rem; + align-items: center; + justify-content: space-between; + padding: 1rem 0; + border-bottom: 1px solid rgba(0, 0, 0, 10%); +} + +.icon { + display: flex; + align-items: center; + justify-content: center; + width: 1.6rem; + aspect-ratio: 1/1; + font-size: 0.75rem; + color: $secondary-color; + background-color: $primary-color; + border-radius: 50%; +} + +.description { + flex: 1; + font-size: 0.75rem; +} + +.buttons { + display: flex; + gap: 0.8rem; + + .button { + cursor: pointer; + } +} + +.check { + color: $green; +} + +.cancel { + color: rgba($red, 0.8); +} + +.like { + background-color: #ecbce7; +} + +.feedback { + background-color: #fef194; +} + +.gathering { + background-color: #bce7e7; +} + +.coffee { + background-color: #c9b693; +} diff --git a/src/features/notification/ui/NoticeItem.tsx b/src/features/notification/ui/NoticeItem.tsx new file mode 100644 index 0000000..ee59765 --- /dev/null +++ b/src/features/notification/ui/NoticeItem.tsx @@ -0,0 +1,29 @@ +import { faCircleCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import cn from 'classnames'; + +import styles from './NoticeItem.module.scss'; +import { NotificationMap, type NotificationType } from '../notification.type'; + +export const NoticeItem = ({ type }: { type: NotificationType }) => { + return ( +
+
+ +
+

{NotificationMap[type].description}

+
+ {}} + /> + {}} + /> +
+
+ ); +}; diff --git a/src/features/notification/ui/index.ts b/src/features/notification/ui/index.ts new file mode 100644 index 0000000..af16c19 --- /dev/null +++ b/src/features/notification/ui/index.ts @@ -0,0 +1 @@ +export * from './NoticeItem'; diff --git a/src/features/search/ui/SearchBar.module.scss b/src/features/search/ui/SearchBar.module.scss index 57b9987..40c8897 100644 --- a/src/features/search/ui/SearchBar.module.scss +++ b/src/features/search/ui/SearchBar.module.scss @@ -1,6 +1,7 @@ .container { position: absolute; right: 2rem; + z-index: 2000; display: flex; align-items: center; justify-content: space-between; @@ -19,6 +20,12 @@ padding: 0.5rem; color: $secondary-color; } + + @media (width <= 768px) { + right: 1rem; + min-width: 280px; + font-size: 0.75rem; + } } .visible { diff --git a/src/shared/ui/Modal/Modal.module.scss b/src/shared/ui/Modal/Modal.module.scss index 01682ff..2389ac1 100644 --- a/src/shared/ui/Modal/Modal.module.scss +++ b/src/shared/ui/Modal/Modal.module.scss @@ -15,6 +15,7 @@ height: 100%; cursor: default; background-color: rgba(0, 0, 0, 50%); + backdrop-filter: blur(5px); } .modalDialog { diff --git a/src/widgets/Layout/ui/Header/Header.module.scss b/src/widgets/Layout/ui/Header/Header.module.scss index 0cac377..6de34ad 100644 --- a/src/widgets/Layout/ui/Header/Header.module.scss +++ b/src/widgets/Layout/ui/Header/Header.module.scss @@ -95,6 +95,7 @@ } .button { + color: $primary-color; cursor: pointer; transition: 0.3s ease; @@ -119,6 +120,16 @@ position: absolute; top: $header-height; right: 0; + z-index: 2000; display: flex; width: 30%; } + +.notiWrapper { + position: absolute; + top: 0; + right: 0; + z-index: 2000; + display: flex; + min-width: 320px; +} diff --git a/src/widgets/Layout/ui/Header/Header.tsx b/src/widgets/Layout/ui/Header/Header.tsx index 81e1689..30d5381 100644 --- a/src/widgets/Layout/ui/Header/Header.tsx +++ b/src/widgets/Layout/ui/Header/Header.tsx @@ -1,7 +1,7 @@ -import { faBars, faHeart, faSearch } from '@fortawesome/free-solid-svg-icons'; +import { faBars, faBell, faHeart, faSearch } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import cn from 'classnames'; -import React, { useRef } from 'react'; +import React from 'react'; import { useState, useEffect } from 'react'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import { useShallow } from 'zustand/shallow'; @@ -17,6 +17,7 @@ import Logo from '@/shared/assets/paletteLogo.svg?react'; import { useModalStore } from '@/shared/model/modalStore'; import { Button, customConfirm } from '@/shared/ui'; import { MenuModal } from '@/widgets/MenuModal/MenuModal'; +import { NoticeContainer } from '@/widgets/NoticeContainer/NoticeContainer'; export const Header = () => { const { pathname } = useLocation(); @@ -42,6 +43,7 @@ export const Header = () => { }); }; const [isSearch, setIsSearch] = useState(false); + const [isNotice, setIsNotice] = useState(false); useEffect(() => { const handleResize = () => { @@ -55,25 +57,14 @@ export const Header = () => { }; }, []); - const searchRef = useRef(null); - useEffect(() => { - const handler = (event: MouseEvent) => { - if (searchRef.current !== null && !searchRef.current.contains(event.target as Node)) { - setTimeout(() => { - setIsSearch(false); - }, 100); - } - }; - document.addEventListener('mousedown', handler); - - return () => { - document.removeEventListener('mousedown', handler); - }; - }, []); + const toggleSearch = () => { + setIsSearch(!isSearch); + if (isNotice) setIsNotice(false); + }; - const toggleSearch = (event: React.MouseEvent) => { - event.stopPropagation(); - if (!isSearch) setIsSearch(true); + const toggleNoti = () => { + setIsNotice(!isNotice); + if (isSearch) setIsSearch(false); }; return ( @@ -93,6 +84,11 @@ export const Header = () => { icon={faSearch} onClick={toggleSearch} /> + { @@ -102,10 +98,7 @@ export const Header = () => { /> {isSearch && ( -
+
)} @@ -140,11 +133,23 @@ export const Header = () => { icon={faSearch} onClick={toggleSearch} /> + { - navigate('/like'); + if (userData) navigate('/like'); + else { + customConfirm({ + text: '로그인이 필요합니다.', + title: '로그인', + icon: 'info', + }).catch(console.error); + } }} /> {userData ? ( @@ -166,15 +171,15 @@ export const Header = () => { )}
{' '} {isSearch && ( -
+
)} )} +
+ +
); }; diff --git a/src/widgets/MenuModal/MenuModal.tsx b/src/widgets/MenuModal/MenuModal.tsx index f668133..3d31fec 100644 --- a/src/widgets/MenuModal/MenuModal.tsx +++ b/src/widgets/MenuModal/MenuModal.tsx @@ -10,7 +10,7 @@ import styles from './MenuModal.module.scss'; import { NAV_LINKS } from '../Layout/constants'; import { useModalStore } from '@/shared/model/modalStore'; -import { Button } from '@/shared/ui'; +import { Button, customConfirm } from '@/shared/ui'; export const MenuModal = ({ isOpen, @@ -78,8 +78,20 @@ export const MenuModal = ({ className={cn(styles.button, styles.heart)} icon={faHeart} onClick={() => { - navigate('/like'); - onClose(false); + if (isUserData) { + navigate('/like'); + onClose(false); + } else { + customConfirm({ + text: '로그인이 필요합니다.', + title: '로그인', + icon: 'info', + }) + .then(() => { + onClose(false); + }) + .catch(console.error); + } }} /> {isUserData ? ( diff --git a/src/widgets/NoticeContainer/NoticeContainer.module.scss b/src/widgets/NoticeContainer/NoticeContainer.module.scss new file mode 100644 index 0000000..b9364db --- /dev/null +++ b/src/widgets/NoticeContainer/NoticeContainer.module.scss @@ -0,0 +1,29 @@ +.container { + position: absolute; + top: $header-height; + right: -340px; + display: flex; + flex-direction: column; + width: 100%; + height: 12rem; + padding: 1rem 2rem; + overflow-x: scroll; + background-color: $secondary-color; + border-radius: 0 0 0 20px; + box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 10%); + transition: 0.3s ease; + + &::-webkit-scrollbar { + width: 5px; + height: 10px; + } + + &::-webkit-scrollbar-thumb { + background: #ededed; + border-radius: 12px; + } +} + +.visible { + right: 0; +} diff --git a/src/widgets/NoticeContainer/NoticeContainer.tsx b/src/widgets/NoticeContainer/NoticeContainer.tsx new file mode 100644 index 0000000..ed4a3bc --- /dev/null +++ b/src/widgets/NoticeContainer/NoticeContainer.tsx @@ -0,0 +1,16 @@ +import cn from 'classnames'; + +import styles from './NoticeContainer.module.scss'; + +import { NoticeItem } from '@/features/notification'; + +export const NoticeContainer = ({ isNotice }: { isNotice: boolean }) => { + return ( +
+ + + + +
+ ); +}; diff --git a/src/widgets/SettingUser/SetArchive.tsx b/src/widgets/SettingUser/SetArchive.tsx index 982afa5..b42e21d 100644 --- a/src/widgets/SettingUser/SetArchive.tsx +++ b/src/widgets/SettingUser/SetArchive.tsx @@ -50,7 +50,7 @@ export const SetArchive = () => { const handleUpdateOrder = () => { const orderRequest: Record = archives.reduce( (acc: Record, archive, index) => { - acc[archive.archiveId] = index + 1; + acc[archive.archiveId] = archives.length - index; return acc; }, {}, diff --git a/src/widgets/index.ts b/src/widgets/index.ts index 7913f19..a5e000a 100644 --- a/src/widgets/index.ts +++ b/src/widgets/index.ts @@ -15,3 +15,4 @@ export * from './WriteGathering/WriteGatheringDetail'; export * from './WriteGathering/WriteGatheringOpts'; export * from './SettingUser'; export * from './MenuModal/MenuModal'; +export * from './NoticeContainer/NoticeContainer'; From 046e1eb40c2df249cb192d019b7240a8bbabc65a Mon Sep 17 00:00:00 2001 From: Cho-heejung <66050038+he2e2@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:40:12 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[Feat]=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=8D=BC=EB=B8=94=EB=A6=AC=EC=8B=B1=20(#9?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Banner * feat: auto slider 반영 * feat: 메인 페이지 레이아웃 * feat: Grid에 hover 시, 제목 올라오도록 변경 * feat: navigate 추가 --- package.json | 3 + src/app/appRouter.tsx | 3 +- src/pages/MainPage/MainPage.module.scss | 4 + src/pages/MainPage/MainPage.tsx | 12 ++ src/pages/index.ts | 1 + src/shared/assets/banner.svg | 3 + src/shared/assets/banner/blue.svg | 30 ++++ src/shared/assets/banner/green.svg | 30 ++++ src/shared/assets/banner/purple.svg | 30 ++++ src/shared/assets/banner/red.svg | 30 ++++ src/shared/assets/banner/yellow.svg | 30 ++++ src/widgets/MainBanner/MainBanner.module.scss | 15 ++ src/widgets/MainBanner/MainBanner.tsx | 35 +++++ .../MainContents/MainContents.module.scss | 12 ++ src/widgets/MainContents/MainContents.tsx | 12 ++ .../MainGridItem/MainGridItem.module.scss | 47 +++++++ src/widgets/MainGridItem/MainGridItem.tsx | 128 ++++++++++++++++++ src/widgets/index.ts | 2 + yarn.lock | 47 ++++++- 19 files changed, 472 insertions(+), 2 deletions(-) create mode 100644 src/pages/MainPage/MainPage.module.scss create mode 100644 src/pages/MainPage/MainPage.tsx create mode 100644 src/shared/assets/banner.svg create mode 100644 src/shared/assets/banner/blue.svg create mode 100644 src/shared/assets/banner/green.svg create mode 100644 src/shared/assets/banner/purple.svg create mode 100644 src/shared/assets/banner/red.svg create mode 100644 src/shared/assets/banner/yellow.svg create mode 100644 src/widgets/MainBanner/MainBanner.module.scss create mode 100644 src/widgets/MainBanner/MainBanner.tsx create mode 100644 src/widgets/MainContents/MainContents.module.scss create mode 100644 src/widgets/MainContents/MainContents.tsx create mode 100644 src/widgets/MainGridItem/MainGridItem.module.scss create mode 100644 src/widgets/MainGridItem/MainGridItem.tsx diff --git a/package.json b/package.json index 71a5373..22ac697 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,9 @@ "react-hook-form": "^7.53.2", "react-router-dom": "^6.28.0", "react-select": "^5.8.3", + "react-slick": "^0.30.2", "sass": "^1.81.0", + "slick-carousel": "^1.8.1", "sweetalert2": "^11.14.5", "yup": "^1.4.0", "zustand": "^5.0.1" @@ -74,6 +76,7 @@ "@types/lodash-es": "^4.17.12", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.1", + "@types/react-slick": "^0.23.13", "@typescript-eslint/eslint-plugin": "^8.12.2", "@typescript-eslint/parser": "^8.12.2", "@vitejs/plugin-react": "^4.3.3", diff --git a/src/app/appRouter.tsx b/src/app/appRouter.tsx index 8dd28a1..4a10fa3 100644 --- a/src/app/appRouter.tsx +++ b/src/app/appRouter.tsx @@ -14,6 +14,7 @@ import { WriteArchivePage, WriteGatheringPage, LikeListPage, + MainPage, } from '@/pages'; import { Layout } from '@/widgets'; @@ -24,7 +25,7 @@ const AppRouter = () => { children: [ { path: '/', - element: <>{/** mainPage */}, + element: , }, { path: '/portfolio', diff --git a/src/pages/MainPage/MainPage.module.scss b/src/pages/MainPage/MainPage.module.scss new file mode 100644 index 0000000..a386e82 --- /dev/null +++ b/src/pages/MainPage/MainPage.module.scss @@ -0,0 +1,4 @@ +.container { + display: flex; + flex-direction: column; +} diff --git a/src/pages/MainPage/MainPage.tsx b/src/pages/MainPage/MainPage.tsx new file mode 100644 index 0000000..5ad4d32 --- /dev/null +++ b/src/pages/MainPage/MainPage.tsx @@ -0,0 +1,12 @@ +import styles from './MainPage.module.scss'; + +import { MainBanner, MainContents } from '@/widgets'; + +export const MainPage = () => { + return ( +
+ + +
+ ); +}; diff --git a/src/pages/index.ts b/src/pages/index.ts index 9e9a061..f7688c8 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -10,3 +10,4 @@ export { WriteArchivePage } from './WriteArchivePage/WriteArchivePage'; export { WriteGatheringPage } from './WriteGatheringPage/WriteGatheringPage'; export { MyPage } from './MyPage/MyPage'; export { LikeListPage } from './LikeListPage/LikeListPage'; +export { MainPage } from './MainPage/MainPage'; diff --git a/src/shared/assets/banner.svg b/src/shared/assets/banner.svg new file mode 100644 index 0000000..24e2f0c --- /dev/null +++ b/src/shared/assets/banner.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/shared/assets/banner/blue.svg b/src/shared/assets/banner/blue.svg new file mode 100644 index 0000000..3c798f1 --- /dev/null +++ b/src/shared/assets/banner/blue.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/shared/assets/banner/green.svg b/src/shared/assets/banner/green.svg new file mode 100644 index 0000000..58d2132 --- /dev/null +++ b/src/shared/assets/banner/green.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/shared/assets/banner/purple.svg b/src/shared/assets/banner/purple.svg new file mode 100644 index 0000000..0834a3f --- /dev/null +++ b/src/shared/assets/banner/purple.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/shared/assets/banner/red.svg b/src/shared/assets/banner/red.svg new file mode 100644 index 0000000..d44d719 --- /dev/null +++ b/src/shared/assets/banner/red.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/shared/assets/banner/yellow.svg b/src/shared/assets/banner/yellow.svg new file mode 100644 index 0000000..f8f846f --- /dev/null +++ b/src/shared/assets/banner/yellow.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/widgets/MainBanner/MainBanner.module.scss b/src/widgets/MainBanner/MainBanner.module.scss new file mode 100644 index 0000000..cb0375d --- /dev/null +++ b/src/widgets/MainBanner/MainBanner.module.scss @@ -0,0 +1,15 @@ +.container { + display: flex; + width: 100%; +} + +.sliderWrapper { + display: flex; + width: 100%; + height: auto; +} + +.banner { + width: 100%; + height: auto; +} diff --git a/src/widgets/MainBanner/MainBanner.tsx b/src/widgets/MainBanner/MainBanner.tsx new file mode 100644 index 0000000..3e3b1c8 --- /dev/null +++ b/src/widgets/MainBanner/MainBanner.tsx @@ -0,0 +1,35 @@ +import Slider from 'react-slick'; + +import styles from './MainBanner.module.scss'; + +import 'slick-carousel/slick/slick.css'; +import 'slick-carousel/slick/slick-theme.css'; + +import Blue from '@/shared/assets/banner/blue.svg?react'; +import Green from '@/shared/assets/banner/green.svg?react'; +import Purple from '@/shared/assets/banner/purple.svg?react'; +import Red from '@/shared/assets/banner/red.svg?react'; +import Yellow from '@/shared/assets/banner/yellow.svg?react'; + +export const MainBanner = () => { + const settings = { + infinite: true, + slidesToShow: 1, + slidesToScroll: 1, + autoplay: true, + autoplaySpeed: 5000, + cssEase: 'linear', + arrows: false, + }; + return ( +
+ + + + + + + +
+ ); +}; diff --git a/src/widgets/MainContents/MainContents.module.scss b/src/widgets/MainContents/MainContents.module.scss new file mode 100644 index 0000000..0c54934 --- /dev/null +++ b/src/widgets/MainContents/MainContents.module.scss @@ -0,0 +1,12 @@ +.container { + display: flex; + flex-direction: column; + gap: 4rem; + width: 100%; + padding: 4rem; + margin-bottom: 2rem; + + @media (width <= 768px) { + padding: 2rem; + } +} diff --git a/src/widgets/MainContents/MainContents.tsx b/src/widgets/MainContents/MainContents.tsx new file mode 100644 index 0000000..56f5c6d --- /dev/null +++ b/src/widgets/MainContents/MainContents.tsx @@ -0,0 +1,12 @@ +import styles from './MainContents.module.scss'; +import { MainGridItem } from '../MainGridItem/MainGridItem'; + +export const MainContents = () => { + return ( +
+ + + +
+ ); +}; diff --git a/src/widgets/MainGridItem/MainGridItem.module.scss b/src/widgets/MainGridItem/MainGridItem.module.scss new file mode 100644 index 0000000..7682075 --- /dev/null +++ b/src/widgets/MainGridItem/MainGridItem.module.scss @@ -0,0 +1,47 @@ +.container { + display: flex; + flex-direction: column; + gap: 2rem; + align-items: center; + width: 100%; + padding: 2rem 0; +} + +.title { + display: flex; + gap: 1rem; + align-items: center; + font-size: 1.5rem; + font-weight: 500; + color: $deep-black; + cursor: pointer; + visibility: hidden; + opacity: 0; + transition: 0.3s ease; +} + +.gridWrapper { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + + &:hover { + .title { + visibility: visible; + opacity: 1; + transform: translateY(-1rem); + } + } +} + +.grid { + display: grid; + grid-template-rows: 1fr; + grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr)); + grid-auto-rows: minmax(20rem, auto); + gap: 1rem; + width: 100rem; + overflow: hidden; +} diff --git a/src/widgets/MainGridItem/MainGridItem.tsx b/src/widgets/MainGridItem/MainGridItem.tsx new file mode 100644 index 0000000..a5ee9f3 --- /dev/null +++ b/src/widgets/MainGridItem/MainGridItem.tsx @@ -0,0 +1,128 @@ +import { faChevronRight } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useNavigate } from 'react-router-dom'; + +import styles from './MainGridItem.module.scss'; + +import type { ArchiveCardDTO } from '@/features'; +import { ArchiveCard, PortfolioCard } from '@/features'; +import { GatheringCard } from '@/shared/ui'; + +const dummyArchive: ArchiveCardDTO = { + archiveId: 1, + title: 'string', + introduction: 'string', + type: 'RED', + likeCount: 2, + username: 'string', + imageUrl: 'string', + createDate: 'string', + isLiked: true, +}; + +export const MainGridItem = ({ type }: { type: string }) => { + const navigate = useNavigate(); + if (type === 'portfolio') { + return ( +
+
+

{ + navigate('/portfolio'); + }} + > + 인기 포트폴리오 유저를 소개합니다 + +

+
+ + + + +
+
+
+ ); + } + + if (type === 'archive') { + return ( +
+
+

{ + navigate('/archive'); + }} + > + 인기 아카이빙을 소개합니다 + +

+
+ + + + +
+
+
+ ); + } + + if (type === 'gathering') { + return ( +
+
+

{ + navigate('/gathering'); + }} + > + 현재 모집 중인 게더링 + +

+
+ + + + +
+
+
+ ); + } +}; diff --git a/src/widgets/index.ts b/src/widgets/index.ts index a5e000a..dbfb467 100644 --- a/src/widgets/index.ts +++ b/src/widgets/index.ts @@ -15,4 +15,6 @@ export * from './WriteGathering/WriteGatheringDetail'; export * from './WriteGathering/WriteGatheringOpts'; export * from './SettingUser'; export * from './MenuModal/MenuModal'; +export * from './MainBanner/MainBanner'; +export * from './MainContents/MainContents'; export * from './NoticeContainer/NoticeContainer'; diff --git a/yarn.lock b/yarn.lock index a59d294..1089658 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3332,6 +3332,13 @@ dependencies: "@types/react" "*" +"@types/react-slick@^0.23.13": + version "0.23.13" + resolved "https://registry.yarnpkg.com/@types/react-slick/-/react-slick-0.23.13.tgz#037434e73a58063047b121e08565f7185d811f36" + integrity sha512-bNZfDhe/L8t5OQzIyhrRhBr/61pfBcWaYJoq6UDqFtv5LMwfg4NsVDD2J8N01JqdAdxLjOt66OZEp6PX+dGs/A== + dependencies: + "@types/react" "*" + "@types/react-transition-group@^4.4.0": version "4.4.11" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.11.tgz#d963253a611d757de01ebb241143b1017d5d63d5" @@ -4301,7 +4308,7 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz#707413784dbb3a72aa11c2f2b042a0bef4004170" integrity sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA== -classnames@*, classnames@^2.5.1: +classnames@*, classnames@^2.2.5, classnames@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== @@ -5036,6 +5043,11 @@ enhanced-resolve@^5.15.0: graceful-fs "^4.2.4" tapable "^2.2.0" +enquire.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/enquire.js/-/enquire.js-2.1.6.tgz#3e8780c9b8b835084c3f60e166dbc3c2a3c89814" + integrity sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw== + enquirer@^2.3.6: version "2.4.1" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" @@ -7190,6 +7202,13 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json2mq@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/json2mq/-/json2mq-0.2.0.tgz#b637bd3ba9eabe122c83e9720483aeb10d2c904a" + integrity sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA== + dependencies: + string-convert "^0.2.0" + json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" @@ -8491,6 +8510,17 @@ react-select@^5.8.3: react-transition-group "^4.3.0" use-isomorphic-layout-effect "^1.1.2" +react-slick@^0.30.2: + version "0.30.2" + resolved "https://registry.yarnpkg.com/react-slick/-/react-slick-0.30.2.tgz#b28e992f9c519bb516a0af8d37e82cb59fee08ce" + integrity sha512-XvQJi7mRHuiU3b9irsqS9SGIgftIfdV5/tNcURTb5LdIokRA5kIIx3l4rlq2XYHfxcSntXapoRg/GxaVOM1yfg== + dependencies: + classnames "^2.2.5" + enquire.js "^2.1.6" + json2mq "^0.2.0" + lodash.debounce "^4.0.8" + resize-observer-polyfill "^1.5.0" + react-transition-group@^4.3.0: version "4.4.5" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" @@ -8623,6 +8653,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== +resize-observer-polyfill@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -8956,6 +8991,11 @@ slice-ansi@^7.1.0: ansi-styles "^6.2.1" is-fullwidth-code-point "^5.0.0" +slick-carousel@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/slick-carousel/-/slick-carousel-1.8.1.tgz#a4bfb29014887bb66ce528b90bd0cda262cc8f8d" + integrity sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA== + smart-buffer@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" @@ -9068,6 +9108,11 @@ string-argv@~0.3.2: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== +string-convert@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97" + integrity sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A== + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" From b1a10159e40b1063d17f8e9f8f1c2d51e7e5f143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B1=84=EC=8A=B9=EA=B7=9C?= <37896060+csk6314@users.noreply.github.com> Date: Fri, 6 Dec 2024 16:21:03 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[Feat]=20=EC=9C=A0=EC=A0=80=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EB=93=B1=EB=A1=9D=20&=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EA=B4=80=EB=A0=A8=20API=20=EC=97=B0=EB=8F=99=20(#9?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * design: Input 컴포넌트 disabled 스타일 추가 * feat: 첫 로그인 유저 등록 이벤트 처리 * feat: form type 정리 및 disabled 옵션 추가 * style: lint style 적용 * feat: useAuthPage 로그인 필요한 페이지 검증 훅 * feat: 유저 프로필 수정 폼에 데이터 불러오기 * feat: 로그아웃 시 navigate 후 알림 생성 * feat: useAuthPage denied function 추가 * feat: user store loading 추가 * feat: role별 접근 제한 훅 useRoleGuard 구현 * feat: image 업로드 관련 api, hook, type * feat: ImageInput react-hook-form 연결 * style: user, portfolio 타입 수정 * feat: portfolio create 훅 구현 * feat: 회원 등록 flow 구현 * refactor: AuthProvider 유저 등록 알림 기능 분리 * design: URL 필드 placeholder 미적용 및 width 수정 * feat: 유저 등록 알림 useRegistAlarm 커스텀 훅 * chore: 기타 변경 사항 * feat: useAuthPage 삭제, useRoleGuard로 통합 * chore: console 삭제 * feat: 유저 수정 flow 구현 * feat: useRegistAlarm 제외 페이지 추가 * feat: token 400 오류 예외 처리 --- src/app/AuthProvider.tsx | 7 +- src/app/styles/variables.scss | 1 + src/features/auth/form.hook.ts | 20 +++- src/features/auth/form.types.ts | 22 +++- src/features/auth/form.utils.ts | 98 ++++++++++++++-- src/features/auth/ui/FormField.module.scss | 3 +- src/features/auth/ui/FormField.tsx | 8 +- src/features/auth/ui/FormInputs.tsx | 44 +++---- src/features/auth/ui/ProfileForm.tsx | 9 +- src/features/portfolio/portfolio.dto.ts | 2 +- src/features/portfolio/portfolio.hook.ts | 10 ++ src/features/user/model/user.store.ts | 13 ++- src/features/user/user.dto.ts | 13 ++- src/pages/MyPage/MyPage.tsx | 12 ++ src/pages/RegisterPage/RegisterPage.tsx | 9 ++ src/shared/api/baseApi.ts | 6 + src/shared/hook/useRegistAlarm.ts | 29 +++++ src/shared/hook/useRoleGuard.ts | 48 ++++++++ src/shared/ui/Input/Input.module.scss | 4 + src/shared/ui/Input/Input.tsx | 2 +- src/widgets/Layout/ui/Header/Header.tsx | 2 + src/widgets/RegisterUser/PortfolioStep.tsx | 46 ++++++-- src/widgets/RegisterUser/ProfileStep.tsx | 91 +++++++++++++-- src/widgets/SettingUser/ContentLayout.tsx | 30 ++++- src/widgets/SettingUser/SetProfile.tsx | 126 +++++++++++++++++++-- 25 files changed, 572 insertions(+), 83 deletions(-) create mode 100644 src/features/portfolio/portfolio.hook.ts create mode 100644 src/shared/hook/useRegistAlarm.ts create mode 100644 src/shared/hook/useRoleGuard.ts diff --git a/src/app/AuthProvider.tsx b/src/app/AuthProvider.tsx index eaf8ec2..cfc9285 100644 --- a/src/app/AuthProvider.tsx +++ b/src/app/AuthProvider.tsx @@ -6,12 +6,14 @@ import { getLocalAccessToken, removeLocalAccessToken } from '@/features/auth/aut import { useUserStore } from '@/features/user/model/user.store'; import { getMyProfile } from '@/features/user/user.api'; import { setInterceptorEvents } from '@/shared/api/baseApi'; +import { useRegistAlarm } from '@/shared/hook/useRegistAlarm'; import { customConfirm } from '@/shared/ui'; const AuthProvider = ({ children }: { children: ReactNode }) => { const navigate = useNavigate(); const accessToken = getLocalAccessToken(); - const { setUserData, clearUserData } = useUserStore(state => state.actions); + const { setUserData, clearUserData, done } = useUserStore(state => state.actions); + useRegistAlarm(); useEffect(() => { const getUserData = async () => { @@ -20,9 +22,12 @@ const AuthProvider = ({ children }: { children: ReactNode }) => { const userData = await getMyProfile().then(res => res.data); if (!userData) throw new Error('유저 정보를 찾을 수가 없습니다.'); setUserData(userData); + + done(); return userData; } clearUserData(); + done(); } catch (error) { console.error('유저 데이터를 불러오는 중 오류가 발생했습니다.', error); } diff --git a/src/app/styles/variables.scss b/src/app/styles/variables.scss index f048fa6..31c68c1 100644 --- a/src/app/styles/variables.scss +++ b/src/app/styles/variables.scss @@ -20,6 +20,7 @@ $header-horizontal-padding: 4rem; $form-gray-color: #d7d7d7; $form-section-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 25%); $form-input-color: #999a99; +$form-input-disabled: #f0f0f0; // 그라데이션 $palette-gradient: linear-gradient(151deg, $red 0%, $yellow 32%, $green 71%, $blue 100%); diff --git a/src/features/auth/form.hook.ts b/src/features/auth/form.hook.ts index 442ea30..0c2f08b 100644 --- a/src/features/auth/form.hook.ts +++ b/src/features/auth/form.hook.ts @@ -11,11 +11,13 @@ interface useProfileFormProps { export const useProfileForm = ({ formConfig }: useProfileFormProps) => { const [formStructure, setFormStructure] = useState([...formConfig.structure]); + const [isResetting, setIsResetting] = useState(false); const method = useForm({ resolver: yupResolver(formConfig.validation), mode: 'onChange', defaultValues: formConfig.defaultValues, }); + const majorJobGroup = method.watch('majorJobGroup'); /** @@ -26,6 +28,11 @@ export const useProfileForm = ({ formConfig }: useProfileF useEffect(() => { if (!majorJobGroup) return; + if (isResetting) { + setIsResetting(false); + return; + } + setFormStructure(prev => { const updatedStructure = [...prev]; @@ -41,7 +48,6 @@ export const useProfileForm = ({ formConfig }: useProfileF break; } } - return updatedStructure; }); @@ -50,9 +56,15 @@ export const useProfileForm = ({ formConfig }: useProfileF }); }, [majorJobGroup, method]); + const handleReset = (data: Partial) => { + setIsResetting(true); + method.reset(data); + }; + return { formStructure, method, + handleReset, }; }; @@ -71,7 +83,11 @@ export const usePortfolioInput = () => { return false; } - if (!(value.startsWith('https://') || value.startsWith('http://'))) { + if ( + !value.match( + /^((ftp|http|https):\/\/)?(www.)?(?!.*(ftp|http|https|www.))[a-zA-Z0-9_-]+(\.[a-zA-Z]+)+((\/)[\w#]+)*(\/\w+\?[a-zA-Z0-9_]+=\w+(&[a-zA-Z0-9_]+=\w+)*)?$/gm, + ) + ) { setError('URL 형식이 잘못됬습니다.'); return false; } diff --git a/src/features/auth/form.types.ts b/src/features/auth/form.types.ts index ea14e60..bd3958a 100644 --- a/src/features/auth/form.types.ts +++ b/src/features/auth/form.types.ts @@ -6,6 +6,11 @@ export type Option = { label: string; }; +export type ImageField = { + url: string; + file: File | null; +}; + export interface FormValues { name: string; briefIntro: string; @@ -14,11 +19,12 @@ export interface FormValues { jobTitle: string; division: string; url: { value: string }[]; - imageUrl: string; + imageUrl: ImageField; } export interface PortfolioFormValues extends FormValues { - portfolioUrl: string; + portfolioLink: string; + email: string; } export type FormInputType = 'default' | 'radio' | 'select' | 'image' | 'textarea'; @@ -26,12 +32,16 @@ export type FormInputType = 'default' | 'radio' | 'select' | 'image' | 'textarea export type FormValuesName = keyof FormValues; export type PortfolioFormValuesName = keyof PortfolioFormValues; -export interface InputFieldProps { +export interface CommonInputAttribute { + maxLength?: number; + disabled?: boolean; + placeholder?: string; +} + +export interface InputFieldProps extends CommonInputAttribute { name: FormValuesName | PortfolioFormValuesName; type?: FormInputType; - placeholder?: string; options?: Option[]; - maxLength?: number; } interface InputInfo extends InputFieldProps { @@ -129,6 +139,8 @@ export const JOB_CATEGORIES = [ }, ]; +export const JOB_SUB_CATEGORY = JOB_CATEGORIES.map(major => major.children).flat(); + export const JOB_DIVISION: Option[] = [ { value: 'student', label: '학생' }, { value: 'worker', label: '회사' }, diff --git a/src/features/auth/form.utils.ts b/src/features/auth/form.utils.ts index 9d824b2..bcf3a37 100644 --- a/src/features/auth/form.utils.ts +++ b/src/features/auth/form.utils.ts @@ -1,11 +1,15 @@ import * as yup from 'yup'; -import type { FormConfigType, FormValues, PortfolioFormValues } from './form.types'; +import type { FormConfigType, FormValues, ImageField, PortfolioFormValues } from './form.types'; import { JOB_CATEGORIES, JOB_DIVISION } from './form.types'; +import { postImages } from '../image/image.api'; export const formValidation = yup.object({ name: yup.string().required('이름을 입력해주세요.'), - briefIntro: yup.string().defined().max(100, '100글자 이하로 소개 글을 작성해주세요.'), + briefIntro: yup + .string() + .required('자기소개를 입력해주세요.') + .max(100, '100글자 이하로 소개 글을 작성해주세요.'), majorJobGroup: yup .object() .shape({ @@ -29,14 +33,22 @@ export const formValidation = yup.object({ yup.object().shape({ value: yup .string() - .defined() .required('URL을 입력해주세요.') - .url('URL 형식에 맞게 입력해주세요.'), + .matches( + /^((ftp|http|https):\/\/)?(www.)?(?!.*(ftp|http|https|www.))[a-zA-Z0-9_-]+(\.[a-zA-Z]+)+((\/)[\w#]+)*(\/\w+\?[a-zA-Z0-9_]+=\w+(&[a-zA-Z0-9_]+=\w+)*)?$/gm, + 'URL 형식에 맞게 입력해주세요.', + ), }), ) .max(5, 'URL은 최대 5개 까지 작성 가능합니다.') .defined(), - imageUrl: yup.string().defined(), //.required('프로필 이미지를 등록해주세요.'), + imageUrl: yup + .object() + .shape({ + url: yup.string().defined(), + file: yup.mixed().nullable(), + }) + .defined(), //.required('프로필 이미지를 등록해주세요.'), }); export const formConfig: FormConfigType = { @@ -118,17 +130,57 @@ export const formConfig: FormConfigType = { jobTitle: '', division: 'student', url: [], - imageUrl: '', + imageUrl: { + url: '', + file: null, + }, }, }; export const profileFormValidation = formValidation.shape({ - portfolioUrl: yup.string().defined().url('URL 형식이 아닙니다.'), + email: yup.string().defined(), + portfolioLink: yup + .string() + .defined() + .matches( + /^\s*$|^((ftp|http|https):\/\/)?(www.)?(?!.*(ftp|http|https|www.))[a-zA-Z0-9_-]+(\.[a-zA-Z]+)+((\/)[\w#]+)*(\/\w+\?[a-zA-Z0-9_]+=\w+(&[a-zA-Z0-9_]+=\w+)*)?$/gm, + 'URL 형식에 맞게 입력해주세요.', + ), }); export const profileFormConfig: FormConfigType = { structure: [ - ...formConfig.structure.slice(0, 2), + { + title: '기본 정보', + inputs: [ + { + label: '프로필 사진', + type: 'image', + name: 'imageUrl', + }, + { + label: '이메일', + type: 'default', + name: 'email', + disabled: true, + }, + { + label: '이름', + type: 'default', + name: 'name', + required: true, + placeholder: '이름을 입력해주세요.', + }, + { + label: '한 줄 소개', + type: 'textarea', + name: 'briefIntro', + maxLength: 100, + placeholder: '한 줄 소개를 입력해주세요.', + }, + ], + }, + ...formConfig.structure.slice(1, 2), { title: 'URL', inputs: [ @@ -140,7 +192,7 @@ export const profileFormConfig: FormConfigType = { { label: '포트폴리오 URL', type: 'default', - name: 'portfolioUrl', + name: 'portfolioLink', placeholder: 'https://', }, ], @@ -155,7 +207,31 @@ export const profileFormConfig: FormConfigType = { jobTitle: '', division: 'student', url: [], - imageUrl: '', - portfolioUrl: '', + imageUrl: { + url: '', + file: null, + }, + portfolioLink: '', + email: 'csk9908@naver.com', }, }; + +export const handleImageUpload = async (imageUrl: ImageField) => { + let profileImageUrl = imageUrl.url; + + //이미지 업로드 처리 + if (imageUrl.file) { + try { + const imageData = new FormData(); + imageData.append('files', imageUrl.file); + const image = await postImages(imageData).then(res => res.data); + if (image && image.imgUrls[0]) { + profileImageUrl = image.imgUrls[0].imgUrl; + } + } catch { + console.error('Failed to upload image'); + } + + return profileImageUrl; + } +}; diff --git a/src/features/auth/ui/FormField.module.scss b/src/features/auth/ui/FormField.module.scss index c05b97b..6c872d4 100644 --- a/src/features/auth/ui/FormField.module.scss +++ b/src/features/auth/ui/FormField.module.scss @@ -18,7 +18,7 @@ } & > div { - width: 100%; + width: 80%; } & > svg { @@ -34,7 +34,6 @@ row-gap: 0.5rem; align-items: flex-end; width: 100%; - max-width: 426px; & .iconBtn { font-size: 1.5rem; diff --git a/src/features/auth/ui/FormField.tsx b/src/features/auth/ui/FormField.tsx index f6b9fc5..52af542 100644 --- a/src/features/auth/ui/FormField.tsx +++ b/src/features/auth/ui/FormField.tsx @@ -99,15 +99,15 @@ export const UrlInputField: React.FC = ({ name, ...restPro }; export const FormField: React.FC = React.memo( - ({ label, required, ...restProps }) => { + ({ label, required, name, ...restProps }) => { return (
{label} - {restProps.name === 'url' ? ( - + {name === 'url' ? ( + ) : ( - + )} {required && }
diff --git a/src/features/auth/ui/FormInputs.tsx b/src/features/auth/ui/FormInputs.tsx index 744f33a..c971e75 100644 --- a/src/features/auth/ui/FormInputs.tsx +++ b/src/features/auth/ui/FormInputs.tsx @@ -1,7 +1,7 @@ //library import cn from 'classnames'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import type { ControllerRenderProps } from 'react-hook-form'; import Select from 'react-select'; import type { ActionMeta, MultiValue, SingleValue } from 'react-select'; @@ -9,17 +9,15 @@ import type { ActionMeta, MultiValue, SingleValue } from 'react-select'; //styles import styles from './FormInputs.module.scss'; //types -import type { InputFieldProps, Option } from '../form.types'; +import type { CommonInputAttribute, ImageField, InputFieldProps, Option } from '../form.types'; //components import { Input, Radio, TextArea } from '@/shared/ui'; -interface InputProps { +interface InputProps extends CommonInputAttribute { value: T; - onChange: (e: React.ChangeEvent) => void; + onChange: (e: T) => void; name: string; - placeholder?: string; - maxLength?: number; } interface RadioGroupProps extends InputProps { @@ -44,7 +42,9 @@ const RadioGroup: React.FC = ({ name, value, onChange, options id={`${name}${idx}`} labelText={option.label} name={name} - onChange={onChange} + onChange={e => { + onChange(e.target.value); + }} value={option.value} /> @@ -53,15 +53,17 @@ const RadioGroup: React.FC = ({ name, value, onChange, options ); }; -const DefaultInput: React.FC = ({ name, value, onChange, placeholder }) => { +const DefaultInput: React.FC = ({ name, value, onChange, ...restProps }) => { return ( { + onChange(e.target.value); + }} spellCheck='false' value={value} + {...restProps} /> ); }; @@ -85,7 +87,7 @@ const SelectInput: React.FC = ({ ); }; -const TextInput: React.FC> = ({ +const TextInput: React.FC> = ({ name, value, onChange, @@ -111,7 +113,7 @@ const TextInput: React.FC> = ({ validateAreaLength(e.target); resizeAreaSize(e.target); - onChange(e); + onChange(e.target.value); }} placeholder={placeholder} spellCheck='false' @@ -122,15 +124,16 @@ const TextInput: React.FC> = ({ ); }; -const ImageInput = () => { +const ImageInput: React.FC> = ({ name, value, onChange }) => { const [preview, setPreview] = useState(null); - const [file, setFile] = useState(null); + + useEffect(() => { + setPreview(value.url); + }, [value.url]); const handleFileChange = (event: React.ChangeEvent) => { const selectedFile = event.target.files?.[0]; - console.log(file); - if (selectedFile) { // 로컬 이미지 미리 보기 설정 const reader = new FileReader(); @@ -140,12 +143,13 @@ const ImageInput = () => { reader.readAsDataURL(selectedFile); // 업로드할 파일 설정 - setFile(selectedFile); + onChange({ ...value, file: selectedFile }); } }; const deleteProfileImage = () => { - setPreview(null); + onChange({ ...value, file: null }); + setPreview(value.url); }; const previewImage = preview ?? 'defaultImage'; @@ -154,7 +158,7 @@ const ImageInput = () => {
profile-img
- + Delete
@@ -199,7 +203,7 @@ export const RenderInput = ({ /> ); case 'image': - return ; + return ; default: return ( { formConfig: FormConfigType; onSubmit: (data: T) => void; + data: Partial; } export const ProfileForm = ({ onSubmit, formConfig, + data, }: ProfileFormProps) => { - const { method, formStructure } = useProfileForm({ formConfig }); + const { method, formStructure, handleReset } = useProfileForm({ formConfig }); + + useEffect(() => { + handleReset(data); + }, []); return ( diff --git a/src/features/portfolio/portfolio.dto.ts b/src/features/portfolio/portfolio.dto.ts index 58cbc12..29fc8a2 100644 --- a/src/features/portfolio/portfolio.dto.ts +++ b/src/features/portfolio/portfolio.dto.ts @@ -3,7 +3,7 @@ import type { PostUserResponseDTO } from '../user/user.dto'; import type { ApiResponse } from '@/shared/api'; export interface PostPortfolioDTO { - portfolioURL: string; + portfolioUrl: string; } export type PostPortfolioApiResponse = ApiResponse; diff --git a/src/features/portfolio/portfolio.hook.ts b/src/features/portfolio/portfolio.hook.ts new file mode 100644 index 0000000..66a874c --- /dev/null +++ b/src/features/portfolio/portfolio.hook.ts @@ -0,0 +1,10 @@ +import { useMutation } from '@tanstack/react-query'; + +import { postCreatePortfolio } from './portfolio.api'; +import type { PostPortfolioDTO } from './portfolio.dto'; + +export const useCreatePortfolio = () => { + return useMutation({ + mutationFn: ({ data }: { data: PostPortfolioDTO }) => postCreatePortfolio(data), + }); +}; diff --git a/src/features/user/model/user.store.ts b/src/features/user/model/user.store.ts index 31c19be..9cbcdb9 100644 --- a/src/features/user/model/user.store.ts +++ b/src/features/user/model/user.store.ts @@ -12,6 +12,7 @@ export interface UserDataState { interface UserState { userData: UserDataState | null; + loading: boolean; } interface UserActions { @@ -19,17 +20,19 @@ interface UserActions { setUserData: (data: UserDataState | null) => void; updateUserData: (updatedData: Partial) => void; clearUserData: () => void; + load: () => void; + done: () => void; }; } const initialState = { - userId: -1, userData: null, + loading: true, }; export const useUserStore = create( immer(set => ({ - userData: null, + ...initialState, actions: { setUserData: data => { set({ userData: data }); @@ -44,6 +47,12 @@ export const useUserStore = create( clearUserData: () => { set(initialState); }, + load: () => { + set({ loading: true }); + }, + done: () => { + set({ loading: false }); + }, }, })), ); diff --git a/src/features/user/user.dto.ts b/src/features/user/user.dto.ts index 299e39b..f438958 100644 --- a/src/features/user/user.dto.ts +++ b/src/features/user/user.dto.ts @@ -13,7 +13,6 @@ export interface UserDefaultInfo { export interface BaseUserDTO { name: string; - email: string; briefIntro: string; imageUrl: string; majorJobGroup: string; @@ -23,16 +22,22 @@ export interface BaseUserDTO { } export interface PostUserDTO extends BaseUserDTO { - url: string[]; + socials: string[]; s3StoredImageUrls: string[]; } -export interface PutUserDTO extends BaseUserDTO { +export interface PutUserDTO extends PostUserDTO { + portfolioLink: string; +} + +export interface EditUserDTO extends BaseUserDTO { + email: string; portfolioLink: string; socials: string[]; } export interface User extends BaseUserDTO { + email: string; socials: string[]; role: UserRole; color: Color; @@ -46,6 +51,6 @@ export interface PostUserResponseDTO { export type GetUserDefaultApiResponse = ApiResponse; export type PostUserApiReponse = ApiResponse; export type GetUserProfileApiResponse = ApiResponse; -export type GetEditUserApiResponse = ApiResponse; +export type GetEditUserApiResponse = ApiResponse; export type PutEditUserApiResponse = ApiResponse; export type GetMyProfileApiResponse = ApiResponse; diff --git a/src/pages/MyPage/MyPage.tsx b/src/pages/MyPage/MyPage.tsx index b5e2461..df9d576 100644 --- a/src/pages/MyPage/MyPage.tsx +++ b/src/pages/MyPage/MyPage.tsx @@ -1,8 +1,20 @@ +import { useNavigate } from 'react-router-dom'; + import styles from './MyPage.module.scss'; +import { useRoleGuard } from '@/shared/hook/useRoleGuard'; import { ContentLayout, SideTab, useMyTab } from '@/widgets'; export const MyPage = () => { + const navigate = useNavigate(); + // REAL_NEWBIE면 register page로 redirect + useRoleGuard({ + requiredRoles: ['JUST_NEWBIE', 'ADMIN', 'OLD_NEWBIE', 'USER'], + onAccessDenied: () => { + navigate('/register'); + }, + }); + const { activeTabItem, isActivePath } = useMyTab(); return (
diff --git a/src/pages/RegisterPage/RegisterPage.tsx b/src/pages/RegisterPage/RegisterPage.tsx index 8d76a33..7695e80 100644 --- a/src/pages/RegisterPage/RegisterPage.tsx +++ b/src/pages/RegisterPage/RegisterPage.tsx @@ -1,13 +1,22 @@ import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import styles from './RegisterPage.module.scss'; import { RegisterProgress, JOIN_STAGES } from '@/features/auth'; +import { useRoleGuard } from '@/shared/hook/useRoleGuard'; import { ProfileStep, PortfolioStep } from '@/widgets/RegisterUser'; const StepComponentList = [ProfileStep, PortfolioStep]; export const RegisterPage = () => { + const navigate = useNavigate(); + useRoleGuard({ + requiredRoles: ['REAL_NEWBIE', 'JUST_NEWBIE'], + onAccessDenied: () => { + navigate('/'); + }, + }); const [stage, setStage] = useState(1); const StepComponent = StepComponentList[stage - 1]; diff --git a/src/shared/api/baseApi.ts b/src/shared/api/baseApi.ts index b02fe33..da8fc9d 100644 --- a/src/shared/api/baseApi.ts +++ b/src/shared/api/baseApi.ts @@ -40,6 +40,12 @@ api.interceptors.response.use( response => response, async error => { console.log('에러 ㅣ ', error.response); + if (error.response.status === 400) { + if (error.response.data.reason === '토큰이 유효하지 않습니다.') { + interceptorEvents['logout']('토큰이 유효하지 않습니다.'); + return Promise.reject(error instanceof Error ? error : new Error('Unknown Error')); + } + } if (error.response?.status === 401) { if (isRefreshing) { return Promise.reject(error instanceof Error ? error : new Error('Unknown Error')); diff --git a/src/shared/hook/useRegistAlarm.ts b/src/shared/hook/useRegistAlarm.ts new file mode 100644 index 0000000..0bb7b68 --- /dev/null +++ b/src/shared/hook/useRegistAlarm.ts @@ -0,0 +1,29 @@ +import { useEffect } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import { customConfirm } from '../ui'; + +import { useUserStore } from '@/features/user/model/user.store'; + +const excludePath = ['/register', '/my']; + +export const useRegistAlarm = () => { + const navigate = useNavigate(); + const location = useLocation(); + const userData = useUserStore(state => state.userData); + + useEffect(() => { + if (!userData) return; + if (userData.role === 'REAL_NEWBIE' && !excludePath.includes(location.pathname)) { + void customConfirm({ + title: '유저 등록', + text: '아직 등록된 유저 프로필이 없습니다!\n프로필을 등록해주세요.', + icon: 'info', + }).then(result => { + if (result.isConfirmed) { + navigate('/register'); + } + }); + } + }, [userData, navigate, location.pathname]); +}; diff --git a/src/shared/hook/useRoleGuard.ts b/src/shared/hook/useRoleGuard.ts new file mode 100644 index 0000000..eb64e99 --- /dev/null +++ b/src/shared/hook/useRoleGuard.ts @@ -0,0 +1,48 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useShallow } from 'zustand/shallow'; + +import { customConfirm } from '../ui'; + +import { useUserStore } from '@/features/user/model/user.store'; +import type { UserRole } from '@/features/user/user.dto'; + +interface UseRoleGuardProps { + requiredRoles: UserRole[]; // 허용된 역할 목록 + onAccessDenied?: () => void; // 접근 제한 시 실행할 이벤트 +} + +export const useRoleGuard = ({ requiredRoles, onAccessDenied }: UseRoleGuardProps) => { + const { userData, loading } = useUserStore( + useShallow(state => ({ userData: state.userData, loading: state.loading })), + ); + const navigate = useNavigate(); + + useEffect(() => { + if (loading) return; // 로딩 중일 때는 아무 작업도 하지 않음 + + const defaultDeniedHandler = async () => { + await customConfirm({ + title: '접근 제한', + text: '접근 권한이 없습니다.', + icon: 'warning', + showCancelButton: false, + }).then(result => { + if (result.isConfirmed) { + navigate('/'); + return; + } + }); + }; + + if (!userData || !requiredRoles.includes(userData.role)) { + if (onAccessDenied) { + onAccessDenied(); + } else { + void defaultDeniedHandler(); + } + } + }, [userData, loading, requiredRoles, onAccessDenied, navigate]); + + return { userData }; +}; diff --git a/src/shared/ui/Input/Input.module.scss b/src/shared/ui/Input/Input.module.scss index 429a9de..afd43c3 100644 --- a/src/shared/ui/Input/Input.module.scss +++ b/src/shared/ui/Input/Input.module.scss @@ -14,4 +14,8 @@ border-color: $active-color; box-shadow: 0 0 0 1px $active-color; } + + &.disabled { + background-color: #f0f0f0; + } } diff --git a/src/shared/ui/Input/Input.tsx b/src/shared/ui/Input/Input.tsx index d7177b7..31d1f1e 100644 --- a/src/shared/ui/Input/Input.tsx +++ b/src/shared/ui/Input/Input.tsx @@ -9,7 +9,7 @@ interface Props extends InputHTMLAttributes { export const Input = ({ className, ...restProps }: Props) => { return ( -
+
); diff --git a/src/widgets/Layout/ui/Header/Header.tsx b/src/widgets/Layout/ui/Header/Header.tsx index 30d5381..31067b4 100644 --- a/src/widgets/Layout/ui/Header/Header.tsx +++ b/src/widgets/Layout/ui/Header/Header.tsx @@ -15,6 +15,7 @@ import { logout } from '@/features/auth/auth.api'; import { useUserStore } from '@/features/user/model/user.store'; import Logo from '@/shared/assets/paletteLogo.svg?react'; import { useModalStore } from '@/shared/model/modalStore'; +//componen import { Button, customConfirm } from '@/shared/ui'; import { MenuModal } from '@/widgets/MenuModal/MenuModal'; import { NoticeContainer } from '@/widgets/NoticeContainer/NoticeContainer'; @@ -33,6 +34,7 @@ export const Header = () => { const [menuOpen, setMenuOpen] = useState(false); const logoutHandler = async () => { + navigate('/'); await logout(); actions.setUserData(null); await customConfirm({ diff --git a/src/widgets/RegisterUser/PortfolioStep.tsx b/src/widgets/RegisterUser/PortfolioStep.tsx index 044fd37..a68c265 100644 --- a/src/widgets/RegisterUser/PortfolioStep.tsx +++ b/src/widgets/RegisterUser/PortfolioStep.tsx @@ -3,7 +3,9 @@ import { useNavigate } from 'react-router-dom'; import styles from './PortfolioStep.module.scss'; import { PortfolioInput, usePortfolioInput } from '@/features/auth'; -import { Button } from '@/shared/ui'; +import { useCreatePortfolio } from '@/features/portfolio/portfolio.hook'; +import { useUserStore } from '@/features/user/model/user.store'; +import { Button, customConfirm } from '@/shared/ui'; interface PortfolioStepProps { setStage: React.Dispatch>; @@ -11,6 +13,8 @@ interface PortfolioStepProps { export const PortfolioStep = ({ setStage }: PortfolioStepProps) => { const { portfolioUrl, error, handleInputChange, validate } = usePortfolioInput(); + const { mutate: createPortfolio } = useCreatePortfolio(); + const { updateUserData } = useUserStore(state => state.actions); const navigate = useNavigate(); return ( @@ -31,13 +35,39 @@ export const PortfolioStep = ({ setStage }: PortfolioStepProps) => { if (!validate(portfolioUrl)) return; /** 포트폴리오 등록 API */ - - /** 나만의 색깔 찾기 페이지 구현 시 해당 페이지로 이동 */ - - /** 임시, 메인으로 이동 */ - setStage(2); // error 방지용 의미 없는 코드 - alert('회원가입을 완료했습니다!'); - navigate('/'); + createPortfolio( + { + data: { + portfolioUrl, + }, + }, + { + onSuccess: () => { + // balance 게임 구현 시 stage 3으로 넘어가게 수정 예정 + setStage(2); + updateUserData({ + role: 'OLD_NEWBIE', + }); + void customConfirm({ + title: '유저 등록 완료', + text: '메인 페이지로 이동합니다.', + icon: 'info', + showCancelButton: false, + }).then(result => { + if (result.isConfirmed) { + navigate('/'); + } + }); + }, + onError: () => { + void customConfirm({ + title: '오류', + text: '포트폴리오 등록에 실패하셨습니다. 다시 시도해주세요.', + icon: 'error', + }); + }, + }, + ); }} > 다음 diff --git a/src/widgets/RegisterUser/ProfileStep.tsx b/src/widgets/RegisterUser/ProfileStep.tsx index f79232c..33d60e8 100644 --- a/src/widgets/RegisterUser/ProfileStep.tsx +++ b/src/widgets/RegisterUser/ProfileStep.tsx @@ -2,24 +2,97 @@ import type React from 'react'; import styles from './ProfileStep.module.scss'; -import { ProfileForm, formConfig } from '@/features/auth'; -import { Button } from '@/shared/ui'; +import type { FormValues } from '@/features/auth'; +import { ProfileForm, formConfig, handleImageUpload } from '@/features/auth'; +import { useUserStore } from '@/features/user/model/user.store'; +import type { PostUserDTO } from '@/features/user/user.dto'; +import { useCreateUser } from '@/features/user/user.hook'; +import { Button, customConfirm } from '@/shared/ui'; interface ProfileStepProps { setStage: React.Dispatch>; } export const ProfileStep = ({ setStage }: ProfileStepProps) => { + const { mutate: createUser } = useCreateUser(); + + const { updateUserData } = useUserStore(state => state.actions); + const userData = useUserStore(state => state.userData); + + const convertDataToFormValue = (): FormValues => { + const defaultValues = { + name: '', + briefIntro: '', + majorJobGroup: null, + minorJobGroup: null, + jobTitle: '', + division: 'student', + url: [], + imageUrl: { + url: '', + file: null, + }, + }; + + if (userData) { + return { + ...defaultValues, + name: userData.name || '', + imageUrl: { url: userData.imageUrl || '', file: null }, + }; + } + return defaultValues; + }; + + const handleSubmit = async (data: FormValues) => { + const profileImageUrl = (await handleImageUpload(data.imageUrl)) || data.imageUrl.url; + + const postUserData: PostUserDTO = { + name: data.name, + briefIntro: data.briefIntro, + jobTitle: data.jobTitle, + division: data.division, + imageUrl: profileImageUrl, + majorJobGroup: data.majorJobGroup?.value || '', + minorJobGroup: data.minorJobGroup?.value || '', + socials: data.url.map(link => link.value), + s3StoredImageUrls: [], + }; + console.log(postUserData); + createUser( + { + data: postUserData, + }, + { + onSuccess: () => { + updateUserData({ + name: data.name, + imageUrl: profileImageUrl, + role: 'JUST_NEWBIE', + }); + setStage(stage => stage + 1); + }, + onError: () => { + void customConfirm({ + title: '오류', + text: '유저 등록에 실패하셨습니다. 다시 시도해주세요.', + icon: 'error', + }); + }, + }, + ); + }; + return (

유저 프로필 등록

- { - console.log(data); - setStage(prev => prev + 1); - }} - /> + {userData && ( + + )}
From 7484dd5bdb352bcb38200504c664d5e27418ab8d Mon Sep 17 00:00:00 2001 From: Cho-heejung <66050038+he2e2@users.noreply.github.com> Date: Fri, 6 Dec 2024 16:27:34 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[Feat]=20=EC=95=84=EC=B9=B4=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=20=EC=B0=A8=ED=8A=B8=20=EB=B0=8F=20=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=20=EC=95=84=EC=B9=B4=EC=9D=B4=EB=B8=8C=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?(#94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 아카이브 차트 연결 * feat: 인기 아카이브 훅 작성 * feat: 메인 페이지 인기 아카이브 연결 * feat: 헤더 로그아웃 버튼 /user 이동 플로우로 변경 * design: 메인 페이지 transfrom 속성 세분화 --- src/features/archive/archive.api.ts | 3 + src/features/archive/archive.hook.ts | 7 ++ src/features/user/ui/UserProfileInfo.tsx | 7 +- src/pages/UserPage/UserPage.module.scss | 1 + .../Layout/ui/Header/Header.module.scss | 8 +++ src/widgets/Layout/ui/Header/Header.tsx | 23 ++---- .../MainContents/MainContents.module.scss | 2 + .../MainGridItem/MainGridItem.module.scss | 50 ++++++++++++- src/widgets/MainGridItem/MainGridItem.tsx | 29 +++----- src/widgets/MenuModal/MenuModal.module.scss | 8 +++ src/widgets/MenuModal/MenuModal.tsx | 15 ++-- src/widgets/UserContents/UserContents.tsx | 71 ++++++++++--------- 12 files changed, 133 insertions(+), 91 deletions(-) diff --git a/src/features/archive/archive.api.ts b/src/features/archive/archive.api.ts index f65a12e..2bc4531 100644 --- a/src/features/archive/archive.api.ts +++ b/src/features/archive/archive.api.ts @@ -86,3 +86,6 @@ export const getMyArchiveList = () => api.get('/archive/me').then(res => res.data); export const patchArchiveOrder = (data: PatchArchiveOrderDTO) => api.patch('/archive', data); + +export const getPopularArchive = () => + api.get('/archive/main').then(res => res.data); diff --git a/src/features/archive/archive.hook.ts b/src/features/archive/archive.hook.ts index 1878419..c480b40 100644 --- a/src/features/archive/archive.hook.ts +++ b/src/features/archive/archive.hook.ts @@ -17,6 +17,7 @@ import { putComment, getMyArchiveList, patchArchiveOrder, + getPopularArchive, } from './archive.api'; import type { BaseArchiveDTO, @@ -62,6 +63,12 @@ export const useArchive = (archiveId: number) => queryFn: () => getArchive(archiveId), }); +export const usePopularArchive = () => + useQuery({ + queryKey: ['/archive/main'], + queryFn: () => getPopularArchive(), + }); + export const useComments = (archiveId: number) => { return useCustomInfiniteQuery( ['/archive', archiveId, 'comment'], diff --git a/src/features/user/ui/UserProfileInfo.tsx b/src/features/user/ui/UserProfileInfo.tsx index bd00c84..f8c3c9d 100644 --- a/src/features/user/ui/UserProfileInfo.tsx +++ b/src/features/user/ui/UserProfileInfo.tsx @@ -15,12 +15,7 @@ export const UserProfileInfo = () => {
채승규 - { - // TODO : 내 정보일 경우 /my로 이동 - }} - /> + {}} />
프론트엔드 개발자 diff --git a/src/pages/UserPage/UserPage.module.scss b/src/pages/UserPage/UserPage.module.scss index 210ffa3..d20bc16 100644 --- a/src/pages/UserPage/UserPage.module.scss +++ b/src/pages/UserPage/UserPage.module.scss @@ -46,6 +46,7 @@ $colors: ( @media (width <= 768px) { flex-direction: column; row-gap: 2rem; + align-items: center; padding: 0 2rem; } diff --git a/src/widgets/Layout/ui/Header/Header.module.scss b/src/widgets/Layout/ui/Header/Header.module.scss index 6de34ad..a1b256d 100644 --- a/src/widgets/Layout/ui/Header/Header.module.scss +++ b/src/widgets/Layout/ui/Header/Header.module.scss @@ -133,3 +133,11 @@ display: flex; min-width: 320px; } + +.userProfile { + width: 2rem; + aspect-ratio: 1/1; + object-fit: cover; + border: 1px solid #cfcfcf; + border-radius: 4px; +} diff --git a/src/widgets/Layout/ui/Header/Header.tsx b/src/widgets/Layout/ui/Header/Header.tsx index 31067b4..51166fb 100644 --- a/src/widgets/Layout/ui/Header/Header.tsx +++ b/src/widgets/Layout/ui/Header/Header.tsx @@ -11,7 +11,6 @@ import { NAV_LINKS } from '../../constants'; //assets import { SearchBar } from '@/features'; -import { logout } from '@/features/auth/auth.api'; import { useUserStore } from '@/features/user/model/user.store'; import Logo from '@/shared/assets/paletteLogo.svg?react'; import { useModalStore } from '@/shared/model/modalStore'; @@ -24,7 +23,7 @@ export const Header = () => { const { pathname } = useLocation(); const navigate = useNavigate(); const open = useModalStore(state => state.actions.open); - const { userData, actions } = useUserStore( + const { userData } = useUserStore( useShallow(state => ({ userData: state.userData, actions: state.actions, @@ -33,17 +32,6 @@ export const Header = () => { const [isMobile, setIsMobile] = useState(false); const [menuOpen, setMenuOpen] = useState(false); - const logoutHandler = async () => { - navigate('/'); - await logout(); - actions.setUserData(null); - await customConfirm({ - title: '로그아웃', - text: '로그아웃 되었습니다.', - icon: 'info', - showCancelButton: false, - }); - }; const [isSearch, setIsSearch] = useState(false); const [isNotice, setIsNotice] = useState(false); @@ -109,7 +97,6 @@ export const Header = () => { isOpen={menuOpen} isUserData={userData ? true : false} onClose={setMenuOpen} - onLogout={logoutHandler} /> )}{' '} @@ -155,13 +142,13 @@ export const Header = () => { }} /> {userData ? ( - + user-profile + ) : ( + ) : (