Skip to content

Commit

Permalink
Merge pull request #294 from boostcampwm-2024/feat/#37/profile
Browse files Browse the repository at this point in the history
[Feat] 프로필 정보 UI 구현
  • Loading branch information
simeunseo authored Nov 27, 2024
2 parents c48f2c6 + 7024318 commit 811628a
Show file tree
Hide file tree
Showing 38 changed files with 378 additions and 67 deletions.
2 changes: 1 addition & 1 deletion apps/api/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions apps/api/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand Down
5 changes: 3 additions & 2 deletions apps/api/src/auth/github/github.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions apps/api/src/auth/google/google.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/entity/ticle.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,7 @@ export class Ticle {
},
})
tags: Tag[];

@Column({ type: 'varchar', name: 'profile_image_url', nullable: true })
profileImageUrl: string;
}
3 changes: 2 additions & 1 deletion apps/api/src/entity/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Provider } from '@repo/types';

import { Applicant } from './applicant.entity';
import { Ticle } from './ticle.entity';
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/ticle/ticle.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/user/dto/createLocalUser.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/user/dto/createSocialUser.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Provider } from '@repo/types';

export class CreateSocialUserDto {
email: string;
provider: string;
provider: Provider;
socialId: string;
nickname?: string;
introduce?: string;
Expand Down
17 changes: 14 additions & 3 deletions apps/api/src/user/dto/userProfileDto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { Provider } from '@repo/types';

interface TicleInfo {
title: string;
ticleId: number;
}

export class UserProfileDto {
@ApiProperty({
Expand All @@ -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[];
}
3 changes: 2 additions & 1 deletion apps/api/src/user/dto/userProfileOfMeDto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { Provider } from '@repo/types';

export class UserProfileOfMeDto {
@ApiProperty({
Expand All @@ -23,5 +24,5 @@ export class UserProfileOfMeDto {
example: 'github',
description: '유저 소셜 로그인 프로바이더',
})
provider: string;
provider: Provider;
}
14 changes: 9 additions & 5 deletions apps/api/src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -57,7 +57,7 @@ export class UserService {
return user;
}

async findUserBySocialIdAndProvider(socialId: string, provider: string): Promise<User | null> {
async findUserBySocialIdAndProvider(socialId: string, provider: Provider): Promise<User | null> {
const user = await this.userRepository.findOne({
where: { socialId, provider },
});
Expand All @@ -84,21 +84,25 @@ 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();

if (!user) {
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,
};
}
}
2 changes: 1 addition & 1 deletion apps/web/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
10 changes: 0 additions & 10 deletions apps/web/src/__mocks__/handlers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,3 @@ export const logIn: HttpResponseResolver<object, SignInData> = async ({ request
},
});
};

export const signOut: HttpResponseResolver = async () => {
return HttpResponse.json(null, {
status: 302,
headers: {
Location: '/',
'Set-Cookie': `accessToken=; Path=/; HttpOnly, refreshToken=; Path=/; HttpOnly`,
},
});
};
6 changes: 3 additions & 3 deletions apps/web/src/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand All @@ -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 };
24 changes: 24 additions & 0 deletions apps/web/src/api/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
UserProfileOfMeResponse,
UserProfileOfMeSchema,
UserProfileResponse,
UserProfileSchema,
} from '@repo/types';

import request from '@/hooks/api/request';

export const getUserProfileOfMe = async () => {
return request<UserProfileOfMeResponse>({
method: 'GET',
url: '/user/me',
schema: UserProfileOfMeSchema,
});
};

export const getUserProfileByUserId = async (userId: number) => {
return request<UserProfileResponse>({
method: 'GET',
url: `/user/${userId}`,
schema: UserProfileSchema,
});
};
2 changes: 0 additions & 2 deletions apps/web/src/components/auth/GuestLogin.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components/common/Avatar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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]',
Expand Down
61 changes: 61 additions & 0 deletions apps/web/src/components/common/Header/User.tsx
Original file line number Diff line number Diff line change
@@ -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<Provider, string> = {
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 = () => (
<>
<section className="flex cursor-pointer items-center gap-2" onClick={onOpen}>
<Avatar size="xs" src={data?.profileImageUrl} />
<span className="text-body1 text-alt">{data?.nickname}</span>
</section>
{isOpen && data && loginType && (
<UserProfileOfMeDialog
onClose={onClose}
isOpen={isOpen}
profileImageUrl={data.profileImageUrl}
nickname={data.nickname}
loginType={loginType}
/>
)}
</>
);

const UnauthorizedContent = () => (
<Link to="/auth/oauth">
<section className="flex items-center justify-center">
<Button size="sm">로그인</Button>
</section>
</Link>
);

return (
<aside className="flex gap-3">
{isUnauthorized ? <UnauthorizedContent /> : <AuthorizedContent />}
</aside>
);
}

export default User;
Loading

0 comments on commit 811628a

Please sign in to comment.