Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e7a3993
refactor: News 예외처리 수정
dohy-eon Feb 24, 2025
ac10bee
refactor: 속성 관련 검증로직 수정
dohy-eon Feb 24, 2025
790a184
fix: ErrorCode 세미콜론추가
dohy-eon Feb 24, 2025
abfcb7b
Merge branch 'dev' into refactor/#47
dohy-eon Feb 24, 2025
dd35ada
feat: 수정삭제 테스트케이스 작성
dohy-eon Feb 25, 2025
30f21f6
Merge for branch 'refactor/#47' of https://github.com/DASOM-GitHub/da…
dohy-eon Feb 25, 2025
04799b7
refactor: 생성 트랜잭션추가
dohy-eon Feb 25, 2025
e125d4e
refactor: 404응답처리 400번으로 수정
dohy-eon Feb 25, 2025
5c11932
refactor: 코드컨벤션 공백수정
dohy-eon Feb 25, 2025
be0d58f
feat: News 이미지 관련 파일 수정
dohy-eon Feb 26, 2025
73256de
refactor: 이미지 저장 로직 수정
dohy-eon Feb 26, 2025
ed8d2d2
refactor: 파일저장 로직수정
dohy-eon Feb 27, 2025
f1707fb
fix: 테스트케이스 수정
dohy-eon Feb 27, 2025
f61ebd1
fix: 파일저장 인코딩 수정
dohy-eon Feb 27, 2025
aa453ae
refactor: 응답 객체 추가 및 반환요청 수정
dohy-eon Feb 27, 2025
9ed50b7
refactor: NewsEntity-FileEntity 양방향 관계설정
dohy-eon Feb 27, 2025
564b27e
fix: 테스트코드 수정
dohy-eon Feb 27, 2025
b99ac23
fix: 테스트케이스 수정
dohy-eon Feb 27, 2025
f791528
infra: 파일 업로드 용량 제한 설정
ysw789 Feb 28, 2025
055941a
refactor: 파일 업로드, 조회 로직 리팩토링
ysw789 Feb 28, 2025
1a63c71
feat: 파일 업로드 관련 에러코드 추가
ysw789 Feb 28, 2025
6a89558
feat: 뉴스 CRUD 리팩토링
ysw789 Feb 28, 2025
5368fe0
Merge branch 'dev' into refactor/#47
ysw789 Feb 28, 2025
d680eac
chore: 테스트 케이스 임시 주석 처리
ysw789 Feb 28, 2025
649c94d
Merge remote-tracking branch 'origin/refactor/#47' into refactor/#47
ysw789 Feb 28, 2025
043fe4b
feat: fileType, targetId로 파일 조회 구현
ysw789 Mar 1, 2025
9e2cfad
feat: 뉴스 목록 썸네일 포함 응답 구현
ysw789 Mar 1, 2025
4ad696c
feat: 뉴스 수정 요청 이미지 삭제 요청 처리 구현
ysw789 Mar 1, 2025
6cb9259
chore: 미사용 파일 제거, 공백 수정
ysw789 Mar 1, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import jakarta.validation.Valid;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@Tag(name = "NEWS API", description = "다솜소식 API")
Expand Down Expand Up @@ -40,50 +43,47 @@ public ResponseEntity<List<NewsResponseDto>> getAllNews() {
@Operation(summary = "소식 상세 조회", description = "ID로 특정 소식을 조회")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "조회 성공"),
@ApiResponse(responseCode = "404", description = "소식을 찾을 수 없음")
@ApiResponse(responseCode = "400", description = "소식을 찾을 수 없음")
})
@GetMapping("/{id}")
public ResponseEntity<NewsResponseDto> getNewsById(@PathVariable Long id) {
NewsResponseDto responseDto = newsService.getNewsById(id);
return ResponseEntity.ok(responseDto);
}

// 생성
// 생성 (multipart/form-data 사용)
@Operation(summary = "소식 등록", description = "새로운 소식을 등록")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "생성 완료"),
@ApiResponse(responseCode = "400", description = "유효하지 않은 요청 데이터",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"errorCode\": \"E003\", \"errorMessage\": \"유효하지 않은 입력입니다.\" }")))
})
@PostMapping
public ResponseEntity<NewsResponseDto> createNews(@Valid @RequestBody NewsRequestDto requestDto) {
NewsResponseDto responseDto = newsService.createNews(requestDto);
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<NewsResponseDto> createNews(
@Valid @RequestPart("news") NewsRequestDto requestDto, // JSON 데이터 받기
@RequestPart(value = "images", required = false) List<MultipartFile> imageFiles // 이미지 파일 받기
) {
NewsResponseDto responseDto = newsService.createNews(requestDto, imageFiles);
return ResponseEntity.status(201).body(responseDto);
}

// 수정
// 수정 (multipart/form-data 사용)
@Operation(summary = "소식 수정", description = "ID로 특정 소식을 수정")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "수정 성공"),
@ApiResponse(responseCode = "404", description = "소식을 찾을 수 없음")
})
@PutMapping("/{id}")
public ResponseEntity<NewsResponseDto> updateNews(@PathVariable Long id, @Valid @RequestBody NewsRequestDto requestDto) {
NewsResponseDto updatedNews = newsService.updateNews(id, requestDto);
@PutMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<NewsResponseDto> updateNews(
@PathVariable Long id,
@Valid @RequestPart("news") NewsRequestDto requestDto,
@RequestPart(value = "images", required = false) List<MultipartFile> imageFiles
) {
NewsResponseDto updatedNews = newsService.updateNews(id, requestDto, imageFiles);
return ResponseEntity.ok(updatedNews);
}

// 삭제
@Operation(summary = "소식 삭제", description = "ID로 특정 소식을 삭제")
@ApiResponses({
@ApiResponse(responseCode = "204", description = "삭제 성공"),
@ApiResponse(responseCode = "404", description = "소식을 찾을 수 없음")
@ApiResponse(responseCode = "400", description = "소식을 찾을 수 없음")
})
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteNews(@PathVariable Long id) {
newsService.deleteNews(id);
return ResponseEntity.noContent().build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package dmu.dasom.api.domain.news.controller;

import dmu.dasom.api.domain.news.service.NewsImageService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@RestController
@RequestMapping("/api/news/images")
@RequiredArgsConstructor
public class NewsImageController {

private final NewsImageService newsImageService;

@PostMapping("/upload")
public ResponseEntity<List<String>> uploadImages(@RequestParam("images") List<MultipartFile> images) {
try {
List<String> imageUrls = newsImageService.uploadImages(images);
return ResponseEntity.ok(imageUrls);
} catch (Exception e) {
return ResponseEntity.status(500).body(List.of("파일 업로드 실패: " + e.getMessage()));
}
}

}
14 changes: 9 additions & 5 deletions src/main/java/dmu/dasom/api/domain/news/dto/NewsRequestDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,27 @@
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "NewsRequestDto", description = "뉴스 생성 요청 DTO")
public class NewsRequestDto {

@NotBlank
@Size(max = 100)
@NotBlank(message = "뉴스 제목은 필수입니다.")
@Size(max = 100, message = "뉴스 제목은 최대 100자입니다.")
@Schema(description = "뉴스 제목", example = "새로운 뉴스 제목")
private String title;

@NotBlank
@NotBlank(message = "뉴스 내용은 필수입니다.")
@Schema(description = "뉴스 내용", example = "새로운 뉴스 내용")
private String content;

@Size(max = 255)
@Size(max = 255, message = "이미지 URL은 최대 255자입니다.")
@Schema(description = "뉴스 이미지 URL", example = "http://example.com/image.jpg", nullable = true)
private String imageUrl;
private List<MultipartFile> images;

}
10 changes: 6 additions & 4 deletions src/main/java/dmu/dasom/api/domain/news/dto/NewsResponseDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import lombok.Getter;

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

@Getter
@Schema(name = "NewsResponseDto", description = "뉴스 응답 DTO")
Expand All @@ -21,14 +22,15 @@ public class NewsResponseDto {
@Schema(description = "작성일", example = "2025-02-14T12:00:00")
private LocalDateTime createdAt;

@Schema(description = "뉴스 이미지 URL", example = "http://example.com/image.jpg", nullable = true)
private String imageUrl;
@Schema(description = "뉴스 이미지 URL", example = "['https://example.com/image.jpg', 'https://example.com/image2.jpg']", nullable = true)
private List<String> imageUrls;

public NewsResponseDto(Long id, String title, String content, LocalDateTime createdAt, String imageUrl) {
public NewsResponseDto(Long id, String title, String content, LocalDateTime createdAt, List<String> imageUrls) {
this.id = id;
this.title = title;
this.content = content;
this.createdAt = createdAt;
this.imageUrl = imageUrl;
this.imageUrls = imageUrls;
}

}
41 changes: 13 additions & 28 deletions src/main/java/dmu/dasom/api/domain/news/entity/NewsEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@
import dmu.dasom.api.domain.news.dto.NewsResponseDto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.*;

import java.util.List;

@Getter
@Entity
@Table(name = "news")
Expand All @@ -24,21 +23,20 @@ public class NewsEntity extends BaseEntity {
@Schema(description = "뉴스 ID", example = "1")
private Long id;

@NotNull
@Schema(description = "뉴스 제목", example = "뉴스 예제 제목")
@Column(nullable = false, length = 100)
@Schema(description = "뉴스 제목", example = "뉴스 예제 제목")
private String title;

@Lob
@NotNull
@Schema(description = "뉴스 내용", example = "뉴스 예제 내용")
@Column(nullable = false)
@Schema(description = "뉴스 내용", example = "뉴스 예제 내용")
private String content;


@Schema(description = "뉴스 이미지 URL", example = "http://example.com/image.jpg")
@Column(length = 255)
private String imageUrl;
@ElementCollection
@CollectionTable(name = "news_images", joinColumns = @JoinColumn(name = "news_id"))
@Column(name = "image_url", length = 255)
@Schema(description = "뉴스 이미지 URL 리스트", example = "[\"https://example.com/image1.jpg\", \"https://example.com/image2.jpg\"]")
private List<String> imageUrls;

// 뉴스 상태 업데이트
public void updateStatus(Status status) {
Expand All @@ -47,27 +45,14 @@ public void updateStatus(Status status) {

// NewsEntity → NewsResponseDto 변환
public NewsResponseDto toResponseDto() {
return new NewsResponseDto(id, title, content, getCreatedAt(), imageUrl);
return new NewsResponseDto(id, title, content, getCreatedAt(), imageUrls);
}

//수정기능
public void update(String title, String content, String imageUrl) {
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("제목은 필수입니다");
}
if (title.length() > 100) {
throw new IllegalArgumentException("제목은 100자까지");
}
if (content == null || content.isBlank()) {
throw new IllegalArgumentException("내용은 필수입니다");
}
if (imageUrl != null && imageUrl.length() > 255) {
throw new IllegalArgumentException("이미지 URL은 255자까지");
}

// 수정 메서드
public void update(String title, String content, List<String> imageUrls) {
this.title = title;
this.content = content;
this.imageUrl = imageUrl;
this.imageUrls = imageUrls;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package dmu.dasom.api.domain.news.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Service
@RequiredArgsConstructor
public class NewsImageService {

private static final String IMAGE_DIR = "src/main/resources/static/images/";

public List<String> uploadImages(List<MultipartFile> images) {

List<String> imageUrls = new ArrayList<>();

if (images != null) {
for (MultipartFile image : images) {
try {
String imageUrl = saveImage(image);
imageUrls.add(imageUrl);
} catch (IOException e) {
throw new RuntimeException("이미지 저장 중 오류", e);
}
}
}

return imageUrls;

}

private String saveImage(MultipartFile image) throws IOException {

File uploadDir = new File(IMAGE_DIR);
if (!uploadDir.exists()) {
uploadDir.mkdirs();
}

String fileName = UUID.randomUUID().toString() + "_" + image.getOriginalFilename();
File destFile = new File(IMAGE_DIR + fileName);
image.transferTo(destFile);

return "/images/" + fileName; // 저장된 이미지 URL 반환
}

}
35 changes: 24 additions & 11 deletions src/main/java/dmu/dasom/api/domain/news/service/NewsService.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
package dmu.dasom.api.domain.news.service;

import dmu.dasom.api.domain.common.exception.CustomException;
import dmu.dasom.api.domain.common.exception.ErrorCode;
import dmu.dasom.api.domain.news.dto.NewsRequestDto;
import dmu.dasom.api.domain.news.dto.NewsResponseDto;
import dmu.dasom.api.domain.news.entity.NewsEntity;
import dmu.dasom.api.domain.news.repository.NewsRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class NewsService {

private final NewsRepository newsRepository;

public NewsService(NewsRepository newsRepository) {
this.newsRepository = newsRepository;
}
private final NewsImageService newsImageService;

// 전체 조회
public List<NewsResponseDto> getAllNews() {
Expand All @@ -29,15 +32,19 @@ public List<NewsResponseDto> getAllNews() {
public NewsResponseDto getNewsById(Long id) {
return newsRepository.findById(id)
.map(NewsEntity::toResponseDto)
.orElseThrow(() -> new IllegalArgumentException("해당 뉴스가 존재하지 않습니다. ID: " + id));
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND));
}

// 생성
public NewsResponseDto createNews(NewsRequestDto requestDto) {
@Transactional
public NewsResponseDto createNews(NewsRequestDto requestDto, List<MultipartFile> imageFiles) {
// 이미지 업로드 후 URL 리스트 받아오기
List<String> uploadedImageUrls = newsImageService.uploadImages(imageFiles); // URL 리스트 생성

NewsEntity news = NewsEntity.builder()
.title(requestDto.getTitle())
.content(requestDto.getContent())
.imageUrl(requestDto.getImageUrl())
.imageUrls(uploadedImageUrls) // 이미지 URL 리스트 저장
.build();

NewsEntity savedNews = newsRepository.save(news);
Expand All @@ -46,20 +53,26 @@ public NewsResponseDto createNews(NewsRequestDto requestDto) {

// 수정
@Transactional
public NewsResponseDto updateNews(Long id, NewsRequestDto requestDto) {
public NewsResponseDto updateNews(Long id, NewsRequestDto requestDto, List<MultipartFile> imageFiles) {
// 뉴스 엔티티 찾기
NewsEntity news = newsRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 뉴스가 존재하지 않습니다. ID: " + id));
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND));

// 이미지 업로드 후 URL 리스트 받아오기
List<String> uploadedImageUrls = newsImageService.uploadImages(imageFiles);

news.update(requestDto.getTitle(), requestDto.getContent(), uploadedImageUrls);

news.update(requestDto.getTitle(), requestDto.getContent(), requestDto.getImageUrl());
return news.toResponseDto();
}

// 삭제
@Transactional
public void deleteNews(Long id) {
NewsEntity news = newsRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 뉴스가 존재하지 않습니다. ID: " + id));
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND));

newsRepository.delete(news);
}

}
Loading