From 9f4aeb499ccc4f25d4efedd934e8c0619f1580fd Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Thu, 12 Feb 2026 15:24:31 +0900 Subject: [PATCH 1/8] =?UTF-8?q?design:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=8C=9D=EC=98=A4=EB=B2=84=20=EA=B5=AC=ED=98=84=20(#199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/layout/LoginButton.tsx | 93 ++++++++++++++------ 1 file changed, 67 insertions(+), 26 deletions(-) diff --git a/src/components/common/layout/LoginButton.tsx b/src/components/common/layout/LoginButton.tsx index bc57a88a..50d71a59 100644 --- a/src/components/common/layout/LoginButton.tsx +++ b/src/components/common/layout/LoginButton.tsx @@ -13,7 +13,7 @@ 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'; @@ -81,40 +81,81 @@ export function LoginButton() { return ( <> - ( - } - items={[ - { - id: 'logout', - label: ( - - 로그아웃 - - - ), - onClick: handleLogout, - variant: 'danger', - }, - { - id: 'withdraw', - label: '회원 탈퇴', - onClick: () => setIsWithdrawModalOpen(true), - variant: 'danger', - }, - ]} - /> + )} + > + {({ close }) => ( +
+
+

내 계정

+
+ +
+

{displayName}

+

{user.email}

+
+
+
+ +
+ +
+ +
+ +
+
+ )} + Date: Thu, 12 Feb 2026 15:28:52 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=95=EB=B3=B4=20=EC=97=B0=EB=8F=99=20(#199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/layout/PresentationTitleEditor.tsx | 9 ++- .../feedback/FeedbackHeaderCenter.tsx | 14 +--- .../feedback/FeedbackHeaderLeft.tsx | 14 +--- src/hooks/useFeedbackHeaderInfo.ts | 67 +++++++++++++++++++ 4 files changed, 79 insertions(+), 25 deletions(-) create mode 100644 src/hooks/useFeedbackHeaderInfo.ts diff --git a/src/components/common/layout/PresentationTitleEditor.tsx b/src/components/common/layout/PresentationTitleEditor.tsx index e93f6dd7..dc107359 100644 --- a/src/components/common/layout/PresentationTitleEditor.tsx +++ b/src/components/common/layout/PresentationTitleEditor.tsx @@ -15,13 +15,18 @@ import { TitleEditorPopover } from '../TitleEditorPopover'; interface PresentationTitleEditorProps { readOnlyContent?: React.ReactNode; + titleOverride?: string; } -export function PresentationTitleEditor({ readOnlyContent }: PresentationTitleEditorProps) { +export function PresentationTitleEditor({ + readOnlyContent, + titleOverride, +}: PresentationTitleEditorProps) { const { projectId } = useParams<{ projectId: string }>(); const { data: presentation } = usePresentation(projectId ?? ''); - const resolvedTitle = presentation?.title?.trim() ? presentation.title : '내 발표'; + const resolvedTitle = + titleOverride?.trim() || (presentation?.title?.trim() ? presentation.title : '내 발표'); if (readOnlyContent) { return ( 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/hooks/useFeedbackHeaderInfo.ts b/src/hooks/useFeedbackHeaderInfo.ts new file mode 100644 index 00000000..01c76c55 --- /dev/null +++ b/src/hooks/useFeedbackHeaderInfo.ts @@ -0,0 +1,67 @@ +import { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; + +import { usePresentation } from '@/hooks/queries/usePresentations'; +import { useSharedContent } from '@/hooks/queries/useShares'; +import dayjs from '@/utils/dayjs'; + +type SharedContentLike = { + shareInfo?: { createdAt?: string }; + projectContent?: { + title?: string; + postedAt?: string; + publisherName?: string; + userName?: string; + }; + presentation?: { + name?: string; + postedAt?: string; + publisherName?: string; + }; +}; + +const FALLBACK_TITLE = '내 발표'; +const FALLBACK_PUBLISHER = '알 수 없음'; +const FALLBACK_POSTED_AT = '-'; + +function formatPostedAt(value?: string): string { + if (!value) return FALLBACK_POSTED_AT; + const parsed = dayjs(value); + if (!parsed.isValid()) return FALLBACK_POSTED_AT; + return parsed.format('YYYY.MM.DD HH:mm:ss'); +} + +export function useFeedbackHeaderInfo() { + const { projectId, shareToken } = useParams<{ projectId?: string; shareToken?: string }>(); + const { data: presentation } = usePresentation(projectId ?? ''); + const { data: sharedContent } = useSharedContent(shareToken); + + return useMemo(() => { + const shared = (sharedContent ?? null) as SharedContentLike | null; + + const title = + presentation?.title?.trim() || + shared?.projectContent?.title?.trim() || + shared?.presentation?.name?.trim() || + FALLBACK_TITLE; + + const publisherName = + presentation?.userName?.trim() || + shared?.projectContent?.publisherName?.trim() || + shared?.projectContent?.userName?.trim() || + shared?.presentation?.publisherName?.trim() || + FALLBACK_PUBLISHER; + + const postedAtRaw = + presentation?.updatedAt || + shared?.projectContent?.postedAt || + shared?.presentation?.postedAt || + shared?.shareInfo?.createdAt; + + return { + title, + publisherName, + postedAt: formatPostedAt(postedAtRaw), + }; + }, [presentation, sharedContent]); +} From 4cce72d190b18110e0af75292795abd346aa5181 Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Thu, 12 Feb 2026 16:09:33 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20max-width=20=EC=A1=B0=EC=A0=88=20(#199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/layout/PresentationTitleEditor.tsx | 16 ++++++++++++++-- src/components/feedback/ScriptPanel.tsx | 6 +++--- src/components/feedback/slide/SlideInfoPanel.tsx | 6 +++--- src/pages/FeedbackSlidePage.tsx | 6 +++--- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/components/common/layout/PresentationTitleEditor.tsx b/src/components/common/layout/PresentationTitleEditor.tsx index dc107359..644288c9 100644 --- a/src/components/common/layout/PresentationTitleEditor.tsx +++ b/src/components/common/layout/PresentationTitleEditor.tsx @@ -6,7 +6,7 @@ * - 클릭하면 Popover 열리고, 입력/저장 가능 * - Enter 또는 저장 버튼으로 제출 */ -import { useParams } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router-dom'; import { usePresentation, useUpdatePresentation } from '@/hooks/queries/usePresentations'; import { showToast } from '@/utils/toast'; @@ -23,10 +23,12 @@ export function PresentationTitleEditor({ titleOverride, }: PresentationTitleEditorProps) { const { projectId } = useParams<{ projectId: string }>(); + const { pathname } = useLocation(); const { data: presentation } = usePresentation(projectId ?? ''); const resolvedTitle = titleOverride?.trim() || (presentation?.title?.trim() ? presentation.title : '내 발표'); + const titleClassName = pathname.endsWith('/slide') ? 'max-w-52 truncate' : undefined; if (readOnlyContent) { return ( @@ -34,19 +36,28 @@ export function PresentationTitleEditor({ 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(); @@ -79,6 +90,7 @@ function PresentationTitleEditorEditable({ onSave={handleSave} ariaLabel="발표 이름 변경" isPending={isPending} + titleClassName={titleClassName} /> ); } 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/pages/FeedbackSlidePage.tsx b/src/pages/FeedbackSlidePage.tsx index a4bfce7a..1a950a71 100644 --- a/src/pages/FeedbackSlidePage.tsx +++ b/src/pages/FeedbackSlidePage.tsx @@ -158,10 +158,10 @@ export default function FeedbackSlidePage({ scriptTabContent={
-
+

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

From 00c863f8903b47fb2b468666a29f6fff69f9cc59 Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Thu, 12 Feb 2026 16:09:51 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20UI=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=88=98=EC=A0=95=20(#199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/layout/LoginButton.tsx | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/components/common/layout/LoginButton.tsx b/src/components/common/layout/LoginButton.tsx index 50d71a59..4886f1c2 100644 --- a/src/components/common/layout/LoginButton.tsx +++ b/src/components/common/layout/LoginButton.tsx @@ -87,30 +87,19 @@ export function LoginButton() { align="end" ariaLabel="사용자 메뉴" className="mt-2 w-80 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-[0_0.5rem_1.25rem_rgba(0,0,0,0.08)]" - trigger={({ isOpen }) => ( + trigger={ - )} + } > {({ close }) => (
-
+

내 계정

From 79a3c5b2cfadcc83527bc35df80525fa185b9344 Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Thu, 12 Feb 2026 16:11:52 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20max-width=20=EC=A1=B0=EC=A0=88=20(#199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/layout/PresentationTitleEditor.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/common/layout/PresentationTitleEditor.tsx b/src/components/common/layout/PresentationTitleEditor.tsx index 644288c9..1d8ff6a6 100644 --- a/src/components/common/layout/PresentationTitleEditor.tsx +++ b/src/components/common/layout/PresentationTitleEditor.tsx @@ -28,7 +28,9 @@ export function PresentationTitleEditor({ const resolvedTitle = titleOverride?.trim() || (presentation?.title?.trim() ? presentation.title : '내 발표'); - const titleClassName = pathname.endsWith('/slide') ? 'max-w-52 truncate' : undefined; + const isProjectTabPath = + /^\/[^/]+\/(slide|insight|videos)(\/[^/]+)?$/.test(pathname) || pathname.endsWith('/videos'); + const titleClassName = isProjectTabPath ? 'max-w-52 truncate' : undefined; if (readOnlyContent) { return ( From 433ea6fb64d7a89b0cc5ca0991900c5593258366 Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Thu, 12 Feb 2026 18:37:16 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=ED=97=A4=EB=8D=94=20=EB=AA=A8?= =?UTF-8?q?=EB=B0=94=EC=9D=BC=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/layout/HeaderButton.tsx | 14 ++++++++-- src/components/common/layout/LoginButton.tsx | 28 ++++++++++++++++--- src/components/common/layout/ShareButton.tsx | 13 ++++++++- src/router/Router.tsx | 2 +- 4 files changed, 49 insertions(+), 8 deletions(-) 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 4886f1c2..63358677 100644 --- a/src/components/common/layout/LoginButton.tsx +++ b/src/components/common/layout/LoginButton.tsx @@ -6,7 +6,7 @@ * 로그인 상태: 사용자 이름 + 프로필 이미지 (클릭 시 로그아웃/회원탈퇴 드롭다운) */ import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; @@ -27,6 +27,7 @@ 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); @@ -39,6 +40,7 @@ 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 handleLogout = () => { logout(); @@ -50,7 +52,14 @@ export function LoginButton() { }; // 로그인 전 (게스트) if (isGuest) { - return } onClick={openLoginModal} />; + return ( + } + onClick={openLoginModal} + iconOnlyOnMobile={isSlideRoute} + /> + ); } // 익명 사용자 @@ -60,7 +69,14 @@ export function LoginButton() { // 소셜이 아닌데 여기까지 왔다면(비정상 상태) 방어 if (!isSocial) { - return } onClick={openLoginModal} />; + return ( + } + onClick={openLoginModal} + iconOnlyOnMobile={isSlideRoute} + /> + ); } const handleWithdraw = async () => { @@ -92,7 +108,11 @@ export function LoginButton() { type="button" className="flex cursor-pointer items-center gap-2 rounded-full px-2 py-1 text-body-s-bold text-gray-800 transition-colors hover:bg-gray-100" > - {displayName} + + {displayName} + } 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/router/Router.tsx b/src/router/Router.tsx index 397472b6..0a327a50 100644 --- a/src/router/Router.tsx +++ b/src/router/Router.tsx @@ -50,7 +50,7 @@ export const router = createBrowserRouter([ } center={} right={ -
+
From 2f47c0303d254db9b42b393e7e3509694d373a2c Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Thu, 12 Feb 2026 19:23:52 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=EB=8B=A4=ED=81=AC=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=ED=86=A0=EA=B8=80=20=EC=B6=94=EA=B0=80=20(#199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/layout/LoginButton.tsx | 31 ++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/components/common/layout/LoginButton.tsx b/src/components/common/layout/LoginButton.tsx index 63358677..52fd208f 100644 --- a/src/components/common/layout/LoginButton.tsx +++ b/src/components/common/layout/LoginButton.tsx @@ -17,6 +17,7 @@ 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'; @@ -33,6 +34,8 @@ export function LoginButton() { 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); @@ -41,6 +44,7 @@ export function LoginButton() { 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(); @@ -131,6 +135,33 @@ export function LoginButton() {
+ +
+ +