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 (
+
+ );
+};
+
+export default VideoPlayer;
diff --git a/client/src/components/chat/ChatContainer.js b/client/src/components/chat/ChatContainer.js
new file mode 100644
index 0000000..e0f53aa
--- /dev/null
+++ b/client/src/components/chat/ChatContainer.js
@@ -0,0 +1,38 @@
+import React from 'react';
+import styled from 'styled-components';
+import ChatInput from './ChatInput';
+import SendChatButton from './SendChatButton';
+import ChatWindow from './ChatWindow';
+import Chatbot from './Chatbot';
+
+// 전체 컨테이너 중앙 정렬
+const ChatContainerWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center; /* 가로축 중앙 정렬 */
+ min-height: 100vh; /* 화면 전체 높이 */
+`;
+
+const InputContainer = styled.div`
+ margin-top: 10px;
+ display: flex;
+ align-items: stretch;
+ justify-content: space-between;
+ border: none;
+ background-color: #f9f9f9;
+`;
+
+const ChatContainer = () => {
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ChatContainer;
\ No newline at end of file
diff --git a/client/src/components/chat/ChatInput.js b/client/src/components/chat/ChatInput.js
new file mode 100644
index 0000000..67f0796
--- /dev/null
+++ b/client/src/components/chat/ChatInput.js
@@ -0,0 +1,38 @@
+import React from 'react';
+import styled from 'styled-components';
+import { useApp } from '../../AppContext';
+
+
+const StyledInput = styled.input`
+ flex: 1;
+ padding: 10px;
+ font-size: 14px;
+ border: 1px solid #ccc;
+ border-radius: 8px;
+ outline: none;
+ margin-right: 10px;
+ background-color: #ffffff;
+
+ &:focus {
+ border-color: ${(props) => props.theme.colors.primaryLight};
+ box-shadow: 0 0 5px ${(props) => props.theme.colors.primary};
+ }
+`;
+
+function ChatInput() {
+ const { currentMessage, setCurrentMessage} = useApp()
+ const handleChange = (e) => {
+ setCurrentMessage(e.target.value);
+ };
+
+ return (
+
+ );
+}
+
+export default ChatInput;
diff --git a/client/src/components/chat/ChatWindow.js b/client/src/components/chat/ChatWindow.js
new file mode 100644
index 0000000..f06593b
--- /dev/null
+++ b/client/src/components/chat/ChatWindow.js
@@ -0,0 +1,63 @@
+import React from 'react';
+import styled from 'styled-components';
+import { useApp } from '../../AppContext';
+
+const WindowContainer = styled.div`
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 10px;
+ height: 300px;
+ min-width: 240px;
+ max-width: 100%;
+ overflow-y: auto;
+ background-color: #f9f9f9;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+`;
+
+const MessageContainer = styled.div`
+ display: flex;
+ justify-content: ${(props) => (props.isUser ? 'flex-end' : 'flex-start')};
+`;
+
+const Message = styled.div`
+ padding: 10px 15px;
+ border-radius: 20px;
+ background-color: ${(props) => (props.isUser ? '#d1f7c4' : '#ffffff')};
+ color: #000000;
+ font-size: 14px;
+ max-width: 70%;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ position: relative;
+
+ /* 말풍선 꼬리 */
+ &:after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ ${(props) =>
+ props.isUser
+ ? 'right: -10px; border-width: 10px 0 10px 10px; border-color: transparent transparent transparent #d1f7c4;'
+ : 'left: -10px; border-width: 10px 10px 10px 0; border-color: transparent #ffffff transparent transparent;'};
+ border-style: solid;
+ transform: translateY(-50%);
+ }
+`;
+
+const ChatWindow = () => {
+ const { messages } = useApp();
+
+ return (
+
+ {messages.map((msg, index) => (
+
+ {msg}
+
+ ))}
+
+ );
+};
+
+export default ChatWindow;
diff --git a/client/src/components/chat/Chatbot.js b/client/src/components/chat/Chatbot.js
new file mode 100644
index 0000000..4d8b05a
--- /dev/null
+++ b/client/src/components/chat/Chatbot.js
@@ -0,0 +1,92 @@
+import React from 'react';
+import styled from 'styled-components';
+import { useApp } from '../../AppContext';
+
+// 컨테이너 스타일 (가로 배치)
+const SettingContainer = styled.div`
+ display: flex;
+ align-items: center; /* 수직 정렬 */
+ gap: 20px; /* 컴포넌트 간 간격 */
+ padding: 10px;
+ background-color: ${(props) => props.theme.colors.background};
+`;
+
+// 토글 버튼 스타일
+const ToggleButton = styled.button`
+ width: 50px;
+ height: 25px;
+ border-radius: 15px;
+ border: none;
+ background-color: ${(props) =>
+ props.active ? props.theme.colors.primary : props.theme.colors.border};
+ cursor: pointer;
+ position: relative;
+ outline: none;
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: ${(props) => (props.active ? '25px' : '2px')};
+ width: 21px;
+ height: 21px;
+ background-color: white;
+ border-radius: 50%;
+ transition: all 0.3s ease;
+ }
+`;
+
+// 드롭다운 스타일
+const VoiceSelect = styled.select`
+ padding: 5px 10px;
+ border: 1px solid ${(props) => props.theme.colors.border};
+ border-radius: 4px;
+ background-color: white;
+ font-size: 14px;
+ font-family: ${(props) => props.theme.fonts.base};
+`;
+
+const Label = styled.span`
+ font-size: 14px;
+ color: ${(props) => props.theme.colors.text};
+ margin-right: 8px;
+`;
+
+function Chatbot() {
+ const { useVoice, setUseVoice, voiceName, setVoiceName } = useApp();
+ const voiceNames = ['잇섭', '아이유'];
+
+ // 토글 상태 변경
+ const handleToggle = () => {
+ setUseVoice((prev) => !prev);
+ };
+
+ // 목소리 모드 변경
+ const handleModeChange = (e) => {
+ setVoiceName(e.target.value);
+ };
+
+ return (
+
+ {/* 소리 토글 */}
+
+
+
+
+
+ {/* 목소리 모드 선택 */}
+
+
+
+ {voiceNames.map((name) => (
+
+ ))}
+
+
+
+ );
+}
+
+export default Chatbot;
diff --git a/client/src/components/chat/SendChatButton.js b/client/src/components/chat/SendChatButton.js
new file mode 100644
index 0000000..10f498f
--- /dev/null
+++ b/client/src/components/chat/SendChatButton.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import styled from 'styled-components';
+import { useApp } from '../../AppContext';
+
+const StyledButton = styled.button`
+ padding: 5px 10px;
+ font-size: 15px;
+ font-weight: bold;
+ color: #ffffff;
+ background-color: ${(props) => props.theme.colors.primaryDark};
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+
+ &:hover {
+ background-color: ${(props) => props.theme.colors.primary};
+ transform: translateY(-1px);
+ box-shadow: 0 6px 8px rgba(0, 0, 0, 0.2);
+ }
+
+ &:active {
+ background-color: ${(props) => props.theme.colors.primaryLight};
+ transform: translateY(0);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ }
+`;
+
+function SendChatButton() {
+ const { handleSendMessage } = useApp()
+ return 전송;
+}
+
+export default SendChatButton;
diff --git a/client/src/index.css b/client/src/index.css
new file mode 100644
index 0000000..ec2585e
--- /dev/null
+++ b/client/src/index.css
@@ -0,0 +1,13 @@
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
diff --git a/client/src/index.js b/client/src/index.js
new file mode 100644
index 0000000..99bfb58
--- /dev/null
+++ b/client/src/index.js
@@ -0,0 +1,13 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { ThemeProvider } from 'styled-components';
+import { theme } from './themes';
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
\ No newline at end of file
diff --git a/client/src/themes/index.js b/client/src/themes/index.js
new file mode 100644
index 0000000..32c0ca2
--- /dev/null
+++ b/client/src/themes/index.js
@@ -0,0 +1,15 @@
+// src/theme.js
+export const theme = {
+ colors: {
+ primary: '#4C8BF5', // 메인 컬러 (밝고 선명한 파랑)
+ primaryLight: '#81ABF7', // (옵션) 조금 더 밝은 톤
+ primaryDark: '#3570CB', // (옵션) 조금 더 어두운 톤
+ text: '#333', // 기본 텍스트 색상
+ background: '#f8f9fa', // 페이지 배경색
+ border: '#ccc', // 테두리(경계선) 색
+ },
+ fonts: {
+ base: "'Noto Sans KR', sans-serif",
+ },
+ };
+
\ No newline at end of file