Skip to content

Commit 88a1095

Browse files
authored
Merge pull request #215 from TTORANG/develop
deploy: 3.0.0 배포 (#214)
2 parents 073e8b8 + 3fe94bd commit 88a1095

File tree

13 files changed

+316
-71
lines changed

13 files changed

+316
-71
lines changed

src/components/common/layout/HeaderButton.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,32 @@ interface HeaderButtonProps {
1111
icon?: ReactNode;
1212
onClick: () => void;
1313
className?: string;
14+
iconOnlyOnMobile?: boolean;
1415
}
1516

1617
/**
1718
* @description 헤더 우측 슬롯에서 공통으로 사용되는 아이콘+텍스트 버튼 컴포넌트
1819
*/
19-
export function HeaderButton({ text, icon, onClick, className }: HeaderButtonProps) {
20+
export function HeaderButton({
21+
text,
22+
icon,
23+
onClick,
24+
className,
25+
iconOnlyOnMobile = false,
26+
}: HeaderButtonProps) {
27+
const shouldHideTextOnMobile = iconOnlyOnMobile && !!icon;
28+
2029
return (
2130
<button
2231
type="button"
2332
onClick={onClick}
2433
className={clsx(
2534
'flex items-center gap-1 text-body-s-bold text-gray-800 cursor-pointer transition-colors hover:text-gray-600',
35+
shouldHideTextOnMobile && 'justify-center',
2636
className,
2737
)}
2838
>
29-
{text}
39+
<span className={clsx(shouldHideTextOnMobile && 'hidden md:inline')}>{text}</span>
3040
{icon}
3141
</button>
3242
);

src/components/common/layout/LoginButton.tsx

Lines changed: 108 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@
66
* 로그인 상태: 사용자 이름 + 프로필 이미지 (클릭 시 로그아웃/회원탈퇴 드롭다운)
77
*/
88
import { useState } from 'react';
9-
import { useNavigate } from 'react-router-dom';
9+
import { useLocation, useNavigate } from 'react-router-dom';
1010

1111
import { useQueryClient } from '@tanstack/react-query';
1212

1313
import { apiClient } from '@/api/client';
1414
import LoginIcon from '@/assets/icons/icon-login.svg?react';
1515
import LogoutIcon from '@/assets/icons/icon-logout.svg?react';
16-
import { Dropdown } from '@/components/common/Dropdown';
16+
import { Popover } from '@/components/common/Popover';
1717
import { UserAvatar } from '@/components/common/UserAvatar';
1818
import { useAuthStore } from '@/stores/authStore';
1919
import { useHomeStore } from '@/stores/homeStore';
20+
import { useThemeStore } from '@/stores/themeStore';
2021
import { isAnonymousEmail } from '@/utils/auth';
2122
import { showToast } from '@/utils/toast';
2223
import { getUserDisplayName } from '@/utils/user';
@@ -27,18 +28,23 @@ import { WithdrawConfirmModal } from './WithdrawConfirmModal';
2728
export function LoginButton() {
2829
const queryClient = useQueryClient();
2930
const navigate = useNavigate();
31+
const { pathname } = useLocation();
3032
const accessToken = useAuthStore((s) => s.accessToken);
3133
const user = useAuthStore((s) => s.user);
3234
const openLoginModal = useAuthStore((s) => s.openLoginModal);
3335
const logout = useAuthStore((s) => s.logout);
3436
const resetHome = useHomeStore((s) => s.reset);
37+
const resolvedTheme = useThemeStore((s) => s.resolvedTheme);
38+
const setTheme = useThemeStore((s) => s.setTheme);
3539

3640
const [isWithdrawModalOpen, setIsWithdrawModalOpen] = useState(false);
3741
const [isWithdrawing, setIsWithdrawing] = useState(false);
3842

3943
const isGuest = !accessToken;
4044
const isAnon = accessToken && isAnonymousEmail(user?.email);
4145
const isSocial = accessToken && user?.email && !isAnonymousEmail(user.email);
46+
const isSlideRoute = /\/slide\/?$/.test(pathname);
47+
const isDark = resolvedTheme === 'dark';
4248

4349
const handleLogout = () => {
4450
logout();
@@ -50,7 +56,14 @@ export function LoginButton() {
5056
};
5157
// 로그인 전 (게스트)
5258
if (isGuest) {
53-
return <HeaderButton text="로그인" icon={<LoginIcon />} onClick={openLoginModal} />;
59+
return (
60+
<HeaderButton
61+
text="로그인"
62+
icon={<LoginIcon />}
63+
onClick={openLoginModal}
64+
iconOnlyOnMobile={isSlideRoute}
65+
/>
66+
);
5467
}
5568

5669
// 익명 사용자
@@ -60,7 +73,14 @@ export function LoginButton() {
6073

6174
// 소셜이 아닌데 여기까지 왔다면(비정상 상태) 방어
6275
if (!isSocial) {
63-
return <HeaderButton text="로그인" icon={<LoginIcon />} onClick={openLoginModal} />;
76+
return (
77+
<HeaderButton
78+
text="로그인"
79+
icon={<LoginIcon />}
80+
onClick={openLoginModal}
81+
iconOnlyOnMobile={isSlideRoute}
82+
/>
83+
);
6484
}
6585

6686
const handleWithdraw = async () => {
@@ -81,40 +101,101 @@ export function LoginButton() {
81101

82102
return (
83103
<>
84-
<Dropdown
104+
<Popover
85105
key={`${accessToken ?? 'guest'}-${user?.id ?? 'nouser'}`}
86106
position="bottom"
87107
align="end"
88108
ariaLabel="사용자 메뉴"
109+
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)]"
89110
trigger={
90111
<button
91112
type="button"
92-
className="flex cursor-pointer items-center gap-2 text-body-s-bold text-gray-800 transition-colors hover:text-gray-600"
113+
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"
93114
>
94-
{displayName}
115+
<span
116+
className={isSlideRoute ? 'hidden md:inline max-w-24 truncate' : 'max-w-24 truncate'}
117+
>
118+
{displayName}
119+
</span>
95120
<UserAvatar src={user.profileImage} alt={displayName} size={24} />
96121
</button>
97122
}
98-
items={[
99-
{
100-
id: 'logout',
101-
label: (
102-
<span className="flex items-center gap-1">
103-
로그아웃
104-
<LogoutIcon className="size-6" />
105-
</span>
106-
),
107-
onClick: handleLogout,
108-
variant: 'danger',
109-
},
110-
{
111-
id: 'withdraw',
112-
label: '회원 탈퇴',
113-
onClick: () => setIsWithdrawModalOpen(true),
114-
variant: 'danger',
115-
},
116-
]}
117-
/>
123+
>
124+
{({ close }) => (
125+
<div className="p-3">
126+
<div className="rounded-lg px-3 py-3">
127+
<p className="text-caption-bold text-gray-600">내 계정</p>
128+
<div className="mt-2 flex items-center gap-3">
129+
<UserAvatar src={user.profileImage} alt={displayName} size={42} />
130+
<div className="min-w-0">
131+
<p className="truncate text-body-m-bold text-gray-800">{displayName}</p>
132+
<p className="truncate text-caption text-gray-600">{user.email}</p>
133+
</div>
134+
</div>
135+
</div>
136+
137+
<div className="mt-2 border-t border-gray-200 pt-2">
138+
<button
139+
type="button"
140+
className="flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-left transition-colors hover:bg-gray-100"
141+
onClick={() => setTheme(isDark ? 'light' : 'dark')}
142+
>
143+
<div>
144+
<p className="text-body-s-bold text-gray-800">테마</p>
145+
<p className="text-caption text-gray-600">
146+
{isDark ? '다크 모드 사용 중' : '라이트 모드 사용 중'}
147+
</p>
148+
</div>
149+
<span
150+
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
151+
isDark ? 'bg-main' : 'bg-gray-400'
152+
}`}
153+
aria-hidden="true"
154+
>
155+
<span
156+
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
157+
isDark ? 'translate-x-4' : 'translate-x-1'
158+
}`}
159+
/>
160+
</span>
161+
</button>
162+
</div>
163+
164+
<div className="mt-1 border-t border-gray-200 pt-2">
165+
<button
166+
type="button"
167+
className="flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-left text-body-s text-gray-800 transition-colors hover:bg-gray-100"
168+
onClick={() => {
169+
close();
170+
handleLogout();
171+
}}
172+
>
173+
<div>
174+
<p className="text-body-s-bold">로그아웃</p>
175+
<p className="text-caption text-gray-600">현재 계정에서 로그아웃합니다.</p>
176+
</div>
177+
<LogoutIcon className="size-5 text-gray-400" />
178+
</button>
179+
</div>
180+
181+
<div className="mt-1 border-t border-gray-200 pt-2">
182+
<button
183+
type="button"
184+
className="w-full rounded-lg px-3 py-2 text-left transition-colors hover:bg-gray-100"
185+
onClick={() => {
186+
close();
187+
setIsWithdrawModalOpen(true);
188+
}}
189+
>
190+
<p className="text-caption-bold text-error">회원 탈퇴</p>
191+
<p className="text-caption text-gray-600">
192+
계정과 데이터가 삭제되며 되돌릴 수 없습니다.
193+
</p>
194+
</button>
195+
</div>
196+
</div>
197+
)}
198+
</Popover>
118199

119200
<WithdrawConfirmModal
120201
isOpen={isWithdrawModalOpen}

src/components/common/layout/PresentationTitleEditor.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* - 클릭하면 Popover 열리고, 입력/저장 가능
77
* - Enter 또는 저장 버튼으로 제출
88
*/
9-
import { useParams } from 'react-router-dom';
9+
import { useLocation, useParams } from 'react-router-dom';
1010

1111
import { usePresentation, useUpdatePresentation } from '@/hooks/queries/usePresentations';
1212
import { showToast } from '@/utils/toast';
@@ -15,33 +15,51 @@ import { TitleEditorPopover } from '../TitleEditorPopover';
1515

1616
interface PresentationTitleEditorProps {
1717
readOnlyContent?: React.ReactNode;
18+
titleOverride?: string;
1819
}
1920

20-
export function PresentationTitleEditor({ readOnlyContent }: PresentationTitleEditorProps) {
21+
export function PresentationTitleEditor({
22+
readOnlyContent,
23+
titleOverride,
24+
}: PresentationTitleEditorProps) {
2125
const { projectId } = useParams<{ projectId: string }>();
26+
const { pathname } = useLocation();
2227
const { data: presentation } = usePresentation(projectId ?? '');
2328

24-
const resolvedTitle = presentation?.title?.trim() ? presentation.title : '내 발표';
29+
const resolvedTitle =
30+
titleOverride?.trim() || (presentation?.title?.trim() ? presentation.title : '내 발표');
31+
const isProjectTabPath =
32+
/^\/[^/]+\/(slide|insight|videos)(\/[^/]+)?$/.test(pathname) || pathname.endsWith('/videos');
33+
const titleClassName = isProjectTabPath ? 'max-w-52 truncate' : undefined;
2534

2635
if (readOnlyContent) {
2736
return (
2837
<TitleEditorPopover
2938
title={resolvedTitle}
3039
readOnlyContent={readOnlyContent}
3140
ariaLabel="발표 정보"
41+
titleClassName={titleClassName}
3242
/>
3343
);
3444
}
3545

36-
return <PresentationTitleEditorEditable projectId={projectId} title={resolvedTitle} />;
46+
return (
47+
<PresentationTitleEditorEditable
48+
projectId={projectId}
49+
title={resolvedTitle}
50+
titleClassName={titleClassName}
51+
/>
52+
);
3753
}
3854

3955
function PresentationTitleEditorEditable({
4056
projectId,
4157
title,
58+
titleClassName,
4259
}: {
4360
projectId?: string;
4461
title: string;
62+
titleClassName?: string;
4563
}) {
4664
const { mutate: updatePresentation, isPending } = useUpdatePresentation();
4765

@@ -74,6 +92,7 @@ function PresentationTitleEditorEditable({
7492
onSave={handleSave}
7593
ariaLabel="발표 이름 변경"
7694
isPending={isPending}
95+
titleClassName={titleClassName}
7796
/>
7897
);
7998
}

src/components/common/layout/ShareButton.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,24 @@
22
* @file ShareButton.tsx
33
* @description 공유 모달을 여는 헤더 버튼
44
*/
5+
import { useLocation } from 'react-router-dom';
6+
57
import ShareIcon from '@/assets/icons/icon-share.svg?react';
68
import { useShareStore } from '@/stores/shareStore';
79

810
import { HeaderButton } from './HeaderButton';
911

1012
export function ShareButton() {
1113
const openShareModal = useShareStore((s) => s.openShareModal);
14+
const { pathname } = useLocation();
15+
const isSlideRoute = /\/slide\/?$/.test(pathname);
1216

13-
return <HeaderButton text="공유" icon={<ShareIcon />} onClick={openShareModal} />;
17+
return (
18+
<HeaderButton
19+
text="공유"
20+
icon={<ShareIcon />}
21+
onClick={openShareModal}
22+
iconOnlyOnMobile={isSlideRoute}
23+
/>
24+
);
1425
}

src/components/feedback/FeedbackHeaderCenter.tsx

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,9 @@
1-
import { useParams } from 'react-router-dom';
2-
31
import InfoIcon from '@/assets/icons/icon-info.svg?react';
42
import { Popover } from '@/components/common';
5-
import { usePresentation } from '@/hooks/queries/usePresentations';
6-
import dayjs from '@/utils/dayjs';
3+
import { useFeedbackHeaderInfo } from '@/hooks/useFeedbackHeaderInfo';
74

85
export default function FeedbackHeaderCenter() {
9-
const { projectId } = useParams<{ projectId: string }>();
10-
11-
const { data: presentation } = usePresentation(projectId ?? '');
12-
const title = presentation?.title?.trim() ? presentation.title : '내 발표';
13-
const postedAt = presentation?.updatedAt
14-
? dayjs(presentation.updatedAt).format('YYYY.MM.DD HH:mm:ss')
15-
: '-';
16-
const publisherName = presentation?.userName ?? '알 수 없음';
6+
const { title, postedAt, publisherName } = useFeedbackHeaderInfo();
177

188
return (
199
<div className="flex md:hidden items-center">

src/components/feedback/FeedbackHeaderLeft.tsx

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
1-
import { useParams } from 'react-router-dom';
2-
31
import { Logo, PresentationTitleEditor } from '@/components/common';
4-
import { usePresentation } from '@/hooks/queries/usePresentations';
5-
import dayjs from '@/utils/dayjs';
2+
import { useFeedbackHeaderInfo } from '@/hooks/useFeedbackHeaderInfo';
63

74
export default function FeedbackHeaderLeft() {
8-
const { projectId } = useParams<{ projectId: string }>();
9-
10-
const { data: presentation } = usePresentation(projectId ?? '');
11-
const postedAt = presentation?.updatedAt
12-
? dayjs(presentation.updatedAt).format('YYYY.MM.DD HH:mm:ss')
13-
: '-';
14-
const publisherName = presentation?.userName ?? '알 수 없음';
5+
const { title, postedAt, publisherName } = useFeedbackHeaderInfo();
156

167
return (
178
<>
189
<Logo />
1910
<PresentationTitleEditor
11+
titleOverride={title}
2012
readOnlyContent={
2113
<div className="grid grid-cols-[6.5rem_1fr] gap-x-5 gap-y-3 text-body-m text-gray-800">
2214
<span className="text-gray-600 text-body-s-bold">게시자</span>

src/components/feedback/ScriptPanel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ export default function ScriptPanel({
2222
return (
2323
<div id={id} role="tabpanel" aria-labelledby={ariaLabelledby} className={className}>
2424
<SlideTitle fallbackTitle={fallbackTitle} />
25-
<div className="mt-3 bg-gray-200 rounded-lg px-4 py-3 h-48 overflow-y-auto">
25+
<div className="mt-3 rounded-2xl border border-gray-200 bg-gray-200 px-4 py-3 h-48 overflow-y-auto pb-4">
2626
<p
27-
className={`text-body-s ${script ? 'text-black' : 'text-gray-600'}`}
28-
style={{ whiteSpace: 'pre-line' }}
27+
className={`text-body-s leading-relaxed wrap-break-word ${script ? 'text-black' : 'text-gray-600'}`}
28+
style={{ whiteSpace: 'pre-wrap' }}
2929
>
3030
{script || '대본이 없습니다.'}
3131
</p>

0 commit comments

Comments
 (0)