diff --git a/backend/backup.sql b/backend/backup.sql index 0aad9380..1292edbc 100644 --- a/backend/backup.sql +++ b/backend/backup.sql @@ -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` ( @@ -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` \ No newline at end of file +-- 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; \ No newline at end of file diff --git a/backend/src/main/java/org/example/backend/news/controller/NewsController.java b/backend/src/main/java/org/example/backend/news/controller/NewsController.java new file mode 100644 index 00000000..d25772c5 --- /dev/null +++ b/backend/src/main/java/org/example/backend/news/controller/NewsController.java @@ -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 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> getAllBoards(@Valid @ModelAttribute PageRequestDto pageRequest) { + + Page newsList = newsService.getAllNewss(pageRequest.toPageable()); + return ResponseDto.ok(newsList.getNumber(), newsList.getTotalPages(), newsList.getContent()); + } + + @Operation(summary = "단일 뉴스 조회 API", description = "단일 뉴스의 리스트 반환") + @GetMapping("/{newsId}") + public ResponseEntity 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 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); + } +} diff --git a/backend/src/main/java/org/example/backend/news/domain/dto/NewsReqDto.java b/backend/src/main/java/org/example/backend/news/domain/dto/NewsReqDto.java new file mode 100644 index 00000000..36a288d5 --- /dev/null +++ b/backend/src/main/java/org/example/backend/news/domain/dto/NewsReqDto.java @@ -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; + } +} diff --git a/backend/src/main/java/org/example/backend/news/domain/dto/NewsResDto.java b/backend/src/main/java/org/example/backend/news/domain/dto/NewsResDto.java new file mode 100644 index 00000000..1e170008 --- /dev/null +++ b/backend/src/main/java/org/example/backend/news/domain/dto/NewsResDto.java @@ -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(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/example/backend/news/domain/entity/News.java b/backend/src/main/java/org/example/backend/news/domain/entity/News.java new file mode 100644 index 00000000..1ccb8b81 --- /dev/null +++ b/backend/src/main/java/org/example/backend/news/domain/entity/News.java @@ -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(); + } +} diff --git a/backend/src/main/java/org/example/backend/news/exception/NewsException.java b/backend/src/main/java/org/example/backend/news/exception/NewsException.java new file mode 100644 index 00000000..e9116afd --- /dev/null +++ b/backend/src/main/java/org/example/backend/news/exception/NewsException.java @@ -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; + } +} diff --git a/backend/src/main/java/org/example/backend/news/exception/NewsExceptionType.java b/backend/src/main/java/org/example/backend/news/exception/NewsExceptionType.java new file mode 100644 index 00000000..d65360e0 --- /dev/null +++ b/backend/src/main/java/org/example/backend/news/exception/NewsExceptionType.java @@ -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; + } +} diff --git a/backend/src/main/java/org/example/backend/news/repository/NewsRepository.java b/backend/src/main/java/org/example/backend/news/repository/NewsRepository.java new file mode 100644 index 00000000..2d98b2de --- /dev/null +++ b/backend/src/main/java/org/example/backend/news/repository/NewsRepository.java @@ -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 { +} diff --git a/backend/src/main/java/org/example/backend/news/service/NewsService.java b/backend/src/main/java/org/example/backend/news/service/NewsService.java new file mode 100644 index 00000000..bbc05acd --- /dev/null +++ b/backend/src/main/java/org/example/backend/news/service/NewsService.java @@ -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 getAllNewss(Pageable pageable) { + return newsRepository.findAll(pageable) + .map(NewsResDto::of); + } +} \ No newline at end of file diff --git a/backend/src/main/resources/data.sql b/backend/src/main/resources/data.sql index e44c84bb..452805d1 100644 --- a/backend/src/main/resources/data.sql +++ b/backend/src/main/resources/data.sql @@ -16,6 +16,8 @@ DELETE FROM admin; DELETE FROM users; +DELETE +FROM news; -- Department 더미 데이터 (1개) @@ -313,4 +315,76 @@ VALUES ('2024-12-01 09:00:00', '2024-12-01 11:00:00', 'MEETING', '홍성무교 ('2024-12-06 10:30:00', '2024-12-06 12:30:00', 'MEETING', '김은희교수님 랩미팅', 1, 1, NOW(), NOW()), ('2024-12-07 13:30:00', '2024-12-07 15:30:00', 'MEETING', '김민수교수님 랩미팅', 1, 2, NOW(), NOW()), ('2024-12-08 11:00:00', '2024-12-08 13:00:00', 'MEETING', '전종훈교수님 랩미팅', 1, 1, NOW(), NOW()), - ('2024-12-09 16:00:00', '2024-12-09 18:00:00', 'MEETING', '서민석교수님 랩미팅', 1, 4, NOW(), NOW()); \ No newline at end of file + ('2024-12-09 16:00:00', '2024-12-09 18:00:00', 'MEETING', '서민석교수님 랩미팅', 1, 4, NOW(), NOW()); + +INSERT INTO news (name, content, view, link, image, created_at) +VALUES + ('세종대학교 바이오융합공학과, 혁신 연구 성과 발표', + '세종대학교 바이오융합공학과가 올해 혁신적인 연구 성과를 발표했습니다.', + 320, + 'https://example.com/news1', + 'https://dibb-bucket.s3.ap-northeast-2.amazonaws.com/news/image1.jpg', + NOW()), + + ('바이오융합공학과, 신입생 환영회 개최', + '세종대학교 바이오융합공학과가 신입생들을 위한 환영회를 열었습니다.', + 150, + 'https://example.com/news2', + 'https://dibb-bucket.s3.ap-northeast-2.amazonaws.com/news/image2.jpg', + NOW()), + + ('세종대, 바이오 분야 특화 교육 프로그램 개설', + '세종대학교가 바이오 분야를 중심으로 한 특화 교육 프로그램을 개설하였습니다.', + 200, + 'https://example.com/news3', + 'https://dibb-bucket.s3.ap-northeast-2.amazonaws.com/news/image3.jpg', + NOW()), + + ('바이오융합공학과 졸업생, 글로벌 기업 취업 성공', + '세종대학교 바이오융합공학과 졸업생이 글로벌 기업에 성공적으로 취업했습니다.', + 400, + 'https://example.com/news4', + 'https://dibb-bucket.s3.ap-northeast-2.amazonaws.com/news/image4.jpg', + NOW()), + + ('바이오융합공학과, 국제 학술대회 논문 발표', + '바이오융합공학과 교수진이 국제 학술대회에서 연구 논문을 발표했습니다.', + 280, + 'https://example.com/news5', + 'https://dibb-bucket.s3.ap-northeast-2.amazonaws.com/news/image5.jpg', + NOW()), + + ('세종대, 바이오 산업 연구 지원 강화', + '세종대학교는 바이오 산업 연구를 위한 지원을 강화하고 있습니다.', + 170, + 'https://example.com/news6', + 'https://dibb-bucket.s3.ap-northeast-2.amazonaws.com/news/image6.jpg', + NOW()), + + ('세종대 바이오융합공학과, 혁신적인 실험실 설립', + '세종대학교 바이오융합공학과가 혁신적인 실험실을 설립하였습니다.', + 310, + 'https://example.com/news7', + 'https://dibb-bucket.s3.ap-northeast-2.amazonaws.com/news/image7.jpg', + NOW()), + + ('세종대 바이오융합공학과, 신약 개발 프로젝트 시작', + '세종대학교 바이오융합공학과가 신약 개발 프로젝트를 시작했습니다.', + 250, + 'https://example.com/news8', + 'https://dibb-bucket.s3.ap-northeast-2.amazonaws.com/news/image8.jpg', + NOW()), + + ('세종대학교, 바이오융합공학과 신임 교수 임용', + '세종대학교는 바이오융합공학과에 새로운 교수님을 임용했습니다.', + 180, + 'https://example.com/news9', + 'https://dibb-bucket.s3.ap-northeast-2.amazonaws.com/news/image9.jpg', + NOW()), + + ('세종대 바이오융합공학과, 학술 논문 출판', + '바이오융합공학과 교수진이 학술 논문을 성공적으로 출판하였습니다.', + 210, + 'https://example.com/news10', + 'https://dibb-bucket.s3.ap-northeast-2.amazonaws.com/news/image10.jpg', + NOW()); \ No newline at end of file