diff --git a/src/main/java/com/listywave/admin/Admin.java b/src/main/java/com/listywave/admin/Admin.java new file mode 100644 index 00000000..a5059f0a --- /dev/null +++ b/src/main/java/com/listywave/admin/Admin.java @@ -0,0 +1,32 @@ +package com.listywave.admin; + +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = PROTECTED) +@AllArgsConstructor +public class Admin { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @Column(nullable = false, length = 40, unique = true) + private String ip; + + @Column(nullable = false, length = 50, unique = true) + private String account; + + @Column(nullable = false, length = 50) + private String password; +} diff --git a/src/main/java/com/listywave/admin/AdminRepository.java b/src/main/java/com/listywave/admin/AdminRepository.java new file mode 100644 index 00000000..121781b8 --- /dev/null +++ b/src/main/java/com/listywave/admin/AdminRepository.java @@ -0,0 +1,13 @@ +package com.listywave.admin; + +import static com.listywave.common.exception.ErrorCode.RESOURCE_NOT_FOUND; + +import com.listywave.common.exception.CustomException; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AdminRepository extends JpaRepository { + + default Admin getById(Long id) { + return findById(id).orElseThrow(() -> new CustomException(RESOURCE_NOT_FOUND)); + } +} diff --git a/src/main/java/com/listywave/admin/AdminService.java b/src/main/java/com/listywave/admin/AdminService.java new file mode 100644 index 00000000..1024ef80 --- /dev/null +++ b/src/main/java/com/listywave/admin/AdminService.java @@ -0,0 +1,17 @@ +package com.listywave.admin; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class AdminService { + + private final AdminRepository adminRepository; + + public void validateExist(Long adminId) { + adminRepository.getById(adminId); + } +} diff --git a/src/main/java/com/listywave/common/exception/ErrorCode.java b/src/main/java/com/listywave/common/exception/ErrorCode.java index 61cb5dd0..4e429d42 100644 --- a/src/main/java/com/listywave/common/exception/ErrorCode.java +++ b/src/main/java/com/listywave/common/exception/ErrorCode.java @@ -44,6 +44,8 @@ public enum ErrorCode { DUPLICATE_NICKNAME_EXCEPTION(BAD_REQUEST, "중복된 닉네임입니다."), DUPLICATE_COLLABORATOR_EXCEPTION(BAD_REQUEST, "이미 동일한 콜라보레이터가 존재합니다"), DUPLICATE_FOLDER_NAME_EXCEPTION(BAD_REQUEST, "중복된 폴더명입니다."), + NULL_OR_BLANK_EXCEPTION(BAD_REQUEST, "값이 null이거나 공백일 수 없습니다."), + NOT_EXIST_CODE(BAD_REQUEST, "존재하지 않는 코드입니다."), // S3 S3_DELETE_OBJECTS_EXCEPTION(INTERNAL_SERVER_ERROR, "S3의 이미지를 삭제 요청하는 과정에서 에러가 발생했습니다."), diff --git a/src/main/java/com/listywave/notice/application/converter/ContentTypeConverter.java b/src/main/java/com/listywave/notice/application/converter/ContentTypeConverter.java new file mode 100644 index 00000000..e31533f9 --- /dev/null +++ b/src/main/java/com/listywave/notice/application/converter/ContentTypeConverter.java @@ -0,0 +1,20 @@ +package com.listywave.notice.application.converter; + + +import com.listywave.notice.application.domain.ContentType; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class ContentTypeConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(ContentType contentType) { + return contentType.name().toLowerCase(); + } + + @Override + public ContentType convertToEntityAttribute(String s) { + return ContentType.valueOf(s.toUpperCase()); + } +} diff --git a/src/main/java/com/listywave/notice/application/converter/NoticeTypeConverter.java b/src/main/java/com/listywave/notice/application/converter/NoticeTypeConverter.java new file mode 100644 index 00000000..fb7c1ca7 --- /dev/null +++ b/src/main/java/com/listywave/notice/application/converter/NoticeTypeConverter.java @@ -0,0 +1,19 @@ +package com.listywave.notice.application.converter; + +import com.listywave.notice.application.domain.NoticeType; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class NoticeTypeConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(NoticeType noticeType) { + return String.valueOf(noticeType.getCode()); + } + + @Override + public NoticeType convertToEntityAttribute(String s) { + return NoticeType.codeOf(Integer.parseInt(s)); + } +} diff --git a/src/main/java/com/listywave/notice/application/domain/ContentType.java b/src/main/java/com/listywave/notice/application/domain/ContentType.java new file mode 100644 index 00000000..4508dd76 --- /dev/null +++ b/src/main/java/com/listywave/notice/application/domain/ContentType.java @@ -0,0 +1,17 @@ +package com.listywave.notice.application.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ContentType { + + SUBTITLE, + BODY, + IMAGE, + BUTTON, + LINE, + NOTE, + ; +} diff --git a/src/main/java/com/listywave/notice/application/domain/Notice.java b/src/main/java/com/listywave/notice/application/domain/Notice.java new file mode 100644 index 00000000..56bc399f --- /dev/null +++ b/src/main/java/com/listywave/notice/application/domain/Notice.java @@ -0,0 +1,70 @@ +package com.listywave.notice.application.domain; + +import static jakarta.persistence.CascadeType.ALL; +import static jakarta.persistence.FetchType.LAZY; +import static lombok.AccessLevel.PROTECTED; + +import com.listywave.common.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.OneToMany; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = PROTECTED) +public class Notice extends BaseEntity { + + @Column(name = "code", nullable = false) + private NoticeType type; + + @Embedded + private NoticeTitle title; + + @Embedded + private NoticeDescription description; + + private boolean isExposed; + + private boolean didSendAlarm; + + @OneToMany(mappedBy = "notice", fetch = LAZY, cascade = ALL, orphanRemoval = true) + private final List contents = new ArrayList<>(); + + public Notice(NoticeType type, NoticeTitle title, NoticeDescription description) { + this.type = type; + this.title = title; + this.description = description; + } + + public void addContents(List contents) { + this.contents.addAll(contents); + } + + public String getFirstImageUrl() { + Optional result = contents.stream() + .sorted(Comparator.comparingInt(NoticeContent::getOrder)) + .map(NoticeContent::getImageUrl) + .findFirst(); + return result.orElse(null); + } + + public void update(NoticeType type, NoticeTitle title, NoticeDescription description, List contents) { + this.type = type; + this.title = title; + this.description = description; + + this.contents.clear(); + this.contents.addAll(contents); + } + + public void changeExposure() { + this.isExposed = !this.isExposed; + } +} diff --git a/src/main/java/com/listywave/notice/application/domain/NoticeContent.java b/src/main/java/com/listywave/notice/application/domain/NoticeContent.java new file mode 100644 index 00000000..9c90a959 --- /dev/null +++ b/src/main/java/com/listywave/notice/application/domain/NoticeContent.java @@ -0,0 +1,61 @@ +package com.listywave.notice.application.domain; + +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PRIVATE; +import static lombok.AccessLevel.PROTECTED; + +import jakarta.annotation.Nullable; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@AllArgsConstructor(access = PRIVATE) +@NoArgsConstructor(access = PROTECTED) +public class NoticeContent { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "notice_id", nullable = false) + private Notice notice; + + @Column(name = "orders", nullable = false) + private int order; + + @Column(nullable = false, length = 30) + private ContentType type; + + @Column(nullable = true, length = 1000) + private String description; + + @Column(nullable = true, length = 2048) + private String imageUrl; + + @Column(nullable = true, length = 50) + private String buttonName; + + @Column(nullable = true, length = 2048) + private String buttonLink; + + public static NoticeContent create( + Notice notice, + int order, + ContentType type, + @Nullable String description, + @Nullable String imageUrl, + @Nullable String buttonName, + @Nullable String buttonLink + ) { + return new NoticeContent(null, notice, order, type, description, imageUrl, buttonName, buttonLink); + } +} diff --git a/src/main/java/com/listywave/notice/application/domain/NoticeDescription.java b/src/main/java/com/listywave/notice/application/domain/NoticeDescription.java new file mode 100644 index 00000000..a1ab884b --- /dev/null +++ b/src/main/java/com/listywave/notice/application/domain/NoticeDescription.java @@ -0,0 +1,38 @@ +package com.listywave.notice.application.domain; + +import static com.listywave.common.exception.ErrorCode.LENGTH_EXCEEDED_EXCEPTION; +import static com.listywave.common.exception.ErrorCode.NULL_OR_BLANK_EXCEPTION; +import static lombok.AccessLevel.PROTECTED; + +import com.listywave.common.exception.CustomException; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Embeddable +@EqualsAndHashCode +@NoArgsConstructor(access = PROTECTED, force = true) +public class NoticeDescription { + + private static final int MAX_LENGTH = 30; + + @Column(name = "description", nullable = false, length = MAX_LENGTH) + private final String value; + + public NoticeDescription(String value) { + validate(value); + this.value = value; + } + + private void validate(String value) { + if (value == null || value.isBlank()) { + throw new CustomException(NULL_OR_BLANK_EXCEPTION, NULL_OR_BLANK_EXCEPTION.getDetail() + " 입력값: " + value); + } + if (value.length() > MAX_LENGTH) { + throw new CustomException(LENGTH_EXCEEDED_EXCEPTION, LENGTH_EXCEEDED_EXCEPTION.getDetail() + " 입력값의 길이: " + value.length()); + } + } +} diff --git a/src/main/java/com/listywave/notice/application/domain/NoticeTitle.java b/src/main/java/com/listywave/notice/application/domain/NoticeTitle.java new file mode 100644 index 00000000..585f8bcb --- /dev/null +++ b/src/main/java/com/listywave/notice/application/domain/NoticeTitle.java @@ -0,0 +1,38 @@ +package com.listywave.notice.application.domain; + +import static com.listywave.common.exception.ErrorCode.LENGTH_EXCEEDED_EXCEPTION; +import static com.listywave.common.exception.ErrorCode.NULL_OR_BLANK_EXCEPTION; +import static lombok.AccessLevel.PROTECTED; + +import com.listywave.common.exception.CustomException; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Embeddable +@EqualsAndHashCode +@NoArgsConstructor(access = PROTECTED, force = true) +public class NoticeTitle { + + private static final int MAX_LENGTH = 30; + + @Column(name = "title", nullable = false, length = MAX_LENGTH) + private final String value; + + public NoticeTitle(String value) { + validate(value); + this.value = value; + } + + private void validate(String value) { + if (value == null || value.isBlank()) { + throw new CustomException(NULL_OR_BLANK_EXCEPTION, NULL_OR_BLANK_EXCEPTION.getDetail() + " 입력값: " + value); + } + if (value.length() > MAX_LENGTH) { + throw new CustomException(LENGTH_EXCEEDED_EXCEPTION, LENGTH_EXCEEDED_EXCEPTION.getDetail() + " 입력값의 길이: " + value.length()); + } + } +} diff --git a/src/main/java/com/listywave/notice/application/domain/NoticeType.java b/src/main/java/com/listywave/notice/application/domain/NoticeType.java new file mode 100644 index 00000000..1a8d2353 --- /dev/null +++ b/src/main/java/com/listywave/notice/application/domain/NoticeType.java @@ -0,0 +1,28 @@ +package com.listywave.notice.application.domain; + +import static com.listywave.common.exception.ErrorCode.NOT_EXIST_CODE; + +import com.listywave.common.exception.CustomException; +import java.util.Arrays; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NoticeType { + + NEWS(1, "소식"), + EVENT(2, "이벤트"), + TIP(3, "팁"), + ; + + private final int code; + private final String viewName; + + public static NoticeType codeOf(int code) { + return Arrays.stream(NoticeType.values()) + .filter(noticeType -> noticeType.getCode() == code) + .findFirst() + .orElseThrow(() -> new CustomException(NOT_EXIST_CODE, NOT_EXIST_CODE.getDetail() + " 입력값: " + code)); + } +} diff --git a/src/main/java/com/listywave/notice/application/service/NoticeService.java b/src/main/java/com/listywave/notice/application/service/NoticeService.java new file mode 100644 index 00000000..d67fd331 --- /dev/null +++ b/src/main/java/com/listywave/notice/application/service/NoticeService.java @@ -0,0 +1,73 @@ +package com.listywave.notice.application.service; + +import com.listywave.notice.application.domain.Notice; +import com.listywave.notice.application.domain.NoticeContent; +import com.listywave.notice.application.domain.NoticeDescription; +import com.listywave.notice.application.domain.NoticeTitle; +import com.listywave.notice.application.domain.NoticeType; +import com.listywave.notice.application.service.dto.NoticeCreateRequest; +import com.listywave.notice.application.service.dto.NoticeFindAllResponseToAdmin; +import com.listywave.notice.application.service.dto.NoticeFindAllResponseToUser; +import com.listywave.notice.application.service.dto.NoticeFindResponse; +import com.listywave.notice.application.service.dto.NoticeUpdateRequest; +import com.listywave.notice.repository.NoticeRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class NoticeService { + + private final NoticeRepository noticeRepository; + + public Long create(NoticeCreateRequest request) { + Notice notice = request.toNotice(); + List noticeContents = request.toNoticeContents(notice); + notice.addContents(noticeContents); + return noticeRepository.save(notice).getId(); + } + + @Transactional(readOnly = true) + public List findAllToAdmin() { + List notices = noticeRepository.findAll(); + return NoticeFindAllResponseToAdmin.toList(notices); + } + + @Transactional(readOnly = true) + public List findAllToUser() { + List notices = noticeRepository.findAll(); + return NoticeFindAllResponseToUser.toList(notices); + } + + @Transactional(readOnly = true) + public NoticeFindResponse findOneSpecific(Long id) { + Notice result = noticeRepository.findByIdWithFetch(id); + Notice prevNotice = noticeRepository.findByIdWithFetch(id - 1); + Notice nextNotice = noticeRepository.findByIdWithFetch(id + 1); + return NoticeFindResponse.of(result, prevNotice, nextNotice); + } + + public void update(NoticeUpdateRequest request, Long noticeId) { + Notice notice = noticeRepository.findByIdWithFetch(noticeId); + List newNoticeContents = request.toNoticeContents(notice); + + notice.update( + NoticeType.codeOf(request.categoryCode()), + new NoticeTitle(request.title()), + new NoticeDescription(request.description()), + newNoticeContents + ); + } + + public void updateExposure(Long id) { + Notice notice = noticeRepository.findByIdWithFetch(id); + notice.changeExposure(); + } + + public void delete(Long id) { + noticeRepository.deleteById(id); + } +} diff --git a/src/main/java/com/listywave/notice/application/service/dto/NoticeCreateRequest.java b/src/main/java/com/listywave/notice/application/service/dto/NoticeCreateRequest.java new file mode 100644 index 00000000..bdfd8518 --- /dev/null +++ b/src/main/java/com/listywave/notice/application/service/dto/NoticeCreateRequest.java @@ -0,0 +1,44 @@ +package com.listywave.notice.application.service.dto; + +import com.listywave.notice.application.domain.ContentType; +import com.listywave.notice.application.domain.Notice; +import com.listywave.notice.application.domain.NoticeContent; +import com.listywave.notice.application.domain.NoticeDescription; +import com.listywave.notice.application.domain.NoticeTitle; +import com.listywave.notice.application.domain.NoticeType; +import jakarta.annotation.Nullable; +import java.util.List; + +public record NoticeCreateRequest( + int categoryCode, + String title, + String description, + List contents +) { + + public record ContentDto( + int order, + String type, + @Nullable String description, + @Nullable String imageUrl, + @Nullable String buttonName, + @Nullable String buttonLink + ) { + } + + public Notice toNotice() { + return new Notice(NoticeType.codeOf(categoryCode), new NoticeTitle(title), new NoticeDescription(description)); + } + + public List toNoticeContents(Notice notice) { + return contents.stream() + .map(it -> NoticeContent.create(notice, + it.order, + ContentType.valueOf(it.type.toUpperCase()), + it.description, + it.imageUrl, + it.buttonName, + it.buttonLink) + ).toList(); + } +} diff --git a/src/main/java/com/listywave/notice/application/service/dto/NoticeFindAllResponseToAdmin.java b/src/main/java/com/listywave/notice/application/service/dto/NoticeFindAllResponseToAdmin.java new file mode 100644 index 00000000..8fa477ef --- /dev/null +++ b/src/main/java/com/listywave/notice/application/service/dto/NoticeFindAllResponseToAdmin.java @@ -0,0 +1,35 @@ +package com.listywave.notice.application.service.dto; + +import com.listywave.notice.application.domain.Notice; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Builder; + +@Builder +public record NoticeFindAllResponseToAdmin( + Long id, + LocalDateTime createdDate, + String title, + String category, + String description, + boolean isExposed, + boolean didSendAlarm +) { + + public static List toList(List notices) { + return notices.stream() + .map(NoticeFindAllResponseToAdmin::of) + .toList(); + } + + public static NoticeFindAllResponseToAdmin of(Notice notice) { + return NoticeFindAllResponseToAdmin.builder() + .id(notice.getId()) + .createdDate(notice.getCreatedDate()) + .title(notice.getTitle().getValue()) + .category(notice.getType().getViewName()) + .description(notice.getDescription().getValue()) + .isExposed(notice.isDidSendAlarm()) + .build(); + } +} diff --git a/src/main/java/com/listywave/notice/application/service/dto/NoticeFindAllResponseToUser.java b/src/main/java/com/listywave/notice/application/service/dto/NoticeFindAllResponseToUser.java new file mode 100644 index 00000000..6e03c73b --- /dev/null +++ b/src/main/java/com/listywave/notice/application/service/dto/NoticeFindAllResponseToUser.java @@ -0,0 +1,35 @@ +package com.listywave.notice.application.service.dto; + +import com.listywave.notice.application.domain.Notice; +import jakarta.annotation.Nullable; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Builder; + +@Builder +public record NoticeFindAllResponseToUser( + Long id, + LocalDateTime createdDate, + String title, + @Nullable String itemImageUrl, + String category, + String description +) { + + public static List toList(List notices) { + return notices.stream() + .map(NoticeFindAllResponseToUser::of) + .toList(); + } + + public static NoticeFindAllResponseToUser of(Notice notice) { + return NoticeFindAllResponseToUser.builder() + .id(notice.getId()) + .createdDate(notice.getCreatedDate()) + .title(notice.getTitle().getValue()) + .itemImageUrl(notice.getFirstImageUrl()) + .category(notice.getType().getViewName()) + .description(notice.getDescription().getValue()) + .build(); + } +} diff --git a/src/main/java/com/listywave/notice/application/service/dto/NoticeFindResponse.java b/src/main/java/com/listywave/notice/application/service/dto/NoticeFindResponse.java new file mode 100644 index 00000000..c96689d6 --- /dev/null +++ b/src/main/java/com/listywave/notice/application/service/dto/NoticeFindResponse.java @@ -0,0 +1,74 @@ +package com.listywave.notice.application.service.dto; + +import com.listywave.notice.application.domain.Notice; +import com.listywave.notice.application.domain.NoticeContent; +import jakarta.annotation.Nullable; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Builder; + +@Builder +public record NoticeFindResponse( + Long id, + String category, + String title, + String description, + List contents, + LocalDateTime createdDate, + BesideNoticeDto prevNotice, + BesideNoticeDto nextNotice +) { + + public static NoticeFindResponse of(Notice notice, @Nullable Notice prevNotice, @Nullable Notice nextNotice) { + return NoticeFindResponse.builder() + .id(notice.getId()) + .category(notice.getType().getViewName()) + .title(notice.getTitle().getValue()) + .description(notice.getDescription().getValue()) + .contents(ContentDto.toList(notice.getContents())) + .createdDate(notice.getCreatedDate()) + .prevNotice(BesideNoticeDto.of(prevNotice)) + .nextNotice(BesideNoticeDto.of(nextNotice)) + .build(); + } + + @Builder + public record ContentDto( + String type, + String description, + @Nullable String imageUrl, + @Nullable String buttonName, + @Nullable String buttonLink + ) { + + public static List toList(List contents) { + return contents.stream() + .map(ContentDto::of) + .toList(); + } + + public static ContentDto of(NoticeContent content) { + return ContentDto.builder() + .type(content.getType().name().toLowerCase()) + .description(content.getDescription()) + .imageUrl(content.getImageUrl()) + .buttonName(content.getButtonName()) + .buttonLink(content.getButtonLink()) + .build(); + } + } + + public record BesideNoticeDto( + Long id, + String title, + String description + ) { + + public static BesideNoticeDto of(Notice notice) { + if (notice == null) { + return null; + } + return new BesideNoticeDto(notice.getId(), notice.getTitle().getValue(), notice.getDescription().getValue()); + } + } +} diff --git a/src/main/java/com/listywave/notice/application/service/dto/NoticeUpdateRequest.java b/src/main/java/com/listywave/notice/application/service/dto/NoticeUpdateRequest.java new file mode 100644 index 00000000..359348d9 --- /dev/null +++ b/src/main/java/com/listywave/notice/application/service/dto/NoticeUpdateRequest.java @@ -0,0 +1,39 @@ +package com.listywave.notice.application.service.dto; + +import com.listywave.notice.application.domain.ContentType; +import com.listywave.notice.application.domain.Notice; +import com.listywave.notice.application.domain.NoticeContent; +import java.util.List; + +public record NoticeUpdateRequest( + int categoryCode, + String title, + String description, + List contents +) { + + + public List toNoticeContents(Notice notice) { + return contents.stream() + .map(it -> NoticeContent.create( + notice, + it.order, + ContentType.valueOf(it.type.toUpperCase()), + it.description, + it.imageUrl, + it.buttonName, + it.buttonLink + ) + ).toList(); + } + + public record ContentDto( + int order, + String type, + String description, + String imageUrl, + String buttonName, + String buttonLink + ) { + } +} diff --git a/src/main/java/com/listywave/notice/presentation/NoticeController.java b/src/main/java/com/listywave/notice/presentation/NoticeController.java new file mode 100644 index 00000000..f7fa8ff1 --- /dev/null +++ b/src/main/java/com/listywave/notice/presentation/NoticeController.java @@ -0,0 +1,79 @@ +package com.listywave.notice.presentation; + +import com.listywave.notice.application.domain.NoticeType; +import com.listywave.notice.application.service.NoticeService; +import com.listywave.notice.application.service.dto.NoticeCreateRequest; +import com.listywave.notice.application.service.dto.NoticeFindAllResponseToAdmin; +import com.listywave.notice.application.service.dto.NoticeFindAllResponseToUser; +import com.listywave.notice.application.service.dto.NoticeFindResponse; +import com.listywave.notice.application.service.dto.NoticeUpdateRequest; +import com.listywave.notice.presentation.dto.NoticeCategoryFindResponse; +import java.net.URI; +import java.util.List; +import lombok.RequiredArgsConstructor; +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.PatchMapping; +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.RestController; + +@RestController +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + @GetMapping("/admin/notices/categories") + ResponseEntity> findNoticeCategories() { + List result = NoticeCategoryFindResponse.toList(NoticeType.values()); + return ResponseEntity.ok(result); + } + + @PostMapping("/admin/notices") + ResponseEntity create( + @RequestBody NoticeCreateRequest request + ) { + Long id = noticeService.create(request); + return ResponseEntity.created(URI.create("/admin/notices/" + id)).build(); + } + + @GetMapping("/admin/notices") + ResponseEntity> findAllToAdmin() { + List result = noticeService.findAllToAdmin(); + return ResponseEntity.ok(result); + } + + @GetMapping("/notices") + ResponseEntity> findAllToUser() { + List result = noticeService.findAllToUser(); + return ResponseEntity.ok(result); + } + + @GetMapping("/notices/{noticeId}") + ResponseEntity findOneSpecific(@PathVariable Long noticeId) { + NoticeFindResponse result = noticeService.findOneSpecific(noticeId); + return ResponseEntity.ok(result); + } + + @PutMapping("/admin/notices/{noticeId}") + ResponseEntity update(@RequestBody NoticeUpdateRequest request, @PathVariable Long noticeId) { + noticeService.update(request, noticeId); + return ResponseEntity.noContent().build(); + } + + @PatchMapping("/admin/notices/{noticeId}") + ResponseEntity updateExposure(@PathVariable Long noticeId) { + noticeService.updateExposure(noticeId); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/admin/notices/{noticeId}") + ResponseEntity delete(@PathVariable Long noticeId) { + noticeService.delete(noticeId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/listywave/notice/presentation/dto/NoticeCategoryFindResponse.java b/src/main/java/com/listywave/notice/presentation/dto/NoticeCategoryFindResponse.java new file mode 100644 index 00000000..0d8d2a84 --- /dev/null +++ b/src/main/java/com/listywave/notice/presentation/dto/NoticeCategoryFindResponse.java @@ -0,0 +1,17 @@ +package com.listywave.notice.presentation.dto; + +import com.listywave.notice.application.domain.NoticeType; +import java.util.Arrays; +import java.util.List; + +public record NoticeCategoryFindResponse( + int code, + String viewName +) { + + public static List toList(NoticeType[] types) { + return Arrays.stream(types) + .map(type -> new NoticeCategoryFindResponse(type.getCode(), type.getViewName())) + .toList(); + } +} diff --git a/src/main/java/com/listywave/notice/repository/NoticeRepository.java b/src/main/java/com/listywave/notice/repository/NoticeRepository.java new file mode 100644 index 00000000..2c609e87 --- /dev/null +++ b/src/main/java/com/listywave/notice/repository/NoticeRepository.java @@ -0,0 +1,23 @@ +package com.listywave.notice.repository; + +import static com.listywave.common.exception.ErrorCode.RESOURCE_NOT_FOUND; + +import com.listywave.common.exception.CustomException; +import com.listywave.notice.application.domain.Notice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface NoticeRepository extends JpaRepository { + + default Notice getById(Long id) { + return findById(id).orElseThrow(() -> new CustomException(RESOURCE_NOT_FOUND, "존재하지 않는 공지입니다.")); + } + + @Query(""" + select n + from Notice n + join fetch NoticeContent nc on nc.notice = n + where n.id = :noticeId + """) + Notice findByIdWithFetch(Long noticeId); +} diff --git a/src/test/java/com/listywave/common/IntegrationTest.java b/src/test/java/com/listywave/common/IntegrationTest.java index f2f5076f..56ce1507 100644 --- a/src/test/java/com/listywave/common/IntegrationTest.java +++ b/src/test/java/com/listywave/common/IntegrationTest.java @@ -6,6 +6,7 @@ import static com.listywave.user.fixture.UserFixture.유진; import static com.listywave.user.fixture.UserFixture.정수; +import com.listywave.admin.AdminRepository; import com.listywave.alarm.application.service.AlarmService; import com.listywave.alarm.repository.AlarmRepository; import com.listywave.auth.application.domain.kakao.KakaoOauthClient; @@ -13,6 +14,7 @@ import com.listywave.collection.application.service.CollectionService; import com.listywave.collection.application.service.FolderService; import com.listywave.collection.repository.CollectionRepository; +import com.listywave.history.repository.HistoryRepository; import com.listywave.list.application.domain.list.ListEntity; import com.listywave.list.application.service.CommentService; import com.listywave.list.application.service.ReplyService; @@ -22,6 +24,10 @@ import com.listywave.list.repository.list.ListRepository; import com.listywave.list.repository.reply.ReplyRepository; import com.listywave.mention.MentionRepository; +import com.listywave.notice.application.service.NoticeService; +import com.listywave.notice.repository.NoticeRepository; +import com.listywave.reaction.repository.ReactionStatsRepository; +import com.listywave.reaction.repository.UserReactionRepository; import com.listywave.topic.application.service.TopicService; import com.listywave.topic.repository.TopicRepository; import com.listywave.user.application.domain.User; @@ -81,12 +87,29 @@ public abstract class IntegrationTest { protected TopicService topicService; @Autowired protected TopicRepository topicRepository; + @Autowired + protected HistoryRepository historyRepository; + @Autowired + protected UserReactionRepository userReactionRepository; + @Autowired + protected ReactionStatsRepository reactionStatsRepository; + @Autowired + protected AdminRepository adminRepository; + @Autowired + protected NoticeService noticeService; + @Autowired + protected NoticeRepository noticeRepository; protected User dh, js, ej, sy; protected ListEntity list; @BeforeEach void setUp() { + noticeRepository.deleteAll(); + adminRepository.deleteAllInBatch(); + historyRepository.deleteAll(); + userReactionRepository.deleteAllInBatch(); + reactionStatsRepository.deleteAllInBatch(); topicRepository.deleteAllInBatch(); followRepository.deleteAllInBatch(); collectionRepository.deleteAllInBatch(); diff --git a/src/test/java/com/listywave/notice/application/domain/NoticeDescriptionTest.java b/src/test/java/com/listywave/notice/application/domain/NoticeDescriptionTest.java new file mode 100644 index 00000000..e77f9359 --- /dev/null +++ b/src/test/java/com/listywave/notice/application/domain/NoticeDescriptionTest.java @@ -0,0 +1,64 @@ +package com.listywave.notice.application.domain; + +import static com.listywave.common.exception.ErrorCode.LENGTH_EXCEEDED_EXCEPTION; +import static com.listywave.common.exception.ErrorCode.NULL_OR_BLANK_EXCEPTION; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.listywave.common.exception.CustomException; +import com.listywave.common.exception.ErrorCode; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +class NoticeDescriptionTest { + + @Test + void 공지_제목의_최대_길이를_넘으면_예외를_발생한다() { + // given + String value = IntStream.range(0, 30) + .mapToObj(String::valueOf) + .collect(Collectors.joining("")); + + // when + ErrorCode result = assertThrows(CustomException.class, () -> new NoticeDescription(value)) + .getErrorCode(); + + // then + assertThat(result).isEqualTo(LENGTH_EXCEEDED_EXCEPTION); + } + + @ParameterizedTest + @NullAndEmptySource + void 값이_null이거나_빈_값이면_예외를_발생한다(String value) { + // when + ErrorCode result = assertThrows(CustomException.class, () -> new NoticeDescription(value)) + .getErrorCode(); + + // then + assertThat(result).isEqualTo(NULL_OR_BLANK_EXCEPTION); + } + + @Test + void 공지_제목을_정상적으로_생성한다() { + // given + String value = "12345678911234567891123456789"; + + // expect + assertThatNoException().isThrownBy(() -> new NoticeDescription(value)); + } + + @Test + void 값이_같으면_같은_객체다() { + // given + NoticeDescription title1 = new NoticeDescription("123456789"); + NoticeDescription title2 = new NoticeDescription("123456789"); + + // expect + assertThat(title1).isEqualTo(title2); + assertThat(title1).hasSameHashCodeAs(title2); + } +} diff --git a/src/test/java/com/listywave/notice/application/domain/NoticeTitleTest.java b/src/test/java/com/listywave/notice/application/domain/NoticeTitleTest.java new file mode 100644 index 00000000..cc5ab6e8 --- /dev/null +++ b/src/test/java/com/listywave/notice/application/domain/NoticeTitleTest.java @@ -0,0 +1,64 @@ +package com.listywave.notice.application.domain; + +import static com.listywave.common.exception.ErrorCode.LENGTH_EXCEEDED_EXCEPTION; +import static com.listywave.common.exception.ErrorCode.NULL_OR_BLANK_EXCEPTION; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.listywave.common.exception.CustomException; +import com.listywave.common.exception.ErrorCode; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +class NoticeTitleTest { + + @Test + void 공지_제목의_최대_길이를_넘으면_예외를_발생한다() { + // given + String value = IntStream.range(0, 30) + .mapToObj(String::valueOf) + .collect(Collectors.joining("")); + + // when + ErrorCode result = assertThrows(CustomException.class, () -> new NoticeTitle(value)) + .getErrorCode(); + + // then + assertThat(result).isEqualTo(LENGTH_EXCEEDED_EXCEPTION); + } + + @ParameterizedTest + @NullAndEmptySource + void 값이_null이거나_빈_값이면_예외를_발생한다(String value) { + // when + ErrorCode result = assertThrows(CustomException.class, () -> new NoticeTitle(value)) + .getErrorCode(); + + // then + assertThat(result).isEqualTo(NULL_OR_BLANK_EXCEPTION); + } + + @Test + void 공지_제목을_정상적으로_생성한다() { + // given + String value = "12345678911234567891123456789"; + + // expect + assertThatNoException().isThrownBy(() -> new NoticeTitle(value)); + } + + @Test + void 값이_같으면_같은_객체다() { + // given + NoticeTitle title1 = new NoticeTitle("123456789"); + NoticeTitle title2 = new NoticeTitle("123456789"); + + // expect + assertThat(title1).isEqualTo(title2); + assertThat(title1).hasSameHashCodeAs(title2); + } +} diff --git a/src/test/java/com/listywave/notice/application/domain/NoticeTypeTest.java b/src/test/java/com/listywave/notice/application/domain/NoticeTypeTest.java new file mode 100644 index 00000000..3e2e84ca --- /dev/null +++ b/src/test/java/com/listywave/notice/application/domain/NoticeTypeTest.java @@ -0,0 +1,38 @@ +package com.listywave.notice.application.domain; + +import static com.listywave.common.exception.ErrorCode.NOT_EXIST_CODE; +import static com.listywave.notice.application.domain.NoticeType.NEWS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.listywave.common.exception.CustomException; +import com.listywave.common.exception.ErrorCode; +import org.junit.jupiter.api.Test; + +class NoticeTypeTest { + + @Test + void 존재하지_않는_코드_번호로_생성하면_예외가_발생한다() { + // given + int code = 0; + + // when + ErrorCode result = assertThrows(CustomException.class, () -> NoticeType.codeOf(code)) + .getErrorCode(); + + // then + assertThat(result).isEqualTo(NOT_EXIST_CODE); + } + + @Test + void 코드_번호로_생성한다() { + // given + int code = 1; + + // when + NoticeType result = NoticeType.codeOf(code); + + // then + assertThat(result).isEqualTo(NEWS); + } +} diff --git a/src/test/java/com/listywave/notice/application/service/NoticeServiceTest.java b/src/test/java/com/listywave/notice/application/service/NoticeServiceTest.java new file mode 100644 index 00000000..0caa0ff1 --- /dev/null +++ b/src/test/java/com/listywave/notice/application/service/NoticeServiceTest.java @@ -0,0 +1,168 @@ +package com.listywave.notice.application.service; + +import static com.listywave.notice.application.domain.ContentType.BODY; +import static com.listywave.notice.application.domain.ContentType.BUTTON; +import static com.listywave.notice.application.domain.ContentType.IMAGE; +import static com.listywave.notice.application.domain.ContentType.NOTE; +import static com.listywave.notice.application.domain.ContentType.SUBTITLE; +import static com.listywave.notice.application.domain.NoticeType.EVENT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.listywave.common.IntegrationTest; +import com.listywave.notice.application.domain.Notice; +import com.listywave.notice.application.service.dto.NoticeCreateRequest; +import com.listywave.notice.application.service.dto.NoticeCreateRequest.ContentDto; +import com.listywave.notice.application.service.dto.NoticeFindResponse; +import com.listywave.notice.application.service.dto.NoticeUpdateRequest; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public class NoticeServiceTest extends IntegrationTest { + + @Test + void 공지를_생성_후_상세_조회한다() { + // given + NoticeCreateRequest request1 = createNoticeCreateRequest(1); + NoticeCreateRequest request2 = createNoticeCreateRequest(2); + NoticeCreateRequest request3 = createNoticeCreateRequest(3); + + // when + Long id1 = noticeService.create(request1); + Long id2 = noticeService.create(request2); + Long id3 = noticeService.create(request3); + + // then + NoticeFindResponse result = noticeService.findOneSpecific(id2); + assertAll( + () -> assertThat(result.id()).isEqualTo(id2), + () -> assertThat(result.category()).isEqualTo(EVENT.getViewName()), + () -> assertThat(result.title()).isEqualTo(2 + "번 째 공지입니다"), + () -> assertThat(result.description()).isEqualTo(2 + "번 째 공지에요"), + () -> { + List contents = result.contents(); + assertThat(contents).hasSize(5); + assertThat(contents.get(0).type()).isEqualTo(SUBTITLE.name().toLowerCase()); + assertThat(contents.get(1).type()).isEqualTo(BODY.name().toLowerCase()); + assertThat(contents.get(2).type()).isEqualTo(IMAGE.name().toLowerCase()); + assertThat(contents.get(3).type()).isEqualTo(BUTTON.name().toLowerCase()); + assertThat(contents.get(4).type()).isEqualTo(NOTE.name().toLowerCase()); + }, + () -> assertThat(result.prevNotice().id()).isEqualTo(id1), + () -> assertThat(result.nextNotice().id()).isEqualTo(id3) + ); + } + + private NoticeCreateRequest createNoticeCreateRequest(int th) { + return new NoticeCreateRequest( + th, + th + "번 째 공지입니다", + th + "번 째 공지에요", + List.of( + new ContentDto(1, "subtitle", "소제목입니다", null, null, null), + new ContentDto(2, "body", "본문입니다", null, null, null), + new ContentDto(3, "image", "이미지입니다", "https://image.com", null, null), + new ContentDto(4, "button", "버튼입니다", null, "버튼 이름", "https://buttonLink.com"), + new ContentDto(5, "note", "유의사항입니다", null, null, null) + ) + ); + } + + @Test + void 이전_혹은_다음_공지가_없으면_null이_반환된다() { + // given + NoticeCreateRequest request = createNoticeCreateRequest(1); + + // when + Long id = noticeService.create(request); + + // then + NoticeFindResponse result = noticeService.findOneSpecific(id); + assertAll( + () -> assertThat(result.prevNotice()).isNull(), + () -> assertThat(result.nextNotice()).isNull() + ); + } + + @Nested + class 공지_수정 { + + @Test + void 공지를_수정한다() { + // given + NoticeCreateRequest createRequest = createNoticeCreateRequest(1); + Long noticeId = noticeService.create(createRequest); + + // when + NoticeUpdateRequest updateRequest = new NoticeUpdateRequest( + 2, + "수정했습니다", + "수정했어요", + List.of( + new NoticeUpdateRequest.ContentDto(5, "subtitle", "소제목입니다", null, null, null), + new NoticeUpdateRequest.ContentDto(4, "body", "본문입니다", null, null, null), + new NoticeUpdateRequest.ContentDto(3, "image", "이미지입니다", "https://image.com", null, null), + new NoticeUpdateRequest.ContentDto(2, "button", "버튼입니다", null, "버튼 이름", "https://buttonLink.com"), + new NoticeUpdateRequest.ContentDto(1, "note", "유의사항입니다", null, null, null) + ) + ); + noticeService.update(updateRequest, noticeId); + + // then + NoticeFindResponse result = noticeService.findOneSpecific(noticeId); + List contents = result.contents(); + assertAll( + () -> assertThat(contents.get(0).type()).isEqualTo(SUBTITLE.name().toLowerCase()), + () -> assertThat(contents.get(1).type()).isEqualTo(BODY.name().toLowerCase()), + () -> assertThat(contents.get(2).type()).isEqualTo(IMAGE.name().toLowerCase()), + () -> assertThat(contents.get(3).type()).isEqualTo(BUTTON.name().toLowerCase()), + () -> assertThat(contents.get(4).type()).isEqualTo(NOTE.name().toLowerCase()) + ); + } + + @Test + void 공지_노출_여부를_수정한다() { + // given + NoticeCreateRequest createRequest = createNoticeCreateRequest(1); + Long noticeId = noticeService.create(createRequest); + + // when + noticeService.updateExposure(noticeId); + + // then + Notice result = noticeRepository.getById(noticeId); + assertThat(result.isExposed()).isTrue(); + } + + @Test + void 노출이_된_공지를_미노출로_변경한다() { + // given + NoticeCreateRequest createRequest = createNoticeCreateRequest(1); + Long noticeId = noticeService.create(createRequest); + noticeService.updateExposure(noticeId); + + // when + noticeService.updateExposure(noticeId); + + // then + Notice result = noticeRepository.getById(noticeId); + assertThat(result.isExposed()).isFalse(); + } + } + + @Test + void 공지_삭제() { + // given + NoticeCreateRequest createRequest = createNoticeCreateRequest(1); + Long noticeId = noticeService.create(createRequest); + + // when + noticeService.delete(noticeId); + + // then + Optional result = noticeRepository.findById(noticeId); + assertThat(result).isEmpty(); + } +} diff --git a/src/test/java/com/listywave/topic/application/service/TopicServiceTest.java b/src/test/java/com/listywave/topic/application/service/TopicServiceTest.java index 41f72fd0..8df38f94 100644 --- a/src/test/java/com/listywave/topic/application/service/TopicServiceTest.java +++ b/src/test/java/com/listywave/topic/application/service/TopicServiceTest.java @@ -42,7 +42,7 @@ class 토픽_생성 { assertThat(topic.getTitle().getValue()).isEqualTo("제일 좋아하는 여자 아이돌 TOP3"); assertThat(topic.getDescription().getValue()).isEqualTo("여러분들은 어떤 여돌을 가장 좋아하나요?"); assertThat(topic.isAnonymous()).isFalse(); - assertThat(topic.isExposed()).isFalse(); + assertThat(topic.isExposed()).isTrue(); // TODO: 어드민 로그인 붙이기 전까지는 항상 True로 생성 } ); }