diff --git a/k8s/helm-value.yaml b/k8s/helm-value.yaml index 32ff051..2bd52c1 100644 --- a/k8s/helm-value.yaml +++ b/k8s/helm-value.yaml @@ -1,2 +1,2 @@ image: - tag: v0.1.2 + tag: v0.1.3 diff --git a/src/main/java/com/earseo/core/common/config/JpaAuditingConfig.java b/src/main/java/com/earseo/core/common/config/JpaAuditingConfig.java new file mode 100644 index 0000000..7161982 --- /dev/null +++ b/src/main/java/com/earseo/core/common/config/JpaAuditingConfig.java @@ -0,0 +1,9 @@ +package com.earseo.core.common.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaAuditingConfig { +} diff --git a/src/main/java/com/earseo/core/common/exception/NoticeError.java b/src/main/java/com/earseo/core/common/exception/NoticeError.java new file mode 100644 index 0000000..e38c86b --- /dev/null +++ b/src/main/java/com/earseo/core/common/exception/NoticeError.java @@ -0,0 +1,26 @@ +package com.earseo.core.common.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum NoticeError implements ErrorCodeInterface{ + + NOTICE_NOT_FOUND("NTC001", "공지사항을 찾을 수 없습니다", HttpStatus.NOT_FOUND), + ; + + private final String status; + private final String message; + private final HttpStatus httpStatus; + + @Override + public ErrorCode getErrorCode() { + return ErrorCode.builder() + .status(status) + .message(message) + .httpStatus(httpStatus) + .build(); + } +} diff --git a/src/main/java/com/earseo/core/controller/NoticeController.java b/src/main/java/com/earseo/core/controller/NoticeController.java new file mode 100644 index 0000000..f57abde --- /dev/null +++ b/src/main/java/com/earseo/core/controller/NoticeController.java @@ -0,0 +1,60 @@ +package com.earseo.core.controller; + +import com.earseo.core.common.BaseResponse; +import com.earseo.core.dto.request.NoticeCreateRequest; +import com.earseo.core.dto.request.NoticeUpdateRequest; +import com.earseo.core.dto.response.NoticeDeleteResponse; +import com.earseo.core.dto.response.NoticePageResponse; +import com.earseo.core.dto.response.NoticeResponse; +import com.earseo.core.service.NoticeService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + @GetMapping("/api/core/notice/{noticeId}") + public ResponseEntity> getNotice( + @PathVariable Long noticeId + ) { + return ResponseEntity.ok(BaseResponse.ok(noticeService.getNotice(noticeId))); + } + + @GetMapping("/api/core/notice") + public ResponseEntity> getNoticeList( + @PageableDefault(size = 10, sort = "id", direction = Sort.Direction.DESC) + Pageable pageable + ) { + return ResponseEntity.ok(BaseResponse.ok(noticeService.getNoticeList(pageable))); + } + + @PostMapping("/api/admin/core/notice") + public ResponseEntity> createNotice( + @RequestBody @Valid NoticeCreateRequest request + ){ + return ResponseEntity.ok(BaseResponse.ok(noticeService.createNotice(request))); + } + + @PutMapping("/api/admin/core/notice/{noticeId}") + public ResponseEntity> updateNotice( + @PathVariable Long noticeId, + @RequestBody @Valid NoticeUpdateRequest request + ){ + return ResponseEntity.ok(BaseResponse.ok(noticeService.updateNotice(noticeId, request))); + } + + @DeleteMapping("/api/admin/core/notice/{noticeId}") + public ResponseEntity> deleteNotice( + @PathVariable Long noticeId + ){ + return ResponseEntity.ok(BaseResponse.ok(noticeService.deleteNotice(noticeId))); + } +} diff --git a/src/main/java/com/earseo/core/dto/etl/NoticePageItem.java b/src/main/java/com/earseo/core/dto/etl/NoticePageItem.java new file mode 100644 index 0000000..930a95d --- /dev/null +++ b/src/main/java/com/earseo/core/dto/etl/NoticePageItem.java @@ -0,0 +1,7 @@ +package com.earseo.core.dto.etl; + +public record NoticePageItem( + Long noticeId, + String title +) { +} diff --git a/src/main/java/com/earseo/core/dto/request/NoticeCreateRequest.java b/src/main/java/com/earseo/core/dto/request/NoticeCreateRequest.java new file mode 100644 index 0000000..9d99878 --- /dev/null +++ b/src/main/java/com/earseo/core/dto/request/NoticeCreateRequest.java @@ -0,0 +1,14 @@ +package com.earseo.core.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record NoticeCreateRequest( + @NotBlank(message = "제목은 필수입니다") + @Size(min = 1, max = 255, message = "제목은 1자 이상 255자 이하여야 합니다") + String title, + @NotBlank(message = "내용은 필수입니다") + @Size(min = 1, max = 1000, message = "내용은 1자 이상 1000자 이하여야 합니다") + String content +) { +} diff --git a/src/main/java/com/earseo/core/dto/request/NoticeUpdateRequest.java b/src/main/java/com/earseo/core/dto/request/NoticeUpdateRequest.java new file mode 100644 index 0000000..89f9355 --- /dev/null +++ b/src/main/java/com/earseo/core/dto/request/NoticeUpdateRequest.java @@ -0,0 +1,14 @@ +package com.earseo.core.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record NoticeUpdateRequest( + @NotBlank(message = "제목은 필수입니다") + @Size(min = 1, max = 255, message = "제목은 1자 이상 255자 이하여야 합니다") + String title, + @NotBlank(message = "내용은 필수입니다") + @Size(min = 1, max = 1000, message = "내용은 1자 이상 1000자 이하여야 합니다") + String content +) { +} diff --git a/src/main/java/com/earseo/core/dto/response/NoticeDeleteResponse.java b/src/main/java/com/earseo/core/dto/response/NoticeDeleteResponse.java new file mode 100644 index 0000000..7c056f3 --- /dev/null +++ b/src/main/java/com/earseo/core/dto/response/NoticeDeleteResponse.java @@ -0,0 +1,6 @@ +package com.earseo.core.dto.response; + +public record NoticeDeleteResponse( + Long noticeId +) { +} diff --git a/src/main/java/com/earseo/core/dto/response/NoticePageResponse.java b/src/main/java/com/earseo/core/dto/response/NoticePageResponse.java new file mode 100644 index 0000000..9406618 --- /dev/null +++ b/src/main/java/com/earseo/core/dto/response/NoticePageResponse.java @@ -0,0 +1,36 @@ +package com.earseo.core.dto.response; + +import com.earseo.core.dto.etl.NoticePageItem; +import io.swagger.v3.oas.annotations.media.Schema; +import org.springframework.data.domain.Slice; + +import java.util.List; + +public record NoticePageResponse( + @Schema(description = "공지사항 목록 (페이징/정렬 반영)") + List content, + @Schema(description = "현재 페이지 번호 (0부터 시작)", example = "0") + int number, + @Schema(description = "페이지 크기", example = "10") + int size, + @Schema(description = "첫 페이지 여부", example = "true") + boolean isFirst, + @Schema(description = "마지막 페이지 여부", example = "false") + boolean isLast, + @Schema(description = "다음 페이지 존재 여부", example = "true") + boolean hasNext, + @Schema(description = "이전 페이지 존재 여부", example = "false") + boolean hasPrevious +) { + public static NoticePageResponse toDto(Slice page) { + return new NoticePageResponse( + page.getContent(), + page.getNumber(), + page.getSize(), + page.isFirst(), + page.isLast(), + page.hasNext(), + page.hasPrevious() + ); + } +} diff --git a/src/main/java/com/earseo/core/dto/response/NoticeResponse.java b/src/main/java/com/earseo/core/dto/response/NoticeResponse.java new file mode 100644 index 0000000..c9cf365 --- /dev/null +++ b/src/main/java/com/earseo/core/dto/response/NoticeResponse.java @@ -0,0 +1,23 @@ +package com.earseo.core.dto.response; + +import com.earseo.core.entity.Notice; + +import java.time.format.DateTimeFormatter; + +public record NoticeResponse( + Long noticeId, + String noticeTitle, + String noticeContent, + String createdAt, + String updatedAt +) { + public static NoticeResponse toDto(Notice notice) { + return new NoticeResponse( + notice.getId(), + notice.getTitle(), + notice.getContent(), + notice.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")), + notice.getUpdatedAt().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) + ); + } +} diff --git a/src/main/java/com/earseo/core/entity/Notice.java b/src/main/java/com/earseo/core/entity/Notice.java new file mode 100644 index 0000000..fe29150 --- /dev/null +++ b/src/main/java/com/earseo/core/entity/Notice.java @@ -0,0 +1,46 @@ +package com.earseo.core.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class Notice { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "title") + private String title; + + @Column(name = "content", columnDefinition = "TEXT") + private String content; + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; + + public void update(String title, String content) { + if (title != null && !title.isBlank()) { + this.title = title; + } + if (content != null && !content.isBlank()) { + this.content = content; + } + } +} diff --git a/src/main/java/com/earseo/core/repository/NoticeRepository.java b/src/main/java/com/earseo/core/repository/NoticeRepository.java new file mode 100644 index 0000000..ccc01cd --- /dev/null +++ b/src/main/java/com/earseo/core/repository/NoticeRepository.java @@ -0,0 +1,14 @@ +package com.earseo.core.repository; + +import com.earseo.core.dto.etl.NoticePageItem; +import com.earseo.core.entity.Notice; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface NoticeRepository extends JpaRepository { + + @Query("SELECT new com.earseo.core.dto.etl.NoticePageItem(n.id, n.title) FROM Notice n") + Slice findNoticeList(Pageable pageable); +} diff --git a/src/main/java/com/earseo/core/service/NoticeService.java b/src/main/java/com/earseo/core/service/NoticeService.java new file mode 100644 index 0000000..e9de2a2 --- /dev/null +++ b/src/main/java/com/earseo/core/service/NoticeService.java @@ -0,0 +1,74 @@ +package com.earseo.core.service; + +import com.earseo.core.common.exception.BaseException; +import com.earseo.core.common.exception.NoticeError; +import com.earseo.core.dto.etl.NoticePageItem; +import com.earseo.core.dto.request.NoticeCreateRequest; +import com.earseo.core.dto.request.NoticeUpdateRequest; +import com.earseo.core.dto.response.NoticeDeleteResponse; +import com.earseo.core.dto.response.NoticePageResponse; +import com.earseo.core.dto.response.NoticeResponse; +import com.earseo.core.entity.Notice; +import com.earseo.core.repository.NoticeRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class NoticeService { + + private final NoticeRepository noticeRepository; + + @Transactional(readOnly = true) + public NoticeResponse getNotice(Long noticeId) { + Notice notice = noticeRepository.findById(noticeId) + .orElseThrow(() -> new BaseException(NoticeError.NOTICE_NOT_FOUND)); + + return NoticeResponse.toDto(notice); + } + + @Transactional(readOnly = true) + public NoticePageResponse getNoticeList(Pageable pageable) { + Slice notices = noticeRepository.findNoticeList(pageable); + return NoticePageResponse.toDto(notices); + } + + @Transactional + public NoticeResponse createNotice(NoticeCreateRequest request) { + Notice notice = Notice.builder() + .title(request.title()) + .content(request.content()) + .build(); + + noticeRepository.save(notice); + + return NoticeResponse.toDto(notice); + } + + @Transactional + public NoticeResponse updateNotice(Long noticeId, NoticeUpdateRequest noticeUpdateRequest) { + Notice notice = noticeRepository.findById(noticeId) + .orElseThrow(() -> new BaseException(NoticeError.NOTICE_NOT_FOUND)); + + notice.update( + noticeUpdateRequest.title(), noticeUpdateRequest.content() + ); + + return NoticeResponse.toDto(notice); + } + + @Transactional + public NoticeDeleteResponse deleteNotice(Long noticeId) { + Notice notice = noticeRepository.findById(noticeId) + .orElseThrow(() -> new BaseException(NoticeError.NOTICE_NOT_FOUND)); + + noticeRepository.delete(notice); + + return new NoticeDeleteResponse(noticeId); + } +}