From b0089a713268070b288a7e494d9cf4d06fcd38d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A1=9C=EB=A1=9C?= Date: Sun, 22 Feb 2026 13:01:56 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=EC=8A=AC=EB=9D=BC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=20title=20nullable=20=ED=83=80=EC=9E=85=20=EC=A0=95=EB=A6=AC?= =?UTF-8?q?=20(#315)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/dto/analytics.dto.ts | 6 +++--- src/api/dto/scripts.dto.ts | 1 + src/api/dto/slides.dto.ts | 6 +++--- src/api/dto/video.dto.ts | 1 + src/components/slide/SlideViewer.tsx | 3 ++- src/hooks/UseSlideSelectors.test.tsx | 10 ++++++++-- src/hooks/useSlideSelectors.ts | 3 ++- src/types/share.ts | 2 +- src/types/slide.ts | 2 +- 9 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/api/dto/analytics.dto.ts b/src/api/dto/analytics.dto.ts index 78f54555..8c78c3f5 100644 --- a/src/api/dto/analytics.dto.ts +++ b/src/api/dto/analytics.dto.ts @@ -51,7 +51,7 @@ export interface RecordExitRequestDto { export interface SlideAnalyticsDto { slideId: string; slideNum: number; - title: string; + title: string | null; viewCount: number; exitCount: number; exitRate: number; @@ -117,7 +117,7 @@ export interface ReadPresentationAnalyticsSummaryDto { export interface SlideRetentionDto { slideId: string; slideNum: number; - title: string; + title: string | null; sessionCount: number; retentionRate: number; } @@ -185,7 +185,7 @@ export interface RecentCommentUserDto { export interface RecentCommentSlideDto { slideId: string; slideNum: number; - title: string; + title: string | null; imageUrl: string; } diff --git a/src/api/dto/scripts.dto.ts b/src/api/dto/scripts.dto.ts index f896f669..509d9139 100644 --- a/src/api/dto/scripts.dto.ts +++ b/src/api/dto/scripts.dto.ts @@ -43,6 +43,7 @@ export interface RestoreScriptResponseDto { */ export interface ProjectScriptItemDto { slideId: string; + title?: string | null; scriptText: string; } diff --git a/src/api/dto/slides.dto.ts b/src/api/dto/slides.dto.ts index 969d28df..2b34447b 100644 --- a/src/api/dto/slides.dto.ts +++ b/src/api/dto/slides.dto.ts @@ -4,7 +4,7 @@ export interface CreateSlideResponseDto { slideId: string; projectId: string; - title: string; + title: string | null; slideNum: number; imageUrl: string; createdAt: string; @@ -24,7 +24,7 @@ export interface UpdateSlideTitleRequestDto { export interface GetSlideResponseDto { slideId: string; projectId: string; - title: string; + title: string | null; slideNum: number; imageUrl: string; prevSlideId: string | null; @@ -37,7 +37,7 @@ export interface GetSlideResponseDto { */ export interface UpdateSlideResponseDto { slideId: string; - title: string; + title: string | null; slideNum: number; imageUrl: string; updatedAt: string; diff --git a/src/api/dto/video.dto.ts b/src/api/dto/video.dto.ts index 4b92e968..c7e663e5 100644 --- a/src/api/dto/video.dto.ts +++ b/src/api/dto/video.dto.ts @@ -227,6 +227,7 @@ export interface ReadVideoCommentsAllResponseDto { */ export interface VideoSlideTimelineItemDto { slideId: string; + title: string | null; timestampMs: number; } diff --git a/src/components/slide/SlideViewer.tsx b/src/components/slide/SlideViewer.tsx index 18e606c3..d8e37034 100644 --- a/src/components/slide/SlideViewer.tsx +++ b/src/components/slide/SlideViewer.tsx @@ -17,6 +17,7 @@ interface SlideViewerProps { export default function SlideViewer({ isLoading }: SlideViewerProps) { const thumb = useSlideThumb(); const title = useSlideTitle(); + const imageAlt = title ?? '슬라이드'; return (
@@ -30,7 +31,7 @@ export default function SlideViewer({ isLoading }: SlideViewerProps) {
{ expect(result.current).toBe(''); }); - it('useSlideTitle returns empty string', () => { + it('useSlideTitle returns null', () => { const { result } = renderHook(() => useSlideTitle()); - expect(result.current).toBe(''); + expect(result.current).toBeNull(); }); it('useSlideThumb returns empty string', () => { @@ -70,6 +70,12 @@ describe('useSlideSelectors', () => { expect(result.current).toBe('Test Title'); }); + it('useSlideTitle preserves null title', () => { + useSlideStore.getState().initSlide(createMockSlide({ title: null })); + const { result } = renderHook(() => useSlideTitle()); + expect(result.current).toBeNull(); + }); + it('useSlideThumb returns imageUrl', () => { const { result } = renderHook(() => useSlideThumb()); expect(result.current).toBe('https://img.url'); diff --git a/src/hooks/useSlideSelectors.ts b/src/hooks/useSlideSelectors.ts index 4acc5be9..177bc477 100644 --- a/src/hooks/useSlideSelectors.ts +++ b/src/hooks/useSlideSelectors.ts @@ -15,7 +15,8 @@ const EMPTY_COMMENTS: Comment[] = []; export const useSlideId = () => useSlideStore((state) => state.slide?.slideId ?? ''); /** 슬라이드 제목 구독 */ -export const useSlideTitle = () => useSlideStore((state) => state.slide?.title ?? ''); +export const useSlideTitle = () => + useSlideStore((state) => (state.slide ? state.slide.title : null)); /** 슬라이드 썸네일 구독 */ export const useSlideThumb = () => useSlideStore((state) => state.slide?.imageUrl ?? ''); diff --git a/src/types/share.ts b/src/types/share.ts index 309b3550..8c8ab572 100644 --- a/src/types/share.ts +++ b/src/types/share.ts @@ -58,7 +58,7 @@ export type ShareableVideosResponse = ApiResponse; export interface SharedPresentationSlide { slideId: string; slideNum: number; - title: string; + title: string | null; imageUrl: string; scriptText: string; timestampMs: number; diff --git a/src/types/slide.ts b/src/types/slide.ts index d8156640..75b9b00b 100644 --- a/src/types/slide.ts +++ b/src/types/slide.ts @@ -10,7 +10,7 @@ export interface SlideListItem { script: string; slideId: string; projectId: string; - title: string; + title: string | null; slideNum: number; imageUrl: string; createdAt: string; From eea061cf0ba67b702a8e4e815a40abd78eebedd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A1=9C=EB=A1=9C?= Date: Sun, 22 Feb 2026 13:02:14 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=EC=8A=AC=EB=9D=BC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=20=EC=A0=9C=EB=AA=A9=20=EB=A0=8C=EB=8D=94=EB=A7=81/=ED=8E=B8?= =?UTF-8?q?=EC=A7=91=20=EB=8F=99=EC=9E=91=20=ED=86=B5=EC=9D=BC=20(#315)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/comment/Comment.tsx | 3 +- src/components/common/TitleEditorPopover.tsx | 16 ++-- src/components/feedback/SlideViewer.tsx | 7 +- .../feedback/slide/SlideInfoPanel.tsx | 3 +- .../feedback/video/SlideWebcamStage.tsx | 3 +- .../insight/DropOffAnalysisSection.tsx | 3 +- .../insight/RecentCommentsSection.test.tsx | 46 ++++++++++++ .../insight/RecentCommentsSection.tsx | 8 +- src/components/slide/SlideThumbnail.tsx | 3 +- .../slide/script/ScriptBulkEditModal.test.tsx | 75 +++++++++++++++++++ .../slide/script/ScriptBulkEditModal.tsx | 7 +- src/components/slide/script/SlideTitle.tsx | 41 ++++++++-- src/hooks/useInsightPageModel.ts | 9 ++- src/mocks/analytics.ts | 3 +- src/pages/FeedbackSlidePage.tsx | 5 +- src/pages/feedback/useFeedbackVideo.ts | 10 ++- src/types/recording.ts | 4 +- src/utils/sharedContent.ts | 3 +- src/utils/slideTitle.test.ts | 18 +++++ src/utils/slideTitle.ts | 19 +++++ 20 files changed, 249 insertions(+), 37 deletions(-) create mode 100644 src/components/slide/script/ScriptBulkEditModal.test.tsx create mode 100644 src/utils/slideTitle.test.ts create mode 100644 src/utils/slideTitle.ts diff --git a/src/components/comment/Comment.tsx b/src/components/comment/Comment.tsx index b2362ec5..f75a8618 100644 --- a/src/components/comment/Comment.tsx +++ b/src/components/comment/Comment.tsx @@ -18,6 +18,7 @@ import { UserAvatar } from '@/components/common'; import { useAuthStore } from '@/stores/authStore'; import type { Comment as CommentType } from '@/types/comment'; import { formatRelativeTime, formatVideoTimestamp } from '@/utils/format'; +import { getSlideTitle } from '@/utils/slideTitle'; import { useCommentContext } from './CommentContext'; import CommentInput from './CommentInput'; @@ -119,7 +120,7 @@ function Comment({ comment, isIndented = false, rootCommentId }: CommentProps) { const commentRef = comment.ref; const refLabel = commentRef ? commentRef.kind === 'slide' - ? `슬라이드 ${commentRef.index + 1}` + ? getSlideTitle(undefined, commentRef.index + 1) : formatVideoTimestamp(commentRef.seconds) : null; diff --git a/src/components/common/TitleEditorPopover.tsx b/src/components/common/TitleEditorPopover.tsx index 9b35fd73..4962c306 100644 --- a/src/components/common/TitleEditorPopover.tsx +++ b/src/components/common/TitleEditorPopover.tsx @@ -5,7 +5,7 @@ * readOnlyContent가 제공되면 InfoIcon + 정보 팝오버를 표시하고, * 없으면 ArrowDownIcon + 편집 팝오버를 표시합니다. */ -import { type ReactNode, useEffect, useState } from 'react'; +import { type ReactNode, useState } from 'react'; import clsx from 'clsx'; @@ -17,6 +17,8 @@ import { TextField } from './TextField'; interface TitleEditorPopoverProps { title: string; + inputTitle?: string; + inputPlaceholder?: string; onSave?: (newTitle: string, close: () => void) => void; readOnlyContent?: ReactNode; isCollapsed?: boolean; @@ -28,6 +30,8 @@ interface TitleEditorPopoverProps { export function TitleEditorPopover({ title, + inputTitle, + inputPlaceholder, onSave, readOnlyContent, isCollapsed = false, @@ -36,11 +40,9 @@ export function TitleEditorPopover({ titleClassName = 'max-w-60 truncate', showOnMobile = false, }: TitleEditorPopoverProps) { - const [editTitle, setEditTitle] = useState(title); - - useEffect(() => { - setEditTitle(title); - }, [title]); + const resolvedInputTitle = inputTitle ?? title; + const inputResetKey = `${resolvedInputTitle}::${inputPlaceholder ?? ''}`; + const [editTitle, setEditTitle] = useState(resolvedInputTitle); if (readOnlyContent) { return ( @@ -69,6 +71,7 @@ export function TitleEditorPopover({ return ( (