From f54fcf167d4816f14a737710a1c22bd5f60ff268 Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 7 Jul 2025 17:54:16 +0900 Subject: [PATCH 1/9] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EA=B3=B5=EC=A7=80?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=B0=8F=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=95=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../query/NoticeQueryController.java | 76 ++++++++++++++++++ .../notice/converter/NoticeConverter.java | 77 +++++++++++++++++++ .../notice/dto/request/NoticeRequestDTO.java | 29 +++++++ .../dto/response/NoticeResponseDTO.java | 26 +++++++ .../domain/notice/entity/Notice.java | 1 + .../notice/repository/NoticeRepository.java | 39 ++++++++++ .../service/query/NoticeQueryService.java | 10 +++ .../service/query/NoticeQueryServiceImpl.java | 30 ++++++++ .../global/annotation/SwaggerPageable.java | 35 +++++++++ .../global/error/code/NoticeErrorCode.java | 29 +++++++ .../error/exception/NoticeException.java | 10 +++ .../global/security/SecurityConfig.java | 1 + 12 files changed, 363 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/notice/controller/query/NoticeQueryController.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/notice/converter/NoticeConverter.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/notice/dto/request/NoticeRequestDTO.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/notice/dto/response/NoticeResponseDTO.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/notice/repository/NoticeRepository.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryService.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryServiceImpl.java create mode 100644 src/main/java/org/withtime/be/withtimebe/global/annotation/SwaggerPageable.java create mode 100644 src/main/java/org/withtime/be/withtimebe/global/error/code/NoticeErrorCode.java create mode 100644 src/main/java/org/withtime/be/withtimebe/global/error/exception/NoticeException.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/query/NoticeQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/query/NoticeQueryController.java new file mode 100644 index 0000000..f9f4bd3 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/query/NoticeQueryController.java @@ -0,0 +1,76 @@ +package org.withtime.be.withtimebe.domain.notice.controller.query; + + +import org.namul.api.payload.response.DefaultResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.withtime.be.withtimebe.domain.notice.converter.NoticeConverter; +import org.withtime.be.withtimebe.domain.notice.dto.request.NoticeRequestDTO; +import org.withtime.be.withtimebe.domain.notice.dto.response.NoticeResponseDTO; +import org.withtime.be.withtimebe.domain.notice.entity.Notice; +import org.withtime.be.withtimebe.domain.notice.service.query.NoticeQueryService; +import org.withtime.be.withtimebe.global.annotation.SwaggerPageable; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1") +public class NoticeQueryController { + + private final NoticeQueryService noticeQueryService; + + @Operation(summary = "공지사항 전체 조회 API by 피우", description = "공지사항 전체 조회 API입니다. (검색어 X)") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공입니다."), + @ApiResponse( + responseCode = "404", + description = """ + - NOTICE404_1 : 해당하는 공지사항 유형을 찾을 수 없습니다." + """) + }) + @Parameter(name = "noticeCategory", description = "SYSTEM / SERVICE") + @SwaggerPageable + @GetMapping("/notices") + public DefaultResponse findNoticeList( + @PageableDefault(page = 0, size = 10) Pageable pageable, + @RequestParam String noticeCategory + ) { + NoticeRequestDTO.FindNoticeList request = NoticeConverter.toFindNoticeList(pageable, noticeCategory); + Page result = noticeQueryService.findNoticeList(request); + NoticeResponseDTO.NoticeList response = NoticeConverter.toNoticeList(result); + return DefaultResponse.ok(response); + } + + @Operation(summary = "공지사항 검색어 전체 조회 API by 피우", description = "공지사항 전체 조회 API입니다. (검색어 O)") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공입니다."), + @ApiResponse( + responseCode = "404", + description = """ + - NOTICE404_1 : 해당하는 공지사항 유형을 찾을 수 없습니다." + """) + }) + @Parameter(name = "noticeCategory", description = "SYSTEM / SERVICE") + @SwaggerPageable + @GetMapping("/notices/search") + public DefaultResponse findNoticeListByKeyword( + @PageableDefault(page = 0, size = 10) Pageable pageable, + @RequestParam String keyword, + @RequestParam String noticeCategory + ) { + NoticeRequestDTO.FindNoticeListByKeyword request = NoticeConverter.toFindNoticeListByKeyword(pageable, keyword, noticeCategory); + Page result = noticeQueryService.findNoticeListByKeyword(request); + NoticeResponseDTO.NoticeList response = NoticeConverter.toNoticeList(result); + return DefaultResponse.ok(response); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/converter/NoticeConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/converter/NoticeConverter.java new file mode 100644 index 0000000..ef88024 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/converter/NoticeConverter.java @@ -0,0 +1,77 @@ +package org.withtime.be.withtimebe.domain.notice.converter; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.withtime.be.withtimebe.domain.notice.dto.request.NoticeRequestDTO; +import org.withtime.be.withtimebe.domain.notice.dto.response.NoticeResponseDTO; +import org.withtime.be.withtimebe.domain.notice.entity.Notice; +import org.withtime.be.withtimebe.domain.notice.entity.enums.NoticeCategory; +import org.withtime.be.withtimebe.global.error.code.NoticeErrorCode; +import org.withtime.be.withtimebe.global.error.exception.NoticeException; + +public class NoticeConverter { + + // Request DTO : 전체 조회 (Controller -> Service) + public static NoticeRequestDTO.FindNoticeList toFindNoticeList(Pageable pageable, String type) { + + NoticeCategory noticeCategory; + + try { + noticeCategory = NoticeCategory.valueOf(type); + } catch (IllegalArgumentException e) { + throw new NoticeException(NoticeErrorCode.NOTICE_CATEGORY_NOT_FOUND); + } + + return NoticeRequestDTO.FindNoticeList.builder() + .pageable(pageable) + .noticeCategory(noticeCategory) + .build(); + } + + // Request DTO : 검색어 전체 조회 (Controller -> Service) + public static NoticeRequestDTO.FindNoticeListByKeyword toFindNoticeListByKeyword(Pageable pageable, String keyword, String type) { + + NoticeCategory noticeCategory; + + try { + noticeCategory = NoticeCategory.valueOf(type); + } catch (IllegalArgumentException e) { + throw new NoticeException(NoticeErrorCode.NOTICE_CATEGORY_NOT_FOUND); + } + + return NoticeRequestDTO.FindNoticeListByKeyword.builder() + .pageable(pageable) + .keyword(keyword) + .noticeCategory(noticeCategory) + .build(); + } + + // Response DTO : NoticeResponseDTO.NoticeList + public static NoticeResponseDTO.NoticeList toNoticeList(Page noticePage) { + + List noticeList = noticePage.getContent().stream() + .map(NoticeConverter::toNotice) + .toList(); + + return NoticeResponseDTO.NoticeList.builder() + .noticeList(noticeList) + .totalPages(noticePage.getTotalPages()) + .currentPage(noticePage.getNumber()) + .currentSize(noticePage.getNumberOfElements()) + .hasNextPage(noticePage.hasNext()) + .build(); + } + + // Response DTO : NoticeResponseDTO.Notice + public static NoticeResponseDTO.Notice toNotice(Notice notice) { + + return NoticeResponseDTO.Notice.builder() + .noticeId(notice.getId()) + .title(notice.getTitle()) + .isPinned(notice.getIsPinned()) + .createdAt(notice.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/request/NoticeRequestDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/request/NoticeRequestDTO.java new file mode 100644 index 0000000..89eaeab --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/request/NoticeRequestDTO.java @@ -0,0 +1,29 @@ +package org.withtime.be.withtimebe.domain.notice.dto.request; + +import org.springframework.data.domain.Pageable; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.notice.entity.enums.NoticeCategory; + +import lombok.Builder; + +public record NoticeRequestDTO() { + + @Builder + public record FindNoticeList( + Pageable pageable, // 게시글 식별자 값 + NoticeCategory noticeCategory // 게시글 유형 + ) {} + + @Builder + public record FindNoticeListByKeyword( + Pageable pageable, // 게시글 식별자 값 + String keyword, // 검색 키워드 + NoticeCategory noticeCategory // 게시글 유형 + ) {} + + @Builder + public record FindNoticeDetail( + Long noticeId, + Member member + ) {} +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/response/NoticeResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/response/NoticeResponseDTO.java new file mode 100644 index 0000000..862b417 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/response/NoticeResponseDTO.java @@ -0,0 +1,26 @@ +package org.withtime.be.withtimebe.domain.notice.dto.response; + +import java.time.LocalDateTime; +import java.util.List; + +import lombok.Builder; + +public record NoticeResponseDTO() { + + @Builder + public record NoticeList( + List noticeList, + Integer totalPages, // 전체 페이지 개수 + Integer currentPage, // 현재 페이지 번호 + Integer currentSize, // 현재 페이지의 크기 + Boolean hasNextPage // 다음 페이지 존재 여부 + ) {} + + @Builder + public record Notice( + Long noticeId, // 게시글 식별자 값 + String title, // 게시글 제목 + Boolean isPinned, // 고정 여부 + LocalDateTime createdAt // 생성 날짜 + ) {} +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/entity/Notice.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/entity/Notice.java index 9ede00b..30ab2ac 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/entity/Notice.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/entity/Notice.java @@ -27,6 +27,7 @@ public class Notice extends BaseEntity { @Column(name = "content") private String content; + @Enumerated(EnumType.STRING) @Column(name = "notice_category") private NoticeCategory noticeCategory; diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/repository/NoticeRepository.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/repository/NoticeRepository.java new file mode 100644 index 0000000..b29e34c --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/repository/NoticeRepository.java @@ -0,0 +1,39 @@ +package org.withtime.be.withtimebe.domain.notice.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.withtime.be.withtimebe.domain.notice.entity.Notice; +import org.withtime.be.withtimebe.domain.notice.entity.enums.NoticeCategory; + +public interface NoticeRepository extends JpaRepository { + + @Query(""" + SELECT n FROM Notice n + WHERE n.noticeCategory = :noticeCategory + AND n.deletedAt IS NULL + ORDER BY n.isPinned DESC, n.createdAt DESC + """) + Page findNoticeListByNoticeCategory( + @Param("noticeCategory") NoticeCategory noticeCategory, + Pageable pageable + ); + + @Query(""" + SELECT n FROM Notice n + WHERE n.noticeCategory = :noticeCategory + AND ( + n.title LIKE %:keyword% + OR n.content LIKE %:keyword% + ) + AND n.deletedAt IS NULL + ORDER BY n.isPinned DESC, n.createdAt DESC + """) + Page findNoticeListByNoticeCategoryAndKeyword( + @Param("noticeCategory") NoticeCategory noticeCategory, + @Param("keyword") String keyword, + Pageable pageable + ); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryService.java new file mode 100644 index 0000000..6eb9d74 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryService.java @@ -0,0 +1,10 @@ +package org.withtime.be.withtimebe.domain.notice.service.query; + +import org.springframework.data.domain.Page; +import org.withtime.be.withtimebe.domain.notice.dto.request.NoticeRequestDTO; +import org.withtime.be.withtimebe.domain.notice.entity.Notice; + +public interface NoticeQueryService { + Page findNoticeList(NoticeRequestDTO.FindNoticeList request); + Page findNoticeListByKeyword(NoticeRequestDTO.FindNoticeListByKeyword request); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryServiceImpl.java new file mode 100644 index 0000000..78d9df3 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryServiceImpl.java @@ -0,0 +1,30 @@ +package org.withtime.be.withtimebe.domain.notice.service.query; + +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.withtime.be.withtimebe.domain.notice.dto.request.NoticeRequestDTO; +import org.withtime.be.withtimebe.domain.notice.entity.Notice; +import org.withtime.be.withtimebe.domain.notice.repository.NoticeRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class NoticeQueryServiceImpl implements NoticeQueryService { + + private final NoticeRepository noticeRepository; + + @Override + public Page findNoticeList(NoticeRequestDTO.FindNoticeList request) { + return noticeRepository.findNoticeListByNoticeCategory( + request.noticeCategory(), request.pageable()); + } + + @Override + public Page findNoticeListByKeyword(NoticeRequestDTO.FindNoticeListByKeyword request) { + return noticeRepository.findNoticeListByNoticeCategoryAndKeyword( + request.noticeCategory(), request.keyword(), request.pageable()); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/global/annotation/SwaggerPageable.java b/src/main/java/org/withtime/be/withtimebe/global/annotation/SwaggerPageable.java new file mode 100644 index 0000000..b14fb22 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/annotation/SwaggerPageable.java @@ -0,0 +1,35 @@ +package org.withtime.be.withtimebe.global.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.enums.ParameterIn; + +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Parameters({ + @Parameter( + name = "pageable", + hidden = true + ), + @Parameter( + in = ParameterIn.QUERY, + name = "page", + description = "페이지 번호입니다.", + schema = @io.swagger.v3.oas.annotations.media.Schema(type = "integer", defaultValue = "0"), + required = true + ), + @Parameter( + in = ParameterIn.QUERY, + name = "size", + description = "한 페이지에 표시될 데이터 개수 입니다.", + schema = @io.swagger.v3.oas.annotations.media.Schema(type = "integer", defaultValue = "10"), + required = true + ) +}) +public @interface SwaggerPageable { +} diff --git a/src/main/java/org/withtime/be/withtimebe/global/error/code/NoticeErrorCode.java b/src/main/java/org/withtime/be/withtimebe/global/error/code/NoticeErrorCode.java new file mode 100644 index 0000000..24802e1 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/error/code/NoticeErrorCode.java @@ -0,0 +1,29 @@ +package org.withtime.be.withtimebe.global.error.code; + +import org.namul.api.payload.code.BaseErrorCode; +import org.namul.api.payload.code.dto.ErrorReasonDTO; +import org.namul.api.payload.code.dto.supports.DefaultResponseErrorReasonDTO; +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public enum NoticeErrorCode implements BaseErrorCode { + + NOTICE_CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "NOTICE404_1", "해당하는 공지사항 유형을 찾을 수 없습니다."), + NOTICE_NOT_FOUND(HttpStatus.NOT_FOUND, "NOTICE404_2", "해당하는 공지사항을 찾을 수 없습니다."), + ; + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ErrorReasonDTO getReason() { + return DefaultResponseErrorReasonDTO.builder() + .httpStatus(this.httpStatus) + .code(this.code) + .message(this.message) + .build(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/global/error/exception/NoticeException.java b/src/main/java/org/withtime/be/withtimebe/global/error/exception/NoticeException.java new file mode 100644 index 0000000..8cbedf4 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/error/exception/NoticeException.java @@ -0,0 +1,10 @@ +package org.withtime.be.withtimebe.global.error.exception; + +import org.namul.api.payload.code.BaseErrorCode; +import org.namul.api.payload.error.exception.ServerApplicationException; + +public class NoticeException extends ServerApplicationException { + public NoticeException(BaseErrorCode baseErrorCode) { + super(baseErrorCode); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java b/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java index 1e71c67..f0ea5e4 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java +++ b/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java @@ -39,6 +39,7 @@ public class SecurityConfig { private String[] allowUrl = { API_PREFIX + "/auth/**", + API_PREFIX + "/notices/**", "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**" From d8b58d40e5e634262abf9e842fc3d26938fa1cfb Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 7 Jul 2025 18:09:44 +0900 Subject: [PATCH 2/9] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EA=B3=B5=EC=A7=80?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../query/NoticeQueryController.java | 28 +++++++++++++++++++ .../notice/converter/NoticeConverter.java | 26 +++++++++++++++++ .../dto/response/NoticeResponseDTO.java | 10 +++++++ .../notice/repository/NoticeRepository.java | 4 +++ .../service/query/NoticeQueryService.java | 2 ++ .../service/query/NoticeQueryServiceImpl.java | 22 +++++++++++++++ .../global/error/code/NoticeErrorCode.java | 2 ++ 7 files changed, 94 insertions(+) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/query/NoticeQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/query/NoticeQueryController.java index f9f4bd3..23124da 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/query/NoticeQueryController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/query/NoticeQueryController.java @@ -6,15 +6,18 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.withtime.be.withtimebe.domain.member.entity.Member; import org.withtime.be.withtimebe.domain.notice.converter.NoticeConverter; import org.withtime.be.withtimebe.domain.notice.dto.request.NoticeRequestDTO; import org.withtime.be.withtimebe.domain.notice.dto.response.NoticeResponseDTO; import org.withtime.be.withtimebe.domain.notice.entity.Notice; import org.withtime.be.withtimebe.domain.notice.service.query.NoticeQueryService; import org.withtime.be.withtimebe.global.annotation.SwaggerPageable; +import org.withtime.be.withtimebe.global.security.annotation.AuthenticatedMember; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -73,4 +76,29 @@ public DefaultResponse findNoticeListByKeyword( NoticeResponseDTO.NoticeList response = NoticeConverter.toNoticeList(result); return DefaultResponse.ok(response); } + + @Operation(summary = "공지사항 상세 조회 API by 피우", description = "공지사항 상세 조회 API입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공입니다."), + @ApiResponse( + responseCode = "403", + description = """ + - NOTICE403_1 : 삭제된 공지사항을 열람할 Admin 권한이 없습니다. + """), + @ApiResponse( + responseCode = "404", + description = """ + - NOTICE404_2 : 해당하는 공지사항을 찾을 수 없습니다. + """) + }) + @GetMapping("/notices/{noticeId}") + public DefaultResponse findNoticeDetail( + @PathVariable("noticeId") Long noticeId, + @AuthenticatedMember Member member + ) { + NoticeRequestDTO.FindNoticeDetail request = NoticeConverter.toFindNoticeDetail(noticeId, member); + Notice result = noticeQueryService.findNoticeDetail(request); + NoticeResponseDTO.NoticeDetail response = NoticeConverter.toNoticeDetail(result, member); + return DefaultResponse.ok(response); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/converter/NoticeConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/converter/NoticeConverter.java index ef88024..f933b72 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/converter/NoticeConverter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/converter/NoticeConverter.java @@ -4,6 +4,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.member.entity.enums.Role; import org.withtime.be.withtimebe.domain.notice.dto.request.NoticeRequestDTO; import org.withtime.be.withtimebe.domain.notice.dto.response.NoticeResponseDTO; import org.withtime.be.withtimebe.domain.notice.entity.Notice; @@ -48,6 +50,15 @@ public static NoticeRequestDTO.FindNoticeListByKeyword toFindNoticeListByKeyword .build(); } + // Request : 상세 조회 요청 (Controller -> Service) DTO + public static NoticeRequestDTO.FindNoticeDetail toFindNoticeDetail(Long noticeId, Member member) { + + return NoticeRequestDTO.FindNoticeDetail.builder() + .noticeId(noticeId) + .member(member) + .build(); + } + // Response DTO : NoticeResponseDTO.NoticeList public static NoticeResponseDTO.NoticeList toNoticeList(Page noticePage) { @@ -74,4 +85,19 @@ public static NoticeResponseDTO.Notice toNotice(Notice notice) { .createdAt(notice.getCreatedAt()) .build(); } + + // Response : NoticeDetail(DTO)로 변환 + public static NoticeResponseDTO.NoticeDetail toNoticeDetail(Notice notice, Member member) { + + boolean hasAdminAuth = member != null && member.getRole().equals(Role.ADMIN); + + return NoticeResponseDTO.NoticeDetail.builder() + .noticeId(notice.getId()) + .title(notice.getTitle()) + .content(notice.getContent()) + .isPinned(notice.getIsPinned()) + .hasAdminAuth(hasAdminAuth) + .createdAt(notice.getCreatedAt()) + .build(); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/response/NoticeResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/response/NoticeResponseDTO.java index 862b417..e4fc557 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/response/NoticeResponseDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/response/NoticeResponseDTO.java @@ -23,4 +23,14 @@ public record Notice( Boolean isPinned, // 고정 여부 LocalDateTime createdAt // 생성 날짜 ) {} + + @Builder + public record NoticeDetail( + Long noticeId, // 게시글 식별자 값 + String title, // 게시글 제목 + String content, // 게시글 내용 + Boolean isPinned, // 고정 여부 + Boolean hasAdminAuth, // 어드민 여부 + LocalDateTime createdAt // 생성 날짜 + ) {} } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/repository/NoticeRepository.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/repository/NoticeRepository.java index b29e34c..f3d63c6 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/repository/NoticeRepository.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/repository/NoticeRepository.java @@ -1,5 +1,7 @@ package org.withtime.be.withtimebe.domain.notice.repository; +import java.util.Optional; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -36,4 +38,6 @@ Page findNoticeListByNoticeCategoryAndKeyword( @Param("keyword") String keyword, Pageable pageable ); + + Optional findNoticeById(Long noticeId); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryService.java index 6eb9d74..7b42a24 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryService.java @@ -7,4 +7,6 @@ public interface NoticeQueryService { Page findNoticeList(NoticeRequestDTO.FindNoticeList request); Page findNoticeListByKeyword(NoticeRequestDTO.FindNoticeListByKeyword request); + + Notice findNoticeDetail(NoticeRequestDTO.FindNoticeDetail request); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryServiceImpl.java index 78d9df3..bdd2ede 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryServiceImpl.java @@ -3,9 +3,15 @@ import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.member.entity.enums.Role; import org.withtime.be.withtimebe.domain.notice.dto.request.NoticeRequestDTO; import org.withtime.be.withtimebe.domain.notice.entity.Notice; import org.withtime.be.withtimebe.domain.notice.repository.NoticeRepository; +import org.withtime.be.withtimebe.global.error.code.AuthErrorCode; +import org.withtime.be.withtimebe.global.error.code.NoticeErrorCode; +import org.withtime.be.withtimebe.global.error.exception.AuthException; +import org.withtime.be.withtimebe.global.error.exception.NoticeException; import lombok.RequiredArgsConstructor; @@ -27,4 +33,20 @@ public Page findNoticeListByKeyword(NoticeRequestDTO.FindNoticeListByKey return noticeRepository.findNoticeListByNoticeCategoryAndKeyword( request.noticeCategory(), request.keyword(), request.pageable()); } + + @Override + public Notice findNoticeDetail(NoticeRequestDTO.FindNoticeDetail request) { + + Notice notice = noticeRepository.findNoticeById(request.noticeId()) + .orElseThrow(() -> new NoticeException(NoticeErrorCode.NOTICE_NOT_FOUND)); + Member member = request.member(); + + // 삭제된 게시글을 USER가 보려는 경우 처리 + if(notice.getDeletedAt() != null) { + if(member == null || !member.getRole().equals(Role.ADMIN)) + throw new AuthException(NoticeErrorCode.DELETED_NOTICE_FORBIDDEN_ACCESS); + } + + return notice; + } } diff --git a/src/main/java/org/withtime/be/withtimebe/global/error/code/NoticeErrorCode.java b/src/main/java/org/withtime/be/withtimebe/global/error/code/NoticeErrorCode.java index 24802e1..6c6389f 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/error/code/NoticeErrorCode.java +++ b/src/main/java/org/withtime/be/withtimebe/global/error/code/NoticeErrorCode.java @@ -10,6 +10,8 @@ @AllArgsConstructor public enum NoticeErrorCode implements BaseErrorCode { + DELETED_NOTICE_FORBIDDEN_ACCESS(HttpStatus.FORBIDDEN, "NOTICE403_1", "삭제된 공지사항을 열람할 Admin 권한이 없습니다."), + NOTICE_CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "NOTICE404_1", "해당하는 공지사항 유형을 찾을 수 없습니다."), NOTICE_NOT_FOUND(HttpStatus.NOT_FOUND, "NOTICE404_2", "해당하는 공지사항을 찾을 수 없습니다."), ; From 738de0cc597b73738e88d162876cbdc5d4372b01 Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 7 Jul 2025 18:18:49 +0900 Subject: [PATCH 3/9] =?UTF-8?q?=E2=9C=A8=20=20feat:=20[ADMIN]=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=ED=95=9C=20=EA=B3=B5=EC=A7=80=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=EC=A1=B0=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../query/NoticeQueryController.java | 26 +++++++++++++++++++ .../notice/repository/NoticeRepository.java | 11 ++++++++ .../service/query/NoticeQueryService.java | 2 +- .../service/query/NoticeQueryServiceImpl.java | 7 +++++ 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/query/NoticeQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/query/NoticeQueryController.java index 23124da..423660f 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/query/NoticeQueryController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/query/NoticeQueryController.java @@ -101,4 +101,30 @@ public DefaultResponse findNoticeDetail( NoticeResponseDTO.NoticeDetail response = NoticeConverter.toNoticeDetail(result, member); return DefaultResponse.ok(response); } + + @Operation(summary = "삭제된 공지사항 전체 조회 API Only Admin by 피우", description = "삭제된 공지사항 상세 조회 API입니다. 어드민만 사용 가능합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공입니다."), + @ApiResponse(responseCode = "403", + description = """ + - COMMON403 : "Admin 권한이 없음을 의미합니다." + """), + @ApiResponse( + responseCode = "404", + description = """ + - NOTICE404_1 : "해당하는 공지사항 유형을 찾을 수 없습니다." + """) + }) + @Parameter(name = "noticeCategory", description = "SYSTEM / SERVICE") + @SwaggerPageable + @GetMapping("/admin/notices/trash") + public DefaultResponse findTrashNoticeList( + @PageableDefault(page = 0, size = 10) Pageable pageable, + @RequestParam String noticeCategory + ) { + NoticeRequestDTO.FindNoticeList request = NoticeConverter.toFindNoticeList(pageable, noticeCategory); + Page result = noticeQueryService.findTrashNoticeList(request); + NoticeResponseDTO.NoticeList response = NoticeConverter.toNoticeList(result); + return DefaultResponse.ok(response); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/repository/NoticeRepository.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/repository/NoticeRepository.java index f3d63c6..8194d07 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/repository/NoticeRepository.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/repository/NoticeRepository.java @@ -39,5 +39,16 @@ Page findNoticeListByNoticeCategoryAndKeyword( Pageable pageable ); + @Query(""" + SELECT n FROM Notice n + WHERE n.noticeCategory = :noticeCategory + AND n.deletedAt IS NOT NULL + ORDER BY n.isPinned DESC, n.deletedAt ASC + """) + Page findTrashNoticeListByNoticeCategory( + @Param("noticeCategory") NoticeCategory noticeCategory, + Pageable pageable + ); + Optional findNoticeById(Long noticeId); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryService.java index 7b42a24..bc6bd86 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryService.java @@ -7,6 +7,6 @@ public interface NoticeQueryService { Page findNoticeList(NoticeRequestDTO.FindNoticeList request); Page findNoticeListByKeyword(NoticeRequestDTO.FindNoticeListByKeyword request); - + Page findTrashNoticeList(NoticeRequestDTO.FindNoticeList request); Notice findNoticeDetail(NoticeRequestDTO.FindNoticeDetail request); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryServiceImpl.java index bdd2ede..4846417 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/query/NoticeQueryServiceImpl.java @@ -34,6 +34,13 @@ public Page findNoticeListByKeyword(NoticeRequestDTO.FindNoticeListByKey request.noticeCategory(), request.keyword(), request.pageable()); } + @Override + public Page findTrashNoticeList(NoticeRequestDTO.FindNoticeList request) { + return noticeRepository.findTrashNoticeListByNoticeCategory( + request.noticeCategory(), request.pageable() + ); + } + @Override public Notice findNoticeDetail(NoticeRequestDTO.FindNoticeDetail request) { From 24f2f534b2f5fac66debe158a9218604cf83f564 Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 7 Jul 2025 18:24:04 +0900 Subject: [PATCH 4/9] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EC=96=B4=EB=93=9C?= =?UTF-8?q?=EB=AF=BC=20API=20=EA=B4=80=EB=A0=A8=20=EC=9D=B8=EA=B0=80=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../withtime/be/withtimebe/global/security/SecurityConfig.java | 1 + .../be/withtimebe/global/security/domain/CustomUserDetails.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java b/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java index f0ea5e4..bb86e4c 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java +++ b/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java @@ -50,6 +50,7 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(request -> request .requestMatchers(allowUrl).permitAll() + .requestMatchers(API_PREFIX + "/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .addFilterBefore(jsonLoginFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class) diff --git a/src/main/java/org/withtime/be/withtimebe/global/security/domain/CustomUserDetails.java b/src/main/java/org/withtime/be/withtimebe/global/security/domain/CustomUserDetails.java index eecc7d0..ed57c06 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/security/domain/CustomUserDetails.java +++ b/src/main/java/org/withtime/be/withtimebe/global/security/domain/CustomUserDetails.java @@ -28,7 +28,7 @@ public String getPassword() { @Override public Collection getAuthorities() { - return Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")); + return Collections.singleton(new SimpleGrantedAuthority("ROLE_" + member.getRole().name())); } public String getProviderType() { From 45950decc4061649fc79481c0c2e9f5d989b825a Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 7 Jul 2025 18:32:49 +0900 Subject: [PATCH 5/9] =?UTF-8?q?=E2=9C=A8=20=20feat:=20[ADMIN]=20=EA=B3=B5?= =?UTF-8?q?=EC=A7=80=EC=82=AC=ED=95=AD=20=EC=83=9D=EC=84=B1=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/NoticeCommandController.java | 50 +++++++++++++++++++ .../notice/converter/NoticeConverter.java | 10 ++++ .../notice/dto/request/NoticeRequestDTO.java | 11 ++++ .../service/command/NoticeCommandService.java | 9 ++++ .../command/NoticeCommandServiceImpl.java | 29 +++++++++++ 5 files changed, 109 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java new file mode 100644 index 0000000..f579243 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java @@ -0,0 +1,50 @@ +package org.withtime.be.withtimebe.domain.notice.controller.command; + +import org.namul.api.payload.response.DefaultResponse; +import org.springframework.web.bind.annotation.DeleteMapping; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.notice.converter.NoticeConverter; +import org.withtime.be.withtimebe.domain.notice.dto.request.NoticeRequestDTO; +import org.withtime.be.withtimebe.domain.notice.dto.response.NoticeResponseDTO; +import org.withtime.be.withtimebe.domain.notice.entity.Notice; +import org.withtime.be.withtimebe.domain.notice.service.command.NoticeCommandService; +import org.withtime.be.withtimebe.global.security.annotation.AuthenticatedMember; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/admin/notices") +public class NoticeCommandController { + + private final NoticeCommandService noticeCommandService; + + @Operation(summary = "공지사항 생성 API Only Admin by 피우", description = "공지사항 생성 API입니다. 어드민만 사용 가능합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공입니다."), + @ApiResponse(responseCode = "403", + description = """ + - COMMON403 : "Admin 권한이 없음을 의미합니다." + """) + }) + @PostMapping + public DefaultResponse createNotice( + @RequestBody @Valid NoticeRequestDTO.CreateNotice request, + @AuthenticatedMember Member member + ) { + Notice result = noticeCommandService.createNotice(request, member); + NoticeResponseDTO.Notice response = NoticeConverter.toNotice(result); + return DefaultResponse.created(response); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/converter/NoticeConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/converter/NoticeConverter.java index f933b72..5d7276c 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/converter/NoticeConverter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/converter/NoticeConverter.java @@ -100,4 +100,14 @@ public static NoticeResponseDTO.NoticeDetail toNoticeDetail(Notice notice, Membe .createdAt(notice.getCreatedAt()) .build(); } + + public static Notice toNoticeEntity(NoticeRequestDTO.CreateNotice request, Member member) { + + return Notice.builder() + .member(member) + .title(request.title()) + .content(request.content()) + .isPinned(request.isPinned()) + .build(); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/request/NoticeRequestDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/request/NoticeRequestDTO.java index 89eaeab..184298c 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/request/NoticeRequestDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/request/NoticeRequestDTO.java @@ -4,6 +4,8 @@ import org.withtime.be.withtimebe.domain.member.entity.Member; import org.withtime.be.withtimebe.domain.notice.entity.enums.NoticeCategory; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Builder; public record NoticeRequestDTO() { @@ -26,4 +28,13 @@ public record FindNoticeDetail( Long noticeId, Member member ) {} + + public record CreateNotice( + @NotBlank(message = "제목을 입력해주세요") + String title, + @NotBlank(message = "내용을 입력해주세요") + String content, + @NotNull(message = "상단 고정 여부를 결정해주세요") + Boolean isPinned + ) {} } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java new file mode 100644 index 0000000..26ccb7e --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java @@ -0,0 +1,9 @@ +package org.withtime.be.withtimebe.domain.notice.service.command; + +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.notice.dto.request.NoticeRequestDTO; +import org.withtime.be.withtimebe.domain.notice.entity.Notice; + +public interface NoticeCommandService { + Notice createNotice(NoticeRequestDTO.CreateNotice request, Member member); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java new file mode 100644 index 0000000..dcb28da --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java @@ -0,0 +1,29 @@ +package org.withtime.be.withtimebe.domain.notice.service.command; + +import java.time.LocalDateTime; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.notice.converter.NoticeConverter; +import org.withtime.be.withtimebe.domain.notice.dto.request.NoticeRequestDTO; +import org.withtime.be.withtimebe.domain.notice.entity.Notice; +import org.withtime.be.withtimebe.domain.notice.repository.NoticeRepository; +import org.withtime.be.withtimebe.global.error.code.NoticeErrorCode; +import org.withtime.be.withtimebe.global.error.exception.NoticeException; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = false) +public class NoticeCommandServiceImpl implements NoticeCommandService{ + + private final NoticeRepository noticeRepository; + + @Override + public Notice createNotice(NoticeRequestDTO.CreateNotice request, Member member) { + Notice notice = NoticeConverter.toNoticeEntity(request, member); + return noticeRepository.save(notice); + } +} From 042bd5703f2da7d413253f4bdde7dd3257389d3a Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 7 Jul 2025 18:36:24 +0900 Subject: [PATCH 6/9] =?UTF-8?q?=E2=9C=A8=20=20feat:=20[ADMIN]=20=EA=B3=B5?= =?UTF-8?q?=EC=A7=80=EC=82=AC=ED=95=AD=20=EC=88=98=EC=A0=95=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/NoticeCommandController.java | 19 +++++++++++++++++++ .../notice/dto/request/NoticeRequestDTO.java | 11 +++++++++++ .../domain/notice/entity/Notice.java | 7 +++++++ .../service/command/NoticeCommandService.java | 1 + .../command/NoticeCommandServiceImpl.java | 10 ++++++++++ 5 files changed, 48 insertions(+) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java index f579243..4a916d3 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java @@ -47,4 +47,23 @@ public DefaultResponse createNotice( NoticeResponseDTO.Notice response = NoticeConverter.toNotice(result); return DefaultResponse.created(response); } + + @Operation(summary = "공지사항 수정 API Only Admin by 피우", description = "공지사항 수정 API입니다. 어드민만 사용 가능합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공입니다."), + @ApiResponse(responseCode = "403", + description = """ + - COMMON403 : "Admin 권한이 없음을 의미합니다." + """), + @ApiResponse(responseCode = "404", + description = """ + - NOTICE404_2 : "해당하는 공지사항을 찾을 수 없습니다." + """) + }) + @PutMapping("/{noticeId}") + public DefaultResponse updateNotice(@RequestBody @Valid NoticeRequestDTO.UpdateNotice request) { + Notice result = noticeCommandService.updateNotice(request); + NoticeResponseDTO.Notice response = NoticeConverter.toNotice(result); + return DefaultResponse.ok(response); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/request/NoticeRequestDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/request/NoticeRequestDTO.java index 184298c..620fbcd 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/request/NoticeRequestDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/request/NoticeRequestDTO.java @@ -37,4 +37,15 @@ public record CreateNotice( @NotNull(message = "상단 고정 여부를 결정해주세요") Boolean isPinned ) {} + + public record UpdateNotice ( + @NotNull(message = "공지사항의 식별자 값을 입력해주세요") + Long noticeId, + @NotBlank(message = "제목을 입력해주세요") + String title, + @NotBlank(message = "내용을 입력해주세요") + String content, + @NotNull(message = "상단 고정 여부를 결정해주세요") + Boolean isPinned + ) {} } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/entity/Notice.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/entity/Notice.java index 30ab2ac..62170df 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/entity/Notice.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/entity/Notice.java @@ -3,6 +3,7 @@ import jakarta.persistence.*; import lombok.*; import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.notice.dto.request.NoticeRequestDTO; import org.withtime.be.withtimebe.domain.notice.entity.enums.NoticeCategory; import org.withtime.be.withtimebe.global.common.BaseEntity; @@ -40,4 +41,10 @@ public class Notice extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; + + public void updateFields(NoticeRequestDTO.UpdateNotice updateNotice) { + this.title = updateNotice.title(); + this.content = updateNotice.content(); + this.isPinned = updateNotice.isPinned(); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java index 26ccb7e..f0934ea 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java @@ -6,4 +6,5 @@ public interface NoticeCommandService { Notice createNotice(NoticeRequestDTO.CreateNotice request, Member member); + Notice updateNotice(NoticeRequestDTO.UpdateNotice request); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java index dcb28da..2567de9 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java @@ -26,4 +26,14 @@ public Notice createNotice(NoticeRequestDTO.CreateNotice request, Member member) Notice notice = NoticeConverter.toNoticeEntity(request, member); return noticeRepository.save(notice); } + + @Override + public Notice updateNotice(NoticeRequestDTO.UpdateNotice request) { + Notice notice = noticeRepository.findNoticeById(request.noticeId()) + .orElseThrow(() -> new NoticeException(NoticeErrorCode.NOTICE_NOT_FOUND)); + + notice.updateFields(request); + + return notice; + } } From 790efb328fd5ec8574378183177a3640ce996c88 Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 7 Jul 2025 18:38:29 +0900 Subject: [PATCH 7/9] =?UTF-8?q?=E2=9C=A8=20=20feat:=20[ADMIN]=20=EA=B3=B5?= =?UTF-8?q?=EC=A7=80=EC=82=AC=ED=95=AD=20=EC=82=AD=EC=A0=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/NoticeCommandController.java | 18 ++++++++++++++++++ .../domain/notice/entity/Notice.java | 4 ++++ .../service/command/NoticeCommandService.java | 1 + .../command/NoticeCommandServiceImpl.java | 8 ++++++++ 4 files changed, 31 insertions(+) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java index 4a916d3..845df08 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java @@ -66,4 +66,22 @@ public DefaultResponse updateNotice(@RequestBody @Vali NoticeResponseDTO.Notice response = NoticeConverter.toNotice(result); return DefaultResponse.ok(response); } + + @Operation(summary = "공지사항 삭제 API Only Admin by 피우", description = "공지사항 삭제 API입니다. 어드민만 사용 가능합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "성공입니다."), + @ApiResponse(responseCode = "403", + description = """ + - COMMON403 : "Admin 권한이 없음을 의미합니다." + """), + @ApiResponse(responseCode = "404", + description = """ + - NOTICE404_2 : "해당하는 공지사항을 찾을 수 없습니다." + """) + }) + @DeleteMapping("/{noticeId}") + public DefaultResponse softDeleteNotice(@PathVariable Long noticeId) { + noticeCommandService.softDeleteNotice(noticeId); + return DefaultResponse.noContent(); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/entity/Notice.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/entity/Notice.java index 62170df..e343aa7 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/entity/Notice.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/entity/Notice.java @@ -47,4 +47,8 @@ public void updateFields(NoticeRequestDTO.UpdateNotice updateNotice) { this.content = updateNotice.content(); this.isPinned = updateNotice.isPinned(); } + + public void updateDeletedAt(LocalDateTime deletedAt) { + this.deletedAt = deletedAt; + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java index f0934ea..68a28a1 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java @@ -7,4 +7,5 @@ public interface NoticeCommandService { Notice createNotice(NoticeRequestDTO.CreateNotice request, Member member); Notice updateNotice(NoticeRequestDTO.UpdateNotice request); + void softDeleteNotice(Long noticeId); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java index 2567de9..1a22a2e 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java @@ -36,4 +36,12 @@ public Notice updateNotice(NoticeRequestDTO.UpdateNotice request) { return notice; } + + @Override + public void softDeleteNotice(Long noticeId) { + Notice notice = noticeRepository.findNoticeById(noticeId) + .orElseThrow(() -> new NoticeException(NoticeErrorCode.NOTICE_NOT_FOUND)); + + notice.updateDeletedAt(LocalDateTime.now()); + } } From 71e3ba9bfc1f536cb0c51e566e2a22847cc46847 Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 7 Jul 2025 18:40:56 +0900 Subject: [PATCH 8/9] =?UTF-8?q?=E2=9C=A8=20=20feat:=20[ADMIN]=20=EA=B3=B5?= =?UTF-8?q?=EC=A7=80=EC=82=AC=ED=95=AD=20=EC=82=AD=EC=A0=9C=20=EB=90=98?= =?UTF-8?q?=EB=8F=8C=EB=A6=AC=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/NoticeCommandController.java | 19 +++++++++++++++++++ .../service/command/NoticeCommandService.java | 1 + .../command/NoticeCommandServiceImpl.java | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java index 845df08..ceed76d 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java @@ -84,4 +84,23 @@ public DefaultResponse softDeleteNotice(@PathVariable Long noticeId) { noticeCommandService.softDeleteNotice(noticeId); return DefaultResponse.noContent(); } + + @Operation(summary = "삭제한 공지사항 되돌리기 API Only Admin by 피우", description = "삭제한 공지사항을 되돌리는 API입니다. 어드민만 사용 가능합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공입니다."), + @ApiResponse(responseCode = "403", + description = """ + - COMMON403 : "Admin 권한이 없음을 의미합니다." + """), + @ApiResponse(responseCode = "404", + description = """ + - NOTICE404_2 : "해당하는 공지사항을 찾을 수 없습니다." + """) + }) + @PatchMapping("/{noticeId}") + public DefaultResponse recoverDeletedNotice(@PathVariable Long noticeId) { + Notice result = noticeCommandService.recoverDeletedNotice(noticeId); + NoticeResponseDTO.Notice response = NoticeConverter.toNotice(result); + return DefaultResponse.ok(response); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java index 68a28a1..7a4a730 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java @@ -8,4 +8,5 @@ public interface NoticeCommandService { Notice createNotice(NoticeRequestDTO.CreateNotice request, Member member); Notice updateNotice(NoticeRequestDTO.UpdateNotice request); void softDeleteNotice(Long noticeId); + Notice recoverDeletedNotice(Long noticeId); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java index 1a22a2e..679232f 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java @@ -44,4 +44,14 @@ public void softDeleteNotice(Long noticeId) { notice.updateDeletedAt(LocalDateTime.now()); } + + @Override + public Notice recoverDeletedNotice(Long noticeId) { + Notice notice = noticeRepository.findNoticeById(noticeId) + .orElseThrow(() -> new NoticeException(NoticeErrorCode.NOTICE_NOT_FOUND)); + + notice.updateDeletedAt(null); + + return notice; + } } From 2d08d4ae0e92665ed05132f4a6b8b24b5e5e0445 Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 10 Jul 2025 20:55:45 +0900 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=90=9B=20fix:=20PathVariable=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notice/controller/command/NoticeCommandController.java | 7 +++++-- .../domain/notice/dto/request/NoticeRequestDTO.java | 2 -- .../notice/service/command/NoticeCommandService.java | 2 +- .../notice/service/command/NoticeCommandServiceImpl.java | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java index ceed76d..32d1261 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/controller/command/NoticeCommandController.java @@ -61,8 +61,11 @@ public DefaultResponse createNotice( """) }) @PutMapping("/{noticeId}") - public DefaultResponse updateNotice(@RequestBody @Valid NoticeRequestDTO.UpdateNotice request) { - Notice result = noticeCommandService.updateNotice(request); + public DefaultResponse updateNotice( + @PathVariable Long noticeId, + @RequestBody @Valid NoticeRequestDTO.UpdateNotice request + ) { + Notice result = noticeCommandService.updateNotice(request, noticeId); NoticeResponseDTO.Notice response = NoticeConverter.toNotice(result); return DefaultResponse.ok(response); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/request/NoticeRequestDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/request/NoticeRequestDTO.java index 620fbcd..b6e28b5 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/request/NoticeRequestDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/dto/request/NoticeRequestDTO.java @@ -39,8 +39,6 @@ public record CreateNotice( ) {} public record UpdateNotice ( - @NotNull(message = "공지사항의 식별자 값을 입력해주세요") - Long noticeId, @NotBlank(message = "제목을 입력해주세요") String title, @NotBlank(message = "내용을 입력해주세요") diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java index 7a4a730..bd68520 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandService.java @@ -6,7 +6,7 @@ public interface NoticeCommandService { Notice createNotice(NoticeRequestDTO.CreateNotice request, Member member); - Notice updateNotice(NoticeRequestDTO.UpdateNotice request); + Notice updateNotice(NoticeRequestDTO.UpdateNotice request, Long noticeId); void softDeleteNotice(Long noticeId); Notice recoverDeletedNotice(Long noticeId); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java index 679232f..ccc28dd 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/notice/service/command/NoticeCommandServiceImpl.java @@ -28,8 +28,8 @@ public Notice createNotice(NoticeRequestDTO.CreateNotice request, Member member) } @Override - public Notice updateNotice(NoticeRequestDTO.UpdateNotice request) { - Notice notice = noticeRepository.findNoticeById(request.noticeId()) + public Notice updateNotice(NoticeRequestDTO.UpdateNotice request, Long noticeId) { + Notice notice = noticeRepository.findNoticeById(noticeId) .orElseThrow(() -> new NoticeException(NoticeErrorCode.NOTICE_NOT_FOUND)); notice.updateFields(request);