diff --git a/.github/workflows/deploy-front.yml b/.github/workflows/deploy-front.yml index 80b6a15..67e2288 100644 --- a/.github/workflows/deploy-front.yml +++ b/.github/workflows/deploy-front.yml @@ -1,56 +1,56 @@ name: auto deploy front on: - push: - branches: - - dev + push: + branches: + - dev jobs: - docker_push_front: - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v3 + docker_push_front: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 - - name: Login to NCP Container Registry - uses: docker/login-action@v2 - with: - registry: ${{ secrets.NCP_CONTAINER_REGISTRY }} - username: ${{ secrets.NCP_ACCESS_KEY }} - password: ${{ secrets.NCP_SECRET_KEY }} + - name: Login to NCP Container Registry + uses: docker/login-action@v2 + with: + registry: ${{ secrets.NCP_CONTAINER_REGISTRY }} + username: ${{ secrets.NCP_ACCESS_KEY }} + password: ${{ secrets.NCP_SECRET_KEY }} - - name: Build and Push Docker Image - uses: docker/build-push-action@v3 - with: - context: ./client - file: ./client/Dockerfile - push: true - tags: ${{ secrets.NCP_CONTAINER_REGISTRY }}/front - cache-from: type=registry,ref=${{ secrets.NCP_CONTAINER_REGISTRY }}/front - cache-to: type=inline + - name: Build and Push Docker Image + uses: docker/build-push-action@v3 + with: + context: ./client + file: ./client/Dockerfile + push: true + tags: ${{ secrets.NCP_CONTAINER_REGISTRY }}/front + cache-from: type=registry,ref=${{ secrets.NCP_CONTAINER_REGISTRY }}/front + cache-to: type=inline - docker_pull_front: - name: Connect server ssh and pull frontend from container registry - needs: docker_push_front - runs-on: ubuntu-latest - steps: - - name: connect ssh - uses: appleboy/ssh-action@master - with: - host: ${{ secrets.FRONT_HOST }} - username: ${{ secrets.FRONT_USERNAME }} - password: ${{ secrets.FRONT_PASSWORD }} - port: ${{ secrets.FRONT_PORT }} - script: | - docker pull ${{ secrets.NCP_CONTAINER_REGISTRY }}/front - docker stop $(docker ps -a -q) - docker rm $(docker ps -a -q) - docker run -d -p 80:80 -p 443:443 --name front ${{ secrets.NCP_CONTAINER_REGISTRY }}/front - docker image prune -f - docker cp /etc/letsencrypt/archive/gbs-live.site/fullchain1.pem front:/ - docker cp /etc/letsencrypt/archive/gbs-live.site/privkey1.pem front:/ - docker cp default.conf front:/etc/nginx/conf.d/ - docker exec front nginx -s reload + docker_pull_front: + name: Connect server ssh and pull frontend from container registry + needs: docker_push_front + runs-on: ubuntu-latest + steps: + - name: connect ssh + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.FRONT_HOST }} + username: ${{ secrets.FRONT_USERNAME }} + password: ${{ secrets.FRONT_PASSWORD }} + port: ${{ secrets.FRONT_PORT }} + script: | + docker pull ${{ secrets.NCP_CONTAINER_REGISTRY }}/front + docker stop $(docker ps -a -q) + docker rm $(docker ps -a -q) + docker run -d -p 80:80 -p 443:443 --name front --env-file ${{ secrets.ENV_PATH }} ${{ secrets.NCP_CONTAINER_REGISTRY }}/front + docker image prune -f + docker cp /etc/letsencrypt/archive/gbs-live.site/fullchain1.pem front:/ + docker cp /etc/letsencrypt/archive/gbs-live.site/privkey1.pem front:/ + docker cp default.conf front:/etc/nginx/conf.d/ + docker exec front nginx -s reload diff --git a/client/Dockerfile b/client/Dockerfile index e7005fc..3808dac 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -4,7 +4,7 @@ FROM --platform=linux/amd64 node:lts-alpine as build-stage WORKDIR /app # 의존성 파일 복사 및 설치 -COPY package.json yarn.lock .env ./ +COPY package.json yarn.lock ./ RUN yarn install && yarn global add typescript # 소스 코드 복사 diff --git a/client/src/components/Broadcast/Broadcast.styles.tsx b/client/src/components/Broadcast/Broadcast.styles.tsx index 8185247..f46845e 100644 --- a/client/src/components/Broadcast/Broadcast.styles.tsx +++ b/client/src/components/Broadcast/Broadcast.styles.tsx @@ -22,7 +22,7 @@ export const Broadcast = styled.div` height: 15rem; ` -export const Thumbnail = styled.div` +export const Thumbnail = styled.img` border: 0.0625rem solid #000000; position: absolute; top: 1.875rem; diff --git a/client/src/components/Broadcast/Broadcast.tsx b/client/src/components/Broadcast/Broadcast.tsx index 83d7a1b..f1967af 100644 --- a/client/src/components/Broadcast/Broadcast.tsx +++ b/client/src/components/Broadcast/Broadcast.tsx @@ -3,17 +3,19 @@ import * as styles from './Broadcast.styles' import { themeState } from '@/states/theme' interface BroadcastProps { + thumbnail: string title: string nickname: string - viewer: string + viewer: number index: number } -const Broadcast = ({ title, nickname, viewer, index }: BroadcastProps) => { +const Broadcast = ({ thumbnail, title, nickname, viewer, index }: BroadcastProps) => { const theme = useRecoilValue(themeState) + return ( - + {title} {nickname} 시청자 {viewer}명 diff --git a/client/src/components/Modal/LoginModal/LoginModal.tsx b/client/src/components/Modal/LoginModal/LoginModal.tsx index 494619e..9b5f219 100644 --- a/client/src/components/Modal/LoginModal/LoginModal.tsx +++ b/client/src/components/Modal/LoginModal/LoginModal.tsx @@ -1,4 +1,3 @@ -import useApi from '@/hooks/useApi' import * as styles from './LoginModal.styles' import { ThemeFlag } from '@/types/theme' @@ -8,29 +7,28 @@ interface LoginModalProps { } const LoginModal = ({ onCancle, currentTheme }: LoginModalProps) => { - const [response, fetchApi] = useApi<{ userId: string; nickname: string }>() - const onLoginImage = () => { const popup = window.open(`${import.meta.env.VITE_API_URL}` + '/oauth/login/naver', '_blank', 'menubar=no, toolbar=no, width=500, height=600') - const popupEvent = async () => { - if (popup !== null && popup.closed === true) { - try { - await fetchApi('GET', '/users/me/', undefined, { credentials: 'include' }) - - if (response.data) { - const { userId, nickname } = response.data - - localStorage.setItem('user', JSON.stringify({ id: userId, nickname: nickname })) + const popupEvent = () => { + if (popup !== null && popup.closed == true) { + fetch(`${import.meta.env.VITE_API_URL}` + '/users/me/', { method: 'GET', credentials: 'include' }) + .then((res) => { + if (res.ok === true) { + return res.json() + } else { + throw new Error('Login Failed') + } + }) + .then((res) => { + const userId = res.userId + const userNickname = res.nickname + + localStorage.setItem('user', JSON.stringify({ id: userId, nickname: userNickname })) window.location.reload() - } else { - throw new Error('Login Failed') - } - } catch (error) { - console.error(response.error || error) - } finally { - window.removeEventListener('focus', popupEvent) - } + }) + .catch((err) => console.error(err)) + .finally(() => window.removeEventListener('focus', popupEvent)) } } diff --git a/client/src/components/Modal/SettingModal/SettingModal.tsx b/client/src/components/Modal/SettingModal/SettingModal.tsx index 1621b5f..0f16d3b 100644 --- a/client/src/components/Modal/SettingModal/SettingModal.tsx +++ b/client/src/components/Modal/SettingModal/SettingModal.tsx @@ -4,7 +4,6 @@ import * as styles from './SettingModal.styles' import { ThemeFlag } from '@/types/theme' import { themeState } from '@/states/theme' import { userState } from '@/states/user' -import useApi from '@/hooks/useApi' interface SettingModalProps { onConfirm: () => void @@ -12,19 +11,18 @@ interface SettingModalProps { const SettingModal = ({ onConfirm }: SettingModalProps) => { const [currentTheme, setTheme] = useRecoilState(themeState) - const isDarkMode = currentTheme === ThemeFlag.dark const [user, setUser] = useRecoilState(userState) const [id, setId] = useState(user.id) const [nickname, setNickname] = useState(user.nickname) const [streamKey, setStreamKey] = useState('') - const [response, fetchApi] = useApi() + const isDarkMode = currentTheme === ThemeFlag.dark const onToggleContainer = () => { setTheme(isDarkMode ? ThemeFlag.light : ThemeFlag.dark) localStorage.setItem('theme', `${currentTheme}`) } - const onIdInputButton = async () => { + const onIdInputButton = () => { if (id.trim() === '') { alert('올바른 ID를 입력해주세요.') setId(user.id) @@ -35,19 +33,34 @@ const SettingModal = ({ onConfirm }: SettingModalProps) => { return } - await fetchApi('PATCH', '/users/', { nickname: user.nickname, userId: id.trim() }) - - if (response.data) { - const { userId, nickname } = response.data - alert('ID가 저장되었습니다.') - setUser({ id: userId, nickname: nickname }) - localStorage.setItem('user', JSON.stringify({ id: userId, nickname: nickname })) - } else { - console.log(response.error) - } + + fetch(`${import.meta.env.VITE_API_URL}` + '/users', { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ nickname: user.nickname, userId: id.trim() }), + credentials: 'include', + }) + .then((res) => { + if (res.ok === true) { + return res.json() + } else { + throw new Error('ID Save Failed') + } + }) + .then((res) => { + const userId = res.userId + const userNickname = res.nickname + + alert('ID가 저장되었습니다.') + setUser({ id: userId, nickname: userNickname }) + localStorage.setItem('user', JSON.stringify({ id: userId, nickname: userNickname })) + }) + .catch((err) => console.error(err)) } - const onNicknameInputButton = async () => { + const onNicknameInputButton = () => { if (nickname.trim() === '') { alert('올바른 닉네임을 입력해주세요.') setNickname(user.nickname) @@ -58,32 +71,52 @@ const SettingModal = ({ onConfirm }: SettingModalProps) => { return } - await fetchApi('PATCH', '/users/', { nickname: nickname.trim(), userId: user.id }) - - if (response.data) { - const { userId, nickname } = response.data - alert('닉네임이 저장되었습니다.') - setUser({ id: userId, nickname: nickname }) - localStorage.setItem('user', JSON.stringify({ id: userId, nickname: nickname })) - } else { - console.log(response.error) - } - } - const onKeyInputButton = async () => { - await fetchApi('GET', '/stream-keys/me/') + fetch(`${import.meta.env.VITE_API_URL}` + '/users', { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ nickname: nickname.trim(), userId: user.id }), + credentials: 'include', + }) + .then((res) => { + if (res.ok === true) { + return res.json() + } else { + throw new Error('Nickname Save Failed') + } + }) + .then((res) => { + const userId = res.userId + const userNickname = res.nickname - if (response.data) { - const { streamKey } = response.data - setStreamKey(streamKey) - navigator.clipboard.writeText(streamKey).then(() => { - alert('방송 비밀 키가 클립보드에 복사되었습니다.') + alert('닉네임이 저장되었습니다.') + setUser({ id: userId, nickname: userNickname }) + localStorage.setItem('user', JSON.stringify({ id: userId, nickname: userNickname })) }) - } + .catch((err) => console.error(err)) + } + + const onKeyInputButton = () => { + navigator.clipboard.writeText(streamKey).then(() => { + alert('방송 비밀 키가 클립보드에 복사되었습니다.') + }) } useEffect(() => { - onKeyInputButton() + fetch(`${import.meta.env.VITE_API_URL}` + '/stream-keys/me', { method: 'GET', credentials: 'include' }) + .then((res) => { + if (res.ok === true) { + return res.json() + } else { + throw new Error('Get Stream Keys Failed') + } + }) + .then((res) => { + setStreamKey(res.streamKey) + }) + .catch((err) => console.error(err)) }, []) return ( diff --git a/client/src/pages/BroadcastPage/BroadcastPage.tsx b/client/src/pages/BroadcastPage/BroadcastPage.tsx index cbb8011..1d83ea5 100644 --- a/client/src/pages/BroadcastPage/BroadcastPage.tsx +++ b/client/src/pages/BroadcastPage/BroadcastPage.tsx @@ -12,7 +12,6 @@ import ConfirmModal from '@components/Modal/ConfirmModal/ConfirmModal' import Chatting from '@components/Chatting/Chatting' import { themeState } from '@/states/theme' import { userState } from '@/states/user' -import useApi from '@/hooks/useApi' interface ViewerModalProps { nickname: string @@ -27,11 +26,14 @@ interface ChattingProps { message: string } +interface StreamerInterface { + title: string + nickname: string + viewer: number +} + const BroadcastPage = () => { - const [response, fetchApi] = useApi() const { id } = useParams() - const nickname = '222' // TODO : 닉네임 response에 추가되면 삭제 - const [settingModal, setSettingModal] = useState(false) const [loginModal, setLoginModal] = useState(false) const [viewerModal, setViewerModal] = useState(false) @@ -41,6 +43,7 @@ const BroadcastPage = () => { const [chattingList, setChattingList] = useState>([]) const [loginCheckModal, setLoginCheckModal] = useState(false) const [emptyChattingModal, setEmptyChattingModal] = useState(false) + const [streamer, setStreamer] = useState({ title: '', nickname: '', viewer: 0 }) const socket = useRef(null) const theme = useRecoilValue(themeState) const user = useRecoilValue(userState) @@ -63,7 +66,7 @@ const BroadcastPage = () => { } const getTarget = (viewerNickname: string): 'viewer' | 'manager' | 'streamer' => { - if (viewerNickname === nickname) { + if (viewerNickname === streamer.nickname) { return 'streamer' } else if (manager.indexOf(viewerNickname) !== -1) { return 'manager' @@ -115,9 +118,28 @@ const BroadcastPage = () => { } } + const getStreamer = () => { + fetch(`${import.meta.env.VITE_API_URL}` + `/streams/${id}`, { method: 'GET', credentials: 'include' }) + .then((res) => { + if (res.ok === true) { + return res.json() + } else { + throw new Error('Get Streamer Data Failed') + } + }) + .then((res) => { + setStreamer({ title: res.title, nickname: res.nickname, viewer: res.viewer }) + }) + .catch((err) => console.error(err)) + } + useEffect(() => { - fetchApi('GET', `/streams/${id}`) - socket.current = io('https://api.gbs-live.site', { withCredentials: true }) + const interval = setInterval(() => { + getStreamer() + }, 30000) + + getStreamer() + socket.current = io(`${import.meta.env.VITE_API_URL}`, { withCredentials: true }) socket.current.emit('join', { room: id }) socket.current.on('chat', (chatting: ChattingProps) => { setChattingList((chattingList) => [chatting, ...chattingList]) @@ -127,78 +149,80 @@ const BroadcastPage = () => { if (socket.current) { socket.current.disconnect() } + if (interval) { + clearInterval(interval) + } } }, []) - if (response.data) - return ( - - - - - - - - {user.id === '' ? ( - - ) : ( - - )} - - - - - {chattingList.map((chatting, index) => ( - - ))} - - - setChatting(event.target.value)} onKeyDown={onEnter}> - - 등록하기 - - - - - {response.data.title} - {response.data.category} - 시청자 {response.data.viewer}명 - - {settingModal && } - {loginModal && } - {viewerModal && ( - - )} - {loginCheckModal && ( - { - setLoginCheckModal(false) - }} - currentTheme={theme} - /> - )} - {emptyChattingModal && ( - { - setEmptyChattingModal(false) - }} - currentTheme={theme} - /> + return ( + + + + + + + + {user.id === '' ? ( + + ) : ( + )} - - ) + + + + + {chattingList.map((chatting, index) => ( + + ))} + + + setChatting(event.target.value)} onKeyDown={onEnter}> + + 등록하기 + + + + + {streamer.title} + {streamer.nickname} + 시청자 {streamer.viewer}명 + + {settingModal && } + {loginModal && } + {viewerModal && ( + + )} + {loginCheckModal && ( + { + setLoginCheckModal(false) + }} + currentTheme={theme} + /> + )} + {emptyChattingModal && ( + { + setEmptyChattingModal(false) + }} + currentTheme={theme} + /> + )} + + ) } export default BroadcastPage diff --git a/client/src/pages/MainPage/MainPage.tsx b/client/src/pages/MainPage/MainPage.tsx index 4112d12..a1897af 100644 --- a/client/src/pages/MainPage/MainPage.tsx +++ b/client/src/pages/MainPage/MainPage.tsx @@ -8,29 +8,21 @@ import Broadcast from '@components/Broadcast/Broadcast' import SettingModal from '@components/Modal/SettingModal/SettingModal' import LoginModal from '@components/Modal/LoginModal/LoginModal' import EmptyList from '@components/EmptyList/EmptyList' -import useApi from '@/hooks/useApi' import { themeState } from '@/states/theme' import { userState } from '@/states/user' -interface BroadcastProps { +interface BroadcastInterface { userId: string + nickname: string title: string - category: string - desc: string - streamKey: string - viewer: string + viewer: number thumbnail: string - startedAt: string - resolution: string - frameRate: number } const MainPage = () => { - const [response, fetchApi] = useApi() - const [settingModal, setSettingModal] = useState(false) const [loginModal, setLoginModal] = useState(false) - const [broadcastList, setBroadcastList] = useState>([]) + const [broadcastList, setBroadcastList] = useState>([]) const theme = useRecoilValue(themeState) const user = useRecoilValue(userState) @@ -48,12 +40,22 @@ const MainPage = () => { } useEffect(() => { - if (!response.data) return - setBroadcastList(response.data.data) - }, [response]) - - useEffect(() => { - fetchApi('GET', '/streams').then(() => {}) + fetch(`${import.meta.env.VITE_API_URL}` + '/streams', { method: 'GET', credentials: 'include' }) + .then((res) => { + if (res.ok === true) { + return res.json() + } else { + throw new Error('Get Streamers Data Failed') + } + }) + .then((res) => { + setBroadcastList( + res.data.map((broadcast: any): BroadcastInterface => { + return { userId: broadcast.userId, nickname: broadcast.nickname, title: broadcast.title, viewer: broadcast.viewer, thumbnail: broadcast.thumbnail } + }), + ) + }) + .catch((err) => console.error(err)) }, []) return ( @@ -73,11 +75,9 @@ const MainPage = () => { {broadcastList.length !== 0 ? ( broadcastList.map((broadcast, index) => ( -
- - - -
+ + + )) ) : ( diff --git a/server/api-server/src/users/users.service.ts b/server/api-server/src/users/users.service.ts index a69dc80..23e9d5a 100644 --- a/server/api-server/src/users/users.service.ts +++ b/server/api-server/src/users/users.service.ts @@ -51,7 +51,7 @@ export class UsersService { } async update(id: string, updateUserDto: UpdateUserDto) { - const user = await this.findOne(id); + const user = await this.findByUserId(id); if (!user) { throw new HttpException('User not found', HttpStatus.NOT_FOUND); }