diff --git a/src/apis/videos.ts b/src/apis/videos.ts index 9c649b1..e6e81d7 100644 --- a/src/apis/videos.ts +++ b/src/apis/videos.ts @@ -1,5 +1,5 @@ -import { APIResponse } from '@/models/config/axios'; -import { IVideo, VideoVersionType } from '@/models/video'; +import { APIBaseResponse, APIResponse } from '@/models/config/axios'; +import { IVideo, UpdateVideoRequest, VideoVersionType } from '@/models/video'; import axios from './config/instance'; import axiosInstance from './config/instance'; @@ -34,3 +34,20 @@ export const getVideoById = async ( const response = await axiosInstance.get(`/videos/${videoId}`); return response.data; }; + +export const updateVideoAPI = ( + videoId: string | number, + data: UpdateVideoRequest, +) => { + return axios.patch>(PREFIX + `/${videoId}`, data); +}; + +export const createVideoSummaryAPI = (videoId: number, content: string[]) => { + return axios.post(PREFIX + `/${videoId}/newSummary`, { + content, + }); +}; + +export const deleteVideoSummaryAPI = (summaryId: number) => { + return axios.delete(PREFIX + `/${summaryId}/deleteSummary`); +}; diff --git a/src/components/Home/InsightVideos.tsx b/src/components/Home/InsightVideos.tsx index 01d8ad4..5216b93 100644 --- a/src/components/Home/InsightVideos.tsx +++ b/src/components/Home/InsightVideos.tsx @@ -17,7 +17,7 @@ const InsightVideos: React.FC = ({ const [categoryItems] = useState([]); const [checkedItems, setCheckedItems] = useState([]); - const onFileClick = (e: React.MouseEvent) => { + const onFileClick = (e: React.MouseEvent) => { e.stopPropagation(); // 비디오 카테고리로 저장 API 호출 후 이런 인사이트는 어때요 API 재호출로 최신화하기 }; diff --git a/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategoryDropdown/CategoryDropdown.tsx b/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategoryDropdown/CategoryDropdown.tsx index f6cae9f..94b5429 100644 --- a/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategoryDropdown/CategoryDropdown.tsx +++ b/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategoryDropdown/CategoryDropdown.tsx @@ -1,21 +1,18 @@ +import { useRecoilValue } from 'recoil'; + import { Dropdown } from '@/styles/SummaryPage'; -import DropdownItem from './DropdownItem'; -import { useRecoilValue } from 'recoil'; import { categoryState } from '@/stores/category'; -import React from 'react'; -import { ISelectedCategoryProps } from 'types/category'; -interface ICategoryDropdownProp { - setIsOpen: React.Dispatch>; - handleSelectCategory: ({ name, categoryId }: ISelectedCategoryProps) => void; -} +import DropdownItem from './DropdownItem'; + +type Props = { + onSelect: (categoryId: number) => void; +}; -const CategoryDropdown = ({ - setIsOpen, - handleSelectCategory, -}: ICategoryDropdownProp) => { +const CategoryDropdown = ({ onSelect }: Props) => { const categories = useRecoilValue(categoryState); + return ( e.stopPropagation()}>
    @@ -23,8 +20,7 @@ const CategoryDropdown = ({ ))}
diff --git a/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategoryDropdown/DropdownItem.tsx b/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategoryDropdown/DropdownItem.tsx index 7fcfb91..2af58c7 100644 --- a/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategoryDropdown/DropdownItem.tsx +++ b/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategoryDropdown/DropdownItem.tsx @@ -1,25 +1,16 @@ import { useState } from 'react'; +import { IFolderProps } from 'types/category'; import DownIcon from '@/assets/icons/down.svg?react'; -import { IFolderProps, ISelectedCategoryProps } from 'types/category'; + import { DropdownTopCategoryName } from '@/styles/SummaryPage'; -interface ICategoryDropdownProp { +type Props = { category: IFolderProps; - setIsOpen: React.Dispatch>; - handleSelectCategory: ({ name, categoryId }: ISelectedCategoryProps) => void; -} - -interface IItemClickProps { - name: string; - categoryId: number; -} + onSelect: (categoryId: number) => void; +}; -const DropdownItem = ({ - category, - setIsOpen, - handleSelectCategory, -}: ICategoryDropdownProp) => { +const DropdownItem = ({ category, onSelect }: Props) => { const [isShow, setIsShow] = useState(false); const dynamicStyles = { @@ -31,11 +22,6 @@ const DropdownItem = ({ }, }; - const handleItemClick = async ({ name, categoryId }: IItemClickProps) => { - handleSelectCategory({ name, categoryId }); - setIsOpen(false); - }; - return ( <>
  • @@ -45,14 +31,8 @@ const DropdownItem = ({ style={dynamicStyles.icon} onClick={() => setIsShow(!isShow)} /> - - handleItemClick({ - name: category.name, - categoryId: category.categoryId, - }) - } - > + + onSelect(category.categoryId)}> {category.name}
  • @@ -61,12 +41,7 @@ const DropdownItem = ({ {category.subFolders.map((subFolder) => (
  • - handleItemClick({ - name: subFolder.name, - categoryId: subFolder.categoryId, - }) - } + onClick={() => onSelect(subFolder.categoryId)} > {subFolder.name}
  • diff --git a/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategorySelectBox.tsx b/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategorySelectBox.tsx index 36be36f..36876d9 100644 --- a/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategorySelectBox.tsx +++ b/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategorySelectBox.tsx @@ -1,44 +1,69 @@ import { useState } from 'react'; +import { useRecoilValue } from 'recoil'; import DownIcon from '@/assets/icons/down.svg?react'; import OpenFileIcon from '@/assets/icons/open-file.svg?react'; import useOutsideClick from '@/hooks/useOutsideClick'; +import { categoryState } from '@/stores/category'; +import { userTokenState } from '@/stores/user'; + import { CategoryDropdown } from './CategoryDropdown'; -import { ISelectedCategoryProps } from 'types/category'; -interface ICategorySelectBoxProps { - selectedCategory: ISelectedCategoryProps; - handleSelectCategory: ({ name, categoryId }: ISelectedCategoryProps) => void; - onFileClick?: (e: React.MouseEvent) => void; -} +type Props = { + selectedCategoryId?: number; + onSelect: (categoryId: number) => void; + onFileClick?: (e: React.MouseEvent) => void; +}; const CategorySelectBox = ({ - selectedCategory, - handleSelectCategory, + selectedCategoryId, + onSelect, onFileClick, -}: ICategorySelectBoxProps) => { - const [isLogin] = useState(true); +}: Props) => { + const userToken = useRecoilValue(userTokenState); + const categories = useRecoilValue(categoryState); + const [isOpen, setIsOpen] = useState(false); + const selectedCategory = + selectedCategoryId && + categories + .reduce( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (acc: any[], category) => [ + ...acc, + { ...category }, + ...category.subFolders, + ], + [], + ) + .find((category) => category.categoryId === selectedCategoryId); + // 다른 영역 클릭 시 dropdown 안보여지게 하기 const [ref] = useOutsideClick(() => { setIsOpen(false); }); const handleBoxClick = () => { - if (!isLogin) return; + if (!userToken) return; setIsOpen(!isOpen); }; + + const handleSelect = (categoryId: number) => { + onSelect(categoryId); + setIsOpen(false); + }; + return (
    - {isLogin - ? selectedCategory.name + {userToken + ? selectedCategory ? selectedCategory.name : '어떤 카테고리에 넣을까요?' : '로그인하고 요약한 영상을 아카이빙해요!'} @@ -47,17 +72,12 @@ const CategorySelectBox = ({
    - {isOpen && ( - - )} + {isOpen && }
    diff --git a/src/components/SummaryPage/SummaryDetailBox/NoteBox/NoteBox.tsx b/src/components/SummaryPage/SummaryDetailBox/NoteBox/NoteBox.tsx index 9c066d5..bb916c5 100644 --- a/src/components/SummaryPage/SummaryDetailBox/NoteBox/NoteBox.tsx +++ b/src/components/SummaryPage/SummaryDetailBox/NoteBox/NoteBox.tsx @@ -1,14 +1,26 @@ import { useRecoilValue } from 'recoil'; +import { + createVideoSummaryAPI, + deleteVideoSummaryAPI, + updateVideoAPI, +} from '@/apis/videos'; + import PlusIcon from '@/assets/icons/plus.svg?react'; import useIndex from '@/hooks/useIndex'; +import { IVideoSummary } from '@/models/video'; + import { summaryVideoState } from '@/stores/summary'; import NoteItem from './NoteItem'; -const NoteBox = () => { +type Props = { + onRefresh: () => void; +}; + +const NoteBox = ({ onRefresh }: Props) => { const summaryVideo = useRecoilValue(summaryVideoState); const [editableIndex, setEditableIndex, setDisableIndex] = useIndex(); @@ -20,6 +32,38 @@ const NoteBox = () => { } }; + const handleUpdateNote = async (summary: IVideoSummary) => { + if (!summaryVideo || editableIndex === null) return; + + try { + if (summary.content === '') { + await deleteVideoSummaryAPI(summary.id); + + setDisableIndex(); + } else { + await updateVideoAPI(summaryVideo.video_id, { summary: [summary] }); + + handleActiveEditable(editableIndex + 1); + } + + onRefresh(); + } catch (e) { + console.error(e); + } + }; + + const handleCreateNote = async (content: string) => { + if (!summaryVideo || content === '') return; + + try { + await createVideoSummaryAPI(summaryVideo.video_id, [content]); + + onRefresh(); + } catch (e) { + console.error(e); + } + }; + return (
    @@ -30,16 +74,17 @@ const NoteBox = () => { isEditable={editableIndex === index} onDisableEditable={setDisableIndex} onActiveEditable={() => handleActiveEditable(index)} - onActiveNextEditable={() => handleActiveEditable(index + 1)} + onEdit={(content) => handleUpdateNote({ id: summary.id, content })} /> ))} {/* 추가 */} {editableIndex === -1 && ( )} diff --git a/src/components/SummaryPage/SummaryDetailBox/NoteBox/NoteItem.tsx b/src/components/SummaryPage/SummaryDetailBox/NoteBox/NoteItem.tsx index a3a5fe2..bd36b28 100644 --- a/src/components/SummaryPage/SummaryDetailBox/NoteBox/NoteItem.tsx +++ b/src/components/SummaryPage/SummaryDetailBox/NoteBox/NoteItem.tsx @@ -9,7 +9,7 @@ type Props = { isEditable: boolean; onDisableEditable: () => void; onActiveEditable?: () => void; - onActiveNextEditable?: () => void; + onEdit: (content: string) => void; }; const NoteItem = ({ @@ -17,7 +17,7 @@ const NoteItem = ({ isEditable, onDisableEditable, onActiveEditable, - onActiveNextEditable, + onEdit, }: Props) => { const textareaRef = useRef(null); const [noteText, setNoteText] = useState(summary.content); @@ -30,7 +30,12 @@ const NoteItem = ({ } else if (e.key === 'Enter') { e.preventDefault(); - onActiveNextEditable && onActiveNextEditable(); + onEdit(noteText); + + // 이어서 생성할 수 있도록 + if (summary.id === -1) { + setNoteText(''); + } } }; diff --git a/src/components/SummaryPage/SummaryDetailBox/SummaryDetailBox.tsx b/src/components/SummaryPage/SummaryDetailBox/SummaryDetailBox.tsx index b5dfd07..9758a48 100644 --- a/src/components/SummaryPage/SummaryDetailBox/SummaryDetailBox.tsx +++ b/src/components/SummaryPage/SummaryDetailBox/SummaryDetailBox.tsx @@ -1,29 +1,33 @@ import { useRecoilValue } from 'recoil'; +import { updateVideoAPI } from '@/apis/videos'; + import { summaryVideoState } from '@/stores/summary'; import { DetailBox } from '@/styles/SummaryPage'; +import { formatDate } from '@/utils/date'; + import { CategorySelectBox } from './CategorySelectBox'; import { NoteBox } from './NoteBox'; -import { useState } from 'react'; -import { ISelectedCategoryProps } from 'types/category'; -const SummaryDetailBox = () => { +type Props = { + onRefresh: () => void; +}; + +const SummaryDetailBox = ({ onRefresh }: Props) => { const summaryVideo = useRecoilValue(summaryVideoState); - const [selectedCategory, setSelectedCategory] = - useState({ name: '', categoryId: 0 }); - - const handleSelectCategory = ({ - name, - categoryId, - }: ISelectedCategoryProps) => { - setSelectedCategory({ - name, - categoryId, - }); - console.log('name, categoryId를 이용한 API 요청'); + const handleSelectCategory = async (category_id: number) => { + if (!summaryVideo) return; + + try { + await updateVideoAPI(summaryVideo.video_id, { category_id }); + + onRefresh(); + } catch (e) { + console.error(e); + } }; return ( @@ -35,9 +39,13 @@ const SummaryDetailBox = () => { }} > - 2024년 1월 1일 + + {formatDate(summaryVideo?.updated_at)} + - {summaryVideo?.title} + + {summaryVideo?.title || '-'} +
    {summaryVideo?.tag.map((hashtag) => ( @@ -58,11 +66,11 @@ const SummaryDetailBox = () => { /> - {summaryVideo?.description} + {summaryVideo?.description || '-'}
    { {summaryVideo?.subHeading.map((subHeading, i) => (
    {i + 1} - {subHeading.content} + {subHeading.name}
    ))}
    - +
    ); diff --git a/src/components/category/Card.tsx b/src/components/category/Card.tsx index 08a270c..571230c 100644 --- a/src/components/category/Card.tsx +++ b/src/components/category/Card.tsx @@ -1,17 +1,19 @@ import React, { useState } from 'react'; -import * as CardStyles from '@/styles/category/Card.style'; -import { IVideoProps } from 'types/videos'; -import { CategorySelectBox } from '../SummaryPage/SummaryDetailBox/CategorySelectBox'; -import { ISelectedCategoryProps } from 'types/category'; import { useRecoilValue } from 'recoil'; +import { IVideoProps } from 'types/videos'; + +import { CategorySelectBox } from '@/components/SummaryPage/SummaryDetailBox/CategorySelectBox'; + import { categoryState } from '@/stores/category'; +import * as CardStyles from '@/styles/category/Card.style'; + interface ICardProps { mode: 'default' | 'category' | 'recommend'; video: IVideoProps; checkedVideos?: number[]; setCheckedVideos?: (value: number[]) => void; - onFileClick?: (e: React.MouseEvent) => void; + onFileClick?: (e: React.MouseEvent) => void; } const Card: React.FC = ({ @@ -23,20 +25,12 @@ const Card: React.FC = ({ }) => { const [isOpen, setIsOpen] = useState(false); const category = useRecoilValue(categoryState); - const [selectedCategory, setSelectedCategory] = - useState({ - name: category[0].name, - categoryId: category[0].categoryId, - }); + const [selectedCategoryId, setSelectedCategoryId] = useState( + category[0].categoryId, + ); - const handleSelectCategory = ({ - name, - categoryId, - }: ISelectedCategoryProps) => { - setSelectedCategory({ - name, - categoryId, - }); + const handleSelectCategory = (categoryId: number) => { + setSelectedCategoryId(categoryId); }; const handleCheckBox = (videoId: number) => { @@ -77,8 +71,8 @@ const Card: React.FC = ({ {isOpen && mode === 'recommend' && ( diff --git a/src/hooks/useIndex.ts b/src/hooks/useIndex.ts index c02c377..db443db 100644 --- a/src/hooks/useIndex.ts +++ b/src/hooks/useIndex.ts @@ -1,22 +1,22 @@ import { useState } from 'react'; /** - * @param { Number | null } initialnumber + * @param { number | null } initialIndex * * @example const [hoverdIndex, enterIndex, LeaveIndex] = useNumber() */ -const useIndex = ( initialnum = null) => { - const [num, setNum] = useState(initialnum); +const useIndex = (initialIndex: number | null = null) => { + const [index, setIndex] = useState(initialIndex); - const setNumber = (num : number) => { - setNum(num); - }; + const setNextIndex = (nextIndex: number) => { + setIndex(nextIndex); + }; - const setNull = () => { - setNum(null); - }; + const setNull = () => { + setIndex(null); + }; - return [num, setNumber, setNull] as const; -} + return [index, setNextIndex, setNull] as const; +}; -export default useIndex; \ No newline at end of file +export default useIndex; diff --git a/src/models/video.ts b/src/models/video.ts index 9655bff..a76d886 100644 --- a/src/models/video.ts +++ b/src/models/video.ts @@ -5,6 +5,7 @@ export interface IVideoSubHeading { start_time: string; end_time: string; content: string; + name: string; } export interface IVideoSummary { @@ -17,6 +18,7 @@ export interface IVideoTag { } export interface IVideo { + category_id?: number; video_id: number; title: string; description: string; @@ -33,3 +35,17 @@ export interface IVideo { export interface VideoResponse { videos: IVideo[]; } + +export interface UpdateVideoSubHeading { + id: number; + name: string; + content: string; +} + +export interface UpdateVideoRequest { + title?: string; + description?: string; + category_id?: number; + subHeading?: UpdateVideoSubHeading; + summary?: IVideoSummary[]; +} diff --git a/src/pages/SummaryPage.tsx b/src/pages/SummaryPage.tsx index 9d1b74e..58ff9f3 100644 --- a/src/pages/SummaryPage.tsx +++ b/src/pages/SummaryPage.tsx @@ -39,7 +39,7 @@ const SummaryPage = () => { return ( - + ); diff --git a/src/utils/date.ts b/src/utils/date.ts index deecc1f..87ac25d 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -11,3 +11,15 @@ export const diffTime = (end: number, start: number) => { day, }; }; + +export const formatDate = (date?: string) => { + if (!date) return '-'; + + const dateObj = new Date(date); + + const year = dateObj.getFullYear(); + const month = dateObj.getMonth() + 1; + const day = dateObj.getDate(); + + return `${year}년 ${month}월 ${day}일`; +};