diff --git a/.env.development b/.env.development
new file mode 100644
index 0000000..93b3654
--- /dev/null
+++ b/.env.development
@@ -0,0 +1,2 @@
+REACT_APP_API_URL = http://localhost:8081
+REACT_APP_CHAT_URL = ws://localhost:8082/chat
diff --git a/.env.production b/.env.production
new file mode 100644
index 0000000..3cf66e1
--- /dev/null
+++ b/.env.production
@@ -0,0 +1,2 @@
+REACT_APP_API_URL = https://api.lemonair.me
+REACT_APP_CHAT_URL = wss://chat.lemonair.me/chat
diff --git a/package-lock.json b/package-lock.json
index f92f04d..10631c1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,7 +14,9 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.6.2",
+ "dotenv": "^16.3.1",
"hls.js": "^1.4.14",
+ "http-proxy-middleware": "^2.0.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-player": "^2.14.0",
@@ -7257,11 +7259,14 @@
}
},
"node_modules/dotenv": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
- "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
+ "version": "16.3.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
+ "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
"engines": {
- "node": ">=10"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/motdotla/dotenv?sponsor=1"
}
},
"node_modules/dotenv-expand": {
@@ -15224,6 +15229,14 @@
}
}
},
+ "node_modules/react-scripts/node_modules/dotenv": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
+ "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/react-stacked-center-carousel": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/react-stacked-center-carousel/-/react-stacked-center-carousel-1.0.13.tgz",
diff --git a/package.json b/package.json
index 97672b7..52bbf6d 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,9 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.6.2",
+ "dotenv": "^16.3.1",
"hls.js": "^1.4.14",
+ "http-proxy-middleware": "^2.0.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-player": "^2.14.0",
diff --git a/src/App.js b/src/App.js
index d7043e8..2e7f0f4 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,5 +1,5 @@
-import "./App.css";
-import Router from "./shared/Router";
+import './App.css';
+import Router from './shared/Router';
const App = () => {
return ;
diff --git a/src/components/Chat.js b/src/components/Chat.js
index a1b4205..22d2beb 100644
--- a/src/components/Chat.js
+++ b/src/components/Chat.js
@@ -41,6 +41,15 @@ const InputContainer = styled.div`
justify-content: center;
`;
+const NotLoginInputContainer = styled.div`
+ margin-top: auto;
+ border-top: 1px solid #333333;
+ height: 8vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`;
+
const TextInput = styled.input`
border-radius: 10px;
width: 70%;
@@ -65,19 +74,29 @@ const ChatComponent = ({ chattingRoomId }) => {
const [messages, setMessages] = useState([]);
const [inputMessage, setInputMessage] = useState('');
const [socket, setSocket] = useState(null);
+ const [socketIntervalId, setSocketIntervalId] = useState(null);
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
const messagesEndRef = useRef(null);
const chattingRoomIdString = chattingRoomId;
const accessToken = localStorage.getItem('accessToken');
+ useEffect(() => {
+ if (accessToken) {
+ setIsLoggedIn(true);
+ }
+ }, [accessToken]);
const fetchToken = useCallback(async () => {
try {
console.log(accessToken);
- const response = await fetch('https://api.lemonair.me/api/auth/chat', {
- method: 'POST',
- headers: {
- Authorization: accessToken,
- },
- });
+ const response = await fetch(
+ `${process.env.REACT_APP_API_URL}/api/auth/chat`,
+ {
+ method: 'POST',
+ headers: {
+ Authorization: accessToken,
+ },
+ }
+ );
if (!response.ok) {
throw new Error('Network response was not ok.');
}
@@ -99,15 +118,20 @@ const ChatComponent = ({ chattingRoomId }) => {
chatToken = 'notlogin'; // 로그인하지 않은 사용자의 경우 토큰 정보를 notlogin으로 요청한다.
}
const newSocket = new WebSocket(
- `wss://chat.lemonair.me/chat/${chattingRoomIdString}/${chatToken}`
+ `${process.env.REACT_APP_CHAT_URL}/${chattingRoomIdString}/${chatToken}`
);
setSocket(newSocket);
newSocket.onopen = () => {
console.log('웹소켓 연결됨');
+ const heartbeatInterval = setInterval(() => {
+ newSocket.send('heartbeat');
+ }, 30000);
+
+ setSocketIntervalId(heartbeatInterval);
};
newSocket.onmessage = (event) => {
- console.log(event.data);
+ // console.log(event.data);
const receiveData = event.data.split(':');
const from = receiveData[0];
const message = receiveData.slice(1).join(':').trim();
@@ -116,6 +140,8 @@ const ChatComponent = ({ chattingRoomId }) => {
newSocket.onclose = () => {
console.log('웹소켓 연결 종료');
+ clearInterval(socketIntervalId);
+
// 연결 종료 시 재연결 시도
setTimeout(async () => {
if (accessToken) {
@@ -124,26 +150,36 @@ const ChatComponent = ({ chattingRoomId }) => {
chatToken = 'notlogin'; // 로그인하지 않은 사용자의 경우 토큰 정보를 notlogin으로 요청한다.
}
const reconnectSocket = new WebSocket(
- `wss://chat.lemonair.me/chat/${chattingRoomIdString}/${chatToken}`
+ `${process.env.REACT_APP_CHAT_URL}/${chattingRoomIdString}/${chatToken}`
);
setSocket(reconnectSocket);
reconnectSocket.onopen = () => {
console.log('웹소켓 재연결됨');
setSocket(reconnectSocket);
+ const heartbeatInterval = setInterval(() => {
+ newSocket.send('heartbeat');
+ }, 30000);
+ setSocketIntervalId(heartbeatInterval);
};
reconnectSocket.onmessage = (event) => {
- console.log(event.data);
+ // console.log(event.data);
const receiveData = event.data.split(':');
const from = receiveData[0];
const message = receiveData.slice(1).join(':').trim();
setMessages((prevMessages) => [...prevMessages, { from, message }]);
};
+
+ reconnectSocket.onclose = () => {
+ console.log('웹소켓 연결 종료');
+ clearInterval(socketIntervalId);
+ };
}, 1000); // 1초 후 재연결 시도
};
return () => {
newSocket.close();
+ clearInterval(socketIntervalId);
};
};
@@ -164,6 +200,11 @@ const ChatComponent = ({ chattingRoomId }) => {
};
const handleKeyDown = (event) => {
+ if (!isLoggedIn && event.key === 'Enter') {
+ setInputMessage('');
+ return;
+ }
+
if (event.key === 'Enter') {
event.preventDefault();
sendMessage();
@@ -181,15 +222,26 @@ const ChatComponent = ({ chattingRoomId }) => {
))}
-
- setInputMessage(e.target.value)}
- onKeyDown={handleKeyDown}
- />
- 전송
-
+ {isLoggedIn ? (
+
+ setInputMessage(e.target.value)}
+ onKeyDown={handleKeyDown}
+ />
+
+ 전송
+
+
+ ) : (
+
+ 로그인한 사용자만 채팅을 입력할 수 있습니다.
+
+ )}
);
};
diff --git a/src/components/HlsPlayer.js b/src/components/HlsPlayer.js
index 6231803..8d6097b 100644
--- a/src/components/HlsPlayer.js
+++ b/src/components/HlsPlayer.js
@@ -1,5 +1,5 @@
import React, { useEffect, useRef } from 'react';
-import Hls from 'hls.js';
+import Hls from 'hls.js/dist/hls.min';
const HlsVideoPlayer = ({ videoUrl }) => {
const videoRef = useRef(null);
@@ -14,21 +14,24 @@ const HlsVideoPlayer = ({ videoUrl }) => {
console.log('initialize 실행');
// 자동재생
console.log('play 실행 전');
- videoElement.play();
+
+ // videoElement.play();
console.log('play 실행 후ㅡ');
// 마지막 청크의 재생시간으로 이동
console.log('hls', hls);
+ // videoElement.muted = false;
// console.log("hls.media.current.segments", hls.media.current.segments);
- console.log(data.lastSegment);
- console.log(hls.media.segments);
- const lastSegment = hls.media.segments[hls.media.segments.length - 1];
- console.log('lastSegment', lastSegment);
+
+ let myFragments = data.levels[0].details.fragments;
+ // console.log(hls.media.segments);
+ const lastSegment = myFragments[myFragments.length - 1];
+
+ // console.log("lastSegment", lastSegment);
if (lastSegment) {
- videoElement.currentTime = lastSegment.end;
+ videoElement.currentTime = lastSegment.start - 0.5;
}
};
- // Hls 지원 여부 확인
if (Hls.isSupported()) {
console.log('hls를 지원한다.');
var config = {
@@ -130,11 +133,6 @@ const HlsVideoPlayer = ({ videoUrl }) => {
};
hls = new Hls(config);
- // 이벤트 리스너 등록
- // hls.on(Hls.Events.MEDIA_ATTACHED, (event, data) => {
- // console.log("event listener 등록");
- // initializeHls(data);
- // });
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
console.log('video and hls.js are now bound together !');
@@ -159,10 +157,20 @@ const HlsVideoPlayer = ({ videoUrl }) => {
console.log('컴포넌트 unmount시에 destroy');
hls.destroy();
}
+ if (videoElement) {
+ console.log('video pause');
+ videoElement.pause();
+ }
};
- }, [videoUrl]);
+ }, [videoUrl, videoRef.current]);
- return ;
+ return (
+
+ {videoRef ? (
+
+ ) : null}
+
+ );
};
export default HlsVideoPlayer;
diff --git a/src/components/ReactPlayer.js b/src/components/ReactPlayer.js
index cf554f3..5bfce35 100644
--- a/src/components/ReactPlayer.js
+++ b/src/components/ReactPlayer.js
@@ -6,8 +6,8 @@
//
+
-
+
{
if (!isCenterSlide) swipeTo(slideIndex);
}}
/>
-
-
+
+
{text}
-
{channelId}
+ {/*
{channelId}
*/}
diff --git a/src/components/header.js b/src/components/header.js
index 0adf5c0..8dfc080 100644
--- a/src/components/header.js
+++ b/src/components/header.js
@@ -1,9 +1,9 @@
-import React, { useState } from "react";
-import styled from "styled-components";
-import { Link } from "react-router-dom";
-import { ReactComponent as LemonSVG } from "../images/lemon.svg";
-import SignupModal from "../modal/SignupModal";
-import LoginModal from "../modal/LoginModal";
+import React, { useState } from 'react';
+import styled from 'styled-components';
+import { Link } from 'react-router-dom';
+import { ReactComponent as LemonSVG } from '../images/lemon.svg';
+import SignupModal from '../modal/SignupModal';
+import LoginModal from '../modal/LoginModal';
const HeaderContainer = styled.header`
display: flex;
@@ -50,7 +50,7 @@ const UserOptions = styled.div`
text-decoration: none;
color: #555;
padding-right: 25px;
- font-family: "Reenie Beanie", cursive;
+ font-family: 'Reenie Beanie', cursive;
}
a:hover {
@@ -65,13 +65,13 @@ const HeaderText = styled.span`
text-decoration: none;
color: #555;
padding-right: 25px;
- font-family: "Reenie Beanie", cursive;
+ font-family: 'Reenie Beanie', cursive;
`;
const Header = () => {
const [isSignupModalOpen, setIsSignupModalOpen] = useState(false);
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
- const accessToken = localStorage.getItem("accessToken");
+ const accessToken = localStorage.getItem('accessToken');
const openSignupModal = () => {
setIsSignupModalOpen(true);
@@ -91,7 +91,7 @@ const Header = () => {
const handleLogout = async () => {
try {
- const response = await fetch('https://api.lemonair.me/api/logout', {
+ const response = await fetch(`${process.env.REACT_APP_API_URL}/api/logout`, {
method: 'POST',
headers: {
Authorization: accessToken,
@@ -99,22 +99,22 @@ const Header = () => {
});
if (response.ok) {
- localStorage.removeItem("accessToken");
- localStorage.removeItem("refreshToken");
- alert("로그아웃 되었습니다.");
+ localStorage.removeItem('accessToken');
+ localStorage.removeItem('refreshToken');
+ alert('로그아웃 되었습니다.');
window.location.reload();
} else {
- console.log("로그아웃에 실패하였습니다.");
+ console.log('로그아웃에 실패하였습니다.');
}
} catch (error) {
- console.error("로그아웃 중 오류 발생:", error);
+ console.error('로그아웃 중 오류 발생:', error);
}
};
return (
-
-
+
+
diff --git a/src/modal/LoginModal.js b/src/modal/LoginModal.js
index be820c4..edf0a77 100644
--- a/src/modal/LoginModal.js
+++ b/src/modal/LoginModal.js
@@ -1,6 +1,6 @@
-import React, { useState } from "react";
-import styled from "styled-components";
-import useInput from "../hooks/useInput";
+import React, { useState } from 'react';
+import styled from 'styled-components';
+import useInput from '../hooks/useInput';
const ModalBackground = styled.div`
position: fixed;
@@ -34,7 +34,7 @@ const Title = styled.h2`
font-size: 2rem;
color: #555;
margin-bottom: 15px;
- font-family: "Reenie Beanie", cursive;
+ font-family: 'Reenie Beanie', cursive;
`;
const InputField = styled.input`
@@ -46,7 +46,7 @@ const InputField = styled.input`
font-size: 13px;
padding-left: 1.2%;
line-height: 16px;
- font-family: "Roboto", sans-serif;
+ font-family: 'Roboto', sans-serif;
margin-bottom: 1vh;
`;
@@ -62,7 +62,7 @@ const SubmitButton = styled.button`
color: #4c3c00;
margin: 0 auto;
margin-top: 6%;
- font-family: "Gamja Flower", sans-serif;
+ font-family: 'Gamja Flower', sans-serif;
&:hover {
background-color: #ffea00;
@@ -77,13 +77,13 @@ const Label = styled.label`
color: #333;
text-align: left;
padding-left: 15%;
- font-family: "Gamja Flower", sans-serif;
+ font-family: 'Gamja Flower', sans-serif;
`;
const LoginModal = ({ closeModal }) => {
- const [loginId, onChangeLoginId] = useInput("");
- const [password, onChangePassword] = useInput("");
- const [error, setError] = useState("");
+ const [loginId, onChangeLoginId] = useInput('');
+ const [password, onChangePassword] = useInput('');
+ const [error, setError] = useState('');
const handleOutsideClick = (e) => {
if (e.target === e.currentTarget) {
@@ -100,24 +100,28 @@ const LoginModal = ({ closeModal }) => {
};
try {
- const response = await fetch('https://api.lemonair.me/api/login', {
- method: 'POST',
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(loginData),
- });
+ const response = await fetch(
+ `${process.env.REACT_APP_API_URL}/api/login`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(loginData),
+ }
+ );
if (response.ok) {
const data = await response.json();
- localStorage.setItem("accessToken", data.accessToken);
- localStorage.setItem("refreshToken", data.refreshToken);
+ localStorage.setItem('accessToken', data.accessToken);
+ localStorage.setItem('refreshToken', data.refreshToken);
+ window.location.reload();
closeModal();
} else {
- setError("아이디 또는 비밀번호가 올바르지 않습니다.");
+ setError('아이디 또는 비밀번호가 올바르지 않습니다.');
}
} catch (error) {
- setError("오류가 발생했습니다.");
+ setError('오류가 발생했습니다.');
}
};
return (
@@ -125,24 +129,24 @@ const LoginModal = ({ closeModal }) => {
Login
diff --git a/src/modal/SignupModal.js b/src/modal/SignupModal.js
index cf97bb8..d74afee 100644
--- a/src/modal/SignupModal.js
+++ b/src/modal/SignupModal.js
@@ -1,6 +1,6 @@
-import React, { useState } from "react";
-import styled from "styled-components";
-import useInput from "../hooks/useInput";
+import React, { useState } from 'react';
+import styled from 'styled-components';
+import useInput from '../hooks/useInput';
const ModalBackground = styled.div`
position: fixed;
@@ -33,7 +33,7 @@ const Title = styled.h2`
font-size: 2rem;
color: #555;
margin-bottom: 15px;
- font-family: "Reenie Beanie", cursive;
+ font-family: 'Reenie Beanie', cursive;
`;
const InputField = styled.input`
@@ -45,7 +45,7 @@ const InputField = styled.input`
font-size: 13px;
padding-left: 1.2%;
line-height: 16px;
- font-family: "Roboto", sans-serif;
+ font-family: 'Roboto', sans-serif;
`;
const SubmitButton = styled.button`
@@ -59,7 +59,7 @@ const SubmitButton = styled.button`
background-color: #f8de7e;
color: #4c3c00;
margin: 0 auto;
- font-family: "Gamja Flower", sans-serif;
+ font-family: 'Gamja Flower', sans-serif;
&:hover {
background-color: #ffea00;
@@ -74,16 +74,16 @@ const Label = styled.label`
color: #333;
text-align: left;
padding-left: 15%;
- font-family: "Gamja Flower", sans-serif;
+ font-family: 'Gamja Flower', sans-serif;
`;
const SignupModal = ({ closeModal }) => {
- const [loginId, onChangeLoginId] = useInput("");
- const [email, onChangeEmail] = useInput("");
- const [password, onChangePassword] = useInput("");
- const [password2, onChangePassword2] = useInput("");
- const [nickname, onChangeNickname] = useInput("");
- const [error, setError] = useState("");
+ const [loginId, onChangeLoginId] = useInput('');
+ const [email, onChangeEmail] = useInput('');
+ const [password, onChangePassword] = useInput('');
+ const [password2, onChangePassword2] = useInput('');
+ const [nickname, onChangeNickname] = useInput('');
+ const [error, setError] = useState('');
const handleOutsideClick = (e) => {
if (e.target === e.currentTarget) {
@@ -95,12 +95,12 @@ const SignupModal = ({ closeModal }) => {
e.preventDefault();
if (!loginId || !email || !password || !password2 || !nickname) {
- setError("모든 필드를 입력해주세요.");
+ setError('모든 필드를 입력해주세요.');
return;
}
if (password !== password2) {
- setError("비밀번호가 일치하지 않습니다.");
+ setError('비밀번호가 일치하지 않습니다.');
return;
}
@@ -113,10 +113,10 @@ const SignupModal = ({ closeModal }) => {
};
try {
- const response = await fetch('https://api.lemonair.me/api/signup', {
+ const response = await fetch(`${process.env.REACT_APP_API_URL}/api/signup`, {
method: 'POST',
headers: {
- "Content-Type": "application/json",
+ 'Content-Type': 'application/json',
},
body: JSON.stringify(signupData),
});
@@ -130,11 +130,11 @@ stream key : ${data.streamKey}`
);
closeModal();
} else {
- setError("회원가입에 실패했습니다.");
+ setError('회원가입에 실패했습니다.');
// 서버 에러 정의되면 서버에러로 바꿔야 함
}
} catch (error) {
- setError("오류가 발생했습니다.");
+ setError('오류가 발생했습니다.');
// 서버 에러 정의되면 서버에러로 바꿔야 함
}
};
@@ -144,48 +144,48 @@ stream key : ${data.streamKey}`
Signup
diff --git a/src/pages/ChannelDetail.js b/src/pages/ChannelDetail.js
index 28d7a6b..0615170 100644
--- a/src/pages/ChannelDetail.js
+++ b/src/pages/ChannelDetail.js
@@ -5,6 +5,7 @@ import styled from 'styled-components';
import Chat from '../components/Chat';
import HlsVideoPlayer from '../components/HlsPlayer';
+
const StreamingContainer = styled.div`
width: 100%;
height: 100%;
@@ -41,18 +42,15 @@ const ChannelDetail = () => {
const fetchData = async () => {
try {
const response = await fetch(
- `https://api.lemonair.me/api/channels/${id}`
+ `${process.env.REACT_APP_API_URL}/api/channels/${id}`
);
if (!response.ok) {
throw new Error('Network response was not ok.');
}
const data = await response.json();
setChannelData(data);
- console.log('title:', data.title);
- console.log('url:', data.hlsUrl);
- console.log('roomid:', data.chattingRoomId);
} catch (error) {
- console.error('데이터를 불러오는 중 오류가 발생했습니다:', error);
+ console.error("데이터를 불러오는 중 오류가 발생했습니다:", error);
}
};
diff --git a/src/pages/Home.js b/src/pages/Home.js
index 064dfed..0b93404 100644
--- a/src/pages/Home.js
+++ b/src/pages/Home.js
@@ -13,7 +13,7 @@ const Home = () => {
const [data, setData] = useState([]);
useEffect(() => {
- fetch('https://api.lemonair.me/api/channels')
+ fetch(`${process.env.REACT_APP_API_URL}/api/channels`)
.then((response) => response.json())
.then((data) => {
setData(data);
diff --git a/src/sbltest/Channel.js b/src/sbltest/Channel.js
deleted file mode 100644
index a930c09..0000000
--- a/src/sbltest/Channel.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import React, { useEffect, useState } from "react";
-import axios from "axios";
-
-const ChannelInfo = () => {
- const [channelData, setChannelData] = useState([]);
-
- useEffect(() => {
- const fetchData = async () => {
- try {
- const response = await axios.get("http://localhost:8081/api/channels");
- setChannelData(response.data);
- } catch (error) {
- console.error("Error fetching data:", error);
- }
- };
-
- fetchData();
- }, []);
-
- return (
-
-
- {channelData.map((channel) => (
- -
- {/* 여기에 채널 데이터를 표시하는 UI 코드 추가 */}
- {channel.channelName}
-
- ))}
-
-
- );
-};
-
-export default ChannelInfo;