Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 프로필 정보 UI 구현 #294

Merged
merged 21 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
357591a
feat: Provider 타입 안정성 강화 및 공통 타입으로 분리
simeunseo Nov 26, 2024
a95b1c5
feat: provider 타입 적용
simeunseo Nov 26, 2024
b9c8d0e
feat: user 관련 api 훅 구현
simeunseo Nov 26, 2024
592649d
feat: Header 내 유저 정보 UI 구현
simeunseo Nov 26, 2024
e43f5ff
feat: Avatar story 수정
simeunseo Nov 26, 2024
0b41c5d
feat: 인증 여부에 따른 헤더 내 로그인 버튼 분기 처리 임시 구현
simeunseo Nov 26, 2024
f61be31
feat: 내 프로필 모달 구현
simeunseo Nov 26, 2024
b6f5dcb
refactor: 컴포넌트 분리
simeunseo Nov 26, 2024
ce97d3c
fix: Select 스토리 에러 해결
simeunseo Nov 26, 2024
b176fde
feat: 유저 정보 조회 API에서 티클 제목과 함께 ticleId도 포함하여 응답
simeunseo Nov 26, 2024
300c5b6
feat: 발표자 프로필 클릭시 유저 정보 다이얼로그 구현
simeunseo Nov 26, 2024
911369d
feat: 티클 상세 페이지에서 프로필 이미지 보여주기
simeunseo Nov 26, 2024
71c8786
feat: 티클 전체 목록에서 프로필 이미지 노출
simeunseo Nov 26, 2024
31c5f29
feat: merge develop
simeunseo Nov 26, 2024
0f907d7
feat: 티클 전체 목록 조회에서 프로필 이미지 가져오는 방식 수정
simeunseo Nov 26, 2024
b608acd
feat: lint 에러 해결
simeunseo Nov 26, 2024
42309b2
feat: merge develop
simeunseo Nov 27, 2024
0824d68
feat: logout 연동
simeunseo Nov 27, 2024
8e76384
Merge branch 'develop' into feat/#37/profile
simeunseo Nov 27, 2024
5faebdd
chore: 포맷팅 문제 해결
simeunseo Nov 27, 2024
7024318
chore: 포맷팅 문제 해결
simeunseo Nov 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -41,7 +41,7 @@ export class AuthController {
this.cookieInsertJWT(response, userId);
}

@Post('guest/login')
@Get('guest/login')
@ApiOperation({ summary: '게스트 로그인' })
@ApiResponse({ status: 302, description: '홈으로 리다이렉션' })
@UseGuards(ThrottlerGuard)
Expand Down
10 changes: 7 additions & 3 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 All @@ -32,17 +33,20 @@ export class AuthService {

async createGuestUser() {
const randomNum = Math.floor(Math.random() * 10000);
const response = await fetch('https://api.thecatapi.com/v1/images/search');
const catImageUrl = (await response.json())[0].url;

const guestUser = {
username: `guest_${randomNum}`,
password: `guest_password_${randomNum}`,
email: `[email protected]`,
nickname: `guest_${randomNum}`,
introduce: `게스트 사용자입니다. `,
profileImageUrl: `https://cataas.com/cat?${Date.now()}`,
profileImageUrl: catImageUrl,
};
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
6 changes: 6 additions & 0 deletions apps/api/src/ticle/dto/ticleDetailDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export class TickleDetailResponseDto {
})
speakerName: string;

@ApiProperty({
example: 1,
description: '발표자 유저 아이디',
})
speakerId: number;

@ApiProperty({
example: '[email protected]',
description: '발표자 이메일',
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/ticle/ticle.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export class TicleService {

return {
...ticleData,
speakerId: ticle.speaker.id,
tags: tags.map((tag) => tag.name),
speakerImgUrl: speaker.profileImageUrl,
isOwner: speaker.id === userId,
Expand All @@ -166,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
43 changes: 43 additions & 0 deletions apps/api/src/user/dto/userProfileDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ApiProperty } from '@nestjs/swagger';
import { Provider } from '@repo/types';

interface TicleInfo {
title: string;
ticleId: number;
}
export class UserProfileDto {
@ApiProperty({
example: '1',
description: '유저 아이디',
})
id: number;

@ApiProperty({
example: 'simeunseo',
description: '유저 닉네임',
})
nickname: string;

@ApiProperty({
example: 'https://avatars.githubusercontent.com/u/55528304?v=4',
description: '유저 프로필 사진',
})
profileImageUrl: string;

@ApiProperty({
example: 'github',
description: '유저 소셜 로그인 프로바이더',
})
provider: Provider;

@ApiProperty({
example: [
{
title: '야, 너두 부캠할 수 있어',
ticleId: 1,
},
],
description: '유저가 개설한 티클 목록',
})
ticleInfo: TicleInfo[];
}
28 changes: 28 additions & 0 deletions apps/api/src/user/dto/userProfileOfMeDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ApiProperty } from '@nestjs/swagger';
import { Provider } from '@repo/types';

export class UserProfileOfMeDto {
@ApiProperty({
example: '1',
description: '유저 아이디',
})
id: number;

@ApiProperty({
example: 'simeunseo',
description: '유저 닉네임',
})
nickname: string;

@ApiProperty({
example: 'https://avatars.githubusercontent.com/u/55528304?v=4',
description: '유저 프로필 사진',
})
profileImageUrl: string;

@ApiProperty({
example: 'github',
description: '유저 소셜 로그인 프로바이더',
})
provider: Provider;
}
22 changes: 16 additions & 6 deletions apps/api/src/user/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import { Controller, Get, Patch } from '@nestjs/common';
import { Controller, Get, Param, UseGuards } from '@nestjs/common';

import { JwtAuthGuard } from '@/auth/jwt/jwt-auth.guard';
import { GetUserId } from '@/common/decorator/get-userId.decorator';

import { UserService } from './user.service';

@Controller('user')
export class UserController {
constructor() {}
constructor(private readonly userService: UserService) {}

@Get('profile')
getUserProfile() {}
@Get('me')
@UseGuards(JwtAuthGuard)
async getUserProfile(@GetUserId() userId: number) {
return await this.userService.findUserProfileOfMeByUserId(userId);
}

@Patch('profile')
patchUserProfile() {}
@Get(':userId')
async patchUserProfile(@Param('userId') userId: number) {
return await this.userService.findUserProfileByUserId(userId);
}
}
47 changes: 44 additions & 3 deletions apps/api/src/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { ConflictException, Injectable } from '@nestjs/common';
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';

import { CreateLocalUserDto } from './dto/createLocalUser.dto';
import { CreateSocialUserDto } from './dto/createSocialUser.dto';
import { UserProfileDto } from './dto/userProfileDto';
import { UserProfileOfMeDto } from './dto/userProfileOfMeDto';

@Injectable()
export class UserService {
Expand All @@ -24,7 +26,7 @@
password: hashedPassword,
});
await this.userRepository.save(user);
const { password, ...result } = user;

Check warning on line 29 in apps/api/src/user/user.service.ts

View workflow job for this annotation

GitHub Actions / check

'password' is assigned a value but never used
return result;
}

Expand Down Expand Up @@ -55,7 +57,7 @@
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 @@ -64,4 +66,43 @@
}
return user;
}

async findUserProfileOfMeByUserId(userId: number): Promise<UserProfileOfMeDto> {
const user = await this.userRepository.findOne({
where: { id: userId },
select: ['id', 'nickname', 'profileImageUrl', 'provider'],
});

if (!user) {
throw new NotFoundException(ErrorMessage.USER_NOT_FOUND);
}

return user;
}

async findUserProfileByUserId(userId: number): Promise<UserProfileDto> {
const user = await this.userRepository
.createQueryBuilder('user')
.leftJoin('user.ticles', 'ticles')
.addSelect(['ticles.title', 'ticles.id'])
.where('user.id = :userId', { userId: userId })
.getOne();

if (!user) {
throw new NotFoundException(ErrorMessage.USER_NOT_FOUND);
}

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,
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
8 changes: 6 additions & 2 deletions apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@
<html lang="ko">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
<link
rel="stylesheet"
as="style"
crossorigin
href="https://cdn.jsdelivr.net/gh/orioncactus/[email protected]/dist/web/static/pretendard-dynamic-subset.min.css"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ticle</title>
<meta
name="Description"
content="작은 지식이 모여 큰 성장이 되는 곳 - 실시간 지식 공유 플랫폼 TICLE"
/>
<title>TICLE</title>
</head>
<body>
<div id="root"></div>
Expand Down
Binary file added apps/web/public/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 8 additions & 3 deletions apps/web/src/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import axiosInstance from '@/api/axios';
import { ENV } from '@/constants/env';

type SignUpDto = {
username: string;
Expand All @@ -23,8 +24,12 @@ const signOut = async () => {
await axiosInstance.post('/auth/logout');
};

const oauthLogin = async (provider: 'google' | 'github') => {
await axiosInstance.get(`/auth/${provider}/login`);
const guestLogin = () => {
window.location.href = `${ENV.API_URL}/auth/guest/login`;
};

export { logIn, signUp, oauthLogin, signOut };
const oauthLogin = (provider: 'google' | 'github') => {
window.location.href = `${ENV.API_URL}/auth/${provider}/login`;
};

export { logIn, signUp, oauthLogin, guestLogin, signOut };
Loading
Loading