diff --git a/src/components/common/layout/HeaderButton.tsx b/src/components/common/layout/HeaderButton.tsx index 690d6da4..08b57bc4 100644 --- a/src/components/common/layout/HeaderButton.tsx +++ b/src/components/common/layout/HeaderButton.tsx @@ -11,22 +11,32 @@ interface HeaderButtonProps { icon?: ReactNode; onClick: () => void; className?: string; + iconOnlyOnMobile?: boolean; } /** * @description 헤더 우측 슬롯에서 공통으로 사용되는 아이콘+텍스트 버튼 컴포넌트 */ -export function HeaderButton({ text, icon, onClick, className }: HeaderButtonProps) { +export function HeaderButton({ + text, + icon, + onClick, + className, + iconOnlyOnMobile = false, +}: HeaderButtonProps) { + const shouldHideTextOnMobile = iconOnlyOnMobile && !!icon; + return ( ); diff --git a/src/components/common/layout/LoginButton.tsx b/src/components/common/layout/LoginButton.tsx index bc57a88a..52fd208f 100644 --- a/src/components/common/layout/LoginButton.tsx +++ b/src/components/common/layout/LoginButton.tsx @@ -6,17 +6,18 @@ * 로그인 상태: 사용자 이름 + 프로필 이미지 (클릭 시 로그아웃/회원탈퇴 드롭다운) */ import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; import { apiClient } from '@/api/client'; import LoginIcon from '@/assets/icons/icon-login.svg?react'; import LogoutIcon from '@/assets/icons/icon-logout.svg?react'; -import { Dropdown } from '@/components/common/Dropdown'; +import { Popover } from '@/components/common/Popover'; import { UserAvatar } from '@/components/common/UserAvatar'; import { useAuthStore } from '@/stores/authStore'; import { useHomeStore } from '@/stores/homeStore'; +import { useThemeStore } from '@/stores/themeStore'; import { isAnonymousEmail } from '@/utils/auth'; import { showToast } from '@/utils/toast'; import { getUserDisplayName } from '@/utils/user'; @@ -27,11 +28,14 @@ import { WithdrawConfirmModal } from './WithdrawConfirmModal'; export function LoginButton() { const queryClient = useQueryClient(); const navigate = useNavigate(); + const { pathname } = useLocation(); const accessToken = useAuthStore((s) => s.accessToken); const user = useAuthStore((s) => s.user); const openLoginModal = useAuthStore((s) => s.openLoginModal); const logout = useAuthStore((s) => s.logout); const resetHome = useHomeStore((s) => s.reset); + const resolvedTheme = useThemeStore((s) => s.resolvedTheme); + const setTheme = useThemeStore((s) => s.setTheme); const [isWithdrawModalOpen, setIsWithdrawModalOpen] = useState(false); const [isWithdrawing, setIsWithdrawing] = useState(false); @@ -39,6 +43,8 @@ export function LoginButton() { const isGuest = !accessToken; const isAnon = accessToken && isAnonymousEmail(user?.email); const isSocial = accessToken && user?.email && !isAnonymousEmail(user.email); + const isSlideRoute = /\/slide\/?$/.test(pathname); + const isDark = resolvedTheme === 'dark'; const handleLogout = () => { logout(); @@ -50,7 +56,14 @@ export function LoginButton() { }; // 로그인 전 (게스트) if (isGuest) { - return } onClick={openLoginModal} />; + return ( + } + onClick={openLoginModal} + iconOnlyOnMobile={isSlideRoute} + /> + ); } // 익명 사용자 @@ -60,7 +73,14 @@ export function LoginButton() { // 소셜이 아닌데 여기까지 왔다면(비정상 상태) 방어 if (!isSocial) { - return } onClick={openLoginModal} />; + return ( + } + onClick={openLoginModal} + iconOnlyOnMobile={isSlideRoute} + /> + ); } const handleWithdraw = async () => { @@ -81,40 +101,101 @@ export function LoginButton() { return ( <> - - {displayName} + + {displayName} + } - items={[ - { - id: 'logout', - label: ( - - 로그아웃 - - - ), - onClick: handleLogout, - variant: 'danger', - }, - { - id: 'withdraw', - label: '회원 탈퇴', - onClick: () => setIsWithdrawModalOpen(true), - variant: 'danger', - }, - ]} - /> + > + {({ close }) => ( +
+
+

내 계정

+
+ +
+

{displayName}

+

{user.email}

+
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+ )} + (); + const { pathname } = useLocation(); const { data: presentation } = usePresentation(projectId ?? ''); - const resolvedTitle = presentation?.title?.trim() ? presentation.title : '내 발표'; + const resolvedTitle = + titleOverride?.trim() || (presentation?.title?.trim() ? presentation.title : '내 발표'); + const isProjectTabPath = + /^\/[^/]+\/(slide|insight|videos)(\/[^/]+)?$/.test(pathname) || pathname.endsWith('/videos'); + const titleClassName = isProjectTabPath ? 'max-w-52 truncate' : undefined; if (readOnlyContent) { return ( @@ -29,19 +38,28 @@ export function PresentationTitleEditor({ readOnlyContent }: PresentationTitleEd title={resolvedTitle} readOnlyContent={readOnlyContent} ariaLabel="발표 정보" + titleClassName={titleClassName} /> ); } - return ; + return ( + + ); } function PresentationTitleEditorEditable({ projectId, title, + titleClassName, }: { projectId?: string; title: string; + titleClassName?: string; }) { const { mutate: updatePresentation, isPending } = useUpdatePresentation(); @@ -74,6 +92,7 @@ function PresentationTitleEditorEditable({ onSave={handleSave} ariaLabel="발표 이름 변경" isPending={isPending} + titleClassName={titleClassName} /> ); } diff --git a/src/components/common/layout/ShareButton.tsx b/src/components/common/layout/ShareButton.tsx index e1a1873a..cdd48e87 100644 --- a/src/components/common/layout/ShareButton.tsx +++ b/src/components/common/layout/ShareButton.tsx @@ -2,6 +2,8 @@ * @file ShareButton.tsx * @description 공유 모달을 여는 헤더 버튼 */ +import { useLocation } from 'react-router-dom'; + import ShareIcon from '@/assets/icons/icon-share.svg?react'; import { useShareStore } from '@/stores/shareStore'; @@ -9,6 +11,15 @@ import { HeaderButton } from './HeaderButton'; export function ShareButton() { const openShareModal = useShareStore((s) => s.openShareModal); + const { pathname } = useLocation(); + const isSlideRoute = /\/slide\/?$/.test(pathname); - return } onClick={openShareModal} />; + return ( + } + onClick={openShareModal} + iconOnlyOnMobile={isSlideRoute} + /> + ); } diff --git a/src/components/feedback/FeedbackHeaderCenter.tsx b/src/components/feedback/FeedbackHeaderCenter.tsx index b03c90f7..e0e666e4 100644 --- a/src/components/feedback/FeedbackHeaderCenter.tsx +++ b/src/components/feedback/FeedbackHeaderCenter.tsx @@ -1,19 +1,9 @@ -import { useParams } from 'react-router-dom'; - import InfoIcon from '@/assets/icons/icon-info.svg?react'; import { Popover } from '@/components/common'; -import { usePresentation } from '@/hooks/queries/usePresentations'; -import dayjs from '@/utils/dayjs'; +import { useFeedbackHeaderInfo } from '@/hooks/useFeedbackHeaderInfo'; export default function FeedbackHeaderCenter() { - const { projectId } = useParams<{ projectId: string }>(); - - const { data: presentation } = usePresentation(projectId ?? ''); - const title = presentation?.title?.trim() ? presentation.title : '내 발표'; - const postedAt = presentation?.updatedAt - ? dayjs(presentation.updatedAt).format('YYYY.MM.DD HH:mm:ss') - : '-'; - const publisherName = presentation?.userName ?? '알 수 없음'; + const { title, postedAt, publisherName } = useFeedbackHeaderInfo(); return (
diff --git a/src/components/feedback/FeedbackHeaderLeft.tsx b/src/components/feedback/FeedbackHeaderLeft.tsx index 727f33f8..8e8e1107 100644 --- a/src/components/feedback/FeedbackHeaderLeft.tsx +++ b/src/components/feedback/FeedbackHeaderLeft.tsx @@ -1,22 +1,14 @@ -import { useParams } from 'react-router-dom'; - import { Logo, PresentationTitleEditor } from '@/components/common'; -import { usePresentation } from '@/hooks/queries/usePresentations'; -import dayjs from '@/utils/dayjs'; +import { useFeedbackHeaderInfo } from '@/hooks/useFeedbackHeaderInfo'; export default function FeedbackHeaderLeft() { - const { projectId } = useParams<{ projectId: string }>(); - - const { data: presentation } = usePresentation(projectId ?? ''); - const postedAt = presentation?.updatedAt - ? dayjs(presentation.updatedAt).format('YYYY.MM.DD HH:mm:ss') - : '-'; - const publisherName = presentation?.userName ?? '알 수 없음'; + const { title, postedAt, publisherName } = useFeedbackHeaderInfo(); return ( <> 게시자 diff --git a/src/components/feedback/ScriptPanel.tsx b/src/components/feedback/ScriptPanel.tsx index f0e8f1e6..60fa2ed3 100644 --- a/src/components/feedback/ScriptPanel.tsx +++ b/src/components/feedback/ScriptPanel.tsx @@ -22,10 +22,10 @@ export default function ScriptPanel({ return (
-
+

{script || '대본이 없습니다.'}

diff --git a/src/components/feedback/slide/SlideInfoPanel.tsx b/src/components/feedback/slide/SlideInfoPanel.tsx index 26aef70f..d21289ba 100644 --- a/src/components/feedback/slide/SlideInfoPanel.tsx +++ b/src/components/feedback/slide/SlideInfoPanel.tsx @@ -42,10 +42,10 @@ export default function SlideInfoPanel({ />
-
+

{script || '대본이 없습니다.'}

diff --git a/src/components/share/share-modal/ShareModal.tsx b/src/components/share/share-modal/ShareModal.tsx index e7e73637..7c462b65 100644 --- a/src/components/share/share-modal/ShareModal.tsx +++ b/src/components/share/share-modal/ShareModal.tsx @@ -14,7 +14,7 @@ import { Modal } from '@/components/common/Modal'; import { useCreateShareLink, useShareableVideos } from '@/hooks/queries/useShares'; import { type ShareType, useShareStore } from '@/stores/shareStore'; import { formatTimestamp } from '@/utils/format'; -import { shareToFacebook, shareToInstagram, shareToKakao, shareToX } from '@/utils/snsShare'; +import { shareQrToInstagram, shareToFacebook, shareToKakao, shareToX } from '@/utils/snsShare'; import { showToast } from '@/utils/toast'; const KAKAO_JS_KEY = import.meta.env?.VITE_KAKAO_JS_KEY ?? ''; @@ -337,7 +337,7 @@ export function ShareModal() {