diff --git a/src/main/java/life/mosu/mosuserver/MosuServerApplication.java b/src/main/java/life/mosu/mosuserver/MosuServerApplication.java index 0df49d3b..31446349 100644 --- a/src/main/java/life/mosu/mosuserver/MosuServerApplication.java +++ b/src/main/java/life/mosu/mosuserver/MosuServerApplication.java @@ -2,9 +2,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.context.properties.ConfigurationPropertiesScan; -@ConfigurationPropertiesScan @SpringBootApplication public class MosuServerApplication { diff --git a/src/main/java/life/mosu/mosuserver/application/faq/FaqAttachmentService.java b/src/main/java/life/mosu/mosuserver/application/faq/FaqAttachmentService.java index 8307f04b..2c03fed0 100644 --- a/src/main/java/life/mosu/mosuserver/application/faq/FaqAttachmentService.java +++ b/src/main/java/life/mosu/mosuserver/application/faq/FaqAttachmentService.java @@ -60,7 +60,8 @@ public List toAttachmentResponses(FaqJpaEntity f s3Service.getPreSignedUrl( attachment.getS3Key(), Duration.ofMinutes(s3Properties.getPresignedUrlExpirationMinutes()) - ) + ), + attachment.getS3Key() )) .toList(); } diff --git a/src/main/java/life/mosu/mosuserver/application/faq/FaqService.java b/src/main/java/life/mosu/mosuserver/application/faq/FaqService.java index 1d28201e..3a2c3e8c 100644 --- a/src/main/java/life/mosu/mosuserver/application/faq/FaqService.java +++ b/src/main/java/life/mosu/mosuserver/application/faq/FaqService.java @@ -7,6 +7,7 @@ import life.mosu.mosuserver.global.exception.ErrorCode; import life.mosu.mosuserver.presentation.faq.dto.FaqCreateRequest; import life.mosu.mosuserver.presentation.faq.dto.FaqResponse; +import life.mosu.mosuserver.presentation.faq.dto.FaqUpdateRequest; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -41,6 +42,26 @@ public List getFaqWithAttachments(int page, int size) { .toList(); } + @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) + public FaqResponse getFaqDetail(Long faqId) { + FaqJpaEntity faq = faqRepository.findById(faqId) + .orElseThrow(() -> new CustomRuntimeException(ErrorCode.FAQ_NOT_FOUND)); + + return toFaqResponse(faq); + } + + @Transactional + public void update(FaqUpdateRequest request, Long faqId) { + FaqJpaEntity faqEntity = faqRepository.findById(faqId) + .orElseThrow(() -> new CustomRuntimeException(ErrorCode.FAQ_NOT_FOUND)); + + faqEntity.update(request.question(), request.answer(), request.author()); + faqRepository.save(faqEntity); + + attachmentService.deleteAttachment(faqEntity); + attachmentService.createAttachment(request.attachments(), faqEntity); + } + @Transactional public void deleteFaq(Long faqId) { diff --git a/src/main/java/life/mosu/mosuserver/application/notice/NoticeService.java b/src/main/java/life/mosu/mosuserver/application/notice/NoticeService.java index b8b8f8dd..f2a387f6 100644 --- a/src/main/java/life/mosu/mosuserver/application/notice/NoticeService.java +++ b/src/main/java/life/mosu/mosuserver/application/notice/NoticeService.java @@ -59,7 +59,7 @@ public void deleteNotice(Long noticeId) { public void updateNotice(Long noticeId, NoticeUpdateRequest request) { NoticeJpaEntity noticeEntity = getNoticeOrThrow(noticeId); - noticeEntity.update(request.title(), request.content()); + noticeEntity.update(request.title(), request.content(), request.author()); attachmentService.deleteAttachment(noticeEntity); attachmentService.createAttachment(request.attachments(), noticeEntity); } diff --git a/src/main/java/life/mosu/mosuserver/domain/faq/FaqJpaEntity.java b/src/main/java/life/mosu/mosuserver/domain/faq/FaqJpaEntity.java index 5c0028f6..d67c2df2 100644 --- a/src/main/java/life/mosu/mosuserver/domain/faq/FaqJpaEntity.java +++ b/src/main/java/life/mosu/mosuserver/domain/faq/FaqJpaEntity.java @@ -28,13 +28,24 @@ public class FaqJpaEntity extends BaseTimeEntity { @Column(name = "answer", nullable = false) private String answer; + @Column(name = "author", nullable = false) + private String author; + @Column(name = "user_id", nullable = false) private Long userId; @Builder - public FaqJpaEntity(final String question, final String answer, final Long userId) { + public FaqJpaEntity(final String question, final String answer, final Long userId, + final String author) { this.question = question; this.answer = answer; this.userId = userId; + this.author = author; + } + + public void update(final String question, final String answer, final String author) { + this.question = question; + this.answer = answer; + this.author = author; } } diff --git a/src/main/java/life/mosu/mosuserver/domain/notice/NoticeJpaEntity.java b/src/main/java/life/mosu/mosuserver/domain/notice/NoticeJpaEntity.java index 61b72723..48a9c491 100644 --- a/src/main/java/life/mosu/mosuserver/domain/notice/NoticeJpaEntity.java +++ b/src/main/java/life/mosu/mosuserver/domain/notice/NoticeJpaEntity.java @@ -32,22 +32,29 @@ public class NoticeJpaEntity extends BaseTimeEntity { @Column(name = "user_id", nullable = false) private Long userId; + @Column(name = "author", nullable = false) + private String author; + @Builder public NoticeJpaEntity( final String title, final String content, - final Long userId + final Long userId, + final String author ) { this.title = title; this.content = content; this.userId = userId; + this.author = author; } public void update( final String title, - final String content + final String content, + final String author ) { this.title = title; this.content = content; + this.author = author; } } diff --git a/src/main/java/life/mosu/mosuserver/infra/property/S3Properties.java b/src/main/java/life/mosu/mosuserver/infra/property/S3Properties.java index fb5c3316..40e5826c 100644 --- a/src/main/java/life/mosu/mosuserver/infra/property/S3Properties.java +++ b/src/main/java/life/mosu/mosuserver/infra/property/S3Properties.java @@ -1,18 +1,18 @@ package life.mosu.mosuserver.infra.property; import jakarta.annotation.PostConstruct; -import lombok.Getter; -import lombok.RequiredArgsConstructor; +import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; -@Getter -@RequiredArgsConstructor +@Data +@Component @ConfigurationProperties(prefix = "aws.s3") @Slf4j public class S3Properties { - private final int presignedUrlExpirationMinutes; + private int presignedUrlExpirationMinutes; @PostConstruct public void init() { diff --git a/src/main/java/life/mosu/mosuserver/presentation/faq/FaqController.java b/src/main/java/life/mosu/mosuserver/presentation/faq/FaqController.java index bb6e50ce..f3804aad 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/faq/FaqController.java +++ b/src/main/java/life/mosu/mosuserver/presentation/faq/FaqController.java @@ -1,10 +1,12 @@ package life.mosu.mosuserver.presentation.faq; +import jakarta.validation.Valid; import java.util.List; import life.mosu.mosuserver.application.faq.FaqService; import life.mosu.mosuserver.global.util.ApiResponseWrapper; import life.mosu.mosuserver.presentation.faq.dto.FaqCreateRequest; import life.mosu.mosuserver.presentation.faq.dto.FaqResponse; +import life.mosu.mosuserver.presentation.faq.dto.FaqUpdateRequest; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -12,6 +14,7 @@ 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.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -21,13 +24,15 @@ @RestController @RequiredArgsConstructor @RequestMapping("/faq") -public class FaqController { +public class FaqController implements FaqControllerDocs { private final FaqService faqService; //TODO: 관리자 권한 체크 추가 @PostMapping - public ResponseEntity> create(@RequestBody FaqCreateRequest request) { + // @PreAuthorize("isAuthenticated() and hasRole('ADMIN')") + public ResponseEntity> createFaq( + @Valid @RequestBody FaqCreateRequest request) { faqService.createFaq(request); return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.CREATED, "게시글 등록 성공")); } @@ -41,9 +46,27 @@ public ResponseEntity>> getFaqs( return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "게시글 조회 성공", responses)); } + @GetMapping("/{faqId}") + public ResponseEntity> getFaqDetail( + @PathVariable Long faqId) { + FaqResponse faq = faqService.getFaqDetail(faqId); + return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "게시글 상세 조회 성공", faq)); + } + + @PutMapping("/{faqId}") + // @PreAuthorize("isAuthenticated() and hasRole('ADMIN')") + public ResponseEntity> updateFaq( + @PathVariable Long faqId, + @Valid @RequestBody FaqUpdateRequest request + ) { + faqService.update(request, faqId); + return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "게시글 수정 성공")); + } + //TODO: 관리자 권한 체크 추가 @DeleteMapping("/{faqId}") - public ResponseEntity> delete(@PathVariable Long faqId) { + // @PreAuthorize("isAuthenticated() and hasRole('ADMIN')") + public ResponseEntity> deleteFaq(@PathVariable Long faqId) { faqService.deleteFaq(faqId); return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "게시글 삭제 성공")); } diff --git a/src/main/java/life/mosu/mosuserver/presentation/faq/FaqControllerDocs.java b/src/main/java/life/mosu/mosuserver/presentation/faq/FaqControllerDocs.java new file mode 100644 index 00000000..9547bab5 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/presentation/faq/FaqControllerDocs.java @@ -0,0 +1,80 @@ +package life.mosu.mosuserver.presentation.faq; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.util.List; +import life.mosu.mosuserver.global.util.ApiResponseWrapper; +import life.mosu.mosuserver.presentation.faq.dto.FaqCreateRequest; +import life.mosu.mosuserver.presentation.faq.dto.FaqResponse; +import life.mosu.mosuserver.presentation.faq.dto.FaqUpdateRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +@Tag(name = "Faq API", description = "FAQ 관련 API 명세") +public interface FaqControllerDocs { + + @Operation(summary = "FAQ 등록", description = "관리자가 새로운 FAQ를 등록합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "FAQ 등록 성공") + }) + ResponseEntity> createFaq( + @Parameter(description = "FAQ 등록 요청 데이터") @RequestBody @Valid FaqCreateRequest request + ); + + @Operation(summary = "FAQ 목록 조회", description = "전체 FAQ 목록을 페이징하여 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "FAQ 목록 조회 성공", + content = @Content(mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = FaqResponse.class)) + ) + ) + + }) + ResponseEntity>> getFaqs( + @Parameter(name = "page", description = "페이지 번호", in = ParameterIn.QUERY) + @RequestParam(defaultValue = "0") int page, + + @Parameter(name = "size", description = "페이지 크기", in = ParameterIn.QUERY) + @RequestParam(defaultValue = "10") int size + ); + + @Operation(summary = "FAQ 상세 조회", description = "FAQ ID를 기반으로 상세 내용을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "FAQ 상세 조회 성공") + }) + ResponseEntity> getFaqDetail( + @Parameter(name = "faqId", description = "FAQ ID", in = ParameterIn.PATH) + @PathVariable Long faqId + ); + + @Operation(summary = "FAQ 삭제", description = "FAQ ID를 기반으로 게시글을 삭제합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "FAQ 삭제 성공") + }) + ResponseEntity> deleteFaq( + @Parameter(name = "faqId", description = "삭제할 FAQ ID", in = ParameterIn.PATH) + @PathVariable Long faqId + ); + + @Operation(summary = "FAQ 수정", description = "기존 FAQ 내용을 수정합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "FAQ 수정 성공") + }) + ResponseEntity> updateFaq( + @Parameter(name = "faqId", description = "수정할 FAQ ID", in = ParameterIn.PATH) + @PathVariable Long faqId, + + @Parameter(description = "FAQ 수정 요청 데이터") + @RequestBody @Valid FaqUpdateRequest request + ); +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/presentation/faq/dto/FaqCreateRequest.java b/src/main/java/life/mosu/mosuserver/presentation/faq/dto/FaqCreateRequest.java index aed85b08..6ccb1395 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/faq/dto/FaqCreateRequest.java +++ b/src/main/java/life/mosu/mosuserver/presentation/faq/dto/FaqCreateRequest.java @@ -1,5 +1,6 @@ package life.mosu.mosuserver.presentation.faq.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import java.util.List; import life.mosu.mosuserver.domain.faq.FaqJpaEntity; @@ -7,9 +8,19 @@ public record FaqCreateRequest( + @Schema(description = "FAQ 질문", example = "서비스 이용에 대해 궁금합니다.") @NotNull String question, + + @Schema(description = "FAQ 답변", example = "서비스는 로그인 후 사용 가능합니다.") @NotNull String answer, + + @Schema(description = "작성자 이름", example = "관리자") + @NotNull String author, + + @Schema(description = "작성자 ID (추후 토큰에서 추출 예정)", example = "1") Long userId, + + @Schema(description = "첨부파일 리스트") List attachments ) { @@ -18,9 +29,8 @@ public FaqJpaEntity toEntity() { return FaqJpaEntity.builder() .question(question) .answer(answer) + .author(author) .userId(userId) .build(); } - - -} +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/presentation/faq/dto/FaqResponse.java b/src/main/java/life/mosu/mosuserver/presentation/faq/dto/FaqResponse.java index 9924b8f0..d7d89a46 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/faq/dto/FaqResponse.java +++ b/src/main/java/life/mosu/mosuserver/presentation/faq/dto/FaqResponse.java @@ -1,22 +1,32 @@ package life.mosu.mosuserver.presentation.faq.dto; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; import life.mosu.mosuserver.domain.faq.FaqJpaEntity; public record FaqResponse( - Long id, - String title, - String content, - List attachments + @Schema(description = "FAQ ID") Long id, + @Schema(description = "질문") String question, + @Schema(description = "답변") String answer, + @Schema(description = "작성 일자 (yyyy-MM-dd)") String createdAt, + @Schema(description = "첨부파일 리스트") List attachments ) { + public static FaqResponse of(FaqJpaEntity faq, List attachments) { return new FaqResponse( - faq.getId(), - faq.getQuestion(), - faq.getAnswer(), - attachments + faq.getId(), + faq.getQuestion(), + faq.getAnswer(), + faq.getCreatedAt().substring(0, 10), // 일자만 반환 + attachments ); } - public record AttachmentResponse(String fileName, String url) {} + public record AttachmentResponse( + @Schema(description = "파일명") String fileName, + @Schema(description = "파일 URL") String url, + @Schema(description = "S3 키") String s3Key + ) { + + } } \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/presentation/faq/dto/FaqUpdateRequest.java b/src/main/java/life/mosu/mosuserver/presentation/faq/dto/FaqUpdateRequest.java new file mode 100644 index 00000000..18e1a010 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/presentation/faq/dto/FaqUpdateRequest.java @@ -0,0 +1,24 @@ +package life.mosu.mosuserver.presentation.faq.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import life.mosu.mosuserver.global.util.FileRequest; + +public record FaqUpdateRequest( + + @Schema(description = "수정할 질문 내용", example = "서비스 사용에 대해 알고 싶습니다.") + @NotNull String question, + + @Schema(description = "수정할 답변 내용", example = "로그인 후 전체 메뉴가 노출됩니다.") + @NotNull String answer, + + @Schema(description = "작성자 이름", example = "관리자") + @NotNull String author, + + @Schema(description = "첨부파일 리스트") + List attachments + +) { + +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/presentation/inquiry/dto/InquiryResponse.java b/src/main/java/life/mosu/mosuserver/presentation/inquiry/dto/InquiryResponse.java index 2ed1f1c2..8f58bc28 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/inquiry/dto/InquiryResponse.java +++ b/src/main/java/life/mosu/mosuserver/presentation/inquiry/dto/InquiryResponse.java @@ -18,10 +18,10 @@ public record InquiryResponse( @Schema(description = "작성자", example = "홍길동") String author, - @Schema(description = "문의 상태 (WAITING: 답변 대기, COMPLETED: 답변 완료)", example = "WAITING") + @Schema(description = "문의 상태 (PENDING: 답변 대기, COMPLETED: 답변 완료)", example = "PENDING") InquiryStatus status, - @Schema(description = "문의 등록일", example = "2025-07-10T10:00:00") + @Schema(description = "문의 등록일", example = "2025-07-10") String createdAt ) { diff --git a/src/main/java/life/mosu/mosuserver/presentation/notice/NoticeController.java b/src/main/java/life/mosu/mosuserver/presentation/notice/NoticeController.java index f2cc96d1..b9d51c95 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/notice/NoticeController.java +++ b/src/main/java/life/mosu/mosuserver/presentation/notice/NoticeController.java @@ -24,12 +24,13 @@ @RestController @RequestMapping("/notice") @RequiredArgsConstructor -public class NoticeController { +public class NoticeController implements NoticeControllerDocs { private final NoticeService noticeService; // TODO: 관리자 권한 체크 추가 @PostMapping + // @PreAuthorize("isAuthenticated() and hasRole('ADMIN')") public ResponseEntity> createNotice( @Valid @RequestBody NoticeCreateRequest request) { noticeService.createNotice(request); @@ -54,6 +55,7 @@ public ResponseEntity> getNoticeDetail( // TODO: 관리자 권한 체크 추가 @DeleteMapping("/{noticeId}") + // @PreAuthorize("isAuthenticated() and hasRole('ADMIN')") public ResponseEntity> deleteNotice(@PathVariable Long noticeId) { noticeService.deleteNotice(noticeId); return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "게시글 삭제 성공")); @@ -61,6 +63,7 @@ public ResponseEntity> deleteNotice(@PathVariable Long // TODO: 관리자 권한 체크 추가 @PutMapping("/{noticeId}") + // @PreAuthorize("isAuthenticated() and hasRole('ADMIN')") public ResponseEntity> updateNotice( @PathVariable Long noticeId, @Valid @RequestBody NoticeUpdateRequest request diff --git a/src/main/java/life/mosu/mosuserver/presentation/notice/NoticeControllerDocs.java b/src/main/java/life/mosu/mosuserver/presentation/notice/NoticeControllerDocs.java new file mode 100644 index 00000000..2310bc7d --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/presentation/notice/NoticeControllerDocs.java @@ -0,0 +1,75 @@ +package life.mosu.mosuserver.presentation.notice; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.util.List; +import life.mosu.mosuserver.global.util.ApiResponseWrapper; +import life.mosu.mosuserver.presentation.notice.dto.NoticeCreateRequest; +import life.mosu.mosuserver.presentation.notice.dto.NoticeDetailResponse; +import life.mosu.mosuserver.presentation.notice.dto.NoticeResponse; +import life.mosu.mosuserver.presentation.notice.dto.NoticeUpdateRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +@Tag(name = "Notice API", description = "공지사항 관련 API 명세") +public interface NoticeControllerDocs { + + @Operation(summary = "공지사항 등록", description = "관리자가 새로운 공지사항을 등록합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "공지사항 등록 성공") + }) + ResponseEntity> createNotice( + @Parameter(description = "공지사항 생성에 필요한 정보") @Valid @RequestBody NoticeCreateRequest request + ); + + @Operation(summary = "공지사항 목록 조회", description = "공지사항 리스트를 페이징하여 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "공지사항 목록 조회 성공", + content = @Content(schema = @Schema(implementation = NoticeResponse.class))) + }) + ResponseEntity>> getNotices( + @Parameter(name = "page", description = "페이지 번호", in = ParameterIn.QUERY) + @RequestParam(defaultValue = "0") int page, + @Parameter(name = "size", description = "페이지 크기", in = ParameterIn.QUERY) + @RequestParam(defaultValue = "10") int size + ); + + @Operation(summary = "공지사항 상세 조회", description = "특정 공지사항의 상세 정보를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "공지사항 상세 조회 성공", + content = @Content(schema = @Schema(implementation = NoticeDetailResponse.class))) + }) + ResponseEntity> getNoticeDetail( + @Parameter(name = "noticeId", description = "조회할 공지사항 ID", in = ParameterIn.PATH) + @PathVariable Long noticeId + ); + + @Operation(summary = "공지사항 삭제", description = "특정 공지사항을 삭제합니다. (관리자 권한)") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "공지사항 삭제 성공") + }) + ResponseEntity> deleteNotice( + @Parameter(name = "noticeId", description = "삭제할 공지사항 ID", in = ParameterIn.PATH) + @PathVariable Long noticeId + ); + + @Operation(summary = "공지사항 수정", description = "특정 공지사항을 수정합니다. (관리자 권한)") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "공지사항 수정 성공") + }) + ResponseEntity> updateNotice( + @Parameter(name = "noticeId", description = "수정할 공지사항 ID", in = ParameterIn.PATH) + @PathVariable Long noticeId, + @Parameter(description = "수정할 공지사항 정보") + @Valid @RequestBody NoticeUpdateRequest request + ); +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/presentation/notice/dto/NoticeCreateRequest.java b/src/main/java/life/mosu/mosuserver/presentation/notice/dto/NoticeCreateRequest.java index 6fcd1ef9..459c4bf6 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/notice/dto/NoticeCreateRequest.java +++ b/src/main/java/life/mosu/mosuserver/presentation/notice/dto/NoticeCreateRequest.java @@ -1,5 +1,6 @@ package life.mosu.mosuserver.presentation.notice.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import java.util.List; import life.mosu.mosuserver.domain.notice.NoticeJpaEntity; @@ -7,9 +8,19 @@ public record NoticeCreateRequest( + @Schema(description = "공지사항 제목", example = "서비스 점검 안내") @NotNull String title, + + @Schema(description = "공지사항 본문 내용", example = "6월 30일 오전 2시부터 서비스 점검이 진행됩니다.") @NotNull String content, + + @Schema(description = "작성자 이름", example = "관리자") + @NotNull String author, + + @Schema(description = "작성자 ID (추후 토큰 기반 자동 추출 예정)", example = "1") Long userId, + + @Schema(description = "첨부파일 리스트 (S3 key 포함)") List attachments ) { @@ -19,6 +30,7 @@ public NoticeJpaEntity toEntity() { .title(title) .content(content) .userId(userId) + .author(author) .build(); } -} +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/presentation/notice/dto/NoticeDetailResponse.java b/src/main/java/life/mosu/mosuserver/presentation/notice/dto/NoticeDetailResponse.java index 8a11c2a3..ad7458c9 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/notice/dto/NoticeDetailResponse.java +++ b/src/main/java/life/mosu/mosuserver/presentation/notice/dto/NoticeDetailResponse.java @@ -1,12 +1,27 @@ package life.mosu.mosuserver.presentation.notice.dto; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; import life.mosu.mosuserver.domain.notice.NoticeJpaEntity; public record NoticeDetailResponse( + + @Schema(description = "공지사항 ID", example = "1") Long id, + + @Schema(description = "공지사항 제목", example = "서비스 점검 안내") String title, + + @Schema(description = "공지사항 내용", example = "6월 30일 오전 2시부터 서비스 점검이 진행됩니다.") String content, + + @Schema(description = "작성자 이름", example = "관리자") + String author, + + @Schema(description = "작성일시 (yyyy-MM-dd)", example = "2025-07-08") + String createdAt, + + @Schema(description = "첨부파일 목록") List attachments ) { @@ -18,13 +33,21 @@ public static NoticeDetailResponse of( notice.getId(), notice.getTitle(), notice.getContent(), + notice.getAuthor(), + notice.getCreatedAt(), attachments ); } public record AttachmentDetailResponse( + + @Schema(description = "파일 이름", example = "service_guide.pdf") String fileName, + + @Schema(description = "S3 Presigned URL", example = "https://bucket.s3.amazonaws.com/.../service_guide.pdf") String url, + + @Schema(description = "S3 Key", example = "notices/2025/service_guide.pdf") String s3Key ) { diff --git a/src/main/java/life/mosu/mosuserver/presentation/notice/dto/NoticeResponse.java b/src/main/java/life/mosu/mosuserver/presentation/notice/dto/NoticeResponse.java index 5e42f349..59e2e92f 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/notice/dto/NoticeResponse.java +++ b/src/main/java/life/mosu/mosuserver/presentation/notice/dto/NoticeResponse.java @@ -1,12 +1,27 @@ package life.mosu.mosuserver.presentation.notice.dto; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; import life.mosu.mosuserver.domain.notice.NoticeJpaEntity; public record NoticeResponse( + + @Schema(description = "공지사항 ID", example = "1") Long id, + + @Schema(description = "공지사항 제목", example = "서비스 점검 안내") String title, + + @Schema(description = "공지사항 내용", example = "6월 30일 오전 2시부터 서비스 점검이 진행됩니다.") String content, + + @Schema(description = "작성자 이름", example = "관리자") + String author, + + @Schema(description = "작성일시 (yyyy-MM-dd)", example = "2025-07-08") + String createdAt, + + @Schema(description = "첨부파일 목록") List attachments ) { @@ -15,11 +30,20 @@ public static NoticeResponse of(NoticeJpaEntity notice, List notice.getId(), notice.getTitle(), notice.getContent(), + notice.getAuthor(), + notice.getCreatedAt(), attachments ); } - public record AttachmentResponse(String fileName, String url) { + public record AttachmentResponse( + + @Schema(description = "파일 이름", example = "service_guide.pdf") + String fileName, + + @Schema(description = "S3 Presigned URL", example = "https://bucket.s3.amazonaws.com/.../service_guide.pdf") + String url + ) { } } \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/presentation/notice/dto/NoticeUpdateRequest.java b/src/main/java/life/mosu/mosuserver/presentation/notice/dto/NoticeUpdateRequest.java index b874107c..c459cca3 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/notice/dto/NoticeUpdateRequest.java +++ b/src/main/java/life/mosu/mosuserver/presentation/notice/dto/NoticeUpdateRequest.java @@ -1,13 +1,28 @@ package life.mosu.mosuserver.presentation.notice.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import java.util.List; import life.mosu.mosuserver.global.util.FileRequest; public record NoticeUpdateRequest( - @NotNull String title, - @NotNull String content, + + @Schema(description = "공지사항 제목", example = "시스템 점검 일정 변경 안내") + @NotNull + String title, + + @Schema(description = "공지사항 내용", example = "점검 일정이 7월 10일로 변경되었습니다.") + @NotNull + String content, + + @Schema(description = "작성자 이름", example = "관리자") + @NotNull + String author, + + @Schema(description = "작성자 ID (토큰에서 추출 예정)", example = "42") Long userId, + + @Schema(description = "첨부파일 목록") List attachments ) {