Skip to content

Commit

Permalink
Merge pull request #307 from boostcampwm-2024/develop
Browse files Browse the repository at this point in the history
[Merge] 6주차 메인 배포 2회차
  • Loading branch information
gamgyul163 authored Dec 4, 2024
2 parents bf68b0a + ea8c960 commit cd9b040
Show file tree
Hide file tree
Showing 56 changed files with 837 additions and 892 deletions.
2 changes: 1 addition & 1 deletion Backend/apps/api/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class AuthController {
return this.handleOAuthCallback(req, res);
}

@Get('lico/callback')
@Get('lico/guest')
@UseGuards(AuthGuard('lico'))
async licoAuthCallback(@Req() req: Request & { user: any }, @Res() res: Response) {
return this.handleOAuthCallback(req, res);
Expand Down
2 changes: 1 addition & 1 deletion Backend/apps/api/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import { ConfigService } from '@nestjs/config';

type OAuthPlatform = 'google' | 'github' | 'naver';
type OAuthPlatform = 'google' | 'github' | 'naver' | 'lico';

@Injectable()
export class AuthService {
Expand Down
14 changes: 11 additions & 3 deletions Backend/apps/api/src/auth/strategies/lico.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,26 @@ import { Strategy } from 'passport-custom';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import * as crypto from 'crypto';
import { adjectives, nouns } from './nickname-data'

@Injectable()
export class LicoStrategy extends PassportStrategy(Strategy, 'lico') {
async validate(req: Request, done: Function) {
try {
const oauthUid = crypto.randomBytes(16).toString('hex');

// 랜덤 닉네임 생성
const randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)];
const randomNoun = nouns[Math.floor(Math.random() * nouns.length)];
const nickname = `${randomAdjective} ${randomNoun}`;

const profileNumber = Math.floor(Math.random() * 31)

const userData = {
oauthUid,
provider: 'lico' as 'lico',
nickname: `User_${oauthUid.substring(0, 8)}`,
profileImage: null,
nickname,
profileImage: `https://kr.object.ncloudstorage.com/lico.image/default-profile-image/lico_${profileNumber}.png`,
email: null,
};

Expand All @@ -22,4 +30,4 @@ export class LicoStrategy extends PassportStrategy(Strategy, 'lico') {
done(error, false);
}
}
}
}
25 changes: 25 additions & 0 deletions Backend/apps/api/src/auth/strategies/nickname-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const adjectives = [
'용감한', '배고픈', '행복한', '슬픈', '귀여운',
'똑똑한', '멋진', '예쁜', '강한', '부드러운',
'신나는', '즐거운', '차가운', '뜨거운', '재미있는',
'친절한', '성실한', '조용한', '시끄러운', '빠른',
'느린', '젊은', '늙은', '건강한', '아픈',
'화난', '놀란', '긴장한', '자신감있는', '호기심많은',
'사랑스러운', '섹시한', '차분한', '활발한', '용의주도한',
'검소한', '풍요로운', '예의바른', '거친', '부지런한',
'게으른', '독특한', '평범한', '엄격한', '유연한',
'진지한', '명랑한', '냉정한', '따뜻한', '낙천적인',
];

export const nouns = [
'사자', '호랑이', '토끼', '코끼리', '독수리',
'고래', '감자', '토마토', '사과', '바나나',
'강아지', '고양이', '여우', '늑대', '곰',
'펭귄', '기린', '원숭이', '돼지', '닭',
'양', '염소', '소', '말', '다람쥐',
'독사', '표범', '하마', '코뿔소', '캥거루',
'수달', '돌고래', '새우', '게', '불가사리',
'달팽이', '나비', '벌', '개미', '거미',
'두꺼비', '개구리', '뱀', '도마뱀', '악어',
'코알라', '판다', '사슴', '너구리', '오소리',
];
9 changes: 5 additions & 4 deletions Backend/apps/api/src/follow/follow.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,13 @@ export class FollowService {

// 팔로워 수
async getFollowerCount(streamerId: number): Promise<number> {
const count = await this.usersRepository
const result = await this.usersRepository
.createQueryBuilder('user')
.innerJoin('user.followers', 'follower')
.leftJoin('user.followers', 'follower')
.where('user.id = :streamerId', { streamerId })
.getCount();
.select('COUNT(follower.id)', 'count')
.getRawOne();

return count;
return parseInt(result.count, 10);
}
}
5 changes: 5 additions & 0 deletions Backend/apps/api/src/lives/entity/live.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@ import {
import { LivesDto } from '../dto/lives.dto';
import { LiveDto } from '../dto/live.dto';
import { StatusDto } from '../dto/status.dto';
import { Index } from 'typeorm';


@Index(['onAir', 'categoriesId'])
@Entity('lives')
export class LiveEntity {
@PrimaryGeneratedColumn({ name: 'lives_id' })
id: number;

@Index()
@Column({ name: 'categories_id', type: 'int', nullable: true })
categoriesId: number | null;

Expand All @@ -39,6 +43,7 @@ export class LiveEntity {
@Column({ name: 'streaming_key', type: 'varchar', length: 36 })
streamingKey: string;

@Index()
@Column({ name: 'onair', type: 'boolean', nullable: true })
onAir: boolean | null;

Expand Down
4 changes: 2 additions & 2 deletions Backend/apps/api/src/users/dto/create.user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ export class CreateUserDto {
@IsString()
oauthUid: string;

@IsEnum(['naver', 'github', 'google'])
oauthPlatform: 'naver' | 'github' | 'google';
@IsEnum(['naver', 'github', 'google', 'lico'])
oauthPlatform: 'naver' | 'github' | 'google'| 'lico';

@IsString()
@IsOptional()
Expand Down
8 changes: 4 additions & 4 deletions Backend/apps/api/src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class UsersService {

async findByOAuthUid(
oauthUid: string,
oauthPlatform: 'naver' | 'github' | 'google',
oauthPlatform: 'naver' | 'github' | 'google'|'lico',
): Promise<UserEntity | null> {
return this.usersRepository.findOne({
where: { oauthUid, oauthPlatform },
Expand All @@ -59,10 +59,10 @@ export class UsersService {
async createUser(createUserDto: CreateUserDto): Promise<UserEntity> {
return this.connection.transaction(async manager => {
const live = manager.create(LiveEntity, {
categoriesId: null,
categoriesId: 4,
channelId: randomUUID(),
name: null,
description: null,
name: `${createUserDto.nickname}의 라이브 방송`,
description: `${createUserDto.nickname}의 라이브 방송입니다`,
streamingKey: randomUUID(),
onAir: false,
startedAt: null,
Expand Down
5 changes: 5 additions & 0 deletions Frontend/src/apis/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ export const authApi = {
});
return response.data;
},

async guestLogin(): Promise<AuthResponse> {
const response = await api.get<AuthResponse>('/auth/lico/guest');
return response.data;
},
};
8 changes: 4 additions & 4 deletions Frontend/src/components/VideoPlayer/Control/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { HLSQuality } from '@/types/hlsQuality';
interface ControlsProps {
isPlaying: boolean;
isFullScreen: boolean;
videoPlayerState: string;
isTheaterMode: boolean;
showControls: boolean;
volume: number;
isMuted: boolean;
Expand All @@ -26,7 +26,7 @@ const CONTROL_ICON_SIZE = 24;
export default function Controls({
isPlaying,
isFullScreen,
videoPlayerState,
isTheaterMode,
showControls,
volume,
isMuted,
Expand Down Expand Up @@ -91,8 +91,8 @@ export default function Controls({
type="button"
onClick={onFullScreenToggle}
className="hover:text-lico-orange-2"
aria-label={videoPlayerState === 'theater' ? '전체화면 종료' : '전체화면'}
aria-pressed={videoPlayerState === 'theater'}
aria-label={isTheaterMode ? '전체화면 종료' : '전체화면'}
aria-pressed={isTheaterMode}
>
{isFullScreen ? <LuMinimize size={CONTROL_ICON_SIZE} /> : <LuMaximize size={CONTROL_ICON_SIZE} />}
</button>
Expand Down
8 changes: 4 additions & 4 deletions Frontend/src/components/VideoPlayer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useState, useRef, useEffect } from 'react';
import useLayoutStore from '@store/useLayoutStore';
import useHls from '@hooks/useHls';
import LoadingSpinner from '@components/common/LoadingSpinner';
import Badge from '@components/common/Badges/Badge';
import OfflinePlayer from '@components/VideoPlayer/OfflinePlayer';
import useViewMode from '@store/useViewMode';
import Controls from './Control/index';

interface VideoPlayerProps {
Expand All @@ -18,7 +18,7 @@ export default function VideoPlayer({ streamUrl, onAir }: VideoPlayerProps) {
const [isFullScreen, setIsFullScreen] = useState(false);
const [showControls, setShowControls] = useState(true);
const [showCursor, setShowCursor] = useState(true);
const { videoPlayerState, toggleVideoPlayer } = useLayoutStore();
const { isTheaterMode, toggleTheaterMode } = useViewMode();

const videoRef = useRef<HTMLVideoElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -162,15 +162,15 @@ export default function VideoPlayer({ streamUrl, onAir }: VideoPlayerProps) {
<Controls
isPlaying={isPlaying}
isFullScreen={isFullScreen}
videoPlayerState={videoPlayerState}
isTheaterMode={isTheaterMode}
showControls={showControls}
volume={volume}
isMuted={isMuted}
onPlayToggle={togglePlay}
onVolumeChange={handleVolumeChange}
onMuteToggle={toggleMute}
onFullScreenToggle={toggleFullScreen}
onVideoPlayerToggle={toggleVideoPlayer}
onVideoPlayerToggle={toggleTheaterMode}
onShowControls={handleShowControls}
qualities={qualities}
setQuality={setQuality}
Expand Down
10 changes: 5 additions & 5 deletions Frontend/src/components/category/CategoryCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ export default function CategoryCard({ id, name, image, totalViewers, totalLives
return (
<button type="button" className="mb-3 cursor-pointer text-left" onClick={handleClick}>
<div className="relative inline-block w-full">
<img src={image} alt={name} className="aspect-[3/4] w-[calc(20vw-12px)] rounded-xl object-cover" />
<div className="absolute left-1.5 top-1.5 flex items-center gap-1 rounded-[4px] bg-black bg-opacity-60 px-1">
<img src={image} alt={name} className="aspect-[3/4] rounded-xl object-cover" />
<div className="absolute left-1.5 top-1.5 flex items-center gap-1 rounded-md bg-black bg-opacity-60 px-1">
<FaCircle className="h-[6px] w-[6px] text-[#E02120]" />
<span className="mt-0.5 font-bold text-xs text-white">{formatUnit(totalViewers)}</span>
<span className="py-[1px] font-bold text-xs text-white">{formatUnit(totalViewers)}</span>
</div>
</div>
<div className="mx-0.5 mt-2 px-[3px]">
<p className="font-bold text-sm text-lico-gray-1">{name}</p>
<p className="font-medium text-xs text-lico-gray-2">라이브 {totalLives}</p>
<p className="font-bold text-lg text-lico-gray-1">{name}</p>
<p className="font-medium text-base text-lico-gray-2">라이브 {totalLives}</p>
</div>
</button>
);
Expand Down
16 changes: 9 additions & 7 deletions Frontend/src/components/category/CategoryGrid/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ type CategoryGridProps = {

export default function CategoryGrid({ categories }: CategoryGridProps) {
return (
<ul className="grid min-w-[851px] grid-cols-5 gap-4 px-4">
{categories.map(category => (
<li key={category.id} className="min-w-[151px]">
<CategoryCard {...category} />
</li>
))}
</ul>
<div className="container mx-auto px-4">
<ul className="grid grid-cols-3 gap-4 cards-4:grid-cols-4 cards-5:grid-cols-5 cards-6:grid-cols-6">
{categories.map(category => (
<li key={category.id} className="min-w-[151px]">
<CategoryCard {...category} />
</li>
))}
</ul>
</div>
);
}
21 changes: 17 additions & 4 deletions Frontend/src/components/channel/ChannelCard/ChannelInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
import CategoryBadge from '@components/common/Badges/CategoryBadge';
import { Link } from 'react-router-dom';

interface ChannelInfoProps {
id: string;
title: string;
streamerName: string;
category: string;
categoryId: number;
profileImgUrl: string;
}

export default function ChannelInfo({ title, streamerName, category, categoryId, profileImgUrl }: ChannelInfoProps) {
export default function ChannelInfo({
id,
title,
streamerName,
category,
categoryId,
profileImgUrl,
}: ChannelInfoProps) {
return (
<div className="flex items-start gap-2 pt-2">
<img className="h-10 w-10 rounded-full bg-lico-gray-4" src={profileImgUrl} alt={streamerName} />
<Link to={`/channel/${id}`} aria-label={`${streamerName}${title} 스트림으로 이동`}>
<img className="h-10 w-10 rounded-full bg-lico-gray-4" src={profileImgUrl} alt={streamerName} />
</Link>
<div>
<h3 className="line-clamp-2 max-h-[48px] font-bold text-base text-lico-orange-2">{title}</h3>
<h3 className="line-clamp-1 font-medium text-xs text-lico-gray-2">{streamerName}</h3>
<Link to={`/channel/${id}`} aria-label={`${streamerName}${title} 스트림으로 이동`}>
<h3 className="line-clamp-2 max-h-[48px] font-bold text-base text-lico-orange-2">{title}</h3>
<h3 className="line-clamp-1 font-medium text-xs text-lico-gray-2">{streamerName}</h3>
</Link>
<CategoryBadge category={category} categoryId={categoryId} className="text-xs text-lico-gray-1" />
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ function ChannelThumbnail({ title, thumbnailUrl, viewers }: ChannelThumbnailProp
/>
{!isLoading && !imgError && (
<div className="flex gap-2">
<Badge text="LIVE" className="absolute left-2 top-2 bg-red-600 font-bold text-sm text-white" />
<Badge text="LIVE" className="absolute left-2 top-2 bg-[#E02120] font-bold text-sm text-white" />
<Badge
text={`${formatNumber(viewers)}명`}
className="absolute bottom-2 left-2 bg-orange-500 font-bold text-sm text-white"
className="absolute bottom-2 left-2 bg-lico-orange-2 font-bold text-sm text-white"
/>
</div>
)}
Expand Down
19 changes: 14 additions & 5 deletions Frontend/src/components/channel/ChannelCard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Link } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import ChannelThumbnail from '@components/channel/ChannelCard/ChannelThumbnail';
import HoverPreviewPlayer from '@components/channel/ChannelCard/HoverPreviewPlayer';
import { useRef, useState } from 'react';
Expand Down Expand Up @@ -27,6 +27,7 @@ export default function ChannelCard({
}: ChannelCardProps) {
const [showPreview, setShowPreview] = useState(false);
const timerRef = useRef<NodeJS.Timeout>();
const navigate = useNavigate();

const handleMouseEnter = () => {
timerRef.current = setTimeout(() => {
Expand All @@ -41,13 +42,20 @@ export default function ChannelCard({
setShowPreview(false);
};

const handleCardClick = (e: React.MouseEvent) => {
const target = e.target as HTMLElement;
if (!target.closest('[data-category-badge]')) {
navigate(`/live/${id}`);
}
};

return (
<Link
to={`/live/${id}`}
className="relative mb-4 block min-w-60"
<div
className="relative mb-4 block min-w-60 cursor-pointer"
aria-label={`${streamerName}${title} 스트림으로 이동`}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleCardClick}
>
<div className="relative aspect-video">
<ChannelThumbnail title={title} thumbnailUrl={thumbnailUrl} viewers={viewers} />
Expand All @@ -58,12 +66,13 @@ export default function ChannelCard({
)}
</div>
<ChannelInfo
id={id}
title={title}
streamerName={streamerName}
category={category}
categoryId={categoryId}
profileImgUrl={profileImgUrl}
/>
</Link>
</div>
);
}
2 changes: 1 addition & 1 deletion Frontend/src/components/channel/ChannelGrid/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface ChannelGridProps {
function ChannelGrid({ channels }: ChannelGridProps) {
return (
<div className="container mx-auto px-4">
<ul className="cards-4:grid-cols-4 cards-5:grid-cols-5 cards-6:grid-cols-6 grid min-w-[752px] grid-cols-3 gap-4">
<ul className="grid min-w-[996px] grid-cols-3 gap-4 cards-4:grid-cols-4 cards-5:grid-cols-5 cards-6:grid-cols-6">
{channels.map(channel => (
<li key={channel.id}>
<ChannelCard {...channel} />
Expand Down
Loading

0 comments on commit cd9b040

Please sign in to comment.