diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts index b45d6d74..ce9b9d95 100644 --- a/apps/api/src/auth/auth.controller.ts +++ b/apps/api/src/auth/auth.controller.ts @@ -82,7 +82,7 @@ export class AuthController { this.loginProcess(response, userId); } - @Post('logout') + @Get('logout') @ApiOperation({ summary: '로그아웃' }) @ApiResponse({ status: 302, description: '홈으로 리다이렉션' }) logout(@Res() response: Response) { diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts index 30baed98..2ecfe0d0 100644 --- a/apps/api/src/auth/auth.service.ts +++ b/apps/api/src/auth/auth.service.ts @@ -1,6 +1,7 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; +import { Provider } from '@repo/types'; import { CreateSocialUserDto } from '@/user/dto/createSocialUser.dto'; import { UserService } from '@/user/user.service'; @@ -15,7 +16,7 @@ export class AuthService { ) {} async signupLocal(signupRequestDto: LocalSignupRequestDto) { - return this.userService.createLocalUser({ provider: 'local', ...signupRequestDto }); + return this.userService.createLocalUser({ provider: Provider.local, ...signupRequestDto }); } async validateLocalLogin(username: string, inputPassword: string) { @@ -45,7 +46,7 @@ export class AuthService { }; const user = await this.userService.findUserByUsername(guestUser.username); if (!user) { - return this.userService.createLocalUser({ provider: 'guest', ...guestUser }); + return this.userService.createLocalUser({ provider: Provider.guest, ...guestUser }); } return user; } diff --git a/apps/api/src/auth/github/github.strategy.ts b/apps/api/src/auth/github/github.strategy.ts index cf8c4502..6fdf01c6 100644 --- a/apps/api/src/auth/github/github.strategy.ts +++ b/apps/api/src/auth/github/github.strategy.ts @@ -2,11 +2,12 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; import { Profile, Strategy } from 'passport-github2'; +import { Provider } from '@repo/types'; import { AuthService } from '../auth.service'; @Injectable() -export class GitHubStrategy extends PassportStrategy(Strategy, 'github') { +export class GitHubStrategy extends PassportStrategy(Strategy, Provider.github) { constructor( private configService: ConfigService, private authService: AuthService @@ -23,7 +24,7 @@ export class GitHubStrategy extends PassportStrategy(Strategy, 'github') { const { id, username, emails, photos } = profile; const user = { - provider: 'github', + provider: Provider.github, socialId: id, nickname: username, email: emails[0].value, diff --git a/apps/api/src/auth/google/google.strategy.ts b/apps/api/src/auth/google/google.strategy.ts index 0d32ed2f..a4dd0aa3 100644 --- a/apps/api/src/auth/google/google.strategy.ts +++ b/apps/api/src/auth/google/google.strategy.ts @@ -2,11 +2,12 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; import { Profile, Strategy } from 'passport-google-oauth20'; +import { Provider } from '@repo/types'; import { AuthService } from '../auth.service'; @Injectable() -export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { +export class GoogleStrategy extends PassportStrategy(Strategy, Provider.google) { constructor( private configService: ConfigService, private authService: AuthService @@ -23,7 +24,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { const { id, displayName, emails, photos } = profile; const user = { - provider: 'google', + provider: Provider.google, socialId: id, nickname: displayName, email: emails[0].value, diff --git a/apps/api/src/entity/ticle.entity.ts b/apps/api/src/entity/ticle.entity.ts index 145d8dbc..c79d50bc 100644 --- a/apps/api/src/entity/ticle.entity.ts +++ b/apps/api/src/entity/ticle.entity.ts @@ -74,4 +74,7 @@ export class Ticle { }, }) tags: Tag[]; + + @Column({ type: 'varchar', name: 'profile_image_url', nullable: true }) + profileImageUrl: string; } diff --git a/apps/api/src/entity/user.entity.ts b/apps/api/src/entity/user.entity.ts index 43cf31e0..c58c56a7 100644 --- a/apps/api/src/entity/user.entity.ts +++ b/apps/api/src/entity/user.entity.ts @@ -6,6 +6,7 @@ import { PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; +import { Provider } from '@repo/types'; import { Applicant } from './applicant.entity'; import { Ticle } from './ticle.entity'; @@ -34,7 +35,7 @@ export class User { profileImageUrl: string; @Column({ type: 'varchar', default: 'local' }) - provider: string; + provider: Provider; @Column({ type: 'varchar', name: 'social_id', nullable: true }) socialId: string; diff --git a/apps/api/src/ticle/ticle.service.ts b/apps/api/src/ticle/ticle.service.ts index a3860388..a2ec54c6 100644 --- a/apps/api/src/ticle/ticle.service.ts +++ b/apps/api/src/ticle/ticle.service.ts @@ -167,6 +167,7 @@ export class TicleService { 'ticle.endTime', 'ticle.speakerName', 'ticle.createdAt', + 'ticle.profileImageUrl', ]) .addSelect('GROUP_CONCAT(DISTINCT tags.name)', 'tagNames') .addSelect('COUNT(DISTINCT applicant.id)', 'applicantCount') diff --git a/apps/api/src/user/dto/createLocalUser.dto.ts b/apps/api/src/user/dto/createLocalUser.dto.ts index 7460d4eb..fbe976a7 100644 --- a/apps/api/src/user/dto/createLocalUser.dto.ts +++ b/apps/api/src/user/dto/createLocalUser.dto.ts @@ -1,8 +1,10 @@ +import { Provider } from '@repo/types'; + export class CreateLocalUserDto { username: string; password: string; email: string; - provider: string; + provider: Provider; nickname?: string; introduce?: string; profileImageUrl?: string; diff --git a/apps/api/src/user/dto/createSocialUser.dto.ts b/apps/api/src/user/dto/createSocialUser.dto.ts index 5a22f6b7..481a1228 100644 --- a/apps/api/src/user/dto/createSocialUser.dto.ts +++ b/apps/api/src/user/dto/createSocialUser.dto.ts @@ -1,6 +1,8 @@ +import { Provider } from '@repo/types'; + export class CreateSocialUserDto { email: string; - provider: string; + provider: Provider; socialId: string; nickname?: string; introduce?: string; diff --git a/apps/api/src/user/dto/userProfileDto.ts b/apps/api/src/user/dto/userProfileDto.ts index 75eaaf64..3387496f 100644 --- a/apps/api/src/user/dto/userProfileDto.ts +++ b/apps/api/src/user/dto/userProfileDto.ts @@ -1,4 +1,10 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Provider } from '@repo/types'; + +interface TicleInfo { + title: string; + ticleId: number; +} export class UserProfileDto { @ApiProperty({ @@ -23,11 +29,16 @@ export class UserProfileDto { example: 'github', description: '유저 소셜 로그인 프로바이더', }) - provider: string; + provider: Provider; @ApiProperty({ - example: ['개발자를 위한 피그마', '야, 너도 부캠할 수 있어'], + example: [ + { + title: '야, 너두 부캠할 수 있어', + ticleId: 1, + }, + ], description: '유저가 개설한 티클 목록', }) - ticles: string[]; + ticleInfo: TicleInfo[]; } diff --git a/apps/api/src/user/dto/userProfileOfMeDto.ts b/apps/api/src/user/dto/userProfileOfMeDto.ts index 320e23f4..b32b1f1d 100644 --- a/apps/api/src/user/dto/userProfileOfMeDto.ts +++ b/apps/api/src/user/dto/userProfileOfMeDto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Provider } from '@repo/types'; export class UserProfileOfMeDto { @ApiProperty({ @@ -23,5 +24,5 @@ export class UserProfileOfMeDto { example: 'github', description: '유저 소셜 로그인 프로바이더', }) - provider: string; + provider: Provider; } diff --git a/apps/api/src/user/user.service.ts b/apps/api/src/user/user.service.ts index 5fea9db3..15685c88 100644 --- a/apps/api/src/user/user.service.ts +++ b/apps/api/src/user/user.service.ts @@ -2,7 +2,7 @@ import { ConflictException, Injectable, NotFoundException } from '@nestjs/common import { InjectRepository } from '@nestjs/typeorm'; import * as bcrypt from 'bcrypt'; import { Repository } from 'typeorm'; -import { ErrorMessage } from '@repo/types'; +import { ErrorMessage, Provider } from '@repo/types'; import { User } from '@/entity/user.entity'; @@ -57,7 +57,7 @@ export class UserService { return user; } - async findUserBySocialIdAndProvider(socialId: string, provider: string): Promise { + async findUserBySocialIdAndProvider(socialId: string, provider: Provider): Promise { const user = await this.userRepository.findOne({ where: { socialId, provider }, }); @@ -84,7 +84,7 @@ export class UserService { const user = await this.userRepository .createQueryBuilder('user') .leftJoin('user.ticles', 'ticles') - .addSelect('ticles.title') + .addSelect(['ticles.title', 'ticles.id']) .where('user.id = :userId', { userId: userId }) .getOne(); @@ -92,13 +92,17 @@ export class UserService { throw new NotFoundException(ErrorMessage.USER_NOT_FOUND); } - const ticles = user.ticles || []; + const ticleInfo = user.ticles.map((ticle) => ({ + title: ticle.title, + ticleId: ticle.id, + })); + return { id: user.id, nickname: user.nickname, profileImageUrl: user.profileImageUrl, provider: user.provider, - ticles: ticles.map((ticle) => ticle.title), + ticleInfo: ticleInfo, }; } } diff --git a/apps/web/.eslintrc.cjs b/apps/web/.eslintrc.cjs index d254cfe8..6ec7ab99 100644 --- a/apps/web/.eslintrc.cjs +++ b/apps/web/.eslintrc.cjs @@ -11,7 +11,7 @@ module.exports = { require.resolve('@repo/lint'), 'plugin:react/recommended', 'plugin:react-hooks/recommended', - 'plugin:jsx-a11y/recommended', + // 'plugin:jsx-a11y/recommended', 'plugin:react/jsx-runtime', ], plugins: ['react-refresh'], diff --git a/apps/web/src/__mocks__/handlers/auth.ts b/apps/web/src/__mocks__/handlers/auth.ts index 7e4a0331..4a8b7771 100644 --- a/apps/web/src/__mocks__/handlers/auth.ts +++ b/apps/web/src/__mocks__/handlers/auth.ts @@ -39,13 +39,3 @@ export const logIn: HttpResponseResolver = async ({ request }, }); }; - -export const signOut: HttpResponseResolver = async () => { - return HttpResponse.json(null, { - status: 302, - headers: { - Location: '/', - 'Set-Cookie': `accessToken=; Path=/; HttpOnly, refreshToken=; Path=/; HttpOnly`, - }, - }); -}; diff --git a/apps/web/src/api/auth.ts b/apps/web/src/api/auth.ts index 92ac87cd..9f0a5fb9 100644 --- a/apps/web/src/api/auth.ts +++ b/apps/web/src/api/auth.ts @@ -20,8 +20,8 @@ const signUp = async (body: SignUpDto) => { return data; }; -const signOut = async () => { - await axiosInstance.post('/auth/logout'); +const logOut = () => { + window.location.href = `${ENV.API_URL}/auth/logout`; }; const guestLogin = () => { @@ -32,4 +32,4 @@ const oauthLogin = (provider: 'google' | 'github') => { window.location.href = `${ENV.API_URL}/auth/${provider}/login`; }; -export { logIn, signUp, oauthLogin, guestLogin, signOut }; +export { logIn, signUp, oauthLogin, guestLogin, logOut }; diff --git a/apps/web/src/api/user.ts b/apps/web/src/api/user.ts new file mode 100644 index 00000000..edefbf7e --- /dev/null +++ b/apps/web/src/api/user.ts @@ -0,0 +1,24 @@ +import { + UserProfileOfMeResponse, + UserProfileOfMeSchema, + UserProfileResponse, + UserProfileSchema, +} from '@repo/types'; + +import request from '@/hooks/api/request'; + +export const getUserProfileOfMe = async () => { + return request({ + method: 'GET', + url: '/user/me', + schema: UserProfileOfMeSchema, + }); +}; + +export const getUserProfileByUserId = async (userId: number) => { + return request({ + method: 'GET', + url: `/user/${userId}`, + schema: UserProfileSchema, + }); +}; diff --git a/apps/web/src/components/auth/GuestLogin.tsx b/apps/web/src/components/auth/GuestLogin.tsx index 9bf17326..7595ca79 100644 --- a/apps/web/src/components/auth/GuestLogin.tsx +++ b/apps/web/src/components/auth/GuestLogin.tsx @@ -1,5 +1,3 @@ -/* eslint-disable jsx-a11y/click-events-have-key-events */ -/* eslint-disable jsx-a11y/no-static-element-interactions */ import { guestLogin } from '@/api/auth'; import ChevronRight from '@/assets/icons/chevron-right.svg?react'; diff --git a/apps/web/src/components/common/Avatar/index.tsx b/apps/web/src/components/common/Avatar/index.tsx index 1d4c66ac..c32192f6 100644 --- a/apps/web/src/components/common/Avatar/index.tsx +++ b/apps/web/src/components/common/Avatar/index.tsx @@ -5,6 +5,7 @@ import PersonIc from '@/assets/icons/person.svg?react'; import cn from '@/utils/cn'; const AVATAR_SIZE = { + xs: 'xs', sm: 'sm', md: 'md', lg: 'lg', @@ -15,6 +16,7 @@ const avatarVariants = cva( { variants: { size: { + [AVATAR_SIZE.xs]: 'h-[30px] w-[30px]', [AVATAR_SIZE.sm]: 'h-[50px] w-[50px]', [AVATAR_SIZE.md]: 'h-[84px] w-[84px]', [AVATAR_SIZE.lg]: 'h-[100px] w-[100px]', diff --git a/apps/web/src/components/common/Header/User.tsx b/apps/web/src/components/common/Header/User.tsx new file mode 100644 index 00000000..75dafab1 --- /dev/null +++ b/apps/web/src/components/common/Header/User.tsx @@ -0,0 +1,61 @@ +/* eslint-disable react-refresh/only-export-components */ +import { Link } from '@tanstack/react-router'; +import axios from 'axios'; +import { Provider } from '@repo/types'; + +import UserProfileOfMeDialog from '@/components/user/UserProfileOfMeDialog'; +import { useUserProfileOfMe } from '@/hooks/api/user'; +import useModal from '@/hooks/useModal'; + +import Avatar from '../Avatar'; +import Button from '../Button'; + +export const LOGIN_TYPE: Record = { + github: 'Github 로그인', + google: 'Google 로그인', + guest: '게스트 로그인', + local: '티클 로그인', +}; + +function User() { + const { data, error } = useUserProfileOfMe(); + const { isOpen, onOpen, onClose } = useModal(); + + const isUnauthorized = axios.isAxiosError(error) && error.response?.status === 401; + + const loginType = data?.provider && LOGIN_TYPE[data.provider]; + + const AuthorizedContent = () => ( + <> +
+ + {data?.nickname} +
+ {isOpen && data && loginType && ( + + )} + + ); + + const UnauthorizedContent = () => ( + +
+ +
+ + ); + + return ( + + ); +} + +export default User; diff --git a/apps/web/src/components/common/Header/index.tsx b/apps/web/src/components/common/Header/index.tsx index 7f5d41da..2942950d 100644 --- a/apps/web/src/components/common/Header/index.tsx +++ b/apps/web/src/components/common/Header/index.tsx @@ -1,13 +1,14 @@ import { Link } from '@tanstack/react-router'; import TicleLogo from '@/assets/ticle.svg?react'; -import Button from '@/components/common/Button'; + +import User from './User'; const NAV_STYLE = 'hover:text-hover text-title1 text-alt transition [&.active]:text-primary'; function Header() { return ( -
+
{/* TODO: User 로그인시 핸들링 */} -
- -
+
); } diff --git a/apps/web/src/components/live/StreamView/AudioPlayer.tsx b/apps/web/src/components/live/StreamView/AudioPlayer.tsx index 091c3c56..3a1f0e7a 100644 --- a/apps/web/src/components/live/StreamView/AudioPlayer.tsx +++ b/apps/web/src/components/live/StreamView/AudioPlayer.tsx @@ -1,4 +1,3 @@ -/* eslint-disable jsx-a11y/media-has-caption */ import { useEffect, useRef } from 'react'; import { VideoPlayerProps } from './VideoPlayer'; diff --git a/apps/web/src/components/ticle/detail/index.tsx b/apps/web/src/components/ticle/detail/index.tsx index ba20a923..8b645de8 100644 --- a/apps/web/src/components/ticle/detail/index.tsx +++ b/apps/web/src/components/ticle/detail/index.tsx @@ -5,19 +5,25 @@ import ClockIc from '@/assets/icons/clock.svg?react'; import Avatar from '@/components/common/Avatar'; import Badge from '@/components/common/Badge'; import Button from '@/components/common/Button'; +import UserProfileDialog from '@/components/user/UserProfileDialog'; import { useApplyTicle, useTicle } from '@/hooks/api/ticle'; +import useModal from '@/hooks/useModal'; import { formatDateTimeRange } from '@/utils/date'; function Detail() { const { ticleId } = useParams({ from: '/ticle/$ticleId' }); const { data } = useTicle(ticleId); - const { mutate } = useApplyTicle(); const handleApplyButtonClick = () => { mutate(ticleId); }; + const { isOpen, onOpen, onClose } = useModal(); + const handleProfileClick = () => { + onOpen(); + }; + if (!data) return; const { dateStr, timeRangeStr } = formatDateTimeRange(data.startTime, data.endTime); return ( @@ -51,10 +57,21 @@ function Detail() {

발표자 소개

-
- +
+ {data.speakerName}
+ {isOpen && ( + + )}
{data.speakerIntroduce}
diff --git a/apps/web/src/components/ticle/list/TicleCard.tsx b/apps/web/src/components/ticle/list/TicleCard.tsx index 6e4a13ad..e53b5920 100644 --- a/apps/web/src/components/ticle/list/TicleCard.tsx +++ b/apps/web/src/components/ticle/list/TicleCard.tsx @@ -21,7 +21,7 @@ const TicleCard = ({ speakerProfileImg, }: TicleCardProps) => { return ( -
+

{title}

diff --git a/apps/web/src/components/ticle/list/index.tsx b/apps/web/src/components/ticle/list/index.tsx index 4890f7ca..b5ecd948 100644 --- a/apps/web/src/components/ticle/list/index.tsx +++ b/apps/web/src/components/ticle/list/index.tsx @@ -76,7 +76,7 @@ function TicleList() {