diff --git a/buy-sell-service/build.gradle b/buy-sell-service/build.gradle index 91bebd1..f59f133 100644 --- a/buy-sell-service/build.gradle +++ b/buy-sell-service/build.gradle @@ -50,6 +50,11 @@ dependencies { annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" + // MiniO + implementation 'io.minio:minio:8.4.2' + + implementation 'com.fasterxml.uuid:java-uuid-generator:4.0.1' // 최신 버전 확인 후 추가 + } dependencyManagement { diff --git a/buy-sell-service/src/main/java/project/buysellservice/BuySellServiceApplication.java b/buy-sell-service/src/main/java/project/buysellservice/BuySellServiceApplication.java index de0dabd..2641bd3 100644 --- a/buy-sell-service/src/main/java/project/buysellservice/BuySellServiceApplication.java +++ b/buy-sell-service/src/main/java/project/buysellservice/BuySellServiceApplication.java @@ -2,10 +2,16 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import project.buysellservice.global.config.MinioConfig; @EnableDiscoveryClient @SpringBootApplication +@EnableConfigurationProperties(MinioConfig.class) +@EnableJpaAuditing public class BuySellServiceApplication { public static void main(String[] args) { diff --git a/buy-sell-service/src/main/java/project/buysellservice/domain/File/service/FileService.java b/buy-sell-service/src/main/java/project/buysellservice/domain/File/service/FileService.java new file mode 100644 index 0000000..ecc1e3a --- /dev/null +++ b/buy-sell-service/src/main/java/project/buysellservice/domain/File/service/FileService.java @@ -0,0 +1,30 @@ +package project.buysellservice.domain.File.service; + +import org.springframework.web.multipart.MultipartFile; +import project.buysellservice.domain.article.dto.response.FileResponse; + +import java.util.List; +import java.util.Map; + +public interface FileService { + // 파일 업로드 + FileResponse uploadFile(List files, String bucketName) throws Exception; + + // 파일 생성하기 + FileResponse createFile(List files) throws Exception; + + // 파일 수정하기 + FileResponse updateFile(List files, Long id) throws Exception; + + // 파일 삭제하기 + void deleteFile(Long id) throws Exception; + + // 버킷 삭제하기 + void deleteBucket(Long id) throws Exception; + + // 파일 이름 가져오기 + List getFileName(Long id) throws Exception; + + // 버킷 이름 생성 + String getBucketName() throws Exception; +} diff --git a/buy-sell-service/src/main/java/project/buysellservice/domain/File/service/impl/FileServiceImpl.java b/buy-sell-service/src/main/java/project/buysellservice/domain/File/service/impl/FileServiceImpl.java new file mode 100644 index 0000000..ea55253 --- /dev/null +++ b/buy-sell-service/src/main/java/project/buysellservice/domain/File/service/impl/FileServiceImpl.java @@ -0,0 +1,139 @@ +package project.buysellservice.domain.File.service.impl; + +import com.fasterxml.uuid.Generators; +import com.fasterxml.uuid.impl.TimeBasedGenerator; +import io.minio.*; +import io.minio.http.Method; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import project.buysellservice.domain.File.service.FileService; +import project.buysellservice.domain.article.dto.response.FileResponse; +import project.buysellservice.domain.article.repository.ArticleRepository; + +import java.io.InputStream; +import java.util.*; + +@Service +@RequiredArgsConstructor +@Slf4j +public class FileServiceImpl implements FileService { + private final MinioClient minioClient; + private final ArticleRepository articleRepository; + + // 버킷 네임 생성 + @Override + public String getBucketName() { + TimeBasedGenerator generator = Generators.timeBasedGenerator(); + // UUID 버전 1 생성 + UUID uuid = generator.generate(); + + return "post-" + uuid.toString().toLowerCase().replaceAll("-", ""); + } + + // 파일 업로드 과정 (생성, 수정 중복) + @Override + public FileResponse uploadFile(List files, String bucketName) throws Exception { + // 이미지 url 저장 + List urls = new ArrayList<>(); + + for (MultipartFile file : files) { + String fileName = file.getOriginalFilename(); // 파일이름 가져오고 + InputStream fileStream = file.getInputStream(); // 파일 데이터를 읽고 가져온다. + + PutObjectArgs putObjectArgs = PutObjectArgs.builder() + .bucket(bucketName) + .object(fileName) + .stream(fileStream, file.getSize(), -1) + .contentType(file.getContentType()) + .build(); // 파일 빌더 + + minioClient.putObject(putObjectArgs); + // 미니오 서버에 반환하는 응답 객체 + + String fileUrl = minioClient.getPresignedObjectUrl( + GetPresignedObjectUrlArgs.builder() + .method(Method.GET) + .bucket(bucketName) + .object(fileName) + .build() + ); + + urls.add(fileUrl); + } + + return FileResponse.of(urls, bucketName); + } + + // 파일 업로드 + @Override + public FileResponse createFile(List files) throws Exception { + // 버킷 이름 저장 + String bucketName = getBucketName(); + + if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) { + minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); + } // 버킷이 없으면 만들어주는 코드 + + return uploadFile(files, bucketName); + } + + @Override + public FileResponse updateFile(List files, Long id) throws Exception { + String bucketName = articleRepository.getByIdOrThrow(id).getBucketName(); + + return uploadFile(files, bucketName); + } + + + // 이미지 파일 삭제하기 + @Override + public void deleteFile(Long id) throws Exception { + String bucketName = articleRepository.getByIdOrThrow(id).getBucketName(); + List fileNames = getFileName(id); + + for (String file : fileNames) { + minioClient.removeObject( + RemoveObjectArgs.builder() + .bucket(bucketName) + .object(file) + .build() + ); + + } + } + + // 버킷 전체 삭제하기 + @Override + public void deleteBucket(Long id) throws Exception { + String bucketName = articleRepository.getByIdOrThrow(id).getBucketName(); + + deleteFile(id); + + minioClient.removeBucket( + RemoveBucketArgs.builder() + .bucket(bucketName) + .build()); + } + + // 이미지 url 에서 파일 이름 가져오기 + @Override + public List getFileName(Long id) { + List urls = articleRepository.getByIdOrThrow(id).getFiles(); + + List fileNames = new ArrayList<>(); + + for (String url : urls) { + String fileNameWithParams = url.substring(url.lastIndexOf("/") + 1); + + String fileName = fileNameWithParams.split("\\?")[0]; + + fileNames.add(fileName); + } + + return fileNames; + } + + +} diff --git a/buy-sell-service/src/main/java/project/buysellservice/domain/article/controller/ApiV1ArticleController.java b/buy-sell-service/src/main/java/project/buysellservice/domain/article/controller/ApiV1ArticleController.java index a2dd4b5..2b8d301 100644 --- a/buy-sell-service/src/main/java/project/buysellservice/domain/article/controller/ApiV1ArticleController.java +++ b/buy-sell-service/src/main/java/project/buysellservice/domain/article/controller/ApiV1ArticleController.java @@ -1,19 +1,33 @@ package project.buysellservice.domain.article.controller; import jakarta.validation.Valid; +import jakarta.ws.rs.core.Response; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import project.buysellservice.domain.article.dto.request.ArticleRequest; import project.buysellservice.domain.article.dto.response.ArticleResponse; import project.buysellservice.domain.article.service.ArticleService; +import java.util.List; + @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/article") +@Slf4j public class ApiV1ArticleController { private final ArticleService articleService; + // 게시글 전체 페이징 처리 + @GetMapping + public ResponseEntity> getArticle(@Valid @RequestParam("page") int page, + @Valid @RequestParam("size") int size) { + return ResponseEntity.ok(articleService.readAllArticles(PageRequest.of(page, size))); + } + // 게시글 가져오기 @GetMapping("/{id}") public ResponseEntity getArticleById(@PathVariable("id") Long id) { @@ -21,26 +35,34 @@ public ResponseEntity getArticleById(@PathVariable("id") Long i } // 게시글 생성 - @PostMapping - public ResponseEntity createArticle(@Valid @RequestBody ArticleRequest articleRequest) { + @PostMapping(consumes = "multipart/form-data") + public ResponseEntity createArticle(@RequestPart("articleRequest") @Valid ArticleRequest articleRequest, + @RequestPart("file") List files) throws Exception { return ResponseEntity.ok(articleService.createArticle( - articleRequest.title(), articleRequest.content(), articleRequest.imageUrl(), articleRequest.price() + articleRequest.title(), articleRequest.content(), files, articleRequest.price() )); } // 게시글 수정 - @PutMapping("/{id}") + @PutMapping(value = "/{id}", consumes = "multipart/form-data") public ResponseEntity updateArticle(@Valid @PathVariable("id") Long id, - @RequestBody ArticleRequest articleRequest) { + @RequestPart("articleRequest") ArticleRequest articleRequest, + @RequestPart("file") List files) throws Exception { return ResponseEntity.ok(articleService.updateArticle( - id, articleRequest.title(), articleRequest.content(), articleRequest.imageUrl(), articleRequest.price() + id, articleRequest.title(), articleRequest.content(), files, articleRequest.price() )); } // 게시글 삭제 @DeleteMapping("/{id}") - public void deleteArticle(@PathVariable("id") Long id) { + public void deleteArticle(@PathVariable("id") Long id) throws Exception { articleService.deleteArticle(id); } + // 해당 물품 판매 처리 -> 게시글 솔드 상태 + @PatchMapping("/{id}") + public ResponseEntity soldArticle(@Valid @PathVariable("id") Long id){ + return ResponseEntity.ok(articleService.soldArticle(id)); + } + } diff --git a/buy-sell-service/src/main/java/project/buysellservice/domain/article/dto/request/ArticleRequest.java b/buy-sell-service/src/main/java/project/buysellservice/domain/article/dto/request/ArticleRequest.java index 5776b36..840a8e8 100644 --- a/buy-sell-service/src/main/java/project/buysellservice/domain/article/dto/request/ArticleRequest.java +++ b/buy-sell-service/src/main/java/project/buysellservice/domain/article/dto/request/ArticleRequest.java @@ -1,9 +1,10 @@ package project.buysellservice.domain.article.dto.request; +import org.springframework.web.multipart.MultipartFile; + public record ArticleRequest( String title, String content, - String imageUrl, Long price ) { diff --git a/buy-sell-service/src/main/java/project/buysellservice/domain/article/dto/response/ArticleResponse.java b/buy-sell-service/src/main/java/project/buysellservice/domain/article/dto/response/ArticleResponse.java index 9604ccf..67b85d3 100644 --- a/buy-sell-service/src/main/java/project/buysellservice/domain/article/dto/response/ArticleResponse.java +++ b/buy-sell-service/src/main/java/project/buysellservice/domain/article/dto/response/ArticleResponse.java @@ -1,6 +1,7 @@ package project.buysellservice.domain.article.dto.response; import project.buysellservice.domain.article.entity.Article; +import project.buysellservice.domain.article.entity.State; import java.util.List; import java.util.stream.Collectors; @@ -8,15 +9,19 @@ public record ArticleResponse( String title, String content, - String imageUrl, - Long price + Long price, + List files, + State state + ) { public static ArticleResponse of(Article article) { return new ArticleResponse( article.getTitle(), article.getContent(), - article.getImageUrl(), - article.getPrice() + article.getPrice(), + article.getFiles(), + article.getState() + ); } diff --git a/buy-sell-service/src/main/java/project/buysellservice/domain/article/dto/response/FileResponse.java b/buy-sell-service/src/main/java/project/buysellservice/domain/article/dto/response/FileResponse.java new file mode 100644 index 0000000..885f3ae --- /dev/null +++ b/buy-sell-service/src/main/java/project/buysellservice/domain/article/dto/response/FileResponse.java @@ -0,0 +1,18 @@ +package project.buysellservice.domain.article.dto.response; + +import project.buysellservice.domain.article.entity.Article; + +import java.util.List; + +public record FileResponse( + List files, + String bucketName +) { + public static FileResponse of(List urls, String bucketName) { + return new FileResponse( + urls, + bucketName + ); + } + +} diff --git a/buy-sell-service/src/main/java/project/buysellservice/domain/article/entity/Article.java b/buy-sell-service/src/main/java/project/buysellservice/domain/article/entity/Article.java index 0f11f3c..64fa7b7 100644 --- a/buy-sell-service/src/main/java/project/buysellservice/domain/article/entity/Article.java +++ b/buy-sell-service/src/main/java/project/buysellservice/domain/article/entity/Article.java @@ -1,47 +1,75 @@ package project.buysellservice.domain.article.entity; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import project.buysellservice.global.Util.BaseEntity; + +import java.util.List; @Entity @Getter -@Builder +@Builder(toBuilder = true) @NoArgsConstructor @AllArgsConstructor -public class Article { +public class Article extends BaseEntity { @Id @GeneratedValue private Long id; // 고유 아이디 + @Column(name = "title", nullable = false) private String title; // 글 제목 + @Column(name = "content", nullable = false) private String content; // 글 내용 - private String imageUrl; // 사진 (임시) 리스트로 받아오기 + @Column(name = "price", length = 20) + private Long price; // 가격 + + @ElementCollection // List을 JPA에서 사용할 수 있도록 설정 + @CollectionTable(name = "bucketName") // 별도의 테이블을 생성 (file_images 테이블) + @Column(name = "image_url", length = 2048) // 테이블의 컬럼명 지정 + private List files; // 이미지 저장 - private Long price; // 상품가격 + @Column(name = "bucketName") + private String bucketName; // UUID + Post - public static Article createFrom(String title, String content, String imageUrl, Long price) { + @Column(name = "state") + @Enumerated(EnumType.STRING) + private State state; + + // 게시글 생성 빌더 + public static Article createFrom(String title, String content, List files, Long price, String bucketName) { return Article.builder() .title(title) .content(content) - .imageUrl(imageUrl) + .files(files) .price(price) + .bucketName(bucketName) + .state(State.SELL) .build(); } - public static Article updateFrom(Long id, String title, String content, String imageUrl, Long price) { + // 게시글 수정 빌더 + public static Article updateFrom(Long id, String title, String content, List files, Long price, String bucketName) { return Article.builder() .id(id) .title(title) .content(content) - .imageUrl(imageUrl) + .files(files) .price(price) + .bucketName(bucketName) + .state(State.SELL) + .build(); + } + + // 물건이 거래 중 및 팔렸을 때 + public static Article soldFrom(Long id, Article article) { + return article.toBuilder() + .id(id) + .state(State.SOLD) .build(); } } diff --git a/buy-sell-service/src/main/java/project/buysellservice/domain/article/entity/State.java b/buy-sell-service/src/main/java/project/buysellservice/domain/article/entity/State.java new file mode 100644 index 0000000..0705edb --- /dev/null +++ b/buy-sell-service/src/main/java/project/buysellservice/domain/article/entity/State.java @@ -0,0 +1,5 @@ +package project.buysellservice.domain.article.entity; + +public enum State { + SELL, SOLD +} diff --git a/buy-sell-service/src/main/java/project/buysellservice/domain/article/exception/FileException.java b/buy-sell-service/src/main/java/project/buysellservice/domain/article/exception/FileException.java new file mode 100644 index 0000000..fb055ac --- /dev/null +++ b/buy-sell-service/src/main/java/project/buysellservice/domain/article/exception/FileException.java @@ -0,0 +1,10 @@ +package project.buysellservice.domain.article.exception; + + +import lombok.RequiredArgsConstructor; +import project.buysellservice.global.exception.ErrorCode; + +@RequiredArgsConstructor +public class FileException extends RuntimeException { + private final ErrorCode errorCode; +} diff --git a/buy-sell-service/src/main/java/project/buysellservice/domain/article/repository/ArticleRepository.java b/buy-sell-service/src/main/java/project/buysellservice/domain/article/repository/ArticleRepository.java index ef2265d..cb17122 100644 --- a/buy-sell-service/src/main/java/project/buysellservice/domain/article/repository/ArticleRepository.java +++ b/buy-sell-service/src/main/java/project/buysellservice/domain/article/repository/ArticleRepository.java @@ -1,8 +1,12 @@ package project.buysellservice.domain.article.repository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import project.buysellservice.domain.article.entity.Article; +import java.util.UUID; + public interface ArticleRepository extends JpaRepository, ArticleRepositoryCustom { // 해당 게시글 가져오기 default Article getByIdOrThrow(Long id) { @@ -10,4 +14,9 @@ default Article getByIdOrThrow(Long id) { // 예외처리 만들기 } + // 버킷 네임 생성 + default String bucketName() { + return "Post - " + UUID.randomUUID(); + } + } diff --git a/buy-sell-service/src/main/java/project/buysellservice/domain/article/repository/ArticleRepositoryCustom.java b/buy-sell-service/src/main/java/project/buysellservice/domain/article/repository/ArticleRepositoryCustom.java index 5edf52a..3877d3f 100644 --- a/buy-sell-service/src/main/java/project/buysellservice/domain/article/repository/ArticleRepositoryCustom.java +++ b/buy-sell-service/src/main/java/project/buysellservice/domain/article/repository/ArticleRepositoryCustom.java @@ -6,5 +6,5 @@ public interface ArticleRepositoryCustom { // 전체 게시글 받아오기 - Page
findAll(Pageable pageable); + Page
findAllByOrderByCreatedAt(Pageable pageable); } diff --git a/buy-sell-service/src/main/java/project/buysellservice/domain/article/repository/impl/ArticleRepositoryCustomImpl.java b/buy-sell-service/src/main/java/project/buysellservice/domain/article/repository/impl/ArticleRepositoryCustomImpl.java index 38f3c1c..49c4202 100644 --- a/buy-sell-service/src/main/java/project/buysellservice/domain/article/repository/impl/ArticleRepositoryCustomImpl.java +++ b/buy-sell-service/src/main/java/project/buysellservice/domain/article/repository/impl/ArticleRepositoryCustomImpl.java @@ -3,16 +3,29 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import project.buysellservice.domain.article.entity.Article; +import project.buysellservice.domain.article.entity.QArticle; import project.buysellservice.domain.article.repository.ArticleRepositoryCustom; +import java.util.List; + @RequiredArgsConstructor public class ArticleRepositoryCustomImpl implements ArticleRepositoryCustom { private final JPAQueryFactory jpaQueryFactory; @Override - public Page
findAll(Pageable pageable) { - return null; + public Page
findAllByOrderByCreatedAt(Pageable pageable) { + QArticle article = QArticle.article; + + List
articles = jpaQueryFactory + .selectFrom(article) + .orderBy(article.createDate.desc()) + .offset(pageable.getOffset()) // 페이지 시작 위치 + .limit(pageable.getPageSize()) // 한 페이지에 표시할 개수 + .fetch(); + + return new PageImpl<>(articles); } } diff --git a/buy-sell-service/src/main/java/project/buysellservice/domain/article/service/ArticleService.java b/buy-sell-service/src/main/java/project/buysellservice/domain/article/service/ArticleService.java index 99125e0..b404828 100644 --- a/buy-sell-service/src/main/java/project/buysellservice/domain/article/service/ArticleService.java +++ b/buy-sell-service/src/main/java/project/buysellservice/domain/article/service/ArticleService.java @@ -1,19 +1,28 @@ package project.buysellservice.domain.article.service; - -import project.buysellservice.domain.article.dto.request.ArticleRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.web.multipart.MultipartFile; import project.buysellservice.domain.article.dto.response.ArticleResponse; +import java.util.List; + public interface ArticleService { // 게시글 생성 - ArticleResponse createArticle(String name, String content, String imageUrl, Long Price); + ArticleResponse createArticle(String name, String content, List files, Long Price) throws Exception; // 게시글 읽기 ArticleResponse readArticle(Long id); // 게시글 수정 - ArticleResponse updateArticle(Long id, String name, String content, String imageUrl, Long Price); + ArticleResponse updateArticle(Long id, String name, String content, List files, Long price) throws Exception; // 게시글 삭제 - void deleteArticle(Long id); + void deleteArticle(Long id) throws Exception; + + // 전체 게시글 읽기 + List readAllArticles(Pageable pageable); + + // 게시글 팔림 처리 + ArticleResponse soldArticle(Long id); + } diff --git a/buy-sell-service/src/main/java/project/buysellservice/domain/article/service/impl/ArticleServiceImpl.java b/buy-sell-service/src/main/java/project/buysellservice/domain/article/service/impl/ArticleServiceImpl.java index 695e188..ff215af 100644 --- a/buy-sell-service/src/main/java/project/buysellservice/domain/article/service/impl/ArticleServiceImpl.java +++ b/buy-sell-service/src/main/java/project/buysellservice/domain/article/service/impl/ArticleServiceImpl.java @@ -1,40 +1,90 @@ package project.buysellservice.domain.article.service.impl; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import project.buysellservice.domain.File.service.FileService; import project.buysellservice.domain.article.dto.response.ArticleResponse; +import project.buysellservice.domain.article.dto.response.FileResponse; import project.buysellservice.domain.article.entity.Article; import project.buysellservice.domain.article.repository.ArticleRepository; import project.buysellservice.domain.article.service.ArticleService; +import java.util.List; +import java.util.Map; + @Service @RequiredArgsConstructor public class ArticleServiceImpl implements ArticleService { private final ArticleRepository articleRepository; + private final FileService fileService; + // 모든 게시글 가져오기 @Override - public ArticleResponse createArticle(String name, String content, String imageUrl, Long price) { - Article article = Article.createFrom(name, content, imageUrl, price); - articleRepository.save(article); - return ArticleResponse.of(article); + public List readAllArticles(Pageable pageable) { + Page
articles = articleRepository.findAllByOrderByCreatedAt(pageable); + + List
article = articles.getContent(); + + return ArticleResponse.listOf(article); } + @Override + public ArticleResponse soldArticle(Long id) { + Article article = articleRepository.getByIdOrThrow(id); + + Article soldArticle = Article.soldFrom(article.getId(), article); + + articleRepository.save(soldArticle); + + return ArticleResponse.of(soldArticle); + } + + // 단일 게시글 가져오기 @Override public ArticleResponse readArticle(Long id) { Article article = articleRepository.getByIdOrThrow(id); + return ArticleResponse.of(article); } + + // 생성 @Override - public ArticleResponse updateArticle(Long id, String name, String content, String imageUrl, Long price) { + public ArticleResponse createArticle(String name, String content, List files, Long price) throws Exception { + FileResponse fileData = fileService.createFile(files); + + Article article = Article.createFrom(name, content, fileData.files(), price, fileData.bucketName()); + + articleRepository.save(article); + + return ArticleResponse.of(article); + } + + // 수정 + @Override + public ArticleResponse updateArticle(Long id, String name, String content, List files, Long price) throws Exception { + fileService.deleteFile(id); + + FileResponse fileData = fileService.updateFile(files, id); + Article article = articleRepository.getByIdOrThrow(id); - Article newArticle = Article.updateFrom(article.getId(), name, content, imageUrl, price); + + Article newArticle = Article.updateFrom(article.getId(), name, content, fileData.files(), price, fileData.bucketName()); + articleRepository.save(newArticle); + return ArticleResponse.of(newArticle); } + // 삭제 @Override - public void deleteArticle(Long id) { + public void deleteArticle(Long id) throws Exception { + fileService.deleteBucket(id); + articleRepository.deleteById(id); } + } diff --git a/buy-sell-service/src/main/java/project/buysellservice/domain/category/entity/Category.java b/buy-sell-service/src/main/java/project/buysellservice/domain/category/entity/Category.java deleted file mode 100644 index 0cdae30..0000000 --- a/buy-sell-service/src/main/java/project/buysellservice/domain/category/entity/Category.java +++ /dev/null @@ -1,16 +0,0 @@ -package project.buysellservice.domain.category.entity; - -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import lombok.Getter; - -@Entity -@Getter -public class Category { - @Id - @GeneratedValue - private Long id; - - private String name; -} diff --git a/buy-sell-service/src/main/java/project/buysellservice/global/Util/BaseEntity.java b/buy-sell-service/src/main/java/project/buysellservice/global/Util/BaseEntity.java new file mode 100644 index 0000000..d808585 --- /dev/null +++ b/buy-sell-service/src/main/java/project/buysellservice/global/Util/BaseEntity.java @@ -0,0 +1,29 @@ +package project.buysellservice.global.Util; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.*; +import lombok.experimental.SuperBuilder; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +import static lombok.AccessLevel.PROTECTED; + +@Getter +@MappedSuperclass +@NoArgsConstructor(access = PROTECTED) +@AllArgsConstructor(access = PROTECTED) +@EntityListeners(AuditingEntityListener.class) +@ToString +@EqualsAndHashCode +public class BaseEntity { + + @CreatedDate + private LocalDateTime createDate; + + @LastModifiedDate + private LocalDateTime modifyDate; +} diff --git a/buy-sell-service/src/main/java/project/buysellservice/global/config/MinioConfig.java b/buy-sell-service/src/main/java/project/buysellservice/global/config/MinioConfig.java new file mode 100644 index 0000000..8da1f9e --- /dev/null +++ b/buy-sell-service/src/main/java/project/buysellservice/global/config/MinioConfig.java @@ -0,0 +1,27 @@ +package project.buysellservice.global.config; + +import io.minio.MinioClient; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@ConfigurationProperties +public class MinioConfig { + + private String minioUrl; + + private String accessKey; + + private String secretKey; + + @Bean + public MinioClient minioClient() { + return MinioClient.builder() + .endpoint("http://localhost:9000") // 실제 Minio 서버의 endpoint를 입력해야 합니다. + .credentials("minioadmin", "minioadmin123") + .build(); + } + +} diff --git a/buy-sell-service/src/main/java/project/buysellservice/global/config/OctetStreamReadMsgConverter.java b/buy-sell-service/src/main/java/project/buysellservice/global/config/OctetStreamReadMsgConverter.java new file mode 100644 index 0000000..02cb0bc --- /dev/null +++ b/buy-sell-service/src/main/java/project/buysellservice/global/config/OctetStreamReadMsgConverter.java @@ -0,0 +1,35 @@ +package project.buysellservice.global.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Type; + +@Component +public class OctetStreamReadMsgConverter extends AbstractJackson2HttpMessageConverter { + + @Autowired + public OctetStreamReadMsgConverter(ObjectMapper objectMapper, ObjectMapper objectMapper1) { + super(objectMapper, MediaType.APPLICATION_OCTET_STREAM); + } + + // 기존 application/octet-stream 타입을 쓰기로 다루는 메시지 컨버터가 이미 존재 (ByteArrayHttpMessageConverter) + // 따라서 해당 컨버터는 쓰기 작업에는 이용하면 안됨 + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return false; + } + + @Override + public boolean canWrite(Type type, Class clazz, MediaType mediaType) { + return false; + } + + @Override + protected boolean canWrite(MediaType mediaType) { + return false; + } +} diff --git a/buy-sell-service/src/main/java/project/buysellservice/global/config/WebConfig.java b/buy-sell-service/src/main/java/project/buysellservice/global/config/WebConfig.java new file mode 100644 index 0000000..032d542 --- /dev/null +++ b/buy-sell-service/src/main/java/project/buysellservice/global/config/WebConfig.java @@ -0,0 +1,23 @@ +package project.buysellservice.global.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + private final OctetStreamReadMsgConverter octetStreamReadMsgConverter; + + @Autowired + public WebConfig(OctetStreamReadMsgConverter octetStreamReadMsgConverter) { + this.octetStreamReadMsgConverter = octetStreamReadMsgConverter; + } + + @Override + public void configureMessageConverters(List> converters) { + converters.add(octetStreamReadMsgConverter); + } +} diff --git a/buy-sell-service/src/main/java/project/buysellservice/global/exception/ErrorCode.java b/buy-sell-service/src/main/java/project/buysellservice/global/exception/ErrorCode.java new file mode 100644 index 0000000..ff435f4 --- /dev/null +++ b/buy-sell-service/src/main/java/project/buysellservice/global/exception/ErrorCode.java @@ -0,0 +1,21 @@ +package project.buysellservice.global.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ErrorCode { + // 파일 + NOT_EXIST_BUCKET(00, "버킷이 존재하지 않습니다."), + FAIL_UPLOAD_IMAGE(00, "이미지 업로드에 실패하였습니다."); + + + + + + private final int errorCode; + private final String errorMsg; + + +} diff --git a/buy-sell-service/src/main/resources/application.yml b/buy-sell-service/src/main/resources/application.yml index 0ddb8b3..79671f6 100644 --- a/buy-sell-service/src/main/resources/application.yml +++ b/buy-sell-service/src/main/resources/application.yml @@ -2,4 +2,12 @@ spring: application: name: buy-sell-service config: - import: "optional:configserver:${CONFIG_SERVER:http://localhost:8888}" \ No newline at end of file + import: "optional:configserver:${CONFIG_SERVER:http://localhost:8888}" + + # Minio server configuration +minio: + server: + url: http://localhost:9000 + access-key: minioadmin + secret-key: minioadmin123 + bucket: files \ No newline at end of file diff --git a/buycision-conf b/buycision-conf index 77c3da2..f8cbd9b 160000 --- a/buycision-conf +++ b/buycision-conf @@ -1 +1 @@ -Subproject commit 77c3da29a2b9ef6aedb590ad726989c8a8e4135b +Subproject commit f8cbd9b1620fd194c5ac4272c71cddd245ca503f diff --git a/docker-compose.yml b/docker-compose.yml index 44e078c..9cf2f95 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -167,10 +167,29 @@ services: networks: - app-network + minio: + image: minio/minio:latest + container_name: minio-service + restart: unless-stopped + ports: + - "9000:9000" + - "9001:9001" + environment: + MINIO_ACCESS_KEY: minio + MINIO_SECRET_KEY: minio123 + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin123 + volumes: + - minio_data:/data + command: server /data --console-address ":9001" + networks: + - app-network + networks: app-network: driver: bridge volumes: Kafka00: - driver: local \ No newline at end of file + driver: local + minio_data: \ No newline at end of file