diff --git a/.eslintrc.json b/.eslintrc.json index 23e864d8..af4bf905 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -32,7 +32,7 @@ "globals": { "_": true }, - "plugins": ["import", "html", "react", "@typescript-eslint", "tailwindcss"], + "plugins": ["import", "html", "react", "@typescript-eslint", "tailwindcss", "prettier"], "rules": { "import/no-unresolved": 0, "no-console": "warn", @@ -50,14 +50,7 @@ "import/order": [ "error", { - "groups": [ - "external", - "internal", - "builtin", - "parent", - "sibling", - "index" - ], + "groups": ["external", "internal", "builtin", "parent", "sibling", "index"], "pathGroupsExcludedImportTypes": ["react"], "newlines-between": "always", "pathGroups": [ @@ -79,10 +72,7 @@ } ], "react/react-in-jsx-scope": "off", - "react/jsx-filename-extension": [ - 1, - { "extensions": [".js", ".jsx", ".ts", ".tsx"] } - ], + "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }], "@typescript-eslint/no-unused-vars": ["warn"], "@typescript-eslint/no-shadow": ["warn"], "@typescript-eslint/explicit-module-boundary-types": "off", diff --git a/public/icons/logo-white-s.svg b/public/icons/logo-white-s.svg new file mode 100644 index 00000000..47b6260c --- /dev/null +++ b/public/icons/logo-white-s.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/icons/logo-white.svg b/public/icons/logo-white.svg new file mode 100644 index 00000000..5a7efceb --- /dev/null +++ b/public/icons/logo-white.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/icons/plus-boxed.svg b/public/icons/plus-boxed.svg new file mode 100644 index 00000000..2851ea76 --- /dev/null +++ b/public/icons/plus-boxed.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Header/DashboardHeader.tsx b/src/components/Header/DashboardHeader.tsx new file mode 100644 index 00000000..089c0b17 --- /dev/null +++ b/src/components/Header/DashboardHeader.tsx @@ -0,0 +1,107 @@ +import Image from 'next/image'; +import Link from 'next/link'; + +import { ProfileIcon } from '../ProfileIcon'; + +import UserMenuDropdown from './UserMenuDropdown'; + +import settingsIcon from '@/../public/icons/gear.svg'; +import plusIcon from '@/../public/icons/plus-boxed.svg'; +import useFetchData from '@/hooks/useFetchData'; +import { getMembersList } from '@/services/getService'; +import { Dashboard } from '@/types/Dashboard.interface'; +import { MembersResponse } from '@/types/Member.interface'; + +export default function DashboardHeader({ id, title, createdByMe }: Dashboard) { + return ( +
+
+

{title}

+ {createdByMe && 왕관 아이콘} +
+
+ +
+ +
+ +
+
+
+ ); +} + +interface ButtonsProps { + id: number; +} + +interface MemberProfilesProps { + id: number; +} + +function Buttons({ id }: ButtonsProps) { + const handleInviteClick = () => { + // TODO: 모달 구현되면 연결하기 + alert('초대하기 모달'); + }; + + return ( +
+ + 대시보드 관리 아이콘 + 관리 + + +
+ ); +} + +function MemberProfiles({ id }: MemberProfilesProps) { + const { data, isLoading, error } = useFetchData(['members', id], () => getMembersList(id)); + if (isLoading) { + return

로딩중...

; + } + + if (error) { + return

{error.message}

; + } + + if (!data?.members) { + // NOTE: 실제론 생성자 한명이라도 존재 + return

멤버가 없습니다.

; + } + + // TODO: 길이도 정확히 하려면 device 정보 있어야 함 + const w = 38 + 30 * (data.members.length - 1); + const ulStyle = { width: w }; + return ( + + ); +} diff --git a/src/components/Header/DefaultHeader.tsx b/src/components/Header/DefaultHeader.tsx new file mode 100644 index 00000000..fc44df22 --- /dev/null +++ b/src/components/Header/DefaultHeader.tsx @@ -0,0 +1,14 @@ +import UserMenuDropdown from './UserMenuDropdown'; + +interface DefaultHeaderProps { + title: string; +} + +export default function DefaultHeader({ title }: DefaultHeaderProps) { + return ( +
+

{title}

+ +
+ ); +} diff --git a/src/components/Header/LandingHeader.tsx b/src/components/Header/LandingHeader.tsx new file mode 100644 index 00000000..78afa4fd --- /dev/null +++ b/src/components/Header/LandingHeader.tsx @@ -0,0 +1,16 @@ +import Image from 'next/image'; +import Link from 'next/link'; + +export default function LandingHeader() { + return ( +
+ + 로고 + +
+ 로그인 + 회원가입 +
+
+ ); +} diff --git a/src/components/Header/UserMenuDropdown.tsx b/src/components/Header/UserMenuDropdown.tsx new file mode 100644 index 00000000..90ec93b5 --- /dev/null +++ b/src/components/Header/UserMenuDropdown.tsx @@ -0,0 +1,46 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { useState } from 'react'; +import { useDispatch } from 'react-redux'; + +import UserProfile from './UserProfile'; + +import { clearUser } from '@/store/reducers/userSlice'; + +export default function UserMenuDropdown() { + const [isOpen, setIsOpen] = useState(false); + const router = useRouter(); + const dispatch = useDispatch(); + + const handleDropdownClick = () => { + setIsOpen((isOpen) => !isOpen); + }; + + const handleLogoutClick = () => { + dispatch(clearUser()); + router.push('/'); + }; + + return ( +
+ + {isOpen && ( + + )} +
+ ); +} diff --git a/src/components/Header/UserProfile.tsx b/src/components/Header/UserProfile.tsx new file mode 100644 index 00000000..942a1028 --- /dev/null +++ b/src/components/Header/UserProfile.tsx @@ -0,0 +1,27 @@ +import { useSelector } from 'react-redux'; + +import { ProfileIcon } from '../ProfileIcon'; + +import { RootState } from '@/store/store'; + +export default function UserProfile() { + const { user, error } = useSelector((state: RootState) => state.user); + + if (error) { + return

{error}

; + } + + if (!user) { + /* NOTE: 유저 정보가 있는 페이지에서만 사용할 것이므로 이 코드는 접근 불가능해야 옳다. + * 만약 유저 정보가 없다면 페이지에서 리다이렉트할 것이므로 여기서 따로 리다이렉트 하지 않았다. + */ + return

유저 정보가 없습니다

; + } + + return ( +
+ +

{user.nickname}

+
+ ); +} diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx new file mode 100644 index 00000000..b7ec5b7f --- /dev/null +++ b/src/components/Header/index.tsx @@ -0,0 +1,54 @@ +import { useRouter } from 'next/router'; +import { useSelector } from 'react-redux'; + +import DashboardHeader from './DashboardHeader'; +import DefaultHeader from './DefaultHeader'; +import LandingHeader from './LandingHeader'; + +import { useFetchDashboards } from '@/hooks/useFetchDashboards'; +import { RootState } from '@/store/store'; +import findDashboardById from '@/utils/findDashboardById'; + +const HEADER_TITLES = { + '/mydashboard': '내 대시보드', + '/mypage': '계정관리', +}; + +export default function Header() { + const { user } = useSelector((state: RootState) => state.user); + const router = useRouter(); + const { pathname } = router; + + const { id } = router.query; + const { isLoading } = useFetchDashboards(); + const { dashboards } = useSelector((state: RootState) => state.dashboards); + + // NOTE: 리다이렉션은 페이지에서 할 것이라 생각하고, 상태에 걸맞은 헤더 보여줌 + if (!user) { + if (pathname === '/') { + // NOTE: 랜딩페이지 + return ; + } + + // NOTE: 로그인 페이지 or 404 페이지 + return <>; + } + + if (pathname.startsWith('/dashboard/')) { + // NOTE: 대시보드 페이지 + if (isLoading) { + return ; + } + + const dashboard = findDashboardById(dashboards, Number(id)); + return ; + } + + if (pathname === '/mydashboard' || pathname === '/mypage') { + // NOTE: 나의 대시보드 페이지 or 마이페이지 + return ; + } + + // NOTE: 404 페이지 + return <>; +} diff --git a/src/components/ProfileIcon/index.tsx b/src/components/ProfileIcon/index.tsx new file mode 100644 index 00000000..9922e29c --- /dev/null +++ b/src/components/ProfileIcon/index.tsx @@ -0,0 +1,24 @@ +import Image from 'next/image'; + +import { User } from '@/types/User.interface'; + +interface ProfileIconProps { + user: User; + imgClassName: string; + fontClassName: string; +} + +export function ProfileIcon({ user, imgClassName, fontClassName }: ProfileIconProps) { + return ( + // TODO: 컬러 지정. 현재는 임의로 회색을 지정함 +
+ {user.profileImageUrl ? ( + 프로필 이미지 + ) : ( +

+ {user.nickname.substring(0, 1)} +

+ )} +
+ ); +} diff --git a/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx index 392bcbc5..c586d279 100644 --- a/src/layouts/MainLayout.tsx +++ b/src/layouts/MainLayout.tsx @@ -1,15 +1,22 @@ import { useRouter } from 'next/router'; +import Header from '@/components/Header'; import Sidebar from '@/components/Sidebar'; -// import Header from '../Header'; - export default function MainLayout({ children }: { children: React.ReactNode }) { const router = useRouter(); const currentPath = router.asPath; - const isDisabled = - currentPath === '/' || currentPath === '/signin' || currentPath === '/signup' || currentPath === '/404'; + if (currentPath === '/') { + return ( + <> +
+ {children} + + ); + } + + const isDisabled = currentPath === '/signin' || currentPath === '/signup' || currentPath === '/404'; if (isDisabled) return <>{children}; @@ -18,10 +25,7 @@ export default function MainLayout({ children }: { children: React.ReactNode })
- {/*
*/} -
-

Header

-
+
{children}
diff --git a/src/pages/signup/index.tsx b/src/pages/signup/index.tsx index 1a14da3b..d28ad6a2 100644 --- a/src/pages/signup/index.tsx +++ b/src/pages/signup/index.tsx @@ -9,7 +9,7 @@ export default function SignUp() {
-

+

이미 가입하셨나요?{' '} 로그인하기 diff --git a/src/services/getService.ts b/src/services/getService.ts index 48cd9321..00ac6290 100644 --- a/src/services/getService.ts +++ b/src/services/getService.ts @@ -13,3 +13,12 @@ export async function getDashboardsList( ) { return await instance.get(`/dashboards?navigationMethod=${navigationMethod}&page=${page}&size=${size}`); } + +// 대시보드 멤버 목록 조회 +export const getMembersList = async ( + dashboardId: number, + page: number = 1, // 기본값 1 + size: number = 4, // 기본값 4 +) => { + return await instance.get(`/members?page=${page}&size=${size}&dashboardId=${dashboardId}`); +}; diff --git a/src/styles/globals.css b/src/styles/globals.css index b5c61c95..fc3d2ecd 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1,3 +1,9 @@ @tailwind base; @tailwind components; @tailwind utilities; + +@layer base { + body { + @apply font-pretendard font-normal; + } +} diff --git a/src/types/Member.interface.ts b/src/types/Member.interface.ts new file mode 100644 index 00000000..2202f057 --- /dev/null +++ b/src/types/Member.interface.ts @@ -0,0 +1,11 @@ +import { User } from './User.interface'; + +export interface Member extends User { + isOwner: boolean; + userId: number; +} + +export interface MembersResponse { + members: Member[]; + totalCount: number; +} diff --git a/src/utils/findDashboardById.ts b/src/utils/findDashboardById.ts new file mode 100644 index 00000000..6fbe67ee --- /dev/null +++ b/src/utils/findDashboardById.ts @@ -0,0 +1,6 @@ +import { Dashboard } from '@/types/Dashboard.interface'; + +const findDashboardById = (dashboards: Dashboard[], id: number) => + dashboards.filter((dashboard) => dashboard.id === id)[0]; + +export default findDashboardById; diff --git a/tailwind.config.ts b/tailwind.config.ts index b7690510..28c0f24b 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -13,6 +13,10 @@ const config: Config = { md: '780px', lg: '1280px', }, + fontFamily: { + pretendard: ['Pretendard'], + montserrat: ['Montserrat'], + }, colors: { black: { DEFAULT: '#000000',