Skip to content
4 changes: 2 additions & 2 deletions apps/web/src/assets/svgs/retrospect/ic_close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
221 changes: 171 additions & 50 deletions apps/web/src/component/common/LocalNavigationBar/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,81 @@
import { css } from "@emotion/react";
import { useState, useRef, useEffect } from "react";
import Cookies from "js-cookie";

import { Icon } from "../Icon";
import { Typography } from "../typography";
import { AcountSettingsModal } from "../Modal/UserSetting/AcountSettingsModal";
import { FeedbackModal } from "../Modal/UserSetting/FeedbackModal";
import { LogoutModal } from "../Modal/UserSetting/LogoutModal";
import { useNavigation } from "./context/NavigationContext";
import { UserProfileDropdown } from "./UserProfileDropdown";

import { DESIGN_TOKEN_COLOR } from "@/style/designTokens";
import { useAtom } from "jotai";
import { authAtom } from "@/store/auth/authAtom";
import { usePostSignOut } from "@/hooks/api/login/usePostSignOut";

export default function Footer() {
const { isCollapsed } = useNavigation();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isAccountSettingsModalOpen, setIsAccountSettingsModalOpen] = useState(false);
const [isFeedbackModalOpen, setIsFeedbackModalOpen] = useState(false);
const [isLogoutModalOpen, setIsLogoutModalOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const [{ name, imageUrl }] = useAtom(authAtom);
const { mutate: signOut } = usePostSignOut();
const memberId = Cookies.get("memberId");

// 외부 클릭시 드롭다운 닫기
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추후에 제가 만든 useClickOutside 훅 사용하셔도 좋을 것 같아요!

if (
dropdownRef.current &&
buttonRef.current &&
!dropdownRef.current.contains(event.target as Node) &&
!buttonRef.current.contains(event.target as Node)
) {
setIsDropdownOpen(false);
}
};

if (isDropdownOpen) {
document.addEventListener("mousedown", handleClickOutside);
}

return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isDropdownOpen]);

const handleProfileClick = () => {
setIsDropdownOpen(!isDropdownOpen);
};

const handleAccountSettings = () => {
setIsAccountSettingsModalOpen(true);
setIsDropdownOpen(false);
};

const handleAccountSettingsClose = () => {
setIsAccountSettingsModalOpen(false);
};

const handleFeedback = () => {
setIsFeedbackModalOpen(true);
setIsDropdownOpen(false);
};

const handleHelp = () => {
// 도움말 로직
setIsDropdownOpen(false);
};

const handleLogout = () => {
setIsLogoutModalOpen(true);
setIsDropdownOpen(false);
};

return (
<footer
Expand All @@ -16,6 +84,7 @@ export default function Footer() {
display: flex;
align-items: center;
gap: 0.8rem;
position: relative;
transition:
padding 0.3s ease-in-out,
gap 0.3s ease-in-out;
Expand All @@ -32,69 +101,103 @@ export default function Footer() {
`}
>
{/* ---------- 프로필 이미지/이름 ---------- */}
<button
<div
css={css`
display: flex;
justify-content: center;
align-items: center;
gap: 1.2rem;
padding: 0rem 0.4rem;
border: none;
background: transparent;
border-radius: 0.8rem;
cursor: pointer;
transition:
background-color 0.2s ease-in-out,
width 0.3s ease-in-out,
height 0.3s ease-in-out;

${isCollapsed
? css`
width: auto;
height: 3.2rem;
`
: css`
width: 100%;
height: 3.6rem;
`}

&:focus {
background-color: ${DESIGN_TOKEN_COLOR.gray100};
}

&:hover {
background-color: ${DESIGN_TOKEN_COLOR.gray100};
}
position: relative;
`}
>
<Icon icon="basicProfile" size={2.4} />
<UserProfileDropdown
ref={dropdownRef}
isOpen={isDropdownOpen}
onAccountSettings={handleAccountSettings}
onFeedback={handleFeedback}
onHelp={handleHelp}
onLogout={handleLogout}
/>

<Typography
variant="body12Medium"
color="gray700"
<button
ref={buttonRef}
css={css`
overflow: hidden;
white-space: nowrap;
transition: opacity 0.3s ease-in-out;
display: flex;
justify-content: center;
align-items: center;
gap: 1.2rem;
padding: 0rem 0.4rem;
border: none;
background: transparent;
border-radius: 0.8rem;
cursor: pointer;
transition:
background-color 0.2s ease-in-out,
width 0.3s ease-in-out,
height 0.3s ease-in-out;

${isCollapsed
? css`
display: none;
width: 0;
opacity: 0;
visibility: hidden;
width: auto;
height: 3.2rem;
`
: css`
display: block;
width: auto;
opacity: 1;
visibility: visible;
width: 100%;
height: 3.6rem;
`}

&:focus {
background-color: ${DESIGN_TOKEN_COLOR.gray100};
}

&:hover {
background-color: ${DESIGN_TOKEN_COLOR.gray100};
}

${isDropdownOpen &&
css`
background-color: ${DESIGN_TOKEN_COLOR.gray100};
`}
`}
onClick={handleProfileClick}
>
{"홍길동"}
</Typography>
</button>
{imageUrl ? (
<img
src={imageUrl}
css={css`
width: 2.4rem;
height: 2.4rem;
border-radius: 100%;
object-fit: cover;
`}
/>
) : (
<Icon icon="basicProfile" size={2.4} />
)}

<Typography
variant="body12Medium"
color="gray700"
css={css`
overflow: hidden;
white-space: nowrap;
transition: opacity 0.3s ease-in-out;

${isCollapsed
? css`
display: none;
width: 0;
opacity: 0;
visibility: hidden;
`
: css`
display: block;
width: auto;
opacity: 1;
visibility: visible;
`}
`}
>
{name}
</Typography>
</button>
</div>

{/* ---------- 구분선 ---------- */}
{!isCollapsed && (
Expand Down Expand Up @@ -172,6 +275,24 @@ export default function Footer() {
헬프 센터
</Typography>
</button>

{/* 계정설정 모달 */}
<AcountSettingsModal isOpen={isAccountSettingsModalOpen} onClose={handleAccountSettingsClose} />

{/* 평가 및 피드백 모달 */}
<FeedbackModal isOpen={isFeedbackModalOpen} onClose={() => setIsFeedbackModalOpen(false)} />

{/* 로그아웃 모달 */}
<LogoutModal
isOpen={isLogoutModalOpen}
onClose={() => setIsLogoutModalOpen(false)}
onConfirm={() => {
if (memberId) {
signOut({ memberId: memberId });
}
setIsLogoutModalOpen(false);
}}
/>
</footer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { css } from "@emotion/react";
import { forwardRef } from "react";

import { DESIGN_TOKEN_COLOR } from "@/style/designTokens";

type UserProfileDropdownProps = {
isOpen: boolean;
onAccountSettings: () => void;
onFeedback: () => void;
onHelp: () => void;
onLogout: () => void;
};

export const UserProfileDropdown = forwardRef<HTMLDivElement, UserProfileDropdownProps>(
({ isOpen, onAccountSettings, onFeedback, onHelp, onLogout }, ref) => {
if (!isOpen) return null;

return (
<div
ref={ref}
css={css`
position: absolute;
bottom: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid ${DESIGN_TOKEN_COLOR.gray200};
border-radius: 0.8rem;
box-shadow: 0 0.4rem 1.2rem rgba(0, 0, 0, 0.1);
margin-bottom: 0.8rem;
z-index: 1000;
overflow: hidden;
animation: slideUp 0.2s ease-out;
min-width: 16.5rem;

@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`}
>
<DropdownItem onClick={onAccountSettings}>계정 설정</DropdownItem>
<DropdownItem onClick={onFeedback}>평가 및 피드백</DropdownItem>
<DropdownItem onClick={onHelp}>도움말</DropdownItem>
<DropdownItem onClick={onLogout}>로그아웃</DropdownItem>
</div>
);
},
);

UserProfileDropdown.displayName = "UserProfileDropdown";

type DropdownItemProps = {
children: React.ReactNode;
onClick: () => void;
};

const DropdownItem = ({ children, onClick }: DropdownItemProps) => {
return (
<button
css={css`
width: 100%;
padding: 0.8rem 2rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
border: none;
background: transparent;
text-align: left;
cursor: pointer;
font-size: 1.4rem;
color: ${DESIGN_TOKEN_COLOR.gray800};
transition: background-color 0.2s ease;

&:hover {
background-color: ${DESIGN_TOKEN_COLOR.gray100};
}
`}
onClick={onClick}
>
{children}
</button>
);
};
Loading