-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 공지사항 기능 구현 #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: 공지사항 기능 구현 #18
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
2e16224
chore: QueryDSL 의존성 추가
brothergiven 80ff8d4
feat: 공지사항 정렬 enum 작성
brothergiven 58c3180
feat: 사용자 공지사항 swagger 작성
brothergiven f07b317
feat: 공지사항 목록 조회 QueryDSL 작성
brothergiven 5f9cafe
feat: 공지사항 목록 조회 구현
brothergiven fe7b30e
feat: 공지사항 단건 조회 기능 구현
brothergiven 1b713c5
fix: GetMapping 추가
brothergiven cb1d677
docs: 관리자 공지사항 Swagger 작성
brothergiven 3cf9665
feat: 공지사항 DTO 작성, Builder 수정, 예외코드 추가
brothergiven a682efd
docs: Swagger 수정
brothergiven 96855ac
feat: 사용자 공지사항 기능 구현
brothergiven ce6839d
feat: 퍼블릭 엔드포인트 추가
brothergiven 9f4f609
fix: Page 기본값 설정
brothergiven 4e856fa
fix: isFixed 로직 수정
brothergiven d8756f2
refact: Service 레이어 가독성 수정
brothergiven e79cc4f
feat: isFixed 필드 삭제
brothergiven f30c242
fix: 공지사항 조회 쿼리 변경
brothergiven 52b15c8
feat: 페이지 직렬화 안정화 Config
brothergiven 424445e
Merge branch 'develop' into feat/notification
brothergiven b1e7ab4
fix: 패키지 구조, 예외코드 수정
brothergiven File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,16 @@ | ||
| package Gotcha.common.config; | ||
|
|
||
| import com.querydsl.jpa.impl.JPAQueryFactory; | ||
| import jakarta.persistence.EntityManager; | ||
| import jakarta.persistence.PersistenceContext; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.data.jpa.repository.config.EnableJpaAuditing; | ||
| import org.springframework.data.web.config.EnableSpringDataWebSupport; | ||
|
|
||
| @Configuration | ||
| @EnableJpaAuditing | ||
| @EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO) | ||
| public class JpaConfig { | ||
|
|
||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
142 changes: 142 additions & 0 deletions
142
src/main/java/Gotcha/domain/notification/api/AdminNotificationApi.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| package Gotcha.domain.notification.api; | ||
|
|
||
| import Gotcha.common.jwt.SecurityUserDetails; | ||
| import Gotcha.domain.notification.dto.NotificationReq; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.media.Content; | ||
| import io.swagger.v3.oas.annotations.media.ExampleObject; | ||
| import io.swagger.v3.oas.annotations.parameters.RequestBody; | ||
| 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 org.springframework.http.ResponseEntity; | ||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
|
|
||
| @Tag(name = "[관리자 공지사항 API]", description = "관리자용 공지사항 관련 API") | ||
| public interface AdminNotificationApi { | ||
|
|
||
| @Operation(summary = "공지사항 생성", description = "공지사항 생성 API") | ||
| @ApiResponses({ | ||
| @ApiResponse(responseCode = "200", description = "공지사항 생성 성공", | ||
| content = @Content(mediaType = "application/json", examples = { | ||
| @ExampleObject(value = """ | ||
| { | ||
| "status": "OK", | ||
| "message": "공지사항 생성에 성공했습니다." | ||
| } | ||
| """) | ||
| }) | ||
| ), | ||
| @ApiResponse(responseCode = "400", description = "필드 검증 오류", | ||
| content = @Content(mediaType = "application/json", examples = { | ||
| @ExampleObject(value = """ | ||
| { | ||
| "status": "BAD_REQUEST", | ||
| "message": "필드 검증 오류입니다.", | ||
| "fields": { | ||
| "title": "제목은 필수 입력 사항입니다.", | ||
| "content": "내용은 필수 입력 사항입니다." | ||
| } | ||
| } | ||
| """) | ||
| }) | ||
| ), | ||
| @ApiResponse(responseCode = "403", description = "권한 없음", | ||
| content = @Content(mediaType = "application/json", examples = { | ||
| @ExampleObject(value = """ | ||
| { | ||
| "status": "FORBIDDEN", | ||
| "message": "권한이 없습니다." | ||
| } | ||
| """) | ||
| }) | ||
| ) | ||
| }) | ||
| ResponseEntity<?> createNotification( | ||
| @Valid @RequestBody NotificationReq notificationReq, | ||
| @AuthenticationPrincipal SecurityUserDetails userDetails); | ||
|
|
||
| @Operation(summary = "공지사항 수정", description = "공지사항 수정 API") | ||
| @ApiResponses({ | ||
| @ApiResponse(responseCode = "200", description = "공지사항 수정 성공", | ||
| content = @Content(mediaType = "application/json", examples = { | ||
| @ExampleObject(value = """ | ||
| { | ||
| "status": "OK", | ||
| "message": "공지사항 수정에 성공했습니다." | ||
| } | ||
| """) | ||
| }) | ||
| ), | ||
| @ApiResponse(responseCode = "400", description = "필드 검증 오류", | ||
| content = @Content(mediaType = "application/json", examples = { | ||
| @ExampleObject(value = """ | ||
| { | ||
| "status": "BAD_REQUEST", | ||
| "message": "필드 검증 오류입니다.", | ||
| "fields": { | ||
| "title": "제목은 필수 입력 사항입니다.", | ||
| "content": "내용은 필수 입력 사항입니다." | ||
| } | ||
| } | ||
| """) | ||
| }) | ||
| ), | ||
| @ApiResponse(responseCode = "403", description = "작성자 불일치 또는 수정 권한 없음", | ||
| content = @Content(mediaType = "application/json", examples = { | ||
| @ExampleObject(value = """ | ||
| { | ||
| "status": "FORBIDDEN", | ||
| "message": "권한이 없습니다." | ||
| } | ||
| """) | ||
| }) | ||
| ), | ||
| @ApiResponse(responseCode = "404", description = "존재하지 않는 공지사항", | ||
| content = @Content(mediaType = "application/json", examples = { | ||
| @ExampleObject(value = """ | ||
| { | ||
| "status": "NOT_FOUND", | ||
| "message": "존재하지 않는 공지사항입니다." | ||
| } | ||
| """) | ||
| }) | ||
| ) | ||
| }) | ||
| ResponseEntity<?> updateNotification( | ||
| @PathVariable(value = "notificationId") Long notificationId, | ||
| @Valid @RequestBody NotificationReq notificationReq, | ||
| @AuthenticationPrincipal SecurityUserDetails userDetails); | ||
|
|
||
| @Operation(summary = "공지사항 삭제", description = "공지사항 삭제 API") | ||
| @ApiResponses({ | ||
| @ApiResponse(responseCode = "204", description = "공지사항 삭제 성공"), | ||
| @ApiResponse(responseCode = "403", description = "작성자 불일치 또는 삭제 권한 없음", | ||
| content = @Content(mediaType = "application/json", examples = { | ||
| @ExampleObject(value = """ | ||
| { | ||
| "status": "FORBIDDEN", | ||
| "message": "권한이 없습니다." | ||
| } | ||
| """) | ||
| }) | ||
| ), | ||
| @ApiResponse(responseCode = "404", description = "존재하지 않는 공지사항", | ||
| content = @Content(mediaType = "application/json", examples = { | ||
| @ExampleObject(value = """ | ||
| { | ||
| "status": "NOT_FOUND", | ||
| "message": "존재하지 않는 공지사항입니다." | ||
| } | ||
| """) | ||
| }) | ||
| ) | ||
| }) | ||
| ResponseEntity<?> deleteNotification( | ||
| @PathVariable(value = "notificationId") Long notificationId, | ||
| @AuthenticationPrincipal SecurityUserDetails userDetails); | ||
|
|
||
|
|
||
| } |
117 changes: 117 additions & 0 deletions
117
src/main/java/Gotcha/domain/notification/api/NotificationApi.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| package Gotcha.domain.notification.api; | ||
|
|
||
|
|
||
| import Gotcha.domain.notification.dto.NotificationSortType; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.media.Content; | ||
| import io.swagger.v3.oas.annotations.media.ExampleObject; | ||
| 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.constraints.Min; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
| import org.springframework.web.bind.annotation.RequestParam; | ||
|
|
||
| @Tag(name = "[공지사항 API]", description = "공지사항 관련 API") | ||
| public interface NotificationApi { | ||
|
|
||
| @Operation(summary = "공지사항 목록", description = "공지사항 목록을 조회하는 API. Keyword로 검색 및 정렬 가능.") | ||
| @ApiResponses({ | ||
| @ApiResponse( | ||
| responseCode = "200", description = "공지사항 목록 조회 성공", | ||
| content = @Content(mediaType = "application/json", examples = { | ||
| @ExampleObject(value = """ | ||
| { | ||
| "totalPages": 1, | ||
| "totalElements": 3, | ||
| "first": true, | ||
| "last": true, | ||
| "size": 10, | ||
| "content": [ | ||
| { | ||
| "notificationId": 2, | ||
| "title" : "걍 공지사항이다", | ||
| "createdAt": "2025-03-27T16:13:32", | ||
| "writer" : "묘묘" | ||
| }, | ||
| { | ||
| "notificationId": 1, | ||
| "title" : "서버 점검 안내", | ||
| "createdAt": "2025-03-27T16:05:35", | ||
| "writer": "루루" | ||
| }, | ||
| { | ||
| "notificationId": 0, | ||
| "title": "이달의 인공지능 선정 결과", | ||
| "createdAt" : "2001-11-16T16:02:26", | ||
| "writer": "킹형준" | ||
| } | ||
| ], | ||
| "pageable": { | ||
| "pageNumber": 0, | ||
| "pageSize": 10, | ||
| "sort": { | ||
| "empty": false, | ||
| "unsorted": false, | ||
| "sorted": true | ||
| }, | ||
| "offset": 0, | ||
| "unpaged": false, | ||
| "paged": true | ||
| }, | ||
| "numberOfElements": 3, | ||
| "sort": { | ||
| "empty": false, | ||
| "sorted": true, | ||
| "unsorted": false | ||
| }, | ||
| "number": 0, | ||
| "empty": false | ||
| } | ||
| """) | ||
| }) | ||
| ) | ||
|
|
||
| }) | ||
| ResponseEntity<?> getNotifications(@RequestParam(value = "keyword", required = false) String keyword, | ||
| @RequestParam(value = "page",defaultValue = "0") @Min(0) Integer page, | ||
| @RequestParam(value = "sort", defaultValue = "DATE_DESC") NotificationSortType sort); | ||
|
|
||
|
|
||
|
|
||
| @Operation(summary = "공지사항 조회", description = "공지사항 ID를 받아 해당 공지사항을 조회하는 API") | ||
| @ApiResponses({ | ||
| @ApiResponse( | ||
| responseCode = "200", description = "공지사항 조회 성공", | ||
| content = @Content(mediaType = "application/json", examples = { | ||
| @ExampleObject(value = """ | ||
| { | ||
| "title": "걍 공지사항이다", | ||
| "content": "걍 공지사항이다 인마", | ||
| "createdAt": "2025-03-27T16:13:32", | ||
| "modifiedAt": "2025-11-16T16:13:32", | ||
| "writer": "묘묘" | ||
| } | ||
| """ | ||
| ) | ||
| }) | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "404", description = "존재하지 않는 공지사항", | ||
| content = @Content(mediaType = "application/json", examples = { | ||
| @ExampleObject(value = """ | ||
| { | ||
| "status": "NOT_FOUND", | ||
| "message": "존재하지 않는 공지사항입니다." | ||
| } | ||
| """ | ||
| ) | ||
| }) | ||
| ) | ||
|
|
||
| }) | ||
| ResponseEntity<?> getNotificationById(@PathVariable(value = "notificationId") Long notificationId); | ||
|
|
||
|
|
||
| } |
51 changes: 51 additions & 0 deletions
51
src/main/java/Gotcha/domain/notification/controller/AdminNotificationController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package Gotcha.domain.notification.controller; | ||
|
|
||
| import Gotcha.common.api.SuccessRes; | ||
| import Gotcha.common.jwt.SecurityUserDetails; | ||
| import Gotcha.domain.notification.api.AdminNotificationApi; | ||
| import Gotcha.domain.notification.dto.NotificationReq; | ||
| import Gotcha.domain.notification.entity.Notification; | ||
| import Gotcha.domain.notification.service.AdminNotificationService; | ||
| import io.swagger.v3.oas.annotations.parameters.RequestBody; | ||
| import jakarta.validation.Valid; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api/v1/admin/notification") | ||
| public class AdminNotificationController implements AdminNotificationApi { | ||
|
|
||
| private final AdminNotificationService adminNotificationService; | ||
|
|
||
| @Override | ||
| @PostMapping | ||
| public ResponseEntity<?> createNotification( | ||
| @Valid @RequestBody NotificationReq notificationReq, | ||
| @AuthenticationPrincipal SecurityUserDetails userDetails){ | ||
| adminNotificationService.createNotification(notificationReq, userDetails.getId()); | ||
| return ResponseEntity.ok(SuccessRes.from("공지사항 생성에 성공했습니다.")); | ||
| } | ||
|
|
||
| @Override | ||
| @PutMapping("/{notificationId}") | ||
| public ResponseEntity<?> updateNotification( | ||
| @PathVariable(value = "notificationId") Long notificationId, | ||
| @Valid @RequestBody NotificationReq notificationReq, | ||
| @AuthenticationPrincipal SecurityUserDetails userDetails){ | ||
| adminNotificationService.updateNotification(notificationReq, notificationId, userDetails.getId()); | ||
| return ResponseEntity.ok(SuccessRes.from("공지사항 수정에 성공했습니다.")); | ||
| } | ||
|
|
||
| @Override | ||
| @DeleteMapping("/{notificationId}") | ||
| public ResponseEntity<?> deleteNotification( | ||
| @PathVariable(value = "notificationId") Long notificationId, | ||
| @AuthenticationPrincipal SecurityUserDetails userDetails){ | ||
| adminNotificationService.deleteNotification(notificationId, userDetails.getId()); | ||
| return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); | ||
| } | ||
| } | ||
39 changes: 39 additions & 0 deletions
39
src/main/java/Gotcha/domain/notification/controller/NotificationController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package Gotcha.domain.notification.controller; | ||
|
|
||
| import Gotcha.domain.notification.api.NotificationApi; | ||
| import Gotcha.domain.notification.dto.NotificationRes; | ||
| import Gotcha.domain.notification.dto.NotificationSortType; | ||
| import Gotcha.domain.notification.dto.NotificationSummaryRes; | ||
| import Gotcha.domain.notification.service.NotificationService; | ||
| import jakarta.validation.constraints.Min; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.data.domain.Page; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api/v1/notifications") | ||
| public class NotificationController implements NotificationApi { | ||
|
|
||
| private final NotificationService notificationService; | ||
|
|
||
|
|
||
| @Override | ||
| @GetMapping | ||
| public ResponseEntity<?> getNotifications(@RequestParam(value = "keyword", required = false) String keyword, | ||
| @RequestParam(value = "page", defaultValue = "0") @Min(0) Integer page, | ||
| @RequestParam(value = "sort", defaultValue = "DATE_DESC") NotificationSortType sort){ | ||
| Page<NotificationSummaryRes> notifications = notificationService.getNotifications(keyword, page, sort); | ||
|
|
||
| return ResponseEntity.status(HttpStatus.OK).body(notifications); | ||
| } | ||
|
|
||
| @Override | ||
| @GetMapping("/{notificationId}") | ||
| public ResponseEntity<?> getNotificationById(@PathVariable(value = "notificationId") Long notificationId) { | ||
| NotificationRes notificationRes = notificationService.getNotificationsById(notificationId); | ||
| return ResponseEntity.status(HttpStatus.OK).body(notificationRes); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AuthenticationPrincipal 애노테이션을 통해 관리자 권한 인증을 하고, 정확한 커스텀 예외 클래스를 던져주는 건 좋은 로직인 것 같습니다!!
권한 인증은 다음과 같은 방법도 있으니 추후 참고해보셔도 좋을 듯 합니다!
@PreAuthorize