Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.2'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2'

// OAuth2 login
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-webflux'

}

tasks.named('test') {
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/example/umc7th/Umc7thApplication.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.example.umc7th;

import com.example.umc7th.domain.oauth2.OAuth2Properties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
@EnableConfigurationProperties(OAuth2Properties.class)
public class Umc7thApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.List;

@RestController
@RequiredArgsConstructor
@Tag(name = "게시글 API") // Swagger에 표시될 API 그룹 이름
Expand Down Expand Up @@ -66,24 +70,6 @@ public CustomResponse<ArticleResponseDTO.ArticlePreviewListDTO> getArticles(@Req
@RequestParam(value = "offset", defaultValue = "10") Integer offset) {
Slice<Article> articles = articleQueryService.getArticles(query, cursor, offset);
return CustomResponse.onSuccess(ArticleResponseDTO.ArticlePreviewListDTO.from(articles));
}*/

/**
* 커서 기반 게시글 조회 API
* @param lastCreatedAt (이전 게시글의 생성 날짜)
* @param pageable (페이지네이션 정보)
* @return 생성 날짜 기준으로 게시글을 조회한 후 성공 응답을 CustomResponse 형태로 반환
*/
@GetMapping("/articles/cursor")
@Operation(summary = "커서 기반 게시글 조회 API", description = "생성 날짜 기준으로 게시글 조회하는 API")
public CustomResponse<ArticleResponseDTO.ArticlePreviewListDTO> getArticlesByCursor(
@RequestParam(required = false) LocalDateTime lastCreatedAt,
Pageable pageable) {

List<Article> articles = articleQueryService.getArticlesByCreatedAtLessThan(lastCreatedAt, pageable);
int totalCount = (int) articleRepository.count(); // 전체 게시글 수 조회

return CustomResponse.onSuccess(ArticleResponseDTO.ArticlePreviewListDTO.from(articles, totalCount, pageable.getPageSize()));
}

/** 게시물 수정 API */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,4 @@ public Article getArticle(Long id) {
new ArticleException(ArticleErrorCode.NOT_FOUND));
}

@Override
public List<Article> getArticlesByCreatedAtLessThan(LocalDateTime createdAt, Pageable pageable) {
// 생성 날짜 기준으로 게시글 조회
return articleRepository.findByCreatedAtLessThan(createdAt, pageable);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public enum MemberErrorCode implements BaseErrorCode {
NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "사용자를 찾을 수 없습니다."),
ALREADY_EXIST(HttpStatus.BAD_REQUEST, "MEMBER400", "이미 존재하는 사용자입니다."),
INCORRECT_PASSWORD(HttpStatus.UNAUTHORIZED, "MEMBER401", "비밀번호가 틀립니다."),
OAUTH_USER_INFO_FAIL(HttpStatus.BAD_REQUEST, "MEMBER400", "OAuth 사용자 정보를 가져오는 데 실패했습니다."),
OAUTH_TOKEN_FAIL(HttpStatus.UNAUTHORIZED, "OAUTH401", "OAuth 토큰을 가져오는 데 실패했습니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.example.umc7th.domain.oauth2;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
@ConfigurationProperties(prefix = "spring.security.oauth2.client")
@Data
@Primary
public class OAuth2Properties {

private Registration registration;
private Provider provider;

@Data
public static class Registration {
private Kakao kakao;

@Data
public static class Kakao {
private String clientId;
private String redirectUri;
}
}

@Data
public static class Provider {
private Kakao kakao;

@Data
public static class Kakao {
private String tokenUri;
private String userInfoUri;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.umc7th.domain.oauth2.controller;

import com.example.umc7th.domain.member.dto.MemberResponseDTO;
import com.example.umc7th.global.apiPayload.CustomResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

public class oauth2controller {
@GetMapping("/oauth2/callback/kakao")
// queryParam 형식으로 코드를 받을 예정이니 RequestParam을 설정해줍니다
// 응답은 저희 서버에 로그인 다 한 뒤에 토큰을 제공할 예정이니 TokenDTO로 설정해줍니다.
public CustomResponse<MemberResponseDTO.MemberTokenDTO> loginWithKakao(@RequestParam("code") String code) {
// 로직 구현 필요
return null;
// 서비스 생성 이후
// return CustomResponse.onSuccess(oAuth2Service.login(code));

}
}
54 changes: 54 additions & 0 deletions src/main/java/com/example/umc7th/domain/oauth2/dto/OAuth2DTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.example.umc7th.domain.oauth2.dto;

import lombok.Getter;

public class OAuth2DTO {

@Getter
public static class OAuth2TokenDTO {
String token_type;
String access_token;
String refresh_token;
Long expires_in;
Long refresh_token_expires_in;
String scope;
}

@Getter
public static class KakaoProfile {
private Long id;
private String connected_at;
private Properties properties;
private KakaoAccount kakao_account;

@Getter
public class Properties {
private String nickname;
private String profile_image;
private String thumbnail_image;
}

@Getter
public class KakaoAccount {
private String email;
private Boolean is_email_verified;
private Boolean email_needs_agreement;
private Boolean has_email;
private Boolean profile_nickname_needs_agreement;
private Boolean profile_image_needs_agreement;
private Boolean email_needs_argument;
private Boolean is_email_valid;
private Profile profile;

@Getter
public class Profile {
private String nickname;
private String thumbnail_image_url;
private String profile_image_url;
private Boolean is_default_nickname;
private Boolean is_default_image;
}
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.umc7th.domain.oauth2.service;

import com.example.umc7th.domain.member.dto.MemberResponseDTO;

public interface OAuth2Service {
MemberResponseDTO.MemberTokenDTO login(String code);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.example.umc7th.domain.oauth2.service;

import com.example.umc7th.domain.member.dto.MemberResponseDTO;
import com.example.umc7th.domain.member.entity.Member;
import com.example.umc7th.domain.member.exception.MemberException;
import com.example.umc7th.domain.member.repository.MemberRepository;
import com.example.umc7th.domain.oauth2.OAuth2Properties;
import com.example.umc7th.domain.oauth2.dto.OAuth2DTO;
import com.example.umc7th.global.jwt.util.JwtProvider;
import com.example.umc7th.domain.member.exception.MemberErrorCode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;


@Service
@RequiredArgsConstructor
public class OAuth2ServiceImpl implements OAuth2Service {

private final MemberRepository memberRepository;
private final JwtProvider jwtProvider;
private final OAuth2Properties oAuth2Properties;

@Override
public MemberResponseDTO.MemberTokenDTO login(String code) {
// WebClient 사용해 Token URI 요청 및 응답 처리
WebClient webClient = WebClient.builder().build();

// Token 요청 설정
OAuth2DTO.OAuth2TokenDTO oAuth2TokenDTO = webClient.post()
.uri(oAuth2Properties.getProvider().getKakao().getTokenUri())
.header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(BodyInserters.fromFormData("grant_type", "authorization_code")
.with("client_id", oAuth2Properties.getRegistration().getKakao().getClientId())
.with("redirect_uri", oAuth2Properties.getRegistration().getKakao().getRedirectUri())
.with("code", code))
.retrieve()
.bodyToMono(OAuth2DTO.OAuth2TokenDTO.class)
.block();

// 토큰을 이용해 사용자 정보 요청
OAuth2DTO.KakaoProfile profile = webClient.get()
.uri(oAuth2Properties.getProvider().getKakao().getUserInfoUri())
.header(HttpHeaders.AUTHORIZATION, "Bearer " + oAuth2TokenDTO.getAccess_token())
.retrieve()
.bodyToMono(OAuth2DTO.KakaoProfile.class)
.block();

// `email`로 회원 검색 및 가입 처리
String email = profile.getKakao_account().getEmail(); // email로 변경하여 가져옴

Member member = memberRepository.findByEmail(email).orElseGet(() ->
memberRepository.save(
Member.builder()
.email(email)
.role("ROLE_USER")
.build()
)
);

// JWT 토큰 생성 후 반환
return MemberResponseDTO.MemberTokenDTO.builder()
.accessToken(jwtProvider.createAccessToken(member))
.refreshToken(jwtProvider.createRefreshToken(member))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,34 +41,8 @@ public CustomResponse<ReplyResponseDTO.ReplyPreviewListDTO> getReplies(@PathVari
// 페이지와 오프셋을 기반으로 특정 게시글의 댓글 목록을 조회하여 응답 DTO로 변환
Page<Reply> replies = replyQueryService.getReplies(articleId, page, offset);
return CustomResponse.onSuccess(ReplyConverter.toReplyPreviewListDTO(replies));
}*/

/**
* 댓글 전체 조회 API (Offset 기반 페이지네이션)
* @param page 페이지 번호
* @param size 한 페이지당 댓글 수
* @return 조회된 페이지네이션 댓글 목록을 CustomResponse로 반환
*/
@GetMapping
@Operation(summary = "댓글 전체 조회 API", description = "Offset 기반 페이지네이션을 통해 댓글 전체를 조회하는 API")
public CustomResponse<ReplyResponseDTO.ReplyPreviewListDTO> getReplies(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {

Page<Reply> replyPage = replyQueryService.getRepliesWithPagination(page, size);

// 응답 DTO 변환 및 페이지네이션 정보 설정
ReplyResponseDTO.ReplyPreviewListDTO response = ReplyConverter.toReplyPreviewListDTO(
replyPage.getContent(),
replyPage.getSize(),
replyPage.getNumber(),
(int) replyPage.getTotalElements()
);

return CustomResponse.onSuccess(response);
}


/** 댓글 하나 조회 API */
@GetMapping("/{replyId}")
@Operation(summary = "댓글 조회 API", description = "댓글 하나 조회하는 API")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,4 @@
public interface ReplyQueryService {
Page<Reply> getReplies(Long articleId, Integer page, Integer offset);
Reply getReply(Long id);
Page<Reply> getRepliesWithPagination(int page, int size);
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,4 @@ public Page<Reply> getReplies(Long articleId, Integer page, Integer offset) {
// 특정 게시글에 대한 댓글 목록을 페이지 단위로 조회
return replyRepository.findAllByArticleIsOrderByCreatedAtDesc(article, pageable);
}

@Override
public Page<Reply> getRepliesWithPagination(int page, int size) {
// 페이지 요청을 생성하고, 댓글을 페이지로 조회하여 반환
Pageable pageable = PageRequest.of(page, size);
return replyRepository.findAllByOrderByCreatedAtDesc(pageable);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
Expand Down Expand Up @@ -38,7 +39,8 @@ public class SecurityConfig {
"/swagger-resources/**",
"/v3/api-docs/**",
"/login",
"/signup"
"/signup",
"/oauth2/callback/**"
};

@Bean // Spring Security의 필터 체인을 설정하는 빈으로 등록
Expand All @@ -56,6 +58,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
.formLogin(AbstractHttpConfigurer::disable)
// 기본 HTTP Basic 인증을 비활성화
.httpBasic(HttpBasicConfigurer::disable)

// OAuth2 Login 설정을 default로 설정
.oauth2Login(Customizer.withDefaults())

// CSRF 보안을 비활성화
.csrf(AbstractHttpConfigurer::disable)

Expand Down
20 changes: 20 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,26 @@ spring:
properties:
hibernate:
format_sql: true
# OAuth2 login
security:
oauth2:
client:
registration:
kakao:
authorization-grant-type: authorization_code
client-id: ${API_KEY}
redirect-uri: ${REDIRECT_URI}
client-authentication-method: client_secret_post
scope:
- profile_nickname
- profile_image
- account_email
provider:
kakao:
authorization-uri: https://kauth.kakao.com/oauth/authorize
token-uri: https://kauth.kakao.com/oauth/token
user-info-uri: https://kapi.kakao.com/v2/user/me
user-name-attribute: id

Jwt:
secret: ${JWT_SECRET}
Expand Down