Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'

implementation 'org.springframework.boot:spring-boot-starter-mail'

implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"


}

tasks.named('test') {
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/Gotcha/common/config/JpaConfig.java
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 {

}
3 changes: 2 additions & 1 deletion src/main/java/Gotcha/common/constants/SecurityConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
public class SecurityConstants {
public static final String[] PUBLIC_ENDPOINTS = {
"/swagger-resources/**", "/swagger-ui/**", "/v3/api-docs/**",
"/webjars/**", "/error", "/api/v1/auth/**", "/api/v1/users/nickname-check"
"/webjars/**", "/error", "/api/v1/auth/**", "/api/v1/users/nickname-check",
"/api/v1/notifications/**"
};

public static final String[] ADMIN_ENDPOINTS = {"/api/v1/admin/**"};
Expand Down
142 changes: 142 additions & 0 deletions src/main/java/Gotcha/domain/notification/api/AdminNotificationApi.java
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 src/main/java/Gotcha/domain/notification/api/NotificationApi.java
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);


}
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){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AuthenticationPrincipal 애노테이션을 통해 관리자 권한 인증을 하고, 정확한 커스텀 예외 클래스를 던져주는 건 좋은 로직인 것 같습니다!!

권한 인증은 다음과 같은 방법도 있으니 추후 참고해보셔도 좋을 듯 합니다!
@PreAuthorize

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();
}
}
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);
}
}
Loading