From ba1383abe6587671757a58b10b78fe8bb6ff7644 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Fri, 22 Nov 2024 17:09:52 +0900 Subject: [PATCH 001/129] refactor: fe internal types --- chatServer/main.ts | 5 +++++ client/src/__mocks__/broadcasts.ts | 24 ++++++++++++------------ client/src/__mocks__/playlists.ts | 2 +- client/src/__test__/actions.test.ts | 6 ++++-- client/src/__test__/msw.test.ts | 2 +- 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/chatServer/main.ts b/chatServer/main.ts index d0f128c3..4536da5b 100644 --- a/chatServer/main.ts +++ b/chatServer/main.ts @@ -7,7 +7,12 @@ const PORT = 7990; const io = new Server(PORT, { path: '/live', cors: { +<<<<<<< HEAD origin: ['localhost', 'https://funch.site', 'https://www.funch.site'], +======= + // origin: 'https://www.funch.site', + origin: '*', +>>>>>>> b16a3be0 (refactor: types) methods: '*', }, }); diff --git a/client/src/__mocks__/broadcasts.ts b/client/src/__mocks__/broadcasts.ts index ef64d484..45c44134 100644 --- a/client/src/__mocks__/broadcasts.ts +++ b/client/src/__mocks__/broadcasts.ts @@ -9,7 +9,7 @@ export const mockedBroadcasts: Broadcast[] = [ tags: ['politics', 'election'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 10870, - username: '슈카월드', + userName: '슈카월드', profileImageUrl: 'https://via.placeholder.com/150', }, { @@ -20,7 +20,7 @@ export const mockedBroadcasts: Broadcast[] = [ moodCategory: 'FUN', thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 100, - username: '짜왕', + userName: '짜왕', profileImageUrl: 'https://via.placeholder.com/150', }, { @@ -31,7 +31,7 @@ export const mockedBroadcasts: Broadcast[] = [ tags: ['cat', 'cute'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 300, - username: '모카', + userName: '모카', profileImageUrl: 'https://via.placeholder.com/150', }, { @@ -42,7 +42,7 @@ export const mockedBroadcasts: Broadcast[] = [ tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 200, - username: '토끼', + userName: '토끼', profileImageUrl: 'https://via.placeholder.com/150', }, { @@ -53,7 +53,7 @@ export const mockedBroadcasts: Broadcast[] = [ tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 300, - username: '펭귄', + userName: '펭귄', profileImageUrl: 'https://via.placeholder.com/150', }, { @@ -64,7 +64,7 @@ export const mockedBroadcasts: Broadcast[] = [ tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 400, - username: '오리', + userName: '오리', profileImageUrl: 'https://via.placeholder.com/150', }, { @@ -75,7 +75,7 @@ export const mockedBroadcasts: Broadcast[] = [ tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 500, - username: '앵무새', + userName: '앵무새', profileImageUrl: 'https://via.placeholder.com/150', }, { @@ -86,7 +86,7 @@ export const mockedBroadcasts: Broadcast[] = [ tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 600, - username: '거북이', + userName: '거북이', profileImageUrl: 'https://via.placeholder.com/150', }, { @@ -97,7 +97,7 @@ export const mockedBroadcasts: Broadcast[] = [ tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 700, - username: '물고기', + userName: '물고기', profileImageUrl: 'https://via.placeholder.com/150', }, { @@ -108,7 +108,7 @@ export const mockedBroadcasts: Broadcast[] = [ tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 800, - username: '물소', + userName: '물소', profileImageUrl: 'https://via.placeholder.com/150', }, { @@ -119,7 +119,7 @@ export const mockedBroadcasts: Broadcast[] = [ tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 900, - username: '말', + userName: '말', profileImageUrl: 'https://via.placeholder.com/150', }, { @@ -130,7 +130,7 @@ export const mockedBroadcasts: Broadcast[] = [ tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 1000, - username: '사자', + userName: '사자', profileImageUrl: 'https://via.placeholder.com/150', }, ]; diff --git a/client/src/__mocks__/playlists.ts b/client/src/__mocks__/playlists.ts index 8dfceadd..3dc8c1a2 100644 --- a/client/src/__mocks__/playlists.ts +++ b/client/src/__mocks__/playlists.ts @@ -6,7 +6,7 @@ const demoPlaylistUrl = export const mockedPlaylists: Playlist[] = mockedBroadcasts.map((broadcast) => ({ playlistUrl: demoPlaylistUrl, - broadCastData: { + broadcastData: { ...broadcast, }, })); diff --git a/client/src/__test__/actions.test.ts b/client/src/__test__/actions.test.ts index 10870295..0023bb9c 100644 --- a/client/src/__test__/actions.test.ts +++ b/client/src/__test__/actions.test.ts @@ -5,14 +5,16 @@ import { describe, expect, test } from 'vitest'; describe('actions', () => { test('should return mocked broadcasts', async () => { const result = await getLiveList(); + console.log(result); + console.log(mockedBroadcasts); expect(result).not.toBeNull(); - expect(result).toStrictEqual(mockedBroadcasts); + expect(result).toStrictEqual(mockedBroadcasts.sort((a, b) => b.viewerCount - a.viewerCount)); }); test('should return mocked playlist by broadcastId', async () => { const mockedBroadcast = mockedBroadcasts[0]; const result = await getPlaylist(mockedBroadcast.broadcastId); expect(result).not.toBeNull(); - expect(result!.broadCastData).toStrictEqual(mockedBroadcast); + expect(result!.broadcastData).toStrictEqual(mockedBroadcast); }); test('should throw error when playlist not found', async () => { const invalidBroadcastId = 'invalid-broadcast-id'; diff --git a/client/src/__test__/msw.test.ts b/client/src/__test__/msw.test.ts index 7f672480..8fdb0f17 100644 --- a/client/src/__test__/msw.test.ts +++ b/client/src/__test__/msw.test.ts @@ -18,7 +18,7 @@ describe('msw handlers', () => { const mockedBroadcast = mockedBroadcasts[0]; const response = await fetch(`/api/live/${mockedBroadcast.broadcastId}`); const data = await response.json(); - expect(data.broadCastData).toStrictEqual(mockedBroadcast); + expect(data.broadcastData).toStrictEqual(mockedBroadcast); }); test('should return 404 when playlist not found', async () => { const response = await fetch('/api/live/invalid-broadcast-id'); From afaf3508b184f5a33af487d62cfa8e7fdafb06ce Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Mon, 25 Nov 2024 01:10:40 +0900 Subject: [PATCH 002/129] =?UTF-8?q?refactor:=20application=20server=20broa?= =?UTF-8?q?dcast=20path=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 방송 파일 경로가 기존 broadcast 이름으로 만들어진 디렉토리에서 내부 추가 디렉토리 경로가 생김에 따라 broadcast properties에 broadcastPath를 추가하였다. Co-authored-by: 최호빈 --- applicationServer/src/live/live.controller.ts | 5 +++-- applicationServer/src/live/live.service.ts | 16 +++++++++------- applicationServer/src/types.ts | 1 + 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/applicationServer/src/live/live.controller.ts b/applicationServer/src/live/live.controller.ts index c971425f..1cc404a0 100644 --- a/applicationServer/src/live/live.controller.ts +++ b/applicationServer/src/live/live.controller.ts @@ -31,13 +31,14 @@ export class LiveController { @Post('/start') @HttpCode(200) - async startLive(@Body('streamKey') streamKey) { + async startLive(@Body('streamKey') streamKey: string, @Body('internalPath') internalPath: string) { const member = await this.liveService.verifyStreamKey(streamKey); - this.liveService.addLiveData(member); + this.liveService.addLiveData(member, internalPath); return { broadcastId: member.broadcast_id }; } @Post('/end') + @HttpCode(200) async endLive(@Body('streamKey') streamKey) { const member = await this.liveService.verifyStreamKey(streamKey); this.liveService.removeLiveData(member); diff --git a/applicationServer/src/live/live.service.ts b/applicationServer/src/live/live.service.ts index e23e53bd..171d049c 100644 --- a/applicationServer/src/live/live.service.ts +++ b/applicationServer/src/live/live.service.ts @@ -25,11 +25,11 @@ export class LiveService { responseLiveData(broadcastId) { if (!this.live.data.has(broadcastId)) throw new HttpException('Not Found', HttpStatus.NOT_FOUND); - const createMultivariantPlaylistUrl = (id) => - `https://kr.object.ncloudstorage.com/media-storage/${id}/master_playlist.m3u8`; + const createMultivariantPlaylistUrl = (path) => + `https://kr.object.ncloudstorage.com/media-storage/${path}/master_playlist.m3u8`; const broadcastData = this.live.data.get(broadcastId); - const playlistUrl = createMultivariantPlaylistUrl(broadcastId); + const playlistUrl = createMultivariantPlaylistUrl(broadcastData.broadcastPath); return { playlistUrl, broadcastData }; } @@ -39,8 +39,9 @@ export class LiveService { if (allLives.length <= count) return allLives; const result: Broadcast[] = []; + const history = {}; + while (result.length < count) { - const history = {}; const randomCount = Math.floor(allLives.length * Math.random()); if (!history[randomCount]) { @@ -52,23 +53,24 @@ export class LiveService { return result; } - async verifyStreamKey(streamKey) { + async verifyStreamKey(streamKey: string) { const member = await this.memberService.findOneMemberWithCondition({ stream_key: streamKey }); if (!member) throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED); return member; } - addLiveData(member: Member) { + addLiveData(member: Member, internalPath: string) { if (this.live.data.has(member.broadcast_id)) throw new HttpException('Conflict', HttpStatus.CONFLICT); this.live.data.set(member.broadcast_id, { broadcastId: member.broadcast_id, + broadcastPath: `${member.broadcast_id}/${internalPath}`, title: `${member.name}의 라이브 방송`, contentCategory: '', moodCategory: '', tags: [], - thumbnailUrl: `https://kr.object.ncloudstorage.com/media-storage/${member.broadcast_id}/dynamic_thumbnail.jpg`, + thumbnailUrl: `https://kr.object.ncloudstorage.com/media-storage/${member.broadcast_id}/${internalPath}/dynamic_thumbnail.jpg`, viewerCount: 0, userName: member.name, profileImageUrl: member.profile_image, diff --git a/applicationServer/src/types.ts b/applicationServer/src/types.ts index a5c5f39c..9eb17270 100644 --- a/applicationServer/src/types.ts +++ b/applicationServer/src/types.ts @@ -1,5 +1,6 @@ type Broadcast = { broadcastId: string; + broadcastPath: string; title: string; contentCategory: string; moodCategory: string; From 13c572cd2d5571082f1e3c4c665f8ccd87a502a0 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Mon, 25 Nov 2024 01:13:03 +0900 Subject: [PATCH 003/129] =?UTF-8?q?refactor:=20amf=20=EB=94=94=EC=BD=94?= =?UTF-8?q?=EB=94=A9=20=ED=95=A8=EC=88=98=20=EB=A1=9C=EA=B1=B0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 console로 로그를 출력하던 로직에서 logger를 이용해 기록하는 방식으로 변경하였습니다. Co-authored-by: 최호빈 --- mediaServer/core/rtmp/utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mediaServer/core/rtmp/utils.ts b/mediaServer/core/rtmp/utils.ts index 8231233f..21e0b678 100644 --- a/mediaServer/core/rtmp/utils.ts +++ b/mediaServer/core/rtmp/utils.ts @@ -1,4 +1,5 @@ import { decodeAmf0Cmd, decodeAmf3Cmd } from 'node-amfutils'; +import { logger } from '@/logger'; function decodeAMF(typeId: number, paylod: Buffer, amf0Num: number, amf3Num: number) { try { @@ -8,7 +9,7 @@ function decodeAMF(typeId: number, paylod: Buffer, amf0Num: number, amf3Num: num return decodeAmf3Cmd(paylod); } } catch (e) { - console.log(typeId, JSON.stringify(paylod), e); + logger.debug(`[typeId${typeId}] ${JSON.stringify(paylod)}, error : ${e}`); } } From 294f1afa4c6e3d9f3a9ae04d5dc5b7705751fcb4 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Mon, 25 Nov 2024 01:15:30 +0900 Subject: [PATCH 004/129] =?UTF-8?q?fix:=20FFMpeg=20480p=20=EC=B6=9C?= =?UTF-8?q?=EB=A0=A5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CPU 사용량을 줄이기 위해 FFMpeg으로 m4v 파일을 만드는 과정에서 480p 생성 코드를 제거하였습니다. Co-authored-by: 최호빈 --- mediaServer/core/media/ffmpeg.ts | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/mediaServer/core/media/ffmpeg.ts b/mediaServer/core/media/ffmpeg.ts index b20baebc..1d949c51 100644 --- a/mediaServer/core/media/ffmpeg.ts +++ b/mediaServer/core/media/ffmpeg.ts @@ -15,10 +15,10 @@ const defaultOutputOptions = [ '-hls_list_size 3', '-hls_segment_type fmp4', '-hls_flags split_by_time+independent_segments+omit_endlist', - '-preset veryfast', + '-preset ultrafast', ]; -function initializeFFMepg(ffmpegInputStream: PassThrough, storagePath: string) { +function initializeFFMpeg(ffmpegInputStream: PassThrough, storagePath: string) { return ffmpeg() .input(ffmpegInputStream) .inputOptions(['-re']) @@ -28,7 +28,7 @@ function initializeFFMepg(ffmpegInputStream: PassThrough, storagePath: string) { .videoCodec('libx264') .audioCodec('aac') .size('1920x1080') - .videoBitrate('5000k') + .videoBitrate('8000k') .audioBitrate('192k') .outputOptions([...defaultOutputOptions, '-hls_fmp4_init_filename chunkList_1080p_0_0.mp4']) @@ -36,29 +36,19 @@ function initializeFFMepg(ffmpegInputStream: PassThrough, storagePath: string) { .videoCodec('libx264') .audioCodec('aac') .size('1280x720') - .videoBitrate('3000k') + .videoBitrate('5000k') .audioBitrate('128k') .outputOptions([...defaultOutputOptions, '-hls_fmp4_init_filename chunkList_720p_0_0.mp4']) - .output(`${storagePath}/chunklist_480p_.m3u8`) - .videoCodec('libx264') - .audioCodec('aac') - .size('854x480') - .videoBitrate('1500k') - .audioBitrate('96k') - .outputOptions([...defaultOutputOptions, '-hls_fmp4_init_filename chunkList_480p_0_0.mp4']) - .output(`${storagePath}/dynamic_thumbnail.jpg`) .outputOptions(['-vf', 'fps=1/30', '-update', '1', '-s', '426x240']) - .on('end', (err, stdout, stderr) => { - logger.info(`Finished processing! err: ${err}, stdout: ${stdout}, stderr: ${stderr}`); - }) .on('close', (code) => { logger.error(`FFmpeg process exited with code ${code}`); }) - .on('error', (code) => { - logger.error(`FFmpeg process error with code ${code}`); + .on('error', (code, stdout, stderr) => { + logger.error(`FFmpeg process error with code: ${code}\nstdout: ${stdout}\nstderr: ${stderr}`); + initializeFFMpeg(ffmpegInputStream, storagePath); }); } @@ -67,16 +57,14 @@ function createMasterPlaylist(storagePath) { #EXTM3U #EXT-X-VERSION:7 -#EXT-X-STREAM-INF:BANDWIDTH=5711200,RESOLUTION=1920x1080,CODECS="avc1.64002a,mp4a.40.2" +#EXT-X-STREAM-INF:BANDWIDTH=8711200,RESOLUTION=1920x1080,CODECS="avc1.64002a,mp4a.40.2" chunklist_1080p_.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=3440800,RESOLUTION=1280x720,CODECS="avc1.640020,mp4a.40.2" +#EXT-X-STREAM-INF:BANDWIDTH=5440800,RESOLUTION=1280x720,CODECS="avc1.640020,mp4a.40.2" chunklist_720p_.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=1755600,RESOLUTION=854x480,CODECS="avc1.64001f,mp4a.40.2" -chunklist_480p_.m3u8 `.trim(); const fileStream = fs.createWriteStream(`${storagePath}/master_playlist.m3u8`); fileStream.write(masterPlaylist, () => fileStream.end()); } -export { initializeFFMepg, createMasterPlaylist }; +export { initializeFFMpeg, createMasterPlaylist }; From 87c13d3c0a1196d721a5421f445fe0565c7b3314 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Mon, 25 Nov 2024 01:17:32 +0900 Subject: [PATCH 005/129] =?UTF-8?q?refactor:=20mediaServer=20=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A6=AC=EB=B0=8D=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20API?= =?UTF-8?q?=20=ED=98=B8=EC=B6=9C=20=ED=95=A8=EC=88=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 중복된 코드였던 startStreaming과 endStreaming을 streamingEvent 함수 하나로 변경하였고, 전달받는 인자값을 streamKey와 internalPath를 함께 받을 수 있도록 객체 형태로 전달받도록 변경하였습니다. Co-authored-by: 최호빈 --- mediaServer/core/fetch.ts | 40 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/mediaServer/core/fetch.ts b/mediaServer/core/fetch.ts index e132c256..1026c661 100644 --- a/mediaServer/core/fetch.ts +++ b/mediaServer/core/fetch.ts @@ -1,27 +1,21 @@ -const API_SERVER_LIVE_URL = 'https://api.funch.site/live'; +import { logger } from '@/logger'; -async function startStreaming(streamKey) { - return fetch(`${API_SERVER_LIVE_URL}/start`, { - method: 'POST', - cache: 'no-store', - headers: { - 'Content-Type': 'application/json', - Connection: 'close', - }, - body: JSON.stringify({ streamKey }), - }); -} +type StreamingEvent = 'start' | 'end'; +const API_SERVER_LIVE_URL = 'https://api.funch.site/live'; -function endStreaming(streamKey) { - return fetch(`${API_SERVER_LIVE_URL}/end`, { - method: 'POST', - cache: 'no-store', - headers: { - 'Content-Type': 'application/json', - Connection: 'close', - }, - body: JSON.stringify({ streamKey }), - }); +async function streamingEvent(event: StreamingEvent, data) { + try { + return fetch(`${API_SERVER_LIVE_URL}/${event}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + } catch (e) { + logger.error(`fetch start stream event: ${e}`); + return; + } } -export { startStreaming, endStreaming }; +export { streamingEvent }; From db7a0ae7eb6405b0561f8ba77597ba0ea50b893c Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Mon, 25 Nov 2024 01:19:38 +0900 Subject: [PATCH 006/129] =?UTF-8?q?refactor:=20=EC=86=8C=EC=BC=93=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?internalPath=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit this.storagePath가 정해지기 전 종료 관련 이벤트가 발생했을 때 storagePath가 undefined으로 처리되는 현상을 막기 위한 소켓 이벤트 분리와 방송 종료 시 방송 디렉토리를 제거하던 로직을 삭제하고, 방송 별 디렉토리 분리로 변경함에 따라 internalPath를 방송 시작 시 함께 전달하도록 변경하였습니다. Co-authored-by: 최호빈 --- mediaServer/core/rtmp/stream.ts | 44 +++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/mediaServer/core/rtmp/stream.ts b/mediaServer/core/rtmp/stream.ts index b7fdf37c..6ed63b67 100644 --- a/mediaServer/core/rtmp/stream.ts +++ b/mediaServer/core/rtmp/stream.ts @@ -12,9 +12,9 @@ import { publish } from '@rtmp/publish'; import { decodeAMF } from '@rtmp/utils'; import { createFlvHeader, previousTagSize0, createFlvTag } from '@media/flv'; import { PassThrough } from 'stream'; -import { initializeFFMepg, createMasterPlaylist } from '@media/ffmpeg'; +import { initializeFFMpeg, createMasterPlaylist } from '@media/ffmpeg'; import { logger } from '@/logger'; -import { startStreaming, endStreaming } from '@/core/fetch'; +import { streamingEvent } from '@/core/fetch'; ffmpeg.setFfmpegPath(ffmpegPath.path); ffmpeg.setFfprobePath(ffprobePath.path); @@ -74,6 +74,9 @@ class RTMPStream { run() { this.socket.on('data', this.dataEvent.bind(this)); + } + + bindCloseEvent() { this.socket.on('close', this.closeEvent.bind(this)); this.socket.on('error', this.errorEvent.bind(this)); this.socket.on('timeout', this.timeoutEvent.bind(this)); @@ -87,29 +90,27 @@ class RTMPStream { this.parseRtmpChunk(data); } catch (e) { logger.error(`parse error: ${e}`); + streamingEvent('end', this.streamKey); } } } async closeEvent() { - fs.rm(this.storagePath!, { recursive: true, force: true }, () => {}); logger.info(`The ${this.streamKey} has been closed.`); - this.socket.end(); - endStreaming(this.streamKey); + await streamingEvent('end', { streamKey: this.streamKey }); + if (this.socket.readableEnded || this.socket.writableEnded) this.socket.end(); } - errorEvent(error) { - fs.rm(this.storagePath!, { recursive: true, force: true }, () => {}); + async errorEvent(error) { logger.error(`The ${this.streamKey} has made an error: ${error}`); - this.socket.destroy(); - endStreaming(this.streamKey); + await streamingEvent('end', { streamKey: this.streamKey }); + if (this.socket.readableEnded || this.socket.writableEnded) this.socket.destroy(); } - timeoutEvent() { - fs.rm(this.storagePath!, { recursive: true, force: true }, () => {}); + async timeoutEvent() { logger.info(`The ${this.streamKey} timed out.`); - this.socket.destroy(); - endStreaming(this.streamKey); + await streamingEvent('end', { streamKey: this.streamKey }); + if (this.socket.readableEnded || this.socket.writableEnded) this.socket.destroy(); } handleMessage() { @@ -165,7 +166,8 @@ class RTMPStream { fs.mkdirSync(this.storagePath!, { recursive: true }); } createMasterPlaylist(this.storagePath); - this.ffmpeg = initializeFFMepg(this.ffmpegInputStream, this.storagePath!); + + this.ffmpeg = initializeFFMpeg(this.ffmpegInputStream, this.storagePath!); this.ffmpegInputStream.write(createFlvHeader()); this.ffmpegInputStream.write(previousTagSize0); this.ffmpeg.run(); @@ -183,12 +185,18 @@ class RTMPStream { break; case 'createStream': { const OK = 200; - const response = await startStreaming(this.streamKey); - - if (response.status === OK) { + const now = new Date().toISOString(); + const broadcastData = { + streamKey: this.streamKey, + internalPath: now, + }; + const response = await streamingEvent('start', broadcastData); + + if (response?.status === OK) { this.streamCount++; const data = await response.json(); - this.storagePath = `../../media-storage/${data.broadcastId}`; + this.storagePath = `../../media-storage/${data.broadcastId}/${now}`; + this.bindCloseEvent(); createStream(this.socket, this.streamCount); } else { this.socket.end(); From fc418c948a2e51ad08deb6cdbe792d56e19c62d6 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Sun, 24 Nov 2024 20:27:01 +0900 Subject: [PATCH 007/129] =?UTF-8?q?feat:=20=EC=8A=A4=ED=8A=B8=EB=A6=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 복사 버튼 컴포넌트 작성 - 스트림 url, 키 api 추후 붙여야함 --- .../features/StreamSettingContainer.tsx | 53 +++++++++++++++++++ .../app/studio/features/StudioCopyButton.tsx | 16 ++++++ .../studio/features/StudioGuideContainer.tsx | 5 ++ .../features/StudioInfoSettingGuide.tsx | 5 ++ client/src/app/studio/page.tsx | 32 ++++++----- 5 files changed, 99 insertions(+), 12 deletions(-) create mode 100644 client/src/app/studio/features/StreamSettingContainer.tsx create mode 100644 client/src/app/studio/features/StudioCopyButton.tsx create mode 100644 client/src/app/studio/features/StudioGuideContainer.tsx create mode 100644 client/src/app/studio/features/StudioInfoSettingGuide.tsx diff --git a/client/src/app/studio/features/StreamSettingContainer.tsx b/client/src/app/studio/features/StreamSettingContainer.tsx new file mode 100644 index 00000000..6e00779e --- /dev/null +++ b/client/src/app/studio/features/StreamSettingContainer.tsx @@ -0,0 +1,53 @@ +import { PropsWithChildren } from 'react'; +import { useState } from 'react'; +import StudioCopyButton from './StudioCopyButton'; + +const StreamSettingContainer = () => { + return ( + +
+ +
+
+ + +
+
+ ); +}; + +const StreamKeyWrapper = ({ children }: PropsWithChildren) => { + return ( +
{children}
+ ); +}; + +const StreamURLContainer = () => { + const [streamURL, setStreamURL] = useState('rtmp://live.twitch.tv/app'); + + return ( +
+
스트림 URL
+
+ {streamURL} + 복사 +
+
+ ); +}; + +const StreamKeyContainer = () => { + const [streamKey, setStreamKey] = useState('ls1evndcyu2xsia18qzir1l7ot52w3e52t'); + + return ( +
+
스트림 키
+
+ {streamKey} + 복사 +
+
+ ); +}; + +export default StreamSettingContainer; diff --git a/client/src/app/studio/features/StudioCopyButton.tsx b/client/src/app/studio/features/StudioCopyButton.tsx new file mode 100644 index 00000000..807a1e5c --- /dev/null +++ b/client/src/app/studio/features/StudioCopyButton.tsx @@ -0,0 +1,16 @@ +import { PropsWithChildren } from 'react'; +import { type ButtonHTMLAttributes } from 'react'; + +type Props = PropsWithChildren & ButtonHTMLAttributes; + +const StudioCopyButton = ({ children, ...rest }: Props) => { + return ( + + ); +}; +export default StudioCopyButton; diff --git a/client/src/app/studio/features/StudioGuideContainer.tsx b/client/src/app/studio/features/StudioGuideContainer.tsx new file mode 100644 index 00000000..b5e84d5a --- /dev/null +++ b/client/src/app/studio/features/StudioGuideContainer.tsx @@ -0,0 +1,5 @@ +const StudioGuideContainer = () => { + return
StudioGuideContainer
; +}; + +export default StudioGuideContainer; diff --git a/client/src/app/studio/features/StudioInfoSettingGuide.tsx b/client/src/app/studio/features/StudioInfoSettingGuide.tsx new file mode 100644 index 00000000..10132c1f --- /dev/null +++ b/client/src/app/studio/features/StudioInfoSettingGuide.tsx @@ -0,0 +1,5 @@ +const StudioInfoSettingGuide = () => { + return
StudioInfoSettingGuide
; +}; + +export default StudioInfoSettingGuide; diff --git a/client/src/app/studio/page.tsx b/client/src/app/studio/page.tsx index fb35d7b2..bbeafc93 100644 --- a/client/src/app/studio/page.tsx +++ b/client/src/app/studio/page.tsx @@ -1,19 +1,27 @@ 'use client'; - -import { useState } from 'react'; -import { StudioDropdownRendererForTest } from '@components/studio/StudioDropdown'; -import StudioInput from '@components/studio/StudioInput'; -import { TextareaRendererForTest } from '@components/studio/StudioTextarea'; -import StudioAddButton from '@components/studio/StudioAddButton'; -import StudioUpdateButton from '@components/studio/StudioUpdateButton'; -import StudioBadge from '@components/studio/StudioBadge'; -import StudioImageInput from '@components/studio/StudioImageInput'; +import { PropsWithChildren } from 'react'; +import StreamSettingContainer from './features/StreamsettingContainer'; +import StudioGuideContainer from './features/StudioGuideContainer'; +import StudioInfoSettingGuide from './features/StudioInfoSettingGuide'; const StudioPage = () => { - const [image, setImage] = useState(null); return ( -
-
+ + +
+ + +
+
+ ); +}; + +type Props = PropsWithChildren<{}>; + +const StudioSettingWrapper = ({ children, ...rest }: Props) => { + return ( +
+ {children}
); }; From a1b8ca40897e0bd96fc0bdae2186cd2030df2606 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Sun, 24 Nov 2024 21:32:35 +0900 Subject: [PATCH 008/129] =?UTF-8?q?feat:=20=EB=B0=A9=EC=86=A1=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=84=A4=EC=A0=95=20=EA=B0=80=EC=9D=B4=EB=93=9C=20?= =?UTF-8?q?UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/components/svgs/StudioCategorySvg.tsx | 193 +++++++++++++ .../app/components/svgs/StudioPreviewSvg.tsx | 257 ++++++++++++++++++ .../app/components/svgs/StudioTitleSvg.tsx | 205 ++++++++++++++ .../studio/features/StudioGuideContainer.tsx | 2 +- .../features/StudioInfoSettingGuide.tsx | 41 ++- client/src/app/studio/page.tsx | 2 +- 6 files changed, 697 insertions(+), 3 deletions(-) create mode 100644 client/src/app/components/svgs/StudioCategorySvg.tsx create mode 100644 client/src/app/components/svgs/StudioPreviewSvg.tsx create mode 100644 client/src/app/components/svgs/StudioTitleSvg.tsx diff --git a/client/src/app/components/svgs/StudioCategorySvg.tsx b/client/src/app/components/svgs/StudioCategorySvg.tsx new file mode 100644 index 00000000..de20e066 --- /dev/null +++ b/client/src/app/components/svgs/StudioCategorySvg.tsx @@ -0,0 +1,193 @@ +import type { SvgComponentProps } from '@libs/internalTypes'; + +const StudioCategorySvg = ({ svgTitle, svgDescription }: SvgComponentProps) => { + return ( + + {svgTitle && {svgTitle}} + {svgDescription && {svgDescription}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default StudioCategorySvg; diff --git a/client/src/app/components/svgs/StudioPreviewSvg.tsx b/client/src/app/components/svgs/StudioPreviewSvg.tsx new file mode 100644 index 00000000..759d2129 --- /dev/null +++ b/client/src/app/components/svgs/StudioPreviewSvg.tsx @@ -0,0 +1,257 @@ +import type { SvgComponentProps } from '@libs/internalTypes'; + +const StudioPreviewSvg = ({ svgTitle, svgDescription }: SvgComponentProps) => { + return ( + + {svgTitle && {svgTitle}} + {svgDescription && {svgDescription}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default StudioPreviewSvg; diff --git a/client/src/app/components/svgs/StudioTitleSvg.tsx b/client/src/app/components/svgs/StudioTitleSvg.tsx new file mode 100644 index 00000000..7bc503ea --- /dev/null +++ b/client/src/app/components/svgs/StudioTitleSvg.tsx @@ -0,0 +1,205 @@ +import type { SvgComponentProps } from '@libs/internalTypes'; + +const StudioTitleSvg = ({ svgTitle, svgDescription }: SvgComponentProps) => { + return ( + + {svgTitle && {svgTitle}} + {svgDescription && {svgDescription}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default StudioTitleSvg; diff --git a/client/src/app/studio/features/StudioGuideContainer.tsx b/client/src/app/studio/features/StudioGuideContainer.tsx index b5e84d5a..392121f5 100644 --- a/client/src/app/studio/features/StudioGuideContainer.tsx +++ b/client/src/app/studio/features/StudioGuideContainer.tsx @@ -1,5 +1,5 @@ const StudioGuideContainer = () => { - return
StudioGuideContainer
; + return
StudioGuideContainer
; }; export default StudioGuideContainer; diff --git a/client/src/app/studio/features/StudioInfoSettingGuide.tsx b/client/src/app/studio/features/StudioInfoSettingGuide.tsx index 10132c1f..e00149d5 100644 --- a/client/src/app/studio/features/StudioInfoSettingGuide.tsx +++ b/client/src/app/studio/features/StudioInfoSettingGuide.tsx @@ -1,5 +1,44 @@ +import StudioCategorySvg from '@components/svgs/StudioCategorySvg'; +import StudioPreviewSvg from '@components/svgs/StudioPreviewSvg'; +import StudioTitleSvg from '@components/svgs/StudioTitleSvg'; + const StudioInfoSettingGuide = () => { - return
StudioInfoSettingGuide
; + return ( +
+ +
+
+
+ +
+

+ 방송 제목 + 매력적인 제목으로 시청자의 관심을 유도해보세요. 시청자가 방송을 찾을 때 사용할 만한 키워드를 넣는 것이 + 좋습니다. +

+
+
+
+ +
+

+ 카테고리 + 시청자가 쉽게 찾을 수 있도록 진행중인 방송 카테고리를 추가하세요. 시청자가 방송을 찾을 때 사용할 만한 + 키워드를 넣는 것이 좋습니다. +

+
+
+
+ +
+

+ 미리보기 이미지 + 진행 중인 방송을 설명할 수 있는 사진을 업로드하세요. 시청자의 관심을 끄는 이미지가 좋습니다. +

+
+
+
+ ); }; export default StudioInfoSettingGuide; diff --git a/client/src/app/studio/page.tsx b/client/src/app/studio/page.tsx index bbeafc93..d7f06c9f 100644 --- a/client/src/app/studio/page.tsx +++ b/client/src/app/studio/page.tsx @@ -8,7 +8,7 @@ const StudioPage = () => { return ( -
+
From ab2d557d3d66fbc0f2460c196c4bc957b859a392 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Mon, 25 Nov 2024 01:48:36 +0900 Subject: [PATCH 009/129] =?UTF-8?q?feat:=20=EB=B0=A9=EC=86=A1=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91=ED=95=98=EA=B8=B0=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - img에 svg 덧대야함 --- client/src/app/components/svgs/ObsSvg.tsx | 14 ++++ client/src/app/components/svgs/PrismSvg.tsx | 19 +++++ .../features/StreamSettingContainer.tsx | 6 +- .../studio/features/StudioGuideContainer.tsx | 69 ++++++++++++++++++- .../features/StudioInfoSettingGuide.tsx | 2 +- 5 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 client/src/app/components/svgs/ObsSvg.tsx create mode 100644 client/src/app/components/svgs/PrismSvg.tsx diff --git a/client/src/app/components/svgs/ObsSvg.tsx b/client/src/app/components/svgs/ObsSvg.tsx new file mode 100644 index 00000000..fdcd040c --- /dev/null +++ b/client/src/app/components/svgs/ObsSvg.tsx @@ -0,0 +1,14 @@ +import { SvgComponentProps } from '@libs/internalTypes'; + +const ObsSvg = ({ svgTitle, svgDescription }: SvgComponentProps) => { + return ( + + {svgTitle && {svgTitle}} + {svgDescription && {svgDescription}} + OBS Studio icon + + + ); +}; + +export default ObsSvg; diff --git a/client/src/app/components/svgs/PrismSvg.tsx b/client/src/app/components/svgs/PrismSvg.tsx new file mode 100644 index 00000000..9342f3ae --- /dev/null +++ b/client/src/app/components/svgs/PrismSvg.tsx @@ -0,0 +1,19 @@ +import { SvgComponentProps } from '@libs/internalTypes'; + +const PrismSvg = ({ svgTitle, svgDescription }: SvgComponentProps) => { + return ( + + {svgTitle && {svgTitle}} + {svgDescription && {svgDescription}} + + + + + + + + + ); +}; + +export default PrismSvg; diff --git a/client/src/app/studio/features/StreamSettingContainer.tsx b/client/src/app/studio/features/StreamSettingContainer.tsx index 6e00779e..42df370f 100644 --- a/client/src/app/studio/features/StreamSettingContainer.tsx +++ b/client/src/app/studio/features/StreamSettingContainer.tsx @@ -8,7 +8,7 @@ const StreamSettingContainer = () => {
-
+
@@ -18,7 +18,9 @@ const StreamSettingContainer = () => { const StreamKeyWrapper = ({ children }: PropsWithChildren) => { return ( -
{children}
+
+ {children} +
); }; diff --git a/client/src/app/studio/features/StudioGuideContainer.tsx b/client/src/app/studio/features/StudioGuideContainer.tsx index 392121f5..fb536102 100644 --- a/client/src/app/studio/features/StudioGuideContainer.tsx +++ b/client/src/app/studio/features/StudioGuideContainer.tsx @@ -1,5 +1,72 @@ +import { PropsWithChildren } from 'react'; +import Link from 'next/link'; +import OBSSvg from '@components/svgs/ObsSvg'; +import PrismSvg from '@components/svgs/PrismSvg'; + const StudioGuideContainer = () => { - return
StudioGuideContainer
; + return ( +
+ +
+
+ 1 + 스트리밍 소프트웨어를 다운로드하세요. +
+
+ + + Open Broadcaster Software + + +
+ +
+ PRISM Live Studio + +
+
+ 2 + 스트림 키를 소프트웨어에 붙여 넣어주세요. +
+

+ 스트림 키는 {'방송 관리' + ' > ' + '설정'} + 에서 확인 가능합니다. +

+
+ 3 + 스트리밍 소프트웨어에서 방송을 시작하면 라이브 방송이 진행됩니다. +
+

방송 시작과 종료를 스트리밍 소프트웨어에서 진행해주세요.

+
+ 방송 시작하기 +
+
+
+ ); +}; + +const StudioNumberIcon = ({ children }: PropsWithChildren) => { + return ( +
+ {children} +
+ ); +}; + +const StudioBroadCastButton = ({ children }: PropsWithChildren) => { + return ( + + ); }; export default StudioGuideContainer; diff --git a/client/src/app/studio/features/StudioInfoSettingGuide.tsx b/client/src/app/studio/features/StudioInfoSettingGuide.tsx index e00149d5..a2fc067d 100644 --- a/client/src/app/studio/features/StudioInfoSettingGuide.tsx +++ b/client/src/app/studio/features/StudioInfoSettingGuide.tsx @@ -4,7 +4,7 @@ import StudioTitleSvg from '@components/svgs/StudioTitleSvg'; const StudioInfoSettingGuide = () => { return ( -
+
From 21577d703e06927b6075581909b2f4ac470e3041 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Mon, 25 Nov 2024 01:55:58 +0900 Subject: [PATCH 010/129] =?UTF-8?q?fix:=20chatServer=20=EC=B6=A9=EB=8F=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chatServer/main.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/chatServer/main.ts b/chatServer/main.ts index 4536da5b..d0f128c3 100644 --- a/chatServer/main.ts +++ b/chatServer/main.ts @@ -7,12 +7,7 @@ const PORT = 7990; const io = new Server(PORT, { path: '/live', cors: { -<<<<<<< HEAD origin: ['localhost', 'https://funch.site', 'https://www.funch.site'], -======= - // origin: 'https://www.funch.site', - origin: '*', ->>>>>>> b16a3be0 (refactor: types) methods: '*', }, }); From a380974898368f078b7f3dcc5ade9f3da940b54e Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Mon, 25 Nov 2024 15:47:57 +0900 Subject: [PATCH 011/129] =?UTF-8?q?chore:=20=EA=B9=83=ED=97=99=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20url=EC=9D=84=20next=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=EC=97=90=20=EC=B6=94=EA=B0=80=20-=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20remotePattern=EC=97=90=20avatars.githubusercontent.?= =?UTF-8?q?com=EC=9D=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/next.config.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/next.config.mjs b/client/next.config.mjs index cd6edad6..df5c0fc6 100644 --- a/client/next.config.mjs +++ b/client/next.config.mjs @@ -24,11 +24,14 @@ const nextConfig = { protocol: 'https', hostname: 'api.funch.site', }, - // https://kr.object.ncloudstorage.com { protocol: 'https', hostname: 'kr.object.ncloudstorage.com', }, + { + protocol: 'https', + hostname: 'avatars.githubusercontent.com', + }, ], }, }; From 4ad15fd7e08382854022359cb5c35d680082fe0d Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Mon, 25 Nov 2024 15:59:44 +0900 Subject: [PATCH 012/129] =?UTF-8?q?fix:=20=EC=B1=84=ED=8C=85=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=ED=85=9C=20'=EB=8B=98'=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20-=20=EC=B1=84=ED=8C=85=EC=B0=BD=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=20=EB=AA=A9=EB=A1=9D=20UI=20=EC=A4=91=20'?= =?UTF-8?q?=EB=8B=98'=20=EC=A0=9C=EA=B1=B0=20#279?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chatServer/main.ts | 2 +- client/src/app/(domain)/features/live/Chat.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chatServer/main.ts b/chatServer/main.ts index d0f128c3..522299fd 100644 --- a/chatServer/main.ts +++ b/chatServer/main.ts @@ -7,7 +7,7 @@ const PORT = 7990; const io = new Server(PORT, { path: '/live', cors: { - origin: ['localhost', 'https://funch.site', 'https://www.funch.site'], + origin: ['http://localhost:3000', 'https://funch.site', 'https://www.funch.site'], methods: '*', }, }); diff --git a/client/src/app/(domain)/features/live/Chat.tsx b/client/src/app/(domain)/features/live/Chat.tsx index 9b2ebaef..4fcc3c0f 100644 --- a/client/src/app/(domain)/features/live/Chat.tsx +++ b/client/src/app/(domain)/features/live/Chat.tsx @@ -131,7 +131,7 @@ const ChatList = ({ chatList }: ChatListProps) => {
{chatList.map((chat, index) => (

- {chat.name} 님 : + {chat.name} {chat.content}

))} From f30a654290b636d370f94eb0f7560f9534add0f9 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Mon, 25 Nov 2024 16:02:02 +0900 Subject: [PATCH 013/129] =?UTF-8?q?refactor:=20=EC=9E=90=EC=9E=98=ED=95=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stroke-width -> strokeWidth - px -> rem으로 단위 변경 --- client/src/app/components/svgs/PrismSvg.tsx | 6 +++--- client/src/app/studio/features/StudioCopyButton.tsx | 2 +- client/src/app/studio/features/StudioGuideContainer.tsx | 8 ++++---- client/src/app/studio/features/StudioInfoSettingGuide.tsx | 2 +- client/src/app/studio/page.tsx | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/client/src/app/components/svgs/PrismSvg.tsx b/client/src/app/components/svgs/PrismSvg.tsx index 9342f3ae..9c9e9219 100644 --- a/client/src/app/components/svgs/PrismSvg.tsx +++ b/client/src/app/components/svgs/PrismSvg.tsx @@ -7,11 +7,11 @@ const PrismSvg = ({ svgTitle, svgDescription }: SvgComponentProps) => { {svgDescription && {svgDescription}} - + - + - + ); }; diff --git a/client/src/app/studio/features/StudioCopyButton.tsx b/client/src/app/studio/features/StudioCopyButton.tsx index 807a1e5c..323fdcf5 100644 --- a/client/src/app/studio/features/StudioCopyButton.tsx +++ b/client/src/app/studio/features/StudioCopyButton.tsx @@ -6,7 +6,7 @@ type Props = PropsWithChildren & ButtonHTMLAttributes; const StudioCopyButton = ({ children, ...rest }: Props) => { return ( ); diff --git a/client/src/app/studio/features/StudioInfoSettingGuide.tsx b/client/src/app/studio/features/StudioInfoSettingGuide.tsx index a2fc067d..867b42bb 100644 --- a/client/src/app/studio/features/StudioInfoSettingGuide.tsx +++ b/client/src/app/studio/features/StudioInfoSettingGuide.tsx @@ -4,7 +4,7 @@ import StudioTitleSvg from '@components/svgs/StudioTitleSvg'; const StudioInfoSettingGuide = () => { return ( -
+
diff --git a/client/src/app/studio/page.tsx b/client/src/app/studio/page.tsx index d7f06c9f..6fb926ad 100644 --- a/client/src/app/studio/page.tsx +++ b/client/src/app/studio/page.tsx @@ -1,6 +1,6 @@ 'use client'; import { PropsWithChildren } from 'react'; -import StreamSettingContainer from './features/StreamsettingContainer'; +import StreamSettingContainer from './features/StreamSettingContainer'; import StudioGuideContainer from './features/StudioGuideContainer'; import StudioInfoSettingGuide from './features/StudioInfoSettingGuide'; From 81a97106586101a2eb4cb58bdf24352898265128 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Mon, 25 Nov 2024 17:11:55 +0900 Subject: [PATCH 014/129] =?UTF-8?q?feat:=20=EB=B0=A9=EC=86=A1=20=EC=84=B8?= =?UTF-8?q?=ED=8C=85=20=ED=8E=98=EC=9D=B4=EC=A7=80=20api=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mydata 타입 추가 - msw, actions에 핸들러 추가 - 재발급 버튼 추가(로직 미구현) - 재발급 버튼 누를시 나올 모달도 구현해야함 --- client/src/__mocks__/mydata.ts | 10 ++++ .../features/StreamSettingContainer.tsx | 53 ++++++++++++++----- .../studio/features/StudioGuideContainer.tsx | 2 +- .../features/StudioInfoSettingGuide.tsx | 2 +- .../studio/features/StudioReIssueButton.tsx | 16 ++++++ client/src/libs/actions.ts | 11 +++- client/src/libs/internalTypes.ts | 11 ++++ client/src/server/handlers.ts | 6 +++ 8 files changed, 95 insertions(+), 16 deletions(-) create mode 100644 client/src/__mocks__/mydata.ts create mode 100644 client/src/app/studio/features/StudioReIssueButton.tsx diff --git a/client/src/__mocks__/mydata.ts b/client/src/__mocks__/mydata.ts new file mode 100644 index 00000000..c8912a93 --- /dev/null +++ b/client/src/__mocks__/mydata.ts @@ -0,0 +1,10 @@ +export const mockedMydata = { + id: 'aaaaaaaaaaaaaaaaaa', + name: '텔레토비TV', + profile_image: 'https://profile.png', + stream_key: 'bbbbbbbbbbbbbbbbbbbbbbb', + broadcast_id: 'cccccccccccccccccccccccc', + follower_count: 1, + createdAt: '2024-11-17T13:12:06.000Z', + deletedAt: null, +}; diff --git a/client/src/app/studio/features/StreamSettingContainer.tsx b/client/src/app/studio/features/StreamSettingContainer.tsx index 42df370f..dc38794d 100644 --- a/client/src/app/studio/features/StreamSettingContainer.tsx +++ b/client/src/app/studio/features/StreamSettingContainer.tsx @@ -1,16 +1,44 @@ +'use client'; + import { PropsWithChildren } from 'react'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import StudioCopyButton from './StudioCopyButton'; +import StudioReissueButton from './StudioReIssueButton'; +import useUser from '@hooks/useUser'; +import { getStreamInfo } from '@libs/actions'; + +const handleCopy = async (streamURL: string) => { + if (typeof streamURL === 'string') { + await navigator.clipboard.writeText(streamURL); + } +}; + +const apiUrl = process.env.NEXT_PUBLIC_MEDIA_SERVER_URL; const StreamSettingContainer = () => { + const { isLoggedin } = useUser(); + const [streamKey, setStreamKey] = useState(''); + + const getStreamKey = async () => { + console.log(isLoggedin); + if (isLoggedin) { + const streamInfo = await getStreamInfo(); + setStreamKey(streamInfo.stream_key); + } + }; + + useEffect(() => { + getStreamKey(); + }, []); + return (
- +
- +
); @@ -18,35 +46,34 @@ const StreamSettingContainer = () => { const StreamKeyWrapper = ({ children }: PropsWithChildren) => { return ( -
+
{children}
); }; const StreamURLContainer = () => { - const [streamURL, setStreamURL] = useState('rtmp://live.twitch.tv/app'); - return (
스트림 URL
- {streamURL} - 복사 + {apiUrl} + handleCopy(apiUrl ?? '')}>복사
); }; -const StreamKeyContainer = () => { - const [streamKey, setStreamKey] = useState('ls1evndcyu2xsia18qzir1l7ot52w3e52t'); - +const StreamKeyContainer = ({ streamKey }: { streamKey: string }) => { return (
스트림 키
-
+
{streamKey} - 복사 +
+ handleCopy(streamKey)}>복사 + 재발급 +
); diff --git a/client/src/app/studio/features/StudioGuideContainer.tsx b/client/src/app/studio/features/StudioGuideContainer.tsx index 3b80e9de..3db40a11 100644 --- a/client/src/app/studio/features/StudioGuideContainer.tsx +++ b/client/src/app/studio/features/StudioGuideContainer.tsx @@ -63,7 +63,7 @@ const StudioNumberIcon = ({ children }: PropsWithChildren) => { const StudioBroadCastButton = ({ children }: PropsWithChildren) => { return ( - ); diff --git a/client/src/app/studio/features/StudioInfoSettingGuide.tsx b/client/src/app/studio/features/StudioInfoSettingGuide.tsx index 867b42bb..ad2dc784 100644 --- a/client/src/app/studio/features/StudioInfoSettingGuide.tsx +++ b/client/src/app/studio/features/StudioInfoSettingGuide.tsx @@ -4,7 +4,7 @@ import StudioTitleSvg from '@components/svgs/StudioTitleSvg'; const StudioInfoSettingGuide = () => { return ( -
+
diff --git a/client/src/app/studio/features/StudioReIssueButton.tsx b/client/src/app/studio/features/StudioReIssueButton.tsx new file mode 100644 index 00000000..090f0fec --- /dev/null +++ b/client/src/app/studio/features/StudioReIssueButton.tsx @@ -0,0 +1,16 @@ +import { PropsWithChildren } from 'react'; +import { type ButtonHTMLAttributes } from 'react'; + +type Props = PropsWithChildren & ButtonHTMLAttributes; + +const StudioReissueButton = ({ children, ...rest }: Props) => { + return ( + + ); +}; +export default StudioReissueButton; diff --git a/client/src/libs/actions.ts b/client/src/libs/actions.ts index 57ecb1cf..c05672d0 100644 --- a/client/src/libs/actions.ts +++ b/client/src/libs/actions.ts @@ -1,4 +1,4 @@ -import type { Broadcast, InternalUserSession, Playlist, User, Update } from '@libs/internalTypes'; +import type { Broadcast, InternalUserSession, Playlist, User, Update, Mydata } from '@libs/internalTypes'; import fetcher from '@libs/fetcher'; export const getLiveList = async (): Promise => { @@ -65,3 +65,12 @@ export const updateInfo = async (formData: Update): Promise => { return result; }; + +export const getStreamInfo = async (): Promise => { + const result = await fetcher({ + method: 'GET', + url: '/api/members/mydata', + }); + + return result; +}; diff --git a/client/src/libs/internalTypes.ts b/client/src/libs/internalTypes.ts index e24e7206..96430abb 100644 --- a/client/src/libs/internalTypes.ts +++ b/client/src/libs/internalTypes.ts @@ -110,3 +110,14 @@ export type Update = { tags: Array; thumbnail?: string | null; }; + +export type Mydata = { + id: string; + name: string; + profile_image: string; + stream_key: string; + broadcast_id: string; + follower_count: number; + createdAt: string; + deletedAt: string | null; +}; diff --git a/client/src/server/handlers.ts b/client/src/server/handlers.ts index d0b5a4d9..4d81e974 100644 --- a/client/src/server/handlers.ts +++ b/client/src/server/handlers.ts @@ -3,6 +3,7 @@ import { mockedPlaylists } from '@mocks/playlists'; import { mockedUsers } from '@mocks/users'; import { mockedUpdates } from '@mocks/updates'; import { http, HttpResponse } from 'msw'; +import { mockedMydata } from '@mocks/mydata'; const getLiveList = () => { return HttpResponse.json(mockedBroadcasts); @@ -49,11 +50,16 @@ const authenticate = () => { }); }; +const getMydata = () => { + return HttpResponse.json(mockedMydata); +}; + export const handlers = [ http.get('/api/live/list', getLiveList), http.get('/api/live/:broadcastId', getPlaylist), http.get('/api/users/:broadcastId', getUserByBroadcastId), http.get('/api/live/list/suggest', getSuggestedLiveList), + http.get('/api/members/mydata', getMydata), http.post('/api/auth/github/callback', authenticate), http.post('/api/login', login), http.post('/api/live/update', update), From 52a8f57df0dd1f02a3ac3c8ed7a4f9875898e327 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Mon, 25 Nov 2024 16:16:05 +0900 Subject: [PATCH 015/129] =?UTF-8?q?feat:=20=EC=BD=98=ED=85=90=EC=B8=A0=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=83=81=EC=88=98=20?= =?UTF-8?q?=EC=98=88=EC=8B=9C=20-=20=EC=BD=98=ED=85=90=EC=B8=A0=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=83=81=EC=88=98=20?= =?UTF-8?q?=EC=98=88=EC=8B=9C=20=EC=83=9D=EC=84=B1=20#263?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../categories/contents/[code]/page.tsx | 5 +++++ client/src/app/(domain)/categories/page.tsx | 4 ++-- client/src/libs/constants.ts | 22 +++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 client/src/app/(domain)/categories/contents/[code]/page.tsx diff --git a/client/src/app/(domain)/categories/contents/[code]/page.tsx b/client/src/app/(domain)/categories/contents/[code]/page.tsx new file mode 100644 index 00000000..ed774edc --- /dev/null +++ b/client/src/app/(domain)/categories/contents/[code]/page.tsx @@ -0,0 +1,5 @@ +const ContentCategoryPage = () => { + return
ContentCategoryPage
; +}; + +export default ContentCategoryPage; diff --git a/client/src/app/(domain)/categories/page.tsx b/client/src/app/(domain)/categories/page.tsx index 40fc4370..1063480d 100644 --- a/client/src/app/(domain)/categories/page.tsx +++ b/client/src/app/(domain)/categories/page.tsx @@ -1,5 +1,4 @@ import { type Metadata } from 'next'; -import CategoryCard from './features/CategoryCard'; export const metadata: Metadata = { title: '카테고리', @@ -8,7 +7,8 @@ export const metadata: Metadata = { const CategoriesPage = () => { return (
- +

콘텐츠 카테고리

+
); }; diff --git a/client/src/libs/constants.ts b/client/src/libs/constants.ts index 4f5ee896..94fb7bb1 100644 --- a/client/src/libs/constants.ts +++ b/client/src/libs/constants.ts @@ -27,3 +27,25 @@ export const SOCKET_EVENT = { CHAT: 'chat' as const, SET_ANONYMOUS_NAME: 'setAnonymousName' as const, }; + +export const CONTENTS_CATEGORY = { + talk: '소통' as const, + game: '게임' as const, + cook: '요리' as const, + outdoor: '야외' as const, + dailylife: '일상' as const, + virtual: '버추얼' as const, + mukbang: '먹방' as const, + politics: '정치' as const, + music: '음악' as const, + economy: '경제' as const, + radio: '라디오' as const, + develop: '개발' as const, + fishing: '낚시' as const, + news: '뉴스' as const, + study: '공부' as const, + beauty: '뷰티' as const, + house: '부동산' as const, + horro: '호러' as const, + travel: '여행' as const, +}; From 67ebab215398c08f2c831f8d2d347ab093d636ec Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Mon, 25 Nov 2024 17:06:46 +0900 Subject: [PATCH 016/129] =?UTF-8?q?feat:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A4=91=20=EC=BD=98?= =?UTF-8?q?=ED=85=90=EC=B8=A0=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=A7=84=ED=96=89=20-=20=EC=BD=98?= =?UTF-8?q?=ED=85=90=EC=B8=A0=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EB=85=B8=EC=B6=9C=20#263=20-=20=EA=B0=81?= =?UTF-8?q?=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C?= =?UTF-8?q?=20=ED=95=B4=EB=8B=B9=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A1=9C=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=8C=85=20#265?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/ContentsCategoryImage.tsx | 73 +++++++++++ client/src/app/(domain)/categories/page.tsx | 27 +++- .../src/assets/categories/communication.svg | 58 ++++----- client/src/libs/constants.ts | 116 +++++++++++++++--- client/src/libs/data.ts | 11 ++ client/src/libs/internalTypes.ts | 4 +- 6 files changed, 231 insertions(+), 58 deletions(-) create mode 100644 client/src/app/(domain)/categories/features/ContentsCategoryImage.tsx create mode 100644 client/src/libs/data.ts diff --git a/client/src/app/(domain)/categories/features/ContentsCategoryImage.tsx b/client/src/app/(domain)/categories/features/ContentsCategoryImage.tsx new file mode 100644 index 00000000..9f30e661 --- /dev/null +++ b/client/src/app/(domain)/categories/features/ContentsCategoryImage.tsx @@ -0,0 +1,73 @@ +import { CONTENTS_CATEGORY } from '@libs/constants'; +import { ContentsCategoryKey } from '@libs/internalTypes'; +import Image from 'next/image'; +import talkImage from '@assets/categories/communication.svg'; +import gameImage from '@assets/categories/game.svg'; +import cookImage from '@assets/categories/cooking.svg'; +import outdoorImage from '@assets/categories/outdoor.svg'; +import dailylifeImage from '@assets/categories/daily-life.svg'; +import virtualImage from '@assets/categories/virtual.svg'; +import mukbangImage from '@assets/categories/mukbang.svg'; +import politicsImage from '@assets/categories/politics.svg'; +import musicImage from '@assets/categories/music.svg'; +import economyImage from '@assets/categories/economy.svg'; +import radioImage from '@assets/categories/radio.svg'; +import developImage from '@assets/categories/development.svg'; +import fishingImage from '@assets/categories/fishing.svg'; +import newsImage from '@assets/categories/news.svg'; +import studyImage from '@assets/categories/education.svg'; +import beautyImage from '@assets/categories/beauty.svg'; +import houseImage from '@assets/categories/real-estate.svg'; +import horrorImage from '@assets/categories/horror.svg'; +import travelImage from '@assets/categories/travel.svg'; + +type Props = { + code: ContentsCategoryKey; +}; + +const ContentsCategoryImage = ({ code }: Props) => { + switch (code) { + case CONTENTS_CATEGORY.talk.code: + return {`${CONTENTS_CATEGORY[code]}; + case CONTENTS_CATEGORY.game.code: + return {`${CONTENTS_CATEGORY[code]}; + case CONTENTS_CATEGORY.cook.code: + return {`${CONTENTS_CATEGORY[code]}; + case CONTENTS_CATEGORY.outdoor.code: + return {`${CONTENTS_CATEGORY[code]}; + case CONTENTS_CATEGORY.dailylife.code: + return {`${CONTENTS_CATEGORY[code]}; + case CONTENTS_CATEGORY.virtual.code: + return {`${CONTENTS_CATEGORY[code]}; + case CONTENTS_CATEGORY.mukbang.code: + return {`${CONTENTS_CATEGORY[code]}; + case CONTENTS_CATEGORY.politics.code: + return {`${CONTENTS_CATEGORY[code]}; + case CONTENTS_CATEGORY.music.code: + return {`${CONTENTS_CATEGORY[code]}; + case CONTENTS_CATEGORY.economy.code: + return {`${CONTENTS_CATEGORY[code]}; + case CONTENTS_CATEGORY.radio.code: + return {`${CONTENTS_CATEGORY[code]}; + case CONTENTS_CATEGORY.develop.code: + return {`${CONTENTS_CATEGORY[code]}; + case CONTENTS_CATEGORY.fishing.code: + return {`${CONTENTS_CATEGORY[code]}; + case CONTENTS_CATEGORY.news.code: + return {`${CONTENTS_CATEGORY[code]}; + case CONTENTS_CATEGORY.study.code: + return {`${CONTENTS_CATEGORY[code]}; + case CONTENTS_CATEGORY.beauty.code: + return {`${CONTENTS_CATEGORY[code]}; + case CONTENTS_CATEGORY.house.code: + return {`${CONTENTS_CATEGORY[code]}; + case CONTENTS_CATEGORY.horror.code: + return {`${CONTENTS_CATEGORY[code]}; + case CONTENTS_CATEGORY.travel.code: + return {`${CONTENTS_CATEGORY[code]}; + default: + return null; + } +}; + +export default ContentsCategoryImage; diff --git a/client/src/app/(domain)/categories/page.tsx b/client/src/app/(domain)/categories/page.tsx index 1063480d..5bc2d563 100644 --- a/client/src/app/(domain)/categories/page.tsx +++ b/client/src/app/(domain)/categories/page.tsx @@ -1,4 +1,8 @@ +import { contentsCategories } from '@libs/data'; +import clsx from 'clsx'; import { type Metadata } from 'next'; +import Link from 'next/link'; +import ContentsCategoryImage from './features/ContentsCategoryImage'; export const metadata: Metadata = { title: '카테고리', @@ -6,9 +10,26 @@ export const metadata: Metadata = { const CategoriesPage = () => { return ( -
-

콘텐츠 카테고리

-
+
+

콘텐츠 카테고리

+
+ {contentsCategories.map((c) => ( +
+ +
+
+ +
+
+ +

{c.name}

+
+ ))} +
); }; diff --git a/client/src/assets/categories/communication.svg b/client/src/assets/categories/communication.svg index bb1edf2f..565c7fe6 100644 --- a/client/src/assets/categories/communication.svg +++ b/client/src/assets/categories/communication.svg @@ -1,44 +1,32 @@ - + - - - - - - - - - - - - - - - - - - + + + + + + - - - - - + + + + + + - - - - - - - - - - + + + + + + + + + + \ No newline at end of file diff --git a/client/src/libs/constants.ts b/client/src/libs/constants.ts index 94fb7bb1..f6954d78 100644 --- a/client/src/libs/constants.ts +++ b/client/src/libs/constants.ts @@ -28,24 +28,102 @@ export const SOCKET_EVENT = { SET_ANONYMOUS_NAME: 'setAnonymousName' as const, }; +// export const CONTENTS_CATEGORY = { +// talk: '소통' as const, +// game: '게임' as const, +// cook: '요리' as const, +// outdoor: '야외' as const, +// dailylife: '일상' as const, +// virtual: '버추얼' as const, +// mukbang: '먹방' as const, +// politics: '정치' as const, +// music: '음악' as const, +// economy: '경제' as const, +// radio: '라디오' as const, +// develop: '개발' as const, +// fishing: '낚시' as const, +// news: '뉴스' as const, +// study: '공부' as const, +// beauty: '뷰티' as const, +// house: '부동산' as const, +// horror: '호러' as const, +// travel: '여행' as const, +// }; export const CONTENTS_CATEGORY = { - talk: '소통' as const, - game: '게임' as const, - cook: '요리' as const, - outdoor: '야외' as const, - dailylife: '일상' as const, - virtual: '버추얼' as const, - mukbang: '먹방' as const, - politics: '정치' as const, - music: '음악' as const, - economy: '경제' as const, - radio: '라디오' as const, - develop: '개발' as const, - fishing: '낚시' as const, - news: '뉴스' as const, - study: '공부' as const, - beauty: '뷰티' as const, - house: '부동산' as const, - horro: '호러' as const, - travel: '여행' as const, + talk: { + code: 'talk', + name: '소통', + } as const, + game: { + code: 'game', + name: '게임', + } as const, + cook: { + code: 'cook', + name: '요리', + } as const, + outdoor: { + code: 'outdoor', + name: '야외', + } as const, + dailylife: { + code: 'dailylife', + name: '일상', + } as const, + virtual: { + code: 'virtual', + name: '버추얼', + } as const, + mukbang: { + code: 'mukbang', + name: '먹방', + } as const, + politics: { + code: 'politics', + name: '정치', + } as const, + music: { + code: 'music', + name: '음악', + } as const, + economy: { + code: 'economy', + name: '경제', + } as const, + radio: { + code: 'radio', + name: '라디오', + } as const, + develop: { + code: 'develop', + name: '개발', + } as const, + fishing: { + code: 'fishing', + name: '낚시', + } as const, + news: { + code: 'news', + name: '뉴스', + } as const, + study: { + code: 'study', + name: '공부', + } as const, + beauty: { + code: 'beauty', + name: '뷰티', + } as const, + house: { + code: 'house', + name: '부동산', + } as const, + horror: { + code: 'horror', + name: '호러', + } as const, + travel: { + code: 'travel', + name: '여행', + } as const, }; diff --git a/client/src/libs/data.ts b/client/src/libs/data.ts new file mode 100644 index 00000000..3923321f --- /dev/null +++ b/client/src/libs/data.ts @@ -0,0 +1,11 @@ +import { CONTENTS_CATEGORY } from '@libs/constants'; +import { ContentsCategoryKey } from '@libs/internalTypes'; + +export const contentsCategories = Object.keys(CONTENTS_CATEGORY).map((categoryKey) => { + const key = categoryKey as ContentsCategoryKey; + return { + key, + code: CONTENTS_CATEGORY[key].code, + name: CONTENTS_CATEGORY[key].name, + }; +}); diff --git a/client/src/libs/internalTypes.ts b/client/src/libs/internalTypes.ts index 96430abb..43a69c4a 100644 --- a/client/src/libs/internalTypes.ts +++ b/client/src/libs/internalTypes.ts @@ -1,4 +1,4 @@ -import { APP_THEME, HTTP_METHOD, VIDEO_ICON_COMPONENT_TYPE } from '@libs/constants'; +import { APP_THEME, CONTENTS_CATEGORY, HTTP_METHOD, VIDEO_ICON_COMPONENT_TYPE } from '@libs/constants'; export type SvgComponentProps = { svgTitle?: string; @@ -21,6 +21,8 @@ export type Live = { }; }; +export type ContentsCategoryKey = keyof typeof CONTENTS_CATEGORY; + export type Follow = { id: string; }; From 9fe32f107a4c29df958dd89e451d0632d0683c32 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Mon, 25 Nov 2024 17:50:38 +0900 Subject: [PATCH 017/129] =?UTF-8?q?feat:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=82=B4=20=EB=B6=84?= =?UTF-8?q?=EC=9C=84=EA=B8=B0=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20-=20=EB=B6=84=EC=9C=84=EA=B8=B0=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EB=85=B8=EC=B6=9C=20#264=20-=20=EB=B6=84=EC=9C=84=EA=B8=B0=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=ED=85=9C=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=ED=95=B4=EB=8B=B9?= =?UTF-8?q?=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=A1=9C=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20#266?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../categories/features/CategoryCard.tsx | 9 --- .../features/MoodsCategoryPalette.tsx | 44 ++++++++++++++ .../(domain)/categories/moods/[code]/page.tsx | 5 ++ client/src/app/(domain)/categories/page.tsx | 22 ++++++- client/src/libs/constants.ts | 60 ++++++++++++------- client/src/libs/data.ts | 13 +++- client/src/libs/internalTypes.ts | 3 +- 7 files changed, 121 insertions(+), 35 deletions(-) delete mode 100644 client/src/app/(domain)/categories/features/CategoryCard.tsx create mode 100644 client/src/app/(domain)/categories/features/MoodsCategoryPalette.tsx create mode 100644 client/src/app/(domain)/categories/moods/[code]/page.tsx diff --git a/client/src/app/(domain)/categories/features/CategoryCard.tsx b/client/src/app/(domain)/categories/features/CategoryCard.tsx deleted file mode 100644 index 526f21b5..00000000 --- a/client/src/app/(domain)/categories/features/CategoryCard.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { type PropsWithChildren } from 'react'; - -type Props = PropsWithChildren; - -const CategoryCard = ({ children }: Props) => { - return
{children}
; -}; - -export default CategoryCard; diff --git a/client/src/app/(domain)/categories/features/MoodsCategoryPalette.tsx b/client/src/app/(domain)/categories/features/MoodsCategoryPalette.tsx new file mode 100644 index 00000000..7fc8c1a8 --- /dev/null +++ b/client/src/app/(domain)/categories/features/MoodsCategoryPalette.tsx @@ -0,0 +1,44 @@ +import { MOODS_CATEGORY } from '@libs/constants'; +import { MoodsCategoryKey } from '@libs/internalTypes'; +import clsx from 'clsx'; + +type Props = { + code: MoodsCategoryKey; +}; + +const MoodsCategoryPalette = ({ code }: Props) => { + switch (code) { + case MOODS_CATEGORY.unknown.code: + return
; + case MOODS_CATEGORY.lonely.code: + return
; + case MOODS_CATEGORY.interesting.code: + return ( +
+ ); + case MOODS_CATEGORY.calm.code: + return
; + case MOODS_CATEGORY.depressed.code: + return ( +
+ ); + case MOODS_CATEGORY.happy.code: + return
; + case MOODS_CATEGORY.getking.code: + return ( +
+ ); + case MOODS_CATEGORY.funny.code: + return
; + case MOODS_CATEGORY.energetic.code: + return ( +
+ ); + default: + return null; + } +}; + +export default MoodsCategoryPalette; diff --git a/client/src/app/(domain)/categories/moods/[code]/page.tsx b/client/src/app/(domain)/categories/moods/[code]/page.tsx new file mode 100644 index 00000000..db2c7654 --- /dev/null +++ b/client/src/app/(domain)/categories/moods/[code]/page.tsx @@ -0,0 +1,5 @@ +const MoodCategoryPage = () => { + return
MoodCategoryPage
; +}; + +export default MoodCategoryPage; diff --git a/client/src/app/(domain)/categories/page.tsx b/client/src/app/(domain)/categories/page.tsx index 5bc2d563..347838cd 100644 --- a/client/src/app/(domain)/categories/page.tsx +++ b/client/src/app/(domain)/categories/page.tsx @@ -1,8 +1,9 @@ -import { contentsCategories } from '@libs/data'; +import { contentsCategories, moodsCategories } from '@libs/data'; import clsx from 'clsx'; import { type Metadata } from 'next'; import Link from 'next/link'; import ContentsCategoryImage from './features/ContentsCategoryImage'; +import MoodsCategoryPalette from './features/MoodsCategoryPalette'; export const metadata: Metadata = { title: '카테고리', @@ -30,6 +31,25 @@ const CategoriesPage = () => {
))}
+

분위기 카테고리

+
+ {moodsCategories.map((c) => ( +
+ +
+
+ +
+
+ +

{c.name}

+
+ ))} +
); }; diff --git a/client/src/libs/constants.ts b/client/src/libs/constants.ts index f6954d78..53b4968a 100644 --- a/client/src/libs/constants.ts +++ b/client/src/libs/constants.ts @@ -28,27 +28,45 @@ export const SOCKET_EVENT = { SET_ANONYMOUS_NAME: 'setAnonymousName' as const, }; -// export const CONTENTS_CATEGORY = { -// talk: '소통' as const, -// game: '게임' as const, -// cook: '요리' as const, -// outdoor: '야외' as const, -// dailylife: '일상' as const, -// virtual: '버추얼' as const, -// mukbang: '먹방' as const, -// politics: '정치' as const, -// music: '음악' as const, -// economy: '경제' as const, -// radio: '라디오' as const, -// develop: '개발' as const, -// fishing: '낚시' as const, -// news: '뉴스' as const, -// study: '공부' as const, -// beauty: '뷰티' as const, -// house: '부동산' as const, -// horror: '호러' as const, -// travel: '여행' as const, -// }; +export const MOODS_CATEGORY = { + unknown: { + code: 'unknown', + name: '나도 모름', + } as const, + lonely: { + code: 'lonely', + name: '쓸쓸한', + } as const, + interesting: { + code: 'interesting', + name: '흥미진진한', + } as const, + calm: { + code: 'calm', + name: '잔잔한', + } as const, + depressed: { + code: 'depressed', + name: '우울한', + } as const, + happy: { + code: 'happy', + name: '행복한', + } as const, + getking: { + code: 'getking', + name: '킹받는', + } as const, + funny: { + code: 'funny', + name: '웃기는', + } as const, + energetic: { + code: 'energetic', + name: '활기찬', + } as const, +}; + export const CONTENTS_CATEGORY = { talk: { code: 'talk', diff --git a/client/src/libs/data.ts b/client/src/libs/data.ts index 3923321f..82ad88cd 100644 --- a/client/src/libs/data.ts +++ b/client/src/libs/data.ts @@ -1,11 +1,18 @@ -import { CONTENTS_CATEGORY } from '@libs/constants'; -import { ContentsCategoryKey } from '@libs/internalTypes'; +import { CONTENTS_CATEGORY, MOODS_CATEGORY } from '@libs/constants'; +import type { ContentsCategoryKey, MoodsCategoryKey } from '@libs/internalTypes'; export const contentsCategories = Object.keys(CONTENTS_CATEGORY).map((categoryKey) => { const key = categoryKey as ContentsCategoryKey; return { - key, code: CONTENTS_CATEGORY[key].code, name: CONTENTS_CATEGORY[key].name, }; }); + +export const moodsCategories = Object.keys(MOODS_CATEGORY).map((categoryKey) => { + const key = categoryKey as MoodsCategoryKey; + return { + code: MOODS_CATEGORY[key].code, + name: MOODS_CATEGORY[key].name, + }; +}); diff --git a/client/src/libs/internalTypes.ts b/client/src/libs/internalTypes.ts index 43a69c4a..8d28063e 100644 --- a/client/src/libs/internalTypes.ts +++ b/client/src/libs/internalTypes.ts @@ -1,4 +1,4 @@ -import { APP_THEME, CONTENTS_CATEGORY, HTTP_METHOD, VIDEO_ICON_COMPONENT_TYPE } from '@libs/constants'; +import { APP_THEME, CONTENTS_CATEGORY, HTTP_METHOD, MOODS_CATEGORY, VIDEO_ICON_COMPONENT_TYPE } from '@libs/constants'; export type SvgComponentProps = { svgTitle?: string; @@ -21,6 +21,7 @@ export type Live = { }; }; +export type MoodsCategoryKey = keyof typeof MOODS_CATEGORY; export type ContentsCategoryKey = keyof typeof CONTENTS_CATEGORY; export type Follow = { From e768fee8206e06f0977dfc7429002f1b3c09eb82 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Mon, 25 Nov 2024 17:55:08 +0900 Subject: [PATCH 018/129] =?UTF-8?q?refactor:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20constants=20=ED=82=A4=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EC=BB=A8=EB=B2=A4=EC=85=98=20=EB=A7=9E=EC=B6=94=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/ContentsCategoryImage.tsx | 38 +++--- .../features/MoodsCategoryPalette.tsx | 18 +-- client/src/libs/constants.ts | 112 +++++++++--------- client/src/libs/data.ts | 8 +- 4 files changed, 88 insertions(+), 88 deletions(-) diff --git a/client/src/app/(domain)/categories/features/ContentsCategoryImage.tsx b/client/src/app/(domain)/categories/features/ContentsCategoryImage.tsx index 9f30e661..c8ad780f 100644 --- a/client/src/app/(domain)/categories/features/ContentsCategoryImage.tsx +++ b/client/src/app/(domain)/categories/features/ContentsCategoryImage.tsx @@ -27,43 +27,43 @@ type Props = { const ContentsCategoryImage = ({ code }: Props) => { switch (code) { - case CONTENTS_CATEGORY.talk.code: + case CONTENTS_CATEGORY.talk.CODE: return {`${CONTENTS_CATEGORY[code]}; - case CONTENTS_CATEGORY.game.code: + case CONTENTS_CATEGORY.game.CODE: return {`${CONTENTS_CATEGORY[code]}; - case CONTENTS_CATEGORY.cook.code: + case CONTENTS_CATEGORY.cook.CODE: return {`${CONTENTS_CATEGORY[code]}; - case CONTENTS_CATEGORY.outdoor.code: + case CONTENTS_CATEGORY.outdoor.CODE: return {`${CONTENTS_CATEGORY[code]}; - case CONTENTS_CATEGORY.dailylife.code: + case CONTENTS_CATEGORY.dailylife.CODE: return {`${CONTENTS_CATEGORY[code]}; - case CONTENTS_CATEGORY.virtual.code: + case CONTENTS_CATEGORY.virtual.CODE: return {`${CONTENTS_CATEGORY[code]}; - case CONTENTS_CATEGORY.mukbang.code: + case CONTENTS_CATEGORY.mukbang.CODE: return {`${CONTENTS_CATEGORY[code]}; - case CONTENTS_CATEGORY.politics.code: + case CONTENTS_CATEGORY.politics.CODE: return {`${CONTENTS_CATEGORY[code]}; - case CONTENTS_CATEGORY.music.code: + case CONTENTS_CATEGORY.music.CODE: return {`${CONTENTS_CATEGORY[code]}; - case CONTENTS_CATEGORY.economy.code: + case CONTENTS_CATEGORY.economy.CODE: return {`${CONTENTS_CATEGORY[code]}; - case CONTENTS_CATEGORY.radio.code: + case CONTENTS_CATEGORY.radio.CODE: return {`${CONTENTS_CATEGORY[code]}; - case CONTENTS_CATEGORY.develop.code: + case CONTENTS_CATEGORY.develop.CODE: return {`${CONTENTS_CATEGORY[code]}; - case CONTENTS_CATEGORY.fishing.code: + case CONTENTS_CATEGORY.fishing.CODE: return {`${CONTENTS_CATEGORY[code]}; - case CONTENTS_CATEGORY.news.code: + case CONTENTS_CATEGORY.news.CODE: return {`${CONTENTS_CATEGORY[code]}; - case CONTENTS_CATEGORY.study.code: + case CONTENTS_CATEGORY.study.CODE: return {`${CONTENTS_CATEGORY[code]}; - case CONTENTS_CATEGORY.beauty.code: + case CONTENTS_CATEGORY.beauty.CODE: return {`${CONTENTS_CATEGORY[code]}; - case CONTENTS_CATEGORY.house.code: + case CONTENTS_CATEGORY.house.CODE: return {`${CONTENTS_CATEGORY[code]}; - case CONTENTS_CATEGORY.horror.code: + case CONTENTS_CATEGORY.horror.CODE: return {`${CONTENTS_CATEGORY[code]}; - case CONTENTS_CATEGORY.travel.code: + case CONTENTS_CATEGORY.travel.CODE: return {`${CONTENTS_CATEGORY[code]}; default: return null; diff --git a/client/src/app/(domain)/categories/features/MoodsCategoryPalette.tsx b/client/src/app/(domain)/categories/features/MoodsCategoryPalette.tsx index 7fc8c1a8..7520a5a1 100644 --- a/client/src/app/(domain)/categories/features/MoodsCategoryPalette.tsx +++ b/client/src/app/(domain)/categories/features/MoodsCategoryPalette.tsx @@ -8,31 +8,31 @@ type Props = { const MoodsCategoryPalette = ({ code }: Props) => { switch (code) { - case MOODS_CATEGORY.unknown.code: + case MOODS_CATEGORY.unknown.CODE: return
; - case MOODS_CATEGORY.lonely.code: + case MOODS_CATEGORY.lonely.CODE: return
; - case MOODS_CATEGORY.interesting.code: + case MOODS_CATEGORY.interesting.CODE: return (
); - case MOODS_CATEGORY.calm.code: + case MOODS_CATEGORY.calm.CODE: return
; - case MOODS_CATEGORY.depressed.code: + case MOODS_CATEGORY.depressed.CODE: return (
); - case MOODS_CATEGORY.happy.code: + case MOODS_CATEGORY.happy.CODE: return
; - case MOODS_CATEGORY.getking.code: + case MOODS_CATEGORY.getking.CODE: return (
); - case MOODS_CATEGORY.funny.code: + case MOODS_CATEGORY.funny.CODE: return
; - case MOODS_CATEGORY.energetic.code: + case MOODS_CATEGORY.energetic.CODE: return (
); diff --git a/client/src/libs/constants.ts b/client/src/libs/constants.ts index 53b4968a..c0121989 100644 --- a/client/src/libs/constants.ts +++ b/client/src/libs/constants.ts @@ -30,118 +30,118 @@ export const SOCKET_EVENT = { export const MOODS_CATEGORY = { unknown: { - code: 'unknown', - name: '나도 모름', + CODE: 'unknown', + NAME: '나도 모름', } as const, lonely: { - code: 'lonely', - name: '쓸쓸한', + CODE: 'lonely', + NAME: '쓸쓸한', } as const, interesting: { - code: 'interesting', - name: '흥미진진한', + CODE: 'interesting', + NAME: '흥미진진한', } as const, calm: { - code: 'calm', - name: '잔잔한', + CODE: 'calm', + NAME: '잔잔한', } as const, depressed: { - code: 'depressed', - name: '우울한', + CODE: 'depressed', + NAME: '우울한', } as const, happy: { - code: 'happy', - name: '행복한', + CODE: 'happy', + NAME: '행복한', } as const, getking: { - code: 'getking', - name: '킹받는', + CODE: 'getking', + NAME: '킹받는', } as const, funny: { - code: 'funny', - name: '웃기는', + CODE: 'funny', + NAME: '웃기는', } as const, energetic: { - code: 'energetic', - name: '활기찬', + CODE: 'energetic', + NAME: '활기찬', } as const, }; export const CONTENTS_CATEGORY = { talk: { - code: 'talk', - name: '소통', + CODE: 'talk', + NAME: '소통', } as const, game: { - code: 'game', - name: '게임', + CODE: 'game', + NAME: '게임', } as const, cook: { - code: 'cook', - name: '요리', + CODE: 'cook', + NAME: '요리', } as const, outdoor: { - code: 'outdoor', - name: '야외', + CODE: 'outdoor', + NAME: '야외', } as const, dailylife: { - code: 'dailylife', - name: '일상', + CODE: 'dailylife', + NAME: '일상', } as const, virtual: { - code: 'virtual', - name: '버추얼', + CODE: 'virtual', + NAME: '버추얼', } as const, mukbang: { - code: 'mukbang', - name: '먹방', + CODE: 'mukbang', + NAME: '먹방', } as const, politics: { - code: 'politics', - name: '정치', + CODE: 'politics', + NAME: '정치', } as const, music: { - code: 'music', - name: '음악', + CODE: 'music', + NAME: '음악', } as const, economy: { - code: 'economy', - name: '경제', + CODE: 'economy', + NAME: '경제', } as const, radio: { - code: 'radio', - name: '라디오', + CODE: 'radio', + NAME: '라디오', } as const, develop: { - code: 'develop', - name: '개발', + CODE: 'develop', + NAME: '개발', } as const, fishing: { - code: 'fishing', - name: '낚시', + CODE: 'fishing', + NAME: '낚시', } as const, news: { - code: 'news', - name: '뉴스', + CODE: 'news', + NAME: '뉴스', } as const, study: { - code: 'study', - name: '공부', + CODE: 'study', + NAME: '공부', } as const, beauty: { - code: 'beauty', - name: '뷰티', + CODE: 'beauty', + NAME: '뷰티', } as const, house: { - code: 'house', - name: '부동산', + CODE: 'house', + NAME: '부동산', } as const, horror: { - code: 'horror', - name: '호러', + CODE: 'horror', + NAME: '호러', } as const, travel: { - code: 'travel', - name: '여행', + CODE: 'travel', + NAME: '여행', } as const, }; diff --git a/client/src/libs/data.ts b/client/src/libs/data.ts index 82ad88cd..2a95973f 100644 --- a/client/src/libs/data.ts +++ b/client/src/libs/data.ts @@ -4,15 +4,15 @@ import type { ContentsCategoryKey, MoodsCategoryKey } from '@libs/internalTypes' export const contentsCategories = Object.keys(CONTENTS_CATEGORY).map((categoryKey) => { const key = categoryKey as ContentsCategoryKey; return { - code: CONTENTS_CATEGORY[key].code, - name: CONTENTS_CATEGORY[key].name, + code: CONTENTS_CATEGORY[key].CODE, + name: CONTENTS_CATEGORY[key].NAME, }; }); export const moodsCategories = Object.keys(MOODS_CATEGORY).map((categoryKey) => { const key = categoryKey as MoodsCategoryKey; return { - code: MOODS_CATEGORY[key].code, - name: MOODS_CATEGORY[key].name, + code: MOODS_CATEGORY[key].CODE, + name: MOODS_CATEGORY[key].NAME, }; }); From 7fd2f91f10ba4a46056c93435d653775690b3ea7 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Mon, 25 Nov 2024 17:46:38 +0900 Subject: [PATCH 019/129] =?UTF-8?q?feat:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=A9=EC=86=A1=20=EB=AA=A9=EB=A1=9D=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `/live/category?content=music&mood=happy`와 같이 카테고리를 포함한 요청이 왔을 때 해당 카테고리에 맞는 방송 목록을 전달할 수 있도록 Controller와 Service 로직을 작성하였습니다. --- applicationServer/src/live/live.controller.ts | 8 +++++++- applicationServer/src/live/live.service.ts | 14 +++++++++++--- .../src/live/mock/register-mock.util.ts | 7 +++++-- applicationServer/src/types.ts | 4 ++-- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/applicationServer/src/live/live.controller.ts b/applicationServer/src/live/live.controller.ts index 1cc404a0..47c59c09 100644 --- a/applicationServer/src/live/live.controller.ts +++ b/applicationServer/src/live/live.controller.ts @@ -15,10 +15,16 @@ export class LiveController { ) {} @Get('/list') - getLivelistAlignViewerCount(@Query('start') start: number, @Query('end') end: number) { + getLiveListAlignViewerCount(@Query('start') start: number, @Query('end') end: number) { return this.liveService.getLiveList(start, end); } + @Get('category') + getCategoryLiveList(@Query() query) { + const liveList = this.liveService.getLiveList(0); + return this.liveService.filterWithCategory(liveList, query); + } + @Get(':broadcastId') getPlaylistUrl(@Param('broadcastId') broadcastId: string) { return this.liveService.responseLiveData(broadcastId); diff --git a/applicationServer/src/live/live.service.ts b/applicationServer/src/live/live.service.ts index 171d049c..172d7f3b 100644 --- a/applicationServer/src/live/live.service.ts +++ b/applicationServer/src/live/live.service.ts @@ -17,7 +17,7 @@ export class LiveService { registerMockLive(this.live); } - getLiveList(start, end) { + getLiveList(start, end?) { const alignLiveList = Array.from(this.live.data.values()).sort((a, b) => b.viewerCount - a.viewerCount); return alignLiveList.slice(start, end ?? alignLiveList.length); } @@ -67,8 +67,8 @@ export class LiveService { broadcastId: member.broadcast_id, broadcastPath: `${member.broadcast_id}/${internalPath}`, title: `${member.name}의 라이브 방송`, - contentCategory: '', - moodCategory: '', + contentCategory: null, + moodCategory: null, tags: [], thumbnailUrl: `https://kr.object.ncloudstorage.com/media-storage/${member.broadcast_id}/${internalPath}/dynamic_thumbnail.jpg`, viewerCount: 0, @@ -111,4 +111,12 @@ export class LiveService { return interval(NOTIFY_LIVE_DATA_INTERVAL_TIME).pipe(map(() => ({ data: this.live.data.get(broadcastId) }))); } + + filterWithCategory(liveList: Array, condition) { + return liveList.filter((live) => { + return ( + live.contentCategory == (condition.content ?? 'unknown') || live.moodCategory == (condition.mood ?? 'unknown') + ); + }); + } } diff --git a/applicationServer/src/live/mock/register-mock.util.ts b/applicationServer/src/live/mock/register-mock.util.ts index 260dc39f..7d190699 100644 --- a/applicationServer/src/live/mock/register-mock.util.ts +++ b/applicationServer/src/live/mock/register-mock.util.ts @@ -8,14 +8,17 @@ function registerMockLive(live) { '58fefadc-3fbd-40ad-bce1-f769c02c887f', 'fec06dd7-11b4-4efd-b69e-5a9dc93c3891', ]; + const contentCategoryList = ['music', 'talk', null, 'cook', 'mukbang']; + const moodCategoryList = [null, 'calm', null, 'happy', null]; mockBroadcastIdList.forEach((data, idx) => { const name = generateRandomName(); live.data.set(mockBroadcastIdList[idx], { broadcastId: mockBroadcastIdList[idx], + boradcastPath: `${mockBroadcastIdList[idx]}`, title: `${name}의 라이브 방송`, - contentCategory: '소통', - moodCategory: '', + contentCategory: contentCategoryList[idx], + moodCategory: moodCategoryList[idx], tags: [`방송 ${idx}`], thumbnailUrl: `https://kr.object.ncloudstorage.com/media-storage/${mockBroadcastIdList[idx]}/dynamic_thumbnail.jpg`, viewerCount: 0, diff --git a/applicationServer/src/types.ts b/applicationServer/src/types.ts index 9eb17270..92be90f0 100644 --- a/applicationServer/src/types.ts +++ b/applicationServer/src/types.ts @@ -2,8 +2,8 @@ type Broadcast = { broadcastId: string; broadcastPath: string; title: string; - contentCategory: string; - moodCategory: string; + contentCategory?: string; + moodCategory?: string; tags: Array; thumbnailUrl: string; viewerCount: number; From f81b6acad346e1ae46d5138d64181abcb3b12ffa Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Mon, 25 Nov 2024 17:48:10 +0900 Subject: [PATCH 020/129] =?UTF-8?q?test:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=A9=EC=86=A1=20=EB=AA=A9=EB=A1=9D=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 카테고리 방송 목록이 전달된 양식에 맞게 반환되는지 확인하는 테스트를 작성하였습니다. --- .../test/live/live.service.spec.ts | 65 +++++++++++++++---- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/applicationServer/test/live/live.service.spec.ts b/applicationServer/test/live/live.service.spec.ts index 0f35680b..41fc4038 100644 --- a/applicationServer/test/live/live.service.spec.ts +++ b/applicationServer/test/live/live.service.spec.ts @@ -10,9 +10,10 @@ function getMockLiveDataList(count) { const result: Broadcast[] = dummy.map((e, i) => { return { broadcastId: `test${i}`, + broadcastPath: `test${i}/testInternalPath`, title: `title${i}`, - contentCategory: `content${i}`, - moodCategory: `mood${i}`, + contentCategory: `testContent`, + moodCategory: `testMood`, tags: [`i${i}`], thumbnailUrl: `http://thumbnail${i}`, viewerCount: Math.ceil((i + 1000) * Math.random()), @@ -59,13 +60,15 @@ describe('LiveService 테스트', () => { it('생방송 중인 스트리머의 broadcast Id로 playlist를 요청하면 playlist url이 담긴 객체를 반환해야 한다.', () => { const live: Live = Live.getInstance(); - getMockLiveDataList(1).forEach((mockData) => live.data.set(mockData.broadcastId, mockData)); + const liveList = getMockLiveDataList(1); + liveList.forEach((mockData) => live.data.set(mockData.broadcastId, mockData)); + const thisLive = liveList[0]; - const result = service.responseLiveData('test0'); - const createMultivariantPlaylistUrl = (id) => - `https://kr.object.ncloudstorage.com/media-storage/${id}/master_playlist.m3u8`; + const result = service.responseLiveData(thisLive.broadcastId); + const createMultivariantPlaylistUrl = (path) => + `https://kr.object.ncloudstorage.com/media-storage/${path}/master_playlist.m3u8`; - expect(result.playlistUrl).toEqual(createMultivariantPlaylistUrl('test0')); + expect(result.playlistUrl).toEqual(createMultivariantPlaylistUrl(thisLive.broadcastPath)); live.data.clear(); }); @@ -92,9 +95,9 @@ describe('LiveService 테스트', () => { const live: Live = Live.getInstance(); getMockLiveDataList(liveCount).forEach((mockData) => live.data.set(mockData.broadcastId, mockData)); - const testList1 = service.getRandomLiveList(liveCount + 10); + const testList1 = service.getRandomLiveList(liveCount + 100); - expect(testList1.length).toBe(liveCount); + expect(testList1.length).toBe(Array.from(live.data.keys()).length); live.data.clear(); }); @@ -104,7 +107,7 @@ describe('LiveService 테스트', () => { const member = getMockMemberList(1)[0]; expect(() => { - service.addLiveData(member); + service.addLiveData(member, 'testInternalPath'); }).toThrow(); live.data.clear(); @@ -113,7 +116,7 @@ describe('LiveService 테스트', () => { it('방송 시작 요청이 왔을 때 해당 Broadcast Id로 방송 중이 아니라면 방송 목록에 등록되어야 한다.', () => { const live: Live = Live.getInstance(); const member = getMockMemberList(1)[0]; - service.addLiveData(member); + service.addLiveData(member, 'testInternalPath'); expect(live.data.get(member.broadcast_id)).toBeDefined(); @@ -141,4 +144,44 @@ describe('LiveService 테스트', () => { live.data.clear(); }); + + it('콘텐츠 카테고리를 포함한 방송 목록 요청이 왔을 때 해당 카테고리를 가진 방송이 반환되어야 한다.', () => { + const live: Live = Live.getInstance(); + const liveList = getMockLiveDataList(1); + liveList.forEach((mockData) => live.data.set(mockData.broadcastId, mockData)); + const thisLive = liveList[0]; + + const contentCondition = { + content: 'testContent', + }; + const result = service.filterWithCategory(Array.from(live.data.values()), contentCondition); + expect(result[0]).toEqual(thisLive); + live.data.clear(); + }); + + it('분위기 카테고리를 포함한 방송 목록 요청이 왔을 때 해당 카테고리를 가진 방송이 반환되어야 한다.', () => { + const live: Live = Live.getInstance(); + const liveList = getMockLiveDataList(1); + liveList.forEach((mockData) => live.data.set(mockData.broadcastId, mockData)); + const thisLive = liveList[0]; + + const moodCondition = { + mood: 'testMood', + }; + const result = service.filterWithCategory(Array.from(live.data.values()), moodCondition); + expect(result[0]).toEqual(thisLive); + live.data.clear(); + }); + + it('카테고리를 포함한 방송 목록 요청이 왔을 때 해당 카테고리를 가진 방송이 없다면 빈 배열이 반환되어야 한다.', () => { + const live: Live = Live.getInstance(); + getMockLiveDataList(1).forEach((mockData) => live.data.set(mockData.broadcastId, mockData)); + const condition = { + content: 'aaa', + mood: 'bbb', + }; + const result = service.filterWithCategory(Array.from(live.data.values()), condition); + expect(result.length).toBe(0); + live.data.clear(); + }); }); From e2dd134349af4192190e0841f67d856fcc86d63e Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Mon, 25 Nov 2024 17:50:57 +0900 Subject: [PATCH 021/129] =?UTF-8?q?style:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit live.controller의 category 라우트에 / 문자를 추가하였습니다. --- applicationServer/src/live/live.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applicationServer/src/live/live.controller.ts b/applicationServer/src/live/live.controller.ts index 47c59c09..3fdf2645 100644 --- a/applicationServer/src/live/live.controller.ts +++ b/applicationServer/src/live/live.controller.ts @@ -19,7 +19,7 @@ export class LiveController { return this.liveService.getLiveList(start, end); } - @Get('category') + @Get('/category') getCategoryLiveList(@Query() query) { const liveList = this.liveService.getLiveList(0); return this.liveService.filterWithCategory(liveList, query); From 1823d6729c17371e7470b5831da317c7460eabe3 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Mon, 25 Nov 2024 18:52:57 +0900 Subject: [PATCH 022/129] =?UTF-8?q?feat:=20stream=20key=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stream key를 갱신하고 반환하는 api를 구현하였습니다. --- applicationServer/src/member/member.controller.ts | 9 ++++++++- applicationServer/src/member/member.service.ts | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/applicationServer/src/member/member.controller.ts b/applicationServer/src/member/member.controller.ts index f1329e39..8b3f04b0 100644 --- a/applicationServer/src/member/member.controller.ts +++ b/applicationServer/src/member/member.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Req, UseGuards } from '@nestjs/common'; +import { Controller, Get, Patch, Req, UseGuards } from '@nestjs/common'; import { MemberService } from '@member/member.service'; import { NeedLoginGuard } from '@src/auth/core/auth.guard'; import { Request } from 'express'; @@ -23,4 +23,11 @@ export class MemberController { const decodedPayload = this.authService.verifyToken(accessToken); return this.memberService.findOneMemberWithCondition({ id: decodedPayload.memberId }); } + + @Patch('/refresh/streamKey') + getRefreshedStreamKey(@Req() req: Request) { + const accessToken = req.headers['authorization']?.split(' ')[1]; + const decodedPayload = this.authService.verifyToken(accessToken); + return this.memberService.refreshStreamKey(decodedPayload.memberId); + } } diff --git a/applicationServer/src/member/member.service.ts b/applicationServer/src/member/member.service.ts index d1ae0d84..cf874e0c 100644 --- a/applicationServer/src/member/member.service.ts +++ b/applicationServer/src/member/member.service.ts @@ -41,6 +41,13 @@ class MemberService { } return name; } + + async refreshStreamKey(id) { + const newStreamKey = crypto.randomUUID(); + this.memberRepository.update(id, { stream_key: newStreamKey }); + + return { stream_key: newStreamKey }; + } } export { MemberService }; From 927e7bb4bb8b0f9a78f1da2fa474c1e008bdf00e Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Mon, 25 Nov 2024 19:55:25 +0900 Subject: [PATCH 023/129] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EC=83=89=EC=83=81=20=EB=B0=98=ED=99=98=EA=B0=92=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 채팅 서버에서 반환하는 값에 color값을 추가하였고, color를 랜덤으로 반환하는 함수를 구현하였습니다. --- chatServer/main.ts | 11 +++++++++-- chatServer/utils/constants.ts | 20 +++++++++++++++++++- chatServer/utils/name.ts | 25 +++++++++++++++++-------- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/chatServer/main.ts b/chatServer/main.ts index 522299fd..f1d69acc 100644 --- a/chatServer/main.ts +++ b/chatServer/main.ts @@ -1,7 +1,7 @@ import { Server } from 'socket.io'; import validate from 'utf-8-validate'; import EventEmitter from 'events'; -import { generateRandomName } from './utils/name'; +import { Name } from './utils/name'; const PORT = 7990; const io = new Server(PORT, { @@ -17,9 +17,15 @@ eventEmitter.setMaxListeners(Infinity); io.on('connection', (socket) => { if (!socket.handshake.query.name) { + const randomName = Name.generateRandomName(); socket.emit('setAnonymousName', { - name: generateRandomName(), + name: randomName, }); + socket.handshake.query.name = randomName; + } + + if (!Name.colors[socket.handshake.query.name as string]) { + Name.colors[socket.handshake.query.name as string] = Name.generateRandomColor(); } const broadcastId = socket.handshake.query.broadcastId as string; @@ -27,6 +33,7 @@ io.on('connection', (socket) => { socket.emit('chat', { name: chatData.name, content: chatData.content, + color: Name.colors[chatData.name], }); }; eventEmitter.on(broadcastId, chatEvent); diff --git a/chatServer/utils/constants.ts b/chatServer/utils/constants.ts index bad4a399..dc282b5f 100644 --- a/chatServer/utils/constants.ts +++ b/chatServer/utils/constants.ts @@ -704,4 +704,22 @@ const suffix = [ 1475, 1476, 1477, 1478, 1479, 1480, 1481, 1482, 1483, 1484, 1485, 1486, 1487, 1488, 1489, 1490, 1491, ]; -export { prefix, objects, suffix }; +const colorList = [ + '#ff0066', + '#009933', + '#ff33cc', + '#663399', + '#66ff66', + '#0066cc', + '#ff6600', + '#ff9900', + '#330033', + '#003366', + '#333399', + '#063300', + '#990000', + '#ffff33', + '#cc6600', +]; + +export { prefix, objects, suffix, colorList }; diff --git a/chatServer/utils/name.ts b/chatServer/utils/name.ts index e102f46b..2fca935e 100644 --- a/chatServer/utils/name.ts +++ b/chatServer/utils/name.ts @@ -1,11 +1,20 @@ -import { prefix, objects, suffix } from './constants'; +import { prefix, objects, suffix, colorList } from './constants'; -const generateRandomName = () => { - const pre = prefix[Math.floor(Math.random() * prefix.length)]; - const obj = objects[Math.floor(Math.random() * objects.length)]; - const suf = suffix[Math.floor(Math.random() * suffix.length)]; +class Name { + static colors: { [key: string]: string } = {}; - return pre + obj + suf; -}; + static generateRandomColor() { + const idx = Math.floor(colorList.length * Math.random()); + return colorList[idx]; + } -export { generateRandomName }; + static generateRandomName = () => { + const pre = prefix[Math.floor(Math.random() * prefix.length)]; + const obj = objects[Math.floor(Math.random() * objects.length)]; + const suf = suffix[Math.floor(Math.random() * suffix.length)]; + + return pre + obj + suf; + }; +} + +export { Name }; From f545d1b280002bccc5c62cf76ae63f2ace6ea45f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=B8=EB=B9=88?= Date: Mon, 25 Nov 2024 23:48:13 +0900 Subject: [PATCH 024/129] =?UTF-8?q?fix:=20DI=20=EC=BB=A8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=84=88=EC=97=90=20DataSource=EB=A5=BC=20=EC=A3=BC=EC=9E=85?= =?UTF-8?q?=ED=95=A0=20=EB=95=8C=20=EB=AC=B8=EC=9E=90=EC=97=B4=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EB=B0=A9=EC=8B=9D=EC=9D=B4=20=EC=95=84=EB=8B=8C=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B0=B8=EC=A1=B0=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존의 DataSource 주입 방식은 문자열 토큰 방식이었다. 이 방식은 여러 DataSource 즉, 여러 데이터베이스를 주입하려할 때 각 DataSource를 구분하는 용도로 주로 사용하므로 하나의 데이터베이스만 사용하고 있는 우리 서비스에서는 DataSource라는 클래스를 참조하는 것이 더 적합하다고 생각하여 변경하였다. --- applicationServer/src/constants.ts | 1 - applicationServer/src/database/database.providers.ts | 3 +-- applicationServer/src/member/member.providers.ts | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/applicationServer/src/constants.ts b/applicationServer/src/constants.ts index 2979c33b..b5c03ccf 100644 --- a/applicationServer/src/constants.ts +++ b/applicationServer/src/constants.ts @@ -1,6 +1,5 @@ // member.providers.ts export const MEMBER_REPOSITORY = 'MEMBER_REPOSITORY'; -export const DATA_SOURCE = 'DATA_SOURCE'; // live.controller.ts export const SUGGEST_LIVE_COUNT = 10; // live.service.ts diff --git a/applicationServer/src/database/database.providers.ts b/applicationServer/src/database/database.providers.ts index b5e8f8fa..f5429851 100644 --- a/applicationServer/src/database/database.providers.ts +++ b/applicationServer/src/database/database.providers.ts @@ -1,9 +1,8 @@ import path from 'path'; import { DataSource } from 'typeorm'; -import { DATA_SOURCE } from '@src/constants'; const databaseProvider = { - provide: DATA_SOURCE, + provide: DataSource, useFactory: async () => { const dataSource = new DataSource({ type: 'mysql', diff --git a/applicationServer/src/member/member.providers.ts b/applicationServer/src/member/member.providers.ts index 7070acd3..a42da41b 100644 --- a/applicationServer/src/member/member.providers.ts +++ b/applicationServer/src/member/member.providers.ts @@ -1,11 +1,11 @@ import { DataSource } from 'typeorm'; import { Member } from '@member/member.entity'; -import { MEMBER_REPOSITORY, DATA_SOURCE } from '@src/constants'; +import { MEMBER_REPOSITORY } from '@src/constants'; const memberProvider = { provide: MEMBER_REPOSITORY, useFactory: (dataSource: DataSource) => dataSource.getRepository(Member), - inject: [DATA_SOURCE], + inject: [DataSource], }; export { memberProvider }; From 76e06eca18e355286e8d51dc1bcbca7be67112e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=B8=EB=B9=88?= Date: Mon, 25 Nov 2024 23:51:20 +0900 Subject: [PATCH 025/129] =?UTF-8?q?chore:=20follow=20path=20alias=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - follow에 해당하는 path alias를 추가하였다. --- applicationServer/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/applicationServer/tsconfig.json b/applicationServer/tsconfig.json index 8854fb08..46c2363b 100644 --- a/applicationServer/tsconfig.json +++ b/applicationServer/tsconfig.json @@ -19,7 +19,8 @@ "@authUtil/*": ["src/auth/util/*"], "@database/*": ["src/database/*"], "@member/*": ["src/member/*"], - "@live/*": ["src/live/*"] + "@live/*": ["src/live/*"], + "@follow/*": ["src/follow/*"] }, "incremental": true, "skipLibCheck": true, From 40397c3c0162dfe863bdbf39bf4fa0f6caeb1d1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=B8=EB=B9=88?= Date: Mon, 25 Nov 2024 23:55:20 +0900 Subject: [PATCH 026/129] =?UTF-8?q?feat:=20=EB=A9=A4=EB=B2=84=20=ED=8C=94?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0/=EC=96=B8=ED=8C=94=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로그인을 한 유저는 멤버를 팔로우/언팔로우 할 수 있다. - 만약 팔로우를 이미 한 상태에서 팔로우 요청을 하면 BAD_REQUEST 오류를 발생시킨다. - 만약 팔로우를 하지 않은 상태에서 언팔로우 요청을 하면 BAD_REQUEST 오류를 발생시킨다. --- applicationServer/src/app.module.ts | 3 ++- applicationServer/src/constants.ts | 2 ++ .../src/follow/follow.controller.ts | 27 +++++++++++++++++++ applicationServer/src/follow/follow.entity.ts | 18 +++++++++++++ applicationServer/src/follow/follow.module.ts | 14 ++++++++++ .../src/follow/follow.providers.ts | 9 +++++++ .../src/follow/follow.service.ts | 26 ++++++++++++++++++ 7 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 applicationServer/src/follow/follow.controller.ts create mode 100644 applicationServer/src/follow/follow.entity.ts create mode 100644 applicationServer/src/follow/follow.module.ts create mode 100644 applicationServer/src/follow/follow.providers.ts create mode 100644 applicationServer/src/follow/follow.service.ts diff --git a/applicationServer/src/app.module.ts b/applicationServer/src/app.module.ts index 95cf66e1..f480faea 100644 --- a/applicationServer/src/app.module.ts +++ b/applicationServer/src/app.module.ts @@ -5,11 +5,12 @@ import { MemberModule } from '@member/member.module'; import { LiveModule } from '@live/live.module'; import { GithubAuthModule } from '@github/github.module'; import { AuthModule } from '@auth/auth.module'; +import { FollowModule } from '@follow/follow.module'; dotenv.config(); @Module({ - imports: [MemberModule, GithubAuthModule, AuthModule, LiveModule], + imports: [MemberModule, GithubAuthModule, AuthModule, LiveModule, FollowModule], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { diff --git a/applicationServer/src/constants.ts b/applicationServer/src/constants.ts index b5c03ccf..7f80852e 100644 --- a/applicationServer/src/constants.ts +++ b/applicationServer/src/constants.ts @@ -22,3 +22,5 @@ export const BUCKET_NAME = 'media-storage'; export const LOCALHOST = 'localhost'; export const CORS_ORIGIN = 'https://funch.site/'; export const CORS_ORIGIN_WWW = 'https://www.funch.site/'; +// follow.provider.ts +export const FOLLOW_REPOSITORY = 'FOLLOW_REPOSITORY'; diff --git a/applicationServer/src/follow/follow.controller.ts b/applicationServer/src/follow/follow.controller.ts new file mode 100644 index 00000000..671fe956 --- /dev/null +++ b/applicationServer/src/follow/follow.controller.ts @@ -0,0 +1,27 @@ +import { Controller, Post, UseGuards, Body, Delete, HttpException, HttpStatus, HttpCode } from '@nestjs/common'; +import { NeedLoginGuard } from '@src/auth/core/auth.guard'; +import { FollowService } from '@follow/follow.service'; + +@Controller('follow') +@UseGuards(NeedLoginGuard) +export class FollowController { + constructor(private readonly followService: FollowService) {} + + @Post() + @HttpCode(201) + async followMember(@Body('follower') follower: string, @Body('following') following: string) { + const isAlreadyFollowing = await this.followService.findOneFollowWithCondition({ follower, following }); + if (isAlreadyFollowing) throw new HttpException('이미 팔로우 중입니다.', HttpStatus.BAD_REQUEST); + + await this.followService.followMember(follower, following); + } + + @Delete() + @HttpCode(204) + async unfollowMember(@Body('follower') follower: string, @Body('following') following: string) { + const result = await this.followService.unfollowMember(follower, following); + if (result.affected === 0) { + throw new HttpException('팔로우 중이지 않습니다.', HttpStatus.BAD_REQUEST); + } + } +} diff --git a/applicationServer/src/follow/follow.entity.ts b/applicationServer/src/follow/follow.entity.ts new file mode 100644 index 00000000..adfe909b --- /dev/null +++ b/applicationServer/src/follow/follow.entity.ts @@ -0,0 +1,18 @@ +import { Entity, Column, PrimaryColumn } from 'typeorm'; + +@Entity() +class Follow { + @PrimaryColumn({ length: 255 }) + id: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + follower: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + following: string; + + @Column('timestamp', { name: 'created_at', nullable: false, default: () => 'CURRENT_TIMESTAMP' }) + createdAt: Date; +} + +export { Follow }; diff --git a/applicationServer/src/follow/follow.module.ts b/applicationServer/src/follow/follow.module.ts new file mode 100644 index 00000000..d6e1c50e --- /dev/null +++ b/applicationServer/src/follow/follow.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { DatabaseModule } from '@src/database/database.module'; +import { followProvider } from '@follow/follow.providers'; +import { FollowController } from '@follow/follow.controller'; +import { FollowService } from '@follow/follow.service'; +import { AuthModule } from '@src/auth/core/auth.module'; + +@Module({ + imports: [DatabaseModule, AuthModule], + controllers: [FollowController], + providers: [followProvider, FollowService], + exports: [FollowService], +}) +export class FollowModule {} diff --git a/applicationServer/src/follow/follow.providers.ts b/applicationServer/src/follow/follow.providers.ts new file mode 100644 index 00000000..cabbff52 --- /dev/null +++ b/applicationServer/src/follow/follow.providers.ts @@ -0,0 +1,9 @@ +import { DataSource } from 'typeorm'; +import { Follow } from '@follow/follow.entity'; +import { FOLLOW_REPOSITORY } from '@src/constants'; + +export const followProvider = { + provide: FOLLOW_REPOSITORY, + useFactory: (dataSource: DataSource) => dataSource.getRepository(Follow), + inject: [DataSource], +}; diff --git a/applicationServer/src/follow/follow.service.ts b/applicationServer/src/follow/follow.service.ts new file mode 100644 index 00000000..1ac38925 --- /dev/null +++ b/applicationServer/src/follow/follow.service.ts @@ -0,0 +1,26 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { Repository } from 'typeorm'; +import { Follow } from '@follow/follow.entity'; +import { FOLLOW_REPOSITORY } from '@src/constants'; +import crypto from 'crypto'; + +@Injectable() +export class FollowService { + constructor( + @Inject(FOLLOW_REPOSITORY) + private readonly followRepository: Repository, + ) {} + + async findOneFollowWithCondition(condition: { [key: string]: string }) { + return this.followRepository.findOne({ where: condition }); + } + + async followMember(follower: string, following: string) { + const follow = { id: crypto.randomUUID(), follower, following }; + await this.followRepository.save(follow); + } + + async unfollowMember(follower: string, following: string) { + return this.followRepository.delete({ follower, following }); + } +} From cd44b293ba44d7119e0118892d07e9505e523ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=B8=EB=B9=88?= Date: Tue, 26 Nov 2024 02:06:49 +0900 Subject: [PATCH 027/129] =?UTF-8?q?feat:=20=EB=B3=B8=EC=9D=B8=20=ED=8C=94?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0/=EC=96=B8=ED=8C=94=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=EA=B8=88=EC=A7=80=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 본인을 팔로우/언팔로우 하지 못하도록 막았다. --- .../src/follow/follow.controller.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/applicationServer/src/follow/follow.controller.ts b/applicationServer/src/follow/follow.controller.ts index 671fe956..05f9eded 100644 --- a/applicationServer/src/follow/follow.controller.ts +++ b/applicationServer/src/follow/follow.controller.ts @@ -1,6 +1,19 @@ -import { Controller, Post, UseGuards, Body, Delete, HttpException, HttpStatus, HttpCode } from '@nestjs/common'; +import { + Controller, + Post, + UseGuards, + Body, + Delete, + HttpException, + HttpStatus, + HttpCode, + Get, + Param, + Query, +} from '@nestjs/common'; import { NeedLoginGuard } from '@src/auth/core/auth.guard'; import { FollowService } from '@follow/follow.service'; +import { FOLLOWERS, FOLLOWING } from '@src/constants'; @Controller('follow') @UseGuards(NeedLoginGuard) @@ -10,6 +23,7 @@ export class FollowController { @Post() @HttpCode(201) async followMember(@Body('follower') follower: string, @Body('following') following: string) { + if (follower === following) throw new HttpException('본인을 팔로우할 수 없습니다.', HttpStatus.BAD_REQUEST); const isAlreadyFollowing = await this.followService.findOneFollowWithCondition({ follower, following }); if (isAlreadyFollowing) throw new HttpException('이미 팔로우 중입니다.', HttpStatus.BAD_REQUEST); @@ -19,6 +33,7 @@ export class FollowController { @Delete() @HttpCode(204) async unfollowMember(@Body('follower') follower: string, @Body('following') following: string) { + if (follower === following) throw new HttpException('본인을 언팔로우할 수 없습니다.', HttpStatus.BAD_REQUEST); const result = await this.followService.unfollowMember(follower, following); if (result.affected === 0) { throw new HttpException('팔로우 중이지 않습니다.', HttpStatus.BAD_REQUEST); From 3f6427fdb353dac3512d3b67834db384e0cb7e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=B8=EB=B9=88?= Date: Tue, 26 Nov 2024 02:07:33 +0900 Subject: [PATCH 028/129] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20=ED=8C=94=EB=A1=9C=EC=9B=8C/=ED=8C=94=EB=A1=9C?= =?UTF-8?q?=EC=9E=89=20=EB=AA=A9=EB=A1=9D=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EB=8A=94=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자의 팔로워와 팔로잉 목록을 가져온다. --- applicationServer/src/constants.ts | 3 +++ .../src/follow/follow.controller.ts | 19 +++++++++++++++++++ .../src/follow/follow.service.ts | 4 ++++ 3 files changed, 26 insertions(+) diff --git a/applicationServer/src/constants.ts b/applicationServer/src/constants.ts index 7f80852e..50dfdf84 100644 --- a/applicationServer/src/constants.ts +++ b/applicationServer/src/constants.ts @@ -24,3 +24,6 @@ export const CORS_ORIGIN = 'https://funch.site/'; export const CORS_ORIGIN_WWW = 'https://www.funch.site/'; // follow.provider.ts export const FOLLOW_REPOSITORY = 'FOLLOW_REPOSITORY'; +// follow.controller.ts +export const FOLLOWERS = 'followers'; +export const FOLLOWING = 'following'; diff --git a/applicationServer/src/follow/follow.controller.ts b/applicationServer/src/follow/follow.controller.ts index 05f9eded..6f34b28e 100644 --- a/applicationServer/src/follow/follow.controller.ts +++ b/applicationServer/src/follow/follow.controller.ts @@ -39,4 +39,23 @@ export class FollowController { throw new HttpException('팔로우 중이지 않습니다.', HttpStatus.BAD_REQUEST); } } + + @Get(':memberId') + async getFollows(@Param('memberId') memberId: string, @Query('search') search: string) { + const { condition, key } = this.getFollowConditions(search, memberId); + + const results = await this.followService.findAllFollowWithCondition(condition); + const data = results.map((result) => result[key]); + + return { [key]: data }; + } + + private getFollowConditions(search: string, memberId: string) { + if (search === FOLLOWERS) { + return { condition: { following: memberId }, key: FOLLOWERS }; + } else if (search === FOLLOWING) { + return { condition: { follower: memberId }, key: FOLLOWING }; + } + throw new HttpException('올바른 팔로워/팔로잉 정보 요청이 아닙니다.', HttpStatus.BAD_REQUEST); + } } diff --git a/applicationServer/src/follow/follow.service.ts b/applicationServer/src/follow/follow.service.ts index 1ac38925..c0d170e3 100644 --- a/applicationServer/src/follow/follow.service.ts +++ b/applicationServer/src/follow/follow.service.ts @@ -15,6 +15,10 @@ export class FollowService { return this.followRepository.findOne({ where: condition }); } + async findAllFollowWithCondition(condition: { [key: string]: string }) { + return this.followRepository.find({ where: condition }); + } + async followMember(follower: string, following: string) { const follow = { id: crypto.randomUUID(), follower, following }; await this.followRepository.save(follow); From 6f3cf5477aae67446c87d0ed8298558d47f34279 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Tue, 26 Nov 2024 02:00:37 +0900 Subject: [PATCH 029/129] =?UTF-8?q?chore:=20naver=20path=20alias=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit naver 로그인 구현에 따라 package.json과 tsconfig.json에 naver path alias를 추가하였습니다. --- applicationServer/package.json | 1 + applicationServer/tsconfig.json | 1 + 2 files changed, 2 insertions(+) diff --git a/applicationServer/package.json b/applicationServer/package.json index 8552cda3..3e471ea7 100644 --- a/applicationServer/package.json +++ b/applicationServer/package.json @@ -69,6 +69,7 @@ "^@member/(.*)$": "/src/member/$1", "^@auth/(.*)$": "/src/auth/core/$1", "^@github/(.*)$": "/src/auth/github/$1", + "^@naver/(.*)$": "/src/auth/naver/$1", "^@cookie/(.*)$": "/src/auth/cookie/$1", "^@authUtil/(.*)$": "/src/auth/util/$1" } diff --git a/applicationServer/tsconfig.json b/applicationServer/tsconfig.json index 46c2363b..dc2cd04e 100644 --- a/applicationServer/tsconfig.json +++ b/applicationServer/tsconfig.json @@ -16,6 +16,7 @@ "@auth/*": ["src/auth/core/*"], "@cookie/*": ["src/auth/cookie/*"], "@github/*": ["src/auth/github/*"], + "@naver/*": ["src/auth/naver/*"], "@authUtil/*": ["src/auth/util/*"], "@database/*": ["src/database/*"], "@member/*": ["src/member/*"], From 3ecbd9a12f5cbeedf5005d42241f27225ff27b84 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Tue, 26 Nov 2024 02:02:10 +0900 Subject: [PATCH 030/129] =?UTF-8?q?refactor:=20github=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=97=90=EB=9F=AC=EC=B2=98=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit github 로그인 service 로직에 axios시 2xx 응답이 돌아오지 않아 프로세스가 종료되는 것을 방지하기 위해 catch로 에러를 처리할 수 있는 로직을 추가했습니다. 추가로 getUserInfo의 authorization header의 token type을 Token -> Bearer로 변경하였습니다. --- .../src/auth/github/github.service.ts | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/applicationServer/src/auth/github/github.service.ts b/applicationServer/src/auth/github/github.service.ts index d5a063a7..8ed9ec8d 100644 --- a/applicationServer/src/auth/github/github.service.ts +++ b/applicationServer/src/auth/github/github.service.ts @@ -1,6 +1,6 @@ import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import axios from 'axios'; -import { ACCESS_TOKEN_URL, APPLICATION_JSON, RESOURCE_URL } from '@src/constants'; +import { GITHUB_ACCESS_TOKEN_URL, APPLICATION_JSON, GITHUB_RESOURCE_URL } from '@src/constants'; @Injectable() class GithubAuthService { @@ -9,14 +9,18 @@ class GithubAuthService { async getAccessToken(code: string): Promise { const request = { code, - client_id: process.env.CLIENT_ID, - client_secret: process.env.CLIENT_SECRETS, + client_id: process.env.GITHUB_CLIENT_ID, + client_secret: process.env.GITHUB_CLIENT_SECRET, }; - const response = await axios.post(ACCESS_TOKEN_URL, request, { - headers: { - accept: APPLICATION_JSON, - }, - }); + const response = await axios + .post(GITHUB_ACCESS_TOKEN_URL, request, { + headers: { + accept: APPLICATION_JSON, + }, + }) + .catch(() => { + throw new HttpException('Github AccessToken 가져오기 실패', HttpStatus.UNAUTHORIZED); + }); if (response.data.error) { throw new HttpException('Github AccessToken 가져오기 실패', HttpStatus.UNAUTHORIZED); @@ -25,11 +29,15 @@ class GithubAuthService { } async getUserInfo(accessToken: string) { - const { data } = await axios.get(RESOURCE_URL, { - headers: { - Authorization: `token ${accessToken}`, - }, - }); + const { data } = await axios + .get(GITHUB_RESOURCE_URL, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + .catch(() => { + throw new HttpException('Github 유저 정보 가져오기 실패', HttpStatus.FAILED_DEPENDENCY); + }); return data; } From 29359150ae9b6bc361e49825d1f1a05b7cc64f06 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Tue, 26 Nov 2024 02:03:56 +0900 Subject: [PATCH 031/129] =?UTF-8?q?style:=20Github=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20=EC=83=81=EC=88=98=20=EB=A6=AC?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B0=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Github 로그인 시 사용하던 상수의 이름을 naver 로그인 추가에 따라 보편적인 이름에서 로그인 사이트를 특정할 수 있도록 변경하였습니다. ex) RESOURCE_URL -> GITHUB_RESOURCE_URL --- applicationServer/src/constants.ts | 2 +- applicationServer/test/auth/github/github.service.spec.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/applicationServer/src/constants.ts b/applicationServer/src/constants.ts index 50dfdf84..797a0400 100644 --- a/applicationServer/src/constants.ts +++ b/applicationServer/src/constants.ts @@ -5,7 +5,7 @@ export const SUGGEST_LIVE_COUNT = 10; // live.service.ts export const NOTIFY_LIVE_DATA_INTERVAL_TIME = 30000; // github.service.ts -export const ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token'; +export const GITHUB_ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token'; export const APPLICATION_JSON = 'application/json'; export const RESOURCE_URL = 'https://api.github.com/user'; // member.service.ts diff --git a/applicationServer/test/auth/github/github.service.spec.ts b/applicationServer/test/auth/github/github.service.spec.ts index 94e04c10..f939ebd4 100644 --- a/applicationServer/test/auth/github/github.service.spec.ts +++ b/applicationServer/test/auth/github/github.service.spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GithubAuthService } from '@github/github.service'; import axios from 'axios'; import { HttpException, HttpStatus } from '@nestjs/common'; -import { ACCESS_TOKEN_URL, APPLICATION_JSON } from '@src/constants'; +import { GITHUB_ACCESS_TOKEN_URL, APPLICATION_JSON } from '@src/constants'; jest.mock('axios'); @@ -30,7 +30,7 @@ describe('GithubAuthService', () => { const result = await githubAuthService.getAccessToken(code); expect(mockAxios.post).toHaveBeenCalledWith( - ACCESS_TOKEN_URL, + GITHUB_ACCESS_TOKEN_URL, { code, client_id: process.env.CLIENT_ID, @@ -53,7 +53,7 @@ describe('GithubAuthService', () => { ); expect(mockAxios.post).toHaveBeenCalledWith( - ACCESS_TOKEN_URL, + GITHUB_ACCESS_TOKEN_URL, { code, client_id: process.env.CLIENT_ID, From 810c304e64d9c5f8bd31328cb8bdc6dd65eb07d5 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Tue, 26 Nov 2024 02:05:39 +0900 Subject: [PATCH 032/129] =?UTF-8?q?style:=20live=20mock=20data=20=EC=98=A4?= =?UTF-8?q?=ED=83=88=EC=9E=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- applicationServer/src/live/mock/register-mock.util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applicationServer/src/live/mock/register-mock.util.ts b/applicationServer/src/live/mock/register-mock.util.ts index 7d190699..03e2332f 100644 --- a/applicationServer/src/live/mock/register-mock.util.ts +++ b/applicationServer/src/live/mock/register-mock.util.ts @@ -15,7 +15,7 @@ function registerMockLive(live) { const name = generateRandomName(); live.data.set(mockBroadcastIdList[idx], { broadcastId: mockBroadcastIdList[idx], - boradcastPath: `${mockBroadcastIdList[idx]}`, + broadcastPath: `${mockBroadcastIdList[idx]}`, title: `${name}의 라이브 방송`, contentCategory: contentCategoryList[idx], moodCategory: moodCategoryList[idx], From 36f16f17c3c1f36d731022cdf9460ea960defa9b Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Tue, 26 Nov 2024 02:06:24 +0900 Subject: [PATCH 033/129] =?UTF-8?q?feat:=20=EB=84=A4=EC=9D=B4=EB=B2=84=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8/=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 네이버 로그인 API를 이용한 회원가입 및 로그인 로직을 구현하였습니다. --- applicationServer/src/app.module.ts | 3 +- .../src/auth/naver/naver.controller.ts | 48 +++++++++++++++++++ .../src/auth/naver/naver.module.ts | 14 ++++++ .../src/auth/naver/naver.service.ts | 44 +++++++++++++++++ applicationServer/src/constants.ts | 5 +- 5 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 applicationServer/src/auth/naver/naver.controller.ts create mode 100644 applicationServer/src/auth/naver/naver.module.ts create mode 100644 applicationServer/src/auth/naver/naver.service.ts diff --git a/applicationServer/src/app.module.ts b/applicationServer/src/app.module.ts index f480faea..be15412e 100644 --- a/applicationServer/src/app.module.ts +++ b/applicationServer/src/app.module.ts @@ -6,11 +6,12 @@ import { LiveModule } from '@live/live.module'; import { GithubAuthModule } from '@github/github.module'; import { AuthModule } from '@auth/auth.module'; import { FollowModule } from '@follow/follow.module'; +import { NaverAuthModule } from '@naver/naver.module'; dotenv.config(); @Module({ - imports: [MemberModule, GithubAuthModule, AuthModule, LiveModule, FollowModule], + imports: [MemberModule, GithubAuthModule, AuthModule, LiveModule, FollowModule, NaverAuthModule], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { diff --git a/applicationServer/src/auth/naver/naver.controller.ts b/applicationServer/src/auth/naver/naver.controller.ts new file mode 100644 index 00000000..0a70f048 --- /dev/null +++ b/applicationServer/src/auth/naver/naver.controller.ts @@ -0,0 +1,48 @@ +import { Controller, Post, Body, Res, UseGuards } from '@nestjs/common'; +import { Response } from 'express'; +import { MemberService } from '@src/member/member.service'; +import { NaverAuthService } from '@naver/naver.service'; +import { AuthService } from '@auth/auth.service'; +import { CookieService } from '@cookie/cookie.service'; +import { NoNeedLoginGuard } from '@auth/auth.guard'; +import { REFRESH_TOKEN } from '@src/constants'; + +@Controller('auth/naver') +class NaverAuthController { + constructor( + private readonly naverAuthService: NaverAuthService, + private readonly memberService: MemberService, + private readonly authService: AuthService, + private readonly cookieService: CookieService, + ) {} + + @Post('/callback') + @UseGuards(NoNeedLoginGuard) + async getAccessToken( + @Body('code') code: string, + @Body('state') state: string, + @Res({ passthrough: true }) res: Response, + ) { + const naverAccessToken = await this.naverAuthService.getAccessToken(code, state); + const { id } = await this.naverAuthService.getUserInfo(naverAccessToken); + const member = await this.findOrRegisterMember(`Naver@${id}`); + const accessToken = this.authService.generateAccessToken(member.id); + const refreshToken = this.authService.generateRefreshToken(member.id); + + this.authService.saveRefreshToken(member.id, refreshToken); + this.cookieService.setCookie(res, REFRESH_TOKEN, refreshToken); + + return { accessToken, name: member.name, profile_image: member.profile_image, broadcast_id: member.broadcast_id }; + } + + private async findOrRegisterMember(memberId: string) { + const member = await this.memberService.findOneMemberWithCondition({ id: memberId }); + if (!member) { + const newMember = await this.memberService.register(memberId); + return newMember; + } + return member; + } +} + +export { NaverAuthController }; diff --git a/applicationServer/src/auth/naver/naver.module.ts b/applicationServer/src/auth/naver/naver.module.ts new file mode 100644 index 00000000..0c0acf05 --- /dev/null +++ b/applicationServer/src/auth/naver/naver.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { NaverAuthController } from '@naver/naver.controller'; +import { NaverAuthService } from '@naver/naver.service'; +import { DatabaseModule } from '@src/database/database.module'; +import { MemberModule } from '@src/member/member.module'; +import { AuthModule } from '@auth/auth.module'; +import { CookieModule } from '@cookie/cookie.module'; + +@Module({ + imports: [DatabaseModule, MemberModule, AuthModule, CookieModule], + controllers: [NaverAuthController], + providers: [NaverAuthService], +}) +export class NaverAuthModule {} diff --git a/applicationServer/src/auth/naver/naver.service.ts b/applicationServer/src/auth/naver/naver.service.ts new file mode 100644 index 00000000..616383fe --- /dev/null +++ b/applicationServer/src/auth/naver/naver.service.ts @@ -0,0 +1,44 @@ +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import axios from 'axios'; +import { NAVER_ACCESS_TOKEN_URL, NAVER_RESOURCE_URL } from '@src/constants'; + +@Injectable() +class NaverAuthService { + async getAccessToken(code: string, state: string): Promise { + const TOKEN_REQUEST_URL = [ + NAVER_ACCESS_TOKEN_URL, + '?grant_type=authorization_code', + `&client_id=${process.env.NAVER_CLIENT_ID}`, + `&client_secret=${process.env.NAVER_CLIENT_SECRET}`, + `&code=${code}`, + `&state=${state}`, + ] + .join('') + .trim(); + + const response = await axios.get(TOKEN_REQUEST_URL).catch(() => { + throw new HttpException('Naver AccessToken 가져오기 실패', HttpStatus.UNAUTHORIZED); + }); + + if (response.data.error) { + throw new HttpException('Naver AccessToken 가져오기 실패', HttpStatus.UNAUTHORIZED); + } + return response.data.access_token; + } + + async getUserInfo(accessToken: string) { + const { data } = await axios + .get(NAVER_RESOURCE_URL, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + .catch(() => { + throw new HttpException('Naver AccessToken 가져오기 실패', HttpStatus.UNAUTHORIZED); + }); + + return data.response; + } +} + +export { NaverAuthService }; diff --git a/applicationServer/src/constants.ts b/applicationServer/src/constants.ts index 797a0400..e93eaf08 100644 --- a/applicationServer/src/constants.ts +++ b/applicationServer/src/constants.ts @@ -7,7 +7,10 @@ export const NOTIFY_LIVE_DATA_INTERVAL_TIME = 30000; // github.service.ts export const GITHUB_ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token'; export const APPLICATION_JSON = 'application/json'; -export const RESOURCE_URL = 'https://api.github.com/user'; +export const GITHUB_RESOURCE_URL = 'https://api.github.com/user'; +// naver.service.ts +export const NAVER_ACCESS_TOKEN_URL = 'https://nid.naver.com/oauth2.0/token'; +export const NAVER_RESOURCE_URL = 'https://openapi.naver.com/v1/nid/me'; // member.service.ts export const DEFAULT_PROFILE_IMAGE = 'https://kr.object.ncloudstorage.com/funch-storage/profile/profile_default.png'; // cookie.service.ts From 8a5e8cd833e5bb38fe51074dfad4b692a9f30133 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Mon, 25 Nov 2024 23:17:21 +0900 Subject: [PATCH 034/129] =?UTF-8?q?fix:=20=EB=B9=84=EB=94=94=EC=98=A4=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EA=B0=9C=EC=84=A0=20-=20savedId?= =?UTF-8?q?=20=EC=83=81=ED=83=9C=EB=A5=BC=20LiveProvider=EC=97=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=98=EA=B3=A0=20=EC=9D=B4=ED=8C=A9?= =?UTF-8?q?=ED=8A=B8=EC=97=90=20=EB=B9=84=EA=B5=90=20=EC=A1=B0=EA=B1=B4?= =?UTF-8?q?=EB=AC=B8=EC=9D=84=20=EC=B6=94=EA=B0=80=20#287?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/app/(domain)/features/live/LiveSection.tsx | 4 ++-- client/src/app/providers/LiveProvider.tsx | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/client/src/app/(domain)/features/live/LiveSection.tsx b/client/src/app/(domain)/features/live/LiveSection.tsx index ebc8a5d1..1be07ef3 100644 --- a/client/src/app/(domain)/features/live/LiveSection.tsx +++ b/client/src/app/(domain)/features/live/LiveSection.tsx @@ -104,7 +104,7 @@ const LiveSection = () => { )} - {isLivePage && ( + {/* {isLivePage && ( {({ chatList, isSocketConnected, socketRef, chatname, sendChat }) => ( <> @@ -117,7 +117,7 @@ const LiveSection = () => { )} - )} + )} */} ); }; diff --git a/client/src/app/providers/LiveProvider.tsx b/client/src/app/providers/LiveProvider.tsx index 709d9e51..9c604420 100644 --- a/client/src/app/providers/LiveProvider.tsx +++ b/client/src/app/providers/LiveProvider.tsx @@ -34,6 +34,7 @@ const LiveProvider = ({ children }: PropsWithChildren) => { const [liveInfo, setLiveInfo] = useState(defaultLiveInfo); const { id } = useParams() as { id: string }; + const [savedId, setSavedId] = useState(null); const [isError, setIsError] = useState(false); const [isLoading, setIsLoading] = useState(true); @@ -75,12 +76,15 @@ const LiveProvider = ({ children }: PropsWithChildren) => { const fetchLive = async (id: string) => { try { if (!id) return; + if (id === savedId) return; + setIsLoading(true); const fetchedPlaylist = await getPlaylist(id); setLiveUrl(fetchedPlaylist.playlistUrl); setLiveInfo(fetchedPlaylist.broadcastData); + setSavedId(id); setIsLoading(false); } catch (err) { setIsError(true); @@ -93,7 +97,7 @@ const LiveProvider = ({ children }: PropsWithChildren) => { // id로 스트리밍 중인 방송이 있는지 확인 fetchLive(id); } - }, [id, isLivePage]); + }, [id, isLivePage, savedId]); return ( Date: Mon, 25 Nov 2024 23:35:24 +0900 Subject: [PATCH 035/129] =?UTF-8?q?feat:=20LiveInfo=EC=97=90=EC=84=9C=20SS?= =?UTF-8?q?E=20=ED=86=B5=EC=8B=A0=20-=20LiveInfo=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=20SSE=20=ED=86=B5=EC=8B=A0=20?= =?UTF-8?q?=EC=9D=B4=ED=8C=A9=ED=8A=B8=20=EC=B6=94=EA=B0=80=20#282?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(domain)/features/live/LiveInfo.tsx | 37 ++++++++++++++++++- client/src/app/providers/LiveProvider.tsx | 6 +++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/client/src/app/(domain)/features/live/LiveInfo.tsx b/client/src/app/(domain)/features/live/LiveInfo.tsx index ac186cd1..cf4ed335 100644 --- a/client/src/app/(domain)/features/live/LiveInfo.tsx +++ b/client/src/app/(domain)/features/live/LiveInfo.tsx @@ -4,13 +4,46 @@ import HeartSvg from '@components/svgs/HeartSvg'; import useLiveContext from '@hooks/useLiveContext'; import clsx from 'clsx'; import Image from 'next/image'; -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import Badge from '@app/(domain)/features/Badge'; import { comma } from '@libs/formats'; const LiveInfo = () => { const [isHovered, setIsHovered] = useState(false); - const { liveInfo } = useLiveContext(); + const { liveInfo, refreshLiveInfo, broadcastId } = useLiveContext(); + const eventSourceRef = useRef(null); + + useEffect(() => { + // sse 통신을 이용해 지속적으로 라이브 정보를 받아서 refresh하도록 하기 + const fetchLiveInfo = () => { + if (process.env.NODE_ENV !== 'production') return; + + const eventSource = new EventSource(`/api/live/sse/${broadcastId}`); + eventSourceRef.current = eventSource; + + eventSource.onopen = () => { + console.log('✅ EVENT SOURCE OPENED'); + }; + + eventSource.onmessage = (event) => { + const data = JSON.parse(event.data); + console.log('🚀 SSE DATA HAVE BEEN SERVED', data); + refreshLiveInfo(data); + }; + + eventSource.onerror = () => { + eventSource.close(); + fetchLiveInfo(); + }; + }; + + fetchLiveInfo(); + + return () => { + eventSourceRef.current?.close(); + }; + }, [broadcastId]); + return (

{liveInfo.title}

diff --git a/client/src/app/providers/LiveProvider.tsx b/client/src/app/providers/LiveProvider.tsx index 9c604420..315f35fb 100644 --- a/client/src/app/providers/LiveProvider.tsx +++ b/client/src/app/providers/LiveProvider.tsx @@ -11,6 +11,7 @@ type LiveContextType = { liveUrl: Playlist['playlistUrl'] | null; broadcastId: string; clear: () => void; + refreshLiveInfo: (updatedBroadcast: Broadcast) => void; }; const defaultLiveInfo: Broadcast = { @@ -46,6 +47,10 @@ const LiveProvider = ({ children }: PropsWithChildren) => { setLiveUrl(null); }; + const refreshLiveInfo = (updatedBroadcast: Broadcast) => { + setLiveInfo(updatedBroadcast); + }; + /* isLivePage <- 이거 확인할 수 있어야 함. [id] -> 뭔가 패칭? id 값으로 streaming중인지 아닌지? <- 이때 방송 정보들도 불러와야 하나...? @@ -107,6 +112,7 @@ const LiveProvider = ({ children }: PropsWithChildren) => { liveUrl, broadcastId: id, clear, + refreshLiveInfo, }} > {isError ?

에러 아님

: isLivePage && isLoading ?

방송을 불러오는 중

: children} From dc74c1b0c3fc933983441cc544ab7e8323316668 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Tue, 26 Nov 2024 00:11:51 +0900 Subject: [PATCH 036/129] =?UTF-8?q?refactor:=20LiveInfo=20=EA=B0=81=20?= =?UTF-8?q?=EC=9A=94=EC=86=8C=20=EB=A9=94=EB=AA=A8=EC=9D=B4=EC=A0=9C?= =?UTF-8?q?=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/(domain)/features/live/Live.tsx | 4 +- .../app/(domain)/features/live/LiveInfo.tsx | 103 +++++++++++++++++- .../(domain)/features/live/LiveSection.tsx | 28 ++++- 3 files changed, 126 insertions(+), 9 deletions(-) diff --git a/client/src/app/(domain)/features/live/Live.tsx b/client/src/app/(domain)/features/live/Live.tsx index 9329a466..9d34f9eb 100644 --- a/client/src/app/(domain)/features/live/Live.tsx +++ b/client/src/app/(domain)/features/live/Live.tsx @@ -12,7 +12,7 @@ import { useState, } from 'react'; import clsx from 'clsx'; -import LiveInfo from './LiveInfo'; +// import LiveInfo from './LiveInfo'; import useFullscreen from '@hooks/useFullscreen'; import usePip from '@hooks/usePip'; import useMouseMovementOnElement from '@hooks/useMouseMovementOnElement'; @@ -174,7 +174,7 @@ const Live = Object.assign(LiveController, { Wrapper: LiveWrapper, VideoWrapper, Video, - Info: LiveInfo, + // Info: LiveInfo, }); export default Live; diff --git a/client/src/app/(domain)/features/live/LiveInfo.tsx b/client/src/app/(domain)/features/live/LiveInfo.tsx index cf4ed335..070a0c0e 100644 --- a/client/src/app/(domain)/features/live/LiveInfo.tsx +++ b/client/src/app/(domain)/features/live/LiveInfo.tsx @@ -4,12 +4,17 @@ import HeartSvg from '@components/svgs/HeartSvg'; import useLiveContext from '@hooks/useLiveContext'; import clsx from 'clsx'; import Image from 'next/image'; -import { useEffect, useRef, useState } from 'react'; +import { memo, type PropsWithChildren, type ReactNode, useEffect, useRef, useState } from 'react'; import Badge from '@app/(domain)/features/Badge'; import { comma } from '@libs/formats'; +import type { Broadcast } from '@libs/internalTypes'; -const LiveInfo = () => { - const [isHovered, setIsHovered] = useState(false); +type Props = { + children: (args: { liveInfo: Broadcast }) => ReactNode; +}; + +const LiveInfoWrapper = ({ children }: Props) => { + // const [isHovered, setIsHovered] = useState(false); const { liveInfo, refreshLiveInfo, broadcastId } = useLiveContext(); const eventSourceRef = useRef(null); @@ -46,7 +51,8 @@ const LiveInfo = () => { return (
-

{liveInfo.title}

+ {children({ liveInfo })} + {/*

{liveInfo.title}

{ 팔로우
+
*/} +
+ ); +}; + +const LiveInfoTitle = memo(({ title }: { title: string }) => { + return

{title}

; +}); + +const LiveInfoDetailWrapper = ({ children }: PropsWithChildren) => { + return
{children}
; +}; + +const LiveInfoProfileImage = memo(({ profileImageUrl, userName }: { profileImageUrl: string; userName: string }) => { + const [isHovered, setIsHovered] = useState(false); + return ( +
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}> +
+
+ {`스트리머 +
+
); +}); + +const LiveInfoDescription = ({ children }: PropsWithChildren) => { + return
{children}
; }; +const LiveInfoUserName = memo(({ userName }: { userName: string }) => { + return ( +

+ {userName} +

+ ); +}); + +const LiveInfoTags = memo( + ({ contentCategory, moodCategory, tags }: { contentCategory: string; moodCategory: string; tags: string[] }) => { + const allTags = [contentCategory, moodCategory, ...tags]; + + return ( +
    + {allTags.map((tag, idx) => ( +
  • + {tag} +
  • + ))} +
+ ); + }, +); + +const LiveInfoViewerCount = memo(({ viewerCount }: { viewerCount: number }) => { + return {comma(viewerCount)}명 시청 중; +}); + +const LiveInfoFollowButton = () => { + return ( +
+ +
+ ); +}; + +const LiveInfo = Object.assign(LiveInfoWrapper, { + Title: LiveInfoTitle, + Wrapper: LiveInfoDetailWrapper, + ProfileImage: LiveInfoProfileImage, + Description: LiveInfoDescription, + UserName: LiveInfoUserName, + Tags: LiveInfoTags, + ViewerCount: LiveInfoViewerCount, + FollowButton: LiveInfoFollowButton, +}); + export default LiveInfo; diff --git a/client/src/app/(domain)/features/live/LiveSection.tsx b/client/src/app/(domain)/features/live/LiveSection.tsx index 1be07ef3..66ccdad6 100644 --- a/client/src/app/(domain)/features/live/LiveSection.tsx +++ b/client/src/app/(domain)/features/live/LiveSection.tsx @@ -8,6 +8,7 @@ import { type PropsWithChildren } from 'react'; import VideoController from './VideoController'; import MiniPlayerController from './MiniPlayerController'; import Chat from './Chat'; +import LiveInfo from './LiveInfo'; const LiveSection = () => { const { isLivePage, liveUrl } = useLiveContext(); @@ -100,11 +101,32 @@ const LiveSection = () => { - {isLivePage && } + {isLivePage && ( + + {({ liveInfo }) => ( + <> + + + + + + + + + + + + )} + + )} )} - {/* {isLivePage && ( + {isLivePage && ( {({ chatList, isSocketConnected, socketRef, chatname, sendChat }) => ( <> @@ -117,7 +139,7 @@ const LiveSection = () => { )} - )} */} + )} ); }; From 704869feae0ae202124e0fdd88331586ddffd112 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Tue, 26 Nov 2024 00:15:01 +0900 Subject: [PATCH 037/129] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=86=8C=EC=BC=93=20=ED=86=B5?= =?UTF-8?q?=EC=8B=A0=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=EC=97=90=20=EC=BB=AC=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/app/(domain)/features/live/Chat.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/client/src/app/(domain)/features/live/Chat.tsx b/client/src/app/(domain)/features/live/Chat.tsx index 4fcc3c0f..4b811895 100644 --- a/client/src/app/(domain)/features/live/Chat.tsx +++ b/client/src/app/(domain)/features/live/Chat.tsx @@ -11,6 +11,7 @@ import { io, type Socket } from 'socket.io-client'; type ChatType = { name: string; content: string; + color?: string; }; type SendChat = (args: { socketRef: MutableRefObject; name: string; content: string }) => void; @@ -62,7 +63,7 @@ const ChatWrapper = ({ children }: Props) => { setIsSocketConnected(true); }); - socket.on(SOCKET_EVENT.CHAT, (receivedData: { name: string; content: string }) => { + socket.on(SOCKET_EVENT.CHAT, (receivedData: ChatType) => { // 상태 업데이트 console.log('😇 RECEIVING : ', receivedData); setChatList((prev) => [...prev, receivedData]); @@ -131,7 +132,13 @@ const ChatList = ({ chatList }: ChatListProps) => {
{chatList.map((chat, index) => (

- {chat.name} + + {chat.name} + {chat.content}

))} From 396f00cc0c0adfb1729cccac73e7e2f0a76ee6a0 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Tue, 26 Nov 2024 00:18:52 +0900 Subject: [PATCH 038/129] =?UTF-8?q?chore:=20production=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=BD=98=EC=86=94=20=EB=A1=9C=EA=B7=B8=20=EB=AF=B8=EB=85=B8?= =?UTF-8?q?=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/next.config.mjs | 3 + .../features/ContentsCategoryImage.tsx | 2 +- .../features/MoodsCategoryPalette.tsx | 2 +- .../src/app/(domain)/features/live/Live.tsx | 1 - .../app/(domain)/features/live/LiveInfo.tsx | 63 +------------------ 5 files changed, 6 insertions(+), 65 deletions(-) diff --git a/client/next.config.mjs b/client/next.config.mjs index df5c0fc6..d27fa677 100644 --- a/client/next.config.mjs +++ b/client/next.config.mjs @@ -2,6 +2,9 @@ const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL || '/api'; /** @type {import('next').NextConfig} */ const nextConfig = { + compiler: { + removeConsole: process.env.NODE_ENV === 'production', + }, rewrites: async () => { return [ { diff --git a/client/src/app/(domain)/categories/features/ContentsCategoryImage.tsx b/client/src/app/(domain)/categories/features/ContentsCategoryImage.tsx index c8ad780f..89bd03f9 100644 --- a/client/src/app/(domain)/categories/features/ContentsCategoryImage.tsx +++ b/client/src/app/(domain)/categories/features/ContentsCategoryImage.tsx @@ -1,5 +1,5 @@ import { CONTENTS_CATEGORY } from '@libs/constants'; -import { ContentsCategoryKey } from '@libs/internalTypes'; +import type { ContentsCategoryKey } from '@libs/internalTypes'; import Image from 'next/image'; import talkImage from '@assets/categories/communication.svg'; import gameImage from '@assets/categories/game.svg'; diff --git a/client/src/app/(domain)/categories/features/MoodsCategoryPalette.tsx b/client/src/app/(domain)/categories/features/MoodsCategoryPalette.tsx index 7520a5a1..5f6595c5 100644 --- a/client/src/app/(domain)/categories/features/MoodsCategoryPalette.tsx +++ b/client/src/app/(domain)/categories/features/MoodsCategoryPalette.tsx @@ -1,5 +1,5 @@ import { MOODS_CATEGORY } from '@libs/constants'; -import { MoodsCategoryKey } from '@libs/internalTypes'; +import type { MoodsCategoryKey } from '@libs/internalTypes'; import clsx from 'clsx'; type Props = { diff --git a/client/src/app/(domain)/features/live/Live.tsx b/client/src/app/(domain)/features/live/Live.tsx index 9d34f9eb..26b425cc 100644 --- a/client/src/app/(domain)/features/live/Live.tsx +++ b/client/src/app/(domain)/features/live/Live.tsx @@ -12,7 +12,6 @@ import { useState, } from 'react'; import clsx from 'clsx'; -// import LiveInfo from './LiveInfo'; import useFullscreen from '@hooks/useFullscreen'; import usePip from '@hooks/usePip'; import useMouseMovementOnElement from '@hooks/useMouseMovementOnElement'; diff --git a/client/src/app/(domain)/features/live/LiveInfo.tsx b/client/src/app/(domain)/features/live/LiveInfo.tsx index 070a0c0e..36d516f3 100644 --- a/client/src/app/(domain)/features/live/LiveInfo.tsx +++ b/client/src/app/(domain)/features/live/LiveInfo.tsx @@ -14,7 +14,6 @@ type Props = { }; const LiveInfoWrapper = ({ children }: Props) => { - // const [isHovered, setIsHovered] = useState(false); const { liveInfo, refreshLiveInfo, broadcastId } = useLiveContext(); const eventSourceRef = useRef(null); @@ -49,67 +48,7 @@ const LiveInfoWrapper = ({ children }: Props) => { }; }, [broadcastId]); - return ( -
- {children({ liveInfo })} - {/*

{liveInfo.title}

-
-
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > -
-
- {`스트리머 -
-
-
-
-
-

- {liveInfo.userName} -

- {liveInfo.tags.length > 0 && ( -
    - {liveInfo.tags.map((tag, idx) => ( -
  • - {tag} -
  • - ))} -
- )} -

- {comma(liveInfo.viewerCount)}명 시청 중 -

-
-
- -
-
*/} -
- ); + return
{children({ liveInfo })}
; }; const LiveInfoTitle = memo(({ title }: { title: string }) => { From 22883cdc6cd203b6ace68f0c164bd9949c2b420f Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Tue, 26 Nov 2024 11:10:43 +0900 Subject: [PATCH 039/129] =?UTF-8?q?feat:=20=EC=8A=A4=ED=8A=9C=EB=94=94?= =?UTF-8?q?=EC=98=A4=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 카테고리, 무드 연결 - StudioCategoryCard UI --- .../components/studio/StudioCategoryCard.tsx | 22 +++++ .../app/components/studio/StudioDropdown.tsx | 98 +++++++++++++------ .../app/studio/my/features/MyStudioForm.tsx | 22 +++-- client/src/server/handlers.ts | 2 +- 4 files changed, 106 insertions(+), 38 deletions(-) create mode 100644 client/src/app/components/studio/StudioCategoryCard.tsx diff --git a/client/src/app/components/studio/StudioCategoryCard.tsx b/client/src/app/components/studio/StudioCategoryCard.tsx new file mode 100644 index 00000000..5658d135 --- /dev/null +++ b/client/src/app/components/studio/StudioCategoryCard.tsx @@ -0,0 +1,22 @@ +import ContentsCategoryImage from '@app/(domain)/categories/features/ContentsCategoryImage'; +import { ContentsCategoryKey } from '@libs/internalTypes'; + +type cardProps = { + code: ContentsCategoryKey; + title: string; +}; + +const StudioCategoryCard = ({ code, title }: cardProps) => { + return ( +
+
+ +
+
+ {title} +
+
+ ); +}; + +export default StudioCategoryCard; diff --git a/client/src/app/components/studio/StudioDropdown.tsx b/client/src/app/components/studio/StudioDropdown.tsx index 2d3774bb..734bc554 100644 --- a/client/src/app/components/studio/StudioDropdown.tsx +++ b/client/src/app/components/studio/StudioDropdown.tsx @@ -16,6 +16,11 @@ import { type ButtonHTMLAttributes, } from 'react'; +import { CONTENTS_CATEGORY, MOODS_CATEGORY } from '@libs/constants'; +import MoodsCategoryPalette from '@app/(domain)/categories/features/MoodsCategoryPalette'; +import { MoodsCategoryKey, ContentsCategoryKey } from '@libs/internalTypes'; +import StudioCategoryCard from './StudioCategoryCard'; + type ChildrenArgs = { inputRef: RefObject; inputValue: string; @@ -103,7 +108,7 @@ const DropdownList = ({ children }: PropsWithChildren) => { return (
    {children} @@ -129,29 +134,29 @@ const StudioDropdown = Object.assign(DropdownWrapper, { Item: DropdownItem, }); -const categories = [ - { - id: 'aaa', - name: '졸림', - }, - { - id: 'bbb', - name: '기쁨', - }, -]; - type CategoryTestProps = { setData: (data: string) => void; + componentType: 'category' | 'mood'; + placeHolder: string; }; -export const StudioDropdownRendererForTest = ({ setData }: CategoryTestProps) => { +const categories = Object.values(CONTENTS_CATEGORY); +const moods = Object.values(MOODS_CATEGORY); + +export const StudioDropdownRenderer = ({ setData, componentType, placeHolder }: CategoryTestProps) => { const [selectedKey, setSelectedKey] = useState(null); + const [selectedCode, setSelectedCode] = useState('talk'); + const [selectedMoodCode, setSelectedMoodCode] = useState('unknown'); const selectCategory = (key: string) => { setSelectedKey(key); setData(key); }; + useEffect(() => { + console.log(selectedKey); + }, [selectedKey]); + return ( {({ inputRef, inputValue, isFocused, handleChangeInput, handleFocusInput, blurDropdown }) => ( @@ -161,26 +166,63 @@ export const StudioDropdownRendererForTest = ({ setData }: CategoryTestProps) => value={inputValue} onChange={handleChangeInput} onFocus={handleFocusInput} - placeholder="플레이스 홀더" + placeholder={placeHolder} /> {isFocused && ( - {categories - .filter((c) => c.name.includes(inputValue)) - .map((c) => ( - { - selectCategory(c.id); - blurDropdown(); - }} - > - {c.name} - - ))} + {componentType === 'category' ? ( + <> + {categories + .filter((c) => c.NAME.includes(inputValue)) + .map((c, idx) => ( + { + selectCategory(c.NAME); + blurDropdown(); + setSelectedCode(c.CODE); + }} + > + {c.NAME} + + ))} + + ) : ( + <> + {moods + .filter((m) => m.NAME.includes(inputValue)) + .map((m, idx) => ( + { + selectCategory(m.NAME); + blurDropdown(); + setSelectedMoodCode(m.CODE); + }} + > + {m.NAME} + + ))} + + )} )} - {selectedKey &&
    {categories.find((c) => c.id === selectedKey)?.name}
    } + {selectedKey && ( +
    + {componentType === 'category' ? ( +
    + +
    + ) : ( +
    + {MoodsCategoryPalette({ code: selectedMoodCode })} +
    + {moods.find((m) => m.NAME === selectedKey)?.NAME} +
    +
    + )} +
    + )} )}
    diff --git a/client/src/app/studio/my/features/MyStudioForm.tsx b/client/src/app/studio/my/features/MyStudioForm.tsx index 232f14c2..686e6bd8 100644 --- a/client/src/app/studio/my/features/MyStudioForm.tsx +++ b/client/src/app/studio/my/features/MyStudioForm.tsx @@ -6,7 +6,7 @@ import StudioUpdateButton from '@components/studio/StudioUpdateButton'; import StudioRows from './StudioRows'; import { TextareaRendererForTest } from '@components/studio/StudioTextarea'; -import { StudioDropdownRendererForTest } from '@components/studio/StudioDropdown'; +import { StudioDropdownRenderer } from '@components/studio/StudioDropdown'; import StudioImageInput from '@components/studio/StudioImageInput'; import StudioInput from '@components/studio/StudioInput'; import StudioBadge from '@components/studio/StudioBadge'; @@ -72,24 +72,30 @@ const MyStudioForm = ({ onSubmit }: MyStudioFormProps) => { return (
    -
    +
    setFormData((prev) => ({ ...prev, title: text }))} /> - setFormData((prev) => ({ ...prev, contentCategory: category }))} /> - setFormData((prev) => ({ ...prev, moodCategory: mood }))} /> + setFormData((prev) => ({ ...prev, moodCategory: mood }))} + />
    setTagInput(e.target.value)} - placeholder="태그를 입력하세요" + placeholder="태그 입력 후 Enter 혹은 추가" />
    { {tags.length > 0 && tags.map((tag, index) => ( -
    - handleDeleteTag(index)}> - {tag} - +
    + handleDeleteTag(index)}>{tag}
    ))} diff --git a/client/src/server/handlers.ts b/client/src/server/handlers.ts index 4d81e974..8a5d7ddc 100644 --- a/client/src/server/handlers.ts +++ b/client/src/server/handlers.ts @@ -62,5 +62,5 @@ export const handlers = [ http.get('/api/members/mydata', getMydata), http.post('/api/auth/github/callback', authenticate), http.post('/api/login', login), - http.post('/api/live/update', update), + http.patch('/api/live/update', update), ]; From 00612a66e9ecd23e8b208b78cfddbb098cad296e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=B8=EB=B9=88?= Date: Tue, 26 Nov 2024 11:22:44 +0900 Subject: [PATCH 040/129] =?UTF-8?q?fix:=20=ED=8C=94=EB=A1=9C=EC=9B=8C=20?= =?UTF-8?q?=EB=A7=A4=ED=95=91=20key=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 상수화 과정 중 팔로워 리스트를 추출하는 키에 S를 추가하여 FOLLOWER가 아닌 FOLLOWERS로 등록했었다. - FOLLOWERS -> FOLLOWER로 변경하여 정상적으로 추출하도록 변경하였다. --- applicationServer/src/constants.ts | 1 + applicationServer/src/follow/follow.controller.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/applicationServer/src/constants.ts b/applicationServer/src/constants.ts index e93eaf08..7b1447d0 100644 --- a/applicationServer/src/constants.ts +++ b/applicationServer/src/constants.ts @@ -28,5 +28,6 @@ export const CORS_ORIGIN_WWW = 'https://www.funch.site/'; // follow.provider.ts export const FOLLOW_REPOSITORY = 'FOLLOW_REPOSITORY'; // follow.controller.ts +export const FOLLOWER = 'follower'; export const FOLLOWERS = 'followers'; export const FOLLOWING = 'following'; diff --git a/applicationServer/src/follow/follow.controller.ts b/applicationServer/src/follow/follow.controller.ts index 6f34b28e..beccd072 100644 --- a/applicationServer/src/follow/follow.controller.ts +++ b/applicationServer/src/follow/follow.controller.ts @@ -13,7 +13,7 @@ import { } from '@nestjs/common'; import { NeedLoginGuard } from '@src/auth/core/auth.guard'; import { FollowService } from '@follow/follow.service'; -import { FOLLOWERS, FOLLOWING } from '@src/constants'; +import { FOLLOWER, FOLLOWERS, FOLLOWING } from '@src/constants'; @Controller('follow') @UseGuards(NeedLoginGuard) @@ -47,15 +47,15 @@ export class FollowController { const results = await this.followService.findAllFollowWithCondition(condition); const data = results.map((result) => result[key]); - return { [key]: data }; + return { [search]: data }; } private getFollowConditions(search: string, memberId: string) { if (search === FOLLOWERS) { - return { condition: { following: memberId }, key: FOLLOWERS }; + return { condition: { following: memberId }, key: FOLLOWER }; } else if (search === FOLLOWING) { return { condition: { follower: memberId }, key: FOLLOWING }; } - throw new HttpException('올바른 팔로워/팔로잉 정보 요청이 아닙니다.', HttpStatus.BAD_REQUEST); + throw new HttpException('올바른 팔로워/팔로잉 리스트 요청이 아닙니다.', HttpStatus.BAD_REQUEST); } } From 512db0d1c1599c9136e34483d09cacffc10f7e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=B8=EB=B9=88?= Date: Tue, 26 Nov 2024 11:24:04 +0900 Subject: [PATCH 041/129] =?UTF-8?q?test:=20follow=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 특정 유저의 팔로워/팔로잉 리스트 요청에 대해서 테스트 코드를 작성하였다. --- applicationServer/package.json | 3 +- applicationServer/test/app.e2e-spec.ts | 5 +- .../test/follow/follow.controller.spec.ts | 91 +++++++++++++++++++ 3 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 applicationServer/test/follow/follow.controller.spec.ts diff --git a/applicationServer/package.json b/applicationServer/package.json index 3e471ea7..69a24a9f 100644 --- a/applicationServer/package.json +++ b/applicationServer/package.json @@ -71,7 +71,8 @@ "^@github/(.*)$": "/src/auth/github/$1", "^@naver/(.*)$": "/src/auth/naver/$1", "^@cookie/(.*)$": "/src/auth/cookie/$1", - "^@authUtil/(.*)$": "/src/auth/util/$1" + "^@authUtil/(.*)$": "/src/auth/util/$1", + "^@follow/(.*)$": "/src/follow/$1" } } } diff --git a/applicationServer/test/app.e2e-spec.ts b/applicationServer/test/app.e2e-spec.ts index 0012dcd2..3ea33d9f 100644 --- a/applicationServer/test/app.e2e-spec.ts +++ b/applicationServer/test/app.e2e-spec.ts @@ -16,9 +16,6 @@ describe('AppController (e2e)', () => { }); it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); + return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); }); }); diff --git a/applicationServer/test/follow/follow.controller.spec.ts b/applicationServer/test/follow/follow.controller.spec.ts new file mode 100644 index 00000000..1d18d1ef --- /dev/null +++ b/applicationServer/test/follow/follow.controller.spec.ts @@ -0,0 +1,91 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FollowController } from '@follow/follow.controller'; +import { FollowService } from '@follow/follow.service'; +import { NeedLoginGuard } from '@src/auth/core/auth.guard'; +import { HttpException } from '@nestjs/common'; + +describe('FollowController 테스트', () => { + let followController: FollowController; + let followService: FollowService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [FollowController], + providers: [ + { + provide: FollowService, + useValue: { + findOneFollowWithCondition: jest.fn(), + findAllFollowWithCondition: jest.fn(), + followMember: jest.fn(), + unfollowMember: jest.fn(), + }, + }, + ], + }) + .overrideGuard(NeedLoginGuard) + .useValue({ canActivate: () => true }) + .compile(); + + followController = module.get(FollowController); + followService = module.get(FollowService); + }); + + it('이미 팔로우 중인 상태라면 팔로우할 수 없다.', async () => { + jest.spyOn(followService, 'findOneFollowWithCondition').mockResolvedValueOnce({ + id: 'id', + follower: 'user1', + following: 'user2', + createdAt: new Date(), + }); + + await expect(followController.followMember('user1', 'user2')).rejects.toThrow( + new HttpException('이미 팔로우 중입니다.', 400), + ); + }); + + it('본인을 팔로우할 수 없다.', async () => { + await expect(followController.followMember('user1', 'user1')).rejects.toThrow( + new HttpException('본인을 팔로우할 수 없습니다.', 400), + ); + }); + + it('팔로우 요청을 정상적으로 처리 할 수 있다.', async () => { + jest.spyOn(followService, 'findOneFollowWithCondition').mockResolvedValueOnce(undefined); + jest.spyOn(followService, 'followMember').mockResolvedValueOnce(undefined); + + await expect(followController.followMember('user1', 'user2')).resolves; + }); + + it('언팔로우 요청을 정상적으로 처리 할 수 있다.', async () => { + jest.spyOn(followService, 'unfollowMember').mockResolvedValueOnce({ affected: 1, raw: {} }); + + await expect(followController.unfollowMember('user1', 'user2')).resolves; + }); + + it('특정 유저의 팔로워 리스트를 정상적으로 반환한다.', async () => { + jest + .spyOn(followService, 'findAllFollowWithCondition') + .mockResolvedValueOnce([{ id: 'id', follower: 'user1', following: 'user2', createdAt: new Date() }]); + + const result = await followController.getFollows('user2', 'followers'); + + expect(result).toEqual({ followers: ['user1'] }); + }); + + it('특정 유저의 팔로잉 리스트를 정상적으로 반환한다.', async () => { + jest + .spyOn(followService, 'findAllFollowWithCondition') + .mockResolvedValueOnce([{ id: 'id', follower: 'user1', following: 'user2', createdAt: new Date() }]); + + const result = await followController.getFollows('user1', 'following'); + + expect(result).toEqual({ following: ['user2'] }); + }); + + it('특정 유저의 팔로워/팔로잉 리스트 요청이 아닌 요청에 대해서는 예외를 발생시킨다.', async () => { + await expect(followController.getFollows('user1', 'invalid')).rejects.toThrow( + new HttpException('올바른 팔로워/팔로잉 리스트 요청이 아닙니다.', 400), + ); + }); +}); From f5f3f2c8fa776eaff15b32561fbf5bd4c29e49b8 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Tue, 26 Nov 2024 13:08:07 +0900 Subject: [PATCH 042/129] =?UTF-8?q?feat:=20=ED=8C=94=EB=A1=9C=EC=9A=B0-?= =?UTF-8?q?=EB=A9=A4=EB=B2=84=20=EC=A1=B0=EC=9D=B8=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 멤버 데이터를 팔로잉 기준으로 가져올 수 있도록 테이블을 조인해 반환하는 쿼리 함수를 작성하였습니다. --- applicationServer/src/member/member.service.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/applicationServer/src/member/member.service.ts b/applicationServer/src/member/member.service.ts index cf874e0c..0366fc5b 100644 --- a/applicationServer/src/member/member.service.ts +++ b/applicationServer/src/member/member.service.ts @@ -16,6 +16,14 @@ class MemberService { return this.memberRepository.find(); } + async findMembersWithFollowTable(id) { + return this.memberRepository + .createQueryBuilder('member') + .innerJoin('follow', 'f', 'f.following = member.id') + .where('f.follower = :id', { id }) + .getMany(); + } + async findOneMemberWithCondition(condition: { [key: string]: string }) { return this.memberRepository.findOne({ where: condition }); } From 17a7ac906f7f35d5a59d334a4405141f086ae358 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Tue, 26 Nov 2024 13:09:52 +0900 Subject: [PATCH 043/129] =?UTF-8?q?style:=20User=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User 타입을 클라이언트에서 일관성있게 사용할 수 있도록 기존 MyData를 반환하는 형태로 속성 이름을 변경하였고, 팔로잉 방송 목록을 반환할 때 온라인은 방송 정보를, 오프라인은 스트리머 정보를 반환하기 때문에 isLive 속성은 제거하였습니다. --- applicationServer/src/types.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/applicationServer/src/types.ts b/applicationServer/src/types.ts index 92be90f0..7c20b092 100644 --- a/applicationServer/src/types.ts +++ b/applicationServer/src/types.ts @@ -18,10 +18,9 @@ type Token = { type User = { name: string; - profileImageUrl: string; - broadcastId: string; - followerCount: number; - isLive: boolean; + profile_image: string; + broadcast_id: string; + follower_count: number; }; export { Broadcast, Token, User }; From c0267a520c054fb7cfbaf79cc0a52df31c70f777 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Tue, 26 Nov 2024 13:11:10 +0900 Subject: [PATCH 044/129] =?UTF-8?q?feat:=20=ED=8C=94=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=AA=A9=EB=A1=9D=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 팔로잉된 스트리머를 기준으로 방송 중이라면 방송 정보를, 아니라면 스트리머 정보를 반환하는 API를 구현하였습니다. --- applicationServer/src/live/live.controller.ts | 9 +++++++ applicationServer/src/live/live.service.ts | 27 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/applicationServer/src/live/live.controller.ts b/applicationServer/src/live/live.controller.ts index 3fdf2645..39fa77e4 100644 --- a/applicationServer/src/live/live.controller.ts +++ b/applicationServer/src/live/live.controller.ts @@ -25,6 +25,15 @@ export class LiveController { return this.liveService.filterWithCategory(liveList, query); } + @UseGuards(NeedLoginGuard) + @Get('/follow') + getFollowLiveList(@Req() req: Request) { + const accessToken = req.headers['authorization']?.split(' ')[1]; + const decodedPayload = this.authService.verifyToken(accessToken); + + return this.liveService.filterWithFollow(decodedPayload.memberId); + } + @Get(':broadcastId') getPlaylistUrl(@Param('broadcastId') broadcastId: string) { return this.liveService.responseLiveData(broadcastId); diff --git a/applicationServer/src/live/live.service.ts b/applicationServer/src/live/live.service.ts index 172d7f3b..7a45afaa 100644 --- a/applicationServer/src/live/live.service.ts +++ b/applicationServer/src/live/live.service.ts @@ -1,6 +1,6 @@ import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { Live } from '@live/entities/live.entity'; -import { Broadcast } from '@src/types'; +import { Broadcast, User } from '@src/types'; import { MemberService } from '@src/member/member.service'; import { Member } from '@src/member/member.entity'; import { interval, map } from 'rxjs'; @@ -119,4 +119,29 @@ export class LiveService { ); }); } + + async filterWithFollow(memberId) { + const followList = await this.memberService.findMembersWithFollowTable(memberId); + const onAir = []; + const offAir = []; + followList.forEach((member) => { + if (this.live.data.has(member.broadcast_id)) { + onAir.push(this.live.data.get(member.broadcast_id)); + } else { + const { name, profile_image, broadcast_id, follower_count } = member; + + offAir.push({ + name, + profile_image, + broadcast_id, + follower_count, + } as User); + } + }); + + return { + onAir, + offAir, + }; + } } From 64e87f261da456015fad3e712f1e9975c1dfb832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=B8=EB=B9=88?= Date: Tue, 26 Nov 2024 10:22:23 +0900 Subject: [PATCH 045/129] =?UTF-8?q?feat:=20=EA=B5=AC=EA=B8=80=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=AA=A8=EB=93=88=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 구글 로그인 구현을 위해 path alias를 설정하였다. - 구글 로그인 구현을 위해 Controller, Service를 생성하여 Module에 등록하였다. --- .../src/auth/google/google.controller.ts | 20 +++++++++++++++++++ .../src/auth/google/google.module.ts | 14 +++++++++++++ .../src/auth/google/google.service.ts | 9 +++++++++ applicationServer/tsconfig.json | 3 ++- 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 applicationServer/src/auth/google/google.controller.ts create mode 100644 applicationServer/src/auth/google/google.module.ts create mode 100644 applicationServer/src/auth/google/google.service.ts diff --git a/applicationServer/src/auth/google/google.controller.ts b/applicationServer/src/auth/google/google.controller.ts new file mode 100644 index 00000000..9a16ce9d --- /dev/null +++ b/applicationServer/src/auth/google/google.controller.ts @@ -0,0 +1,20 @@ +import { Controller, Post, Body, Res, UseGuards } from '@nestjs/common'; +import { Response } from 'express'; +import { MemberService } from '@src/member/member.service'; +import { GoogleAuthService } from '@google/google.service'; +import { AuthService } from '@auth/auth.service'; +import { CookieService } from '@cookie/cookie.service'; +import { NoNeedLoginGuard } from '@auth/auth.guard'; +import { REFRESH_TOKEN } from '@src/constants'; + +@Controller('auth/google') +class GoogleAuthController { + constructor( + private readonly googleAuthService: GoogleAuthService, + private readonly memberService: MemberService, + private readonly authService: AuthService, + private readonly cookieService: CookieService, + ) {} +} + +export { GoogleAuthController }; diff --git a/applicationServer/src/auth/google/google.module.ts b/applicationServer/src/auth/google/google.module.ts new file mode 100644 index 00000000..a0760fe0 --- /dev/null +++ b/applicationServer/src/auth/google/google.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { GoogleAuthController } from '@google/google.controller'; +import { GoogleAuthService } from '@google/google.service'; +import { DatabaseModule } from '@src/database/database.module'; +import { MemberModule } from '@src/member/member.module'; +import { AuthModule } from '@auth/auth.module'; +import { CookieModule } from '@cookie/cookie.module'; + +@Module({ + imports: [DatabaseModule, MemberModule, AuthModule, CookieModule], + controllers: [GoogleAuthController], + providers: [GoogleAuthService], +}) +export class GoogleAuthModule {} diff --git a/applicationServer/src/auth/google/google.service.ts b/applicationServer/src/auth/google/google.service.ts new file mode 100644 index 00000000..df9b8429 --- /dev/null +++ b/applicationServer/src/auth/google/google.service.ts @@ -0,0 +1,9 @@ +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import axios from 'axios'; + +@Injectable() +class GoogleAuthService { + constructor() {} +} + +export { GoogleAuthService }; diff --git a/applicationServer/tsconfig.json b/applicationServer/tsconfig.json index dc2cd04e..2da0b24f 100644 --- a/applicationServer/tsconfig.json +++ b/applicationServer/tsconfig.json @@ -21,7 +21,8 @@ "@database/*": ["src/database/*"], "@member/*": ["src/member/*"], "@live/*": ["src/live/*"], - "@follow/*": ["src/follow/*"] + "@follow/*": ["src/follow/*"], + "@google/*": ["src/auth/google/*"] }, "incremental": true, "skipLibCheck": true, From d3f6be56094e78c96e1a654fa81c25af94ead8dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=B8=EB=B9=88?= Date: Tue, 26 Nov 2024 16:45:23 +0900 Subject: [PATCH 046/129] =?UTF-8?q?feat:=20Google=20authorization=20code?= =?UTF-8?q?=EC=99=80=20access=20token=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Github 로그인과 유사한 Flow로 authorization code와 access token을 가져온다. - 미리 등록해놓은 redirect_uri를 반드시 명시한다. --- .../src/auth/google/google.service.ts | 39 +++++++++++++++++++ applicationServer/src/constants.ts | 4 ++ 2 files changed, 43 insertions(+) diff --git a/applicationServer/src/auth/google/google.service.ts b/applicationServer/src/auth/google/google.service.ts index df9b8429..3e235c02 100644 --- a/applicationServer/src/auth/google/google.service.ts +++ b/applicationServer/src/auth/google/google.service.ts @@ -1,9 +1,48 @@ import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import axios from 'axios'; +import { AUTHORIZATION_CODE, GOOGLE_ACCESS_TOKEN_URL, APPLICATION_JSON, GOOGLE_RESOURCE_URL } from '@src/constants'; @Injectable() class GoogleAuthService { constructor() {} + + async getAccessToken(code: string): Promise { + const request = { + code, + client_id: process.env.GOOGLE_CLIENT_ID, + client_secret: process.env.GOOGLE_CLIENT_SECRET, + redirect_uri: process.env.GOOGLE_REDIRECT_URL, + grant_type: AUTHORIZATION_CODE, + }; + + const response = await axios + .post(GOOGLE_ACCESS_TOKEN_URL, request, { + headers: { + accept: APPLICATION_JSON, + }, + }) + .catch(() => { + throw new HttpException('Google AccessToken 가져오기 실패', HttpStatus.UNAUTHORIZED); + }); + + if (response.data.error) { + throw new HttpException('Google AccessToken 가져오기 실패', HttpStatus.UNAUTHORIZED); + } + return response.data.access_token; + } + + async getUserInfo(accessToken: string) { + const { data } = await axios + .get(GOOGLE_RESOURCE_URL, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + .catch(() => { + throw new HttpException('Google 유저 정보 가져오기 실패', HttpStatus.FAILED_DEPENDENCY); + }); + return data; + } } export { GoogleAuthService }; diff --git a/applicationServer/src/constants.ts b/applicationServer/src/constants.ts index 7b1447d0..f79ce355 100644 --- a/applicationServer/src/constants.ts +++ b/applicationServer/src/constants.ts @@ -31,3 +31,7 @@ export const FOLLOW_REPOSITORY = 'FOLLOW_REPOSITORY'; export const FOLLOWER = 'follower'; export const FOLLOWERS = 'followers'; export const FOLLOWING = 'following'; +// google.service.ts +export const AUTHORIZATION_CODE = 'authorization_code'; +export const GOOGLE_ACCESS_TOKEN_URL = 'https://oauth2.googleapis.com/token'; +export const GOOGLE_RESOURCE_URL = 'https://www.googleapis.com/oauth2/v2/userinfo'; From 8ed55e81cce023dd9fcd542d61ded3e84993c43d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=B8=EB=B9=88?= Date: Tue, 26 Nov 2024 16:46:43 +0900 Subject: [PATCH 047/129] =?UTF-8?q?feat:=20Google=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Google 로그인 요청을 통해 사용자의 자원을 가져와서 회원가입/로그인 로직을 처리한다. --- .../src/auth/google/google.controller.ts | 16 ++++++++++++++++ applicationServer/src/member/member.service.ts | 9 +++++++++ 2 files changed, 25 insertions(+) diff --git a/applicationServer/src/auth/google/google.controller.ts b/applicationServer/src/auth/google/google.controller.ts index 9a16ce9d..67f47989 100644 --- a/applicationServer/src/auth/google/google.controller.ts +++ b/applicationServer/src/auth/google/google.controller.ts @@ -15,6 +15,22 @@ class GoogleAuthController { private readonly authService: AuthService, private readonly cookieService: CookieService, ) {} + + @Post('/callback') + @UseGuards(NoNeedLoginGuard) + async getAccessToken(@Body('code') code: string, @Res({ passthrough: true }) res: Response) { + const googleAccessToken = await this.googleAuthService.getAccessToken(decodeURIComponent(code)); + const { id, picture } = await this.googleAuthService.getUserInfo(googleAccessToken); + + const member = await this.memberService.findOrRegisterMember(`Google@${id}`, picture); + const accessToken = this.authService.generateAccessToken(member.id); + const refreshToken = this.authService.generateRefreshToken(member.id); + + this.authService.saveRefreshToken(member.id, refreshToken); + this.cookieService.setCookie(res, REFRESH_TOKEN, refreshToken); + + return { accessToken, name: member.name, profile_image: member.profile_image, broadcast_id: member.broadcast_id }; + } } export { GoogleAuthController }; diff --git a/applicationServer/src/member/member.service.ts b/applicationServer/src/member/member.service.ts index 0366fc5b..74673bac 100644 --- a/applicationServer/src/member/member.service.ts +++ b/applicationServer/src/member/member.service.ts @@ -28,6 +28,15 @@ class MemberService { return this.memberRepository.findOne({ where: condition }); } + async findOrRegisterMember(memberId: string, profile_url?: string) { + const member = await this.findOneMemberWithCondition({ id: memberId }); + if (!member) { + const newMember = await this.register(memberId, profile_url); + return newMember; + } + return member; + } + async register(id: string, image_url?: string): Promise { const name = this.generateUniqueName(); const user = { From 8927315af5cd5f5183c29e18550f641232d6f115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=B8=EB=B9=88?= Date: Tue, 26 Nov 2024 16:47:58 +0900 Subject: [PATCH 048/129] =?UTF-8?q?feat:=20Google=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EB=93=88=20Root=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Google 로그인 모듈을 Root 모듈인 app.module에 추가하였다. --- applicationServer/src/app.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/applicationServer/src/app.module.ts b/applicationServer/src/app.module.ts index be15412e..1652422c 100644 --- a/applicationServer/src/app.module.ts +++ b/applicationServer/src/app.module.ts @@ -7,11 +7,12 @@ import { GithubAuthModule } from '@github/github.module'; import { AuthModule } from '@auth/auth.module'; import { FollowModule } from '@follow/follow.module'; import { NaverAuthModule } from '@naver/naver.module'; +import { GoogleAuthModule } from '@google/google.module'; dotenv.config(); @Module({ - imports: [MemberModule, GithubAuthModule, AuthModule, LiveModule, FollowModule, NaverAuthModule], + imports: [MemberModule, GithubAuthModule, AuthModule, LiveModule, FollowModule, NaverAuthModule, GoogleAuthModule], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { From 754bc68503d15ef091cdfdc8d245795f1e6ab535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=B8=EB=B9=88?= Date: Tue, 26 Nov 2024 16:51:13 +0900 Subject: [PATCH 049/129] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85&=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=BD=94=EB=93=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Github, Naver, Google 로그인마다 존재했던 회원가입&로그인 로직을 memberService로 옮겨 중복 코드를 제거하였다. --- .../src/auth/github/github.controller.ts | 11 +---------- applicationServer/src/auth/naver/naver.controller.ts | 11 +---------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/applicationServer/src/auth/github/github.controller.ts b/applicationServer/src/auth/github/github.controller.ts index 346d32b1..347657b6 100644 --- a/applicationServer/src/auth/github/github.controller.ts +++ b/applicationServer/src/auth/github/github.controller.ts @@ -22,7 +22,7 @@ class GithubAuthController { const githubAccessToken = await this.githubAuthService.getAccessToken(code); const { id, avatar_url } = await this.githubAuthService.getUserInfo(githubAccessToken); - const member = await this.findOrRegisterMember(`Github@${id}`, avatar_url); + const member = await this.memberService.findOrRegisterMember(`Github@${id}`, avatar_url); const accessToken = this.authService.generateAccessToken(member.id); const refreshToken = this.authService.generateRefreshToken(member.id); @@ -31,15 +31,6 @@ class GithubAuthController { return { accessToken, name: member.name, profile_image: member.profile_image, broadcast_id: member.broadcast_id }; } - - private async findOrRegisterMember(memberId: string, avatar_url: string) { - const member = await this.memberService.findOneMemberWithCondition({ id: memberId }); - if (!member) { - const newMember = await this.memberService.register(memberId, avatar_url); - return newMember; - } - return member; - } } export { GithubAuthController }; diff --git a/applicationServer/src/auth/naver/naver.controller.ts b/applicationServer/src/auth/naver/naver.controller.ts index 0a70f048..6fb4695f 100644 --- a/applicationServer/src/auth/naver/naver.controller.ts +++ b/applicationServer/src/auth/naver/naver.controller.ts @@ -25,7 +25,7 @@ class NaverAuthController { ) { const naverAccessToken = await this.naverAuthService.getAccessToken(code, state); const { id } = await this.naverAuthService.getUserInfo(naverAccessToken); - const member = await this.findOrRegisterMember(`Naver@${id}`); + const member = await this.memberService.findOrRegisterMember(`Naver@${id}`); const accessToken = this.authService.generateAccessToken(member.id); const refreshToken = this.authService.generateRefreshToken(member.id); @@ -34,15 +34,6 @@ class NaverAuthController { return { accessToken, name: member.name, profile_image: member.profile_image, broadcast_id: member.broadcast_id }; } - - private async findOrRegisterMember(memberId: string) { - const member = await this.memberService.findOneMemberWithCondition({ id: memberId }); - if (!member) { - const newMember = await this.memberService.register(memberId); - return newMember; - } - return member; - } } export { NaverAuthController }; From dda01f6bbd7583049ab90de5f7c655c1b675e39d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=B8=EB=B9=88?= Date: Tue, 26 Nov 2024 18:35:46 +0900 Subject: [PATCH 050/129] =?UTF-8?q?feat:=20playlistUrl=EB=8F=84=20?= =?UTF-8?q?=EA=B0=99=EC=9D=B4=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - playlistUrl도 반환하도록 코드를 추가하였다. --- applicationServer/src/live/live.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applicationServer/src/live/live.service.ts b/applicationServer/src/live/live.service.ts index 7a45afaa..df6c292d 100644 --- a/applicationServer/src/live/live.service.ts +++ b/applicationServer/src/live/live.service.ts @@ -126,7 +126,7 @@ export class LiveService { const offAir = []; followList.forEach((member) => { if (this.live.data.has(member.broadcast_id)) { - onAir.push(this.live.data.get(member.broadcast_id)); + onAir.push(this.responseLiveData(member.broadcast_id)); } else { const { name, profile_image, broadcast_id, follower_count } = member; From 222e48b40e9385904e5b9e81a72d68e316dffb4e Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Tue, 26 Nov 2024 16:14:16 +0900 Subject: [PATCH 051/129] =?UTF-8?q?feat:=20=EC=BD=98=ED=85=90=EC=B8=A0=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20-=20=EC=BD=98=ED=85=90=EC=B8=A0?= =?UTF-8?q?=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80(dynamic=20routing,=20static/dynamic?= =?UTF-8?q?=20rendering,=20server=20component)=20#267=20-=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=20=EA=B7=B8=EB=A6=AC=EB=93=9C=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A5=BC=20=EC=9E=AC=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=EB=AA=A9=EB=A1=9D=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#269=20-=20=EB=A1=9C=EB=94=A9(Suspense)=20=EB=B0=8F?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=EB=B0=94=EC=9A=B4=EB=8D=94=EB=A6=AC(Ne?= =?UTF-8?q?xt=20error.tsx)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/__mocks__/broadcasts.ts | 117 ++++++++++++++++-- .../categories/contents/[code]/error.tsx | 17 +++ .../[code]/features/ContentCategory.tsx | 27 ++++ .../categories/contents/[code]/page.tsx | 63 +++++++++- .../(domain)/features/RecommendedLives.tsx | 6 +- 5 files changed, 213 insertions(+), 17 deletions(-) create mode 100644 client/src/app/(domain)/categories/contents/[code]/error.tsx create mode 100644 client/src/app/(domain)/categories/contents/[code]/features/ContentCategory.tsx diff --git a/client/src/__mocks__/broadcasts.ts b/client/src/__mocks__/broadcasts.ts index 45c44134..7b5b1986 100644 --- a/client/src/__mocks__/broadcasts.ts +++ b/client/src/__mocks__/broadcasts.ts @@ -1,10 +1,11 @@ +import { CONTENTS_CATEGORY } from '@libs/constants'; import type { Broadcast } from '@libs/internalTypes'; export const mockedBroadcasts: Broadcast[] = [ { broadcastId: 'aaa', title: '[충격] 트럼프 당선', - contentCategory: 'POLITICS', + contentCategory: CONTENTS_CATEGORY.politics.CODE, moodCategory: 'INTERESTING', tags: ['politics', 'election'], thumbnailUrl: 'https://via.placeholder.com/150', @@ -12,10 +13,21 @@ export const mockedBroadcasts: Broadcast[] = [ userName: '슈카월드', profileImageUrl: 'https://via.placeholder.com/150', }, + { + broadcastId: 'aaagggggg', + title: '[충격] 해리스 낙선', + contentCategory: CONTENTS_CATEGORY.politics.CODE, + moodCategory: 'INTERESTING', + tags: ['politics', 'election'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 1870, + userName: '슈카월드', + profileImageUrl: 'https://via.placeholder.com/150', + }, { broadcastId: 'bbb', title: '[데모 공유] 팀 무지개 치즈 3주차 발표', - contentCategory: 'TECH', + contentCategory: CONTENTS_CATEGORY.develop.CODE, tags: ['funch', 'boostcamp'], moodCategory: 'FUN', thumbnailUrl: 'https://via.placeholder.com/150', @@ -26,7 +38,7 @@ export const mockedBroadcasts: Broadcast[] = [ { broadcastId: 'ccc', title: '고양이 냥냥이 냥냥냥이', - contentCategory: 'ANIMAL', + contentCategory: CONTENTS_CATEGORY.dailylife.CODE, moodCategory: 'HAPPY', tags: ['cat', 'cute'], thumbnailUrl: 'https://via.placeholder.com/150', @@ -37,7 +49,7 @@ export const mockedBroadcasts: Broadcast[] = [ { broadcastId: 'ddd', title: '방송 제목', - contentCategory: 'CATEGORY', + contentCategory: CONTENTS_CATEGORY.cook.CODE, moodCategory: 'MOOD', tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', @@ -48,7 +60,7 @@ export const mockedBroadcasts: Broadcast[] = [ { broadcastId: 'eee', title: '방송 제목', - contentCategory: 'CATEGORY', + contentCategory: CONTENTS_CATEGORY.game.CODE, moodCategory: 'MOOD', tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', @@ -59,7 +71,7 @@ export const mockedBroadcasts: Broadcast[] = [ { broadcastId: 'fff', title: '방송 제목', - contentCategory: 'CATEGORY', + contentCategory: CONTENTS_CATEGORY.economy.CODE, moodCategory: 'MOOD', tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', @@ -70,7 +82,7 @@ export const mockedBroadcasts: Broadcast[] = [ { broadcastId: 'ggg', title: '방송 제목', - contentCategory: 'CATEGORY', + contentCategory: CONTENTS_CATEGORY.fishing.CODE, moodCategory: 'MOOD', tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', @@ -81,7 +93,7 @@ export const mockedBroadcasts: Broadcast[] = [ { broadcastId: 'hhh', title: '방송 제목', - contentCategory: 'CATEGORY', + contentCategory: CONTENTS_CATEGORY.horror.CODE, moodCategory: 'MOOD', tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', @@ -92,7 +104,7 @@ export const mockedBroadcasts: Broadcast[] = [ { broadcastId: 'iii', title: '방송 제목', - contentCategory: 'CATEGORY', + contentCategory: CONTENTS_CATEGORY.house.CODE, moodCategory: 'MOOD', tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', @@ -103,7 +115,18 @@ export const mockedBroadcasts: Broadcast[] = [ { broadcastId: 'jjj', title: '방송 제목', - contentCategory: 'CATEGORY', + contentCategory: CONTENTS_CATEGORY.music.CODE, + moodCategory: 'MOOD', + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 800, + userName: '물소', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'jjasdgj', + title: '방송 제목', + contentCategory: CONTENTS_CATEGORY.mukbang.CODE, moodCategory: 'MOOD', tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', @@ -114,7 +137,7 @@ export const mockedBroadcasts: Broadcast[] = [ { broadcastId: 'sss', title: '방송 제목', - contentCategory: 'CATEGORY', + contentCategory: CONTENTS_CATEGORY.news.CODE, moodCategory: 'MOOD', tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', @@ -125,7 +148,73 @@ export const mockedBroadcasts: Broadcast[] = [ { broadcastId: 'tadg', title: '방송 제목', - contentCategory: 'CATEGORY', + contentCategory: CONTENTS_CATEGORY.outdoor.CODE, + moodCategory: 'MOOD', + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 1000, + userName: '사자', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'tasdgdg', + title: '방송 제목sdg', + contentCategory: CONTENTS_CATEGORY.outdoor.CODE, + moodCategory: 'MOOD', + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 1000, + userName: '사자', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'tasdgs21245dg', + title: '방송 제목sdg', + contentCategory: CONTENTS_CATEGORY.study.CODE, + moodCategory: 'MOOD', + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 1000, + userName: '사자', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'tasdgs2125dg', + title: '방송 제목sdg', + contentCategory: CONTENTS_CATEGORY.talk.CODE, + moodCategory: 'MOOD', + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 1000, + userName: '사자', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'tdgs2125dg', + title: '방송 제목sdg', + contentCategory: CONTENTS_CATEGORY.travel.CODE, + moodCategory: 'MOOD', + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 1000, + userName: '사자', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'tbbbbbdgs2125dg', + title: '방송 제목sdg', + contentCategory: CONTENTS_CATEGORY.virtual.CODE, + moodCategory: 'MOOD', + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 1000, + userName: '사자', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'tdaaags2125dg', + title: '방송 제목sdg', + contentCategory: CONTENTS_CATEGORY.virtual.CODE, moodCategory: 'MOOD', tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', @@ -134,3 +223,7 @@ export const mockedBroadcasts: Broadcast[] = [ profileImageUrl: 'https://via.placeholder.com/150', }, ]; + +export const getBroadcastsByContentCategory = (contentCategory: string) => { + return mockedBroadcasts.filter((broadcast) => broadcast.contentCategory === contentCategory); +}; diff --git a/client/src/app/(domain)/categories/contents/[code]/error.tsx b/client/src/app/(domain)/categories/contents/[code]/error.tsx new file mode 100644 index 00000000..98748f2e --- /dev/null +++ b/client/src/app/(domain)/categories/contents/[code]/error.tsx @@ -0,0 +1,17 @@ +'use client'; // Error boundaries must be Client Components + +import Button from '@components/Button'; +import useInternalRouter from '@hooks/useInternalRouter'; + +export default function Error({ reset }: { error: Error & { digest?: string }; reset: () => void }) { + const { push } = useInternalRouter(); + return ( +
    +

    라이브 목록을 불러오는데 실패했어요.

    +
    + + +
    +
    + ); +} diff --git a/client/src/app/(domain)/categories/contents/[code]/features/ContentCategory.tsx b/client/src/app/(domain)/categories/contents/[code]/features/ContentCategory.tsx new file mode 100644 index 00000000..1defaf4e --- /dev/null +++ b/client/src/app/(domain)/categories/contents/[code]/features/ContentCategory.tsx @@ -0,0 +1,27 @@ +'use client'; + +import Lives from '@components/livesGrid/Lives'; +import type { Broadcast } from '@libs/internalTypes'; + +type Props = { + lives: Broadcast[]; +}; + +const ContentCategory = ({ lives }: Props) => { + return ( + + {({ visibleLives, isExpanded, toggle }) => ( + <> + + {visibleLives.map((live, index) => ( + + ))} + + {lives.length > 3 && } + + )} + + ); +}; + +export default ContentCategory; diff --git a/client/src/app/(domain)/categories/contents/[code]/page.tsx b/client/src/app/(domain)/categories/contents/[code]/page.tsx index ed774edc..2bc252e7 100644 --- a/client/src/app/(domain)/categories/contents/[code]/page.tsx +++ b/client/src/app/(domain)/categories/contents/[code]/page.tsx @@ -1,5 +1,64 @@ -const ContentCategoryPage = () => { - return
    ContentCategoryPage
    ; +import { Suspense } from 'react'; +import ContentCategory from './features/ContentCategory'; +import type { Broadcast, ContentsCategoryKey } from '@libs/internalTypes'; +import { getBroadcastsByContentCategory } from '@mocks/broadcasts'; +import clsx from 'clsx'; +import { CONTENTS_CATEGORY } from '@libs/constants'; +import { unstable_noStore as noStore } from 'next/cache'; + +const fetchData = async (code: string): Promise => { + if (process.env.NODE_ENV !== 'production') return getBroadcastsByContentCategory(code); + + const response = await fetch(`/api/live/category?content=${code}`, { + cache: 'no-cache', + }); + + if (!response.ok) { + throw new Error('라이브 목록을 불러오는데 실패했어요.'); + } + + const data = await response.json(); + + return data; +}; + +type Props = { + params: Promise<{ + code: ContentsCategoryKey; + }>; +}; + +const ContentCategoryPage = async ({ params }: Props) => { + noStore(); + + const code = (await params).code; + + const cateogryName = CONTENTS_CATEGORY[code].NAME; + + return ( +
    +
    +

    {cateogryName}

    +
    + 라이브 목록을 불러우는 중...

    }> + +
    +
    + ); +}; + +type FetcherProps = { + code: string; +}; + +const ContentCategoryFetcher = async ({ code }: FetcherProps) => { + const lives = await fetchData(code); + + if (lives.length === 0) { + return
    아직 방송 중인 스트리머가 없어요. 🥲
    ; + } + + return ; }; export default ContentCategoryPage; diff --git a/client/src/app/(domain)/features/RecommendedLives.tsx b/client/src/app/(domain)/features/RecommendedLives.tsx index b0ff770c..bbead2e7 100644 --- a/client/src/app/(domain)/features/RecommendedLives.tsx +++ b/client/src/app/(domain)/features/RecommendedLives.tsx @@ -1,6 +1,6 @@ 'use client'; -import DeemedLink from '@components/DeemedLink'; +// import DeemedLink from '@components/DeemedLink'; import clsx from 'clsx'; import Lives from '@components/livesGrid/Lives'; import { useEffect, useState } from 'react'; @@ -43,9 +43,9 @@ const RecommendedLives = () => { return (
    -
    +

    이 방송 어때요?

    - + {/* */}
    {({ visibleLives, isExpanded, toggle }) => ( From fbf1145d4a75aac04aace9b62d557627571bf918 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Tue, 26 Nov 2024 16:19:51 +0900 Subject: [PATCH 052/129] =?UTF-8?q?feat:=20=EB=AC=B4=EB=93=9C=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=9E=91=EC=97=85=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20getBroadcastsByMoodCategory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/__mocks__/broadcasts.ts | 46 ++++++++++++++++-------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/client/src/__mocks__/broadcasts.ts b/client/src/__mocks__/broadcasts.ts index 7b5b1986..070e5f73 100644 --- a/client/src/__mocks__/broadcasts.ts +++ b/client/src/__mocks__/broadcasts.ts @@ -1,4 +1,4 @@ -import { CONTENTS_CATEGORY } from '@libs/constants'; +import { CONTENTS_CATEGORY, MOODS_CATEGORY } from '@libs/constants'; import type { Broadcast } from '@libs/internalTypes'; export const mockedBroadcasts: Broadcast[] = [ @@ -6,7 +6,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'aaa', title: '[충격] 트럼프 당선', contentCategory: CONTENTS_CATEGORY.politics.CODE, - moodCategory: 'INTERESTING', + moodCategory: MOODS_CATEGORY.calm.CODE, tags: ['politics', 'election'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 10870, @@ -17,7 +17,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'aaagggggg', title: '[충격] 해리스 낙선', contentCategory: CONTENTS_CATEGORY.politics.CODE, - moodCategory: 'INTERESTING', + moodCategory: MOODS_CATEGORY.calm.CODE, tags: ['politics', 'election'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 1870, @@ -28,8 +28,8 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'bbb', title: '[데모 공유] 팀 무지개 치즈 3주차 발표', contentCategory: CONTENTS_CATEGORY.develop.CODE, + moodCategory: MOODS_CATEGORY.energetic.CODE, tags: ['funch', 'boostcamp'], - moodCategory: 'FUN', thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 100, userName: '짜왕', @@ -39,7 +39,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'ccc', title: '고양이 냥냥이 냥냥냥이', contentCategory: CONTENTS_CATEGORY.dailylife.CODE, - moodCategory: 'HAPPY', + moodCategory: MOODS_CATEGORY.energetic.CODE, tags: ['cat', 'cute'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 300, @@ -50,7 +50,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'ddd', title: '방송 제목', contentCategory: CONTENTS_CATEGORY.cook.CODE, - moodCategory: 'MOOD', + moodCategory: MOODS_CATEGORY.depressed.CODE, tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 200, @@ -61,7 +61,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'eee', title: '방송 제목', contentCategory: CONTENTS_CATEGORY.game.CODE, - moodCategory: 'MOOD', + moodCategory: MOODS_CATEGORY.depressed.CODE, tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 300, @@ -72,7 +72,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'fff', title: '방송 제목', contentCategory: CONTENTS_CATEGORY.economy.CODE, - moodCategory: 'MOOD', + moodCategory: MOODS_CATEGORY.funny.CODE, tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 400, @@ -83,7 +83,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'ggg', title: '방송 제목', contentCategory: CONTENTS_CATEGORY.fishing.CODE, - moodCategory: 'MOOD', + moodCategory: MOODS_CATEGORY.getking.CODE, tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 500, @@ -94,7 +94,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'hhh', title: '방송 제목', contentCategory: CONTENTS_CATEGORY.horror.CODE, - moodCategory: 'MOOD', + moodCategory: MOODS_CATEGORY.getking.CODE, tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 600, @@ -105,7 +105,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'iii', title: '방송 제목', contentCategory: CONTENTS_CATEGORY.house.CODE, - moodCategory: 'MOOD', + moodCategory: MOODS_CATEGORY.happy.CODE, tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 700, @@ -116,7 +116,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'jjj', title: '방송 제목', contentCategory: CONTENTS_CATEGORY.music.CODE, - moodCategory: 'MOOD', + moodCategory: MOODS_CATEGORY.interesting.CODE, tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 800, @@ -127,7 +127,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'jjasdgj', title: '방송 제목', contentCategory: CONTENTS_CATEGORY.mukbang.CODE, - moodCategory: 'MOOD', + moodCategory: MOODS_CATEGORY.lonely.CODE, tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 800, @@ -138,7 +138,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'sss', title: '방송 제목', contentCategory: CONTENTS_CATEGORY.news.CODE, - moodCategory: 'MOOD', + moodCategory: MOODS_CATEGORY.lonely.CODE, tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 900, @@ -149,7 +149,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'tadg', title: '방송 제목', contentCategory: CONTENTS_CATEGORY.outdoor.CODE, - moodCategory: 'MOOD', + moodCategory: MOODS_CATEGORY.unknown.CODE, tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 1000, @@ -160,7 +160,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'tasdgdg', title: '방송 제목sdg', contentCategory: CONTENTS_CATEGORY.outdoor.CODE, - moodCategory: 'MOOD', + moodCategory: MOODS_CATEGORY.unknown.CODE, tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 1000, @@ -171,7 +171,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'tasdgs21245dg', title: '방송 제목sdg', contentCategory: CONTENTS_CATEGORY.study.CODE, - moodCategory: 'MOOD', + moodCategory: MOODS_CATEGORY.unknown.CODE, tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 1000, @@ -182,7 +182,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'tasdgs2125dg', title: '방송 제목sdg', contentCategory: CONTENTS_CATEGORY.talk.CODE, - moodCategory: 'MOOD', + moodCategory: MOODS_CATEGORY.unknown.CODE, tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 1000, @@ -193,7 +193,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'tdgs2125dg', title: '방송 제목sdg', contentCategory: CONTENTS_CATEGORY.travel.CODE, - moodCategory: 'MOOD', + moodCategory: MOODS_CATEGORY.interesting.CODE, tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 1000, @@ -204,7 +204,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'tbbbbbdgs2125dg', title: '방송 제목sdg', contentCategory: CONTENTS_CATEGORY.virtual.CODE, - moodCategory: 'MOOD', + moodCategory: MOODS_CATEGORY.getking.CODE, tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 1000, @@ -215,7 +215,7 @@ export const mockedBroadcasts: Broadcast[] = [ broadcastId: 'tdaaags2125dg', title: '방송 제목sdg', contentCategory: CONTENTS_CATEGORY.virtual.CODE, - moodCategory: 'MOOD', + moodCategory: MOODS_CATEGORY.getking.CODE, tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 1000, @@ -227,3 +227,7 @@ export const mockedBroadcasts: Broadcast[] = [ export const getBroadcastsByContentCategory = (contentCategory: string) => { return mockedBroadcasts.filter((broadcast) => broadcast.contentCategory === contentCategory); }; + +export const getBroadcastsByMoodCategory = (moodCategory: string) => { + return mockedBroadcasts.filter((broadcast) => broadcast.moodCategory === moodCategory); +}; From 1c7cfd6af4c22ad5e213c1259dbb71088142c8ca Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Tue, 26 Nov 2024 16:34:38 +0900 Subject: [PATCH 053/129] =?UTF-8?q?feat:=20=EB=B6=84=EC=9C=84=EA=B8=B0=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20-=20=EB=B6=84=EC=9C=84=EA=B8=B0=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=83=81=EC=84=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=9E=91=EC=84=B1=20#268=20-=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=20=EA=B7=B8=EB=A6=AC=EB=93=9C=20=EC=9E=AC?= =?UTF-8?q?=ED=99=9C=EC=9A=A9=20#270=20-=20=EC=97=90=EB=9F=AC=20=EB=B0=94?= =?UTF-8?q?=EC=9A=B4=EB=8D=94=EB=A6=AC=20=EC=9C=84=EC=B9=98=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(/categories/(details)/error.tsx)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ => (details)}/contents/[code]/page.tsx | 9 ++- .../{contents/[code] => (details)}/error.tsx | 0 .../features/CategoryLives.tsx} | 4 +- .../(details)/moods/[code]/page.tsx | 63 +++++++++++++++++++ .../(domain)/categories/moods/[code]/page.tsx | 5 -- 5 files changed, 69 insertions(+), 12 deletions(-) rename client/src/app/(domain)/categories/{ => (details)}/contents/[code]/page.tsx (84%) rename client/src/app/(domain)/categories/{contents/[code] => (details)}/error.tsx (100%) rename client/src/app/(domain)/categories/{contents/[code]/features/ContentCategory.tsx => (details)/features/CategoryLives.tsx} (88%) create mode 100644 client/src/app/(domain)/categories/(details)/moods/[code]/page.tsx delete mode 100644 client/src/app/(domain)/categories/moods/[code]/page.tsx diff --git a/client/src/app/(domain)/categories/contents/[code]/page.tsx b/client/src/app/(domain)/categories/(details)/contents/[code]/page.tsx similarity index 84% rename from client/src/app/(domain)/categories/contents/[code]/page.tsx rename to client/src/app/(domain)/categories/(details)/contents/[code]/page.tsx index 2bc252e7..76d23530 100644 --- a/client/src/app/(domain)/categories/contents/[code]/page.tsx +++ b/client/src/app/(domain)/categories/(details)/contents/[code]/page.tsx @@ -1,10 +1,9 @@ import { Suspense } from 'react'; -import ContentCategory from './features/ContentCategory'; import type { Broadcast, ContentsCategoryKey } from '@libs/internalTypes'; import { getBroadcastsByContentCategory } from '@mocks/broadcasts'; -import clsx from 'clsx'; import { CONTENTS_CATEGORY } from '@libs/constants'; import { unstable_noStore as noStore } from 'next/cache'; +import CategoryLives from '@app/(domain)/categories/(details)/features/CategoryLives'; const fetchData = async (code: string): Promise => { if (process.env.NODE_ENV !== 'production') return getBroadcastsByContentCategory(code); @@ -37,8 +36,8 @@ const ContentCategoryPage = async ({ params }: Props) => { return (
    -
    -

    {cateogryName}

    +
    +

    {cateogryName}

    라이브 목록을 불러우는 중...

    }> @@ -58,7 +57,7 @@ const ContentCategoryFetcher = async ({ code }: FetcherProps) => { return
    아직 방송 중인 스트리머가 없어요. 🥲
    ; } - return ; + return ; }; export default ContentCategoryPage; diff --git a/client/src/app/(domain)/categories/contents/[code]/error.tsx b/client/src/app/(domain)/categories/(details)/error.tsx similarity index 100% rename from client/src/app/(domain)/categories/contents/[code]/error.tsx rename to client/src/app/(domain)/categories/(details)/error.tsx diff --git a/client/src/app/(domain)/categories/contents/[code]/features/ContentCategory.tsx b/client/src/app/(domain)/categories/(details)/features/CategoryLives.tsx similarity index 88% rename from client/src/app/(domain)/categories/contents/[code]/features/ContentCategory.tsx rename to client/src/app/(domain)/categories/(details)/features/CategoryLives.tsx index 1defaf4e..f8e1a271 100644 --- a/client/src/app/(domain)/categories/contents/[code]/features/ContentCategory.tsx +++ b/client/src/app/(domain)/categories/(details)/features/CategoryLives.tsx @@ -7,7 +7,7 @@ type Props = { lives: Broadcast[]; }; -const ContentCategory = ({ lives }: Props) => { +const CategoryLives = ({ lives }: Props) => { return ( {({ visibleLives, isExpanded, toggle }) => ( @@ -24,4 +24,4 @@ const ContentCategory = ({ lives }: Props) => { ); }; -export default ContentCategory; +export default CategoryLives; diff --git a/client/src/app/(domain)/categories/(details)/moods/[code]/page.tsx b/client/src/app/(domain)/categories/(details)/moods/[code]/page.tsx new file mode 100644 index 00000000..0687f450 --- /dev/null +++ b/client/src/app/(domain)/categories/(details)/moods/[code]/page.tsx @@ -0,0 +1,63 @@ +import { MOODS_CATEGORY } from '@libs/constants'; +import type { Broadcast, MoodsCategoryKey } from '@libs/internalTypes'; +import { getBroadcastsByMoodCategory } from '@mocks/broadcasts'; +import { unstable_noStore as noStore } from 'next/cache'; +import CategoryLives from '@app/(domain)/categories/(details)/features/CategoryLives'; +import { Suspense } from 'react'; + +const fetchData = async (code: string): Promise => { + if (process.env.NODE_ENV !== 'production') return getBroadcastsByMoodCategory(code); + + const response = await fetch(`/api/live/category?mood=${code}`, { + cache: 'no-cache', + }); + + if (!response.ok) { + throw new Error('라이브 목록을 불러오는데 실패했어요.'); + } + + const data = await response.json(); + + return data; +}; + +type Props = { + params: Promise<{ + code: MoodsCategoryKey; + }>; +}; + +const MoodCategoryPage = async ({ params }: Props) => { + noStore(); + + const code = (await params).code; + + const cateogryName = MOODS_CATEGORY[code].NAME; + + return ( +
    +
    +

    {cateogryName}

    +
    + 라이브 목록을 불러우는 중...

    }> + +
    +
    + ); +}; + +type FetcherProps = { + code: string; +}; + +const MoodCategoryFetcher = async ({ code }: FetcherProps) => { + const lives = await fetchData(code); + + if (lives.length === 0) { + return
    아직 방송 중인 스트리머가 없어요. 🥲
    ; + } + + return ; +}; + +export default MoodCategoryPage; diff --git a/client/src/app/(domain)/categories/moods/[code]/page.tsx b/client/src/app/(domain)/categories/moods/[code]/page.tsx deleted file mode 100644 index db2c7654..00000000 --- a/client/src/app/(domain)/categories/moods/[code]/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const MoodCategoryPage = () => { - return
    MoodCategoryPage
    ; -}; - -export default MoodCategoryPage; From 8bdf60bad56508cbb0c9933dfad4957233ce7f3b Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Tue, 26 Nov 2024 16:35:11 +0900 Subject: [PATCH 054/129] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/app/(domain)/categories/(details)/error.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/app/(domain)/categories/(details)/error.tsx b/client/src/app/(domain)/categories/(details)/error.tsx index 98748f2e..f5dbf936 100644 --- a/client/src/app/(domain)/categories/(details)/error.tsx +++ b/client/src/app/(domain)/categories/(details)/error.tsx @@ -1,4 +1,4 @@ -'use client'; // Error boundaries must be Client Components +'use client'; import Button from '@components/Button'; import useInternalRouter from '@hooks/useInternalRouter'; From 15c7503a853ac9adc7238ab0dfcf71add49e6a46 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Tue, 26 Nov 2024 16:54:59 +0900 Subject: [PATCH 055/129] =?UTF-8?q?fix:=20=EB=B3=BC=EB=A5=A8=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20overflow=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=94=BD=EC=8A=A4=20-=20=EB=B3=BC=EB=A5=B0=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=9D=BC=EB=B6=80=20=EC=98=81?= =?UTF-8?q?=EC=97=AD=EC=9D=B4=20overflow=20hidden=EC=97=90=20=EC=9D=98?= =?UTF-8?q?=ED=95=B4=20=EA=B0=80=EB=A0=A4=EC=A7=80=EB=8A=94=20=ED=98=84?= =?UTF-8?q?=EC=83=81=20=EC=88=98=EC=A0=95=20#277?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/app/(domain)/features/live/Live.tsx | 1 - client/src/app/(domain)/features/live/VideoController.tsx | 8 +++++--- client/src/app/components/RangeInput.tsx | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/client/src/app/(domain)/features/live/Live.tsx b/client/src/app/(domain)/features/live/Live.tsx index 26b425cc..3f15d49b 100644 --- a/client/src/app/(domain)/features/live/Live.tsx +++ b/client/src/app/(domain)/features/live/Live.tsx @@ -173,7 +173,6 @@ const Live = Object.assign(LiveController, { Wrapper: LiveWrapper, VideoWrapper, Video, - // Info: LiveInfo, }); export default Live; diff --git a/client/src/app/(domain)/features/live/VideoController.tsx b/client/src/app/(domain)/features/live/VideoController.tsx index 44af92e3..96ad8df6 100644 --- a/client/src/app/(domain)/features/live/VideoController.tsx +++ b/client/src/app/(domain)/features/live/VideoController.tsx @@ -187,7 +187,7 @@ const VolumeController = () => { return (
    { setIsHidden(false); }} @@ -201,10 +201,12 @@ const VolumeController = () => {
    - +
    + +
    ); diff --git a/client/src/app/components/RangeInput.tsx b/client/src/app/components/RangeInput.tsx index 647798cd..83c4cfe4 100644 --- a/client/src/app/components/RangeInput.tsx +++ b/client/src/app/components/RangeInput.tsx @@ -74,6 +74,7 @@ const RangeInput = ({ value, updateValue, min = 0, max = 100, step = 10 }: Props }; const percentage = ((internalValue - min) / (max - min)) * 100; + const handlerPosition = `calc(${percentage}% - 0.3rem)`; useEffect(() => { if (!isChangingRef.current) { @@ -101,7 +102,7 @@ const RangeInput = ({ value, updateValue, min = 0, max = 100, step = 10 }: Props
    @@ -150,7 +150,7 @@ const SuggestedListItem = ({ suggest, isDesktop }: { suggest: Broadcast; isDeskt
    {suggest.title}
    -
    {'· ' + suggest.viewerCount}
    +
    {'· ' + comma(suggest.viewerCount)}
    )}
From 577e6fdf7b26760fdf885f0710b591acd7fd4710 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Tue, 26 Nov 2024 15:44:09 +0900 Subject: [PATCH 060/129] =?UTF-8?q?feat:=20=ED=8C=94=EB=A1=9C=EC=9E=89=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=95=A1?= =?UTF-8?q?=EC=85=98=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/__mocks__/follow.ts | 106 +++++------------- client/src/__mocks__/mydata.ts | 7 +- client/src/__mocks__/suggest.ts | 78 +++++++++++++ .../app/(domain)/features/FollowingLives.tsx | 73 ++++++++++++ .../(domain)/features/live/FollowingLive.tsx | 19 ++++ client/src/app/(domain)/page.tsx | 2 + .../app/components/cabinet/SuggestedList.tsx | 10 +- client/src/libs/actions.ts | 15 ++- client/src/libs/internalTypes.ts | 23 +++- client/src/server/handlers.ts | 6 + 10 files changed, 249 insertions(+), 90 deletions(-) create mode 100644 client/src/__mocks__/suggest.ts create mode 100644 client/src/app/(domain)/features/FollowingLives.tsx create mode 100644 client/src/app/(domain)/features/live/FollowingLive.tsx diff --git a/client/src/__mocks__/follow.ts b/client/src/__mocks__/follow.ts index e0068668..e9c01942 100644 --- a/client/src/__mocks__/follow.ts +++ b/client/src/__mocks__/follow.ts @@ -1,78 +1,34 @@ -import type { Live } from '@libs/internalTypes'; - -export const mockedFollows: Live[] = [ - { - id: '1', - isStreaming: false, - thumbnail: 'https://via.placeholder.com/150', - viewers: 9999, - title: '[V리그] 생중계 GS칼텍스 vs IBK기업은행 #watchparty (IBK 응원)', - category: '도널드 트럼프', - tags: ['태그1', '태그2', '태그2', '태그2', '태그2', '태그2'], - streamer: { - name: '슈카월드', - profileImage: 'https://via.placeholder.com/150', +export const mockedFollowingList = { + onAir: [ + { + playlistUrl: 'http://example.com/0', + broadCastData: { + broadcastId: 'example-broadcast-id', + broadcastPath: 'example-broadcast-id/internal-path', + title: '펀치의 라이브 방송', + contentCategory: '', + moodCategory: '행복한', + tags: ['부스트캠프', '펀치', '무지개 치즈'], + thumbnailUrl: 'http://example.com/1', + viewerCount: 20, + userName: '지존펀치', + profileImageUrl: 'http://example.com/2', + }, }, - }, - { - id: '2', - isStreaming: true, - thumbnail: 'https://via.placeholder.com/150', - viewers: 330, - title: '방송 제목', - category: '고영희', - tags: ['태그1', '태그2'], - streamer: { - name: '모카', - profileImage: 'https://via.placeholder.com/150', + ], + offAir: [ + { + name: '섬세한장어1405', + profile_image: 'https://avatars.githubusercontent.com/u/103445254?v=4', + broadcast_id: '3fd04e87-57b3-46da-b5be-8c97fa72cf22', + follower_count: 0, }, - }, - { - id: '3', - thumbnail: 'https://via.placeholder.com/150', - viewers: 103, - title: '방송 제목', - category: '펀치', - tags: ['태그1', '태그2'], - streamer: { - name: '짜왕', - profileImage: 'https://via.placeholder.com/150', + { + name: 'zzawang', + profile_image: + 'https://kr.object.ncloudstorage.com/funch-storage/profile/profile_67482aed-3098-4c31-94ee-52110e9073cc.png', + broadcast_id: '67482aed-3098-4c31-94ee-52110e9073cc', + follower_count: 278, }, - }, - { - id: '4', - thumbnail: 'https://via.placeholder.com/150', - viewers: 220, - title: '방송 제목', - category: '카테고리', - tags: ['태그1', '태그2'], - streamer: { - name: '스트리머 이름', - profileImage: 'https://via.placeholder.com/150', - }, - }, - { - id: '5', - thumbnail: 'https://via.placeholder.com/150', - viewers: 300, - title: '방송 제목', - category: '카테고리', - tags: ['태그1', '태그2'], - streamer: { - name: '스트리머 이름', - profileImage: 'https://via.placeholder.com/150', - }, - }, - { - id: '6', - thumbnail: 'https://via.placeholder.com/150', - viewers: 1100, - title: '방송 제목', - category: '카테고리', - tags: ['태그1', '태그2'], - streamer: { - name: '스트리머 이름', - profileImage: 'https://via.placeholder.com/150', - }, - }, -]; + ], +}; diff --git a/client/src/__mocks__/mydata.ts b/client/src/__mocks__/mydata.ts index c8912a93..8d8c38de 100644 --- a/client/src/__mocks__/mydata.ts +++ b/client/src/__mocks__/mydata.ts @@ -1,10 +1,11 @@ -export const mockedMydata = { +import { MyData } from '@libs/internalTypes'; + +export const mockedMydata: MyData = { id: 'aaaaaaaaaaaaaaaaaa', name: '텔레토비TV', profile_image: 'https://profile.png', stream_key: 'bbbbbbbbbbbbbbbbbbbbbbb', broadcast_id: 'cccccccccccccccccccccccc', follower_count: 1, - createdAt: '2024-11-17T13:12:06.000Z', - deletedAt: null, + created_at: '2024-11-17T13:12:06.000Z', }; diff --git a/client/src/__mocks__/suggest.ts b/client/src/__mocks__/suggest.ts new file mode 100644 index 00000000..50bf7ed2 --- /dev/null +++ b/client/src/__mocks__/suggest.ts @@ -0,0 +1,78 @@ +import type { Live } from '@libs/internalTypes'; + +export const mockedSuggests: Live[] = [ + { + id: '1', + isStreaming: false, + thumbnail: 'https://via.placeholder.com/150', + viewers: 9999, + title: '[V리그] 생중계 GS칼텍스 vs IBK기업은행 #watchparty (IBK 응원)', + category: '도널드 트럼프', + tags: ['태그1', '태그2', '태그2', '태그2', '태그2', '태그2'], + streamer: { + name: '슈카월드', + profileImage: 'https://via.placeholder.com/150', + }, + }, + { + id: '2', + isStreaming: true, + thumbnail: 'https://via.placeholder.com/150', + viewers: 330, + title: '방송 제목', + category: '고영희', + tags: ['태그1', '태그2'], + streamer: { + name: '모카', + profileImage: 'https://via.placeholder.com/150', + }, + }, + { + id: '3', + thumbnail: 'https://via.placeholder.com/150', + viewers: 103, + title: '방송 제목', + category: '펀치', + tags: ['태그1', '태그2'], + streamer: { + name: '짜왕', + profileImage: 'https://via.placeholder.com/150', + }, + }, + { + id: '4', + thumbnail: 'https://via.placeholder.com/150', + viewers: 220, + title: '방송 제목', + category: '카테고리', + tags: ['태그1', '태그2'], + streamer: { + name: '스트리머 이름', + profileImage: 'https://via.placeholder.com/150', + }, + }, + { + id: '5', + thumbnail: 'https://via.placeholder.com/150', + viewers: 300, + title: '방송 제목', + category: '카테고리', + tags: ['태그1', '태그2'], + streamer: { + name: '스트리머 이름', + profileImage: 'https://via.placeholder.com/150', + }, + }, + { + id: '6', + thumbnail: 'https://via.placeholder.com/150', + viewers: 1100, + title: '방송 제목', + category: '카테고리', + tags: ['태그1', '태그2'], + streamer: { + name: '스트리머 이름', + profileImage: 'https://via.placeholder.com/150', + }, + }, +]; diff --git a/client/src/app/(domain)/features/FollowingLives.tsx b/client/src/app/(domain)/features/FollowingLives.tsx new file mode 100644 index 00000000..59dc5fd0 --- /dev/null +++ b/client/src/app/(domain)/features/FollowingLives.tsx @@ -0,0 +1,73 @@ +'use client'; +import DeemedLink from '@components/DeemedLink'; +import clsx from 'clsx'; +import useUser from '@hooks/useUser'; +import Lives from '@components/livesGrid/Lives'; +import { useEffect, useState } from 'react'; +import type { Broadcast } from '@libs/internalTypes'; +import { getFollowingLiveList } from '@libs/actions'; + +const FollowingLives = () => { + const { isLoggedin } = useUser(); + + const [isLoading, setIsLoading] = useState(true); + const [isError, setIsError] = useState(false); + const [lives, setLives] = useState([]); + + useEffect(() => { + let isValidEffect = true; + const fetchLives = async () => { + try { + const fetchedLives = await getFollowingLiveList(); + console.log(fetchedLives); + setLives(fetchedLives); + setIsLoading(false); + } catch (err) { + if (!isValidEffect) return; + setLives([]); + setIsError(true); + } + }; + + fetchLives(); + + return () => { + isValidEffect = false; + }; + }, []); + + if (isError) { + return
에러가 발생했습니다.
; + } + + if (isLoading) { + return
로딩 중...
; + } + + return ( + <> + {isLoggedin && ( +
+
+

팔로우 중인 방송

+ +
+ + {({ visibleLives, isExpanded, toggle }) => ( + <> + + {visibleLives.map((live, index) => ( + + ))} + + {lives.length > 3 && } + + )} + +
+ )} + + ); +}; + +export default FollowingLives; diff --git a/client/src/app/(domain)/features/live/FollowingLive.tsx b/client/src/app/(domain)/features/live/FollowingLive.tsx new file mode 100644 index 00000000..9ee3b216 --- /dev/null +++ b/client/src/app/(domain)/features/live/FollowingLive.tsx @@ -0,0 +1,19 @@ +'use client'; + +import { + type ChangeEvent, + type ForwardedRef, + type PropsWithChildren, + type ReactNode, + type RefObject, + forwardRef, + useEffect, + useRef, + useState, +} from 'react'; + +const FollowingLive = () => { + return
FollowingLive
; +}; + +export default FollowingLive; diff --git a/client/src/app/(domain)/page.tsx b/client/src/app/(domain)/page.tsx index 64f5b839..bc97a985 100644 --- a/client/src/app/(domain)/page.tsx +++ b/client/src/app/(domain)/page.tsx @@ -1,8 +1,10 @@ import RecommendedLives from './features/RecommendedLives'; +import FollowingLives from './features/FollowingLives'; const HomePage = () => { return (
+
); diff --git a/client/src/app/components/cabinet/SuggestedList.tsx b/client/src/app/components/cabinet/SuggestedList.tsx index 8c8e4428..3825c269 100644 --- a/client/src/app/components/cabinet/SuggestedList.tsx +++ b/client/src/app/components/cabinet/SuggestedList.tsx @@ -17,8 +17,6 @@ const SuggestedList = ({ const [isLoading, setIsLoading] = useState(true); const [suggestedList, setSuggestedList] = useState([]); - const hoverRef = useRef(null); - useEffect(() => { const fetchSuggestions = async () => { const suggestions = await getSuggestedLiveList(); @@ -104,10 +102,10 @@ const SuggestedListItem = ({ suggest, isDesktop }: { suggest: Broadcast; isDeskt />
-
{suggest.userName}
-
{suggest.contentCategory}
+
{suggest.userName}
+
{suggest.contentCategory}
-

+

{'· ' + comma(suggest.viewerCount)}

@@ -116,7 +114,7 @@ const SuggestedListItem = ({ suggest, isDesktop }: { suggest: Broadcast; isDeskt {isTooltipVisible && (
{suggest.title} diff --git a/client/src/libs/actions.ts b/client/src/libs/actions.ts index 47f65e9f..e304da8e 100644 --- a/client/src/libs/actions.ts +++ b/client/src/libs/actions.ts @@ -1,4 +1,4 @@ -import type { Broadcast, InternalUserSession, Playlist, Update, Mydata } from '@libs/internalTypes'; +import type { Broadcast, InternalUserSession, Playlist, Update } from '@libs/internalTypes'; import fetcher from '@libs/fetcher'; export const getLiveList = async (): Promise => { @@ -10,6 +10,15 @@ export const getLiveList = async (): Promise => { return result.sort((a, b) => b.viewerCount - a.viewerCount); }; +export const getFollowingLiveList = async (): Promise => { + const result = await fetcher({ + method: 'GET', + url: '/api/live/follow', + }); + + return result.sort((a, b) => b.viewerCount - a.viewerCount); +}; + export const getPlaylist = async (broadcastId: string): Promise => { const result = await fetcher({ method: 'GET', @@ -97,8 +106,8 @@ export const updateInfo = async (formData: Update): Promise => { return result; }; -export const getStreamInfo = async (): Promise => { - const result = await fetcher({ +export const getStreamInfo = async (): Promise => { + const result = await fetcher({ method: 'GET', url: '/api/members/mydata', }); diff --git a/client/src/libs/internalTypes.ts b/client/src/libs/internalTypes.ts index 54c47c0e..910fda39 100644 --- a/client/src/libs/internalTypes.ts +++ b/client/src/libs/internalTypes.ts @@ -70,6 +70,23 @@ export type Broadcast = { profileImageUrl: string; }; +export type FollowingList = { + onAir: OnAirBroadcast[]; + offAir: User2[]; +}; + +type OnAirBroadcast = { + playlistUrl: string; + broadCastData: Broadcast; +}; + +type User2 = { + name: string; + profile_image: string; + broadcast_id: string; + follower_count: number; +}; + // 11.20 18:30 export type Playlist = { playlistUrl: string; @@ -101,13 +118,13 @@ export type Update = { thumbnail?: string | null; }; -export type Mydata = { +export type MyData = { id: string; name: string; profile_image: string; stream_key: string; broadcast_id: string; follower_count: number; - createdAt: string; - deletedAt: string | null; + created_at: string; + deleted_at?: string; }; diff --git a/client/src/server/handlers.ts b/client/src/server/handlers.ts index 532820a6..f5f86922 100644 --- a/client/src/server/handlers.ts +++ b/client/src/server/handlers.ts @@ -4,11 +4,16 @@ import { mockedUsers } from '@mocks/users'; import { mockedUpdates } from '@mocks/updates'; import { http, HttpResponse } from 'msw'; import { mockedMydata } from '@mocks/mydata'; +import { mockedFollowingList } from '@mocks/follow'; const getLiveList = () => { return HttpResponse.json(mockedBroadcasts); }; +const getFollowingList = () => { + return HttpResponse.json(mockedFollowingList); +}; + const getPlaylist = ({ params }: { params: { broadcastId: string } }) => { const playlist = mockedPlaylists.find((playlist) => playlist.broadcastData.broadcastId === params.broadcastId); if (!playlist) { @@ -60,6 +65,7 @@ export const handlers = [ http.get('/api/users/:broadcastId', getUserByBroadcastId), http.get('/api/live/list/suggest', getSuggestedLiveList), http.get('/api/members/mydata', getMydata), + http.get('/api/live/follow', getFollowingList), http.post('/api/auth/github/callback', authenticate), http.post('/api/auth/naver/callback', authenticate), http.post('/api/auth/google/callback', authenticate), From 43bf745115e6c2fb1672ef6e927eff319a210fdb Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Tue, 26 Nov 2024 16:14:41 +0900 Subject: [PATCH 061/129] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=8C=94=EB=A1=9C=EC=9E=89=20=EC=B1=84?= =?UTF-8?q?=EB=84=90=20UI=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/__mocks__/follow.ts | 51 +++++++++++++++++-- .../app/(domain)/features/FollowingLives.tsx | 7 ++- client/src/libs/actions.ts | 16 ++++-- client/src/libs/internalTypes.ts | 2 +- client/src/server/handlers.ts | 2 +- 5 files changed, 67 insertions(+), 11 deletions(-) diff --git a/client/src/__mocks__/follow.ts b/client/src/__mocks__/follow.ts index e9c01942..6b48b33e 100644 --- a/client/src/__mocks__/follow.ts +++ b/client/src/__mocks__/follow.ts @@ -1,7 +1,7 @@ export const mockedFollowingList = { onAir: [ { - playlistUrl: 'http://example.com/0', + playlistUrl: 'https://via.placeholder.com/150', broadCastData: { broadcastId: 'example-broadcast-id', broadcastPath: 'example-broadcast-id/internal-path', @@ -9,10 +9,55 @@ export const mockedFollowingList = { contentCategory: '', moodCategory: '행복한', tags: ['부스트캠프', '펀치', '무지개 치즈'], - thumbnailUrl: 'http://example.com/1', + thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 20, userName: '지존펀치', - profileImageUrl: 'http://example.com/2', + profileImageUrl: 'https://via.placeholder.com/150', + }, + }, + { + playlistUrl: 'https://via.placeholder.com/150', + broadCastData: { + broadcastId: 'example-broadcast-id', + broadcastPath: 'example-broadcast-id/internal-path', + title: '펀치의 라이브 방송', + contentCategory: '', + moodCategory: '행복한', + tags: ['부스트캠프', '펀치', '무지개 치즈'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 20, + userName: '지존펀치', + profileImageUrl: 'https://via.placeholder.com/150', + }, + }, + { + playlistUrl: 'https://via.placeholder.com/150', + broadCastData: { + broadcastId: 'example-broadcast-id', + broadcastPath: 'example-broadcast-id/internal-path', + title: '펀치의 라이브 방송', + contentCategory: '', + moodCategory: '행복한', + tags: ['부스트캠프', '펀치', '무지개 치즈'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 20, + userName: '지존펀치', + profileImageUrl: 'https://via.placeholder.com/150', + }, + }, + { + playlistUrl: 'https://via.placeholder.com/150', + broadCastData: { + broadcastId: 'example-broadcast-id', + broadcastPath: 'example-broadcast-id/internal-path', + title: '펀치의 라이브 방송', + contentCategory: '', + moodCategory: '행복한', + tags: ['부스트캠프', '펀치', '무지개 치즈'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 20, + userName: '지존펀치', + profileImageUrl: 'https://via.placeholder.com/150', }, }, ], diff --git a/client/src/app/(domain)/features/FollowingLives.tsx b/client/src/app/(domain)/features/FollowingLives.tsx index 59dc5fd0..dd41e86f 100644 --- a/client/src/app/(domain)/features/FollowingLives.tsx +++ b/client/src/app/(domain)/features/FollowingLives.tsx @@ -19,8 +19,11 @@ const FollowingLives = () => { const fetchLives = async () => { try { const fetchedLives = await getFollowingLiveList(); - console.log(fetchedLives); - setLives(fetchedLives); + + const fetchedFollowingLives = fetchedLives.onAir.map((live) => live.broadCastData); + + setLives(fetchedFollowingLives); + setIsLoading(false); } catch (err) { if (!isValidEffect) return; diff --git a/client/src/libs/actions.ts b/client/src/libs/actions.ts index e304da8e..3db1b8bd 100644 --- a/client/src/libs/actions.ts +++ b/client/src/libs/actions.ts @@ -1,4 +1,12 @@ -import type { Broadcast, InternalUserSession, Playlist, Update } from '@libs/internalTypes'; +import type { + Broadcast, + FollowingList, + InternalUserSession, + Playlist, + User, + Update, + MyData, +} from '@libs/internalTypes'; import fetcher from '@libs/fetcher'; export const getLiveList = async (): Promise => { @@ -10,13 +18,13 @@ export const getLiveList = async (): Promise => { return result.sort((a, b) => b.viewerCount - a.viewerCount); }; -export const getFollowingLiveList = async (): Promise => { - const result = await fetcher({ +export const getFollowingLiveList = async (): Promise => { + const result = await fetcher({ method: 'GET', url: '/api/live/follow', }); - return result.sort((a, b) => b.viewerCount - a.viewerCount); + return result; }; export const getPlaylist = async (broadcastId: string): Promise => { diff --git a/client/src/libs/internalTypes.ts b/client/src/libs/internalTypes.ts index 910fda39..fbba055e 100644 --- a/client/src/libs/internalTypes.ts +++ b/client/src/libs/internalTypes.ts @@ -75,7 +75,7 @@ export type FollowingList = { offAir: User2[]; }; -type OnAirBroadcast = { +export type OnAirBroadcast = { playlistUrl: string; broadCastData: Broadcast; }; diff --git a/client/src/server/handlers.ts b/client/src/server/handlers.ts index f5f86922..d1b91ac1 100644 --- a/client/src/server/handlers.ts +++ b/client/src/server/handlers.ts @@ -61,11 +61,11 @@ const getMydata = () => { export const handlers = [ http.get('/api/live/list', getLiveList), + http.get('/api/live/follow', getFollowingList), http.get('/api/live/:broadcastId', getPlaylist), http.get('/api/users/:broadcastId', getUserByBroadcastId), http.get('/api/live/list/suggest', getSuggestedLiveList), http.get('/api/members/mydata', getMydata), - http.get('/api/live/follow', getFollowingList), http.post('/api/auth/github/callback', authenticate), http.post('/api/auth/naver/callback', authenticate), http.post('/api/auth/google/callback', authenticate), From 790429ca471cc949653bbadae90114ca8d390d35 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Tue, 26 Nov 2024 18:02:52 +0900 Subject: [PATCH 062/129] =?UTF-8?q?feat:=20offline=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=ED=85=9C=20=EC=B6=94=EA=B0=80,=20Lives=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=ED=85=9C=20hover=EC=8B=9C=20border=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/__mocks__/follow.ts | 41 ++++++++--- .../app/(domain)/features/FollowingLives.tsx | 1 + .../(domain)/features/live/FollowingLive.tsx | 19 ----- .../following/features/FollowingOffair.tsx | 73 +++++++++++++++++++ .../following/features/InduceLoginContent.tsx | 21 ++++++ client/src/app/(domain)/following/page.tsx | 24 +++++- client/src/app/components/livesGrid/Lives.tsx | 3 +- client/src/app/components/svgs/LoginSvg.tsx | 70 ++++++++++++++++++ client/src/libs/internalTypes.ts | 2 +- 9 files changed, 221 insertions(+), 33 deletions(-) delete mode 100644 client/src/app/(domain)/features/live/FollowingLive.tsx create mode 100644 client/src/app/(domain)/following/features/FollowingOffair.tsx create mode 100644 client/src/app/(domain)/following/features/InduceLoginContent.tsx create mode 100644 client/src/app/components/svgs/LoginSvg.tsx diff --git a/client/src/__mocks__/follow.ts b/client/src/__mocks__/follow.ts index 6b48b33e..b4bae3e2 100644 --- a/client/src/__mocks__/follow.ts +++ b/client/src/__mocks__/follow.ts @@ -3,12 +3,12 @@ export const mockedFollowingList = { { playlistUrl: 'https://via.placeholder.com/150', broadCastData: { - broadcastId: 'example-broadcast-id', + broadcastId: 'ccc', broadcastPath: 'example-broadcast-id/internal-path', title: '펀치의 라이브 방송', - contentCategory: '', + contentCategory: 'FUNCH', moodCategory: '행복한', - tags: ['부스트캠프', '펀치', '무지개 치즈'], + tags: ['펀치', '무지개 치즈'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 20, userName: '지존펀치', @@ -18,10 +18,10 @@ export const mockedFollowingList = { { playlistUrl: 'https://via.placeholder.com/150', broadCastData: { - broadcastId: 'example-broadcast-id', + broadcastId: 'bbb', broadcastPath: 'example-broadcast-id/internal-path', title: '펀치의 라이브 방송', - contentCategory: '', + contentCategory: 'FUNCH', moodCategory: '행복한', tags: ['부스트캠프', '펀치', '무지개 치즈'], thumbnailUrl: 'https://via.placeholder.com/150', @@ -33,10 +33,10 @@ export const mockedFollowingList = { { playlistUrl: 'https://via.placeholder.com/150', broadCastData: { - broadcastId: 'example-broadcast-id', + broadcastId: 'aaa', broadcastPath: 'example-broadcast-id/internal-path', title: '펀치의 라이브 방송', - contentCategory: '', + contentCategory: 'FUNCH', moodCategory: '행복한', tags: ['부스트캠프', '펀치', '무지개 치즈'], thumbnailUrl: 'https://via.placeholder.com/150', @@ -48,12 +48,12 @@ export const mockedFollowingList = { { playlistUrl: 'https://via.placeholder.com/150', broadCastData: { - broadcastId: 'example-broadcast-id', + broadcastId: 'ddd', broadcastPath: 'example-broadcast-id/internal-path', title: '펀치의 라이브 방송', - contentCategory: '', + contentCategory: 'FUNCH', moodCategory: '행복한', - tags: ['부스트캠프', '펀치', '무지개 치즈'], + tags: ['politics', 'election'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 20, userName: '지존펀치', @@ -75,5 +75,26 @@ export const mockedFollowingList = { broadcast_id: '67482aed-3098-4c31-94ee-52110e9073cc', follower_count: 278, }, + { + name: 'zzawang', + profile_image: + 'https://kr.object.ncloudstorage.com/funch-storage/profile/profile_67482aed-3098-4c31-94ee-52110e9073cc.png', + broadcast_id: '67482aed-3098-4c31-94ee-52110e9073cc', + follower_count: 278, + }, + { + name: 'zzawang', + profile_image: + 'https://kr.object.ncloudstorage.com/funch-storage/profile/profile_67482aed-3098-4c31-94ee-52110e9073cc.png', + broadcast_id: '67482aed-3098-4c31-94ee-52110e9073cc', + follower_count: 278, + }, + { + name: 'zzawang', + profile_image: + 'https://kr.object.ncloudstorage.com/funch-storage/profile/profile_67482aed-3098-4c31-94ee-52110e9073cc.png', + broadcast_id: '67482aed-3098-4c31-94ee-52110e9073cc', + follower_count: 278, + }, ], }; diff --git a/client/src/app/(domain)/features/FollowingLives.tsx b/client/src/app/(domain)/features/FollowingLives.tsx index dd41e86f..7ce9cdcc 100644 --- a/client/src/app/(domain)/features/FollowingLives.tsx +++ b/client/src/app/(domain)/features/FollowingLives.tsx @@ -21,6 +21,7 @@ const FollowingLives = () => { const fetchedLives = await getFollowingLiveList(); const fetchedFollowingLives = fetchedLives.onAir.map((live) => live.broadCastData); + console.log(fetchedFollowingLives[0].tags); setLives(fetchedFollowingLives); diff --git a/client/src/app/(domain)/features/live/FollowingLive.tsx b/client/src/app/(domain)/features/live/FollowingLive.tsx deleted file mode 100644 index 9ee3b216..00000000 --- a/client/src/app/(domain)/features/live/FollowingLive.tsx +++ /dev/null @@ -1,19 +0,0 @@ -'use client'; - -import { - type ChangeEvent, - type ForwardedRef, - type PropsWithChildren, - type ReactNode, - type RefObject, - forwardRef, - useEffect, - useRef, - useState, -} from 'react'; - -const FollowingLive = () => { - return
FollowingLive
; -}; - -export default FollowingLive; diff --git a/client/src/app/(domain)/following/features/FollowingOffair.tsx b/client/src/app/(domain)/following/features/FollowingOffair.tsx new file mode 100644 index 00000000..7302c480 --- /dev/null +++ b/client/src/app/(domain)/following/features/FollowingOffair.tsx @@ -0,0 +1,73 @@ +'use client'; +import clsx from 'clsx'; +import { useEffect, useState } from 'react'; +import type { Broadcast, User2 } from '@libs/internalTypes'; +import { getFollowingLiveList } from '@libs/actions'; +import Image from 'next/image'; + +const FollowingOffair = () => { + const [isLoading, setIsLoading] = useState(true); + const [isError, setIsError] = useState(false); + const [offlines, setOfflines] = useState([]); + + useEffect(() => { + let isValidEffect = true; + const fetchOfflines = async () => { + try { + const fetchedOfflines = await getFollowingLiveList(); + + const fetchedFollowingOfflines = fetchedOfflines.offAir; + + setOfflines(fetchedFollowingOfflines); + setIsLoading(false); + } catch (err) { + if (!isValidEffect) return; + setOfflines([]); + setIsError(true); + } + }; + + fetchOfflines(); + + return () => { + isValidEffect = false; + }; + }, []); + + if (isError) { + return
에러가 발생했습니다.
; + } + + if (isLoading) { + return
로딩 중...
; + } + return ( +
+
+

오프라인

+
+ +
+ ); +}; + +const OfflineItems = ({ offlines }: { offlines: User2[] }) => { + return ( +
+ {offlines.map((item) => { + return ( +
+
+
+ profile +
+

{item.name}

+
+
+ ); + })} +
+ ); +}; + +export default FollowingOffair; diff --git a/client/src/app/(domain)/following/features/InduceLoginContent.tsx b/client/src/app/(domain)/following/features/InduceLoginContent.tsx new file mode 100644 index 00000000..51e28cba --- /dev/null +++ b/client/src/app/(domain)/following/features/InduceLoginContent.tsx @@ -0,0 +1,21 @@ +import LoginSvg from '@components/svgs/LoginSvg'; +import LoginBtn from '@components/layout/LoginBtn'; +import Link from 'next/link'; + +const InduceLoginContent = () => { + return ( +
+ +

로그인하고 팔로잉 목록을 확인해보세요.

+
+ +
+
+ ); +}; + +const InduceLoginButton = () => { + return
; +}; + +export default InduceLoginContent; diff --git a/client/src/app/(domain)/following/page.tsx b/client/src/app/(domain)/following/page.tsx index 922d7e02..a05b723f 100644 --- a/client/src/app/(domain)/following/page.tsx +++ b/client/src/app/(domain)/following/page.tsx @@ -1,7 +1,27 @@ -import React from 'react'; +'use client'; + +import FollowingLives from '../features/FollowingLives'; +import useUser from '@hooks/useUser'; +import FollowingOffair from './features/FollowingOffair'; +import InduceLoginContent from './features/InduceLoginContent'; const FollowingPage = () => { - return
page
; + const { isLoggedin } = useUser(); + + return ( +
+ {isLoggedin ? ( +
+ + +
+ ) : ( +
+ +
+ )} +
+ ); }; export default FollowingPage; diff --git a/client/src/app/components/livesGrid/Lives.tsx b/client/src/app/components/livesGrid/Lives.tsx index 8aeefb22..6ea771b3 100644 --- a/client/src/app/components/livesGrid/Lives.tsx +++ b/client/src/app/components/livesGrid/Lives.tsx @@ -57,7 +57,8 @@ const Live = ({ live, isPriority = false }: LiveProps) => { href={`/lives/${live.broadcastId}`} className={clsx( 'pb-live-aspect-ratio relative block overflow-hidden', - 'rounded-xl border-0 border-solid border-transparent', + 'rounded-xl border-0 border-solid hover:m-[-0.1075rem]', + 'hover:border-surface-brand-strong hover:border-2', )} >
diff --git a/client/src/app/components/svgs/LoginSvg.tsx b/client/src/app/components/svgs/LoginSvg.tsx new file mode 100644 index 00000000..3c921ef1 --- /dev/null +++ b/client/src/app/components/svgs/LoginSvg.tsx @@ -0,0 +1,70 @@ +const LoginSvg = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default LoginSvg; diff --git a/client/src/libs/internalTypes.ts b/client/src/libs/internalTypes.ts index fbba055e..316aa8c3 100644 --- a/client/src/libs/internalTypes.ts +++ b/client/src/libs/internalTypes.ts @@ -80,7 +80,7 @@ export type OnAirBroadcast = { broadCastData: Broadcast; }; -type User2 = { +export type User2 = { name: string; profile_image: string; broadcast_id: string; From aec459aede94745f39daabeb64038b2e8124a798 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Tue, 26 Nov 2024 21:31:50 +0900 Subject: [PATCH 063/129] =?UTF-8?q?refactor:=20=ED=8C=94=EB=A1=9C=EC=9A=B0?= =?UTF-8?q?=20Provider=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - globalProvider에서 감싸도록 형성 - FollowingLives, FollowingOffair에서 참조 --- .../app/(domain)/features/FollowingLives.tsx | 47 +++---------- .../(domain)/following/features/Follow.tsx | 27 ++++++++ .../following/features/FollowingOffair.tsx | 34 +-------- client/src/app/(domain)/following/page.tsx | 24 +------ client/src/app/(domain)/page.tsx | 2 +- client/src/app/GlobalProvider.tsx | 5 +- .../app/providers/FollowingLivesProvider.tsx | 69 +++++++++++++++++++ 7 files changed, 114 insertions(+), 94 deletions(-) create mode 100644 client/src/app/(domain)/following/features/Follow.tsx create mode 100644 client/src/app/providers/FollowingLivesProvider.tsx diff --git a/client/src/app/(domain)/features/FollowingLives.tsx b/client/src/app/(domain)/features/FollowingLives.tsx index 7ce9cdcc..0a604828 100644 --- a/client/src/app/(domain)/features/FollowingLives.tsx +++ b/client/src/app/(domain)/features/FollowingLives.tsx @@ -1,44 +1,16 @@ 'use client'; -import DeemedLink from '@components/DeemedLink'; -import clsx from 'clsx'; + +import React from 'react'; +import { Broadcast, User2 } from '@libs/internalTypes'; import useUser from '@hooks/useUser'; import Lives from '@components/livesGrid/Lives'; -import { useEffect, useState } from 'react'; -import type { Broadcast } from '@libs/internalTypes'; -import { getFollowingLiveList } from '@libs/actions'; - -const FollowingLives = () => { - const { isLoggedin } = useUser(); - - const [isLoading, setIsLoading] = useState(true); - const [isError, setIsError] = useState(false); - const [lives, setLives] = useState([]); - - useEffect(() => { - let isValidEffect = true; - const fetchLives = async () => { - try { - const fetchedLives = await getFollowingLiveList(); - - const fetchedFollowingLives = fetchedLives.onAir.map((live) => live.broadCastData); - console.log(fetchedFollowingLives[0].tags); - - setLives(fetchedFollowingLives); - - setIsLoading(false); - } catch (err) { - if (!isValidEffect) return; - setLives([]); - setIsError(true); - } - }; +import clsx from 'clsx'; +import { useFollowingLives } from '@providers/FollowingLivesProvider'; - fetchLives(); +export const FollowingLives = () => { + const { isLoggedin } = useUser(); // 기존 useUser 훅 - return () => { - isValidEffect = false; - }; - }, []); + const { isError, isLoading, lives } = useFollowingLives(); if (isError) { return
에러가 발생했습니다.
; @@ -54,7 +26,6 @@ const FollowingLives = () => {

팔로우 중인 방송

-
{({ visibleLives, isExpanded, toggle }) => ( @@ -73,5 +44,3 @@ const FollowingLives = () => { ); }; - -export default FollowingLives; diff --git a/client/src/app/(domain)/following/features/Follow.tsx b/client/src/app/(domain)/following/features/Follow.tsx new file mode 100644 index 00000000..90a3f1fa --- /dev/null +++ b/client/src/app/(domain)/following/features/Follow.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { FollowingLives } from '@app/(domain)/features/FollowingLives'; +import useUser from '@hooks/useUser'; +import FollowingOffair from './FollowingOffair'; +import InduceLoginContent from './InduceLoginContent'; + +const Follow = () => { + const { isLoggedin } = useUser(); + + return ( +
+ {isLoggedin ? ( +
+ + +
+ ) : ( +
+ +
+ )} +
+ ); +}; + +export default Follow; diff --git a/client/src/app/(domain)/following/features/FollowingOffair.tsx b/client/src/app/(domain)/following/features/FollowingOffair.tsx index 7302c480..b6fb09f1 100644 --- a/client/src/app/(domain)/following/features/FollowingOffair.tsx +++ b/client/src/app/(domain)/following/features/FollowingOffair.tsx @@ -1,38 +1,10 @@ -'use client'; import clsx from 'clsx'; -import { useEffect, useState } from 'react'; -import type { Broadcast, User2 } from '@libs/internalTypes'; -import { getFollowingLiveList } from '@libs/actions'; +import type { User2 } from '@libs/internalTypes'; import Image from 'next/image'; +import { useFollowingLives } from '@providers/FollowingLivesProvider'; const FollowingOffair = () => { - const [isLoading, setIsLoading] = useState(true); - const [isError, setIsError] = useState(false); - const [offlines, setOfflines] = useState([]); - - useEffect(() => { - let isValidEffect = true; - const fetchOfflines = async () => { - try { - const fetchedOfflines = await getFollowingLiveList(); - - const fetchedFollowingOfflines = fetchedOfflines.offAir; - - setOfflines(fetchedFollowingOfflines); - setIsLoading(false); - } catch (err) { - if (!isValidEffect) return; - setOfflines([]); - setIsError(true); - } - }; - - fetchOfflines(); - - return () => { - isValidEffect = false; - }; - }, []); + const { isError, isLoading, offlines } = useFollowingLives(); if (isError) { return
에러가 발생했습니다.
; diff --git a/client/src/app/(domain)/following/page.tsx b/client/src/app/(domain)/following/page.tsx index a05b723f..0bdd3472 100644 --- a/client/src/app/(domain)/following/page.tsx +++ b/client/src/app/(domain)/following/page.tsx @@ -1,27 +1,7 @@ -'use client'; - -import FollowingLives from '../features/FollowingLives'; -import useUser from '@hooks/useUser'; -import FollowingOffair from './features/FollowingOffair'; -import InduceLoginContent from './features/InduceLoginContent'; +import Follow from './features/Follow'; const FollowingPage = () => { - const { isLoggedin } = useUser(); - - return ( -
- {isLoggedin ? ( -
- - -
- ) : ( -
- -
- )} -
- ); + return ; }; export default FollowingPage; diff --git a/client/src/app/(domain)/page.tsx b/client/src/app/(domain)/page.tsx index bc97a985..07e085b8 100644 --- a/client/src/app/(domain)/page.tsx +++ b/client/src/app/(domain)/page.tsx @@ -1,5 +1,5 @@ import RecommendedLives from './features/RecommendedLives'; -import FollowingLives from './features/FollowingLives'; +import { FollowingLives } from './features/FollowingLives'; const HomePage = () => { return ( diff --git a/client/src/app/GlobalProvider.tsx b/client/src/app/GlobalProvider.tsx index 9b5fd91f..dfadb99f 100644 --- a/client/src/app/GlobalProvider.tsx +++ b/client/src/app/GlobalProvider.tsx @@ -3,13 +3,16 @@ import { type PropsWithChildren } from 'react'; import UserProvider from '@providers/UserProvider'; import ThemeProvider from '@providers/ThemeProvider'; +import { FollowingLivesProvider } from '@providers/FollowingLivesProvider'; type Props = PropsWithChildren; const GlobalProvider = ({ children }: Props) => { return ( - {children} + + {children} + ); }; diff --git a/client/src/app/providers/FollowingLivesProvider.tsx b/client/src/app/providers/FollowingLivesProvider.tsx new file mode 100644 index 00000000..99266a45 --- /dev/null +++ b/client/src/app/providers/FollowingLivesProvider.tsx @@ -0,0 +1,69 @@ +'use client'; + +import React, { createContext, useState, useContext, useEffect, PropsWithChildren } from 'react'; +import { getFollowingLiveList } from '@libs/actions'; +import { Broadcast, User2 } from '@libs/internalTypes'; + +interface FollowingLivesContextType { + lives: Broadcast[]; + offlines: User2[]; + isLoading: boolean; + isError: boolean; + fetchLives: () => Promise; +} + +const FollowingLivesContext = createContext(undefined); + +export const FollowingLivesProvider = ({ children }: PropsWithChildren) => { + const [isLoading, setIsLoading] = useState(true); + const [isError, setIsError] = useState(false); + const [lives, setLives] = useState([]); + const [offlines, setOfflines] = useState([]); + + const fetchLives = async () => { + try { + setIsLoading(true); + setIsError(false); + + const fetchedLives = await getFollowingLiveList(); + const fetchedFollowingLives = fetchedLives.onAir.map((live) => live.broadCastData); + const fetchedFollowingOfflines = fetchedLives.offAir; + + setLives(fetchedFollowingLives); + setOfflines(fetchedFollowingOfflines); + setIsLoading(false); + } catch (err) { + setLives([]); + setIsError(true); + setIsLoading(false); + } + }; + + useEffect(() => { + fetchLives(); + }, []); + + return ( + + {children} + + ); +}; + +export const useFollowingLives = () => { + const context = useContext(FollowingLivesContext); + + if (context === undefined) { + throw new Error('useFollowingLives must be used within a FollowingLivesProvider'); + } + + return context; +}; From 197239324d479c5b83392d6d683015e6f40af236 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Tue, 26 Nov 2024 22:00:33 +0900 Subject: [PATCH 064/129] =?UTF-8?q?refactor:=20useFollowingLives=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC,=20provider=20globalProvider=20->=20main=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/(domain)/features/FollowingLives.tsx | 2 +- .../following/features/FollowingOffair.tsx | 2 +- client/src/app/(domain)/layout.tsx | 5 ++++- client/src/app/GlobalProvider.tsx | 5 +---- .../src/app/providers/FollowingLivesProvider.tsx | 12 +----------- client/src/hooks/useFollowingLives.ts | 14 ++++++++++++++ 6 files changed, 22 insertions(+), 18 deletions(-) create mode 100644 client/src/hooks/useFollowingLives.ts diff --git a/client/src/app/(domain)/features/FollowingLives.tsx b/client/src/app/(domain)/features/FollowingLives.tsx index 0a604828..278c0bc7 100644 --- a/client/src/app/(domain)/features/FollowingLives.tsx +++ b/client/src/app/(domain)/features/FollowingLives.tsx @@ -5,7 +5,7 @@ import { Broadcast, User2 } from '@libs/internalTypes'; import useUser from '@hooks/useUser'; import Lives from '@components/livesGrid/Lives'; import clsx from 'clsx'; -import { useFollowingLives } from '@providers/FollowingLivesProvider'; +import useFollowingLives from '@hooks/useFollowingLives'; export const FollowingLives = () => { const { isLoggedin } = useUser(); // 기존 useUser 훅 diff --git a/client/src/app/(domain)/following/features/FollowingOffair.tsx b/client/src/app/(domain)/following/features/FollowingOffair.tsx index b6fb09f1..e557f538 100644 --- a/client/src/app/(domain)/following/features/FollowingOffair.tsx +++ b/client/src/app/(domain)/following/features/FollowingOffair.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; import type { User2 } from '@libs/internalTypes'; import Image from 'next/image'; -import { useFollowingLives } from '@providers/FollowingLivesProvider'; +import useFollowingLives from '@hooks/useFollowingLives'; const FollowingOffair = () => { const { isError, isLoading, offlines } = useFollowingLives(); diff --git a/client/src/app/(domain)/layout.tsx b/client/src/app/(domain)/layout.tsx index 0f93b73b..3dad9e66 100644 --- a/client/src/app/(domain)/layout.tsx +++ b/client/src/app/(domain)/layout.tsx @@ -3,6 +3,7 @@ import Cabinet from '@components/cabinet/Cabinet'; import Header from '@components/layout/Header'; import LiveProvider from '@providers/LiveProvider'; import { type PropsWithChildren } from 'react'; +import { FollowingLivesProvider } from '@providers/FollowingLivesProvider'; const DomainLayout = ({ children }: PropsWithChildren) => { return ( @@ -12,7 +13,9 @@ const DomainLayout = ({ children }: PropsWithChildren) => {
{children} - + + +
diff --git a/client/src/app/GlobalProvider.tsx b/client/src/app/GlobalProvider.tsx index dfadb99f..9b5fd91f 100644 --- a/client/src/app/GlobalProvider.tsx +++ b/client/src/app/GlobalProvider.tsx @@ -3,16 +3,13 @@ import { type PropsWithChildren } from 'react'; import UserProvider from '@providers/UserProvider'; import ThemeProvider from '@providers/ThemeProvider'; -import { FollowingLivesProvider } from '@providers/FollowingLivesProvider'; type Props = PropsWithChildren; const GlobalProvider = ({ children }: Props) => { return ( - - {children} - + {children} ); }; diff --git a/client/src/app/providers/FollowingLivesProvider.tsx b/client/src/app/providers/FollowingLivesProvider.tsx index 99266a45..3445ee16 100644 --- a/client/src/app/providers/FollowingLivesProvider.tsx +++ b/client/src/app/providers/FollowingLivesProvider.tsx @@ -12,7 +12,7 @@ interface FollowingLivesContextType { fetchLives: () => Promise; } -const FollowingLivesContext = createContext(undefined); +export const FollowingLivesContext = createContext(undefined); export const FollowingLivesProvider = ({ children }: PropsWithChildren) => { const [isLoading, setIsLoading] = useState(true); @@ -57,13 +57,3 @@ export const FollowingLivesProvider = ({ children }: PropsWithChildren) => { ); }; - -export const useFollowingLives = () => { - const context = useContext(FollowingLivesContext); - - if (context === undefined) { - throw new Error('useFollowingLives must be used within a FollowingLivesProvider'); - } - - return context; -}; diff --git a/client/src/hooks/useFollowingLives.ts b/client/src/hooks/useFollowingLives.ts new file mode 100644 index 00000000..aac80717 --- /dev/null +++ b/client/src/hooks/useFollowingLives.ts @@ -0,0 +1,14 @@ +import { useContext } from 'react'; +import { FollowingLivesContext } from '@providers/FollowingLivesProvider'; + +const useFollowingLives = () => { + const context = useContext(FollowingLivesContext); + + if (context === undefined) { + throw new Error('useFollowingLives must be used within a FollowingLivesProvider'); + } + + return context; +}; + +export default useFollowingLives; From b02258d8c657e61a6bc44e610c5e02f92bcc63c8 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Wed, 27 Nov 2024 00:20:00 +0900 Subject: [PATCH 065/129] =?UTF-8?q?feat:=20=ED=8C=94=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ids = 팔로잉 리스트에서 broadcastId만 뽑아낸 것, myId = 내 broadcastId, broadcastId = 해당 방송의 id - mock api 연결. 리팩터링은 내일 해야겠다... --- .../app/(domain)/features/live/LiveInfo.tsx | 40 ++++++++++++++++--- .../(domain)/features/live/LiveSection.tsx | 9 ++++- .../following/features/FollowingOffair.tsx | 4 +- .../following/features/InduceLoginContent.tsx | 4 -- client/src/app/(domain)/layout.tsx | 18 ++++----- .../app/providers/FollowingLivesProvider.tsx | 6 +++ .../studio/features/StudioGuideContainer.tsx | 2 +- client/src/libs/actions.ts | 27 +++++++++++++ client/src/libs/internalTypes.ts | 5 +++ client/src/server/handlers.ts | 14 +++++++ 10 files changed, 107 insertions(+), 22 deletions(-) diff --git a/client/src/app/(domain)/features/live/LiveInfo.tsx b/client/src/app/(domain)/features/live/LiveInfo.tsx index 36d516f3..29e51706 100644 --- a/client/src/app/(domain)/features/live/LiveInfo.tsx +++ b/client/src/app/(domain)/features/live/LiveInfo.tsx @@ -8,6 +8,7 @@ import { memo, type PropsWithChildren, type ReactNode, useEffect, useRef, useSta import Badge from '@app/(domain)/features/Badge'; import { comma } from '@libs/formats'; import type { Broadcast } from '@libs/internalTypes'; +import { makeFollow, makeUnfollow } from '@libs/actions'; type Props = { children: (args: { liveInfo: Broadcast }) => ReactNode; @@ -113,17 +114,46 @@ const LiveInfoViewerCount = memo(({ viewerCount }: { viewerCount: number }) => { return {comma(viewerCount)}명 시청 중; }); -const LiveInfoFollowButton = () => { +type LiveInfoFollowToggleButtonProps = { + Ids: string[]; + broadcastId: string; + myId: string; +}; + +const LiveInfoFollowToggleButton = ({ Ids, broadcastId, myId }: LiveInfoFollowToggleButtonProps) => { + const isFollowed = Ids.includes(broadcastId); + const [followed, setFollowed] = useState(isFollowed); + + const followInfo = { + follower: myId, + following: broadcastId, + }; + + const fetchFollow = async () => { + if (!followed) { + await makeFollow(followInfo); + setFollowed(true); + } else { + await makeUnfollow(followInfo); + setFollowed(false); + } + }; + return (
); @@ -137,7 +167,7 @@ const LiveInfo = Object.assign(LiveInfoWrapper, { UserName: LiveInfoUserName, Tags: LiveInfoTags, ViewerCount: LiveInfoViewerCount, - FollowButton: LiveInfoFollowButton, + FollowButton: LiveInfoFollowToggleButton, }); export default LiveInfo; diff --git a/client/src/app/(domain)/features/live/LiveSection.tsx b/client/src/app/(domain)/features/live/LiveSection.tsx index 224d718e..edcd8a53 100644 --- a/client/src/app/(domain)/features/live/LiveSection.tsx +++ b/client/src/app/(domain)/features/live/LiveSection.tsx @@ -9,9 +9,16 @@ import VideoController from './VideoController'; import MiniPlayerController from './MiniPlayerController'; import Chat from './Chat'; import LiveInfo from './LiveInfo'; +import useFollowingLives from '@hooks/useFollowingLives'; +import useUserContext from '@hooks/useUserContext'; const LiveSection = () => { const { isLivePage, liveUrl } = useLiveContext(); + const { Ids } = useFollowingLives(); + const { userSession } = useUserContext(); + + const myId = userSession?.user?.broadcastId || ''; + // ** lives 페이지라면 [id]에 해당하는 스트리밍 중인 방송이 있는지 확인하여 // 없으면 NoLiveContent를 보여주고,(liveId를 null로) // 있으면 확장된 Live 섹션을 보여준다.(liveId를 id로) @@ -117,7 +124,7 @@ const LiveSection = () => { /> - + )} diff --git a/client/src/app/(domain)/following/features/FollowingOffair.tsx b/client/src/app/(domain)/following/features/FollowingOffair.tsx index e557f538..4786e07d 100644 --- a/client/src/app/(domain)/following/features/FollowingOffair.tsx +++ b/client/src/app/(domain)/following/features/FollowingOffair.tsx @@ -26,9 +26,9 @@ const FollowingOffair = () => { const OfflineItems = ({ offlines }: { offlines: User2[] }) => { return (
- {offlines.map((item) => { + {offlines.map((item, idx) => { return ( -
+
profile diff --git a/client/src/app/(domain)/following/features/InduceLoginContent.tsx b/client/src/app/(domain)/following/features/InduceLoginContent.tsx index 51e28cba..dfd7de91 100644 --- a/client/src/app/(domain)/following/features/InduceLoginContent.tsx +++ b/client/src/app/(domain)/following/features/InduceLoginContent.tsx @@ -14,8 +14,4 @@ const InduceLoginContent = () => { ); }; -const InduceLoginButton = () => { - return
; -}; - export default InduceLoginContent; diff --git a/client/src/app/(domain)/layout.tsx b/client/src/app/(domain)/layout.tsx index 3dad9e66..69dc5488 100644 --- a/client/src/app/(domain)/layout.tsx +++ b/client/src/app/(domain)/layout.tsx @@ -8,16 +8,16 @@ import { FollowingLivesProvider } from '@providers/FollowingLivesProvider'; const DomainLayout = ({ children }: PropsWithChildren) => { return ( <> -
- -
- {children} - - + +
+ +
+ {children} + - - -
+ +
+ ); }; diff --git a/client/src/app/providers/FollowingLivesProvider.tsx b/client/src/app/providers/FollowingLivesProvider.tsx index 3445ee16..c06cfb2e 100644 --- a/client/src/app/providers/FollowingLivesProvider.tsx +++ b/client/src/app/providers/FollowingLivesProvider.tsx @@ -6,6 +6,7 @@ import { Broadcast, User2 } from '@libs/internalTypes'; interface FollowingLivesContextType { lives: Broadcast[]; + Ids: string[]; offlines: User2[]; isLoading: boolean; isError: boolean; @@ -17,6 +18,7 @@ export const FollowingLivesContext = createContext { const [isLoading, setIsLoading] = useState(true); const [isError, setIsError] = useState(false); + const [Ids, setIds] = useState([]); const [lives, setLives] = useState([]); const [offlines, setOfflines] = useState([]); @@ -29,11 +31,14 @@ export const FollowingLivesProvider = ({ children }: PropsWithChildren) => { const fetchedFollowingLives = fetchedLives.onAir.map((live) => live.broadCastData); const fetchedFollowingOfflines = fetchedLives.offAir; + setIds(fetchedLives.onAir.map((live) => live.broadCastData.broadcastId)); setLives(fetchedFollowingLives); setOfflines(fetchedFollowingOfflines); + setIsLoading(false); } catch (err) { setLives([]); + setIds([]); setIsError(true); setIsLoading(false); } @@ -47,6 +52,7 @@ export const FollowingLivesProvider = ({ children }: PropsWithChildren) => { { 1 스트리밍 소프트웨어를 다운로드하세요.
-
+
=> { return result; }; +export const makeFollow = async ({ follower, following }: ToggleFollow): Promise => { + const requestBody = { follower, following } as any; + const result = await fetcher({ + method: 'POST', + url: '/api/follow', + customOptions: { + body: requestBody, + }, + }); + + return result; +}; + +export const makeUnfollow = async ({ follower, following }: ToggleFollow): Promise => { + const requestBody = { follower, following } as any; + const result = await fetcher({ + method: 'DELETE', + url: '/api/follow', + customOptions: { + body: requestBody, + }, + }); + + return result; +}; + export const getPlaylist = async (broadcastId: string): Promise => { const result = await fetcher({ method: 'GET', diff --git a/client/src/libs/internalTypes.ts b/client/src/libs/internalTypes.ts index 316aa8c3..00412b8e 100644 --- a/client/src/libs/internalTypes.ts +++ b/client/src/libs/internalTypes.ts @@ -40,6 +40,11 @@ export type FetcherParams = { customOptions?: RequestInit; }; +export type ToggleFollow = { + follower: string; + following: string; +}; + type UserSession = { name: string; profileImageUrl: string; diff --git a/client/src/server/handlers.ts b/client/src/server/handlers.ts index d1b91ac1..c8d2b513 100644 --- a/client/src/server/handlers.ts +++ b/client/src/server/handlers.ts @@ -25,6 +25,18 @@ const getPlaylist = ({ params }: { params: { broadcastId: string } }) => { return HttpResponse.json(playlist); }; +const makeFollow = ({ params }: { params: { follower: string; following: string } }) => { + return HttpResponse.json(null, { + status: 201, + }); +}; + +const makeUnfollow = ({ params }: { params: { follower: string; following: string } }) => { + return HttpResponse.json(null, { + status: 201, + }); +}; + const login = () => { const user = mockedUsers[0]; return HttpResponse.json(user); @@ -70,5 +82,7 @@ export const handlers = [ http.post('/api/auth/naver/callback', authenticate), http.post('/api/auth/google/callback', authenticate), http.post('/api/login', login), + http.post('/api/follow', makeFollow), + http.delete('/api/follow', makeUnfollow), http.patch('/api/live/update', update), ]; From 33fa438ca8d99d6af0ffbb81bad6bd0c1cddd83f Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Tue, 26 Nov 2024 22:35:01 +0900 Subject: [PATCH 066/129] =?UTF-8?q?feat:=20google=20login=20-=20=EA=B5=AC?= =?UTF-8?q?=EA=B8=80=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=95=A1=EC=85=98=20=EB=B0=8F=20=EC=BD=9C=EB=B0=B1?= =?UTF-8?q?=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20=EC=84=B8=EA=B7=B8=EB=A8=BC?= =?UTF-8?q?=ED=8A=B8=20-=20=EA=B5=AC=EA=B8=80=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20url=EC=9D=84=20next=20=ED=8C=A8=ED=84=B4=EC=97=90?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/next.config.mjs | 4 ++ .../github/callback/features/AuthGithub.tsx | 8 ++++ .../google/callback/features/AuthGoogle.tsx | 39 +++++++++++++++++++ .../src/app/(auth)/google/callback/page.tsx | 20 ++++++++++ .../naver/callback/features/AuthNaver.tsx | 8 ++++ client/src/app/(auth)/naver/callback/page.tsx | 4 +- .../src/app/components/layout/LoginModal.tsx | 18 ++++++++- client/src/app/providers/UserProvider.tsx | 18 +++++++++ client/src/hooks/useUser.ts | 3 +- client/src/libs/actions.ts | 25 ++++++++++++ 10 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 client/src/app/(auth)/google/callback/features/AuthGoogle.tsx create mode 100644 client/src/app/(auth)/google/callback/page.tsx diff --git a/client/next.config.mjs b/client/next.config.mjs index d27fa677..d4afeb2f 100644 --- a/client/next.config.mjs +++ b/client/next.config.mjs @@ -35,6 +35,10 @@ const nextConfig = { protocol: 'https', hostname: 'avatars.githubusercontent.com', }, + { + protocol: 'https', + hostname: 'lh3.googleusercontent.com', + }, ], }, }; diff --git a/client/src/app/(auth)/github/callback/features/AuthGithub.tsx b/client/src/app/(auth)/github/callback/features/AuthGithub.tsx index 510b16d1..5558b1d1 100644 --- a/client/src/app/(auth)/github/callback/features/AuthGithub.tsx +++ b/client/src/app/(auth)/github/callback/features/AuthGithub.tsx @@ -13,17 +13,25 @@ const AuthGithub = ({ authCode }: Props) => { const { saveUserSession } = useUser(); const { replace } = useInternalRouter(); useEffect(() => { + let isValidEffect = true; const fetchUser = async (code: string) => { try { const fetchResult = await authenticateByGithub(code); + if (!isValidEffect) return; saveUserSession(fetchResult); } catch (err) { + if (!isValidEffect) return; alert('로그인에 실패했어요.'); } finally { + if (!isValidEffect) return; replace('/'); } }; fetchUser(authCode); + + return () => { + isValidEffect = false; + }; }, [authCode, replace, saveUserSession]); return
인증 중...
; }; diff --git a/client/src/app/(auth)/google/callback/features/AuthGoogle.tsx b/client/src/app/(auth)/google/callback/features/AuthGoogle.tsx new file mode 100644 index 00000000..4f727b55 --- /dev/null +++ b/client/src/app/(auth)/google/callback/features/AuthGoogle.tsx @@ -0,0 +1,39 @@ +'use client'; + +import useInternalRouter from '@hooks/useInternalRouter'; +import { authenticateByGoogle } from '@libs/actions'; +import { useEffect } from 'react'; +import useUser from '@hooks/useUser'; + +type Props = { + authCode: string; +}; + +const AuthGoogle = ({ authCode }: Props) => { + const { saveUserSession } = useUser(); + const { replace } = useInternalRouter(); + useEffect(() => { + let isValidEffect = true; + const fetchUser = async (code: string) => { + try { + const fetchResult = await authenticateByGoogle(code); + if (!isValidEffect) return; + saveUserSession(fetchResult); + } catch (err) { + if (!isValidEffect) return; + alert('로그인에 실패했어요.'); + } finally { + if (!isValidEffect) return; + replace('/'); + } + }; + fetchUser(authCode); + + return () => { + isValidEffect = false; + }; + }, [authCode, replace, saveUserSession]); + return
인증 중...
; +}; + +export default AuthGoogle; diff --git a/client/src/app/(auth)/google/callback/page.tsx b/client/src/app/(auth)/google/callback/page.tsx new file mode 100644 index 00000000..e57bb57e --- /dev/null +++ b/client/src/app/(auth)/google/callback/page.tsx @@ -0,0 +1,20 @@ +import AuthRedirection from '@app/(auth)/features/AuthRedirection'; +import AuthGoogle from '@app/(auth)/google/callback/features/AuthGoogle'; + +const GoogleCallbackPgae = ({ + searchParams, +}: { + searchParams?: { + code: string; + }; +}) => { + const code = searchParams?.code; + + if (!code) { + return ; + } + + return ; +}; + +export default GoogleCallbackPgae; diff --git a/client/src/app/(auth)/naver/callback/features/AuthNaver.tsx b/client/src/app/(auth)/naver/callback/features/AuthNaver.tsx index 20ad3c3d..6deb75f4 100644 --- a/client/src/app/(auth)/naver/callback/features/AuthNaver.tsx +++ b/client/src/app/(auth)/naver/callback/features/AuthNaver.tsx @@ -14,20 +14,28 @@ const AuthNaver = ({ authCode, authState }: Props) => { const { saveUserSession } = useUser(); const { replace } = useInternalRouter(); useEffect(() => { + let isValidEffect = true; const fetchUser = async (code: string, state: string) => { try { const fetchResult = await authenticateByNaver({ code, state, }); + if (!isValidEffect) return; saveUserSession(fetchResult); } catch (err) { + if (!isValidEffect) return; alert('로그인에 실패했어요.'); } finally { + if (!isValidEffect) return; replace('/'); } }; fetchUser(authCode, authState); + + return () => { + isValidEffect = false; + }; }, [authCode, authState, replace, saveUserSession]); return
인증 중...
; }; diff --git a/client/src/app/(auth)/naver/callback/page.tsx b/client/src/app/(auth)/naver/callback/page.tsx index c664d311..82c2f955 100644 --- a/client/src/app/(auth)/naver/callback/page.tsx +++ b/client/src/app/(auth)/naver/callback/page.tsx @@ -1,7 +1,7 @@ import AuthRedirection from '@app/(auth)/features/AuthRedirection'; import AuthNaver from './features/AuthNaver'; -const GithubCallbackPage = ({ +const NaverCallbackPage = ({ searchParams, }: { searchParams?: { @@ -19,4 +19,4 @@ const GithubCallbackPage = ({ return ; }; -export default GithubCallbackPage; +export default NaverCallbackPage; diff --git a/client/src/app/components/layout/LoginModal.tsx b/client/src/app/components/layout/LoginModal.tsx index 07bb6489..bbae7088 100644 --- a/client/src/app/components/layout/LoginModal.tsx +++ b/client/src/app/components/layout/LoginModal.tsx @@ -8,7 +8,7 @@ import BrandButton from '@components/BrandButton'; type Props = ComponentPropsWithoutRef & {}; const LoginModal = ({ children, close }: Props) => { - const { loginByGithub, loginByNaver } = useUser(); + const { loginByGithub, loginByNaver, loginByGoogle } = useUser(); return ( @@ -35,6 +35,7 @@ const LoginModal = ({ children, close }: Props) => {
{ e.preventDefault(); loginByNaver(); @@ -49,6 +50,21 @@ const LoginModal = ({ children, close }: Props) => { 네이버로 로그인
+
{ + e.preventDefault(); + loginByGoogle(); + }} + > + + 구글로 로그인 + +
); }; diff --git a/client/src/app/providers/UserProvider.tsx b/client/src/app/providers/UserProvider.tsx index adef0133..528fc453 100644 --- a/client/src/app/providers/UserProvider.tsx +++ b/client/src/app/providers/UserProvider.tsx @@ -10,6 +10,7 @@ type UserContextType = { userSession: InternalUserSession | null; loginByGithub: () => Promise; loginByNaver: () => Promise; + loginByGoogle: () => void; logout: () => void; saveUserSession: (user: InternalUserSession) => void; }; @@ -18,6 +19,7 @@ export const UserContext = createContext({ userSession: null, loginByGithub: async () => {}, loginByNaver: async () => {}, + loginByGoogle: () => {}, logout: () => {}, saveUserSession: () => {}, }); @@ -70,6 +72,21 @@ const UserProvider = ({ children }: Props) => { location.href = `${process.env.NEXT_PUBLIC_NAVER_AUTH_BASE_URL}?response_type=code&client_id=${naverClientId}&redirect_uri=${redirectUri}`; }; + const loginByGoogle = () => { + // new URLSearchParams() + // https://accounts.google.com/o/oauth2/v2/auth?client_id={CLIENT_ID}&redirect_uri=https://funch.site/google/callback&response_type=code&scope=profile email + const clientId = `800585509743-8j219mpv4uuclvbo4f1397ocj2ei8tdh.apps.googleusercontent.com`; + const redirectUri = 'https://funch.site/google/callback'; + const searchParams = new URLSearchParams({ + client_id: clientId, + redirect_uri: redirectUri, + response_type: 'code', + scope: 'profile email', + }); + + location.href = `https://accounts.google.com/o/oauth2/v2/auth?${searchParams.toString()}`; + }; + useEffect(() => { // localStorage에 저장된 사용자 정보가 있다면 로그인 상태로 설정 const user = localStorage.getItem(LOCAL_STORAGE_USER_KEY); @@ -84,6 +101,7 @@ const UserProvider = ({ children }: Props) => { userSession, loginByGithub, loginByNaver, + loginByGoogle, logout, saveUserSession, }} diff --git a/client/src/hooks/useUser.ts b/client/src/hooks/useUser.ts index 862f90f7..61a65601 100644 --- a/client/src/hooks/useUser.ts +++ b/client/src/hooks/useUser.ts @@ -6,7 +6,7 @@ import useUserContext from '@hooks/useUserContext'; const getIsLoggedin = (user: any) => user !== null; const useUser = () => { - const { userSession, logout, loginByGithub, loginByNaver, saveUserSession } = useUserContext(); + const { userSession, logout, loginByGithub, loginByNaver, loginByGoogle, saveUserSession } = useUserContext(); const [isLoggedin, setIsLoggedin] = useState(getIsLoggedin(userSession)); useEffect(() => { @@ -19,6 +19,7 @@ const useUser = () => { logout, loginByGithub, loginByNaver, + loginByGoogle, saveUserSession, }; }; diff --git a/client/src/libs/actions.ts b/client/src/libs/actions.ts index 94be25cb..92009e26 100644 --- a/client/src/libs/actions.ts +++ b/client/src/libs/actions.ts @@ -101,6 +101,31 @@ export const authenticateByGithub = async (code: string): Promise => { + const requestBody = { code } as any; + const result = await fetcher<{ + accessToken: string; + name: string; + profile_image: string; + broadcast_id: string; + }>({ + method: 'POST', + url: '/api/auth/google/callback', + customOptions: { + body: requestBody, + }, + }); + + return { + accessToken: result.accessToken, + user: { + name: result.name, + profileImageUrl: result['profile_image'], + broadcastId: result['broadcast_id'], + }, + }; +}; + export const authenticateByNaver = async ({ code, state, From ccb39ed46d65b4dc21b584334841e3ed01472238 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Wed, 27 Nov 2024 00:44:59 +0900 Subject: [PATCH 067/129] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EB=82=B4=20=EB=B2=84=ED=8A=BC=EB=93=A4=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=BB=A4=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EC=A6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/app/components/Modal.tsx | 2 +- .../src/app/components/layout/LoginModal.tsx | 105 ++++++++++++++---- client/src/app/components/svgs/GithubSvg.tsx | 13 +++ client/src/app/components/svgs/GoogleSvg.tsx | 13 +++ client/src/app/components/svgs/NaverSvg.tsx | 10 ++ 5 files changed, 119 insertions(+), 24 deletions(-) create mode 100644 client/src/app/components/svgs/GithubSvg.tsx create mode 100644 client/src/app/components/svgs/GoogleSvg.tsx create mode 100644 client/src/app/components/svgs/NaverSvg.tsx diff --git a/client/src/app/components/Modal.tsx b/client/src/app/components/Modal.tsx index ba2d48ff..11563980 100644 --- a/client/src/app/components/Modal.tsx +++ b/client/src/app/components/Modal.tsx @@ -37,7 +37,7 @@ const Modal = ({ children, close }: Props) => { aria-labelledby="modal-title" aria-describedby="modal-description" className={clsx( - 'max-w-56 px-6 py-4', + 'min-w-56 max-w-64 px-6 py-4', 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2 transform', 'bg-surface-neutral-primary shadow-modal rounded-lg', 'border-border-neutral-base border border-solid', diff --git a/client/src/app/components/layout/LoginModal.tsx b/client/src/app/components/layout/LoginModal.tsx index bbae7088..1e87e0d2 100644 --- a/client/src/app/components/layout/LoginModal.tsx +++ b/client/src/app/components/layout/LoginModal.tsx @@ -1,9 +1,12 @@ 'use client'; -import { type ComponentPropsWithoutRef } from 'react'; +import { type ButtonHTMLAttributes, type PropsWithChildren, type ComponentPropsWithoutRef } from 'react'; import Modal from '@components/Modal'; import useUser from '@hooks/useUser'; -import BrandButton from '@components/BrandButton'; +import GoogleSvg from '@components/svgs/GoogleSvg'; +import clsx from 'clsx'; +import NaverSvg from '@components/svgs/NaverSvg'; +import GithubSvg from '@components/svgs/GithubSvg'; type Props = ComponentPropsWithoutRef & {}; @@ -25,14 +28,9 @@ const LoginModal = ({ children, close }: Props) => { loginByGithub(); }} > - + 깃허브로 로그인 - +
{ loginByNaver(); }} > - + 네이버로 로그인 - +
{ @@ -56,17 +49,83 @@ const LoginModal = ({ children, close }: Props) => { loginByGoogle(); }} > - + 구글로 로그인 - +
); }; +const LOGIN_BUTTON_COMPONENT_TYPE = { + GITHUB: 'GITHUB' as const, + NAVER: 'NAVER' as const, + GOOGLE: 'GOOGLE' as const, +}; + +type LoginButtonComponentType = keyof typeof LOGIN_BUTTON_COMPONENT_TYPE; + +const LOGIN_BUTTON_COMPONENT_COLOR: Record = { + GITHUB: '#181717', + NAVER: '#03C75A', + GOOGLE: '#4285F4', +}; + +type LoginButtonProps = ButtonHTMLAttributes & + PropsWithChildren<{ + componentType: LoginButtonComponentType; + }>; + +const LoginButton = ({ componentType, children, ...rest }: LoginButtonProps) => { + return ( + + ); +}; + +const LoginIconWrapper = ({ children }: PropsWithChildren) => { + return ( + + {children} + + ); +}; + +const LoginIconRenderer = ({ componentType }: { componentType: LoginButtonComponentType }) => { + switch (componentType) { + case LOGIN_BUTTON_COMPONENT_TYPE.GITHUB: + return ( + + + + ); + case LOGIN_BUTTON_COMPONENT_TYPE.NAVER: + return ( + + + + ); + case LOGIN_BUTTON_COMPONENT_TYPE.GOOGLE: + return ( + + + + ); + default: + return null; + } +}; + export default LoginModal; diff --git a/client/src/app/components/svgs/GithubSvg.tsx b/client/src/app/components/svgs/GithubSvg.tsx new file mode 100644 index 00000000..2c71d13c --- /dev/null +++ b/client/src/app/components/svgs/GithubSvg.tsx @@ -0,0 +1,13 @@ +const GithubSvg = () => { + return ( + + ); +}; + +export default GithubSvg; diff --git a/client/src/app/components/svgs/GoogleSvg.tsx b/client/src/app/components/svgs/GoogleSvg.tsx new file mode 100644 index 00000000..506b04eb --- /dev/null +++ b/client/src/app/components/svgs/GoogleSvg.tsx @@ -0,0 +1,13 @@ +const GoogleSvg = () => { + return ( + + ); +}; + +export default GoogleSvg; diff --git a/client/src/app/components/svgs/NaverSvg.tsx b/client/src/app/components/svgs/NaverSvg.tsx new file mode 100644 index 00000000..5053f379 --- /dev/null +++ b/client/src/app/components/svgs/NaverSvg.tsx @@ -0,0 +1,10 @@ +const NaverSvg = () => { + return ( + + ); +}; + +export default NaverSvg; From 9ac0f51203c0ee300656544fd37f1bd3cf3cce93 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Wed, 27 Nov 2024 00:55:35 +0900 Subject: [PATCH 068/129] =?UTF-8?q?chore:=20loginmodal=20=EB=82=B4=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=84=A0=EC=96=B8=EB=AC=B8=20=EC=9C=84?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/components/layout/LoginModal.tsx | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/client/src/app/components/layout/LoginModal.tsx b/client/src/app/components/layout/LoginModal.tsx index 1e87e0d2..659525b7 100644 --- a/client/src/app/components/layout/LoginModal.tsx +++ b/client/src/app/components/layout/LoginModal.tsx @@ -3,11 +3,25 @@ import { type ButtonHTMLAttributes, type PropsWithChildren, type ComponentPropsWithoutRef } from 'react'; import Modal from '@components/Modal'; import useUser from '@hooks/useUser'; -import GoogleSvg from '@components/svgs/GoogleSvg'; import clsx from 'clsx'; +import GoogleSvg from '@components/svgs/GoogleSvg'; import NaverSvg from '@components/svgs/NaverSvg'; import GithubSvg from '@components/svgs/GithubSvg'; +const LOGIN_BUTTON_COMPONENT_TYPE = { + GITHUB: 'GITHUB' as const, + NAVER: 'NAVER' as const, + GOOGLE: 'GOOGLE' as const, +}; + +type LoginButtonComponentType = keyof typeof LOGIN_BUTTON_COMPONENT_TYPE; + +const LOGIN_BUTTON_COMPONENT_COLOR: Record = { + GITHUB: '#181717', + NAVER: '#03C75A', + GOOGLE: '#4285F4', +}; + type Props = ComponentPropsWithoutRef & {}; const LoginModal = ({ children, close }: Props) => { @@ -57,20 +71,6 @@ const LoginModal = ({ children, close }: Props) => { ); }; -const LOGIN_BUTTON_COMPONENT_TYPE = { - GITHUB: 'GITHUB' as const, - NAVER: 'NAVER' as const, - GOOGLE: 'GOOGLE' as const, -}; - -type LoginButtonComponentType = keyof typeof LOGIN_BUTTON_COMPONENT_TYPE; - -const LOGIN_BUTTON_COMPONENT_COLOR: Record = { - GITHUB: '#181717', - NAVER: '#03C75A', - GOOGLE: '#4285F4', -}; - type LoginButtonProps = ButtonHTMLAttributes & PropsWithChildren<{ componentType: LoginButtonComponentType; From e105497b3e94a810be3022ab9d7c00597a30c8a3 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Wed, 27 Nov 2024 02:11:29 +0900 Subject: [PATCH 069/129] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20-=20=EC=B1=84=ED=8C=85=20=ED=8F=BC=EA=B3=BC=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=20=EC=95=84=EC=9D=B4=ED=85=9C=20=EA=B0=84=20?= =?UTF-8?q?=EA=B0=84=EA=B2=A9=20=EC=A1=B0=EC=A0=88=20(=EC=B1=84=ED=8C=85?= =?UTF-8?q?=20=EC=A0=84=EC=B2=B4=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0)=20#280=20-=20=EC=83=88=EB=A1=9C=EC=9A=B4=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=20=EC=A1=B4=EC=9E=AC=20=EC=8B=9C=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EB=90=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20-=20=EA=B0=81=20=EC=B1=84=ED=8C=85=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=ED=85=9C=20=EB=A9=94=EB=AA=A8=EC=9D=B4=EC=A0=9C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20-=20=ED=99=94=EB=A9=B4=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=ED=9B=84=20=EC=9E=AC=EB=B0=A9=EB=AC=B8=20=EC=8B=9C?= =?UTF-8?q?=20=EC=B1=84=ED=8C=85=20=EC=9C=A0=EC=A7=80=EB=90=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/configs/tailwind.constant.ts | 2 +- .../src/app/(domain)/features/live/Chat.tsx | 118 ++++++++++++++---- .../(domain)/features/live/LiveSection.tsx | 28 ++--- client/src/app/providers/LiveProvider.tsx | 6 +- client/src/hooks/useUser.ts | 6 +- 5 files changed, 120 insertions(+), 40 deletions(-) diff --git a/client/configs/tailwind.constant.ts b/client/configs/tailwind.constant.ts index df428168..1f54e90e 100644 --- a/client/configs/tailwind.constant.ts +++ b/client/configs/tailwind.constant.ts @@ -17,4 +17,4 @@ export const SEARCH_WIDTH_WIDE = 'calc(100% - 58.125rem)'; // 930px = 58.125rem export const CHAT_FORM_HEIGHT = '5rem'; export const CHAT_HEADER_HEIGHT = '2.75rem'; -export const CAHT_HEIGHT = `calc(${LIVE_SECTION_DEFAULT_HEIGHT} - ${CHAT_HEADER_HEIGHT} - ${CHAT_FORM_HEIGHT})`; +export const CAHT_HEIGHT = `calc(${LIVE_SECTION_DEFAULT_HEIGHT} - ${CHAT_HEADER_HEIGHT} - ${CHAT_FORM_HEIGHT} - 1.5rem)`; diff --git a/client/src/app/(domain)/features/live/Chat.tsx b/client/src/app/(domain)/features/live/Chat.tsx index 4b811895..1b13da03 100644 --- a/client/src/app/(domain)/features/live/Chat.tsx +++ b/client/src/app/(domain)/features/live/Chat.tsx @@ -5,7 +5,16 @@ import useLiveContext from '@hooks/useLiveContext'; import useUser from '@hooks/useUser'; import { SOCKET_EVENT } from '@libs/constants'; import clsx from 'clsx'; -import { memo, type MutableRefObject, type ReactNode, useCallback, useEffect, useRef, useState } from 'react'; +import { + ChangeEvent, + memo, + type MutableRefObject, + type ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import { io, type Socket } from 'socket.io-client'; type ChatType = { @@ -29,6 +38,7 @@ type Props = { }; const ChatWrapper = ({ children }: Props) => { + const { isLivePage } = useLiveContext(); const { broadcastId } = useLiveContext(); const { loggedinUser } = useUser(); const [chatList, setChatList] = useState([]); @@ -103,7 +113,16 @@ const ChatWrapper = ({ children }: Props) => { ); return ( -
+ } + > + {children} + + + ); +}; + +const ErrorFallback = ({ isLivePage }: { isLivePage: boolean }) => { + if (!isLivePage) return null; + + return ; +}; + const PlaylistFetcher = ({ broadcastId, children }: PropsWithChildren<{ broadcastId: string }>) => { const { injectPlaylistData } = useLiveContext(); const { data } = useSuspenseQuery({ From 43269936ba7d698b1aca77f564a3ee64e84df0be Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Wed, 27 Nov 2024 21:18:14 +0900 Subject: [PATCH 076/129] feat: AuthLoading --- client/src/app/(auth)/features/AuthLoading.tsx | 11 +++++++++++ .../(auth)/github/callback/features/AuthGithub.tsx | 3 ++- .../(auth)/google/callback/features/AuthGoogle.tsx | 3 ++- .../app/(auth)/naver/callback/features/AuthNaver.tsx | 3 ++- client/src/app/(domain)/features/live/LiveSection.tsx | 1 - 5 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 client/src/app/(auth)/features/AuthLoading.tsx diff --git a/client/src/app/(auth)/features/AuthLoading.tsx b/client/src/app/(auth)/features/AuthLoading.tsx new file mode 100644 index 00000000..9eaeb48f --- /dev/null +++ b/client/src/app/(auth)/features/AuthLoading.tsx @@ -0,0 +1,11 @@ +const AuthLoading = () => { + return ( +
+
+

인증 중...

+
+
+ ); +}; + +export default AuthLoading; diff --git a/client/src/app/(auth)/github/callback/features/AuthGithub.tsx b/client/src/app/(auth)/github/callback/features/AuthGithub.tsx index 5558b1d1..4c2680e9 100644 --- a/client/src/app/(auth)/github/callback/features/AuthGithub.tsx +++ b/client/src/app/(auth)/github/callback/features/AuthGithub.tsx @@ -4,6 +4,7 @@ import useInternalRouter from '@hooks/useInternalRouter'; import { authenticateByGithub } from '@libs/actions'; import { useEffect } from 'react'; import useUser from '@hooks/useUser'; +import AuthLoading from '@app/(auth)/features/AuthLoading'; type Props = { authCode: string; @@ -33,7 +34,7 @@ const AuthGithub = ({ authCode }: Props) => { isValidEffect = false; }; }, [authCode, replace, saveUserSession]); - return
인증 중...
; + return ; }; export default AuthGithub; diff --git a/client/src/app/(auth)/google/callback/features/AuthGoogle.tsx b/client/src/app/(auth)/google/callback/features/AuthGoogle.tsx index 4f727b55..50cb8378 100644 --- a/client/src/app/(auth)/google/callback/features/AuthGoogle.tsx +++ b/client/src/app/(auth)/google/callback/features/AuthGoogle.tsx @@ -4,6 +4,7 @@ import useInternalRouter from '@hooks/useInternalRouter'; import { authenticateByGoogle } from '@libs/actions'; import { useEffect } from 'react'; import useUser from '@hooks/useUser'; +import AuthLoading from '@app/(auth)/features/AuthLoading'; type Props = { authCode: string; @@ -33,7 +34,7 @@ const AuthGoogle = ({ authCode }: Props) => { isValidEffect = false; }; }, [authCode, replace, saveUserSession]); - return
인증 중...
; + return ; }; export default AuthGoogle; diff --git a/client/src/app/(auth)/naver/callback/features/AuthNaver.tsx b/client/src/app/(auth)/naver/callback/features/AuthNaver.tsx index 6deb75f4..3cd2d013 100644 --- a/client/src/app/(auth)/naver/callback/features/AuthNaver.tsx +++ b/client/src/app/(auth)/naver/callback/features/AuthNaver.tsx @@ -4,6 +4,7 @@ import useInternalRouter from '@hooks/useInternalRouter'; import { authenticateByGithub, authenticateByNaver } from '@libs/actions'; import { useEffect } from 'react'; import useUser from '@hooks/useUser'; +import AuthLoading from '@app/(auth)/features/AuthLoading'; type Props = { authCode: string; @@ -37,7 +38,7 @@ const AuthNaver = ({ authCode, authState }: Props) => { isValidEffect = false; }; }, [authCode, authState, replace, saveUserSession]); - return
인증 중...
; + return ; }; export default AuthNaver; diff --git a/client/src/app/(domain)/features/live/LiveSection.tsx b/client/src/app/(domain)/features/live/LiveSection.tsx index 035c1d1e..b49ca13c 100644 --- a/client/src/app/(domain)/features/live/LiveSection.tsx +++ b/client/src/app/(domain)/features/live/LiveSection.tsx @@ -2,7 +2,6 @@ import Live from './Live'; import useLiveContext from '@hooks/useLiveContext'; -import NoLiveContent from './NoLiveContent'; import clsx from 'clsx'; import { type PropsWithChildren } from 'react'; import VideoController from './VideoController'; From 045efb8610814d295678eecbfa6626c2adb1d132 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Wed, 27 Nov 2024 21:56:14 +0900 Subject: [PATCH 077/129] =?UTF-8?q?fix:=20HLS=20=EC=97=90=EB=9F=AC,=20?= =?UTF-8?q?=EB=A1=9C=EB=94=A9,=20=EB=B2=84=ED=8D=BC=EB=A7=81=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/(domain)/features/live/Live.tsx | 43 ++++++++- .../(domain)/features/live/LiveSection.tsx | 5 +- client/src/app/components/layout/Search.tsx | 2 +- client/src/hooks/useHls.ts | 89 +++++++++++++++---- 4 files changed, 116 insertions(+), 23 deletions(-) diff --git a/client/src/app/(domain)/features/live/Live.tsx b/client/src/app/(domain)/features/live/Live.tsx index 3f15d49b..d8d56e6c 100644 --- a/client/src/app/(domain)/features/live/Live.tsx +++ b/client/src/app/(domain)/features/live/Live.tsx @@ -35,6 +35,9 @@ type ChildrenArgs = { toggleMute: () => void; handleChangeVolume: (e: ChangeEvent) => void; updateVolume: (value: number) => void; + isBuffering: boolean; + isError: boolean; + isLoading: boolean; }; type Props = { @@ -43,7 +46,7 @@ type Props = { }; const LiveController = ({ children, liveUrl }: Props) => { - const videoRef = useRef(null); + const videoRef = useRef(null); const videoWrapperRef = useRef(null); const [volume, setVolume] = useState(0); const [savedVolume, setSavedVolume] = useState(1); @@ -80,7 +83,7 @@ const LiveController = ({ children, liveUrl }: Props) => { if (!nextVolume) setSavedVolume(nextVolume); }; - useHls({ videoRef, liveUrl }); + const { isBuffering, isError, isLoading } = useHls({ videoRef, liveUrl }); useEffect(() => { const playVideo = async () => { @@ -126,6 +129,9 @@ const LiveController = ({ children, liveUrl }: Props) => { togglePip, handleChangeVolume, updateVolume, + isBuffering, + isError, + isLoading, }); }; @@ -156,7 +162,13 @@ const VideoWrapper = forwardRef( }, ); -const Video = forwardRef(({}: {}, ref: ForwardedRef) => { +type VideoProps = { + isBuffering: boolean; + isError: boolean; + isLoading: boolean; +}; + +const Video = forwardRef(({ isBuffering, isError, isLoading }: VideoProps, ref: ForwardedRef) => { return (
); }); +const Buffering = () => { + return ( +
+

비디오 청크를 정성들여 만드는 중...

+
+ ); +}; + +const Loading = () => { + return ( +
+

로딩 중...

+
+ ); +}; + +const Error = () => { + return ( +
+

비디오를 불러오는 중에 에러가 발생했어요.

+
+ ); +}; + const Live = Object.assign(LiveController, { Wrapper: LiveWrapper, VideoWrapper, diff --git a/client/src/app/(domain)/features/live/LiveSection.tsx b/client/src/app/(domain)/features/live/LiveSection.tsx index b49ca13c..b1de1f3d 100644 --- a/client/src/app/(domain)/features/live/LiveSection.tsx +++ b/client/src/app/(domain)/features/live/LiveSection.tsx @@ -77,10 +77,13 @@ const LiveSection = () => { exitFullscreen, volume, updateVolume, + isBuffering, + isError, + isLoading, }) => ( - + { const resetInput = () => setInput(''); const handleSubmit = () => { resetInput(); - alert('검색어: ' + input); + alert('안녕하세요, FUNCH입니다. ^o^\n검색 기능은 준비 중이에요.'); }; return ( diff --git a/client/src/hooks/useHls.ts b/client/src/hooks/useHls.ts index 46ea7d5b..2015d03a 100644 --- a/client/src/hooks/useHls.ts +++ b/client/src/hooks/useHls.ts @@ -1,25 +1,78 @@ import Hls from 'hls.js'; -import { type RefObject, useEffect } from 'react'; +import { type MutableRefObject, useEffect, useRef, useState } from 'react'; + +const useHls = ({ videoRef, liveUrl }: { videoRef: MutableRefObject; liveUrl: string }) => { + const [isLoading, setIsLoading] = useState(true); + const [isBuffering, setIsBuffering] = useState(false); + const [isError, setIsError] = useState(false); + const hlsRef = useRef(null); -const useHls = ({ videoRef, liveUrl }: { videoRef: RefObject; liveUrl: string }) => { useEffect(() => { - if (!videoRef.current) return; - if (Hls.isSupported()) { - const hls = new Hls(); - hls.loadSource(liveUrl); - hls.attachMedia(videoRef.current); - - hls.on(Hls.Events.MANIFEST_PARSED, () => { - videoRef.current!.play(); - }); - return () => hls.destroy(); - } else if (videoRef.current!.canPlayType('application/vnd.apple.mpegurl')) { - videoRef.current.src = liveUrl; - videoRef.current.addEventListener('loadedmetadata', () => { - videoRef.current!.play(); - }); - } + const handleWaiting = () => { + setIsBuffering(true); + }; + const handlePlaying = () => { + setIsBuffering(false); + }; + + const addEventListeners = () => { + if (!videoRef.current) return; + videoRef.current.addEventListener('waiting', handleWaiting); + videoRef.current.addEventListener('playing', handlePlaying); + }; + const removeEventListeners = () => { + if (!videoRef.current) return; + videoRef.current.removeEventListener('waiting', handleWaiting); + videoRef.current.removeEventListener('playing', handlePlaying); + }; + + const init = () => { + setIsLoading(false); + setIsBuffering(false); + setIsError(false); + if (!videoRef.current) return; + if (Hls.isSupported()) { + hlsRef.current = new Hls({ + startLevel: -1, + backBufferLength: 0, + liveSyncDuration: 1, + liveMaxLatencyDuration: 2, + liveDurationInfinity: true, + maxBufferHole: 1, + }); + hlsRef.current.loadSource(liveUrl); + hlsRef.current.attachMedia(videoRef.current); + + hlsRef.current.on(Hls.Events.MANIFEST_PARSED, () => { + videoRef.current!.play(); + }); + + addEventListeners(); + } else if (videoRef.current!.canPlayType('application/vnd.apple.mpegurl')) { + videoRef.current.src = liveUrl; + videoRef.current.addEventListener('loadedmetadata', () => { + videoRef.current!.play(); + }); + + addEventListeners(); + } else { + setIsError(true); + } + + setIsLoading(false); + }; + + init(); + + return () => { + if (hlsRef.current) { + hlsRef.current.destroy(); + } + removeEventListeners(); + }; }, [liveUrl, videoRef]); + + return { isBuffering, isError, isLoading }; }; export default useHls; From 49628d3ad5bc68efd18ffe99e339aa3409c8819e Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Wed, 27 Nov 2024 22:06:19 +0900 Subject: [PATCH 078/129] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/__test__/actions.test.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/client/src/__test__/actions.test.ts b/client/src/__test__/actions.test.ts index 0023bb9c..76692370 100644 --- a/client/src/__test__/actions.test.ts +++ b/client/src/__test__/actions.test.ts @@ -1,4 +1,4 @@ -import { getLiveList, getPlaylist, getSuggestedLiveList, authenticate } from '@libs/actions'; +import { getLiveList, getPlaylist, getSuggestedLiveList } from '@libs/actions'; import { mockedBroadcasts } from '@mocks/broadcasts'; import { describe, expect, test } from 'vitest'; @@ -26,13 +26,4 @@ describe('actions', () => { expect(suggestedList).toStrictEqual(mockedBroadcasts); }); - test('should authenticate user', async () => { - const code = 'auth-code'; - const result = await authenticate(code); - - expect(result.accessToken).toBeDefined(); - expect(result.user.name).toBeDefined(); - expect(result.user.profileImageUrl).toBeDefined(); - expect(result.user.broadcastId).toBeDefined(); - }); }); From 3d8c47f16f4a3260895c6c4962899c59a633a8bf Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Wed, 27 Nov 2024 22:36:10 +0900 Subject: [PATCH 079/129] =?UTF-8?q?fix:=20=EB=B9=84=EB=94=94=EC=98=A4=20?= =?UTF-8?q?=EB=B2=84=ED=8D=BC=EB=A7=81=20=EB=B0=B0=EA=B2=BD=EC=83=89=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=88=20=EB=B0=8F=20sharp=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/package-lock.json | 461 +++++++++++++++++- client/package.json | 1 + .../src/app/(domain)/features/live/Live.tsx | 12 +- .../app/(domain)/features/live/LiveInfo.tsx | 31 +- client/tailwind.config.ts | 1 + 5 files changed, 489 insertions(+), 17 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 134a4da6..931fb3da 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -15,6 +15,7 @@ "next": "14.2.16", "react": "^18", "react-dom": "^18", + "sharp": "^0.33.5", "socket.io-client": "^4.8.1" }, "devDependencies": { @@ -405,6 +406,16 @@ "tough-cookie": "^4.1.4" } }, + "node_modules/@emnapi/runtime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -874,6 +885,367 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@inquirer/confirm": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.0.1.tgz", @@ -2835,11 +3207,23 @@ "node": ">=6" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2852,9 +3236,18 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3113,6 +3506,15 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -4643,6 +5045,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/is-async-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", @@ -6644,7 +7052,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6687,6 +7094,45 @@ "node": ">= 0.4" } }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -6748,6 +7194,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/socket.io-client": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", diff --git a/client/package.json b/client/package.json index b547aca9..72a44237 100644 --- a/client/package.json +++ b/client/package.json @@ -18,6 +18,7 @@ "next": "14.2.16", "react": "^18", "react-dom": "^18", + "sharp": "^0.33.5", "socket.io-client": "^4.8.1" }, "devDependencies": { diff --git a/client/src/app/(domain)/features/live/Live.tsx b/client/src/app/(domain)/features/live/Live.tsx index d8d56e6c..b8fd5306 100644 --- a/client/src/app/(domain)/features/live/Live.tsx +++ b/client/src/app/(domain)/features/live/Live.tsx @@ -184,24 +184,24 @@ const Video = forwardRef(({ isBuffering, isError, isLoading }: VideoProps, ref: const Buffering = () => { return ( -
-

비디오 청크를 정성들여 만드는 중...

+
+

비디오 청크를 정성들여 만드는 중...

); }; const Loading = () => { return ( -
-

로딩 중...

+
+

로딩 중...

); }; const Error = () => { return ( -
-

비디오를 불러오는 중에 에러가 발생했어요.

+
+

비디오를 불러오는 중에 에러가 발생했어요.

); }; diff --git a/client/src/app/(domain)/features/live/LiveInfo.tsx b/client/src/app/(domain)/features/live/LiveInfo.tsx index 5dfdd985..9f75e3c4 100644 --- a/client/src/app/(domain)/features/live/LiveInfo.tsx +++ b/client/src/app/(domain)/features/live/LiveInfo.tsx @@ -5,7 +5,7 @@ import FullHeart from '@components/svgs/FullHeart'; import useLiveContext from '@hooks/useLiveContext'; import clsx from 'clsx'; import Image from 'next/image'; -import { memo, type PropsWithChildren, type ReactNode, useEffect, useRef, useState } from 'react'; +import { memo, type PropsWithChildren, type ReactNode, useEffect, useMemo, useRef, useState } from 'react'; import Badge from '@app/(domain)/features/Badge'; import { comma } from '@libs/formats'; import type { Broadcast } from '@libs/internalTypes'; @@ -97,20 +97,35 @@ const LiveInfoUserName = memo(({ userName }: { userName: string }) => { const LiveInfoTags = memo( ({ contentCategory, moodCategory, tags }: { contentCategory: string; moodCategory: string; tags: string[] }) => { - const allTags = [contentCategory, moodCategory, ...tags]; - + const memoizedTags = useMemo(() => tags, [tags]); return (
    - {allTags.map((tag, idx) => ( -
  • - {tag} -
  • - ))} + {contentCategory && {contentCategory}} + {moodCategory && {moodCategory}} + {memoizedTags.length > 0 && }
); }, ); +const Tags = memo(({ tags }: { tags: string[] }) => { + return ( + <> + {tags.map((tag, idx) => ( + {tag} + ))} + + ); +}); + +const Tag = memo(({ children }: PropsWithChildren) => { + return ( +
  • + {children} +
  • + ); +}); + const LiveInfoViewerCount = memo(({ viewerCount }: { viewerCount: number }) => { return {comma(viewerCount)}명 시청 중; }); diff --git a/client/tailwind.config.ts b/client/tailwind.config.ts index d4857b2e..0fb3ca97 100644 --- a/client/tailwind.config.ts +++ b/client/tailwind.config.ts @@ -142,6 +142,7 @@ const config: Config = { base: 'var(--bg-base)', strong: 'var(--bg-strong)', modal: 'rgba(0, 0, 0, .5)', + 'video-buffer': 'rgba(0, 0, 0, .7)', }, // Surface 색상 From c388fb813004b12ab81046ba48484a345eab53f6 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Thu, 28 Nov 2024 01:09:25 +0900 Subject: [PATCH 080/129] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EB=A7=A8?= =?UTF-8?q?=20=EC=9C=84=EC=97=90=20=EB=A9=94=EC=84=B8=EC=A7=80=20=EB=85=B8?= =?UTF-8?q?=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/app/(domain)/features/live/Chat.tsx | 10 +++++++++- client/src/app/components/Button.tsx | 1 + client/src/hooks/useHls.ts | 18 +++++++++++++----- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/client/src/app/(domain)/features/live/Chat.tsx b/client/src/app/(domain)/features/live/Chat.tsx index ce80fa3f..68084127 100644 --- a/client/src/app/(domain)/features/live/Chat.tsx +++ b/client/src/app/(domain)/features/live/Chat.tsx @@ -212,6 +212,11 @@ const ChatList = ({ chatList }: ChatListProps) => {
    +
    +

    + 아름다운 채팅 문화를 만들어보아요. +

    +
    {chatList.map((chat, index) => ( ))} @@ -261,6 +266,7 @@ const ChatForm = memo(({ socketRef, chatname, sendChat }: ChatFormProps) => { { e.preventDefault(); + if (!inputValue) return; sendChat({ socketRef, name: chatname, @@ -286,7 +292,9 @@ const ChatForm = memo(({ socketRef, chatname, sendChat }: ChatFormProps) => { />
    - +
    diff --git a/client/src/app/components/Button.tsx b/client/src/app/components/Button.tsx index 1882f49c..f5c96b20 100644 --- a/client/src/app/components/Button.tsx +++ b/client/src/app/components/Button.tsx @@ -11,6 +11,7 @@ const Button = ({ children, ...rest }: Props) => { 'funch-bold12', 'border-border-neutral-base rounded-lg border border-solid', 'text-content-neutral-primary hover:bg-surface-neutral-base bg-transparent', + 'disabled:opacity-35', )} {...rest} > diff --git a/client/src/hooks/useHls.ts b/client/src/hooks/useHls.ts index 2015d03a..b3efdd81 100644 --- a/client/src/hooks/useHls.ts +++ b/client/src/hooks/useHls.ts @@ -33,16 +33,24 @@ const useHls = ({ videoRef, liveUrl }: { videoRef: MutableRefObject { + if (data.type === Hls.ErrorTypes.NETWORK_ERROR) { + if (data.details === Hls.ErrorDetails.FRAG_LOAD_ERROR) { + // HLS.js가 다음 파일을 요청하도록 에러를 무시 + hlsRef.current!.startLoad(); + } + } + }); + hlsRef.current.on(Hls.Events.MANIFEST_PARSED, () => { videoRef.current!.play(); }); From 1761070954a5f33db9514047cd34128e413daa93 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Thu, 28 Nov 2024 01:19:37 +0900 Subject: [PATCH 081/129] =?UTF-8?q?fix:=20reconnect=20=EC=8B=9C=20error=20?= =?UTF-8?q?=EB=93=B1=20=EC=B4=88=EA=B8=B0=ED=99=94=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/app/(domain)/features/live/Chat.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/app/(domain)/features/live/Chat.tsx b/client/src/app/(domain)/features/live/Chat.tsx index 68084127..25a9d50b 100644 --- a/client/src/app/(domain)/features/live/Chat.tsx +++ b/client/src/app/(domain)/features/live/Chat.tsx @@ -97,6 +97,7 @@ const ChatWrapper = ({ children }: Props) => { socket.on(SOCKET_EVENT.CONNECT, () => { console.log('✅ SOCKET CONNECTED'); setIsLoading(false); + setIsError(false); }); socket.on(SOCKET_EVENT.CHAT, (receivedData: ChatType) => { From 81aa1526c728246feba3bfe7a2fcc47904f2bf94 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Thu, 28 Nov 2024 01:36:50 +0900 Subject: [PATCH 082/129] =?UTF-8?q?chore:=20error=20=EC=86=8C=EC=BC=93=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=EB=8F=84=20constants=EB=A1=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/app/(domain)/features/live/Chat.tsx | 10 +++++----- client/src/libs/constants.ts | 5 +++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/client/src/app/(domain)/features/live/Chat.tsx b/client/src/app/(domain)/features/live/Chat.tsx index 25a9d50b..e7ef5532 100644 --- a/client/src/app/(domain)/features/live/Chat.tsx +++ b/client/src/app/(domain)/features/live/Chat.tsx @@ -75,21 +75,21 @@ const ChatWrapper = ({ children }: Props) => { socketRef.current = socket; - socket.on('connect_error', () => { + socket.on(SOCKET_EVENT.CONNECT_ERROR, () => { console.log('❌ SOCKET CONNECT ERROR'); setIsError(true); }); - socket.on('connect_timeout', () => { + socket.on(SOCKET_EVENT.CONNECT_TIMEOUT, () => { console.log('❌ SOCKET CONNECT TIMEOUT'); setIsError(true); }); - socket.on('disconnect', () => { + socket.on(SOCKET_EVENT.DISCONNECT, () => { console.log('❌ SOCKET DISCONNECTED'); }); - socket.on('reconnect_failed', () => { + socket.on(SOCKET_EVENT.RECONNECT_FAILED, () => { console.log('❌ SOCKET RECONNECT FAILED'); }); - socket.on('error', () => { + socket.on(SOCKET_EVENT.ERROR, () => { console.log('❌ SOCKET ERROR'); setIsError(true); }); diff --git a/client/src/libs/constants.ts b/client/src/libs/constants.ts index c0121989..e9991827 100644 --- a/client/src/libs/constants.ts +++ b/client/src/libs/constants.ts @@ -26,6 +26,11 @@ export const SOCKET_EVENT = { CONNECT: 'connect' as const, CHAT: 'chat' as const, SET_ANONYMOUS_NAME: 'setAnonymousName' as const, + CONNECT_ERROR: 'connect_error' as const, + CONNECT_TIMEOUT: 'connect_timeout' as const, + DISCONNECT: 'disconnect' as const, + RECONNECT_FAILED: 'reconnect_failed' as const, + ERROR: 'error' as const, }; export const MOODS_CATEGORY = { From 2e7ca577fc77959f9680b27aa66ebfcf33869b5e Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Thu, 28 Nov 2024 01:40:54 +0900 Subject: [PATCH 083/129] =?UTF-8?q?chore:=20test=20=EB=82=B4=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20console.log=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/__test__/actions.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/src/__test__/actions.test.ts b/client/src/__test__/actions.test.ts index 76692370..d06fbea9 100644 --- a/client/src/__test__/actions.test.ts +++ b/client/src/__test__/actions.test.ts @@ -5,8 +5,6 @@ import { describe, expect, test } from 'vitest'; describe('actions', () => { test('should return mocked broadcasts', async () => { const result = await getLiveList(); - console.log(result); - console.log(mockedBroadcasts); expect(result).not.toBeNull(); expect(result).toStrictEqual(mockedBroadcasts.sort((a, b) => b.viewerCount - a.viewerCount)); }); From 8d9809cd394f5a90bbb4572bd759e35de2b8d0c5 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Thu, 28 Nov 2024 01:52:35 +0900 Subject: [PATCH 084/129] =?UTF-8?q?feat:=20=EB=AF=B8=EB=8B=88=20=ED=94=8C?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=96=B4=20=EC=83=81=EB=8B=A8=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=ED=85=8C=EB=91=90=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/app/(domain)/features/live/MiniPlayerController.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/app/(domain)/features/live/MiniPlayerController.tsx b/client/src/app/(domain)/features/live/MiniPlayerController.tsx index 06814acf..d9953374 100644 --- a/client/src/app/(domain)/features/live/MiniPlayerController.tsx +++ b/client/src/app/(domain)/features/live/MiniPlayerController.tsx @@ -19,7 +19,7 @@ const MiniPlayerController = () => { className={clsx( 'absolute bottom-full right-0 px-2.5 py-1', 'bg-surface-neutral-primary', - 'rounded-tl-xl rounded-tr-xl', + 'border-border-neutral-weak rounded-tl-xl rounded-tr-xl border-x border-t border-solid', )} >
    From 9e14be52355a40c08b0f6504a52ffb9409f7805f Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Thu, 28 Nov 2024 03:02:24 +0900 Subject: [PATCH 085/129] =?UTF-8?q?refactor:=20=ED=99=88=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=EC=9D=84=20=EC=84=9C=EB=B2=84=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EB=A1=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/__mocks__/broadcasts.ts | 2 +- .../(details)/contents/[code]/page.tsx | 4 +- .../(details)/moods/[code]/page.tsx | 4 +- .../(domain)/features/RecommendedLives.tsx | 80 ++++++++----------- .../features/RecommendedLivesRenderer.tsx | 23 ++++++ .../features/live/VideoController.tsx | 7 +- client/src/app/components/ErrorBoundary.tsx | 2 + client/src/app/components/livesGrid/Lives.tsx | 3 +- 8 files changed, 72 insertions(+), 53 deletions(-) create mode 100644 client/src/app/(domain)/features/RecommendedLivesRenderer.tsx diff --git a/client/src/__mocks__/broadcasts.ts b/client/src/__mocks__/broadcasts.ts index 24ca34d1..5c5fc31f 100644 --- a/client/src/__mocks__/broadcasts.ts +++ b/client/src/__mocks__/broadcasts.ts @@ -7,7 +7,7 @@ export const mockedBroadcasts: Broadcast[] = [ title: '[충격] 트럼프 당선', contentCategory: CONTENTS_CATEGORY.politics.CODE, moodCategory: MOODS_CATEGORY.calm.CODE, - tags: ['politics', 'election'], + tags: ['politics', 'election', 'trump', 'usa', 'president'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 10870, userName: '슈카월드', diff --git a/client/src/app/(domain)/categories/(details)/contents/[code]/page.tsx b/client/src/app/(domain)/categories/(details)/contents/[code]/page.tsx index 76d23530..588e1ac6 100644 --- a/client/src/app/(domain)/categories/(details)/contents/[code]/page.tsx +++ b/client/src/app/(domain)/categories/(details)/contents/[code]/page.tsx @@ -8,7 +8,9 @@ import CategoryLives from '@app/(domain)/categories/(details)/features/CategoryL const fetchData = async (code: string): Promise => { if (process.env.NODE_ENV !== 'production') return getBroadcastsByContentCategory(code); - const response = await fetch(`/api/live/category?content=${code}`, { + const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL; + + const response = await fetch(`${apiUrl}/live/category?content=${code}`, { cache: 'no-cache', }); diff --git a/client/src/app/(domain)/categories/(details)/moods/[code]/page.tsx b/client/src/app/(domain)/categories/(details)/moods/[code]/page.tsx index 0687f450..0853047d 100644 --- a/client/src/app/(domain)/categories/(details)/moods/[code]/page.tsx +++ b/client/src/app/(domain)/categories/(details)/moods/[code]/page.tsx @@ -8,7 +8,9 @@ import { Suspense } from 'react'; const fetchData = async (code: string): Promise => { if (process.env.NODE_ENV !== 'production') return getBroadcastsByMoodCategory(code); - const response = await fetch(`/api/live/category?mood=${code}`, { + const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL; + + const response = await fetch(`${apiUrl}/live/category?mood=${code}`, { cache: 'no-cache', }); diff --git a/client/src/app/(domain)/features/RecommendedLives.tsx b/client/src/app/(domain)/features/RecommendedLives.tsx index bbead2e7..a4baee88 100644 --- a/client/src/app/(domain)/features/RecommendedLives.tsx +++ b/client/src/app/(domain)/features/RecommendedLives.tsx @@ -1,66 +1,50 @@ -'use client'; - -// import DeemedLink from '@components/DeemedLink'; import clsx from 'clsx'; -import Lives from '@components/livesGrid/Lives'; -import { useEffect, useState } from 'react'; -import type { Broadcast } from '@libs/internalTypes'; -import { getLiveList } from '@libs/actions'; - -const RecommendedLives = () => { - const [isLoading, setIsLoading] = useState(true); - const [isError, setIsError] = useState(false); - const [lives, setLives] = useState([]); +import { mockedBroadcasts } from '@mocks/broadcasts'; +import RecommendedLivesRenderer from './RecommendedLivesRenderer'; +import { Suspense } from 'react'; +import { unstable_noStore as noStore } from 'next/cache'; +import ErrorBoundary from '@components/ErrorBoundary'; - useEffect(() => { - let isValidEffect = true; - const fetchLives = async () => { - try { - const fetchedLives = await getLiveList(); - setLives(fetchedLives); - setIsLoading(false); - } catch (err) { - if (!isValidEffect) return; - setLives([]); - setIsError(true); - } - }; +const fetchData = async () => { + if (process.env.NODE_ENV !== 'production') return mockedBroadcasts; - fetchLives(); + const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL; - return () => { - isValidEffect = false; - }; - }, []); + const response = await fetch(`${apiUrl}/live/list`, { + cache: 'no-cache', + }); - if (isError) { - return
    에러가 발생했습니다.
    ; + if (!response.ok) { + throw new Error('라이브 목록을 불러오는데 실패했어요.'); } - if (isLoading) { - return
    로딩 중...
    ; - } + const data = await response.json(); + + return data; +}; +const RecommendedLives = async () => { + noStore(); return (

    이 방송 어때요?

    - {/* */}
    - - {({ visibleLives, isExpanded, toggle }) => ( - <> - - {visibleLives.map((live, index) => ( - - ))} - - {lives.length > 3 && } - - )} - + 추천 목록을 불러올 수 없어요.

    } + > + 추천 목록을 불러오고 있어요.

    }> + +
    +
    ); }; +const RecommendedLivesFetcher = async () => { + const lives = await fetchData(); + + return ; +}; + export default RecommendedLives; diff --git a/client/src/app/(domain)/features/RecommendedLivesRenderer.tsx b/client/src/app/(domain)/features/RecommendedLivesRenderer.tsx new file mode 100644 index 00000000..902b7cd8 --- /dev/null +++ b/client/src/app/(domain)/features/RecommendedLivesRenderer.tsx @@ -0,0 +1,23 @@ +'use client'; + +import Lives from '@components/livesGrid/Lives'; +import type { Broadcast } from '@libs/internalTypes'; + +const RecommendedLivesRenderer = ({ lives }: { lives: Broadcast[] }) => { + return ( + + {({ visibleLives, isExpanded, toggle }) => ( + <> + + {visibleLives.map((live, index) => ( + + ))} + + {lives.length > 3 && } + + )} + + ); +}; + +export default RecommendedLivesRenderer; diff --git a/client/src/app/(domain)/features/live/VideoController.tsx b/client/src/app/(domain)/features/live/VideoController.tsx index 96ad8df6..e6a72cf6 100644 --- a/client/src/app/(domain)/features/live/VideoController.tsx +++ b/client/src/app/(domain)/features/live/VideoController.tsx @@ -195,7 +195,12 @@ const VolumeController = () => { setIsHidden(true); }} > - +
    {

    {live.title}

    {live.userName}

    - {live.contentCategory} + {live.contentCategory && {live.contentCategory}} + {live.moodCategory && {live.moodCategory}} {live.tags.map((tag, index) => ( {tag} ))} From 908267a8b903a3ca86271eb77a611ab6efce8551 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Thu, 28 Nov 2024 03:18:26 +0900 Subject: [PATCH 086/129] feat: not-found.tsx --- client/src/app/not-found.tsx | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 client/src/app/not-found.tsx diff --git a/client/src/app/not-found.tsx b/client/src/app/not-found.tsx new file mode 100644 index 00000000..4de29867 --- /dev/null +++ b/client/src/app/not-found.tsx @@ -0,0 +1,29 @@ +import FunchSvg from '@components/svgs/FunchSvg'; +import clsx from 'clsx'; +import Link from 'next/link'; + +const NotFound = () => { + return ( +
    +
    +
    + +
    +

    존재하지 않는 페이지예요.

    + + 홈으로 돌아가기 + +
    +
    + ); +}; + +export default NotFound; From 9f3d7ff8465f4ed7db00fba8cc7875497ff9c0cd Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Thu, 28 Nov 2024 03:25:08 +0900 Subject: [PATCH 087/129] feat: global-error.tsx --- client/src/app/global-error.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 client/src/app/global-error.tsx diff --git a/client/src/app/global-error.tsx b/client/src/app/global-error.tsx new file mode 100644 index 00000000..d61be4e6 --- /dev/null +++ b/client/src/app/global-error.tsx @@ -0,0 +1,23 @@ +'use client'; + +export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + return ( + + + FUNCH + + + + +

    뭔가 엄청난 일이 일어나고 있어요.

    +

    + 여기로 문의 주시면 빠르게 해결해 드릴게요. +

    +
    {error.message}
    +
    {error.stack}
    + {error.digest &&
    {error.digest}
    } + + + + ); +} From 44244d046df93081f12107e21018541f8cb88327 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Wed, 27 Nov 2024 20:37:04 +0900 Subject: [PATCH 088/129] =?UTF-8?q?feat:=20=ED=8C=94=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=EC=B1=84=EB=84=90(=EC=84=9C=EB=9E=8D)=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(domain)/features/live/LiveInfo.tsx | 6 +-- .../(domain)/features/live/LiveSection.tsx | 4 +- .../components/cabinet/CabinetContainer.tsx | 36 ++++++-------- ...{SuggestedList.tsx => CabinetItemList.tsx} | 48 +++++++++---------- .../app/providers/FollowingLivesProvider.tsx | 6 +-- client/src/app/studio/page.tsx | 2 +- 6 files changed, 48 insertions(+), 54 deletions(-) rename client/src/app/components/cabinet/{SuggestedList.tsx => CabinetItemList.tsx} (74%) diff --git a/client/src/app/(domain)/features/live/LiveInfo.tsx b/client/src/app/(domain)/features/live/LiveInfo.tsx index 9f75e3c4..9015b138 100644 --- a/client/src/app/(domain)/features/live/LiveInfo.tsx +++ b/client/src/app/(domain)/features/live/LiveInfo.tsx @@ -131,13 +131,13 @@ const LiveInfoViewerCount = memo(({ viewerCount }: { viewerCount: number }) => { }); type LiveInfoFollowToggleButtonProps = { - Ids: string[]; + ids: string[]; broadcastId: string; myId: string; }; -const LiveInfoFollowToggleButton = ({ Ids, broadcastId, myId }: LiveInfoFollowToggleButtonProps) => { - const isFollowed = Ids.includes(broadcastId); +const LiveInfoFollowToggleButton = ({ ids, broadcastId, myId }: LiveInfoFollowToggleButtonProps) => { + const isFollowed = ids.includes(broadcastId); const [followed, setFollowed] = useState(isFollowed); const followInfo = { diff --git a/client/src/app/(domain)/features/live/LiveSection.tsx b/client/src/app/(domain)/features/live/LiveSection.tsx index b1de1f3d..6837c66b 100644 --- a/client/src/app/(domain)/features/live/LiveSection.tsx +++ b/client/src/app/(domain)/features/live/LiveSection.tsx @@ -13,7 +13,7 @@ import useUserContext from '@hooks/useUserContext'; const LiveSection = () => { const { isLivePage, liveUrl } = useLiveContext(); - const { Ids } = useFollowingLives(); + const { ids } = useFollowingLives(); const { userSession } = useUserContext(); const myId = userSession?.user?.broadcastId || ''; @@ -128,7 +128,7 @@ const LiveSection = () => { /> - + )} diff --git a/client/src/app/components/cabinet/CabinetContainer.tsx b/client/src/app/components/cabinet/CabinetContainer.tsx index cd612b39..a89fa7b6 100644 --- a/client/src/app/components/cabinet/CabinetContainer.tsx +++ b/client/src/app/components/cabinet/CabinetContainer.tsx @@ -1,10 +1,10 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, PropsWithChildren } from 'react'; import CabinetLink from './CabinetLink'; import useDesktop from '@hooks/useDesktop'; import AccordionButton from '@components/AccordionButton'; -import SuggestedList from './SuggestedList'; +import CabinetItemList from './CabinetItemList'; import DesktopHeader from './DesktopHeader'; import { Broadcast } from '@libs/internalTypes'; import { getSuggestedLiveList } from '@libs/actions'; @@ -30,11 +30,16 @@ const CategoryNavigator = () => { }; const FollowNavigator = () => { - const title = '팔로우'; const [isExpanded, setIsExpanded] = useState(false); const [isFolded, setIsFolded] = useState(false); const { isDesktop } = useDesktop(); + useEffect(() => { + if (!isDesktop) { + setIsExpanded(true); + } + }, [isDesktop]); + const { lives, fetchLives } = useFollowingLives(); useEffect(() => { @@ -49,10 +54,10 @@ const FollowNavigator = () => { {isDesktop ? ( ) : ( - + 팔로우 )} - - {isDesktop && ( + + {isDesktop && lives.length > 5 && (
    setIsFolded((prev) => !prev)} />
    @@ -63,8 +68,6 @@ const FollowNavigator = () => { }; const SuggestedNavigator = () => { - const title = '추천'; - const [isExpanded, setIsExpanded] = useState(false); const [isFolded, setIsFolded] = useState(false); const [suggestedList, setSuggestedList] = useState([]); @@ -91,14 +94,9 @@ const SuggestedNavigator = () => { {isDesktop ? ( ) : ( - + 추천 )} - + {isDesktop && suggestedList.length > 5 && (
    setIsFolded((prev) => !prev)} /> @@ -109,14 +107,10 @@ const SuggestedNavigator = () => { ); }; -type HeaderProps = { - title: string; -}; - -const NavHeader = ({ title }: HeaderProps) => { +const NavHeader = ({ children }: PropsWithChildren) => { return (
    -

    {title}

    +

    {children}

    ); }; diff --git a/client/src/app/components/cabinet/SuggestedList.tsx b/client/src/app/components/cabinet/CabinetItemList.tsx similarity index 74% rename from client/src/app/components/cabinet/SuggestedList.tsx rename to client/src/app/components/cabinet/CabinetItemList.tsx index e6790d6d..f987a678 100644 --- a/client/src/app/components/cabinet/SuggestedList.tsx +++ b/client/src/app/components/cabinet/CabinetItemList.tsx @@ -6,15 +6,15 @@ import { useState, useEffect, useRef } from 'react'; import Link from 'next/link'; import { comma } from '@libs/formats'; -type SuggestedListProps = { +type ItemListProps = { isDesktop: boolean; isExpanded: boolean; isFolded: boolean; - suggestedList: Broadcast[]; + itemList: Broadcast[]; }; -const SuggestedList = ({ isDesktop, isExpanded, isFolded, suggestedList }: SuggestedListProps) => { - const foldedContent = suggestedList.slice(0, 5); +const CabinetItemList = ({ isDesktop, isExpanded, isFolded, itemList }: ItemListProps) => { + const foldedContent = itemList.slice(0, 5); return (
    @@ -22,14 +22,14 @@ const SuggestedList = ({ isDesktop, isExpanded, isFolded, suggestedList }: Sugge <> {isFolded ? ( <> - {suggestedList.map((suggest: Broadcast, key) => ( - + {itemList.map((item: Broadcast, key) => ( + ))} ) : ( <> - {foldedContent.map((suggest: Broadcast, key) => ( - + {foldedContent.map((item: Broadcast, key) => ( + ))} )} @@ -39,7 +39,7 @@ const SuggestedList = ({ isDesktop, isExpanded, isFolded, suggestedList }: Sugge ); }; -const SuggestedListItem = ({ suggest, isDesktop }: { suggest: Broadcast; isDesktop: boolean }) => { +const CabinetListItem = ({ item, isDesktop }: { item: Broadcast; isDesktop: boolean }) => { const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 }); const [isTooltipVisible, setTooltipVisible] = useState(false); @@ -75,7 +75,7 @@ const SuggestedListItem = ({ suggest, isDesktop }: { suggest: Broadcast; isDeskt <> {isDesktop ? (
    - +
    {suggest.title}
    -
    {suggest.userName}
    -
    {suggest.contentCategory}
    +
    {item.userName}
    +
    {item.contentCategory}

    - {'· ' + comma(suggest.viewerCount)} + {'· ' + comma(item.viewerCount)}

    @@ -106,21 +106,21 @@ const SuggestedListItem = ({ suggest, isDesktop }: { suggest: Broadcast; isDeskt className="text-content-neutral-primary border-neutral-weak bg-surface-neutral-strong funch-medium12 fixed flex h-20 w-60 items-center rounded-md p-4" style={{ top: tooltipPosition.top, left: tooltipPosition.left }} > - {suggest.title} + {item.title}
    )}
    ) : (
    - +
    {suggest.title}
    -
    {suggest.userName}
    +
    {item.userName}
    - {suggest.tags[0]} + {item.tags[0]}
    -
    {suggest.title}
    -
    {'· ' + comma(suggest.viewerCount)}
    +
    {item.title}
    +
    {'· ' + comma(item.viewerCount)}
    )}
    @@ -148,4 +148,4 @@ const SuggestedListItem = ({ suggest, isDesktop }: { suggest: Broadcast; isDeskt ); }; -export default SuggestedList; +export default CabinetItemList; diff --git a/client/src/app/providers/FollowingLivesProvider.tsx b/client/src/app/providers/FollowingLivesProvider.tsx index c06cfb2e..83f19412 100644 --- a/client/src/app/providers/FollowingLivesProvider.tsx +++ b/client/src/app/providers/FollowingLivesProvider.tsx @@ -6,7 +6,7 @@ import { Broadcast, User2 } from '@libs/internalTypes'; interface FollowingLivesContextType { lives: Broadcast[]; - Ids: string[]; + ids: string[]; offlines: User2[]; isLoading: boolean; isError: boolean; @@ -18,7 +18,7 @@ export const FollowingLivesContext = createContext { const [isLoading, setIsLoading] = useState(true); const [isError, setIsError] = useState(false); - const [Ids, setIds] = useState([]); + const [ids, setIds] = useState([]); const [lives, setLives] = useState([]); const [offlines, setOfflines] = useState([]); @@ -52,7 +52,7 @@ export const FollowingLivesProvider = ({ children }: PropsWithChildren) => { ; const StudioSettingWrapper = ({ children, ...rest }: Props) => { return ( -
    +
    {children}
    ); From 1aaaec6d93c81dc9241c3030e1102d884f5e4560 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Wed, 27 Nov 2024 21:17:02 +0900 Subject: [PATCH 089/129] =?UTF-8?q?fix:=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=EA=B0=84=20=ED=8C=94=EB=A1=9C=EC=9A=B0?= =?UTF-8?q?=EA=B0=80=20=EB=90=98=EC=96=B4=EC=9E=88=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/__mocks__/broadcasts.ts | 198 ++++++++++++++++++ client/src/__mocks__/follow.ts | 106 ++++------ .../app/(domain)/features/live/LiveInfo.tsx | 7 +- .../components/cabinet/CabinetContainer.tsx | 8 +- 4 files changed, 250 insertions(+), 69 deletions(-) diff --git a/client/src/__mocks__/broadcasts.ts b/client/src/__mocks__/broadcasts.ts index 5c5fc31f..bb6c100c 100644 --- a/client/src/__mocks__/broadcasts.ts +++ b/client/src/__mocks__/broadcasts.ts @@ -24,6 +24,204 @@ export const mockedBroadcasts: Broadcast[] = [ userName: '슈카월드', profileImageUrl: 'https://via.placeholder.com/150', }, + { + broadcastId: 'bbb', + title: '[데모 공유] 팀 무지개 치즈 3주차 발표', + contentCategory: CONTENTS_CATEGORY.develop.CODE, + moodCategory: MOODS_CATEGORY.energetic.CODE, + tags: ['funch', 'boostcamp'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 100, + userName: '짜왕', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'ccc', + title: '고양이 냥냥이 냥냥냥이', + contentCategory: CONTENTS_CATEGORY.dailylife.CODE, + moodCategory: MOODS_CATEGORY.energetic.CODE, + tags: ['cat', 'cute'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 300, + userName: '모카', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'ddd', + title: '방송 제목', + contentCategory: CONTENTS_CATEGORY.cook.CODE, + moodCategory: MOODS_CATEGORY.depressed.CODE, + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 200, + userName: '토끼', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'eee', + title: '방송 제목', + contentCategory: CONTENTS_CATEGORY.game.CODE, + moodCategory: MOODS_CATEGORY.depressed.CODE, + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 300, + userName: '펭귄', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'fff', + title: '방송 제목', + contentCategory: CONTENTS_CATEGORY.economy.CODE, + moodCategory: MOODS_CATEGORY.funny.CODE, + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 400, + userName: '오리', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'ggg', + title: '방송 제목', + contentCategory: CONTENTS_CATEGORY.fishing.CODE, + moodCategory: MOODS_CATEGORY.getking.CODE, + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 500, + userName: '앵무새', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'hhh', + title: '방송 제목', + contentCategory: CONTENTS_CATEGORY.horror.CODE, + moodCategory: MOODS_CATEGORY.getking.CODE, + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 600, + userName: '거북이', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'iii', + title: '방송 제목', + contentCategory: CONTENTS_CATEGORY.house.CODE, + moodCategory: MOODS_CATEGORY.happy.CODE, + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 700, + userName: '물고기', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'jjj', + title: '방송 제목', + contentCategory: CONTENTS_CATEGORY.music.CODE, + moodCategory: MOODS_CATEGORY.interesting.CODE, + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 800, + userName: '물소', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'jjasdgj', + title: '방송 제목', + contentCategory: CONTENTS_CATEGORY.mukbang.CODE, + moodCategory: MOODS_CATEGORY.lonely.CODE, + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 800, + userName: '물소', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'sss', + title: '방송 제목', + contentCategory: CONTENTS_CATEGORY.news.CODE, + moodCategory: MOODS_CATEGORY.lonely.CODE, + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 900, + userName: '말', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'tadg', + title: '방송 제목', + contentCategory: CONTENTS_CATEGORY.outdoor.CODE, + moodCategory: MOODS_CATEGORY.unknown.CODE, + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 1000, + userName: '사자', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'tasdgdg', + title: '방송 제목sdg', + contentCategory: CONTENTS_CATEGORY.outdoor.CODE, + moodCategory: MOODS_CATEGORY.unknown.CODE, + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 1000, + userName: '사자', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'tasdgs21245dg', + title: '방송 제목sdg', + contentCategory: CONTENTS_CATEGORY.study.CODE, + moodCategory: MOODS_CATEGORY.unknown.CODE, + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 1000, + userName: '사자', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'tasdgs2125dg', + title: '방송 제목sdg', + contentCategory: CONTENTS_CATEGORY.talk.CODE, + moodCategory: MOODS_CATEGORY.unknown.CODE, + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 1000, + userName: '사자', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'tdgs2125dg', + title: '방송 제목sdg', + contentCategory: CONTENTS_CATEGORY.travel.CODE, + moodCategory: MOODS_CATEGORY.interesting.CODE, + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 1000, + userName: '사자', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'tbbbbbdgs2125dg', + title: '방송 제목sdg', + contentCategory: CONTENTS_CATEGORY.virtual.CODE, + moodCategory: MOODS_CATEGORY.getking.CODE, + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 1000, + userName: '사자', + profileImageUrl: 'https://via.placeholder.com/150', + }, + { + broadcastId: 'tdaaags2125dg', + title: '방송 제목sdg', + contentCategory: CONTENTS_CATEGORY.virtual.CODE, + moodCategory: MOODS_CATEGORY.getking.CODE, + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 1000, + userName: '사자', + profileImageUrl: 'https://via.placeholder.com/150', + }, ]; export const getBroadcastsByContentCategory = (contentCategory: string) => { diff --git a/client/src/__mocks__/follow.ts b/client/src/__mocks__/follow.ts index b4bae3e2..76125cd3 100644 --- a/client/src/__mocks__/follow.ts +++ b/client/src/__mocks__/follow.ts @@ -1,100 +1,80 @@ +import { CONTENTS_CATEGORY, MOODS_CATEGORY } from '@libs/constants'; + export const mockedFollowingList = { onAir: [ { playlistUrl: 'https://via.placeholder.com/150', broadCastData: { - broadcastId: 'ccc', - broadcastPath: 'example-broadcast-id/internal-path', - title: '펀치의 라이브 방송', - contentCategory: 'FUNCH', - moodCategory: '행복한', - tags: ['펀치', '무지개 치즈'], + broadcastId: 'sss', + title: '방송 제목', + contentCategory: CONTENTS_CATEGORY.news.CODE, + moodCategory: MOODS_CATEGORY.lonely.CODE, + tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', - viewerCount: 20, - userName: '지존펀치', + viewerCount: 900, + userName: '말', profileImageUrl: 'https://via.placeholder.com/150', }, }, { playlistUrl: 'https://via.placeholder.com/150', broadCastData: { - broadcastId: 'bbb', - broadcastPath: 'example-broadcast-id/internal-path', - title: '펀치의 라이브 방송', - contentCategory: 'FUNCH', - moodCategory: '행복한', - tags: ['부스트캠프', '펀치', '무지개 치즈'], + broadcastId: 'tadg', + title: '방송 제목', + contentCategory: CONTENTS_CATEGORY.outdoor.CODE, + moodCategory: MOODS_CATEGORY.unknown.CODE, + tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', - viewerCount: 20, - userName: '지존펀치', + viewerCount: 1000, + userName: '사자', profileImageUrl: 'https://via.placeholder.com/150', }, }, { playlistUrl: 'https://via.placeholder.com/150', broadCastData: { - broadcastId: 'aaa', - broadcastPath: 'example-broadcast-id/internal-path', - title: '펀치의 라이브 방송', - contentCategory: 'FUNCH', - moodCategory: '행복한', - tags: ['부스트캠프', '펀치', '무지개 치즈'], + broadcastId: 'tasdgdg', + title: '방송 제목sdg', + contentCategory: CONTENTS_CATEGORY.outdoor.CODE, + moodCategory: MOODS_CATEGORY.unknown.CODE, + tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', - viewerCount: 20, - userName: '지존펀치', - profileImageUrl: 'https://via.placeholder.com/150', - }, - }, - { - playlistUrl: 'https://via.placeholder.com/150', - broadCastData: { - broadcastId: 'ddd', - broadcastPath: 'example-broadcast-id/internal-path', - title: '펀치의 라이브 방송', - contentCategory: 'FUNCH', - moodCategory: '행복한', - tags: ['politics', 'election'], - thumbnailUrl: 'https://via.placeholder.com/150', - viewerCount: 20, - userName: '지존펀치', + viewerCount: 1000, + userName: '사자', profileImageUrl: 'https://via.placeholder.com/150', }, }, ], offAir: [ { - name: '섬세한장어1405', - profile_image: 'https://avatars.githubusercontent.com/u/103445254?v=4', - broadcast_id: '3fd04e87-57b3-46da-b5be-8c97fa72cf22', - follower_count: 0, + name: '여행의달인', + profile_image: 'https://via.placeholder.com/150', + broadcast_id: '9f3a-4b2c-8d1e-6f5g', + follower_count: 1205, }, { - name: 'zzawang', - profile_image: - 'https://kr.object.ncloudstorage.com/funch-storage/profile/profile_67482aed-3098-4c31-94ee-52110e9073cc.png', - broadcast_id: '67482aed-3098-4c31-94ee-52110e9073cc', - follower_count: 278, + name: '재테크전문가', + profile_image: 'https://via.placeholder.com/150', + broadcast_id: 'a7b9-2c3d-4e5f-6g7h', + follower_count: 754, }, { - name: 'zzawang', - profile_image: - 'https://kr.object.ncloudstorage.com/funch-storage/profile/profile_67482aed-3098-4c31-94ee-52110e9073cc.png', - broadcast_id: '67482aed-3098-4c31-94ee-52110e9073cc', - follower_count: 278, + name: '피트니스트레이너', + profile_image: 'hhttps://via.placeholder.com/150', + broadcast_id: 'h2j4-5k6l-7m8n-9p0q', + follower_count: 512, }, { - name: 'zzawang', - profile_image: - 'https://kr.object.ncloudstorage.com/funch-storage/profile/profile_67482aed-3098-4c31-94ee-52110e9073cc.png', - broadcast_id: '67482aed-3098-4c31-94ee-52110e9073cc', - follower_count: 278, + name: '뷰티인플루언서', + profile_image: 'https://via.placeholder.com/150', + broadcast_id: 'r3s5-6t7u-8v9w-0x1y', + follower_count: 987, }, { - name: 'zzawang', - profile_image: - 'https://kr.object.ncloudstorage.com/funch-storage/profile/profile_67482aed-3098-4c31-94ee-52110e9073cc.png', - broadcast_id: '67482aed-3098-4c31-94ee-52110e9073cc', - follower_count: 278, + name: '공부의신', + profile_image: 'https://via.placeholder.com/150', + broadcast_id: 'z2a4-5b6c-7d8e-9f0g', + follower_count: 345, }, ], }; diff --git a/client/src/app/(domain)/features/live/LiveInfo.tsx b/client/src/app/(domain)/features/live/LiveInfo.tsx index 9015b138..4114a9c5 100644 --- a/client/src/app/(domain)/features/live/LiveInfo.tsx +++ b/client/src/app/(domain)/features/live/LiveInfo.tsx @@ -137,14 +137,17 @@ type LiveInfoFollowToggleButtonProps = { }; const LiveInfoFollowToggleButton = ({ ids, broadcastId, myId }: LiveInfoFollowToggleButtonProps) => { - const isFollowed = ids.includes(broadcastId); - const [followed, setFollowed] = useState(isFollowed); + const [followed, setFollowed] = useState(false); const followInfo = { follower: myId, following: broadcastId, }; + useEffect(() => { + setFollowed(ids.includes(broadcastId)); + }, [ids, broadcastId]); + const fetchFollow = async () => { if (!followed) { await makeFollow(followInfo); diff --git a/client/src/app/components/cabinet/CabinetContainer.tsx b/client/src/app/components/cabinet/CabinetContainer.tsx index a89fa7b6..cded555d 100644 --- a/client/src/app/components/cabinet/CabinetContainer.tsx +++ b/client/src/app/components/cabinet/CabinetContainer.tsx @@ -57,8 +57,8 @@ const FollowNavigator = () => { 팔로우 )} - {isDesktop && lives.length > 5 && ( -
    + {isDesktop && isExpanded && lives.length > 5 && ( +
    setIsFolded((prev) => !prev)} />
    )} @@ -97,8 +97,8 @@ const SuggestedNavigator = () => { 추천 )} - {isDesktop && suggestedList.length > 5 && ( -
    + {isDesktop && isExpanded && suggestedList.length > 5 && ( +
    setIsFolded((prev) => !prev)} />
    )} From b960ed4f6580354a52759861045d72786bf17bd2 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Wed, 27 Nov 2024 21:23:09 +0900 Subject: [PATCH 090/129] =?UTF-8?q?fix:=20=EC=84=9C=EB=9E=8D=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=90=98=EC=96=B4?= =?UTF-8?q?=EC=9E=88=EC=9D=84=EB=95=8C=EB=A7=8C=20=ED=8C=94=EB=A1=9C?= =?UTF-8?q?=EC=9E=89=20=EC=B1=84=EB=84=90=20=EB=B3=B4=EC=9D=B4=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(domain)/following/features/FollowingOffair.tsx | 2 +- client/src/app/components/cabinet/CabinetContainer.tsx | 7 ++++--- client/src/app/components/studio/StudioCategoryCard.tsx | 2 +- client/src/app/studio/features/StudioGuideContainer.tsx | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/client/src/app/(domain)/following/features/FollowingOffair.tsx b/client/src/app/(domain)/following/features/FollowingOffair.tsx index 4786e07d..fbe8ef6c 100644 --- a/client/src/app/(domain)/following/features/FollowingOffair.tsx +++ b/client/src/app/(domain)/following/features/FollowingOffair.tsx @@ -30,7 +30,7 @@ const OfflineItems = ({ offlines }: { offlines: User2[] }) => { return (
    -
    +
    profile

    {item.name}

    diff --git a/client/src/app/components/cabinet/CabinetContainer.tsx b/client/src/app/components/cabinet/CabinetContainer.tsx index cded555d..fe5d5ec1 100644 --- a/client/src/app/components/cabinet/CabinetContainer.tsx +++ b/client/src/app/components/cabinet/CabinetContainer.tsx @@ -9,12 +9,15 @@ import DesktopHeader from './DesktopHeader'; import { Broadcast } from '@libs/internalTypes'; import { getSuggestedLiveList } from '@libs/actions'; import useFollowingLives from '@hooks/useFollowingLives'; +import useUser from '@hooks/useUser'; const CabinetContainer = () => { + const { isLoggedin } = useUser(); + return (
    - + {isLoggedin && }
    ); @@ -44,8 +47,6 @@ const FollowNavigator = () => { useEffect(() => { fetchLives(); - - console.log(lives); }, []); return ( diff --git a/client/src/app/components/studio/StudioCategoryCard.tsx b/client/src/app/components/studio/StudioCategoryCard.tsx index 9074cee3..7b82b63f 100644 --- a/client/src/app/components/studio/StudioCategoryCard.tsx +++ b/client/src/app/components/studio/StudioCategoryCard.tsx @@ -8,7 +8,7 @@ type cardProps = { const StudioCategoryCard = ({ code, title }: cardProps) => { return ( -
    +
    diff --git a/client/src/app/studio/features/StudioGuideContainer.tsx b/client/src/app/studio/features/StudioGuideContainer.tsx index 48e6c754..226159d0 100644 --- a/client/src/app/studio/features/StudioGuideContainer.tsx +++ b/client/src/app/studio/features/StudioGuideContainer.tsx @@ -17,14 +17,14 @@ const StudioGuideContainer = () => {
    Open Broadcaster Software
    From 1d73512d574debfcd7fc9390c3a4f46eaa12ede2 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Wed, 27 Nov 2024 21:51:23 +0900 Subject: [PATCH 091/129] =?UTF-8?q?feat:=20=ED=8C=94=EB=A1=9C=EC=9A=B0=20r?= =?UTF-8?q?efetch=20=ED=95=A8=EC=88=98=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/app/(domain)/features/FollowingLives.tsx | 2 +- client/src/app/(domain)/features/live/LiveInfo.tsx | 13 ++++++++++++- .../src/app/(domain)/features/live/LiveSection.tsx | 12 ++++++++++-- .../src/app/components/cabinet/CabinetContainer.tsx | 6 +----- client/src/app/providers/FollowingLivesProvider.tsx | 10 +++++++--- 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/client/src/app/(domain)/features/FollowingLives.tsx b/client/src/app/(domain)/features/FollowingLives.tsx index 278c0bc7..e9900584 100644 --- a/client/src/app/(domain)/features/FollowingLives.tsx +++ b/client/src/app/(domain)/features/FollowingLives.tsx @@ -23,7 +23,7 @@ export const FollowingLives = () => { return ( <> {isLoggedin && ( -
    +

    팔로우 중인 방송

    diff --git a/client/src/app/(domain)/features/live/LiveInfo.tsx b/client/src/app/(domain)/features/live/LiveInfo.tsx index 4114a9c5..7e2dcf1e 100644 --- a/client/src/app/(domain)/features/live/LiveInfo.tsx +++ b/client/src/app/(domain)/features/live/LiveInfo.tsx @@ -134,9 +134,17 @@ type LiveInfoFollowToggleButtonProps = { ids: string[]; broadcastId: string; myId: string; + isloggedin: boolean; + refetchLives: () => void; }; -const LiveInfoFollowToggleButton = ({ ids, broadcastId, myId }: LiveInfoFollowToggleButtonProps) => { +const LiveInfoFollowToggleButton = ({ + ids, + broadcastId, + myId, + isloggedin, + refetchLives, +}: LiveInfoFollowToggleButtonProps) => { const [followed, setFollowed] = useState(false); const followInfo = { @@ -156,11 +164,14 @@ const LiveInfoFollowToggleButton = ({ ids, broadcastId, myId }: LiveInfoFollowTo await makeUnfollow(followInfo); setFollowed(false); } + + refetchLives(); }; return (
    ); From fd330e9eb1cd4d0894f4bb5c5ff36a423113c129 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Thu, 28 Nov 2024 01:44:58 +0900 Subject: [PATCH 093/129] =?UTF-8?q?fix:=20=EC=84=9C=EB=9E=8D=20=EB=B0=B0?= =?UTF-8?q?=EA=B2=BD=EC=83=89=20=EB=B6=80=EC=97=AC,=20=EC=84=9C=EB=9E=8D?= =?UTF-8?q?=20Image=20=EC=83=81=EC=9C=84=ED=83=9C=EA=B7=B8=EC=97=90=20widt?= =?UTF-8?q?h,=20height=20=EB=B6=80=EC=97=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/__mocks__/follow.ts | 2 +- .../app/components/cabinet/CabinetItemList.tsx | 18 ++++++++++-------- .../src/app/components/studio/StudioHeader.tsx | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/client/src/__mocks__/follow.ts b/client/src/__mocks__/follow.ts index 76125cd3..18042037 100644 --- a/client/src/__mocks__/follow.ts +++ b/client/src/__mocks__/follow.ts @@ -12,7 +12,7 @@ export const mockedFollowingList = { tags: ['tag1', 'tag2'], thumbnailUrl: 'https://via.placeholder.com/150', viewerCount: 900, - userName: '말', + userName: '말이 아홉마리지요 그러니까 10마리는 아닌거지요', profileImageUrl: 'https://via.placeholder.com/150', }, }, diff --git a/client/src/app/components/cabinet/CabinetItemList.tsx b/client/src/app/components/cabinet/CabinetItemList.tsx index f987a678..1ee6bcbf 100644 --- a/client/src/app/components/cabinet/CabinetItemList.tsx +++ b/client/src/app/components/cabinet/CabinetItemList.tsx @@ -82,16 +82,18 @@ const CabinetListItem = ({ item, isDesktop }: { item: Broadcast; isDesktop: bool onMouseLeave={handleMouseLeave} >
    - {item.title} +
    + {item.title} +
    -
    {item.userName}
    +
    {item.userName}
    {item.contentCategory}

    diff --git a/client/src/app/components/studio/StudioHeader.tsx b/client/src/app/components/studio/StudioHeader.tsx index 62287f35..332c9bb3 100644 --- a/client/src/app/components/studio/StudioHeader.tsx +++ b/client/src/app/components/studio/StudioHeader.tsx @@ -4,7 +4,7 @@ import StudioRouter from './StudioRouter'; const StudioHeader = () => { return ( -

    +
    From cc67e076403781a1ff2b6b3bae69eb2a53f3d021 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Thu, 28 Nov 2024 10:55:38 +0900 Subject: [PATCH 094/129] =?UTF-8?q?fix:=20=EC=BA=90=EB=B9=84=EB=84=B7=20?= =?UTF-8?q?=EC=88=AB=EC=9E=90=20=EC=83=89=20=EC=88=98=EC=A0=95,=20?= =?UTF-8?q?=ED=8C=94=EB=A1=9C=EC=9A=B0=20=EB=AA=A9=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/__mocks__/follow.ts | 2 +- client/src/app/components/cabinet/CabinetItemList.tsx | 2 +- client/src/app/layout.tsx | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/__mocks__/follow.ts b/client/src/__mocks__/follow.ts index 18042037..5075c3b4 100644 --- a/client/src/__mocks__/follow.ts +++ b/client/src/__mocks__/follow.ts @@ -60,7 +60,7 @@ export const mockedFollowingList = { }, { name: '피트니스트레이너', - profile_image: 'hhttps://via.placeholder.com/150', + profile_image: 'https://via.placeholder.com/150', broadcast_id: 'h2j4-5k6l-7m8n-9p0q', follower_count: 512, }, diff --git a/client/src/app/components/cabinet/CabinetItemList.tsx b/client/src/app/components/cabinet/CabinetItemList.tsx index 1ee6bcbf..89c89c64 100644 --- a/client/src/app/components/cabinet/CabinetItemList.tsx +++ b/client/src/app/components/cabinet/CabinetItemList.tsx @@ -96,7 +96,7 @@ const CabinetListItem = ({ item, isDesktop }: { item: Broadcast; isDesktop: bool
    {item.userName}
    {item.contentCategory}
    -

    +

    {'· ' + comma(item.viewerCount)}

    diff --git a/client/src/app/layout.tsx b/client/src/app/layout.tsx index bb9654a0..117cddd1 100644 --- a/client/src/app/layout.tsx +++ b/client/src/app/layout.tsx @@ -21,6 +21,7 @@ export const metadata: Metadata = { default: 'FUNCH', }, description: '뻔한 일상에 웃음 한 방 FUNCH!', + keywords: ['FUNCH', '펀치', '스트리밍', '라이브', '콘텐츠'], }; const RootLayout = ({ From 6c5e5f5ea92f603912a8a12dbbdf43dffe8aa43a Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Thu, 28 Nov 2024 15:04:00 +0900 Subject: [PATCH 095/129] =?UTF-8?q?fix:=20eventSource.onerror=20=EC=8B=9C?= =?UTF-8?q?=20close()=EB=A7=8C=20=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/app/(domain)/features/live/LiveInfo.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/app/(domain)/features/live/LiveInfo.tsx b/client/src/app/(domain)/features/live/LiveInfo.tsx index 7e2dcf1e..b62deb3f 100644 --- a/client/src/app/(domain)/features/live/LiveInfo.tsx +++ b/client/src/app/(domain)/features/live/LiveInfo.tsx @@ -39,7 +39,6 @@ const LiveInfoWrapper = ({ children }: Props) => { eventSource.onerror = () => { eventSource.close(); - fetchLiveInfo(); }; }; From 5fdd3eda823a998bbfd362a878788946235b666d Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Thu, 28 Nov 2024 15:05:55 +0900 Subject: [PATCH 096/129] =?UTF-8?q?fix:=20data=EA=B0=80=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=ED=95=98=EB=8A=94=20=EA=B2=BD=EC=9A=B0=EC=97=90?= =?UTF-8?q?=EB=A7=8C=20refreshInfo=20=ED=95=98=EB=8F=84=EB=A1=9D=20eventSo?= =?UTF-8?q?urce.onmessage=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/app/(domain)/features/live/LiveInfo.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/src/app/(domain)/features/live/LiveInfo.tsx b/client/src/app/(domain)/features/live/LiveInfo.tsx index b62deb3f..f0472d93 100644 --- a/client/src/app/(domain)/features/live/LiveInfo.tsx +++ b/client/src/app/(domain)/features/live/LiveInfo.tsx @@ -34,10 +34,13 @@ const LiveInfoWrapper = ({ children }: Props) => { eventSource.onmessage = (event) => { const data = JSON.parse(event.data); console.log('🚀 SSE DATA HAVE BEEN SERVED', data); + if (!data) return; + console.log('🥳 LIVE INFO DATA ARE BEING HANDLED', data); refreshLiveInfo(data); }; eventSource.onerror = () => { + console.log('❌ EVENT SOURCE ERROR'); eventSource.close(); }; }; From b3e5d5bf065bb29b2d69ef6b6b23cdd9fae69f37 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Thu, 28 Nov 2024 15:38:09 +0900 Subject: [PATCH 097/129] =?UTF-8?q?fix:=20=EB=B3=B5=EC=82=AC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 위에서 props를 받아서 childNode에서 복사와 토스트 띄우기를 한번에 하는 형태로 변경 --- client/plugins/tailwind.plugin.ts | 1 + .../src/app/components/cabinet/CabinetContainer.tsx | 2 +- .../src/app/components/cabinet/CabinetItemList.tsx | 2 +- client/src/app/components/cabinet/CabinetLink.tsx | 12 ++++++------ client/src/app/components/cabinet/DesktopHeader.tsx | 8 ++++++-- .../app/studio/features/StreamSettingContainer.tsx | 12 +++--------- client/src/app/studio/features/StudioCopyButton.tsx | 8 ++++++-- 7 files changed, 24 insertions(+), 21 deletions(-) diff --git a/client/plugins/tailwind.plugin.ts b/client/plugins/tailwind.plugin.ts index 29953ae5..6dc7fb94 100644 --- a/client/plugins/tailwind.plugin.ts +++ b/client/plugins/tailwind.plugin.ts @@ -85,6 +85,7 @@ const appBoxes = plugin(function ({ addUtilities }) { overflowY: 'auto', overscrollBehavior: 'contain', scrollbarWidth: 'none', + overflowX: 'hidden', '-ms-overflow-style': 'none', '&::-webkit-scrollbar': { display: 'none', diff --git a/client/src/app/components/cabinet/CabinetContainer.tsx b/client/src/app/components/cabinet/CabinetContainer.tsx index 3299c74c..cd139a85 100644 --- a/client/src/app/components/cabinet/CabinetContainer.tsx +++ b/client/src/app/components/cabinet/CabinetContainer.tsx @@ -106,7 +106,7 @@ const SuggestedNavigator = () => { const NavHeader = ({ children }: PropsWithChildren) => { return ( -
    +

    {children}

    ); diff --git a/client/src/app/components/cabinet/CabinetItemList.tsx b/client/src/app/components/cabinet/CabinetItemList.tsx index 89c89c64..6ce925ec 100644 --- a/client/src/app/components/cabinet/CabinetItemList.tsx +++ b/client/src/app/components/cabinet/CabinetItemList.tsx @@ -105,7 +105,7 @@ const CabinetListItem = ({ item, isDesktop }: { item: Broadcast; isDesktop: bool {isTooltipVisible && (
    {item.title} diff --git a/client/src/app/components/cabinet/CabinetLink.tsx b/client/src/app/components/cabinet/CabinetLink.tsx index 05a9f1f7..ad78f785 100644 --- a/client/src/app/components/cabinet/CabinetLink.tsx +++ b/client/src/app/components/cabinet/CabinetLink.tsx @@ -16,19 +16,19 @@ const CabinetLink = ({ link }: LinkProps) => {
    카테고리 @@ -38,19 +38,19 @@ const CabinetLink = ({ link }: LinkProps) => {
    팔로잉 diff --git a/client/src/app/components/cabinet/DesktopHeader.tsx b/client/src/app/components/cabinet/DesktopHeader.tsx index eec0b5b5..a830d63e 100644 --- a/client/src/app/components/cabinet/DesktopHeader.tsx +++ b/client/src/app/components/cabinet/DesktopHeader.tsx @@ -28,12 +28,14 @@ const DesktopHeader = ({ isExpanded, setIsExpanded, componentType }: ExpandedPro }; return ( -
    +

    {componentType === COMPONENT_TYPE.SUGGEST ? '추천 채널' : '팔로우 채널'}

    - +
    ); diff --git a/client/src/app/studio/features/StreamSettingContainer.tsx b/client/src/app/studio/features/StreamSettingContainer.tsx index 022a0f47..e8165669 100644 --- a/client/src/app/studio/features/StreamSettingContainer.tsx +++ b/client/src/app/studio/features/StreamSettingContainer.tsx @@ -7,13 +7,7 @@ import StudioReissueButton from './StudioReIssueButton'; import useUser from '@hooks/useUser'; import { getStreamInfo } from '@libs/actions'; -const handleCopy = async (streamURL: string) => { - if (typeof streamURL === 'string') { - await navigator.clipboard.writeText(streamURL); - } -}; - -const apiUrl = process.env.NEXT_PUBLIC_MEDIA_SERVER_URL; +const apiUrl = process.env.NEXT_PUBLIC_MEDIA_SERVER_URL ?? ''; const StreamSettingContainer = () => { const { isLoggedin } = useUser(); @@ -57,7 +51,7 @@ const StreamURLContainer = () => {
    스트림 URL
    {apiUrl} - handleCopy(apiUrl ?? '')}>복사 + 복사
    ); @@ -70,7 +64,7 @@ const StreamKeyContainer = ({ streamKey }: { streamKey: string }) => {
    {streamKey}
    - handleCopy(streamKey)}>복사 + 복사 재발급
    diff --git a/client/src/app/studio/features/StudioCopyButton.tsx b/client/src/app/studio/features/StudioCopyButton.tsx index f5773004..f476a220 100644 --- a/client/src/app/studio/features/StudioCopyButton.tsx +++ b/client/src/app/studio/features/StudioCopyButton.tsx @@ -5,12 +5,16 @@ import { type ButtonHTMLAttributes } from 'react'; import StudioToast from '@components/studio/StudioToast'; import { useState } from 'react'; -type Props = PropsWithChildren & ButtonHTMLAttributes; +type Props = PropsWithChildren<{ + text: string; +}> & + ButtonHTMLAttributes; -const StudioCopyButton = ({ children }: Props) => { +const StudioCopyButton = ({ children, text }: Props) => { const [isShowToast, setIsShowToast] = useState(false); const openToast = () => { setIsShowToast(true); + navigator.clipboard.writeText(text); }; const closeToast = () => { setIsShowToast(false); From abc124e0f6a8a16bffeaba72d1fe4733d45ea6f6 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Thu, 28 Nov 2024 15:43:50 +0900 Subject: [PATCH 098/129] =?UTF-8?q?fix:=20followingLivesWrapper=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=EC=8B=9D=EB=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(domain)/features/FollowingLives.tsx | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/client/src/app/(domain)/features/FollowingLives.tsx b/client/src/app/(domain)/features/FollowingLives.tsx index e9900584..04ad88e5 100644 --- a/client/src/app/(domain)/features/FollowingLives.tsx +++ b/client/src/app/(domain)/features/FollowingLives.tsx @@ -1,15 +1,12 @@ 'use client'; -import React from 'react'; -import { Broadcast, User2 } from '@libs/internalTypes'; +import React, { ReactNode, type PropsWithChildren } from 'react'; import useUser from '@hooks/useUser'; import Lives from '@components/livesGrid/Lives'; import clsx from 'clsx'; import useFollowingLives from '@hooks/useFollowingLives'; export const FollowingLives = () => { - const { isLoggedin } = useUser(); // 기존 useUser 훅 - const { isError, isLoading, lives } = useFollowingLives(); if (isError) { @@ -21,26 +18,34 @@ export const FollowingLives = () => { } return ( - <> - {isLoggedin && ( -
    -
    -

    팔로우 중인 방송

    -
    - - {({ visibleLives, isExpanded, toggle }) => ( - <> - - {visibleLives.map((live, index) => ( - - ))} - - {lives.length > 3 && } - - )} - + +
    +
    +

    팔로우 중인 방송

    - )} - + + {({ visibleLives, isExpanded, toggle }) => ( + <> + + {visibleLives.map((live, index) => ( + + ))} + + {lives.length > 3 && } + + )} + +
    +
    ); }; + +const FollowingLivesWrapper = ({ children }: PropsWithChildren) => { + const { isLoggedin } = useUser(); + + if (!isLoggedin) { + return null; + } + + return <>{children}; +}; From d0e0e9c866a8e126ca5c0fbf2bdaf75eb9e44b2d Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Thu, 28 Nov 2024 16:49:25 +0900 Subject: [PATCH 099/129] =?UTF-8?q?fix:=20=EC=97=90=EB=9F=AC=20=EB=B0=94?= =?UTF-8?q?=EC=9A=B4=EB=8D=94=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isError를 검사하기 전 isLoggedin 여부를 검사하도록 수정 --- .../app/(domain)/features/FollowingLives.tsx | 50 ++++++++----------- .../(domain)/following/features/Follow.tsx | 2 +- client/src/app/(domain)/page.tsx | 2 +- .../app/components/cabinet/DesktopHeader.tsx | 30 +++++++++-- .../app/studio/my/features/MyStudioForm.tsx | 1 - 5 files changed, 50 insertions(+), 35 deletions(-) diff --git a/client/src/app/(domain)/features/FollowingLives.tsx b/client/src/app/(domain)/features/FollowingLives.tsx index 04ad88e5..9efde563 100644 --- a/client/src/app/(domain)/features/FollowingLives.tsx +++ b/client/src/app/(domain)/features/FollowingLives.tsx @@ -1,14 +1,16 @@ 'use client'; -import React, { ReactNode, type PropsWithChildren } from 'react'; import useUser from '@hooks/useUser'; import Lives from '@components/livesGrid/Lives'; import clsx from 'clsx'; import useFollowingLives from '@hooks/useFollowingLives'; -export const FollowingLives = () => { +const FollowingLives = () => { + const { isLoggedin } = useUser(); const { isError, isLoading, lives } = useFollowingLives(); + if (!isLoggedin) return null; + if (isError) { return
    에러가 발생했습니다.
    ; } @@ -18,34 +20,24 @@ export const FollowingLives = () => { } return ( - -
    -
    -

    팔로우 중인 방송

    -
    - - {({ visibleLives, isExpanded, toggle }) => ( - <> - - {visibleLives.map((live, index) => ( - - ))} - - {lives.length > 3 && } - - )} - +
    +
    +

    팔로우 중인 방송

    - + + {({ visibleLives, isExpanded, toggle }) => ( + <> + + {visibleLives.map((live, index) => ( + + ))} + + {lives.length > 3 && } + + )} + +
    ); }; -const FollowingLivesWrapper = ({ children }: PropsWithChildren) => { - const { isLoggedin } = useUser(); - - if (!isLoggedin) { - return null; - } - - return <>{children}; -}; +export default FollowingLives; diff --git a/client/src/app/(domain)/following/features/Follow.tsx b/client/src/app/(domain)/following/features/Follow.tsx index 90a3f1fa..754d80f4 100644 --- a/client/src/app/(domain)/following/features/Follow.tsx +++ b/client/src/app/(domain)/following/features/Follow.tsx @@ -1,6 +1,6 @@ 'use client'; -import { FollowingLives } from '@app/(domain)/features/FollowingLives'; +import FollowingLives from '@app/(domain)/features/FollowingLives'; import useUser from '@hooks/useUser'; import FollowingOffair from './FollowingOffair'; import InduceLoginContent from './InduceLoginContent'; diff --git a/client/src/app/(domain)/page.tsx b/client/src/app/(domain)/page.tsx index 07e085b8..bc97a985 100644 --- a/client/src/app/(domain)/page.tsx +++ b/client/src/app/(domain)/page.tsx @@ -1,5 +1,5 @@ import RecommendedLives from './features/RecommendedLives'; -import { FollowingLives } from './features/FollowingLives'; +import FollowingLives from './features/FollowingLives'; const HomePage = () => { return ( diff --git a/client/src/app/components/cabinet/DesktopHeader.tsx b/client/src/app/components/cabinet/DesktopHeader.tsx index a830d63e..9e22454c 100644 --- a/client/src/app/components/cabinet/DesktopHeader.tsx +++ b/client/src/app/components/cabinet/DesktopHeader.tsx @@ -6,6 +6,7 @@ import UpArrowSvg from '@components/svgs/UpArrowSvg'; import clsx from 'clsx'; import { useState } from 'react'; +import Button from '@components/Button'; const COMPONENT_TYPE = { FOLLOW: 'FOLLOW' as const, @@ -44,12 +45,35 @@ const DesktopHeader = ({ isExpanded, setIsExpanded, componentType }: ExpandedPro > - + {isExpanded ? ( + + ) : ( + + )}
    ); }; +type ArrowButtonProps = { + componentType: 'UP' | 'DOWN'; + setIsExpanded: (value: React.SetStateAction) => void; +}; + +const ArrowButton = ({ componentType, setIsExpanded }: ArrowButtonProps) => { + return ( + <> + {componentType === 'UP' ? ( + + ) : ( + + )} + + ); +}; + export default DesktopHeader; diff --git a/client/src/app/studio/my/features/MyStudioForm.tsx b/client/src/app/studio/my/features/MyStudioForm.tsx index 686e6bd8..1838e879 100644 --- a/client/src/app/studio/my/features/MyStudioForm.tsx +++ b/client/src/app/studio/my/features/MyStudioForm.tsx @@ -53,7 +53,6 @@ const MyStudioForm = ({ onSubmit }: MyStudioFormProps) => { }; const handleDeleteTag = (index: number) => { - console.log(index); const newTags = tags.filter((_, i) => i !== index); setTags(newTags); setFormData({ From 629f6e77d322ef9340db5f987dba8218c189d814 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Thu, 28 Nov 2024 16:57:19 +0900 Subject: [PATCH 100/129] =?UTF-8?q?feat:=20=EC=8A=A4=ED=8A=9C=EB=94=94?= =?UTF-8?q?=EC=98=A4=20=EB=B9=84=EB=94=94=EC=98=A4=20=EC=98=81=EC=97=AD=20?= =?UTF-8?q?=EB=B0=8F=20=EC=B1=84=ED=8C=85=20=EC=98=81=EC=97=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/__mocks__/broadcasts.ts | 11 + client/src/app/providers/UserProvider.tsx | 21 ++ .../app/studio/my/features/MyStudioChat.tsx | 31 +- .../app/studio/my/features/MyStudioVideo.tsx | 184 ++++++++-- .../app/studio/my/features/StudioChatDemo.tsx | 325 ++++++++++++++++++ client/src/app/studio/my/page.tsx | 3 +- client/src/hooks/useUser.ts | 4 +- 7 files changed, 536 insertions(+), 43 deletions(-) create mode 100644 client/src/app/studio/my/features/StudioChatDemo.tsx diff --git a/client/src/__mocks__/broadcasts.ts b/client/src/__mocks__/broadcasts.ts index bb6c100c..9ee0cab5 100644 --- a/client/src/__mocks__/broadcasts.ts +++ b/client/src/__mocks__/broadcasts.ts @@ -222,6 +222,17 @@ export const mockedBroadcasts: Broadcast[] = [ userName: '사자', profileImageUrl: 'https://via.placeholder.com/150', }, + { + broadcastId: 'cccccccccccccccccccccccc', + title: '레이디 가가의 방송', + contentCategory: CONTENTS_CATEGORY.virtual.CODE, + moodCategory: MOODS_CATEGORY.getking.CODE, + tags: ['tag1', 'tag2'], + thumbnailUrl: 'https://via.placeholder.com/150', + viewerCount: 1000, + userName: '사자', + profileImageUrl: 'https://via.placeholder.com/150', + }, ]; export const getBroadcastsByContentCategory = (contentCategory: string) => { diff --git a/client/src/app/providers/UserProvider.tsx b/client/src/app/providers/UserProvider.tsx index 528fc453..91790a06 100644 --- a/client/src/app/providers/UserProvider.tsx +++ b/client/src/app/providers/UserProvider.tsx @@ -13,6 +13,7 @@ type UserContextType = { loginByGoogle: () => void; logout: () => void; saveUserSession: (user: InternalUserSession) => void; + updateBroadcastId: (broadcastId: string) => void; }; export const UserContext = createContext({ @@ -22,6 +23,7 @@ export const UserContext = createContext({ loginByGoogle: () => {}, logout: () => {}, saveUserSession: () => {}, + updateBroadcastId: () => {}, }); type Props = PropsWithChildren; @@ -44,6 +46,24 @@ const UserProvider = ({ children }: Props) => { cookies.remove(COOKIE_USER_KEY); }; + const updateBroadcastId = useCallback((broadcastId: string) => { + if (!broadcastId) { + return; + } + setUserSession((prev) => { + if (!prev) { + return null; + } + return { + ...prev, + user: { + ...prev.user, + broadcastId, + }, + }; + }); + }, []); + const logout = () => { const ok = confirm('로그아웃하시겠어요?'); if (!ok) { @@ -104,6 +124,7 @@ const UserProvider = ({ children }: Props) => { loginByGoogle, logout, saveUserSession, + updateBroadcastId, }} > {children} diff --git a/client/src/app/studio/my/features/MyStudioChat.tsx b/client/src/app/studio/my/features/MyStudioChat.tsx index 303342eb..d8b1ba9f 100644 --- a/client/src/app/studio/my/features/MyStudioChat.tsx +++ b/client/src/app/studio/my/features/MyStudioChat.tsx @@ -1,12 +1,31 @@ -import StudioChatHeader from './StudioChatHeader'; -import StudioChatBody from './StudioChatBody'; +'use client'; + +// import StudioChatHeader from './StudioChatHeader'; +// import StudioChatBody from './StudioChatBody'; +import StudioChatDemo from './StudioChatDemo'; const MyStudioChat = () => { return ( -
    - - -
    + //
    + // + // + //
    + + {({ chatList, socketRef, chatname, sendChat, isLoading, isError }) => ( + <> + {isError ? ( + + ) : isLoading ? ( + + ) : ( + <> + + + + )} + + )} + ); }; diff --git a/client/src/app/studio/my/features/MyStudioVideo.tsx b/client/src/app/studio/my/features/MyStudioVideo.tsx index f0c01ac0..c2207089 100644 --- a/client/src/app/studio/my/features/MyStudioVideo.tsx +++ b/client/src/app/studio/my/features/MyStudioVideo.tsx @@ -1,65 +1,154 @@ 'use client'; +import ErrorBoundary from '@components/ErrorBoundary'; import useHls from '@hooks/useHls'; import useUser from '@hooks/useUser'; -import { getPlaylist } from '@libs/actions'; +import { getPlaylist, getStreamInfo } from '@libs/actions'; import type { Broadcast, Playlist } from '@libs/internalTypes'; +import { useQuery } from '@tanstack/react-query'; import clsx from 'clsx'; -import { useEffect, useRef, useState } from 'react'; +import Link from 'next/link'; +import { createContext, type PropsWithChildren, useContext, useEffect, useRef, useState } from 'react'; + +type MyStudioVideoContextType = { + playlistUrl: Playlist['playlistUrl'] | null; + injectPlaylistUrl: (playlistUrl: Playlist['playlistUrl']) => void; +}; + +const MyStudioVideoContext = createContext(null); + +const useMyStudioVideo = () => { + const context = useContext(MyStudioVideoContext); + if (!context) { + throw new Error('useMyStudioVideo must be used within a MyStudioVideoProvider'); + } + return context; +}; const MyStudioVideo = () => { - const [isError, setIsError] = useState(false); - const [isLoading, setIsLoading] = useState(true); const [playlistUrl, setPlaylistUrl] = useState(null); - const { loggedinUser } = useUser(); + + const injectPlaylistUrl = (playlistUrl: Playlist['playlistUrl']) => { + setPlaylistUrl(playlistUrl); + }; + + return ( + + + 회원 정보를 불러올 수 없어요.}> + + + + + + + ); +}; + +const FallbackWrapper = ({ children }: PropsWithChildren) => { + return ( +
    +

    + {children} +

    +
    + ); +}; + +const BroadcastIdRefresher = ({ children }: PropsWithChildren) => { + const { updateBroadcastId } = useUser(); + const { data, isLoading, isError } = useQuery({ + queryKey: ['my-studio-stream-info'], + queryFn: () => getStreamInfo(), + }); useEffect(() => { - let isValidEffect = true; - const fetchLive = async (broadcastId: Broadcast['broadcastId']) => { - try { - const fetchedPlaylist = await getPlaylist(broadcastId); - if (!isValidEffect) return; - setPlaylistUrl(fetchedPlaylist.playlistUrl); - setIsLoading(false); - } catch (err) { - if (!isValidEffect) return; - setPlaylistUrl(null); - setIsError(true); + if (data) { + updateBroadcastId(data.broadcast_id); + } + }, [data, updateBroadcastId]); + + if (isError) { + throw Error('스트리머 정보를 불러오는 중 에러가 발생했어요.'); + } + + if (isLoading || !data) { + return 스트리머 정보 갱신 중...; + } + + return children; +}; + +const BroadcastIdSender = () => { + const { loggedinUser } = useUser(); + + if (!loggedinUser?.broadcastId) { + return
    방송이 없어요
    ; + } + + return ( + + + } - }; - const broadcastId = loggedinUser?.broadcastId; + > + + + ); +}; - if (broadcastId) { - fetchLive(broadcastId); +const NoPlaylist = () => { + return ( +
    +

    방송 정보를 불러올 수 없어요.

    + + 방송 설정하러 가기 + +
    + ); +}; + +const PlaylistFetcher = ({ broadcastId }: { broadcastId: Broadcast['broadcastId'] }) => { + const { playlistUrl, injectPlaylistUrl } = useMyStudioVideo(); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['my-studio-playlist', broadcastId], + queryFn: () => getPlaylist(broadcastId), + }); + + useEffect(() => { + if (data) { + injectPlaylistUrl(data.playlistUrl); } + }, [data, injectPlaylistUrl]); - return () => { - isValidEffect = false; - }; - }, [loggedinUser]); + if (isError) { + throw Error('플레이 리스트를 불러오는 중 에러가 발생했어요.'); + } - if (isLoading) { - return
    Loading...
    ; + if (isLoading || !data || !playlistUrl) { + return 플레이 리스트를 불러오는 중...; } - const shouldShowError = isError || !playlistUrl; + return
    + ); +}; + +const VideoBuffering = () => { + return ( +
    +

    비디오 청크를 정성들여 만드는 중...

    +
    + ); +}; + +const VideoLoading = () => { + return ( +
    +

    로딩 중...

    +
    + ); +}; + +const VideoError = () => { + return ( +
    +

    비디오를 불러오는 중에 에러가 발생했어요.

    ); }; diff --git a/client/src/app/studio/my/features/StudioChatDemo.tsx b/client/src/app/studio/my/features/StudioChatDemo.tsx new file mode 100644 index 00000000..9a6302bf --- /dev/null +++ b/client/src/app/studio/my/features/StudioChatDemo.tsx @@ -0,0 +1,325 @@ +'use client'; + +import Button from '@components/Button'; +// import useLiveContext from '@hooks/useLiveContext'; +import useUser from '@hooks/useUser'; +import { SOCKET_EVENT } from '@libs/constants'; +import clsx from 'clsx'; +import { + type ChangeEvent, + type MutableRefObject, + type ReactNode, + memo, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; +import { io, type Socket } from 'socket.io-client'; + +type ChatType = { + name: string; + content: string; + color?: string; +}; + +type SendChat = (args: { socketRef: MutableRefObject; name: string; content: string }) => void; + +type ChildrenArgs = { + chatList: ChatType[]; + isLoading: boolean; + isError: boolean; + socketRef: MutableRefObject; + chatname: string; + sendChat: SendChat; +}; + +type Props = { + children: (args: ChildrenArgs) => ReactNode; +}; + +const ChatWrapper = ({ children }: Props) => { + // const { isLivePage } = useLiveContext(); + // const { broadcastId } = useLiveContext(); + const { loggedinUser } = useUser(); + const [chatList, setChatList] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isError, setIsError] = useState(false); + const socketRef = useRef(null); + const [chatname, setChatname] = useState(loggedinUser?.name || '익명'); + const [broadcastId, setBroadcastId] = useState(null); + + useEffect(() => { + if (!loggedinUser) return; + setChatname(loggedinUser.name); + setBroadcastId(loggedinUser.broadcastId); + }, [loggedinUser]); + + useEffect(() => { + const connectSocket = () => { + setIsError(false); + setIsLoading(true); + setChatList([]); + + const socketUrl = + process.env.NODE_ENV === 'production' + ? process.env.NEXT_PUBLIC_CHAT_SERVER_URL + : process.env.NEXT_PUBLIC_CHAT_SERVER_URL_DEV; + + const socketQuery = { + broadcastId, + } as any; + + if (loggedinUser) { + socketQuery.name = loggedinUser.name; + } + + const socket = io(socketUrl, { + path: '/live', + query: socketQuery, + reconnectionDelay: 1000 * 10, + }); + + socketRef.current = socket; + + socket.on(SOCKET_EVENT.CONNECT_ERROR, () => { + console.log('❌ SOCKET CONNECT ERROR'); + setIsError(true); + }); + socket.on(SOCKET_EVENT.CONNECT_TIMEOUT, () => { + console.log('❌ SOCKET CONNECT TIMEOUT'); + setIsError(true); + }); + socket.on(SOCKET_EVENT.DISCONNECT, () => { + console.log('❌ SOCKET DISCONNECTED'); + }); + socket.on(SOCKET_EVENT.RECONNECT_FAILED, () => { + console.log('❌ SOCKET RECONNECT FAILED'); + }); + socket.on(SOCKET_EVENT.ERROR, () => { + console.log('❌ SOCKET ERROR'); + setIsError(true); + }); + + socket.on(SOCKET_EVENT.CONNECT, () => { + console.log('✅ SOCKET CONNECTED'); + setIsLoading(false); + setIsError(false); + }); + + socket.on(SOCKET_EVENT.CHAT, (receivedData: ChatType) => { + // 상태 업데이트 + console.log('😇 RECEIVING : ', receivedData); + setChatList((prev) => [...prev, receivedData]); + }); + + socket.on(SOCKET_EVENT.SET_ANONYMOUS_NAME, (data) => { + console.log('🚀 SETTING ANONYMOUS NAME : ', data); + setChatname(data.name); + }); + }; + + connectSocket(); + + return () => { + if (socketRef.current) { + socketRef.current.disconnect(); + socketRef.current = null; + } + }; + }, [broadcastId, loggedinUser]); + + const sendChat = useCallback( + ({ socketRef, name, content }: { socketRef: MutableRefObject; name: string; content: string }) => { + if (!socketRef.current) return; + + console.log('🚀 EMITING TO SERVER : ', { + name, + content, + }); + + socketRef.current.emit( + SOCKET_EVENT.CHAT, + Buffer.from( + JSON.stringify({ + name, + content, + }), + ), + ); + }, + [], + ); + + return ( + + ); +}; + +type ChatListProps = { + chatList: ChatType[]; +}; + +const ChatList = ({ chatList }: ChatListProps) => { + const [isUserScrolling, setIsUserScrolling] = useState(false); + const chatListRef = useRef(null); + const chatBottomRef = useRef(null); + + useEffect(() => { + if (chatBottomRef.current) { + if (!isUserScrolling) { + chatBottomRef.current.scrollIntoView({ behavior: 'smooth' }); + } + } + }, [chatList]); + + useEffect(() => { + const handleScroll = () => { + if (chatListRef.current) { + const { scrollHeight, scrollTop, clientHeight } = chatListRef.current; + const isScrolledToBottom = scrollHeight - scrollTop < clientHeight + 20; + setIsUserScrolling(!isScrolledToBottom); + } + }; + + if (chatListRef.current) { + chatListRef.current.addEventListener('scroll', handleScroll); + } + + return () => { + if (chatListRef.current) { + chatListRef.current.removeEventListener('scroll', handleScroll); + } + }; + }, [chatListRef]); + + return ( +
    +
    +
    +
    +

    + 아름다운 채팅 문화를 만들어보아요. +

    +
    + {chatList.map((chat, index) => ( + + ))} +
    +
    +
    +
    + ); +}; + +const ChatItem = memo( + ({ name, color = 'var(--content-neutral-primary)', content }: { name: string; color?: string; content: string }) => { + console.log('🚀 RENDERING CHAT ITEM : ', name, content); + return ( +

    + + {name} + + {content} +

    + ); + }, +); + +type ChatFormProps = { + socketRef: MutableRefObject; + chatname: string; + sendChat: SendChat; +}; + +const ChatForm = memo(({ socketRef, chatname, sendChat }: ChatFormProps) => { + const [inputValue, setInputValue] = useState(''); + const handleChage = (e: ChangeEvent) => { + let value = e.target.value.trimStart().replace(/\s+/g, ' '); + if (value.length > 120) { + value = value.slice(0, 120); + } + setInputValue(value); + }; + return ( +
    +
    { + e.preventDefault(); + if (!inputValue) return; + sendChat({ + socketRef, + name: chatname, + content: inputValue, + }); + setInputValue(''); + }} + > +
    + +
    +
    + +
    +
    +
    + ); +}); + +const ChatError = () => { + return ( +

    + 채팅 서버에 연결할 수 없어요. +

    + ); +}; +const ChatLoading = () => { + return ( +

    + 채팅 서버에 연결 중이에요. +

    + ); +}; + +const StudioChatDemo = Object.assign(ChatWrapper, { + List: ChatList, + Form: ChatForm, + Error: ChatError, + Loading: ChatLoading, +}); + +export default StudioChatDemo; diff --git a/client/src/app/studio/my/page.tsx b/client/src/app/studio/my/page.tsx index 8264ce25..42eac729 100644 --- a/client/src/app/studio/my/page.tsx +++ b/client/src/app/studio/my/page.tsx @@ -11,7 +11,8 @@ const StudioMyPage = () => { return (
    diff --git a/client/src/hooks/useUser.ts b/client/src/hooks/useUser.ts index 2ec4d4f9..a71db0de 100644 --- a/client/src/hooks/useUser.ts +++ b/client/src/hooks/useUser.ts @@ -6,7 +6,8 @@ import useUserContext from '@hooks/useUserContext'; const getIsLoggedin = (user: any) => user !== null; const useUser = () => { - const { userSession, logout, loginByGithub, loginByNaver, loginByGoogle, saveUserSession } = useUserContext(); + const { userSession, logout, loginByGithub, loginByNaver, loginByGoogle, saveUserSession, updateBroadcastId } = + useUserContext(); const [isLoggedin, setIsLoggedin] = useState(getIsLoggedin(userSession)); useEffect(() => { @@ -23,6 +24,7 @@ const useUser = () => { loginByNaver, loginByGoogle, saveUserSession, + updateBroadcastId, }; }; From 9a8a14f4269f03b1c4ff4246441f584593023666 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Thu, 28 Nov 2024 15:56:08 +0900 Subject: [PATCH 101/129] =?UTF-8?q?feat:=20Github=20Actions=20Workflow=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI/CD를 위한 workflow를 작성하였습니다. --- .github/workflows/deployment.yml | 45 +++++++++++++++++++++++++++++++ .github/workflows/integration.yml | 28 +++++++++++++++++++ .gitignore | 1 - 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/deployment.yml create mode 100644 .github/workflows/integration.yml diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml new file mode 100644 index 00000000..48d22989 --- /dev/null +++ b/.github/workflows/deployment.yml @@ -0,0 +1,45 @@ +name: Deployment + +on: + push: + branches: + - main + +jobs: + funch-deploy: + runs-on: ubuntu-latest + + steps: + - name: funch server deploy + env: + SSH_PRIVATE_KEY: ${{ secrets.FUNCH_SSH_KEY }} + SERVER_HOST: ${{ secrets.FUNCH_HOST }} + SERVER_USER: ${{ secrets.FUNCH_USER }} + run: | + echo "${FUNCH_SSH_KEY}" > deploy_key + chmod 600 deploy_key + ssh -i deploy_key ${{ secrets.FUNCH_USER }}@${{ secrets.FUNCH_HOST}} + cd /root/web25-funch + git switch main + git pull origin main + pm2 reload + + + media-deploy: + runs-on: ubuntu-latest + + steps: + - name: funch server deploy + env: + SSH_PRIVATE_KEY: ${{ secrets.MEDIA_SSH_KEY }} + SERVER_HOST: ${{ secrets.MEDIA_HOST }} + SERVER_USER: ${{ secrets.MEDIA_USER }} + run: | + echo "${MEDIA_SSH_KEY}" > deploy_key + chmod 600 deploy_key + ssh -i deploy_key ${{ secrets.MEDIA_USER }}@${{ secrets.MEDIA_HOST}} + cd /service/web25-funch + git switch main + git pull origin main + cd mediaServer + pm2 reload \ No newline at end of file diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 00000000..0a48c5aa --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,28 @@ +name: Integration + +on: + pull.request: + branches: + - dev + - main + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22.11.0 + cache: npm + - name: application server test + run: | + cd applicationServer + npm install + npm test + + - name: client test + run: | + cd client + npm install + npm test diff --git a/.gitignore b/.gitignore index 52eb16bc..e5015a4a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ node_modules/ -.github .env .gitmessage.txt logs/ From 92232d924455e300cd5bde0432e107991fb09041 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Thu, 28 Nov 2024 15:59:09 +0900 Subject: [PATCH 102/129] =?UTF-8?q?chore:=20.gitignore=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit workflow 업데이트를 위해 제거했던 .github 디렉토리 경로를 .gitignore에 복구했습니다. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e5015a4a..2bfc8c03 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules/ .env .gitmessage.txt logs/ +.github/ # client .env* From 865559990309acd8db752a8631139b1c144d29da Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Thu, 28 Nov 2024 16:17:03 +0900 Subject: [PATCH 103/129] =?UTF-8?q?refactor:=20workflow=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 일부 형식에 맞지 않는 부분을 수정했습니다. --- .github/workflows/deployment.yml | 64 +++++++++++++++---------------- .github/workflows/integration.yml | 2 +- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 48d22989..b2f7aed0 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -7,39 +7,39 @@ on: jobs: funch-deploy: - runs-on: ubuntu-latest + runs-on: ubuntu-latest - steps: - - name: funch server deploy - env: - SSH_PRIVATE_KEY: ${{ secrets.FUNCH_SSH_KEY }} - SERVER_HOST: ${{ secrets.FUNCH_HOST }} - SERVER_USER: ${{ secrets.FUNCH_USER }} - run: | - echo "${FUNCH_SSH_KEY}" > deploy_key - chmod 600 deploy_key - ssh -i deploy_key ${{ secrets.FUNCH_USER }}@${{ secrets.FUNCH_HOST}} - cd /root/web25-funch - git switch main - git pull origin main - pm2 reload + steps: + - name: funch server deploy + env: + SSH_PRIVATE_KEY: ${{ secrets.FUNCH_SSH_KEY }} + SERVER_HOST: ${{ secrets.FUNCH_HOST }} + SERVER_USER: ${{ secrets.FUNCH_USER }} + run: | + echo "${FUNCH_SSH_KEY}" > deploy_key + chmod 600 deploy_key + ssh -i deploy_key ${{ secrets.FUNCH_USER }}@${{ secrets.FUNCH_HOST}} + cd /root/web25-funch + git switch main + git pull origin main + pm2 reload media-deploy: - runs-on: ubuntu-latest - - steps: - - name: funch server deploy - env: - SSH_PRIVATE_KEY: ${{ secrets.MEDIA_SSH_KEY }} - SERVER_HOST: ${{ secrets.MEDIA_HOST }} - SERVER_USER: ${{ secrets.MEDIA_USER }} - run: | - echo "${MEDIA_SSH_KEY}" > deploy_key - chmod 600 deploy_key - ssh -i deploy_key ${{ secrets.MEDIA_USER }}@${{ secrets.MEDIA_HOST}} - cd /service/web25-funch - git switch main - git pull origin main - cd mediaServer - pm2 reload \ No newline at end of file + runs-on: ubuntu-latest + + steps: + - name: funch server deploy + env: + SSH_PRIVATE_KEY: ${{ secrets.MEDIA_SSH_KEY }} + SERVER_HOST: ${{ secrets.MEDIA_HOST }} + SERVER_USER: ${{ secrets.MEDIA_USER }} + run: | + echo "${MEDIA_SSH_KEY}" > deploy_key + chmod 600 deploy_key + ssh -i deploy_key ${{ secrets.MEDIA_USER }}@${{ secrets.MEDIA_HOST}} + git switch main + cd /service/web25-funch + git pull origin main + cd mediaServer + pm2 reload \ No newline at end of file diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 0a48c5aa..3f61bf47 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -1,7 +1,7 @@ name: Integration on: - pull.request: + pull_request: branches: - dev - main From cbf4a5ddf6baa228306b6361c5e6bac8eabee343 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Thu, 28 Nov 2024 16:25:59 +0900 Subject: [PATCH 104/129] =?UTF-8?q?=EC=9D=B4=EC=A0=84=EA=B3=BC=20=EB=8F=99?= =?UTF-8?q?=EC=9D=BC=ED=95=9C=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deployment.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index b2f7aed0..959d1402 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -8,7 +8,6 @@ on: jobs: funch-deploy: runs-on: ubuntu-latest - steps: - name: funch server deploy env: @@ -24,10 +23,8 @@ jobs: git pull origin main pm2 reload - media-deploy: runs-on: ubuntu-latest - steps: - name: funch server deploy env: From a213de57b9ba21ef67f45f6d5aa0b6d0c1045171 Mon Sep 17 00:00:00 2001 From: Jinyoung Kim <83767872+JYKIM317@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:29:25 +0900 Subject: [PATCH 105/129] =?UTF-8?q?workflow=20=EC=9D=B8=EB=8D=B4=ED=8A=B8?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deployment.yml | 62 ++++++++++++++++---------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 959d1402..eb0e4c59 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -7,36 +7,36 @@ on: jobs: funch-deploy: - runs-on: ubuntu-latest - steps: - - name: funch server deploy - env: - SSH_PRIVATE_KEY: ${{ secrets.FUNCH_SSH_KEY }} - SERVER_HOST: ${{ secrets.FUNCH_HOST }} - SERVER_USER: ${{ secrets.FUNCH_USER }} - run: | - echo "${FUNCH_SSH_KEY}" > deploy_key - chmod 600 deploy_key - ssh -i deploy_key ${{ secrets.FUNCH_USER }}@${{ secrets.FUNCH_HOST}} - cd /root/web25-funch - git switch main - git pull origin main - pm2 reload + runs-on: ubuntu-latest + steps: + - name: funch server deploy + env: + SSH_PRIVATE_KEY: ${{ secrets.FUNCH_SSH_KEY }} + SERVER_HOST: ${{ secrets.FUNCH_HOST }} + SERVER_USER: ${{ secrets.FUNCH_USER }} + run: | + echo "${FUNCH_SSH_KEY}" > deploy_key + chmod 600 deploy_key + ssh -i deploy_key ${{ secrets.FUNCH_USER }}@${{ secrets.FUNCH_HOST}} + cd /root/web25-funch + git switch main + git pull origin main + pm2 reload media-deploy: - runs-on: ubuntu-latest - steps: - - name: funch server deploy - env: - SSH_PRIVATE_KEY: ${{ secrets.MEDIA_SSH_KEY }} - SERVER_HOST: ${{ secrets.MEDIA_HOST }} - SERVER_USER: ${{ secrets.MEDIA_USER }} - run: | - echo "${MEDIA_SSH_KEY}" > deploy_key - chmod 600 deploy_key - ssh -i deploy_key ${{ secrets.MEDIA_USER }}@${{ secrets.MEDIA_HOST}} - git switch main - cd /service/web25-funch - git pull origin main - cd mediaServer - pm2 reload \ No newline at end of file + runs-on: ubuntu-latest + steps: + - name: funch server deploy + env: + SSH_PRIVATE_KEY: ${{ secrets.MEDIA_SSH_KEY }} + SERVER_HOST: ${{ secrets.MEDIA_HOST }} + SERVER_USER: ${{ secrets.MEDIA_USER }} + run: | + echo "${MEDIA_SSH_KEY}" > deploy_key + chmod 600 deploy_key + ssh -i deploy_key ${{ secrets.MEDIA_USER }}@${{ secrets.MEDIA_HOST}} + git switch main + cd /service/web25-funch + git pull origin main + cd mediaServer + pm2 reload From a71bee9f916b97559e055a4cfb781da92df1b9c3 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Thu, 28 Nov 2024 16:36:41 +0900 Subject: [PATCH 106/129] =?UTF-8?q?style:=20=EC=9D=B8=EB=8D=B4=ED=8A=B8=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit deployment.yml 인덴트 변경 --- .github/workflows/deployment.yml | 50 ++++++++++++++++---------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index eb0e4c59..17efb4b6 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -10,33 +10,33 @@ jobs: runs-on: ubuntu-latest steps: - name: funch server deploy - env: - SSH_PRIVATE_KEY: ${{ secrets.FUNCH_SSH_KEY }} - SERVER_HOST: ${{ secrets.FUNCH_HOST }} - SERVER_USER: ${{ secrets.FUNCH_USER }} - run: | - echo "${FUNCH_SSH_KEY}" > deploy_key - chmod 600 deploy_key - ssh -i deploy_key ${{ secrets.FUNCH_USER }}@${{ secrets.FUNCH_HOST}} - cd /root/web25-funch - git switch main - git pull origin main - pm2 reload + env: + SSH_PRIVATE_KEY: ${{ secrets.FUNCH_SSH_KEY }} + SERVER_HOST: ${{ secrets.FUNCH_HOST }} + SERVER_USER: ${{ secrets.FUNCH_USER }} + run: | + echo "${FUNCH_SSH_KEY}" > deploy_key + chmod 600 deploy_key + ssh -i deploy_key ${{ secrets.FUNCH_USER }}@${{ secrets.FUNCH_HOST}} + cd /root/web25-funch + git switch main + git pull origin main + pm2 reload media-deploy: runs-on: ubuntu-latest steps: - - name: funch server deploy + - name: media server deploy env: - SSH_PRIVATE_KEY: ${{ secrets.MEDIA_SSH_KEY }} - SERVER_HOST: ${{ secrets.MEDIA_HOST }} - SERVER_USER: ${{ secrets.MEDIA_USER }} - run: | - echo "${MEDIA_SSH_KEY}" > deploy_key - chmod 600 deploy_key - ssh -i deploy_key ${{ secrets.MEDIA_USER }}@${{ secrets.MEDIA_HOST}} - git switch main - cd /service/web25-funch - git pull origin main - cd mediaServer - pm2 reload + SSH_PRIVATE_KEY: ${{ secrets.MEDIA_SSH_KEY }} + SERVER_HOST: ${{ secrets.MEDIA_HOST }} + SERVER_USER: ${{ secrets.MEDIA_USER }} + run: | + echo "${MEDIA_SSH_KEY}" > deploy_key + chmod 600 deploy_key + ssh -i deploy_key ${{ secrets.MEDIA_USER }}@${{ secrets.MEDIA_HOST}} + git switch main + cd /service/web25-funch + git pull origin main + cd mediaServer + pm2 reload From 620d0602dc62daf1b293a61524b2f7bfb805aa1f Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Thu, 28 Nov 2024 16:46:21 +0900 Subject: [PATCH 107/129] =?UTF-8?q?fix:=20deployment.yml=20=EC=9D=B8?= =?UTF-8?q?=EB=8D=B4=ED=8A=B8=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tab size가 local과 github에서 달라 발생하는 문제를 해결했습니다. --- .github/workflows/deployment.yml | 52 ++++++++++++++++---------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 17efb4b6..9d5ece1f 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -10,33 +10,33 @@ jobs: runs-on: ubuntu-latest steps: - name: funch server deploy - env: - SSH_PRIVATE_KEY: ${{ secrets.FUNCH_SSH_KEY }} - SERVER_HOST: ${{ secrets.FUNCH_HOST }} - SERVER_USER: ${{ secrets.FUNCH_USER }} - run: | - echo "${FUNCH_SSH_KEY}" > deploy_key - chmod 600 deploy_key - ssh -i deploy_key ${{ secrets.FUNCH_USER }}@${{ secrets.FUNCH_HOST}} - cd /root/web25-funch - git switch main - git pull origin main - pm2 reload - + env: + SSH_PRIVATE_KEY: ${{ secrets.FUNCH_SSH_KEY }} + SERVER_HOST: ${{ secrets.FUNCH_HOST }} + SERVER_USER: ${{ secrets.FUNCH_USER }} + run: | + echo "${FUNCH_SSH_KEY}" > deploy_key + chmod 600 deploy_key + ssh -i deploy_key ${{ secrets.FUNCH_USER }}@${{ secrets.FUNCH_HOST}} + cd /root/web25-funch + git switch main + git pull origin main + pm2 reload + media-deploy: runs-on: ubuntu-latest steps: - name: media server deploy - env: - SSH_PRIVATE_KEY: ${{ secrets.MEDIA_SSH_KEY }} - SERVER_HOST: ${{ secrets.MEDIA_HOST }} - SERVER_USER: ${{ secrets.MEDIA_USER }} - run: | - echo "${MEDIA_SSH_KEY}" > deploy_key - chmod 600 deploy_key - ssh -i deploy_key ${{ secrets.MEDIA_USER }}@${{ secrets.MEDIA_HOST}} - git switch main - cd /service/web25-funch - git pull origin main - cd mediaServer - pm2 reload + env: + SSH_PRIVATE_KEY: ${{ secrets.MEDIA_SSH_KEY }} + SERVER_HOST: ${{ secrets.MEDIA_HOST }} + SERVER_USER: ${{ secrets.MEDIA_USER }} + run: | + echo "${MEDIA_SSH_KEY}" > deploy_key + chmod 600 deploy_key + ssh -i deploy_key ${{ secrets.MEDIA_USER }}@${{ secrets.MEDIA_HOST}} + git switch main + cd /service/web25-funch + git pull origin main + cd mediaServer + pm2 reload From 6ff815708ff80bd030b9c5f552f9d2e04a415610 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Thu, 28 Nov 2024 17:42:19 +0900 Subject: [PATCH 108/129] =?UTF-8?q?refactor:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 토큰이 null일 때 false를 반환하도록 변경 Co-authored-by: 최호빈 --- applicationServer/src/auth/core/auth.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/applicationServer/src/auth/core/auth.service.ts b/applicationServer/src/auth/core/auth.service.ts index 1f4dadb4..1197dde6 100644 --- a/applicationServer/src/auth/core/auth.service.ts +++ b/applicationServer/src/auth/core/auth.service.ts @@ -16,6 +16,7 @@ class AuthService { } verifyToken(token: string) { + if (!token) return false; try { return this.jwtService.verify(token); } catch (error) { From 31dd618c12bf6e92dbb602ab8c32589b6e9b20e8 Mon Sep 17 00:00:00 2001 From: Jinyoung Kim <83767872+JYKIM317@users.noreply.github.com> Date: Thu, 28 Nov 2024 18:50:44 +0900 Subject: [PATCH 109/129] =?UTF-8?q?README.md=20=EA=B8=B0=EC=88=A0=EC=8A=A4?= =?UTF-8?q?=ED=83=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 741c5d38..1f32d5d7 100644 --- a/README.md +++ b/README.md @@ -30,3 +30,21 @@


    + +## 👩🏻‍💻🧑🏻‍💻 기술 스택 + +
    + + +### 공통 + + +### FE + + + +### BE + + + +

    From 25a10da090ef2df001ce283036d8cdf6b3e30d25 Mon Sep 17 00:00:00 2001 From: Jinyoung Kim <83767872+JYKIM317@users.noreply.github.com> Date: Thu, 28 Nov 2024 19:12:20 +0900 Subject: [PATCH 110/129] =?UTF-8?q?README.md=20=EC=9D=B8=ED=94=84=EB=9D=BC?= =?UTF-8?q?=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 1f32d5d7..bb4f8234 100644 --- a/README.md +++ b/README.md @@ -48,3 +48,11 @@

    + + +## 👩🏻‍💻🧑🏻‍💻 인프라 + +
    + +![image](https://github.com/user-attachments/assets/6e3e3128-04eb-45cb-b9d8-02c60c37b8f3) + From 4e72b2d2a80c3205f1e8f5d8ec1a7baa08dcf7ce Mon Sep 17 00:00:00 2001 From: Jinyoung Kim <83767872+JYKIM317@users.noreply.github.com> Date: Thu, 28 Nov 2024 19:43:13 +0900 Subject: [PATCH 111/129] =?UTF-8?q?README.md=20=EC=9D=B8=ED=94=84=EB=9D=BC?= =?UTF-8?q?=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bb4f8234..27aa8786 100644 --- a/README.md +++ b/README.md @@ -54,5 +54,6 @@
    -![image](https://github.com/user-attachments/assets/6e3e3128-04eb-45cb-b9d8-02c60c37b8f3) +![image](https://github.com/user-attachments/assets/8fb4260a-714d-49f7-8057-93d47da0bdb7) + From 49910ebc39eb6f236a2aa01469ad7e5d18218256 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Thu, 28 Nov 2024 20:59:28 +0900 Subject: [PATCH 112/129] =?UTF-8?q?fix:=20=EC=A2=81=EC=9D=80=20=EC=84=9C?= =?UTF-8?q?=EB=9E=8D=20=EC=83=81=EC=84=B8=EB=B3=B4=EA=B8=B0=20min-width=20?= =?UTF-8?q?=EB=B6=80=EC=97=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/app/components/cabinet/CabinetItemList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/app/components/cabinet/CabinetItemList.tsx b/client/src/app/components/cabinet/CabinetItemList.tsx index 6ce925ec..0e9720ef 100644 --- a/client/src/app/components/cabinet/CabinetItemList.tsx +++ b/client/src/app/components/cabinet/CabinetItemList.tsx @@ -131,7 +131,7 @@ const CabinetListItem = ({ item, isDesktop }: { item: Broadcast; isDesktop: bool {isTooltipVisible && (
    From 5f9e535cdbff8071f5c4ebaf4cde243bd7a1d3b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=B8=EB=B9=88?= Date: Thu, 28 Nov 2024 21:04:12 +0900 Subject: [PATCH 113/129] =?UTF-8?q?fix:=20=EB=A9=A4=EB=B2=84=EC=9D=98=20?= =?UTF-8?q?=EB=B0=A9=EC=86=A1=20=EC=95=84=EC=9D=B4=EB=94=94=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=A9=A4=EB=B2=84=20=EC=95=84=EC=9D=B4=EB=94=94?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EC=B6=9C=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/follow/follow.controller.ts | 20 +++++++++++++++---- applicationServer/src/follow/follow.module.ts | 3 ++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/applicationServer/src/follow/follow.controller.ts b/applicationServer/src/follow/follow.controller.ts index beccd072..e0d9e520 100644 --- a/applicationServer/src/follow/follow.controller.ts +++ b/applicationServer/src/follow/follow.controller.ts @@ -14,27 +14,39 @@ import { import { NeedLoginGuard } from '@src/auth/core/auth.guard'; import { FollowService } from '@follow/follow.service'; import { FOLLOWER, FOLLOWERS, FOLLOWING } from '@src/constants'; +import { MemberService } from '@src/member/member.service'; @Controller('follow') @UseGuards(NeedLoginGuard) export class FollowController { - constructor(private readonly followService: FollowService) {} + constructor( + private readonly followService: FollowService, + private readonly memberService: MemberService, + ) {} @Post() @HttpCode(201) async followMember(@Body('follower') follower: string, @Body('following') following: string) { if (follower === following) throw new HttpException('본인을 팔로우할 수 없습니다.', HttpStatus.BAD_REQUEST); - const isAlreadyFollowing = await this.followService.findOneFollowWithCondition({ follower, following }); + const followerId = (await this.memberService.findOneMemberWithCondition({ broadcast_id: follower })).id; + const followingId = (await this.memberService.findOneMemberWithCondition({ broadcast_id: following })).id; + const isAlreadyFollowing = await this.followService.findOneFollowWithCondition({ + follower: followerId, + following: followingId, + }); if (isAlreadyFollowing) throw new HttpException('이미 팔로우 중입니다.', HttpStatus.BAD_REQUEST); - await this.followService.followMember(follower, following); + await this.followService.followMember(followerId, followingId); } @Delete() @HttpCode(204) async unfollowMember(@Body('follower') follower: string, @Body('following') following: string) { if (follower === following) throw new HttpException('본인을 언팔로우할 수 없습니다.', HttpStatus.BAD_REQUEST); - const result = await this.followService.unfollowMember(follower, following); + const followerId = (await this.memberService.findOneMemberWithCondition({ broadcast_id: follower })).id; + const followingId = (await this.memberService.findOneMemberWithCondition({ broadcast_id: following })).id; + + const result = await this.followService.unfollowMember(followerId, followingId); if (result.affected === 0) { throw new HttpException('팔로우 중이지 않습니다.', HttpStatus.BAD_REQUEST); } diff --git a/applicationServer/src/follow/follow.module.ts b/applicationServer/src/follow/follow.module.ts index d6e1c50e..d0757ff2 100644 --- a/applicationServer/src/follow/follow.module.ts +++ b/applicationServer/src/follow/follow.module.ts @@ -4,9 +4,10 @@ import { followProvider } from '@follow/follow.providers'; import { FollowController } from '@follow/follow.controller'; import { FollowService } from '@follow/follow.service'; import { AuthModule } from '@src/auth/core/auth.module'; +import { MemberModule } from '@src/member/member.module'; @Module({ - imports: [DatabaseModule, AuthModule], + imports: [DatabaseModule, AuthModule, MemberModule], controllers: [FollowController], providers: [followProvider, FollowService], exports: [FollowService], From 07fe5d5304d5dc7eed180f72afbf873b298264fa Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Thu, 28 Nov 2024 21:06:21 +0900 Subject: [PATCH 114/129] =?UTF-8?q?fix:=20=EB=B9=84=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B0=80=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 비로그인 가드에서 토큰이 포함되어 오지 않을 때 401을 반환하던 문제를 해결하였습니다. --- applicationServer/src/auth/core/auth.guard.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/applicationServer/src/auth/core/auth.guard.ts b/applicationServer/src/auth/core/auth.guard.ts index d5884a5e..5defc95d 100644 --- a/applicationServer/src/auth/core/auth.guard.ts +++ b/applicationServer/src/auth/core/auth.guard.ts @@ -11,6 +11,7 @@ class NoNeedLoginGuard implements CanActivate { const request = context.switchToHttp().getRequest(); const accessToken = request.headers['authorization']?.split(' ')[1]; + if (accessToken == 'null') return true; if (accessToken && this.authService.verifyToken(accessToken)) { throw new HttpException('이미 로그인이 되어있습니다.', HttpStatus.BAD_REQUEST); } From 9d8a350cfc863455f41bb182ceafb85ad8eaec04 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Thu, 28 Nov 2024 21:13:48 +0900 Subject: [PATCH 115/129] =?UTF-8?q?refactor:=20=EB=B0=A9=EC=86=A1=20mock?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B3=A0=EC=A0=95=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=ED=95=A0=EB=8B=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 제목이 곧 내용입니다. --- .../src/live/mock/register-mock.util.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/applicationServer/src/live/mock/register-mock.util.ts b/applicationServer/src/live/mock/register-mock.util.ts index 03e2332f..c1f93472 100644 --- a/applicationServer/src/live/mock/register-mock.util.ts +++ b/applicationServer/src/live/mock/register-mock.util.ts @@ -1,5 +1,3 @@ -import { generateRandomName } from '@src/auth/util/name'; - function registerMockLive(live) { const mockBroadcastIdList = [ '872bc363-0d0c-4bde-a867-7b0edd076ad6', @@ -8,21 +6,28 @@ function registerMockLive(live) { '58fefadc-3fbd-40ad-bce1-f769c02c887f', 'fec06dd7-11b4-4efd-b69e-5a9dc93c3891', ]; + const mockNameList = [ + '부정적인회색늑대1145', + '완벽한고슴도치1272', + '열정적인눈표범1449', + '고상한닭1321', + '흐뭇한깃대말1177', + ]; + const contentCategoryList = ['music', 'talk', null, 'cook', 'mukbang']; const moodCategoryList = [null, 'calm', null, 'happy', null]; mockBroadcastIdList.forEach((data, idx) => { - const name = generateRandomName(); live.data.set(mockBroadcastIdList[idx], { broadcastId: mockBroadcastIdList[idx], broadcastPath: `${mockBroadcastIdList[idx]}`, - title: `${name}의 라이브 방송`, + title: `${mockNameList[idx]}의 라이브 방송`, contentCategory: contentCategoryList[idx], moodCategory: moodCategoryList[idx], tags: [`방송 ${idx}`], thumbnailUrl: `https://kr.object.ncloudstorage.com/media-storage/${mockBroadcastIdList[idx]}/dynamic_thumbnail.jpg`, viewerCount: 0, - userName: name, + userName: mockNameList[idx], profileImageUrl: 'https://kr.object.ncloudstorage.com/funch-storage/profile/profile_default.png', }); }); From d2fa36643ac51d920bdbe5588096aa390362b085 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Thu, 28 Nov 2024 21:19:35 +0900 Subject: [PATCH 116/129] =?UTF-8?q?feat:=20follow=20Provider=20console.log?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/next.config.mjs | 2 +- client/src/app/components/cabinet/CabinetItemList.tsx | 4 ++-- client/src/app/providers/FollowingLivesProvider.tsx | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/client/next.config.mjs b/client/next.config.mjs index d4afeb2f..6cc7cf44 100644 --- a/client/next.config.mjs +++ b/client/next.config.mjs @@ -3,7 +3,7 @@ const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL || '/api'; /** @type {import('next').NextConfig} */ const nextConfig = { compiler: { - removeConsole: process.env.NODE_ENV === 'production', + // removeConsole: process.env.NODE_ENV === 'production', }, rewrites: async () => { return [ diff --git a/client/src/app/components/cabinet/CabinetItemList.tsx b/client/src/app/components/cabinet/CabinetItemList.tsx index 0e9720ef..7681ca9f 100644 --- a/client/src/app/components/cabinet/CabinetItemList.tsx +++ b/client/src/app/components/cabinet/CabinetItemList.tsx @@ -22,13 +22,13 @@ const CabinetItemList = ({ isDesktop, isExpanded, isFolded, itemList }: ItemList <> {isFolded ? ( <> - {itemList.map((item: Broadcast, key) => ( + {itemList.map((item: Broadcast) => ( ))} ) : ( <> - {foldedContent.map((item: Broadcast, key) => ( + {foldedContent.map((item: Broadcast) => ( ))} diff --git a/client/src/app/providers/FollowingLivesProvider.tsx b/client/src/app/providers/FollowingLivesProvider.tsx index 227d8a05..19ff65e9 100644 --- a/client/src/app/providers/FollowingLivesProvider.tsx +++ b/client/src/app/providers/FollowingLivesProvider.tsx @@ -28,9 +28,14 @@ export const FollowingLivesProvider = ({ children }: PropsWithChildren) => { setIsError(false); const fetchedLives = await getFollowingLiveList(); + console.log('fetchedLives:', fetchedLives); const fetchedFollowingLives = fetchedLives.onAir.map((live) => live.broadCastData); + + console.log('fetchedFollowingLives:', fetchedFollowingLives); const fetchedFollowingOfflines = fetchedLives.offAir; + console.log('fetchedFollowingOfflines:', fetchedFollowingOfflines); + setIds(fetchedLives.onAir.map((live) => live.broadCastData.broadcastId)); setLives(fetchedFollowingLives); setOfflines(fetchedFollowingOfflines); From ac1da0fc806365e9e898d33f90f1a98afe8a74ac Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Thu, 28 Nov 2024 21:28:06 +0900 Subject: [PATCH 117/129] =?UTF-8?q?fix:=20fetchFollowingLives=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/app/providers/FollowingLivesProvider.tsx | 8 ++------ client/src/libs/internalTypes.ts | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/client/src/app/providers/FollowingLivesProvider.tsx b/client/src/app/providers/FollowingLivesProvider.tsx index 19ff65e9..f0ebaa0e 100644 --- a/client/src/app/providers/FollowingLivesProvider.tsx +++ b/client/src/app/providers/FollowingLivesProvider.tsx @@ -28,15 +28,11 @@ export const FollowingLivesProvider = ({ children }: PropsWithChildren) => { setIsError(false); const fetchedLives = await getFollowingLiveList(); - console.log('fetchedLives:', fetchedLives); - const fetchedFollowingLives = fetchedLives.onAir.map((live) => live.broadCastData); + const fetchedFollowingLives = fetchedLives.onAir.map((live) => live.broadcastData); - console.log('fetchedFollowingLives:', fetchedFollowingLives); const fetchedFollowingOfflines = fetchedLives.offAir; - console.log('fetchedFollowingOfflines:', fetchedFollowingOfflines); - - setIds(fetchedLives.onAir.map((live) => live.broadCastData.broadcastId)); + setIds(fetchedLives.onAir.map((live) => live.broadcastData.broadcastId)); setLives(fetchedFollowingLives); setOfflines(fetchedFollowingOfflines); diff --git a/client/src/libs/internalTypes.ts b/client/src/libs/internalTypes.ts index 00412b8e..6b9bdefe 100644 --- a/client/src/libs/internalTypes.ts +++ b/client/src/libs/internalTypes.ts @@ -82,7 +82,7 @@ export type FollowingList = { export type OnAirBroadcast = { playlistUrl: string; - broadCastData: Broadcast; + broadcastData: Broadcast; }; export type User2 = { From 39e3ce68221e607890d38820b2e1a2f22358eb80 Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Thu, 28 Nov 2024 21:28:35 +0900 Subject: [PATCH 118/129] =?UTF-8?q?fix:=20removeConsole=20=EB=B6=80?= =?UTF-8?q?=ED=99=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/next.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/next.config.mjs b/client/next.config.mjs index 6cc7cf44..d4afeb2f 100644 --- a/client/next.config.mjs +++ b/client/next.config.mjs @@ -3,7 +3,7 @@ const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL || '/api'; /** @type {import('next').NextConfig} */ const nextConfig = { compiler: { - // removeConsole: process.env.NODE_ENV === 'production', + removeConsole: process.env.NODE_ENV === 'production', }, rewrites: async () => { return [ From 818c5bf63ccde53905ef30c2e2793b25d09a7f0a Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Thu, 28 Nov 2024 21:29:36 +0900 Subject: [PATCH 119/129] =?UTF-8?q?refactor:=20deployment.yml=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pm2 reload -> pm2 reload ecosystem.config.js 로 변경했습니다. --- .github/workflows/deployment.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 9d5ece1f..7bf7705f 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -21,7 +21,7 @@ jobs: cd /root/web25-funch git switch main git pull origin main - pm2 reload + pm2 reload ecosystem.config.js media-deploy: runs-on: ubuntu-latest @@ -39,4 +39,4 @@ jobs: cd /service/web25-funch git pull origin main cd mediaServer - pm2 reload + pm2 reload ecosystem.config.js From 227132c682e4be9239996b5fe6dd18f0ce9cd14f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=B8=EB=B9=88?= Date: Thu, 28 Nov 2024 21:45:04 +0900 Subject: [PATCH 120/129] =?UTF-8?q?feat:=20=ED=8C=94=EB=A1=9C=EC=9A=B0/?= =?UTF-8?q?=EC=96=B8=ED=8C=94=EB=A1=9C=EC=9A=B0=20API=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=20=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 응답으로 OK 객체를 반환한다. --- applicationServer/src/follow/follow.controller.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/applicationServer/src/follow/follow.controller.ts b/applicationServer/src/follow/follow.controller.ts index e0d9e520..0da582ec 100644 --- a/applicationServer/src/follow/follow.controller.ts +++ b/applicationServer/src/follow/follow.controller.ts @@ -37,6 +37,7 @@ export class FollowController { if (isAlreadyFollowing) throw new HttpException('이미 팔로우 중입니다.', HttpStatus.BAD_REQUEST); await this.followService.followMember(followerId, followingId); + return { response: 'OK' }; } @Delete() @@ -50,6 +51,7 @@ export class FollowController { if (result.affected === 0) { throw new HttpException('팔로우 중이지 않습니다.', HttpStatus.BAD_REQUEST); } + return { response: 'OK' }; } @Get(':memberId') From e11bb68701da195d54d4c8eb75f4f12c65b3dd7b Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Thu, 28 Nov 2024 21:53:33 +0900 Subject: [PATCH 121/129] =?UTF-8?q?refactor:=20unfollow=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EC=BD=94=EB=93=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 본문을 포함하기 위해 204 -> 200으로 변경하였습니다. --- applicationServer/src/follow/follow.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applicationServer/src/follow/follow.controller.ts b/applicationServer/src/follow/follow.controller.ts index 0da582ec..6834785a 100644 --- a/applicationServer/src/follow/follow.controller.ts +++ b/applicationServer/src/follow/follow.controller.ts @@ -41,7 +41,7 @@ export class FollowController { } @Delete() - @HttpCode(204) + @HttpCode(200) async unfollowMember(@Body('follower') follower: string, @Body('following') following: string) { if (follower === following) throw new HttpException('본인을 언팔로우할 수 없습니다.', HttpStatus.BAD_REQUEST); const followerId = (await this.memberService.findOneMemberWithCondition({ broadcast_id: follower })).id; From faec4bf3c389b4388ec019e042a08e2991b766d3 Mon Sep 17 00:00:00 2001 From: Wille Lee <1992season@gmail.com> Date: Thu, 28 Nov 2024 21:56:12 +0900 Subject: [PATCH 122/129] =?UTF-8?q?hotfix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EC=95=8C=EB=9F=AC=ED=8A=B8=20=EC=97=86=EC=95=A0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/app/(auth)/github/callback/features/AuthGithub.tsx | 3 ++- client/src/app/(auth)/google/callback/features/AuthGoogle.tsx | 3 ++- client/src/app/(auth)/naver/callback/features/AuthNaver.tsx | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/client/src/app/(auth)/github/callback/features/AuthGithub.tsx b/client/src/app/(auth)/github/callback/features/AuthGithub.tsx index 4c2680e9..6b3107fa 100644 --- a/client/src/app/(auth)/github/callback/features/AuthGithub.tsx +++ b/client/src/app/(auth)/github/callback/features/AuthGithub.tsx @@ -22,7 +22,8 @@ const AuthGithub = ({ authCode }: Props) => { saveUserSession(fetchResult); } catch (err) { if (!isValidEffect) return; - alert('로그인에 실패했어요.'); + console.log('로그인에 실패했어요.'); + // alert('로그인에 실패했어요.'); } finally { if (!isValidEffect) return; replace('/'); diff --git a/client/src/app/(auth)/google/callback/features/AuthGoogle.tsx b/client/src/app/(auth)/google/callback/features/AuthGoogle.tsx index 50cb8378..123eadf9 100644 --- a/client/src/app/(auth)/google/callback/features/AuthGoogle.tsx +++ b/client/src/app/(auth)/google/callback/features/AuthGoogle.tsx @@ -22,7 +22,8 @@ const AuthGoogle = ({ authCode }: Props) => { saveUserSession(fetchResult); } catch (err) { if (!isValidEffect) return; - alert('로그인에 실패했어요.'); + console.log('로그인에 실패했어요.'); + // alert('로그인에 실패했어요.'); } finally { if (!isValidEffect) return; replace('/'); diff --git a/client/src/app/(auth)/naver/callback/features/AuthNaver.tsx b/client/src/app/(auth)/naver/callback/features/AuthNaver.tsx index 3cd2d013..f16744a4 100644 --- a/client/src/app/(auth)/naver/callback/features/AuthNaver.tsx +++ b/client/src/app/(auth)/naver/callback/features/AuthNaver.tsx @@ -26,7 +26,8 @@ const AuthNaver = ({ authCode, authState }: Props) => { saveUserSession(fetchResult); } catch (err) { if (!isValidEffect) return; - alert('로그인에 실패했어요.'); + console.log('로그인에 실패했어요.'); + // alert('로그인에 실패했어요.'); } finally { if (!isValidEffect) return; replace('/'); From e79133736727d33fc80bea4590a7180e8a0bbcee Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Thu, 28 Nov 2024 22:00:51 +0900 Subject: [PATCH 123/129] =?UTF-8?q?docs:=20README(FE)=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 27aa8786..477add36 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,7 @@ ### FE - - + ### BE From 29df12f8d021ca488222f8ca74e4e14bbaa014fe Mon Sep 17 00:00:00 2001 From: Jungwoo Hong Date: Thu, 28 Nov 2024 23:37:50 +0900 Subject: [PATCH 124/129] =?UTF-8?q?fix:=20body=EC=97=90=20formData=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/libs/actions.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/src/libs/actions.ts b/client/src/libs/actions.ts index 92009e26..9f192531 100644 --- a/client/src/libs/actions.ts +++ b/client/src/libs/actions.ts @@ -158,9 +158,14 @@ export const authenticateByNaver = async ({ }; export const updateInfo = async (formData: Update): Promise => { + const requestBody = formData as any; + const result = await fetcher({ method: 'PATCH', url: '/api/live/update', + customOptions: { + body: requestBody, + }, }); return result; From e593a188474af4607bbb341f32120347132215d0 Mon Sep 17 00:00:00 2001 From: Jinyoung Kim <83767872+JYKIM317@users.noreply.github.com> Date: Thu, 28 Nov 2024 23:40:31 +0900 Subject: [PATCH 125/129] =?UTF-8?q?=EA=B8=B0=EC=88=A0=EC=8A=A4=ED=83=9D=20?= =?UTF-8?q?BE=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 477add36..2b527562 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,7 @@ ### BE - - +

    From 4c563117ff1a5ca787957eb9b5851d3842fa7b0d Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Thu, 28 Nov 2024 23:43:10 +0900 Subject: [PATCH 126/129] =?UTF-8?q?fix:=20=EB=B0=A9=EC=86=A1=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20API=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 방송 중일 때 Not Found를 반환하던 문제를 해결했습니다. --- applicationServer/src/live/live.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applicationServer/src/live/live.service.ts b/applicationServer/src/live/live.service.ts index df6c292d..ff137e02 100644 --- a/applicationServer/src/live/live.service.ts +++ b/applicationServer/src/live/live.service.ts @@ -84,7 +84,7 @@ export class LiveService { async updateLiveData(tokenPayload, requestBody) { const member = await this.memberService.findOneMemberWithCondition({ id: tokenPayload.memberId }); - if (this.live.data.has(member.broadcast_id)) throw new HttpException('Not Found', HttpStatus.NOT_FOUND); + if (!this.live.data.has(member.broadcast_id)) throw new HttpException('Not Found', HttpStatus.NOT_FOUND); if (requestBody.thumbnail) { const imageData = Buffer.from(requestBody.thumbnail, 'base64'); From d3348d5801ad12c9c1b52f185757b280374bce93 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Thu, 28 Nov 2024 23:48:38 +0900 Subject: [PATCH 127/129] =?UTF-8?q?refactor:=20=EB=B0=A9=EC=86=A1=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20API?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 정적으로 생성해서 전달하는 이미지 URL의 Path를 변경하였습니다. --- applicationServer/src/live/live.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/applicationServer/src/live/live.service.ts b/applicationServer/src/live/live.service.ts index ff137e02..f59d3e93 100644 --- a/applicationServer/src/live/live.service.ts +++ b/applicationServer/src/live/live.service.ts @@ -85,20 +85,20 @@ export class LiveService { async updateLiveData(tokenPayload, requestBody) { const member = await this.memberService.findOneMemberWithCondition({ id: tokenPayload.memberId }); if (!this.live.data.has(member.broadcast_id)) throw new HttpException('Not Found', HttpStatus.NOT_FOUND); + const memberLiveData = this.live.data.get(member.broadcast_id); if (requestBody.thumbnail) { const imageData = Buffer.from(requestBody.thumbnail, 'base64'); - uploadData(`${member.broadcast_id}/static_thumbnail.jpg`, imageData); + uploadData(`${memberLiveData.broadcastPath}/static_thumbnail.jpg`, imageData); } - const memberLiveData = this.live.data.get(member.broadcast_id); memberLiveData.title = requestBody.title; memberLiveData.contentCategory = requestBody.contentCategory; memberLiveData.moodCategory = requestBody.moodCategory; memberLiveData.tags = requestBody.tags; memberLiveData.thumbnailUrl = requestBody.thumbnail - ? `https://kr.object.ncloudstorage.com/media-storage/${member.broadcast_id}/static_thumbnail.jpg` - : `https://kr.object.ncloudstorage.com/media-storage/${member.broadcast_id}/dynamic_thumbnail.jpg`; + ? `https://kr.object.ncloudstorage.com/media-storage/${memberLiveData.broadcastPath}/static_thumbnail.jpg` + : `https://kr.object.ncloudstorage.com/media-storage/${memberLiveData.broadcastPath}/dynamic_thumbnail.jpg`; } notifyLiveDataInterval(broadcastId: string, req: Request) { From bb1219ad81fe39a8796ae4b0c1e0d931a08dc1a8 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Fri, 29 Nov 2024 00:14:10 +0900 Subject: [PATCH 128/129] =?UTF-8?q?refactor:=20=EB=B0=A9=EC=86=A1=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20API?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이미지를 전달받았을 때 확장자를 파악하고 이미지 데이터만 저장하도록 변경하였습니다. --- applicationServer/src/live/live.service.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/applicationServer/src/live/live.service.ts b/applicationServer/src/live/live.service.ts index f59d3e93..2d54d7c8 100644 --- a/applicationServer/src/live/live.service.ts +++ b/applicationServer/src/live/live.service.ts @@ -87,9 +87,13 @@ export class LiveService { if (!this.live.data.has(member.broadcast_id)) throw new HttpException('Not Found', HttpStatus.NOT_FOUND); const memberLiveData = this.live.data.get(member.broadcast_id); + let fileEXT = '.jpg'; if (requestBody.thumbnail) { - const imageData = Buffer.from(requestBody.thumbnail, 'base64'); - uploadData(`${memberLiveData.broadcastPath}/static_thumbnail.jpg`, imageData); + const [imageHeader, imageString] = requestBody.thumbnail.split(','); + const dataType = imageHeader.split(':')[1]; + fileEXT = dataType.match(/^image\/([a-z0-9\-+]+);base64$/)[0]; + const imageData = Buffer.from(imageString, 'base64'); + uploadData(`${memberLiveData.broadcastPath}/static_thumbnail.${fileEXT}`, imageData); } memberLiveData.title = requestBody.title; @@ -97,7 +101,7 @@ export class LiveService { memberLiveData.moodCategory = requestBody.moodCategory; memberLiveData.tags = requestBody.tags; memberLiveData.thumbnailUrl = requestBody.thumbnail - ? `https://kr.object.ncloudstorage.com/media-storage/${memberLiveData.broadcastPath}/static_thumbnail.jpg` + ? `https://kr.object.ncloudstorage.com/media-storage/${memberLiveData.broadcastPath}/static_thumbnail.${fileEXT}` : `https://kr.object.ncloudstorage.com/media-storage/${memberLiveData.broadcastPath}/dynamic_thumbnail.jpg`; } @@ -106,7 +110,8 @@ export class LiveService { this.live.data.get(broadcastId).viewerCount++; req.on('close', () => { - this.live.data.get(broadcastId).viewerCount--; + const liveData = this.live.data.get(broadcastId); + if (liveData) liveData.viewerCount--; }); return interval(NOTIFY_LIVE_DATA_INTERVAL_TIME).pipe(map(() => ({ data: this.live.data.get(broadcastId) }))); From 85d2c8c25676bc05f9143db4a999585c1faea625 Mon Sep 17 00:00:00 2001 From: JYKIM317 Date: Fri, 29 Nov 2024 00:27:12 +0900 Subject: [PATCH 129/129] =?UTF-8?q?refactor:=20=EB=B0=A9=EC=86=A1=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20API?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이미지 확장자 반환 코드 변경 --- applicationServer/src/live/live.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applicationServer/src/live/live.service.ts b/applicationServer/src/live/live.service.ts index 2d54d7c8..16ba5d3a 100644 --- a/applicationServer/src/live/live.service.ts +++ b/applicationServer/src/live/live.service.ts @@ -91,7 +91,7 @@ export class LiveService { if (requestBody.thumbnail) { const [imageHeader, imageString] = requestBody.thumbnail.split(','); const dataType = imageHeader.split(':')[1]; - fileEXT = dataType.match(/^image\/([a-z0-9\-+]+);base64$/)[0]; + fileEXT = dataType.match(/^image\/([a-z0-9\-+]+);base64$/)[1]; const imageData = Buffer.from(imageString, 'base64'); uploadData(`${memberLiveData.broadcastPath}/static_thumbnail.${fileEXT}`, imageData); }