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/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.test.tsx b/src/components/common/TitleEditorPopover.test.tsx new file mode 100644 index 00000000..0994a8d7 --- /dev/null +++ b/src/components/common/TitleEditorPopover.test.tsx @@ -0,0 +1,63 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, expect, it } from 'vitest'; + +import { TitleEditorPopover } from './TitleEditorPopover'; + +describe('TitleEditorPopover', () => { + it('re-initializes input value from inputTitle whenever popover opens', async () => { + const user = userEvent.setup(); + + render( + , + ); + + const triggerButton = screen.getByRole('button', { name: '슬라이드 이름 변경' }); + + await user.click(triggerButton); + const input = screen.getByRole('textbox', { name: '슬라이드 이름 변경' }); + expect(input).toHaveValue(''); + expect(input).toHaveAttribute('placeholder', '슬라이드 1'); + + await user.type(input, '임시 제목'); + expect(input).toHaveValue('임시 제목'); + + await user.click(triggerButton); // close + await user.click(triggerButton); // reopen + + expect(screen.getByRole('textbox', { name: '슬라이드 이름 변경' })).toHaveValue(''); + }); + + it('uses title as default input value when title exists', async () => { + const user = userEvent.setup(); + + render( + , + ); + + const triggerButton = screen.getByRole('button', { name: '슬라이드 이름 변경' }); + + await user.click(triggerButton); + const input = screen.getByRole('textbox', { name: '슬라이드 이름 변경' }); + expect(input).toHaveValue('도입'); + + await user.clear(input); + await user.type(input, '변경 전 임시 값'); + expect(input).toHaveValue('변경 전 임시 값'); + + await user.click(triggerButton); // close + await user.click(triggerButton); // reopen + + expect(screen.getByRole('textbox', { name: '슬라이드 이름 변경' })).toHaveValue('도입'); + }); +}); diff --git a/src/components/common/TitleEditorPopover.tsx b/src/components/common/TitleEditorPopover.tsx index 9b35fd73..db64948a 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,14 @@ export function TitleEditorPopover({ titleClassName = 'max-w-60 truncate', showOnMobile = false, }: TitleEditorPopoverProps) { - const [editTitle, setEditTitle] = useState(title); + const resolvedInputTitle = inputTitle ?? title; + const [editTitle, setEditTitle] = useState(resolvedInputTitle); - useEffect(() => { - setEditTitle(title); - }, [title]); + const handleOpenChange = (isOpen: boolean) => { + if (!isOpen) return; + // Popover를 열 때마다 현재 슬라이드 기준 입력값으로 초기화합니다. + setEditTitle(resolvedInputTitle); + }; if (readOnlyContent) { return ( @@ -69,6 +76,7 @@ export function TitleEditorPopover({ return ( (