Skip to content

Commit

Permalink
feat: 공지 CRUD API 구현 (#323)
Browse files Browse the repository at this point in the history
* test: 전체 테스트가 실패하는 문제 해결

정확한 원인은 파악하지 못했지만, 추측컨데 AcceptanceTest와 IntegrationTest의 테스트 DB가 공유되면서 외래키 제약 조건으로 발생한 것으로 예상 (#312)

* test: 요청 주제(토픽) 초기 생성 시 isExposed를 항상 true로 변경함에 따른 테스트 코드 수정 (#312)

* feat: 공지 카테고리(타입) 조회 API 구현 (#312)

* feat: Notice 도메인 객체 생성 (#312)

* feat: 공지 생성 API 구현 (#312)

* feat: 사용자용, 관리자용 공지 전체 조회 및 공지 상세 조회 API 구현 (#312)

* feat: 공지 수정 API 구현 (#312)

* feat: 공지 삭제 API 구현 (#312)

* fix: 관리자 존재 체크 로직 제거 (#312)

* refactor: 빠진 @nullable 추가 (#312)

* test: NoticeCreateRequest 생성 부분 리팩터링 (#312)
  • Loading branch information
kdkdhoho authored Oct 30, 2024
1 parent c35cfc3 commit 29fdecd
Show file tree
Hide file tree
Showing 27 changed files with 1,132 additions and 1 deletion.
32 changes: 32 additions & 0 deletions src/main/java/com/listywave/admin/Admin.java
Original file line number Diff line number Diff line change
@@ -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;
}
13 changes: 13 additions & 0 deletions src/main/java/com/listywave/admin/AdminRepository.java
Original file line number Diff line number Diff line change
@@ -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<Admin, Long> {

default Admin getById(Long id) {
return findById(id).orElseThrow(() -> new CustomException(RESOURCE_NOT_FOUND));
}
}
17 changes: 17 additions & 0 deletions src/main/java/com/listywave/admin/AdminService.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 2 additions & 0 deletions src/main/java/com/listywave/common/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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의 이미지를 삭제 요청하는 과정에서 에러가 발생했습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ContentType, String> {

@Override
public String convertToDatabaseColumn(ContentType contentType) {
return contentType.name().toLowerCase();
}

@Override
public ContentType convertToEntityAttribute(String s) {
return ContentType.valueOf(s.toUpperCase());
}
}
Original file line number Diff line number Diff line change
@@ -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<NoticeType, String> {

@Override
public String convertToDatabaseColumn(NoticeType noticeType) {
return String.valueOf(noticeType.getCode());
}

@Override
public NoticeType convertToEntityAttribute(String s) {
return NoticeType.codeOf(Integer.parseInt(s));
}
}
Original file line number Diff line number Diff line change
@@ -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,
;
}
70 changes: 70 additions & 0 deletions src/main/java/com/listywave/notice/application/domain/Notice.java
Original file line number Diff line number Diff line change
@@ -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<NoticeContent> contents = new ArrayList<>();

public Notice(NoticeType type, NoticeTitle title, NoticeDescription description) {
this.type = type;
this.title = title;
this.description = description;
}

public void addContents(List<NoticeContent> contents) {
this.contents.addAll(contents);
}

public String getFirstImageUrl() {
Optional<String> 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<NoticeContent> contents) {
this.type = type;
this.title = title;
this.description = description;

this.contents.clear();
this.contents.addAll(contents);
}

public void changeExposure() {
this.isExposed = !this.isExposed;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
Loading

0 comments on commit 29fdecd

Please sign in to comment.