diff --git a/src/app/routes/MainLayout.tsx b/src/app/routes/MainLayout.tsx
index f3d122c..9fb5f8c 100644
--- a/src/app/routes/MainLayout.tsx
+++ b/src/app/routes/MainLayout.tsx
@@ -1,6 +1,7 @@
import type { JSX } from "react";
import { Outlet } from "react-router-dom";
+import Footer from "@widgets/Footer";
import Header from "@widgets/Header/Header";
const MainLayout = (): JSX.Element => {
@@ -10,6 +11,7 @@ const MainLayout = (): JSX.Element => {
+
>
);
};
diff --git a/src/shared/ui/DevelopersDropdown.tsx b/src/shared/ui/DevelopersDropdown.tsx
new file mode 100644
index 0000000..9b02b05
--- /dev/null
+++ b/src/shared/ui/DevelopersDropdown.tsx
@@ -0,0 +1,184 @@
+import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
+import {
+ Box,
+ Typography,
+ Collapse,
+ IconButton,
+ Link,
+ styled,
+ useTheme,
+ useMediaQuery,
+} from "@mui/material";
+import type { JSX } from "react";
+import { useState } from "react";
+
+import GradientGitHubIcon from "@shared/ui/icons/GradientGitHubIcon";
+
+const developers = [
+ { name: "윤다빈", url: "https://github.com/czmcm5" },
+ { name: "윤태관", url: "https://github.com/tkyoun0421" },
+ { name: "석민영", url: "https://github.com/MINYOUNG-SEOK" },
+ { name: "한사라", url: "https://github.com/namee-h" },
+];
+
+export default function DevelopersDropdown(): JSX.Element {
+ const [open, setOpen] = useState(false);
+ const [hover, setHover] = useState(false);
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
+
+ const isActive = open || hover;
+
+ return (
+
+ {/* 타이틀 박스 */}
+ setOpen((prev) => !prev)}
+ onMouseEnter={() => setHover(true)}
+ onMouseLeave={() => setHover(false)}
+ >
+
+
+ 개발자들
+
+
+
+
+
+ {/* 리스트 */}
+
+
+ {developers.map((dev) => (
+
+ {dev.name}
+
+ {dev.url}
+
+
+ ))}
+
+
+
+ );
+}
+
+const DropdownContainer = styled(Box)({
+ width: 380,
+ maxWidth: "100%",
+});
+
+const DropdownTitle = styled(Box)<{ $active?: boolean }>(
+ ({ $active, theme }) => ({
+ display: "flex",
+ alignItems: "center",
+ gap: 16,
+ padding: "16px 24px",
+ borderRadius: 8,
+ background: $active ? "#f5f5f5" : "transparent",
+ boxShadow: "none",
+ cursor: "pointer",
+ transition:
+ "box-shadow 0.2s, background 0.2s, transform 0.18s cubic-bezier(0.4,0,0.2,1)",
+ fontWeight: 600,
+ fontSize: 18,
+ color: "#23294a",
+ marginBottom: 8,
+ transform: $active ? "scale(1.04)" : "scale(1)",
+ [theme.breakpoints.down("sm")]: {
+ padding: "12px 8px",
+ gap: 8,
+ },
+ })
+);
+
+const DropdownTitleCenter = styled(Box)(({ theme }) => ({
+ display: "flex",
+ alignItems: "center",
+ flex: 1,
+ justifyContent: "center",
+ [theme.breakpoints.down("sm")]: {
+ gap: 4,
+ },
+}));
+
+const DropdownTitleText = styled(Typography, {
+ shouldForwardProp: (prop) => prop !== "$active",
+})<{ $active: boolean }>(({ $active, theme }) => ({
+ fontWeight: 600,
+ fontSize: 18,
+ color: $active ? "#2563eb" : "#888",
+ letterSpacing: "0.2rem",
+ transition: "color 0.2s",
+ [theme.breakpoints.down("sm")]: {
+ fontSize: 15,
+ },
+}));
+
+const DropdownList = styled(Box)({
+ background: "transparent",
+ borderRadius: 0,
+ padding: "4px 12px",
+ boxShadow: "none",
+});
+
+const DropdownItem = styled(Box)({
+ display: "flex",
+ alignItems: "center",
+ padding: "4px 8px",
+ borderRadius: 0,
+ transition: "background 0.15s",
+ margin: "2px 0",
+ overflow: "hidden",
+ cursor: "pointer",
+ "&:hover": {
+ background: "#f5f5f5",
+ border: "none",
+ borderRadius: 0,
+ boxShadow: "none",
+ },
+});
+
+const NameText = styled(Typography)({
+ fontWeight: 500,
+ color: "#888",
+ minWidth: 70,
+ fontSize: 10,
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+});
+
+const GithubLink = styled(Link)({
+ marginLeft: 8,
+ color: "#bbb",
+ fontSize: 12,
+ textDecoration: "none",
+ wordBreak: "break-all",
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ maxWidth: 220,
+ transition: "color 0.18s cubic-bezier(0.4,0,0.2,1)",
+ "&:hover": {
+ color: "#2563eb",
+ textDecoration: "none",
+ },
+});
diff --git a/src/shared/ui/LogoBox.tsx b/src/shared/ui/LogoBox.tsx
index 4b6c5d2..58bbd7f 100644
--- a/src/shared/ui/LogoBox.tsx
+++ b/src/shared/ui/LogoBox.tsx
@@ -1,4 +1,5 @@
import { Box, styled, alpha } from "@mui/material";
+import type { SxProps, Theme } from "@mui/material";
import type { JSX } from "react";
import { useNavigate } from "react-router-dom";
@@ -8,6 +9,8 @@ interface LogoBoxProps {
showText?: boolean;
text?: string;
className?: string;
+ disableHover?: boolean;
+ sx?: SxProps;
}
const LogoBox = ({
@@ -16,6 +19,8 @@ const LogoBox = ({
showText = true,
text = "프로젝트 잼",
className,
+ disableHover = false,
+ sx,
}: LogoBoxProps): JSX.Element => {
const navigate = useNavigate();
@@ -28,7 +33,13 @@ const LogoBox = ({
};
return (
-
+
{showText && {text}}
@@ -37,25 +48,30 @@ const LogoBox = ({
export default LogoBox;
-const StyledLogoBox = styled(Box)<{ $size: "small" | "medium" | "large" }>(
- ({ theme, $size }) => ({
- display: "flex",
- alignItems: "center",
- cursor: "pointer",
- padding:
- $size === "small"
- ? "0.3rem 0.8rem"
- : $size === "medium"
- ? "0.5rem 1rem"
- : "0.8rem 1.5rem",
- borderRadius: theme.spacing(1.5),
- transition: "all 0.2s ease-in-out",
- "&:hover": {
- backgroundColor: alpha(theme.palette.primary.main, 0.08),
- transform: "translateY(-1px)",
- },
- })
-);
+const StyledLogoBox = styled(Box)<{
+ $size: "small" | "medium" | "large";
+ $disableHover?: boolean;
+}>(({ theme, $size, $disableHover }) => ({
+ display: "flex",
+ alignItems: "center",
+ cursor: "pointer",
+ padding:
+ $size === "small"
+ ? "0.3rem 0.8rem"
+ : $size === "medium"
+ ? "0.5rem 1rem"
+ : "0.8rem 1.5rem",
+ borderRadius: theme.spacing(1.5),
+ transition: "all 0.2s ease-in-out",
+ ...(!!$disableHover
+ ? {}
+ : {
+ "&:hover": {
+ backgroundColor: alpha(theme.palette.primary.main, 0.08),
+ transform: "translateY(-1px)",
+ },
+ }),
+}));
const LogoImage = styled("img")<{ $size: "small" | "medium" | "large" }>(
({ theme, $size }) => ({
diff --git a/src/shared/ui/SnackbarAlert.tsx b/src/shared/ui/SnackbarAlert.tsx
index 80d174b..50452d9 100644
--- a/src/shared/ui/SnackbarAlert.tsx
+++ b/src/shared/ui/SnackbarAlert.tsx
@@ -28,6 +28,9 @@ const SnackbarAlert = ({
autoHideDuration={duration}
onClose={onClose}
anchorOrigin={anchorOrigin}
+ sx={{
+ marginTop: { xs: "6.4rem", md: "8rem" },
+ }}
>
{message}
diff --git a/src/shared/ui/icons/CommonIcons.tsx b/src/shared/ui/icons/CommonIcons.tsx
index c6673b8..a4b4090 100644
--- a/src/shared/ui/icons/CommonIcons.tsx
+++ b/src/shared/ui/icons/CommonIcons.tsx
@@ -30,3 +30,5 @@ export { default as HistoryToggleOffIcon } from "@mui/icons-material/HistoryTogg
export { default as ChevronLeftIcon } from "@mui/icons-material/ChevronLeft";
export { default as ChevronRightIcon } from "@mui/icons-material/ChevronRight";
export { default as MoreHorizIcon } from "@mui/icons-material/MoreHoriz";
+
+export { default as GitHubIcon } from "@mui/icons-material/GitHub";
diff --git a/src/shared/ui/icons/GradientGitHubIcon.tsx b/src/shared/ui/icons/GradientGitHubIcon.tsx
new file mode 100644
index 0000000..a403d30
--- /dev/null
+++ b/src/shared/ui/icons/GradientGitHubIcon.tsx
@@ -0,0 +1,37 @@
+import type { JSX } from "react";
+
+export default function GradientGitHubIcon({
+ size = 32,
+ color,
+}: {
+ size?: number;
+ color?: string;
+}): JSX.Element {
+ return (
+
+ );
+}
diff --git a/src/widgets/Footer/Footer.tsx b/src/widgets/Footer/Footer.tsx
new file mode 100644
index 0000000..1f3fec5
--- /dev/null
+++ b/src/widgets/Footer/Footer.tsx
@@ -0,0 +1,211 @@
+import {
+ Box,
+ Divider,
+ Typography,
+ styled,
+ useTheme,
+ useMediaQuery,
+} from "@mui/material";
+import type { JSX } from "react";
+
+import DevelopersDropdown from "@shared/ui/DevelopersDropdown";
+import LogoBox from "@shared/ui/LogoBox";
+import NavigateButton from "@shared/ui/NavigateButton";
+
+const Footer = (): JSX.Element => {
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
+
+ return (
+
+
+
+
+
+
+
+
+ {isMobile ? (
+ <>
+ 함께 성장하는 개발자{"\n"}프로젝트 잼에서 시작하세요!!
+ >
+ ) : (
+ <>
+
+ 모든 개발자가 자신의 전문성을 바탕으로 함께 성장할 수 있는
+ 협업의 문을 엽니다.
+
+
+ 프로잭트 잼은 단순한 프로젝트 매칭을 넘어,
+
+
+ 다양한 개발 직군이 서로의 강점을 살려 함께 성장하는 생태계를
+ 만들어갑니다.
+
+
+
+ 함께 성장하는 개발자 커뮤니티, 프로젝트 잼에서 시작하세요!
+ 🚀
+
+
+ >
+ )}
+
+
+
+
+
+
+ 프로젝트 찾기
+
+
+ 프로젝트 등록
+
+
+
+ {/* 디벨로퍼즈 드롭다운 메뉴 */}
+
+
+
+
+
+
+
+
+
+ © 2025 프로젝트잼. All rights reserved
+
+ );
+};
+
+export default Footer;
+
+const FooterContainer = styled(Box)({
+ width: "100%",
+ background: "#fff",
+ padding: "2rem 0 1rem 0",
+ marginTop: "auto",
+ boxShadow: " 0 -6px 12px -8px rgba(0,0,0,0.08)", // top 이너+아웃터 그림자
+});
+
+const FooterContent = styled(Box)(({ theme }) => ({
+ height: "100%",
+ maxWidth: 1280,
+ margin: "0 auto",
+ display: "flex",
+ flexDirection: "row",
+ alignItems: "flex-start",
+ justifyContent: "space-between",
+ gap: theme.spacing(2),
+ [theme.breakpoints.down("sm")]: {
+ flexDirection: "column",
+ alignItems: "flex-start",
+ gap: theme.spacing(1),
+ },
+}));
+
+const LogoSection = styled(Box)(({ theme }) => ({
+ display: "flex",
+ flexDirection: "column",
+ justifyContent: "space-between",
+ gap: 8,
+ height: "100%",
+ [theme.breakpoints.down("sm")]: {
+ alignItems: "center",
+ width: "100%",
+ flexDirection: "column",
+ },
+}));
+
+const InfoSection = styled(Box)(({ theme }) => ({
+ flex: 1,
+ minWidth: 200,
+ marginLeft: 16,
+ marginTop: 52,
+ [theme.breakpoints.down("sm")]: {
+ marginTop: 0,
+ marginLeft: 0,
+ textAlign: "center",
+ },
+}));
+
+const NavSection = styled(Box)(({ theme }) => ({
+ display: "flex",
+ alignItems: "center",
+ gap: 16,
+ marginTop: 16,
+ [theme.breakpoints.down("sm")]: {
+ width: "100%",
+ justifyContent: "center",
+ },
+}));
+
+const DevelopersSection = styled(Box)(({ theme }) => ({
+ display: "flex",
+ alignItems: "center",
+ gap: 16,
+ [theme.breakpoints.down("sm")]: {
+ width: "100%",
+ justifyContent: "center",
+ },
+}));
+
+const Copyright = styled(Typography)({
+ textAlign: "center",
+ color: "#bbb",
+ fontSize: 13,
+ marginTop: 16,
+});
+
+const VisionText = styled(Typography)({
+ marginTop: 16,
+ fontSize: 14,
+ color: "#666",
+});
diff --git a/src/widgets/Footer/index.ts b/src/widgets/Footer/index.ts
new file mode 100644
index 0000000..3738288
--- /dev/null
+++ b/src/widgets/Footer/index.ts
@@ -0,0 +1 @@
+export { default } from "./Footer";
diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx
index e57a260..e97dae1 100644
--- a/src/widgets/Header/Header.tsx
+++ b/src/widgets/Header/Header.tsx
@@ -1,56 +1,165 @@
-import { Box, styled, Avatar, Button, alpha } from "@mui/material";
+import MenuIcon from "@mui/icons-material/Menu";
+import {
+ Box,
+ styled,
+ Avatar,
+ Button,
+ alpha,
+ IconButton,
+ Drawer,
+ List,
+ ListItem,
+ ListItemButton,
+ ListItemText,
+ Divider,
+ useTheme,
+ useMediaQuery,
+} from "@mui/material";
import type { JSX } from "react";
+import { useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import LoginButton from "@features/auth/ui/LoginButton";
import LogoutButton from "@features/auth/ui/LogoutButton";
+import { auth } from "@shared/firebase/firebase";
import { useAuthStore } from "@shared/stores/authStore";
+import { useSnackbarStore } from "@shared/stores/snackbarStore";
import LogoBox from "@shared/ui/LogoBox";
const Header = (): JSX.Element => {
const user = useAuthStore((state) => state.user);
+ const isLoggedIn = useAuthStore((state) => !!state.user);
const navigate = useNavigate();
const location = useLocation();
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
+ const [drawerOpen, setDrawerOpen] = useState(false);
+ const { showWarning } = useSnackbarStore();
const isActive = (path: string): boolean => location.pathname === path;
+ // Drawer 메뉴 항목
+ const drawerMenu = [
+ {
+ label: "마이페이지",
+ onClick: () => {
+ if (!user) {
+ showWarning("로그인 후 이용해 주세요");
+ } else {
+ navigate("/profile");
+ }
+ },
+ },
+ { label: "프로젝트 찾기", onClick: () => navigate("/project") },
+ { label: "프로젝트 등록", onClick: () => navigate("/project/insert") },
+ ];
+
+ console.log("Header user:", user);
+
return (
-
-
-
-
-
- navigate("/project")}
- $isActive={isActive("/project")}
- >
- 프로젝트 찾기
-
- navigate("/project/insert")}
- $isActive={isActive("/project/insert")}
- >
- 프로젝트 등록
-
-
-
-
- {user ? (
-
- navigate("/profile")}
- />
-
-
- ) : (
-
- )}
-
+ {/* 모바일 헤더 */}
+ {isMobile ? (
+ <>
+
+
+
+
+ {!isLoggedIn && }
+ setDrawerOpen(true)}
+ sx={{ ml: 1, mr: 1, p: 1.5 }}
+ >
+
+
+
+ setDrawerOpen(false)}
+ sx={{ zIndex: 13000 }}
+ PaperProps={{ sx: { top: 0, height: "100%" } }}
+ >
+
+
+ {drawerMenu.map((item) => (
+
+ {
+ item.onClick();
+ setDrawerOpen(false);
+ }}
+ selected={isActive(
+ item.label === "마이페이지"
+ ? "/profile"
+ : item.label === "프로젝트 찾기"
+ ? "/project"
+ : "/project/insert"
+ )}
+ >
+
+
+
+ ))}
+ {user && (
+ <>
+
+
+ {
+ await auth.signOut();
+ setDrawerOpen(false);
+ navigate("/");
+ }}
+ >
+
+
+
+ >
+ )}
+
+
+
+ >
+ ) : (
+ // 데스크탑 헤더 기존 구조 유지
+ <>
+
+
+ navigate("/project")}
+ $isActive={isActive("/project")}
+ >
+ 프로젝트 찾기
+
+ navigate("/project/insert")}
+ $isActive={isActive("/project/insert")}
+ >
+ 프로젝트 등록
+
+
+
+ {user ? (
+
+ navigate("/profile")}
+ />
+
+
+ ) : (
+
+ )}
+
+ >
+ )}
);
@@ -87,24 +196,17 @@ const HeaderContent = styled(Box)(() => ({
margin: "0 auto",
}));
-const LeftSection = styled(Box)(() => ({
- display: "flex",
- alignItems: "center",
- flex: "0 0 auto",
-}));
-
const CenterSection = styled(Box)(() => ({
display: "flex",
alignItems: "center",
gap: "2rem",
- flex: "1 1 auto",
justifyContent: "center",
}));
const RightSection = styled(Box)(() => ({
display: "flex",
alignItems: "center",
- flex: "0 0 auto",
+ paddingRight: 16,
}));
const NavButton = styled(Button, {