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 all 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 @@ -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 { 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 All @@ -26,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 @@ -57,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 @@ -84,21 +84,25 @@
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
Loading