diff --git a/http/test.http b/http/test.http index 9cd3bb74..d7de85aa 100644 --- a/http/test.http +++ b/http/test.http @@ -48,7 +48,9 @@ Content-Type: application/json "title": "무신사 스폰서십", "type": "SPONSORSHIP", "category": "MARKETING", - "content": "무신사 스폰서십을 진행할 대학교 학생회를 모집합니다." + "content": "무신사 스폰서십을 진행할 대학교 학생회를 모집합니다.", + "images": [ + ] } ### 공고 상세 조회 diff --git a/src/main/java/com/sponus/sponusbe/domain/announcement/controller/AnnouncementController.java b/src/main/java/com/sponus/sponusbe/domain/announcement/controller/AnnouncementController.java index 53825bcc..006db534 100644 --- a/src/main/java/com/sponus/sponusbe/domain/announcement/controller/AnnouncementController.java +++ b/src/main/java/com/sponus/sponusbe/domain/announcement/controller/AnnouncementController.java @@ -7,16 +7,18 @@ import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; -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; import com.sponus.sponusbe.auth.annotation.AuthOrganization; import com.sponus.sponusbe.domain.announcement.dto.request.AnnouncementCreateRequest; import com.sponus.sponusbe.domain.announcement.dto.request.AnnouncementUpdateRequest; import com.sponus.sponusbe.domain.announcement.dto.response.AnnouncementCreateResponse; -import com.sponus.sponusbe.domain.announcement.dto.response.AnnouncementResponse; +import com.sponus.sponusbe.domain.announcement.dto.response.AnnouncementDetailResponse; +import com.sponus.sponusbe.domain.announcement.dto.response.AnnouncementSummaryResponse; import com.sponus.sponusbe.domain.announcement.dto.response.AnnouncementUpdateResponse; import com.sponus.sponusbe.domain.announcement.entity.enums.AnnouncementStatus; import com.sponus.sponusbe.domain.announcement.service.AnnouncementQueryService; @@ -48,27 +50,34 @@ public ApiResponse getAnnouncement() { } @GetMapping("/{announcementId}") - public ApiResponse getAnnouncement(@PathVariable Long announcementId) { + public ApiResponse getAnnouncement(@PathVariable Long announcementId) { return ApiResponse.onSuccess(announcementService.getAnnouncement(announcementId)); } @GetMapping("/status") - public ApiResponse> getListAnnouncement( + public ApiResponse> getListAnnouncement( @RequestParam("status") AnnouncementStatus status) { return ApiResponse.onSuccess(announcementService.getListAnnouncement(status)); } @GetMapping - public ApiResponse> searchAnnouncement(@RequestParam("search") String keyword) { + public ApiResponse> searchAnnouncement(@RequestParam("search") String keyword) { return ApiResponse.onSuccess(announcementQueryService.searchAnnouncement(keyword)); } - @PostMapping + @PostMapping(consumes = "multipart/form-data") public ApiResponse createAnnouncement( @AuthOrganization Organization authOrganization, - @RequestBody @Valid AnnouncementCreateRequest request + @RequestPart("request") @Valid AnnouncementCreateRequest request, + @RequestPart(value = "images") List images ) { - return ApiResponse.onSuccess(announcementService.createAnnouncement(authOrganization, request)); + return ApiResponse.onSuccess( + announcementService.createAnnouncement( + authOrganization, + request, + images + ) + ); } @DeleteMapping("/{announcementId}") @@ -79,14 +88,19 @@ public ApiResponse deleteAnnouncement( return ApiResponse.onSuccess(null); } - @PatchMapping("/{announcementId}") + @PatchMapping(value = "/{announcementId}", consumes = "multipart/form-data") public ApiResponse updateAnnouncement( @AuthOrganization Organization authOrganization, @PathVariable Long announcementId, - @RequestBody @Valid AnnouncementUpdateRequest request + @RequestPart("request") @Valid AnnouncementUpdateRequest request, + @RequestPart(value = "images", required = false) List images ) { - announcementService.updateAnnouncement(authOrganization, announcementId, request); - return ApiResponse.onSuccess(announcementService.updateAnnouncement(authOrganization, announcementId, request)); + return ApiResponse.onSuccess(announcementService.updateAnnouncement( + authOrganization, + announcementId, + request, + images + )); } } diff --git a/src/main/java/com/sponus/sponusbe/domain/announcement/dto/request/AnnouncementCreateRequest.java b/src/main/java/com/sponus/sponusbe/domain/announcement/dto/request/AnnouncementCreateRequest.java index a7bbc825..87189c17 100644 --- a/src/main/java/com/sponus/sponusbe/domain/announcement/dto/request/AnnouncementCreateRequest.java +++ b/src/main/java/com/sponus/sponusbe/domain/announcement/dto/request/AnnouncementCreateRequest.java @@ -8,19 +8,17 @@ import com.sponus.sponusbe.domain.announcement.entity.enums.AnnouncementType; import com.sponus.sponusbe.domain.organization.entity.Organization; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; public record AnnouncementCreateRequest( - @NotBlank(message = "[ERROR] 타이틀 입력은 필수 입니다.") + @NotNull(message = "[ERROR] 타이틀 입력은 필수 입니다.") String title, @NotNull(message = "[ERROR] 유형 입력은 필수 입니다.") AnnouncementType type, @NotNull(message = "[ERROR] 카테코리 입력은 필수 입니다.") AnnouncementCategory category, - @NotBlank(message = "[ERROR] 내용 입력은 필수 입니다.") + @NotNull(message = "[ERROR] 내용 입력은 필수 입니다.") String content, - AnnouncementStatus status ) { @@ -35,3 +33,4 @@ public Announcement toEntity(Organization writer) { .build(); } } + diff --git a/src/main/java/com/sponus/sponusbe/domain/announcement/dto/response/AnnouncementBriefResponse.java b/src/main/java/com/sponus/sponusbe/domain/announcement/dto/response/AnnouncementBriefResponse.java deleted file mode 100644 index 11c10770..00000000 --- a/src/main/java/com/sponus/sponusbe/domain/announcement/dto/response/AnnouncementBriefResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.sponus.sponusbe.domain.announcement.dto.response; - -public record AnnouncementBriefResponse() { -} diff --git a/src/main/java/com/sponus/sponusbe/domain/announcement/dto/response/AnnouncementResponse.java b/src/main/java/com/sponus/sponusbe/domain/announcement/dto/response/AnnouncementDetailResponse.java similarity index 66% rename from src/main/java/com/sponus/sponusbe/domain/announcement/dto/response/AnnouncementResponse.java rename to src/main/java/com/sponus/sponusbe/domain/announcement/dto/response/AnnouncementDetailResponse.java index abe7f38e..6eb27aa7 100644 --- a/src/main/java/com/sponus/sponusbe/domain/announcement/dto/response/AnnouncementResponse.java +++ b/src/main/java/com/sponus/sponusbe/domain/announcement/dto/response/AnnouncementDetailResponse.java @@ -1,5 +1,7 @@ package com.sponus.sponusbe.domain.announcement.dto.response; +import java.util.List; + import com.sponus.sponusbe.domain.announcement.entity.Announcement; import com.sponus.sponusbe.domain.announcement.entity.enums.AnnouncementCategory; import com.sponus.sponusbe.domain.announcement.entity.enums.AnnouncementStatus; @@ -8,24 +10,31 @@ import lombok.Builder; @Builder -public record AnnouncementResponse( +public record AnnouncementDetailResponse( Long id, Long writerId, String title, AnnouncementType type, AnnouncementCategory category, String content, + List announcementImages, AnnouncementStatus status, Long viewCount ) { - public static AnnouncementResponse from(Announcement announcement) { - return AnnouncementResponse.builder() + public static AnnouncementDetailResponse from(Announcement announcement) { + List announcementImages = announcement.getAnnouncementImages() + .stream() + .map(AnnouncementImageResponse::from) + .toList(); + + return AnnouncementDetailResponse.builder() .id(announcement.getId()) .writerId(announcement.getWriter().getId()) .title(announcement.getTitle()) .type(announcement.getType()) .category(announcement.getCategory()) .content(announcement.getContent()) + .announcementImages(announcementImages) .status(announcement.getStatus()) .viewCount(announcement.getViewCount()) .build(); diff --git a/src/main/java/com/sponus/sponusbe/domain/announcement/dto/response/AnnouncementImageResponse.java b/src/main/java/com/sponus/sponusbe/domain/announcement/dto/response/AnnouncementImageResponse.java new file mode 100644 index 00000000..316a9ffe --- /dev/null +++ b/src/main/java/com/sponus/sponusbe/domain/announcement/dto/response/AnnouncementImageResponse.java @@ -0,0 +1,20 @@ +package com.sponus.sponusbe.domain.announcement.dto.response; + +import com.sponus.sponusbe.domain.announcement.entity.AnnouncementImage; + +import lombok.Builder; + +@Builder +public record AnnouncementImageResponse( + Long id, + String name, + String url +) { + public static AnnouncementImageResponse from(AnnouncementImage image) { + return AnnouncementImageResponse.builder() + .id(image.getId()) + .name(image.getName()) + .url(image.getUrl()) + .build(); + } +} diff --git a/src/main/java/com/sponus/sponusbe/domain/announcement/dto/response/AnnouncementSummaryResponse.java b/src/main/java/com/sponus/sponusbe/domain/announcement/dto/response/AnnouncementSummaryResponse.java new file mode 100644 index 00000000..6d5bcf6b --- /dev/null +++ b/src/main/java/com/sponus/sponusbe/domain/announcement/dto/response/AnnouncementSummaryResponse.java @@ -0,0 +1,38 @@ +package com.sponus.sponusbe.domain.announcement.dto.response; + +import com.sponus.sponusbe.domain.announcement.entity.Announcement; +import com.sponus.sponusbe.domain.announcement.entity.AnnouncementImage; +import com.sponus.sponusbe.domain.announcement.entity.enums.AnnouncementCategory; +import com.sponus.sponusbe.domain.announcement.entity.enums.AnnouncementStatus; +import com.sponus.sponusbe.domain.announcement.entity.enums.AnnouncementType; + +import lombok.Builder; + +@Builder +public record AnnouncementSummaryResponse( + Long id, + Long writerId, + String title, + AnnouncementType type, + AnnouncementCategory category, + AnnouncementImageResponse mainImage, + AnnouncementStatus status, + Long viewCount +) { + public static AnnouncementSummaryResponse from(Announcement announcement) { + AnnouncementImage mainImage = announcement.getAnnouncementImages() + .stream() + .findFirst().orElseThrow(); + + return AnnouncementSummaryResponse.builder() + .id(announcement.getId()) + .writerId(announcement.getWriter().getId()) + .title(announcement.getTitle()) + .type(announcement.getType()) + .category(announcement.getCategory()) + .mainImage(AnnouncementImageResponse.from(mainImage)) + .status(announcement.getStatus()) + .viewCount(announcement.getViewCount()) + .build(); + } +} diff --git a/src/main/java/com/sponus/sponusbe/domain/announcement/entity/Announcement.java b/src/main/java/com/sponus/sponusbe/domain/announcement/entity/Announcement.java index 08d8dbc1..c3c907fa 100644 --- a/src/main/java/com/sponus/sponusbe/domain/announcement/entity/Announcement.java +++ b/src/main/java/com/sponus/sponusbe/domain/announcement/entity/Announcement.java @@ -9,6 +9,7 @@ import com.sponus.sponusbe.domain.organization.entity.Organization; import com.sponus.sponusbe.global.common.BaseEntity; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.ConstraintMode; import jakarta.persistence.Entity; @@ -68,14 +69,14 @@ public class Announcement extends BaseEntity { private Organization writer; @Builder.Default - @OneToMany(mappedBy = "announcement") - private List announcementAttachments = new ArrayList<>(); + @OneToMany(mappedBy = "announcement", cascade = CascadeType.ALL, orphanRemoval = true) + private List announcementImages = new ArrayList<>(); public void increaseViewCount() { this.viewCount++; } - public void update(String title, AnnouncementType type, AnnouncementCategory category, String content, + public void updateInfo(String title, AnnouncementType type, AnnouncementCategory category, String content, AnnouncementStatus status) { this.title = title == null ? this.title : title; this.type = type == null ? this.type : type; @@ -84,6 +85,13 @@ public void update(String title, AnnouncementType type, AnnouncementCategory cat this.status = (status != null) ? status : this.status; } + public void updateImages(List images) { + if (images != null) { + this.announcementImages.clear(); + this.announcementImages.addAll(images); + } + } + public boolean isAvailable() { return this.status == AnnouncementStatus.OPENED; } diff --git a/src/main/java/com/sponus/sponusbe/domain/announcement/entity/AnnouncementAttachment.java b/src/main/java/com/sponus/sponusbe/domain/announcement/entity/AnnouncementImage.java similarity index 70% rename from src/main/java/com/sponus/sponusbe/domain/announcement/entity/AnnouncementAttachment.java rename to src/main/java/com/sponus/sponusbe/domain/announcement/entity/AnnouncementImage.java index 5f7c92a4..9f8b0453 100644 --- a/src/main/java/com/sponus/sponusbe/domain/announcement/entity/AnnouncementAttachment.java +++ b/src/main/java/com/sponus/sponusbe/domain/announcement/entity/AnnouncementImage.java @@ -22,21 +22,28 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @Entity -@Table(name = "announcement_attachment") -public class AnnouncementAttachment { - +@Table(name = "announcement_image") +public class AnnouncementImage { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "attachment_id") + @Column(name = "image_id") private Long id; - @Column(name = "file_name", nullable = false) + @Column(name = "image_name", nullable = false) private String name; - @Column(name = "file_url", nullable = false) + @Column(name = "image_url", nullable = false) private String url; @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "announcement_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) private Announcement announcement; + + public void setAnnouncement(Announcement announcement) { + if (this.announcement != null) { + this.announcement.getAnnouncementImages().remove(this); + } + this.announcement = announcement; + announcement.getAnnouncementImages().add(this); + } } diff --git a/src/main/java/com/sponus/sponusbe/domain/announcement/service/AnnouncementQueryService.java b/src/main/java/com/sponus/sponusbe/domain/announcement/service/AnnouncementQueryService.java index a6502437..8b10cd90 100644 --- a/src/main/java/com/sponus/sponusbe/domain/announcement/service/AnnouncementQueryService.java +++ b/src/main/java/com/sponus/sponusbe/domain/announcement/service/AnnouncementQueryService.java @@ -5,9 +5,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.sponus.sponusbe.domain.announcement.dto.request.AnnouncementCreateRequest; -import com.sponus.sponusbe.domain.announcement.dto.response.AnnouncementBriefResponse; -import com.sponus.sponusbe.domain.announcement.dto.response.AnnouncementResponse; +import com.sponus.sponusbe.domain.announcement.dto.response.AnnouncementSummaryResponse; import com.sponus.sponusbe.domain.announcement.repository.AnnouncementRepository; import lombok.RequiredArgsConstructor; @@ -21,13 +19,9 @@ public class AnnouncementQueryService { private final AnnouncementRepository announcementRepository; - public List searchAnnouncement(String keyword) { + public List searchAnnouncement(String keyword) { log.info("search announcement by keyword: {}", keyword); return announcementRepository.findByTitleContains(keyword).stream() - .map(AnnouncementResponse::from).toList(); - } - - public AnnouncementBriefResponse createAnnouncement(AnnouncementCreateRequest request) { - return null; + .map(AnnouncementSummaryResponse::from).toList(); } } diff --git a/src/main/java/com/sponus/sponusbe/domain/announcement/service/AnnouncementService.java b/src/main/java/com/sponus/sponusbe/domain/announcement/service/AnnouncementService.java index 4bb937ff..f06e6ca1 100644 --- a/src/main/java/com/sponus/sponusbe/domain/announcement/service/AnnouncementService.java +++ b/src/main/java/com/sponus/sponusbe/domain/announcement/service/AnnouncementService.java @@ -1,22 +1,25 @@ package com.sponus.sponusbe.domain.announcement.service; import java.util.List; -import java.util.stream.Collectors; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import com.sponus.sponusbe.domain.announcement.dto.request.AnnouncementCreateRequest; import com.sponus.sponusbe.domain.announcement.dto.request.AnnouncementUpdateRequest; import com.sponus.sponusbe.domain.announcement.dto.response.AnnouncementCreateResponse; -import com.sponus.sponusbe.domain.announcement.dto.response.AnnouncementResponse; +import com.sponus.sponusbe.domain.announcement.dto.response.AnnouncementDetailResponse; +import com.sponus.sponusbe.domain.announcement.dto.response.AnnouncementSummaryResponse; import com.sponus.sponusbe.domain.announcement.dto.response.AnnouncementUpdateResponse; import com.sponus.sponusbe.domain.announcement.entity.Announcement; +import com.sponus.sponusbe.domain.announcement.entity.AnnouncementImage; import com.sponus.sponusbe.domain.announcement.entity.enums.AnnouncementStatus; import com.sponus.sponusbe.domain.announcement.exception.AnnouncementErrorCode; import com.sponus.sponusbe.domain.announcement.exception.AnnouncementException; import com.sponus.sponusbe.domain.announcement.repository.AnnouncementRepository; import com.sponus.sponusbe.domain.organization.entity.Organization; +import com.sponus.sponusbe.domain.s3.S3Service; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -28,26 +31,30 @@ public class AnnouncementService { private final AnnouncementRepository announcementRepository; + private final S3Service s3Service; public AnnouncementCreateResponse createAnnouncement( Organization authOrganization, - AnnouncementCreateRequest request) { - final Announcement announcement = announcementRepository.save(request.toEntity(authOrganization)); - return AnnouncementCreateResponse.from(announcement); + AnnouncementCreateRequest request, + List images + ) { + final Announcement announcement = request.toEntity(authOrganization); + setAnnouncementImages(images, announcement); + return AnnouncementCreateResponse.from(announcementRepository.save(announcement)); } - public AnnouncementResponse getAnnouncement(Long announcementId) { + public AnnouncementDetailResponse getAnnouncement(Long announcementId) { Announcement announcement = announcementRepository.findById(announcementId) .orElseThrow(() -> new AnnouncementException(AnnouncementErrorCode.ANNOUNCEMENT_NOT_FOUND)); announcement.increaseViewCount(); - return AnnouncementResponse.from(announcement); + return AnnouncementDetailResponse.from(announcement); } - public List getListAnnouncement(AnnouncementStatus status) { + public List getListAnnouncement(AnnouncementStatus status) { List announcements = announcementRepository.findByStatus(status); return announcements.stream() - .map(AnnouncementResponse::from) - .collect(Collectors.toList()); + .map(AnnouncementSummaryResponse::from) + .toList(); } public void deleteAnnouncement(Organization organization, Long announcementId) { @@ -59,8 +66,12 @@ public void deleteAnnouncement(Organization organization, Long announcementId) { announcementRepository.delete(announcement); } - public AnnouncementUpdateResponse updateAnnouncement(Organization authOrganization, Long proposeId, - AnnouncementUpdateRequest request) { + public AnnouncementUpdateResponse updateAnnouncement( + Organization authOrganization, + Long proposeId, + AnnouncementUpdateRequest request, + List images + ) { final Announcement announcement = announcementRepository.findById(proposeId) .orElseThrow(() -> new AnnouncementException(AnnouncementErrorCode.ANNOUNCEMENT_NOT_FOUND)); @@ -69,7 +80,9 @@ public AnnouncementUpdateResponse updateAnnouncement(Organization authOrganizati if (!isOrganizationsAnnouncement(authOrganization.getId(), announcement)) throw new AnnouncementException(AnnouncementErrorCode.INVALID_ORGANIZATION); - announcement.update(request.title(), request.type(), request.category(), request.content(), request.status()); + announcement.updateInfo(request.title(), request.type(), request.category(), request.content(), + request.status()); + setAnnouncementImages(images, announcement); announcementRepository.save(announcement); return AnnouncementUpdateResponse.from(announcement); } @@ -78,4 +91,16 @@ private boolean isOrganizationsAnnouncement(Long organizationId, Announcement an return announcement.getWriter().getId().equals(organizationId); } + private void setAnnouncementImages(List images, Announcement announcement) { + announcement.getAnnouncementImages().clear(); + images.forEach(image -> { + final String url = s3Service.uploadFile(image); + AnnouncementImage announcementImage = AnnouncementImage.builder() + .name(image.getOriginalFilename()) + .url(url) + .build(); + announcementImage.setAnnouncement(announcement); + }); + } + } diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 97b3a25c..c945ce9a 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -38,7 +38,7 @@ VALUES ('무신사 스폰서십', 'SPONSORSHIP', 'MARKETING', '무신사 스폰서십을 진행할 대학교 학생회를 모집합니다.', - 'IN_PROGRESS', + 'OPENED', 0, 1); INSERT INTO tag (organization_id, tag_name)