diff --git a/.gitignore b/.gitignore index a173014..6ca58a4 100644 --- a/.gitignore +++ b/.gitignore @@ -163,4 +163,32 @@ cython_debug/ XTTS/ test/ -DB/ \ No newline at end of file +DB/ + +old/tts_model/tts_model.pth + +# client gitignore +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + + + diff --git a/client/public/index.html b/client/public/index.html new file mode 100644 index 0000000..aa069f2 --- /dev/null +++ b/client/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/client/public/robots.txt b/client/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/client/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/client/src/App.js b/client/src/App.js new file mode 100644 index 0000000..cf4943f --- /dev/null +++ b/client/src/App.js @@ -0,0 +1,78 @@ +import React from 'react'; +import CategorySelector from './components/CategorySelector'; +import ChannelSelector from './components/ChannelSelector'; +import VideoPlayer from './components/VideoPlayer'; +import SentimentValue from './components/SentimentValue'; +import RealTimeChart from './components/RealTimeChart' +import styled from 'styled-components'; +import { AppProvider } from './AppContext'; +import ChatContainer from './components/chat/ChatContainer'; + +const AppContainer = styled.div` + display: flex; + flex-direction: column; + height: 100vh; + /* 배경색 & 폰트 */ + background-color: ${(props) => props.theme.colors.background}; + color: ${(props) => props.theme.colors.text}; + font-family: ${(props) => props.theme.fonts.base}; +`; + +const HeaderSection = styled.header` + background-color: ${(props) => props.theme.colors.primary}; + padding: 12px 20px; + color: #fff; + display: flex; + align-items: center; +`; + +const MainSection = styled.div` + flex: 1; + display: flex; + flex-direction: row; +`; +const LeftSection = styled.div` + flex: 1; + display: flex; + padding: 0px 10px; + flex-direction: column; + box-sizing: border-box; +`; + +function App() { + return ( + + + + {/* 카테고리 선택 스크롤 버튼 */} + + + + + + {/* 채널 선택 스크롤 버튼 */} + + + {/* 동영상 플레이어 */} + + + {/* 감정분석 수치 값 */} + + + {/* 실시간 차트 */} + + + + + + + ); +} + +export default App; diff --git a/client/src/AppContext.js b/client/src/AppContext.js new file mode 100644 index 0000000..3f6c421 --- /dev/null +++ b/client/src/AppContext.js @@ -0,0 +1,84 @@ +import React, { createContext, useContext, useState } from 'react'; +import axios from 'axios'; + +const AppContext = createContext(); + +export const AppProvider = ({ children }) => { + const [messages, setMessages] = useState([]); // 채팅 메시지 목록 + const [currentMessage, setCurrentMessage] = useState(''); // 현재 입력된 메시지 + const [useVoice, setUseVoice] = useState(false); // 음성 사용 여부 + const [voiceName, setVoiceName] = useState('잇섭'); // 음성 이름 + const [selectedCategory, setSelectedCategory] = useState('뷰티'); // 선택된 카테고리 + const [selectedChannel, setSelectedChannel] = useState(null); // 선택된 채널 + const [sentimentScore, setSentimentScore] = useState(undefined); // 감성 분석 점수 + const [channelList, setChannelList] = useState([]); // 채널 목록 + + // 메시지 전송 핸들러 + const handleSendMessage = async () => { + if (currentMessage.trim() === '') return; // 빈 메시지 방지 + + // 유저 메시지 추가 + setMessages((prevMessages) => [ + ...prevMessages, + { sender: 'user', text: currentMessage }, + ]); + + try { + // 서버로 POST 요청 + const response = await axios.post('/chat', { + Category: selectedCategory, + Channel: selectedChannel, + Text: currentMessage, + Voice: useVoice, + Who: voiceName, + }); + + // 서버 응답 메시지 추가 + if (response.data?.Text) { + setMessages((prevMessages) => [ + ...prevMessages, + { text: response.data.Text, sender: 'bot' }, + ]); + } + + // 음성 데이터 처리 + if (useVoice && response.data?.Audio) { + console.log('Received audio file URL:', response.data.Audio); + } + } catch (error) { + console.error('Error sending message to /chat:', error); + } finally { + // 메시지 입력창 초기화 + setCurrentMessage(''); + } + }; + + return ( + + {children} + + ); +}; + +// Context를 쉽게 사용할 수 있도록 제공 +export const useApp = () => useContext(AppContext); diff --git a/client/src/components/CategorySelector.js b/client/src/components/CategorySelector.js new file mode 100644 index 0000000..30fb9c5 --- /dev/null +++ b/client/src/components/CategorySelector.js @@ -0,0 +1,75 @@ +import React, { useEffect } from 'react'; +import styled from 'styled-components'; +import axios from 'axios'; +import { useApp } from '../AppContext'; + +// 컨테이너 스타일 +const CategorySelectorContainer = styled.div` + display: flex; + flex-wrap: nowrap; + gap: 8px; + margin: 10px; + /* 필요하면 스크롤 추가 + overflow-x: auto; + */ +`; + +// 버튼 스타일 +const CategoryButton = styled.button` + padding: 8px 12px; + border: 1px solid #ccc; + background-color: #fff; + cursor: pointer; + border-radius: 4px; + transition: background-color 0.2s, color 0.2s; + + &:hover { + background-color: #f0f0f0; + } + + /* 선택된(active) 상태 */ + &.active { + background-color: #007bff; + color: #ffffff; + border-color: #007bff; + } +`; + +function CategorySelector() { + const { selectedCategory, setSelectedCategory, channelList, setChannelList } = useApp() + const categories = ['뷰티', '푸드', '패션', '라이프', '여행/체험', '키즈', '테크', '취미레저', '문화생활']; + + // 카테고리 선택 시 실행될 함수 + const handleSelectCategory = async (category) => { + setSelectedCategory(category); + try { + const response = await axios.get('/channels', { + params: { category }, + }); + setChannelList(response.data); + } catch (error) { + console.error('Error fetching channel List:', error); + } + }; + + + // useEffect(() => { + + // }, [selectedCategory]) + + return ( + + {categories.map((cat) => ( + handleSelectCategory(cat)} + > + {cat} + + ))} + + ); +} + +export default CategorySelector; diff --git a/client/src/components/ChannelSelector.js b/client/src/components/ChannelSelector.js new file mode 100644 index 0000000..39d8e7b --- /dev/null +++ b/client/src/components/ChannelSelector.js @@ -0,0 +1,67 @@ +import React from 'react'; +import styled from 'styled-components'; +import { useApp } from '../AppContext'; + +// 컨테이너 스타일 +const SelectorContainer = styled.div` + display: flex; + align-items: center; /* 수직 정렬 */ + gap: 10px; /* 레이블과 드롭다운 사이 간격 */ + margin: 10px; + font-family: ${(props) => props.theme.fonts.base}; +`; + +// 레이블 스타일 +const Label = styled.label` + font-size: 16px; + color: ${(props) => props.theme.colors.text}; + font-weight: bold; +`; + +// 드롭다운 스타일 +const StyledSelect = styled.select` + padding: 8px 12px; + border: 1px solid ${(props) => props.theme.colors.border}; + background-color: white; + font-size: 14px; + color: ${(props) => props.theme.colors.text}; + cursor: pointer; + transition: border-color 0.2s, box-shadow 0.2s; + + &:focus { + border-color: ${(props) => props.theme.colors.primary}; + box-shadow: 0 0 4px ${(props) => props.theme.colors.primary}; + outline: none; + } + + &:hover { + border-color: ${(props) => props.theme.colors.primaryLight}; + } +`; + +function ChannelSelector() { + const { selectedChannel, setSelectedChannel, channelList } = useApp(); + + const handleChange = (e) => { + setSelectedChannel(e.target.value); + }; + + return ( + + + + {channelList.map((ch, index) => ( + + ))} + + + ); +} + +export default ChannelSelector; diff --git a/client/src/components/RealTimeChart.js b/client/src/components/RealTimeChart.js new file mode 100644 index 0000000..f72d321 --- /dev/null +++ b/client/src/components/RealTimeChart.js @@ -0,0 +1,11 @@ +import React from 'react'; + +const RealTimeChart = () => { + return ( +
+ +
+ ); +}; + +export default RealTimeChart; \ No newline at end of file diff --git a/client/src/components/SentimentValue.js b/client/src/components/SentimentValue.js new file mode 100644 index 0000000..acaf2ce --- /dev/null +++ b/client/src/components/SentimentValue.js @@ -0,0 +1,17 @@ +// src/components/SentimentValue.js +import React from 'react'; +import { useApp } from '../AppContext'; + +function SentimentValue() { + const { SentimentScore, setSentimentScore } = useApp() + // 점수에 따라 색상 달리 표시하는 간단한 예시 + const color = SentimentScore > 0.5 ? 'green' : 'red'; + + return ( +
+ 감성 분석 값: {SentimentScore} +
+ ); +} + +export default SentimentValue; diff --git a/client/src/components/VideoPlayer.js b/client/src/components/VideoPlayer.js new file mode 100644 index 0000000..c5408ef --- /dev/null +++ b/client/src/components/VideoPlayer.js @@ -0,0 +1,74 @@ +import React, { useRef, useEffect } from 'react'; +import Hls from 'hls.js'; + +const VideoPlayer = ({ + src, + autoPlay = true, + controls = true, + width = '100%', + height = 'auto', + style = {}, +}) => { + const videoRef = useRef(null); + + useEffect(() => { + const video = videoRef.current; + + if (!video) return; + + // Hls.js 지원 여부 확인 + if (Hls.isSupported()) { + console.log('잘 되고 있음'); + const hls = new Hls(); + + // HLS 스트림 로드 + hls.loadSource(src); + hls.attachMedia(video); + + // 실시간 업데이트를 처리하기 위해 HLS 이벤트 설정 + hls.on(Hls.Events.MANIFEST_PARSED, () => { + console.log('HLS manifest loaded'); + }); + + hls.on(Hls.Events.ERROR, (event, data) => { + if (data.fatal) { + console.error('Fatal HLS error:', data); + if (data.type === Hls.ErrorTypes.NETWORK_ERROR) { + console.warn('Attempting to recover from network error...'); + hls.startLoad(); + } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) { + console.warn('Attempting to recover from media error...'); + hls.recoverMediaError(); + } else { + console.error('Cannot recover from error, destroying HLS instance.'); + hls.destroy(); + } + } + }); + + return () => { + // HLS 인스턴스 정리 + hls.destroy(); + }; + } + // Safari 및 HLS 네이티브 지원 + else if (video.canPlayType('application/vnd.apple.mpegurl')) { + video.src = src; + } + // HLS 미지원 브라우저 + else { + console.warn('HLS is not supported in this browser.'); + } + }, [src]); + + return ( +