+
{isEditingView ? : }
diff --git a/src/components/SummaryPage/SummaryScriptBox/ToolBox/Indicator.tsx b/src/components/SummaryPage/SummaryScriptBox/ToolBox/Indicator.tsx
index 9b04cec..c6d608c 100644
--- a/src/components/SummaryPage/SummaryScriptBox/ToolBox/Indicator.tsx
+++ b/src/components/SummaryPage/SummaryScriptBox/ToolBox/Indicator.tsx
@@ -1,22 +1,83 @@
-// 임시 타입
-interface Item {
- id: number;
-}
-
-type Props = {
- list: Item[];
- focusId: number;
- onChange: (focusId: number) => void;
-};
+import { useEffect, useState } from 'react';
+import { useRecoilState, useRecoilValue } from 'recoil';
+
+import { IVideo } from '@/models/video';
+
+import {
+ summaryPlaySubHeadingIdState,
+ summaryVideoState,
+} from '@/stores/summary';
+
+const Indicator = () => {
+ const summaryVideo = useRecoilValue(summaryVideoState) as IVideo;
+ const [playSubHeadingId, setPlaySubHeadingId] = useRecoilState(
+ summaryPlaySubHeadingIdState,
+ );
+
+ const [focusId, setFocusId] = useState(-1);
+
+ useEffect(() => {
+ const handleScroll = () => {
+ const { top: containerTop } = container.getBoundingClientRect();
+ const list = Array.from(document.querySelectorAll('.script-box'))
+ .map((el) => el.getBoundingClientRect().top - containerTop)
+ .filter((top) => top < 100);
+ // window.innerHeight * 0.3
+
+ (document.querySelector('.tools') as HTMLDivElement).style.boxShadow =
+ `0 4px 40px 0 rgba(0, 0, 0, ${container.scrollTop > 0 ? 0.05 : 0})`;
+
+ if (list.length) {
+ const { id } = summaryVideo.subHeading[list.length - 1];
+
+ setFocusId(id);
+ }
+ };
+
+ if (summaryVideo.subHeading.length) {
+ setFocusId(summaryVideo.subHeading[0].id);
+ }
+
+ const container = document.querySelector('#script-box') as HTMLDivElement;
+ container.addEventListener('scroll', handleScroll);
+
+ return () => {
+ container.removeEventListener('scroll', handleScroll);
+ };
+ }, [summaryVideo]);
+
+ useEffect(() => {
+ if (playSubHeadingId < 0) return;
+
+ const findIdx = summaryVideo.subHeading.findIndex(
+ (s) => s.id === playSubHeadingId,
+ );
+
+ if (findIdx > -1) {
+ const container = document.querySelector('#script-box') as HTMLDivElement;
+ const element = document.querySelectorAll('.script-box')[findIdx];
+
+ if (element) {
+ const { top: containerTop } = container.getBoundingClientRect();
+ const { top } = element.getBoundingClientRect();
+
+ container.scrollTo({
+ top: container.scrollTop + top - containerTop - 20,
+ behavior: 'smooth',
+ });
+ }
+ }
+
+ setFocusId(playSubHeadingId);
+ }, [playSubHeadingId, summaryVideo]);
-const Indicator = ({ list, focusId, onChange }: Props) => {
return (
- {list.map((item) => (
+ {summaryVideo.subHeading.map((item) => (
onChange(item.id)}
+ onClick={() => setPlaySubHeadingId(item.id)}
/>
))}
diff --git a/src/components/SummaryPage/SummaryScriptBox/ToolBox/ToolBox.tsx b/src/components/SummaryPage/SummaryScriptBox/ToolBox/ToolBox.tsx
index f219031..1056b81 100644
--- a/src/components/SummaryPage/SummaryScriptBox/ToolBox/ToolBox.tsx
+++ b/src/components/SummaryPage/SummaryScriptBox/ToolBox/ToolBox.tsx
@@ -5,6 +5,8 @@ import { getVideoAPI, updateVideoAPI } from '@/apis/videos';
import ModifyIcon from '@/assets/icons/modify.svg?react';
+import useCreateToast from '@/hooks/useCreateToast';
+
import { IVideo } from '@/models/video';
import {
@@ -17,7 +19,6 @@ import {
import Indicator from './Indicator';
import { SearchKeyword } from './SearchKeyword';
import { ChangeKeyword } from './ChangeKeyword';
-import useCreateToast from '@/hooks/useCreateToast';
type Props = {
onRefresh: () => void;
@@ -33,10 +34,11 @@ const ToolBox = ({ onRefresh, onChangeKeyword }: Props) => {
const [isEditingView, setIsEditingView] = useRecoilState(
summaryIsEditingViewState,
);
- const { createToast } = useCreateToast();
const [originalSummary, setOriginalSummary] = useState
(null);
+ const { createToast } = useCreateToast();
+
const handleClickModifyIcon = () => {
setPlaySubHeadingId(-1);
setIsEditingView(true);
@@ -125,11 +127,7 @@ const ToolBox = ({ onRefresh, onChangeKeyword }: Props) => {
>
) : (
<>
- {}}
- />
+
diff --git a/src/components/category/Card.tsx b/src/components/category/Card.tsx
index ea7e021..06bbccd 100644
--- a/src/components/category/Card.tsx
+++ b/src/components/category/Card.tsx
@@ -1,13 +1,14 @@
-import React, { useState } from 'react';
+import React from 'react';
import { useRecoilValue } from 'recoil';
import { IVideoProps } from 'types/videos';
import { CategorySelectBox } from '@/components/SummaryPage/SummaryDetailBox/CategorySelectBox';
-import { categoryState } from '@/stores/category';
+import { userTokenState } from '@/stores/user';
import * as CardStyles from '@/styles/category/Card.style';
-import Chip from '../common/chip/Chip';
+
+import Chip from '@/components/common/chip/Chip';
interface ICardProps {
mode: 'default' | 'category' | 'recommend';
@@ -28,15 +29,9 @@ const Card: React.FC
= ({
setCheckedVideos,
onFileClick,
}) => {
- const category = useRecoilValue(categoryState);
-
- const [selectedCategoryId, setSelectedCategoryId] = useState(
- category.length ? category[0].categoryId : -1,
- );
- const [startSelect, setStartSelect] = useState(false);
+ const userToken = useRecoilValue(userTokenState);
const onFileClickWithProps = (categoryId: number, categoryName?: string) => {
- setSelectedCategoryId(categoryId);
onFileClick && onFileClick(video.video_id, categoryId, categoryName);
};
@@ -48,7 +43,7 @@ const Card: React.FC = ({
}
};
return (
-
+
{mode === 'category' && (
@@ -65,9 +60,7 @@ const Card: React.FC = ({
{video.title}
{video.description}
@@ -77,12 +70,11 @@ const Card: React.FC = ({
))}
- {mode === 'recommend' && (
+ {mode === 'recommend' && userToken && (
diff --git a/src/components/common/ModelController/ModelController.tsx b/src/components/common/ModelController/ModelController.tsx
index 90de4ea..6dab468 100644
--- a/src/components/common/ModelController/ModelController.tsx
+++ b/src/components/common/ModelController/ModelController.tsx
@@ -1,11 +1,5 @@
import { useEffect, useRef } from 'react';
-import { useRecoilState, useSetRecoilState } from 'recoil';
-
-import {
- modelingProcess1,
- modelingProcess2,
- modelingProcess3,
-} from '@/apis/video';
+import { useRecoilState, useSetRecoilState, useRecoilValue } from 'recoil';
import {
modelingDataState,
@@ -13,10 +7,18 @@ import {
modelingStatusState,
videoLinkState,
} from '@/stores/model-controller';
+import {
+ modelingProcess1,
+ modelingProcess2,
+ modelingProcess3,
+} from '@/apis/video';
+import { userTokenState } from '@/stores/user';
+
import { createVideoAlarmAPI } from '@/apis/user';
const ModelController = () => {
const interval = useRef();
+ const userToken = useRecoilValue(userTokenState);
const [videoLink, setVideoLink] = useRecoilState(videoLinkState);
const setModelingStatus = useSetRecoilState(modelingStatusState);
const setModelingProgress = useSetRecoilState(modelingProgressState);
@@ -32,11 +34,13 @@ const ModelController = () => {
const handleError = async () => {
try {
- await createVideoAlarmAPI(0, 'fail', {
- title: '앗, 영상 변환 중 오류가 생겼어요',
- content: '어떤 문제인지 확인해보세요!',
- is_confirm: false,
- });
+ if (userToken) {
+ await createVideoAlarmAPI(0, 'fail', {
+ title: '앗, 영상 변환 중 오류가 생겼어요',
+ content: '어떤 문제인지 확인해보세요!',
+ is_confirm: false,
+ });
+ }
} catch (e) {
console.error(e);
}
@@ -45,6 +49,8 @@ const ModelController = () => {
clearInterval(interval.current);
}
+ clearInterval(interval.current);
+
setModelingStatus('ERROR');
setVideoLink(null);
};
@@ -53,6 +59,8 @@ const ModelController = () => {
if (!videoLink) return;
const callProcess1API = async () => {
+ setModelingProgress(0);
+
try {
const { videoId } = (await modelingProcess1(videoLink)).data.result;
@@ -83,11 +91,14 @@ const ModelController = () => {
try {
const { finalData } = (await modelingProcess3({ videoId })).data.result;
- if (interval.current) {
- clearInterval(interval.current);
- }
+ clearInterval(interval.current);
- setModelingData(finalData);
+ setModelingData({
+ ...finalData,
+ youtube_id: videoId,
+ created_at: new Date().toString(),
+ updated_at: new Date().toString(),
+ });
setModelingProgress(100);
setModelingStatus('COMPLETE');
} catch (e) {
@@ -98,7 +109,6 @@ const ModelController = () => {
};
setModelingStatus('CONTINUE');
- setModelingProgress(Math.ceil(Math.random() * 5));
callProcess1API();
startInterval();
// eslint-disable-next-line react-hooks/exhaustive-deps
diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx
index 00d149e..b828841 100644
--- a/src/components/layout/Layout.tsx
+++ b/src/components/layout/Layout.tsx
@@ -1,5 +1,5 @@
import { Outlet, useLocation } from 'react-router-dom';
-import { useRecoilValue } from 'recoil';
+import { useRecoilValue } from 'recoil';
import { isSideBarOpenState } from '@/stores/ui';
@@ -7,13 +7,17 @@ import Footer from './footer/Footer';
import Header from './header';
import SideBar from './sideBar';
import NicknameModal from '@/components/NicknameModal';
+import ErrorModal from '../modals/ErrorModal';
+
import { useMemo, useEffect } from 'react';
import { userInfoState } from '@/stores/user';
+import { errorModalState } from '@/stores/modal';
const Layout = () => {
const { pathname } = useLocation();
const isSideBarOpen = useRecoilValue(isSideBarOpenState);
const userInfo = useRecoilValue(userInfoState);
+ const isErrorModalOpen = useRecoilValue(errorModalState)
const isShowFooter = useMemo(
() => pathname === '/' || /^(\/category)/g.test(pathname),
@@ -38,6 +42,7 @@ const Layout = () => {
{isShowFooter && }
{userInfo && userInfo.nick_name === '' && }
+ {isErrorModalOpen && }
>
);
};
diff --git a/src/components/layout/header/alarm/AlarmItem.tsx b/src/components/layout/header/alarm/AlarmItem.tsx
index d026ac1..83b66dd 100644
--- a/src/components/layout/header/alarm/AlarmItem.tsx
+++ b/src/components/layout/header/alarm/AlarmItem.tsx
@@ -11,12 +11,20 @@ import ErrorImage from '@/assets/Error.png';
import { Container } from '@/styles/layout/header/alarm/AlarmItem.style';
import { diffTime } from '@/utils/date';
+import { useRecoilValue } from 'recoil';
+import {
+ modelingProgressState,
+ modelingStatusState,
+} from '@/stores/model-controller';
+import theme from '@/styles/theme';
+import { confirmSelectAlarmAPI } from '@/apis/user';
type Props = {
alarm: IAlarm;
selectIdList: number[];
onUpdateSelectIdList: (list: number[]) => void;
onClose: () => void;
+ onRefresh: () => void;
};
const AlarmItem = ({
@@ -24,8 +32,11 @@ const AlarmItem = ({
selectIdList,
onUpdateSelectIdList,
onClose,
+ onRefresh,
}: Props) => {
const navigate = useNavigate();
+ const status = useRecoilValue(modelingStatusState);
+ const progress = useRecoilValue(modelingProgressState);
const isSelected = selectIdList.indexOf(alarm.alarm_id) > -1;
const type = () => {
@@ -58,11 +69,26 @@ const AlarmItem = ({
return `${second}초`;
};
- const handleClick = () => {
+ const handleClick = async () => {
+ if (!alarm.is_confirm) {
+ try {
+ await confirmSelectAlarmAPI({ alarms: [alarm.alarm_id] });
+ onRefresh();
+ } catch (e) {
+ console.error(e);
+ }
+ }
if (alarm.type === 'notice') {
navigate('/guide');
- onClose();
}
+ if (
+ alarm.type === 'video' &&
+ alarm.state === 'success' &&
+ alarm.alarm_id !== 999
+ ) {
+ navigate(`/summary/${alarm.video_id}`);
+ }
+ onClose();
};
const handleClickRemoveButton: React.MouseEventHandler = (
@@ -110,6 +136,23 @@ const AlarmItem = ({
{alarm.content}
+ {status !== 'NONE' && alarm.alarm_id === 999 && (
+
+
+
+
+ {status === 'ERROR' ? '변환 중 오류' : `${progress}%`}
+
+
+ )}
);
};
diff --git a/src/components/layout/header/alarm/AlarmList.tsx b/src/components/layout/header/alarm/AlarmList.tsx
index 72336a5..41b5a53 100644
--- a/src/components/layout/header/alarm/AlarmList.tsx
+++ b/src/components/layout/header/alarm/AlarmList.tsx
@@ -75,6 +75,7 @@ const AlarmList = ({ alarmList, onRefresh, onClose }: Props) => {
selectIdList={selectIdList}
onUpdateSelectIdList={setSelectIdList}
onClose={onClose}
+ onRefresh={onRefresh}
/>
))}
diff --git a/src/components/layout/header/alarm/index.tsx b/src/components/layout/header/alarm/index.tsx
index 365177c..b60c51d 100644
--- a/src/components/layout/header/alarm/index.tsx
+++ b/src/components/layout/header/alarm/index.tsx
@@ -1,20 +1,18 @@
import { useEffect, useState } from 'react';
-import { useRecoilValue } from 'recoil';
-
-import { getAlarmAPI } from '@/apis/user';
+import { useRecoilState, useRecoilValue } from 'recoil';
import NotifyOffIcon from '@/assets/icons/notify-off.svg?react';
import NotifyOnIcon from '@/assets/icons/notify-on.svg?react';
import useOutsideClick from '@/hooks/useOutsideClick';
-import { IAlarm } from '@/models/alarm';
-
import { modelingStatusState } from '@/stores/model-controller';
import * as HeaderStyle from '@/styles/layout/header';
import AlarmList from './AlarmList';
+import { userAlarmState } from '@/stores/user';
+import useGetAlarm from '@/hooks/useGetAlarm';
type Props = {
isDark: boolean;
@@ -23,26 +21,39 @@ type Props = {
const Alarm = ({ isDark }: Props) => {
const status = useRecoilValue(modelingStatusState);
const [isOpen, setIsOpen] = useState(false);
- const [alarmList, setAlarmList] = useState