Skip to content

Commit

Permalink
[BOOK-14]-네이버 Open API 연동 및 도서 검색 API 구현 (#10)
Browse files Browse the repository at this point in the history
* [BOOK-14]-chore: 네이버 오픈 API 관련 설정

* [BOOK-14]-feat: 도서 엔티티 생성

* [BOOK-14]-feat: Open API 이용한 도서 검색 API

* [BOOK-14]-feat: 유저 도서 간 다대다 매핑

* [BOOK-14]-refactor: API 호출 권한 검증 시 토큰 인증 필터 추가
  • Loading branch information
LeeHanEum authored Oct 10, 2024
1 parent f052d78 commit 06726c0
Show file tree
Hide file tree
Showing 13 changed files with 339 additions and 6 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dependencies {

implementation 'com.amazonaws:aws-java-sdk-s3:1.11.238'

implementation 'org.json:json:20210307'
}

tasks.named('test') {
Expand Down
31 changes: 27 additions & 4 deletions src/main/java/goorm/unit/booklog/common/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,31 @@

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;

import goorm.unit.booklog.common.auth.application.UserDetailService;
import goorm.unit.booklog.common.auth.filter.TokenAuthenticationFilter;
import goorm.unit.booklog.common.auth.jwt.TokenProvider;
import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {
private final TokenProvider tokenProvider;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
Expand All @@ -34,6 +42,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth
.requestMatchers(SWAGGER_PATTERNS).permitAll()
.requestMatchers(STATIC_RESOURCES_PATTERNS).permitAll()
Expand All @@ -59,10 +68,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
"/error",
"/favicon.ico",
"/index.html",
"/api/v1/users/duplication",
"/api/v1/users/signup",
"/api/v1/auth/login"

"/api/v1/users/signup",
"/api/v1/auth/login"
"/api/v1/users/duplication",
};

CorsConfigurationSource corsConfigurationSource() {
Expand All @@ -76,6 +84,21 @@ CorsConfigurationSource corsConfigurationSource() {
};
}

@Bean
public TokenAuthenticationFilter tokenAuthenticationFilter() {
return new TokenAuthenticationFilter(tokenProvider);
}

@Bean
public AuthenticationManager authenticationManager(
BCryptPasswordEncoder bCryptPasswordEncoder,
UserDetailService userDetailService
){
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailService);
authProvider.setPasswordEncoder(bCryptPasswordEncoder);
return new ProviderManager(authProvider);
}

@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package goorm.unit.booklog.domain.book.application;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.RequestEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import goorm.unit.booklog.common.response.PageableResponse;
import goorm.unit.booklog.domain.book.domain.BookRepository;
import goorm.unit.booklog.domain.book.presentation.response.BookPageResponse;
import goorm.unit.booklog.domain.book.presentation.response.BookResponse;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class BookService {
private final BookRepository bookRepository;

@Value("${naver.api.clientId}")
private String clientId;

@Value("${naver.api.clientSecret}")
private String clientSecret;

public BookPageResponse searchBooks(int page, int size, String keyword) {
URI uri = UriComponentsBuilder
.fromUriString("https://openapi.naver.com")
.path("/v1/search/book.json")
.queryParam("query", keyword)
.queryParam("display", size)
.queryParam("start", page)
.queryParam("sort", "sim")
.encode()
.build()
.toUri();

RequestEntity<Void> req = RequestEntity
.get(uri)
.header("X-Naver-Client-Id", clientId)
.header("X-Naver-Client-Secret", clientSecret)
.build();

RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.exchange(req, String.class).getBody();

JSONObject jsonResponse = new JSONObject(response);
JSONArray items = jsonResponse.getJSONArray("items");

List<BookResponse> bookResponses = new ArrayList<>();
for (int i = 0; i < items.length(); i++) {
JSONObject item = items.getJSONObject(i);
BookResponse bookResponse = BookResponse.of(
(long)(i + 1),
item.getString("title"),
item.getString("author"),
item.getString("description"),
item.getString("link")
);
bookResponses.add(bookResponse);
}

int total = jsonResponse.getInt("total");
return BookPageResponse.of(bookResponses, PageableResponse.of(PageRequest.of(page, size), (long)total));

}
}
60 changes: 60 additions & 0 deletions src/main/java/goorm/unit/booklog/domain/book/domain/Book.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package goorm.unit.booklog.domain.book.domain;

import static jakarta.persistence.GenerationType.IDENTITY;

import java.util.ArrayList;
import java.util.List;

import goorm.unit.booklog.common.domain.BaseTimeEntity;
import goorm.unit.booklog.domain.file.domain.File;
import goorm.unit.booklog.domain.user.domain.User;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.OneToOne;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor(access= AccessLevel.PROTECTED)
public class Book extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;

@Column(nullable = false)
private String title;

@Column(nullable = false)
private String author;

@Column(nullable = false, length = 1000)
private String description;

@OneToOne
@JoinColumn(name = "file_id")
private File file;

@ManyToMany(mappedBy = "books")
private List<User> users = new ArrayList<>();

public static Book create(Long id, String title, String author, String description, File file) {
return Book.builder()
.id(id)
.title(title)
.author(author)
.description(description)
.file(file)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package goorm.unit.booklog.domain.book.domain;

public interface BookRepository {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package goorm.unit.booklog.domain.book.infrastructure;

import org.springframework.stereotype.Repository;

import goorm.unit.booklog.domain.book.domain.BookRepository;
import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class BookRepositoryImpl implements BookRepository {
private final JpaBookRepository jpaBookRepository;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package goorm.unit.booklog.domain.book.infrastructure;

import org.springframework.data.jpa.repository.JpaRepository;

import goorm.unit.booklog.domain.book.domain.Book;

public interface JpaBookRepository extends JpaRepository<Book, String> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package goorm.unit.booklog.domain.book.presentation;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import goorm.unit.booklog.domain.book.application.BookService;
import goorm.unit.booklog.domain.book.presentation.response.BookPageResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/books")
@Tag(name = "Book", description = "도서 관련 api / 담당자 : 이한음")
public class BookController {
private final BookService bookService;

@Operation(summary = "도서 검색", description = "네이버 도서 검색 Open API에서 키워드 기반으로 도서를 검색 합니다.")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "도서 검색 성공",
content = @Content(schema = @Schema(implementation = BookPageResponse.class))

)
})
@GetMapping("/search")
public ResponseEntity<BookPageResponse> searchBooks(
@Parameter(description = "페이지 인덱스", example = "1", required = true) @RequestParam(value = "page", defaultValue = "1") int page,
@Parameter(description = "응답 개수", example = "10", required = true) @RequestParam(value = "size", defaultValue = "10") int size,
@Parameter(description = "키워드", example = "스프링", required = true) @RequestParam(value = "keyword") String keyword
) {
BookPageResponse response = bookService.searchBooks(page, size, keyword);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package goorm.unit.booklog.domain.book.presentation.response;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import java.util.List;

import goorm.unit.booklog.common.response.PageableResponse;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

@Builder
public record BookPageResponse<T>(
@Schema(
description = "도서 목록",
example = "[{\"id\": 1, \"title\": \"스프링 부트와 AWS로 혼자 구현하는 웹 서비스\", \"author\": \"이한음\", \"description\": \"스프링 부트와 AWS로 혼자 구현하는 웹 서비스\", \"file\": {\"id\": 1, \"logicalName\": \"example.jpg\", \"physicalPath\": \"https://example-bucket.ncp.com/files/example.jpg\"}}]",
requiredMode = REQUIRED
)
List<BookResponse> contents,

@Schema(description = "페이징 정보", requiredMode = REQUIRED)
PageableResponse<T> pageable
) {
public static <T> BookPageResponse<T> of(List<BookResponse> books, PageableResponse<T> pageableResponse) {
return BookPageResponse.<T>builder()
.contents(books)
.pageable(pageableResponse)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package goorm.unit.booklog.domain.book.presentation.response;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import goorm.unit.booklog.domain.book.domain.Book;
import goorm.unit.booklog.domain.file.presentation.response.FileResponse;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

@Builder
public record BookResponse(
@Schema(description = "도서 ID", example = "1", requiredMode = REQUIRED)
Long id,

@Schema(description = "도서 제목", example = "스프링 부트와 AWS로 혼자 구현하는 웹 서비스", requiredMode = REQUIRED)
String title,

@Schema(description = "도서 저자", example = "이한음", requiredMode = NOT_REQUIRED)
String author,

@Schema(description = "도서 설명", example = "스프링 부트와 AWS로 혼자 구현하는 웹 서비스", requiredMode = NOT_REQUIRED)
String description,

@Schema(
description = "파일 정보",
example = "{\"id\": 1, \"logicalName\": \"example.jpg\", \"physicalPath\": \"https://example-bucket.ncp.com/files/example.jpg\"}",
requiredMode = NOT_REQUIRED)
FileResponse file
) {
public static BookResponse from(Book book) {
return BookResponse.builder()
.id(book.getId())
.title(book.getTitle())
.author(book.getAuthor())
.description(book.getDescription())
.file(FileResponse.from(book.getFile()))
.build();
}

public static BookResponse of (Long id, String title, String author, String description, String physicalPath) {
return BookResponse.builder()
.id(id)
.title(title)
.author(author)
.description(description)
.file(FileResponse.of(title, physicalPath))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,11 @@ public static FileResponse from(File file) {
.physicalPath(endpoint + "/" + bucketName + "/" + file.getPhysicalPath())
.build();
}

public static FileResponse of(String logicalName, String physicalPath) {
return FileResponse.builder()
.logicalName(logicalName + " 대표 이미지")
.physicalPath(physicalPath)
.build();
}
}
Loading

0 comments on commit 06726c0

Please sign in to comment.