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 (
+
+ );
+}
+
+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 (
+
+ {data.members.map((user, i) => {
+ const offset = 30 * i;
+ const liStyle = { left: offset };
+ const hidden = i >= 2 ? 'hidden' : '';
+ return (
+ -
+
+
+ );
+ })}
+ {/* TODO: + 버튼 추가하려면 device 정보가 있어야 함 */}
+
+ );
+}
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 (
+
+ );
+}
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 (
+
+ );
+}
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 })
- {/* */}
-
+
{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',