From a8ea27c6a512179c07489f811a0d33866baad90c Mon Sep 17 00:00:00 2001 From: heebindev Date: Tue, 29 Sep 2026 10:37:50 +0900 Subject: [PATCH] =?UTF-8?q?[feat]:=20=EA=B3=B5=EC=A7=80=EC=82=AC=ED=95=AD?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notice/controller/NoticeController.java | 20 +++++++ .../notice/converter/NoticeConverter.java | 11 ++++ .../dto/request/NoticeCreateRequestDTO.java | 16 ++++++ .../exception/code/NoticeSuccessCode.java | 5 ++ .../domain/notice/service/NoticeService.java | 3 ++ .../notice/service/NoticeServiceImpl.java | 26 +++++++++ .../notice/service/NoticeServiceImplTest.java | 54 +++++++++++++++++++ 7 files changed, 135 insertions(+) create mode 100644 src/main/java/com/mulmi/backend/domain/notice/dto/request/NoticeCreateRequestDTO.java diff --git a/src/main/java/com/mulmi/backend/domain/notice/controller/NoticeController.java b/src/main/java/com/mulmi/backend/domain/notice/controller/NoticeController.java index ccb2d5e..e6ecc52 100644 --- a/src/main/java/com/mulmi/backend/domain/notice/controller/NoticeController.java +++ b/src/main/java/com/mulmi/backend/domain/notice/controller/NoticeController.java @@ -1,5 +1,6 @@ package com.mulmi.backend.domain.notice.controller; +import com.mulmi.backend.domain.notice.dto.request.NoticeCreateRequestDTO; import com.mulmi.backend.domain.notice.dto.response.NoticeDetailResponseDTO; import com.mulmi.backend.domain.notice.dto.response.NoticePageResponseDTO; import com.mulmi.backend.domain.notice.exception.code.NoticeSuccessCode; @@ -7,12 +8,17 @@ import com.mulmi.backend.global.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -51,4 +57,18 @@ public ApiResponse getNotice( noticeService.getNotice(noticeId) ); } + + // 공지사항 작성 + @Operation(summary = "공지사항 작성") + @PostMapping + @PreAuthorize("hasAnyRole('ASSISTANT', 'WORKER')") + public ApiResponse createNotice( + @AuthenticationPrincipal Long userId, + @RequestBody @Valid NoticeCreateRequestDTO dto + ) { + return ApiResponse.onSuccess( + NoticeSuccessCode.NOTICE_CREATED, + noticeService.createNotice(userId, dto) + ); + } } diff --git a/src/main/java/com/mulmi/backend/domain/notice/converter/NoticeConverter.java b/src/main/java/com/mulmi/backend/domain/notice/converter/NoticeConverter.java index a055e80..9e89f43 100644 --- a/src/main/java/com/mulmi/backend/domain/notice/converter/NoticeConverter.java +++ b/src/main/java/com/mulmi/backend/domain/notice/converter/NoticeConverter.java @@ -1,14 +1,25 @@ package com.mulmi.backend.domain.notice.converter; +import com.mulmi.backend.domain.notice.dto.request.NoticeCreateRequestDTO; import com.mulmi.backend.domain.notice.dto.response.NoticeDetailResponseDTO; import com.mulmi.backend.domain.notice.dto.response.NoticeSummaryResponseDTO; import com.mulmi.backend.domain.notice.entity.Notice; +import com.mulmi.backend.domain.user.entity.User; public class NoticeConverter { private NoticeConverter() { } + public static Notice toNotice(NoticeCreateRequestDTO dto, User author) { + return Notice.builder() + .title(dto.title().trim()) + .content(dto.content().trim()) + .important(dto.important()) + .author(author) + .build(); + } + public static NoticeSummaryResponseDTO toNoticeSummaryResponseDTO(Notice notice) { return new NoticeSummaryResponseDTO( notice.getId(), diff --git a/src/main/java/com/mulmi/backend/domain/notice/dto/request/NoticeCreateRequestDTO.java b/src/main/java/com/mulmi/backend/domain/notice/dto/request/NoticeCreateRequestDTO.java new file mode 100644 index 0000000..bfd3a55 --- /dev/null +++ b/src/main/java/com/mulmi/backend/domain/notice/dto/request/NoticeCreateRequestDTO.java @@ -0,0 +1,16 @@ +package com.mulmi.backend.domain.notice.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record NoticeCreateRequestDTO( + @NotBlank(message = "공지사항 제목은 필수입니다.") + @Size(max = 200, message = "공지사항 제목은 200자를 넘을 수 없습니다.") + String title, + + @NotBlank(message = "공지사항 내용은 필수입니다.") + String content, + + boolean important +) { +} diff --git a/src/main/java/com/mulmi/backend/domain/notice/exception/code/NoticeSuccessCode.java b/src/main/java/com/mulmi/backend/domain/notice/exception/code/NoticeSuccessCode.java index b25fc5b..44b0fdb 100644 --- a/src/main/java/com/mulmi/backend/domain/notice/exception/code/NoticeSuccessCode.java +++ b/src/main/java/com/mulmi/backend/domain/notice/exception/code/NoticeSuccessCode.java @@ -17,6 +17,11 @@ public enum NoticeSuccessCode implements BaseSuccessCode { HttpStatus.OK, "NOTICE200_2", "공지사항을 조회했습니다." + ), + NOTICE_CREATED( + HttpStatus.CREATED, + "NOTICE201_1", + "공지사항을 작성했습니다." ); private final HttpStatus status; diff --git a/src/main/java/com/mulmi/backend/domain/notice/service/NoticeService.java b/src/main/java/com/mulmi/backend/domain/notice/service/NoticeService.java index c1eed3b..8234252 100644 --- a/src/main/java/com/mulmi/backend/domain/notice/service/NoticeService.java +++ b/src/main/java/com/mulmi/backend/domain/notice/service/NoticeService.java @@ -1,5 +1,6 @@ package com.mulmi.backend.domain.notice.service; +import com.mulmi.backend.domain.notice.dto.request.NoticeCreateRequestDTO; import com.mulmi.backend.domain.notice.dto.response.NoticeDetailResponseDTO; import com.mulmi.backend.domain.notice.dto.response.NoticePageResponseDTO; @@ -8,4 +9,6 @@ public interface NoticeService { NoticePageResponseDTO getNotices(Boolean important, int page, int size); NoticeDetailResponseDTO getNotice(Long noticeId); + + NoticeDetailResponseDTO createNotice(Long authorId, NoticeCreateRequestDTO dto); } diff --git a/src/main/java/com/mulmi/backend/domain/notice/service/NoticeServiceImpl.java b/src/main/java/com/mulmi/backend/domain/notice/service/NoticeServiceImpl.java index cb78a8f..49062b2 100644 --- a/src/main/java/com/mulmi/backend/domain/notice/service/NoticeServiceImpl.java +++ b/src/main/java/com/mulmi/backend/domain/notice/service/NoticeServiceImpl.java @@ -1,6 +1,7 @@ package com.mulmi.backend.domain.notice.service; import com.mulmi.backend.domain.notice.converter.NoticeConverter; +import com.mulmi.backend.domain.notice.dto.request.NoticeCreateRequestDTO; import com.mulmi.backend.domain.notice.dto.response.NoticeDetailResponseDTO; import com.mulmi.backend.domain.notice.dto.response.NoticePageResponseDTO; import com.mulmi.backend.domain.notice.dto.response.NoticeSummaryResponseDTO; @@ -8,6 +9,11 @@ import com.mulmi.backend.domain.notice.exception.NoticeException; import com.mulmi.backend.domain.notice.exception.code.NoticeErrorCode; import com.mulmi.backend.domain.notice.repository.NoticeRepository; +import com.mulmi.backend.domain.user.entity.User; +import com.mulmi.backend.domain.user.enums.UserStatus; +import com.mulmi.backend.domain.user.exception.UserException; +import com.mulmi.backend.domain.user.exception.code.UserErrorCode; +import com.mulmi.backend.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -21,6 +27,7 @@ public class NoticeServiceImpl implements NoticeService { private final NoticeRepository noticeRepository; + private final UserRepository userRepository; @Override public NoticePageResponseDTO getNotices(Boolean important, int page, int size) { @@ -45,4 +52,23 @@ public NoticeDetailResponseDTO getNotice(Long noticeId) { return NoticeConverter.toNoticeDetailResponseDTO(notice); } + + @Override + @Transactional + public NoticeDetailResponseDTO createNotice( + Long authorId, + NoticeCreateRequestDTO dto + ) { + User author = userRepository.findById(authorId) + .orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND)); + + if (author.getStatus() != UserStatus.ACTIVE) { + throw new UserException(UserErrorCode.INACTIVE_USER); + } + + Notice notice = NoticeConverter.toNotice(dto, author); + Notice savedNotice = noticeRepository.save(notice); + + return NoticeConverter.toNoticeDetailResponseDTO(savedNotice); + } } diff --git a/src/test/java/com/mulmi/backend/domain/notice/service/NoticeServiceImplTest.java b/src/test/java/com/mulmi/backend/domain/notice/service/NoticeServiceImplTest.java index 561ee46..7dea78b 100644 --- a/src/test/java/com/mulmi/backend/domain/notice/service/NoticeServiceImplTest.java +++ b/src/test/java/com/mulmi/backend/domain/notice/service/NoticeServiceImplTest.java @@ -1,5 +1,6 @@ package com.mulmi.backend.domain.notice.service; +import com.mulmi.backend.domain.notice.dto.request.NoticeCreateRequestDTO; import com.mulmi.backend.domain.notice.dto.response.NoticeDetailResponseDTO; import com.mulmi.backend.domain.notice.dto.response.NoticePageResponseDTO; import com.mulmi.backend.domain.notice.entity.Notice; @@ -9,6 +10,9 @@ import com.mulmi.backend.domain.user.entity.User; import com.mulmi.backend.domain.user.enums.UserRole; import com.mulmi.backend.domain.user.enums.UserStatus; +import com.mulmi.backend.domain.user.exception.UserException; +import com.mulmi.backend.domain.user.exception.code.UserErrorCode; +import com.mulmi.backend.domain.user.repository.UserRepository; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -24,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.given; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) @@ -32,6 +37,9 @@ class NoticeServiceImplTest { @Mock private NoticeRepository noticeRepository; + @Mock + private UserRepository userRepository; + @InjectMocks private NoticeServiceImpl noticeService; @@ -118,6 +126,52 @@ void getNoticeThrowsWhenNoticeDoesNotExist() { .isEqualTo(NoticeErrorCode.NOTICE_NOT_FOUND); } + @Test + void createNoticeSavesNoticeWithAuthenticatedAuthor() { + User author = createAuthor(); + NoticeCreateRequestDTO request = new NoticeCreateRequestDTO( + " 대여실 운영 안내 ", + " 운영 시간을 안내합니다. ", + true + ); + given(userRepository.findById(2L)).willReturn(Optional.of(author)); + given(noticeRepository.save(any(Notice.class))) + .willAnswer(invocation -> invocation.getArgument(0)); + + NoticeDetailResponseDTO result = noticeService.createNotice(2L, request); + + assertThat(result.title()).isEqualTo("대여실 운영 안내"); + assertThat(result.content()).isEqualTo("운영 시간을 안내합니다."); + assertThat(result.important()).isTrue(); + assertThat(result.authorId()).isEqualTo(2L); + verify(noticeRepository).save(any(Notice.class)); + } + + @Test + void createNoticeThrowsWhenAuthorIsInactive() { + User inactiveAuthor = User.builder() + .id(2L) + .loginId("assistant") + .password("encoded-password") + .name("조교") + .email("assistant@example.com") + .phoneNumber("01012345678") + .role(UserRole.ASSISTANT) + .status(UserStatus.SUSPENDED) + .build(); + NoticeCreateRequestDTO request = new NoticeCreateRequestDTO( + "공지사항", + "공지사항 내용", + false + ); + given(userRepository.findById(2L)).willReturn(Optional.of(inactiveAuthor)); + + assertThatThrownBy(() -> noticeService.createNotice(2L, request)) + .isInstanceOf(UserException.class) + .extracting("code") + .isEqualTo(UserErrorCode.INACTIVE_USER); + } + private User createAuthor() { return User.builder() .id(2L)