Skip to content
Merged
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
78 changes: 78 additions & 0 deletions src/main/java/com/board/controller/BlogApiController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.board.controller;

import com.board.domain.Article;
import com.board.dto.ArticleCreateRequest;
import com.board.dto.ArticleResponse;
import com.board.dto.ArticleUpdateRequest;
import com.board.service.BlogService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
@RequestMapping("/articles")
public class BlogApiController {

public final BlogService blogService;

@PostMapping("")
public ResponseEntity<ArticleResponse> addArticle(@RequestBody ArticleCreateRequest request) {
Article savedArticle = blogService.save(request);

return ResponseEntity.status(HttpStatus.CREATED)
.body(new ArticleResponse(savedArticle));
}

@GetMapping("")
public ResponseEntity<List<ArticleResponse>> findAllArticles(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {

Pageable pageable = PageRequest.of(page, size);

List<ArticleResponse> articles = blogService.findAll(pageable)
.stream()
.map(ArticleResponse::new)
.toList();

return ResponseEntity.ok()
.body(articles);
}

@GetMapping("/{id}")
public ResponseEntity<ArticleResponse> findArticle(@PathVariable long id) {
Article article = blogService.findById(id);

return ResponseEntity.ok()
.body(new ArticleResponse(article));
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteArticle(@PathVariable long id) {
blogService.delete(id);

return ResponseEntity.noContent()
.build();
}

@PutMapping("/{id}")
public ResponseEntity<ArticleResponse> updateArticle(@PathVariable long id,
@RequestBody ArticleUpdateRequest request) {
Article updateArticle = blogService.update(id, request);

return ResponseEntity.ok()
.body(new ArticleResponse(updateArticle));
}
}
38 changes: 38 additions & 0 deletions src/main/java/com/board/domain/Article.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.board.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor
@Getter
public class Article {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false)
private Long id;

@Column(name = "title", nullable = false)
private String title;

@Column(name = "content", nullable = false)
private String content;

@Builder
public Article(String title, String content) {
this.title = title;
this.content = content;
}

public void update(String title, String content) {
this.title = title;
this.content = content;
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/board/dto/ArticleCreateRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.board.dto;

import com.board.domain.Article;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class ArticleCreateRequest {

private final String title;
private final String content;

public Article toEntity() {
return Article.builder()
.title(title)
.content(content)
.build();
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/board/dto/ArticleResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.board.dto;

import com.board.domain.Article;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
public class ArticleResponse {

private final String title;
private final String content;

public ArticleResponse(Article article) {
this.title = article.getTitle();
this.content = article.getContent();
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/board/dto/ArticleUpdateRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.board.dto;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class ArticleUpdateRequest {

private final String title;
private final String content;
}
23 changes: 23 additions & 0 deletions src/main/java/com/board/exception/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.board.exception;

import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
public enum ErrorCode {
ENTITY_NOT_FOUND("ENTITY_NOT_FOUND", "엔티티를 찾을 수 없습니다", HttpStatus.NOT_FOUND),
VALIDATION_ERROR("VALIDATION_ERROR", "유효성 검증 실패", HttpStatus.BAD_REQUEST),
AUTHENTICATION_ERROR("AUTHENTICATION_ERROR", "인증 실패", HttpStatus.UNAUTHORIZED),
AUTHORIZATION_ERROR("AUTHORIZATION_ERROR", "권한 없음", HttpStatus.FORBIDDEN),
SYSTEM_ERROR("SYSTEM_ERROR", "시스템 오류", HttpStatus.INTERNAL_SERVER_ERROR);

private final String code;
private final String message;
private final HttpStatus httpStatus;

ErrorCode(String code, String message, HttpStatus httpStatus) {
this.code = code;
this.message = message;
this.httpStatus = httpStatus;
}
}
40 changes: 40 additions & 0 deletions src/main/java/com/board/exception/ErrorResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.board.exception;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import lombok.Builder;
import lombok.Getter;

@Getter
class ErrorResponse {
private int statusCode;
private LocalDateTime timestamp;
private String code;
private String message;
private Map<String, Object> details;

@Builder
public ErrorResponse(int statusCode, LocalDateTime timestamp, String code, String message) {
this.statusCode = statusCode;
this.timestamp = timestamp;
this.code = code;
this.message = message;
}

public static ErrorResponse of(ErrorCode errorCode) {
return builder()
.statusCode(errorCode.getHttpStatus().value())
.timestamp(LocalDateTime.now())
.code(errorCode.getCode())
.message(errorCode.getMessage())
.build();
}

public void addDetail(String key, Object value) {
if (details == null) {
details = new HashMap<>();
}
this.details.put(key, value);
}
}
27 changes: 27 additions & 0 deletions src/main/java/com/board/exception/GlobalExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.board.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

@ExceptionHandler(MyEntityNotFoundException.class)
public ResponseEntity<ErrorResponse> handleEntityNotFound(MyEntityNotFoundException e) {

log.error("Error Code: {}, Message: {}, entityId: {}",
e.getErrorCode().getCode(),
e.getErrorCode().getMessage(),
e.getEntityId(),
e);

ErrorResponse errorResponse = ErrorResponse.of(e.getErrorCode());
errorResponse.addDetail("entityId : ", e.getEntityId());

return new ResponseEntity<>(errorResponse, e.getErrorCode().getHttpStatus());
}

}
21 changes: 21 additions & 0 deletions src/main/java/com/board/exception/MyEntityNotFoundException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.board.exception;

import lombok.Getter;

@Getter
public class MyEntityNotFoundException extends RuntimeException {
private final ErrorCode errorCode;
private final long entityId;

private MyEntityNotFoundException(ErrorCode errorCode, long entityId) {
this.errorCode = errorCode;
this.entityId = entityId;
}

public static MyEntityNotFoundException of(long entityId) {
return new MyEntityNotFoundException(
ErrorCode.ENTITY_NOT_FOUND,
entityId
);
}
}
7 changes: 7 additions & 0 deletions src/main/java/com/board/repository/BlogRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.board.repository;

import com.board.domain.Article;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BlogRepository extends JpaRepository<Article, Long> {
}
50 changes: 50 additions & 0 deletions src/main/java/com/board/service/BlogService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.board.service;

import com.board.domain.Article;
import com.board.dto.ArticleCreateRequest;
import com.board.dto.ArticleUpdateRequest;
import com.board.exception.MyEntityNotFoundException;
import com.board.repository.BlogRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
@Transactional
public class BlogService {

private final BlogRepository blogRepository;

public Article save(ArticleCreateRequest request) {
return blogRepository.save(request.toEntity());
}

@Transactional(readOnly = true)
public Page<Article> findAll(Pageable pageable) {
return blogRepository.findAll(pageable);
}

@Transactional(readOnly = true)
public Article findById(long id) {
return findArticle(id);
}

public void delete(long id) {
blogRepository.deleteById(id);
}

public Article update(long id, ArticleUpdateRequest request) {
Article article = findArticle(id);

article.update(request.getTitle(), request.getContent());
return article;
}

private Article findArticle(long id) {
return blogRepository.findById(id)
.orElseThrow(() -> MyEntityNotFoundException.of(id));
}
}
1 change: 0 additions & 1 deletion src/main/resources/application.properties

This file was deleted.

22 changes: 22 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
server:
servlet:
context-path: /api

spring:
application:
name: board

jpa:
show-sql: true
properties:
hibernate:
format_sql: true

defer_datasource-initialization: true #data.sql

datasource:
url: jdbc:h2:mem:testdb

h2:
console:
enabled: true
3 changes: 3 additions & 0 deletions src/main/resources/data.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
INSERT INTO article (title, content) VALUES ('제목 1', '내용 1')
INSERT INTO article (title, content) VALUES ('제목 2', '내용 2')
INSERT INTO article (title, content) VALUES ('제목 3', '내용 3')
Loading