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

[BE] [FEAT] 뉴스 CRUD API 생성 #240

Merged
merged 16 commits into from
Jan 3, 2025
Merged
15 changes: 14 additions & 1 deletion backend/backup.sql
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ DROP TABLE IF EXISTS `admin_SEQ`;
DROP TABLE IF EXISTS `admin`;
DROP TABLE IF EXISTS `UserEntity`;
DROP TABLE IF EXISTS `professor_SEQ`;
DROP TABLE IF EXISTS `news`;

-- Admin table
CREATE TABLE `admin` (
Expand Down Expand Up @@ -119,4 +120,16 @@ CREATE TABLE `reservation` (
CONSTRAINT `FK_reservation_seminar_room` FOREIGN KEY (`seminar_room_id`) REFERENCES `seminar_room` (`seminar_room_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;

-- Continue for other tables like `thesis`, `board`, and `users`
-- Continue for other tables like `thesis`, `board`, and `users`

-- News 테이블 생성
CREATE TABLE `news` (
`news_id` BIGINT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255),
`content` TEXT,
`view` INT DEFAULT 0,
`link` VARCHAR(255),
`image` VARCHAR(1000),
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`news_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.example.backend.news.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.example.backend.common.dto.PageRequestDto;
import org.example.backend.common.dto.ResponseDto;
import org.example.backend.news.domain.dto.NewsReqDto;
import org.example.backend.news.domain.dto.NewsResDto;
import org.example.backend.news.service.NewsService;
import org.springframework.data.domain.Page;
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.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequiredArgsConstructor
@Tag(name = "뉴스", description = "뉴스 API")
@RequestMapping("/api/news")
public class NewsController {
private final NewsService newsService;

@Operation(summary = "뉴스 생성 API", description = "뉴스 생성")
@PostMapping(consumes = "multipart/form-data")
public ResponseEntity<Long> createNews(
@RequestPart(value = "newsReqDto") @Valid NewsReqDto newsReqDto,
@RequestPart(value = "newsImage", required = false) MultipartFile multipartFile
) {
Long newsId = newsService.saveNews(newsReqDto, multipartFile);
return new ResponseEntity<>(newsId, HttpStatus.OK);
}

@Operation(summary = "모든 뉴스 조회 API", description = "모든 뉴스의 리스트 반환")
@GetMapping
public ResponseDto<List<NewsResDto>> getAllBoards(@Valid @ModelAttribute PageRequestDto pageRequest) {

Page<NewsResDto> newsList = newsService.getAllNewss(pageRequest.toPageable());
return ResponseDto.ok(newsList.getNumber(), newsList.getTotalPages(), newsList.getContent());
}

@Operation(summary = "단일 뉴스 조회 API", description = "단일 뉴스의 리스트 반환")
@GetMapping("/{newsId}")
public ResponseEntity<NewsResDto> getNews(@PathVariable(name = "newsId") Long newsId) {
NewsResDto newsResDto = newsService.getNews(newsId);
return new ResponseEntity<>(newsResDto, HttpStatus.OK);
}

@Operation(summary = "뉴스 정보 업데이트 API", description = "뉴스 정보 업데이트")
@PostMapping("/{newsId}")
public ResponseEntity<NewsResDto> updateNews(@PathVariable(name = "newsId") Long newsId,
@RequestPart(value = "newsReqDto") NewsReqDto newsReqDto,
@RequestPart(value = "newsImage", required = false) MultipartFile multipartFile) {
NewsResDto newsResDto = newsService.updateNews(newsId, newsReqDto, multipartFile);
return new ResponseEntity<>(newsResDto, HttpStatus.OK);
}

@Operation(summary = "뉴스 삭제 API", description = "뉴스 삭제")
@DeleteMapping("/{newsId}")
public ResponseEntity<?> deleteNews(@PathVariable(name = "newsId") Long newsId) {
newsService.deleteNews(newsId);
return new ResponseEntity<>(HttpStatus.OK);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.example.backend.news.domain.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class NewsReqDto {

@NotBlank(message = "제목은 필수 입력값입니다.")
@Size(max = 200, message = "제목은 최대 200자 입력 가능합니다.")
private String title;

@NotBlank(message = "내용은 필수 입력값입니다.")
@Size(max = 5000, message = "내용은 최대 5000자까지 입력 가능합니다.")
private String content;

private String createDate;
private String link;
private String image;

@Builder
private NewsReqDto(String title, String content, String createDate,
String link, String image) {
this.title = title;
this.content = content;
this.createDate = createDate;
this.link = link;
this.image = image;
}

public static NewsReqDto of(String title, String content, String createDate,
String link, String image) {
return NewsReqDto.builder()
.title(title)
.content(content)
.createDate(createDate)
.link(link)
.image(image)
.build();
}

public void setImage(String image) {
this.image = image;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.example.backend.news.domain.dto;

import java.time.format.DateTimeFormatter;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.example.backend.news.domain.entity.News;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class NewsResDto {
private Long id;
private String title;
private String content;
private int view;
private String createDate;
private String link;
private String image;

@Builder
private NewsResDto(Long id, String title, int view, String content, String createDate,
String link, String image) {
this.id = id;
this.title = title;
this.content = content;
this.view = view;
this.createDate = createDate;
this.link = link;
this.image = image;
}

public static NewsResDto of(News news) {
return NewsResDto.builder()
.id(news.getId())
.title(news.getTitle())
.content(news.getContent())
.view(news.getView())
.createDate(news.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")))
.link(news.getLink())
.image(news.getImage())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.example.backend.news.domain.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.example.backend.common.domain.BaseEntity;
import org.example.backend.news.domain.dto.NewsReqDto;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class News extends BaseEntity {

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

@Column(name = "name")
private String title;

@Column(name = "content")
private String content;

@Column(name = "view")
private int view;

@Column(name = "link")
private String link;

@Column(name = "image", length = 1000)
private String image;


@Builder
private News(String title, String content, int view, String link,
String image) {
this.title = title;
this.content = content;
this.view = view;
this.link = link;
this.image = image;
}

public static News of(NewsReqDto dto) {
return News.builder()
.title(dto.getTitle())
.content(dto.getContent())
.view(0)
.link(dto.getLink())
.image(dto.getImage())
.build();
}

public void update(NewsReqDto dto) {
this.title = dto.getTitle();
this.content = dto.getContent();
this.link = dto.getLink();
this.image = dto.getImage();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.example.backend.news.exception;

import lombok.RequiredArgsConstructor;
import org.example.backend.common.exception.BaseException;
import org.example.backend.common.exception.BaseExceptionType;

@RequiredArgsConstructor
public class NewsException extends BaseException {

private final NewsExceptionType exceptionType;

@Override
public BaseExceptionType exceptionType() {
return exceptionType;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.example.backend.news.exception;

import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.NOT_FOUND;

import lombok.RequiredArgsConstructor;
import org.example.backend.common.exception.BaseExceptionType;
import org.springframework.http.HttpStatus;

@RequiredArgsConstructor
public enum NewsExceptionType implements BaseExceptionType {

NOT_FOUND_NEWS(NOT_FOUND, "뉴스를 찾을 수 없습니다")
;

private final HttpStatus httpStatus;
private final String errorMessage;

@Override
public HttpStatus httpStatus() {
return httpStatus;
}

@Override
public String errorMessage() {
return errorMessage;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.example.backend.news.repository;

import org.example.backend.news.domain.entity.News;
import org.springframework.data.jpa.repository.JpaRepository;

public interface NewsRepository extends JpaRepository<News, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.example.backend.news.service;

import static org.example.backend.news.exception.NewsExceptionType.NOT_FOUND_NEWS;

import lombok.RequiredArgsConstructor;
import org.example.backend.global.config.aws.S3Uploader;
import org.example.backend.news.domain.dto.NewsReqDto;
import org.example.backend.news.domain.dto.NewsResDto;
import org.example.backend.news.domain.entity.News;
import org.example.backend.news.exception.NewsException;
import org.example.backend.news.exception.NewsExceptionType;
import org.example.backend.news.repository.NewsRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class NewsService {
private final NewsRepository newsRepository;
private final S3Uploader s3Uploader;
private static final String dirName = "news";

@Transactional
public Long saveNews(NewsReqDto newsReqDto, MultipartFile multipartFile) {

if (multipartFile != null && !multipartFile.isEmpty()) {
String uploadImageUrl = s3Uploader.upload(multipartFile, dirName);
newsReqDto.setImage(uploadImageUrl);
}
News news = News.of(newsReqDto);
News savedNews = newsRepository.save(news);
return savedNews.getId();
}

public NewsResDto getNews(Long newsId) {
News news = findNewsById(newsId);
return NewsResDto.of(news);
}

@Transactional
public NewsResDto updateNews(Long newsId, NewsReqDto newsReqDto, MultipartFile multipartFile) {
if (multipartFile != null && !multipartFile.isEmpty()) {
String uploadImageUrl = s3Uploader.upload(multipartFile, dirName);
newsReqDto.setImage(uploadImageUrl);
}

News news = findNewsById(newsId);
news.update(newsReqDto);
return NewsResDto.of(news);
}

public void deleteNews(Long newsId) {
News news = findNewsById(newsId);
newsRepository.delete(news);
}

private News findNewsById(Long newsId) {
return newsRepository.findById(newsId)
.orElseThrow(() -> new NewsException(NOT_FOUND_NEWS));
}

public Page<NewsResDto> getAllNewss(Pageable pageable) {
return newsRepository.findAll(pageable)
.map(NewsResDto::of);
}
}
Loading
Loading