diff --git a/src/main/java/com/krylov/refound/config/WebMvcConfig.java b/src/main/java/com/krylov/refound/config/WebMvcConfig.java new file mode 100644 index 0000000..5fc2c18 --- /dev/null +++ b/src/main/java/com/krylov/refound/config/WebMvcConfig.java @@ -0,0 +1,16 @@ +package com.krylov.refound.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry + .addResourceHandler("/uploads/**") + .addResourceLocations("file:uploads/"); // папка uploads/ в корне проекта + } +} diff --git a/src/main/java/com/krylov/refound/controller/PostController.java b/src/main/java/com/krylov/refound/controller/PostController.java index 7507b15..c650790 100644 --- a/src/main/java/com/krylov/refound/controller/PostController.java +++ b/src/main/java/com/krylov/refound/controller/PostController.java @@ -1,24 +1,23 @@ package com.krylov.refound.controller; -import com.krylov.refound.dto.PostFilter; import com.krylov.refound.dto.PostRequest; import com.krylov.refound.dto.PostResponse; import com.krylov.refound.entity.Post; -import com.krylov.refound.repository.PostRepository; +import com.krylov.refound.enums.PostType; import com.krylov.refound.service.PostService; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.http.MediaType; 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.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.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -31,20 +30,38 @@ public class PostController { private final PostService service; - @PostMapping - public PostRequest create(@RequestBody PostRequest request) { + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public PostRequest create( + @ModelAttribute PostRequest request, + @RequestPart(value = "image", required = false) MultipartFile image + ) { log.info(request.toString()); - return service.create(request); + return service.create(request, image); } @GetMapping - public List getFeed() { + public List getFeed() { return service.getFeed(); } - @PutMapping("/{id}") - public PostResponse update(@PathVariable Long id, @RequestBody PostRequest request) { - return service.update(id, request); + @PutMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public PostResponse update( + @PathVariable Long id, + @RequestParam String type, + @RequestParam String title, + @RequestParam(required = false) String description, + @RequestParam(required = false) String category, + @RequestParam(required = false) String phone, + @RequestParam(value = "image", required = false) MultipartFile image + ) { + PostRequest request = new PostRequest(); + request.setType(PostType.from(type)); + request.setTitle(title); + request.setDescription(description); + request.setCategory(category); + request.setPhone(phone); + + return service.update(id, request, image); } @DeleteMapping("/{id}") diff --git a/src/main/java/com/krylov/refound/dto/PostRequest.java b/src/main/java/com/krylov/refound/dto/PostRequest.java index b376953..2c4d532 100644 --- a/src/main/java/com/krylov/refound/dto/PostRequest.java +++ b/src/main/java/com/krylov/refound/dto/PostRequest.java @@ -19,4 +19,6 @@ public class PostRequest { private String category; private String phone; + + private String imageUrl; } diff --git a/src/main/java/com/krylov/refound/dto/PostResponse.java b/src/main/java/com/krylov/refound/dto/PostResponse.java index 3dc7e6a..d15085d 100644 --- a/src/main/java/com/krylov/refound/dto/PostResponse.java +++ b/src/main/java/com/krylov/refound/dto/PostResponse.java @@ -1,8 +1,10 @@ package com.krylov.refound.dto; +import com.krylov.refound.entity.Image; import com.krylov.refound.enums.PostStatus; import com.krylov.refound.enums.PostType; import java.time.LocalDateTime; +import java.util.List; import lombok.Data; @Data @@ -18,5 +20,7 @@ public class PostResponse { private PostStatus status; private LocalDateTime createdAt; private String userEmail; + private String phone; + private List images; private boolean isFavorite; } diff --git a/src/main/java/com/krylov/refound/entity/Image.java b/src/main/java/com/krylov/refound/entity/Image.java index 7da2c07..c17dd3f 100644 --- a/src/main/java/com/krylov/refound/entity/Image.java +++ b/src/main/java/com/krylov/refound/entity/Image.java @@ -1,5 +1,6 @@ package com.krylov.refound.entity; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -21,5 +22,6 @@ public class Image { private String url; @ManyToOne + @JsonIgnore private Post post; } diff --git a/src/main/java/com/krylov/refound/entity/Post.java b/src/main/java/com/krylov/refound/entity/Post.java index c039184..094b95a 100644 --- a/src/main/java/com/krylov/refound/entity/Post.java +++ b/src/main/java/com/krylov/refound/entity/Post.java @@ -3,6 +3,7 @@ package com.krylov.refound.entity; import com.fasterxml.jackson.annotation.JsonIgnore; import com.krylov.refound.enums.PostStatus; import com.krylov.refound.enums.PostType; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -11,8 +12,11 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; import lombok.Getter; import lombok.Setter; @@ -25,24 +29,19 @@ public class Post { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Enumerated(EnumType.STRING) - private PostType type; - private String title; - @Column(length = 2000) private String description; - private String category; - private String city; - private String district; - private Double latitude; private Double longitude; - private LocalDateTime createdAt; + private String phone; + + @Enumerated(EnumType.STRING) + private PostType type; @Enumerated(EnumType.STRING) private PostStatus status; @@ -51,5 +50,8 @@ public class Post { @JsonIgnore private User user; - private String phone; + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List images = new ArrayList<>(); + + } diff --git a/src/main/java/com/krylov/refound/mapper/PostMapper.java b/src/main/java/com/krylov/refound/mapper/PostMapper.java index 3ff4bb9..585704b 100644 --- a/src/main/java/com/krylov/refound/mapper/PostMapper.java +++ b/src/main/java/com/krylov/refound/mapper/PostMapper.java @@ -5,6 +5,9 @@ import com.krylov.refound.dto.PostResponse; import com.krylov.refound.entity.Post; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import com.krylov.refound.entity.Image; +import org.mapstruct.Named; +import java.util.List; @Mapper(componentModel = "spring") public interface PostMapper { @@ -12,5 +15,14 @@ public interface PostMapper { Post toEntity(PostRequest request); @Mapping(source = "user.login", target = "userEmail") + @Mapping(source = "images", target = "images", qualifiedByName = "imagesToUrls") PostResponse toResponse(Post post); + + @Named("imagesToUrls") + default List imagesToUrls(List images) { + if (images == null) return List.of(); + return images.stream() + .map(Image::getUrl) + .toList(); + } } diff --git a/src/main/java/com/krylov/refound/service/PostService.java b/src/main/java/com/krylov/refound/service/PostService.java index 91fd0e2..6b099d1 100644 --- a/src/main/java/com/krylov/refound/service/PostService.java +++ b/src/main/java/com/krylov/refound/service/PostService.java @@ -1,58 +1,75 @@ package com.krylov.refound.service; -import com.krylov.refound.dto.PostFilter; import com.krylov.refound.dto.PostRequest; import com.krylov.refound.dto.PostResponse; import com.krylov.refound.entity.Image; import com.krylov.refound.entity.Post; -import com.krylov.refound.entity.User; import com.krylov.refound.enums.ErrorCode; import com.krylov.refound.enums.PostStatus; import com.krylov.refound.exception.ApiException; import com.krylov.refound.mapper.PostMapper; -import com.krylov.refound.repository.FavoriteRepository; import com.krylov.refound.repository.ImageRepository; import com.krylov.refound.repository.PostRepository; -import com.krylov.refound.util.PostSpecification; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.LocalDateTime; -import java.util.HashSet; import java.util.List; -import java.util.Set; +import java.util.UUID; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @Service @RequiredArgsConstructor +@Slf4j public class PostService { private final PostRepository repository; - private final ImageRepository imageRepository; - private final FileStorageService storageService; private final PostMapper mapper; - private final UserService userService; - private final FavoriteRepository favoriteRepository; + private final ImageRepository imageRepository; @CacheEvict(value = "posts", allEntries = true) - public PostRequest create(PostRequest request) { + public PostRequest create(PostRequest request, MultipartFile image) { Post post = mapper.toEntity(request); post.setCreatedAt(LocalDateTime.now()); post.setStatus(PostStatus.ACTIVE); + repository.save(post); // сначала сохраняем post, чтобы получить id - repository.save(post); + if (image != null && !image.isEmpty()) { + String filename = UUID.randomUUID() + "_" + image.getOriginalFilename(); + Path uploadDir = Paths.get("uploads"); + + try { + Files.createDirectories(uploadDir); + Files.copy(image.getInputStream(), uploadDir.resolve(filename)); + } catch (IOException e) { + throw new RuntimeException("Не удалось сохранить файл: " + filename, e); + } + + Image img = new Image(); + img.setUrl("/uploads/" + filename); + img.setPost(post); + imageRepository.save(img); + } return request; } - public List getFeed() { - List postList = repository.findAll(); - return postList; + public List getFeed() { + return repository.findAll().stream() + .map(post -> { + PostResponse response = mapper.toResponse(post); + log.info("Post {}: images = {}, phone = {}", post.getId(), response.getImages()); // ← сюда + return response; + }).toList(); + } @Cacheable(value = "post", key = "#id") @@ -64,7 +81,7 @@ public class PostService { } @CacheEvict(value = {"posts", "post"}, allEntries = true) - public PostResponse update(Long id, PostRequest request) { + public PostResponse update(Long id, PostRequest request, MultipartFile image) { Post post = repository.findById(id) .orElseThrow(() -> new ApiException( @@ -79,8 +96,35 @@ public class PostService { post.setPhone(request.getPhone()); post.setType(request.getType()); - Post updated = repository.save(post); + if (image != null && !image.isEmpty()) { + // удаляем старые картинки с диска + post.getImages().forEach(img -> { + try { + Files.deleteIfExists(Paths.get("." + img.getUrl())); + } catch (IOException e) { + log.warn("Не удалось удалить файл: {}", img.getUrl()); + } + }); + // очищаем список (orphanRemoval = true удалит из БД) + post.getImages().clear(); + // сохраняем новую картинку + String filename = UUID.randomUUID() + "_" + image.getOriginalFilename(); + Path uploadDir = Paths.get("uploads"); + try { + Files.createDirectories(uploadDir); + Files.copy(image.getInputStream(), uploadDir.resolve(filename)); + } catch (IOException e) { + throw new RuntimeException("Не удалось сохранить файл: " + filename, e); + } + + Image img = new Image(); + img.setUrl("/uploads/" + filename); + img.setPost(post); + post.getImages().add(img); + } + + Post updated = repository.save(post); return mapper.toResponse(updated); } diff --git a/src/main/java/com/krylov/refound/util/StringToPostTypeConverter.java b/src/main/java/com/krylov/refound/util/StringToPostTypeConverter.java new file mode 100644 index 0000000..c20f16b --- /dev/null +++ b/src/main/java/com/krylov/refound/util/StringToPostTypeConverter.java @@ -0,0 +1,13 @@ +package com.krylov.refound.util; + +import com.krylov.refound.enums.PostType; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; + +@Component +public class StringToPostTypeConverter implements Converter { + @Override + public PostType convert(String value) { + return PostType.from(value); + } +}