From f256f4debddf5a2d141e878ea44d3d31a973cca3 Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 10 Jul 2025 18:48:53 +0900 Subject: [PATCH 01/11] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EC=9E=90=EC=A3=BC?= =?UTF-8?q?=20=EB=AC=BB=EB=8A=94=20=EC=A7=88=EB=AC=B8=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20=EA=B2=80=EC=83=89=EC=96=B4?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../faq/controller/FaqQueryController.java | 75 ++++++++++++++++++ .../domain/faq/converter/FaqConverter.java | 76 +++++++++++++++++++ .../domain/faq/dto/request/FaqRequestDTO.java | 22 ++++++ .../faq/dto/response/FaqResponseDTO.java | 24 ++++++ .../be/withtimebe/domain/faq/entity/Faq.java | 55 ++++++++++++++ .../domain/faq/entity/enums/FaqCategory.java | 17 +++++ .../domain/faq/repository/FaqRepository.java | 39 ++++++++++ .../faq/service/query/FaqQueryService.java | 10 +++ .../service/query/FaqQueryServiceImpl.java | 30 ++++++++ .../domain/notice/entity/Notice.java | 2 +- .../global/annotation/SwaggerPageable.java | 35 +++++++++ .../global/error/code/FaqErrorCode.java | 28 +++++++ .../global/error/exception/FaqException.java | 11 +++ .../global/security/SecurityConfig.java | 1 + 14 files changed, 424 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqQueryController.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqConverter.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/faq/dto/response/FaqResponseDTO.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/faq/entity/Faq.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/faq/entity/enums/FaqCategory.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/faq/repository/FaqRepository.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/faq/service/query/FaqQueryService.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/faq/service/query/FaqQueryServiceImpl.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/FaqErrorCode.java create mode 100644 src/main/java/org/withtime/be/withtimebe/global/error/exception/FaqException.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqQueryController.java new file mode 100644 index 0000000..7b6a496 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqQueryController.java @@ -0,0 +1,75 @@ +package org.withtime.be.withtimebe.domain.faq.controller; + +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.faq.converter.FaqConverter; +import org.withtime.be.withtimebe.domain.faq.dto.request.FaqRequestDTO; +import org.withtime.be.withtimebe.domain.faq.dto.response.FaqResponseDTO; +import org.withtime.be.withtimebe.domain.faq.entity.Faq; +import org.withtime.be.withtimebe.domain.faq.service.query.FaqQueryService; +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.AllArgsConstructor; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/v1") +public class FaqQueryController { + + private final FaqQueryService faqQueryService; + + @Operation(summary = "자주 묻는 질문 전체 조회 API by 피우", description = "자주 묻는 질문 전체 조회 API입니다. (검색어 X)") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공입니다."), + @ApiResponse( + responseCode = "404", + description = """ + - FAQ404_1 : 해당하는 질문 유형을 찾을 수 없습니다. + """) + }) + @Parameter(name = "faqCategory", description = "USAGE / ALGORITHM / FEATURE / SCHEDULE / ERROR / ACCOUNT") + @SwaggerPageable + @GetMapping("/faqs") + public DefaultResponse findFaqList( + @PageableDefault(page = 0, size = 10) Pageable pageable, + @RequestParam String faqCategory + ) { + FaqRequestDTO.FindFaqList request = FaqConverter.toFindFaqList(pageable, faqCategory); + Page result = faqQueryService.findFaqList(request); + FaqResponseDTO.FaqList response = FaqConverter.toFaqList(result); + return DefaultResponse.ok(response); + } + + @Operation(summary = "자주 묻는 질문 검색어 전체 조회 API by 피우", description = "자주 묻는 질문 검색어 전체 조회 API입니다. (검색어 O)") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공입니다."), + @ApiResponse( + responseCode = "404", + description = """ + - FAQ404_1 : 해당하는 질문 유형을 찾을 수 없습니다. + """) + }) + @Parameter(name = "faqCategory", description = "USAGE / ALGORITHM / FEATURE / SCHEDULE / ERROR / ACCOUNT") + @SwaggerPageable + @GetMapping("/faqs/search") + public DefaultResponse findFaqListByKeyword( + @PageableDefault(page = 0, size = 10) Pageable pageable, + @RequestParam String keyword, + @RequestParam String faqCategory + ) { + FaqRequestDTO.FindFaqListByKeyword request = FaqConverter.toFindFaqListByKeyword(pageable, keyword, faqCategory); + Page result = faqQueryService.findFaqListByKeyword(request); + FaqResponseDTO.FaqList response = FaqConverter.toFaqList(result); + return DefaultResponse.ok(response); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqConverter.java new file mode 100644 index 0000000..541e4b8 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqConverter.java @@ -0,0 +1,76 @@ +package org.withtime.be.withtimebe.domain.faq.converter; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.withtime.be.withtimebe.domain.faq.dto.request.FaqRequestDTO; +import org.withtime.be.withtimebe.domain.faq.dto.response.FaqResponseDTO; +import org.withtime.be.withtimebe.domain.faq.entity.Faq; +import org.withtime.be.withtimebe.domain.faq.entity.enums.FaqCategory; +import org.withtime.be.withtimebe.global.error.code.FaqErrorCode; +import org.withtime.be.withtimebe.global.error.exception.FaqException; + +public class FaqConverter { + + // Request DTO : 전체 조회 (Controller -> Service) + public static FaqRequestDTO.FindFaqList toFindFaqList(Pageable pageable, String type) { + + FaqCategory faqCategory; + + try { + faqCategory = FaqCategory.valueOf(type); + } catch (IllegalArgumentException e) { + throw new FaqException(FaqErrorCode.FAQ_CATEGORY_NOT_FOUND); + } + + return FaqRequestDTO.FindFaqList.builder() + .pageable(pageable) + .faqCategory(faqCategory) + .build(); + } + + // Request DTO : 검색어 전체 조회 (Controller -> Service) + public static FaqRequestDTO.FindFaqListByKeyword toFindFaqListByKeyword(Pageable pageable, String keyword, String type) { + + FaqCategory faqCategory; + + try { + faqCategory = FaqCategory.valueOf(type); + } catch (IllegalArgumentException e) { + throw new FaqException(FaqErrorCode.FAQ_CATEGORY_NOT_FOUND); + } + + return FaqRequestDTO.FindFaqListByKeyword.builder() + .pageable(pageable) + .keyword(keyword) + .faqCategory(faqCategory) + .build(); + } + + // Response DTO : FaqResponseDTO.FaqList + public static FaqResponseDTO.FaqList toFaqList(Page faqPage) { + + List faqList = faqPage.getContent().stream() + .map(FaqConverter::toFaq) + .toList(); + + return FaqResponseDTO.FaqList.builder() + .faqList(faqList) + .totalPages(faqPage.getTotalPages()) + .currentPage(faqPage.getNumber()) + .currentSize(faqPage.getNumberOfElements()) + .hasNextPage(faqPage.hasNext()) + .build(); + } + + // Response DTO : FaqResponseDTO.Faq + public static FaqResponseDTO.Faq toFaq(Faq faq) { + + return FaqResponseDTO.Faq.builder() + .faqId(faq.getId()) + .title(faq.getTitle()) + .content(faq.getContent()) + .build(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java new file mode 100644 index 0000000..d9189fa --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java @@ -0,0 +1,22 @@ +package org.withtime.be.withtimebe.domain.faq.dto.request; + +import org.springframework.data.domain.Pageable; +import org.withtime.be.withtimebe.domain.faq.entity.enums.FaqCategory; + +import lombok.Builder; + +public class FaqRequestDTO { + + @Builder + public record FindFaqList( + Pageable pageable, // 자주 묻는 질문글 식별자 값 + FaqCategory faqCategory // 질문 유형 + ) {} + + @Builder + public record FindFaqListByKeyword( + Pageable pageable, // 자주 묻는 질문글 식별자 값 + String keyword, // 검색 키워드 + FaqCategory faqCategory // 질문 유형 + ) {} +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/response/FaqResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/response/FaqResponseDTO.java new file mode 100644 index 0000000..1e78a12 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/response/FaqResponseDTO.java @@ -0,0 +1,24 @@ +package org.withtime.be.withtimebe.domain.faq.dto.response; + +import java.util.List; + +import lombok.Builder; + +public class FaqResponseDTO { + + @Builder + public record FaqList( + List faqList, + Integer totalPages, // 전체 페이지 개수 + Integer currentPage, // 현재 페이지 번호 + Integer currentSize, // 현재 페이지의 크기 + Boolean hasNextPage // 다음 페이지 존재 여부 + ) {} + + @Builder + public record Faq( + Long faqId, // 자주 묻는 질문글 식별자 값 + String title, // 질문글 제목 + String content // 내용 + ) {} +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/entity/Faq.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/entity/Faq.java new file mode 100644 index 0000000..54d08d3 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/entity/Faq.java @@ -0,0 +1,55 @@ +package org.withtime.be.withtimebe.domain.faq.entity; + +import java.time.LocalDateTime; + +import org.withtime.be.withtimebe.domain.faq.entity.enums.FaqCategory; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.global.common.BaseEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Table(name = "faq") +public class Faq extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "faq_id") + private Long id; + + @Column(name = "title") + private String title; + + @Column(name = "content", columnDefinition = "TEXT") + private String content; + + @Enumerated(EnumType.STRING) + @Column(name = "faq_category") + private FaqCategory faqCategory; + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/entity/enums/FaqCategory.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/entity/enums/FaqCategory.java new file mode 100644 index 0000000..1b9f2b1 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/entity/enums/FaqCategory.java @@ -0,0 +1,17 @@ +package org.withtime.be.withtimebe.domain.faq.entity.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum FaqCategory { + USAGE("서비스 이용 방법"), + ALGORITHM("추천 알고리즘 관련"), + FEATURE("기능 및 사용성"), + SCHEDULE("예약/일정 관리"), + ERROR("기타/문의 오류 신고"), + ACCOUNT("계정 및 개인정보"); + + private final String label; +} \ No newline at end of file diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/repository/FaqRepository.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/repository/FaqRepository.java new file mode 100644 index 0000000..84f80fa --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/repository/FaqRepository.java @@ -0,0 +1,39 @@ +package org.withtime.be.withtimebe.domain.faq.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.faq.entity.Faq; +import org.withtime.be.withtimebe.domain.faq.entity.enums.FaqCategory; + +public interface FaqRepository extends JpaRepository { + + @Query(""" + SELECT f FROM Faq f + WHERE f.faqCategory = :faqCategory + AND f.deletedAt IS NULL + ORDER BY f.createdAt DESC + """) + Page findFaqListByFaqCategory( + @Param("faqCategory") FaqCategory faqCategory, + Pageable pageable + ); + + @Query(""" + SELECT f FROM Faq f + WHERE f.faqCategory = :faqCategory + AND ( + f.title LIKE %:keyword% + OR f.content LIKE %:keyword% + ) + AND f.deletedAt IS NULL + ORDER BY f.createdAt DESC + """) + Page findFaqListByFaqCategoryAndKeyword( + @Param("faqCategory") FaqCategory faqCategory, + @Param("keyword") String keyword, + Pageable pageable + ); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/query/FaqQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/query/FaqQueryService.java new file mode 100644 index 0000000..6ca80ec --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/query/FaqQueryService.java @@ -0,0 +1,10 @@ +package org.withtime.be.withtimebe.domain.faq.service.query; + +import org.springframework.data.domain.Page; +import org.withtime.be.withtimebe.domain.faq.dto.request.FaqRequestDTO; +import org.withtime.be.withtimebe.domain.faq.entity.Faq; + +public interface FaqQueryService { + Page findFaqList(FaqRequestDTO.FindFaqList request); + Page findFaqListByKeyword(FaqRequestDTO.FindFaqListByKeyword request); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/query/FaqQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/query/FaqQueryServiceImpl.java new file mode 100644 index 0000000..8c6de8d --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/query/FaqQueryServiceImpl.java @@ -0,0 +1,30 @@ +package org.withtime.be.withtimebe.domain.faq.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.faq.dto.request.FaqRequestDTO; +import org.withtime.be.withtimebe.domain.faq.entity.Faq; +import org.withtime.be.withtimebe.domain.faq.repository.FaqRepository; + +import lombok.AllArgsConstructor; + +@Service +@AllArgsConstructor +@Transactional(readOnly = true) +public class FaqQueryServiceImpl implements FaqQueryService { + + private final FaqRepository faqRepository; + + @Override + public Page findFaqList(FaqRequestDTO.FindFaqList request) { + return faqRepository.findFaqListByFaqCategory( + request.faqCategory(), request.pageable()); + } + + @Override + public Page findFaqListByKeyword(FaqRequestDTO.FindFaqListByKeyword request) { + return faqRepository.findFaqListByFaqCategoryAndKeyword( + request.faqCategory(), request.keyword(), request.pageable()); + } +} 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..9f1ffb7 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 @@ -24,7 +24,7 @@ public class Notice extends BaseEntity { @Column(name = "title") private String title; - @Column(name = "content") + @Column(name = "content", columnDefinition = "TEXT") private String content; @Column(name = "notice_category") 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..130955f --- /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 { +} \ No newline at end of file diff --git a/src/main/java/org/withtime/be/withtimebe/global/error/code/FaqErrorCode.java b/src/main/java/org/withtime/be/withtimebe/global/error/code/FaqErrorCode.java new file mode 100644 index 0000000..b14965a --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/error/code/FaqErrorCode.java @@ -0,0 +1,28 @@ +package org.withtime.be.withtimebe.global.error.code; + +import org.namul.api.payload.code.BaseErrorCode; +import org.namul.api.payload.code.dto.supports.DefaultResponseErrorReasonDTO; +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public enum FaqErrorCode implements BaseErrorCode { + + FAQ_CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "FAQ404_1", "해당하는 질문 유형을 찾을 수 없습니다."), + FAQ_NOT_FOUND(HttpStatus.NOT_FOUND, "FAQ404_2", "해당하는 질문을 찾을 수 없습니다."), + ; + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public DefaultResponseErrorReasonDTO 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/FaqException.java b/src/main/java/org/withtime/be/withtimebe/global/error/exception/FaqException.java new file mode 100644 index 0000000..b9b4bd1 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/error/exception/FaqException.java @@ -0,0 +1,11 @@ +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 FaqException extends ServerApplicationException { + + public FaqException(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..48aad9b 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 + "/faqs/**", "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**" From 9c5c67f328862fd31eb9f8aeb405ac1a567884fa Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 10 Jul 2025 20:29:10 +0900 Subject: [PATCH 02/11] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EC=9E=90=EC=A3=BC?= =?UTF-8?q?=20=EB=AC=BB=EB=8A=94=20=EC=A7=88=EB=AC=B8=EA=B8=80=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../faq/controller/FaqCommandController.java | 46 +++++++++++++++++++ .../domain/faq/converter/FaqConverter.java | 11 +++++ .../domain/faq/dto/request/FaqRequestDTO.java | 16 ++++++- .../service/command/FaqCommandService.java | 10 ++++ .../command/FaqCommandServiceImpl.java | 25 ++++++++++ .../annotation/AuthenticatedMember.java | 3 ++ 6 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandService.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandServiceImpl.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java new file mode 100644 index 0000000..e0b371f --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java @@ -0,0 +1,46 @@ +package org.withtime.be.withtimebe.domain.faq.controller; + +import org.namul.api.payload.response.DefaultResponse; +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.RestController; +import org.withtime.be.withtimebe.domain.faq.converter.FaqConverter; +import org.withtime.be.withtimebe.domain.faq.dto.request.FaqRequestDTO; +import org.withtime.be.withtimebe.domain.faq.dto.response.FaqResponseDTO; +import org.withtime.be.withtimebe.domain.faq.entity.Faq; +import org.withtime.be.withtimebe.domain.faq.service.command.FaqCommandService; +import org.withtime.be.withtimebe.domain.member.entity.Member; +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.AllArgsConstructor; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/v1/admin") +public class FaqCommandController { + + private final FaqCommandService faqCommandService; + + @Operation(summary = "자주 묻는 질문 생성 API Only Admin by 피우", description = "자주 묻는 질문 생성 API입니다. 어드민만 사용 가능합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공입니다."), + @ApiResponse(responseCode = "403", + description = """ + - COMMON403 : "Admin 권한이 없음을 의미합니다." + """) + }) + @PostMapping("/faqs") + public DefaultResponse createFaq( + @RequestBody @Valid FaqRequestDTO.CreateFaq request, + @AuthenticatedMember Member member + ) { + Faq result = faqCommandService.createFaq(request, member); + FaqResponseDTO.Faq response = FaqConverter.toFaq(result); + return DefaultResponse.created(response); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqConverter.java index 541e4b8..bc4cd95 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqConverter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqConverter.java @@ -8,6 +8,7 @@ import org.withtime.be.withtimebe.domain.faq.dto.response.FaqResponseDTO; import org.withtime.be.withtimebe.domain.faq.entity.Faq; import org.withtime.be.withtimebe.domain.faq.entity.enums.FaqCategory; +import org.withtime.be.withtimebe.domain.member.entity.Member; import org.withtime.be.withtimebe.global.error.code.FaqErrorCode; import org.withtime.be.withtimebe.global.error.exception.FaqException; @@ -73,4 +74,14 @@ public static FaqResponseDTO.Faq toFaq(Faq faq) { .content(faq.getContent()) .build(); } + + public static Faq toFaqEntity(FaqRequestDTO.CreateFaq request, Member member) { + + return Faq.builder() + .member(member) + .title(request.title()) + .content(request.content()) + .faqCategory(request.faqCategory()) + .build(); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java index d9189fa..c1b1735 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java @@ -3,6 +3,9 @@ import org.springframework.data.domain.Pageable; import org.withtime.be.withtimebe.domain.faq.entity.enums.FaqCategory; +import jakarta.validation.constraints.NotBlank; + +import jakarta.validation.constraints.NotNull; import lombok.Builder; public class FaqRequestDTO { @@ -16,7 +19,16 @@ public record FindFaqList( @Builder public record FindFaqListByKeyword( Pageable pageable, // 자주 묻는 질문글 식별자 값 - String keyword, // 검색 키워드 - FaqCategory faqCategory // 질문 유형 + String keyword, // 검색 키워드 + FaqCategory faqCategory // 질문 유형 + ) {} + + public record CreateFaq( + @NotBlank(message = "제목을 입력해주세요") + String title, + @NotBlank(message = "내용을 입력해주세요") + String content, + @NotNull(message = "질문글 유형을 입력해주세요") + FaqCategory faqCategory ) {} } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandService.java new file mode 100644 index 0000000..6420227 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandService.java @@ -0,0 +1,10 @@ +package org.withtime.be.withtimebe.domain.faq.service.command; + +import org.withtime.be.withtimebe.domain.faq.dto.request.FaqRequestDTO; +import org.withtime.be.withtimebe.domain.faq.entity.Faq; +import org.withtime.be.withtimebe.domain.member.entity.Member; + +public interface FaqCommandService { + + Faq createFaq(FaqRequestDTO.CreateFaq request, Member member); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandServiceImpl.java new file mode 100644 index 0000000..9f9d0e8 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandServiceImpl.java @@ -0,0 +1,25 @@ +package org.withtime.be.withtimebe.domain.faq.service.command; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.withtime.be.withtimebe.domain.faq.converter.FaqConverter; +import org.withtime.be.withtimebe.domain.faq.dto.request.FaqRequestDTO; +import org.withtime.be.withtimebe.domain.faq.entity.Faq; +import org.withtime.be.withtimebe.domain.faq.repository.FaqRepository; +import org.withtime.be.withtimebe.domain.member.entity.Member; + +import lombok.AllArgsConstructor; + +@Service +@AllArgsConstructor +@Transactional(readOnly = false) +public class FaqCommandServiceImpl implements FaqCommandService { + + private FaqRepository faqRepository; + + @Override + public Faq createFaq(FaqRequestDTO.CreateFaq request, Member member) { + Faq faq = FaqConverter.toFaqEntity(request, member); + return faqRepository.save(faq); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/global/security/annotation/AuthenticatedMember.java b/src/main/java/org/withtime/be/withtimebe/global/security/annotation/AuthenticatedMember.java index b1c4511..3e4d534 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/security/annotation/AuthenticatedMember.java +++ b/src/main/java/org/withtime/be/withtimebe/global/security/annotation/AuthenticatedMember.java @@ -2,8 +2,11 @@ import java.lang.annotation.*; +import io.swagger.v3.oas.annotations.Parameter; + @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER}) @Documented +@Parameter(hidden = true) public @interface AuthenticatedMember{ } From 024faf57d03bdb1204b72db62284559eb9c2966a Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 10 Jul 2025 20:35:38 +0900 Subject: [PATCH 03/11] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EC=9E=90=EC=A3=BC?= =?UTF-8?q?=20=EB=AC=BB=EB=8A=94=20=EC=A7=88=EB=AC=B8=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../faq/controller/FaqCommandController.java | 21 +++++++++++++++++++ .../domain/faq/dto/request/FaqRequestDTO.java | 12 +++++++++++ .../be/withtimebe/domain/faq/entity/Faq.java | 6 ++++++ .../service/command/FaqCommandService.java | 2 ++ .../command/FaqCommandServiceImpl.java | 12 +++++++++++ 5 files changed, 53 insertions(+) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java index e0b371f..48cdf8d 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java @@ -2,6 +2,7 @@ import org.namul.api.payload.response.DefaultResponse; 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; @@ -43,4 +44,24 @@ public DefaultResponse createFaq( FaqResponseDTO.Faq response = FaqConverter.toFaq(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 = """ + - FAQ404_2 : "해당하는 질문글을 찾을 수 없습니다." + """) + }) + @PutMapping("/faqs/{faqId}") + public DefaultResponse createFaq(@RequestBody @Valid FaqRequestDTO.UpdateFaq request) { + Faq result = faqCommandService.updateFaq(request); + FaqResponseDTO.Faq response = FaqConverter.toFaq(result); + return DefaultResponse.ok(response); + } + } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java index c1b1735..03877dc 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java @@ -1,5 +1,7 @@ package org.withtime.be.withtimebe.domain.faq.dto.request; +import static jdk.javadoc.internal.doclets.formats.html.markup.HtmlStyle.*; + import org.springframework.data.domain.Pageable; import org.withtime.be.withtimebe.domain.faq.entity.enums.FaqCategory; @@ -31,4 +33,14 @@ public record CreateFaq( @NotNull(message = "질문글 유형을 입력해주세요") FaqCategory faqCategory ) {} + + public record UpdateFaq ( + @NotNull(message = "질문글의 식별자 값을 입력해주세요") + Long faqId, + @NotBlank(message = "제목을 입력해주세요") + String title, + @NotBlank(message = "내용을 입력해주세요") + String content + ) {} + } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/entity/Faq.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/entity/Faq.java index 54d08d3..cdddb54 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/entity/Faq.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/entity/Faq.java @@ -2,6 +2,7 @@ import java.time.LocalDateTime; +import org.withtime.be.withtimebe.domain.faq.dto.request.FaqRequestDTO; import org.withtime.be.withtimebe.domain.faq.entity.enums.FaqCategory; import org.withtime.be.withtimebe.domain.member.entity.Member; import org.withtime.be.withtimebe.global.common.BaseEntity; @@ -52,4 +53,9 @@ public class Faq extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; + + public void updateFields(FaqRequestDTO.UpdateFaq request) { + this.title = request.title(); + this.content = request.content(); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandService.java index 6420227..6bb1598 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandService.java @@ -7,4 +7,6 @@ public interface FaqCommandService { Faq createFaq(FaqRequestDTO.CreateFaq request, Member member); + + Faq updateFaq(FaqRequestDTO.UpdateFaq request); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandServiceImpl.java index 9f9d0e8..666482b 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandServiceImpl.java @@ -7,6 +7,8 @@ import org.withtime.be.withtimebe.domain.faq.entity.Faq; import org.withtime.be.withtimebe.domain.faq.repository.FaqRepository; import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.global.error.code.FaqErrorCode; +import org.withtime.be.withtimebe.global.error.exception.FaqException; import lombok.AllArgsConstructor; @@ -22,4 +24,14 @@ public Faq createFaq(FaqRequestDTO.CreateFaq request, Member member) { Faq faq = FaqConverter.toFaqEntity(request, member); return faqRepository.save(faq); } + + @Override + public Faq updateFaq(FaqRequestDTO.UpdateFaq request) { + Faq faq = faqRepository.findById(request.faqId()) + .orElseThrow(() -> new FaqException(FaqErrorCode.FAQ_NOT_FOUND)); + + faq.updateFields(request); + + return faq; + } } From c75c12e1aae2745cdbdd04003ea1aa92cd62e268 Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 10 Jul 2025 20:43:12 +0900 Subject: [PATCH 04/11] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/faq/controller/FaqCommandController.java | 9 +++++++-- .../withtimebe/domain/faq/dto/request/FaqRequestDTO.java | 2 -- .../domain/faq/service/command/FaqCommandService.java | 2 +- .../faq/service/command/FaqCommandServiceImpl.java | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java index 48cdf8d..b9a23f0 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java @@ -1,6 +1,8 @@ package org.withtime.be.withtimebe.domain.faq.controller; import org.namul.api.payload.response.DefaultResponse; +import org.springframework.web.bind.annotation.DeleteMapping; +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; @@ -58,8 +60,11 @@ public DefaultResponse createFaq( """) }) @PutMapping("/faqs/{faqId}") - public DefaultResponse createFaq(@RequestBody @Valid FaqRequestDTO.UpdateFaq request) { - Faq result = faqCommandService.updateFaq(request); + public DefaultResponse updateFaq( + @PathVariable("faqId") Long faqId, + @RequestBody @Valid FaqRequestDTO.UpdateFaq request + ) { + Faq result = faqCommandService.updateFaq(request, faqId); FaqResponseDTO.Faq response = FaqConverter.toFaq(result); return DefaultResponse.ok(response); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java index 03877dc..e4d8fe3 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java @@ -35,8 +35,6 @@ public record CreateFaq( ) {} public record UpdateFaq ( - @NotNull(message = "질문글의 식별자 값을 입력해주세요") - Long faqId, @NotBlank(message = "제목을 입력해주세요") String title, @NotBlank(message = "내용을 입력해주세요") diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandService.java index 6bb1598..0983119 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandService.java @@ -8,5 +8,5 @@ public interface FaqCommandService { Faq createFaq(FaqRequestDTO.CreateFaq request, Member member); - Faq updateFaq(FaqRequestDTO.UpdateFaq request); + Faq updateFaq(FaqRequestDTO.UpdateFaq request, Long faqId); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandServiceImpl.java index 666482b..9fbc563 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandServiceImpl.java @@ -26,8 +26,8 @@ public Faq createFaq(FaqRequestDTO.CreateFaq request, Member member) { } @Override - public Faq updateFaq(FaqRequestDTO.UpdateFaq request) { - Faq faq = faqRepository.findById(request.faqId()) + public Faq updateFaq(FaqRequestDTO.UpdateFaq request, Long faqId) { + Faq faq = faqRepository.findById(faqId) .orElseThrow(() -> new FaqException(FaqErrorCode.FAQ_NOT_FOUND)); faq.updateFields(request); From 4c28b729f14246d88ea7b0a9a31c68d1a9649b30 Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 10 Jul 2025 20:45:47 +0900 Subject: [PATCH 05/11] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EC=9E=90=EC=A3=BC?= =?UTF-8?q?=20=EB=AC=BB=EB=8A=94=20=EC=A7=88=EB=AC=B8=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../faq/controller/FaqCommandController.java | 19 ++++++++++++++++++- .../service/command/FaqCommandService.java | 2 ++ .../command/FaqCommandServiceImpl.java | 7 +++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java index b9a23f0..70fc0fd 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java @@ -68,5 +68,22 @@ public DefaultResponse updateFaq( FaqResponseDTO.Faq response = FaqConverter.toFaq(result); 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 = """ + - FAQ404_2 : "해당하는 질문글을 찾을 수 없습니다." + """) + }) + @DeleteMapping("/faqs/{faqId}") + public DefaultResponse deleteFaq(@PathVariable("faqId") Long faqId) { + faqCommandService.deleteFaq(faqId); + return DefaultResponse.noContent(); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandService.java index 0983119..cdd99f2 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandService.java @@ -9,4 +9,6 @@ public interface FaqCommandService { Faq createFaq(FaqRequestDTO.CreateFaq request, Member member); Faq updateFaq(FaqRequestDTO.UpdateFaq request, Long faqId); + + void deleteFaq(Long faqId); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandServiceImpl.java index 9fbc563..2732092 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/command/FaqCommandServiceImpl.java @@ -34,4 +34,11 @@ public Faq updateFaq(FaqRequestDTO.UpdateFaq request, Long faqId) { return faq; } + + @Override + public void deleteFaq(Long faqId) { + Faq faq = faqRepository.findById(faqId) + .orElseThrow(() -> new FaqException(FaqErrorCode.FAQ_NOT_FOUND)); + faqRepository.delete(faq); + } } From 51779e179a7bb4ca8a754f79de57723bd2f14c52 Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 10 Jul 2025 21:35:21 +0900 Subject: [PATCH 06/11] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=20import=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java index e4d8fe3..02b7534 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java @@ -1,7 +1,5 @@ package org.withtime.be.withtimebe.domain.faq.dto.request; -import static jdk.javadoc.internal.doclets.formats.html.markup.HtmlStyle.*; - import org.springframework.data.domain.Pageable; import org.withtime.be.withtimebe.domain.faq.entity.enums.FaqCategory; From 93210bbdcbbb83506525f0f0c40fd56c5e472b60 Mon Sep 17 00:00:00 2001 From: pywoo Date: Sun, 13 Jul 2025 21:40:55 +0900 Subject: [PATCH 07/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20FaqCateg?= =?UTF-8?q?ory=20Enum=20=EA=B4=80=EB=A0=A8=20=EC=BB=A8=EB=B2=84=ED=84=B0?= =?UTF-8?q?=20=EB=B0=8F=20@JsonCreator=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../faq/controller/FaqQueryController.java | 11 +++--- .../faq/converter/FaqCategoryConverter.java | 17 +++++++++ .../domain/faq/converter/FaqConverter.java | 35 ------------------- .../domain/faq/dto/request/FaqRequestDTO.java | 16 +-------- .../domain/faq/entity/enums/FaqCategory.java | 18 ++++++++++ .../faq/service/query/FaqQueryService.java | 6 ++-- .../service/query/FaqQueryServiceImpl.java | 11 +++--- .../withtimebe/global/config/WebConfig.java | 7 ++++ .../global/error/code/FaqErrorCode.java | 2 ++ 9 files changed, 61 insertions(+), 62 deletions(-) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqCategoryConverter.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqQueryController.java index 7b6a496..7fc38ed 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqQueryController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqQueryController.java @@ -12,6 +12,7 @@ import org.withtime.be.withtimebe.domain.faq.dto.request.FaqRequestDTO; import org.withtime.be.withtimebe.domain.faq.dto.response.FaqResponseDTO; import org.withtime.be.withtimebe.domain.faq.entity.Faq; +import org.withtime.be.withtimebe.domain.faq.entity.enums.FaqCategory; import org.withtime.be.withtimebe.domain.faq.service.query.FaqQueryService; import org.withtime.be.withtimebe.global.annotation.SwaggerPageable; @@ -42,10 +43,9 @@ public class FaqQueryController { @GetMapping("/faqs") public DefaultResponse findFaqList( @PageableDefault(page = 0, size = 10) Pageable pageable, - @RequestParam String faqCategory + @RequestParam FaqCategory faqCategory ) { - FaqRequestDTO.FindFaqList request = FaqConverter.toFindFaqList(pageable, faqCategory); - Page result = faqQueryService.findFaqList(request); + Page result = faqQueryService.findFaqList(pageable, faqCategory); FaqResponseDTO.FaqList response = FaqConverter.toFaqList(result); return DefaultResponse.ok(response); } @@ -65,10 +65,9 @@ public DefaultResponse findFaqList( public DefaultResponse findFaqListByKeyword( @PageableDefault(page = 0, size = 10) Pageable pageable, @RequestParam String keyword, - @RequestParam String faqCategory + @RequestParam FaqCategory faqCategory ) { - FaqRequestDTO.FindFaqListByKeyword request = FaqConverter.toFindFaqListByKeyword(pageable, keyword, faqCategory); - Page result = faqQueryService.findFaqListByKeyword(request); + Page result = faqQueryService.findFaqListByKeyword(pageable, keyword, faqCategory); FaqResponseDTO.FaqList response = FaqConverter.toFaqList(result); return DefaultResponse.ok(response); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqCategoryConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqCategoryConverter.java new file mode 100644 index 0000000..27fc842 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqCategoryConverter.java @@ -0,0 +1,17 @@ +package org.withtime.be.withtimebe.domain.faq.converter; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.util.StringUtils; +import org.withtime.be.withtimebe.domain.faq.entity.enums.FaqCategory; +import org.withtime.be.withtimebe.global.error.code.FaqErrorCode; +import org.withtime.be.withtimebe.global.error.exception.FaqException; + +// @PathVariable, @RequestParam +public class FaqCategoryConverter implements Converter { + + @Override + public FaqCategory convert(String source) { + if(StringUtils.hasText(source)) throw new FaqException(FaqErrorCode.FAQ_CATEGORY_EMPTY); + return FaqCategory.findFaqCategory(source); + } +} \ No newline at end of file diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqConverter.java index bc4cd95..2bcf834 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqConverter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqConverter.java @@ -14,41 +14,6 @@ public class FaqConverter { - // Request DTO : 전체 조회 (Controller -> Service) - public static FaqRequestDTO.FindFaqList toFindFaqList(Pageable pageable, String type) { - - FaqCategory faqCategory; - - try { - faqCategory = FaqCategory.valueOf(type); - } catch (IllegalArgumentException e) { - throw new FaqException(FaqErrorCode.FAQ_CATEGORY_NOT_FOUND); - } - - return FaqRequestDTO.FindFaqList.builder() - .pageable(pageable) - .faqCategory(faqCategory) - .build(); - } - - // Request DTO : 검색어 전체 조회 (Controller -> Service) - public static FaqRequestDTO.FindFaqListByKeyword toFindFaqListByKeyword(Pageable pageable, String keyword, String type) { - - FaqCategory faqCategory; - - try { - faqCategory = FaqCategory.valueOf(type); - } catch (IllegalArgumentException e) { - throw new FaqException(FaqErrorCode.FAQ_CATEGORY_NOT_FOUND); - } - - return FaqRequestDTO.FindFaqListByKeyword.builder() - .pageable(pageable) - .keyword(keyword) - .faqCategory(faqCategory) - .build(); - } - // Response DTO : FaqResponseDTO.FaqList public static FaqResponseDTO.FaqList toFaqList(Page faqPage) { diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java index 02b7534..d899268 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/dto/request/FaqRequestDTO.java @@ -10,25 +10,12 @@ public class FaqRequestDTO { - @Builder - public record FindFaqList( - Pageable pageable, // 자주 묻는 질문글 식별자 값 - FaqCategory faqCategory // 질문 유형 - ) {} - - @Builder - public record FindFaqListByKeyword( - Pageable pageable, // 자주 묻는 질문글 식별자 값 - String keyword, // 검색 키워드 - FaqCategory faqCategory // 질문 유형 - ) {} - public record CreateFaq( @NotBlank(message = "제목을 입력해주세요") String title, @NotBlank(message = "내용을 입력해주세요") String content, - @NotNull(message = "질문글 유형을 입력해주세요") + @NotNull(message = "질문 유형을 입력해주세요") FaqCategory faqCategory ) {} @@ -38,5 +25,4 @@ public record UpdateFaq ( @NotBlank(message = "내용을 입력해주세요") String content ) {} - } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/entity/enums/FaqCategory.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/entity/enums/FaqCategory.java index 1b9f2b1..5d90c9c 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/entity/enums/FaqCategory.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/entity/enums/FaqCategory.java @@ -1,5 +1,12 @@ package org.withtime.be.withtimebe.domain.faq.entity.enums; +import java.util.Arrays; + +import org.withtime.be.withtimebe.global.error.code.FaqErrorCode; +import org.withtime.be.withtimebe.global.error.exception.FaqException; + +import com.fasterxml.jackson.annotation.JsonCreator; + import lombok.AllArgsConstructor; import lombok.Getter; @@ -14,4 +21,15 @@ public enum FaqCategory { ACCOUNT("계정 및 개인정보"); private final String label; + + // @RequestBody + @JsonCreator + public static FaqCategory findFaqCategory(String label) { + return Arrays.stream(values()) + .filter(type -> type.getLabel().equals(label)) + .findAny() + .orElseThrow( + () -> new FaqException(FaqErrorCode.FAQ_CATEGORY_NOT_FOUND) + ); + } } \ No newline at end of file diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/query/FaqQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/query/FaqQueryService.java index 6ca80ec..b01e29f 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/query/FaqQueryService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/query/FaqQueryService.java @@ -1,10 +1,12 @@ package org.withtime.be.withtimebe.domain.faq.service.query; import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.withtime.be.withtimebe.domain.faq.dto.request.FaqRequestDTO; import org.withtime.be.withtimebe.domain.faq.entity.Faq; +import org.withtime.be.withtimebe.domain.faq.entity.enums.FaqCategory; public interface FaqQueryService { - Page findFaqList(FaqRequestDTO.FindFaqList request); - Page findFaqListByKeyword(FaqRequestDTO.FindFaqListByKeyword request); + Page findFaqList(Pageable pageable, FaqCategory faqCategory); + Page findFaqListByKeyword(Pageable pageable, String keyword, FaqCategory faqCategory); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/query/FaqQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/query/FaqQueryServiceImpl.java index 8c6de8d..aed51f5 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/service/query/FaqQueryServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/service/query/FaqQueryServiceImpl.java @@ -1,10 +1,12 @@ package org.withtime.be.withtimebe.domain.faq.service.query; import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.withtime.be.withtimebe.domain.faq.dto.request.FaqRequestDTO; import org.withtime.be.withtimebe.domain.faq.entity.Faq; +import org.withtime.be.withtimebe.domain.faq.entity.enums.FaqCategory; import org.withtime.be.withtimebe.domain.faq.repository.FaqRepository; import lombok.AllArgsConstructor; @@ -17,14 +19,15 @@ public class FaqQueryServiceImpl implements FaqQueryService { private final FaqRepository faqRepository; @Override - public Page findFaqList(FaqRequestDTO.FindFaqList request) { + public Page findFaqList(Pageable pageable, FaqCategory faqCategory) { return faqRepository.findFaqListByFaqCategory( - request.faqCategory(), request.pageable()); + faqCategory, pageable); } @Override - public Page findFaqListByKeyword(FaqRequestDTO.FindFaqListByKeyword request) { + public Page findFaqListByKeyword(Pageable pageable, String keyword, FaqCategory faqCategory) { return faqRepository.findFaqListByFaqCategoryAndKeyword( - request.faqCategory(), request.keyword(), request.pageable()); + faqCategory, keyword, pageable + ); } } diff --git a/src/main/java/org/withtime/be/withtimebe/global/config/WebConfig.java b/src/main/java/org/withtime/be/withtimebe/global/config/WebConfig.java index 05eb654..89b594a 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/config/WebConfig.java +++ b/src/main/java/org/withtime/be/withtimebe/global/config/WebConfig.java @@ -2,8 +2,10 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.withtime.be.withtimebe.domain.faq.converter.FaqCategoryConverter; import org.withtime.be.withtimebe.global.security.annotation.resolver.AuthenticatedMemberResolver; import java.util.List; @@ -18,4 +20,9 @@ public class WebConfig implements WebMvcConfigurer { public void addArgumentResolvers(List resolvers) { resolvers.add(authenticatedMemberResolver); } + + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(new FaqCategoryConverter()); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/global/error/code/FaqErrorCode.java b/src/main/java/org/withtime/be/withtimebe/global/error/code/FaqErrorCode.java index b14965a..609ce6a 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/error/code/FaqErrorCode.java +++ b/src/main/java/org/withtime/be/withtimebe/global/error/code/FaqErrorCode.java @@ -9,6 +9,8 @@ @AllArgsConstructor public enum FaqErrorCode implements BaseErrorCode { + FAQ_CATEGORY_EMPTY(HttpStatus.BAD_REQUEST, "FAQ400_1", "질문 유형을 입력해주세요."), + FAQ_CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "FAQ404_1", "해당하는 질문 유형을 찾을 수 없습니다."), FAQ_NOT_FOUND(HttpStatus.NOT_FOUND, "FAQ404_2", "해당하는 질문을 찾을 수 없습니다."), ; From 95edf8dc9fb6ff5ae2eacc5ad12f18ca914be6b0 Mon Sep 17 00:00:00 2001 From: pywoo Date: Sun, 13 Jul 2025 22:15:54 +0900 Subject: [PATCH 08/11] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=AA=85=EA=B3=BC=20=EB=85=BC=EB=A6=AC=EC=A0=81=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../withtimebe/domain/faq/converter/FaqCategoryConverter.java | 2 +- .../be/withtimebe/domain/faq/entity/enums/FaqCategory.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqCategoryConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqCategoryConverter.java index 27fc842..ad5993d 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqCategoryConverter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/converter/FaqCategoryConverter.java @@ -11,7 +11,7 @@ public class FaqCategoryConverter implements Converter { @Override public FaqCategory convert(String source) { - if(StringUtils.hasText(source)) throw new FaqException(FaqErrorCode.FAQ_CATEGORY_EMPTY); + if(!StringUtils.hasText(source)) throw new FaqException(FaqErrorCode.FAQ_CATEGORY_EMPTY); return FaqCategory.findFaqCategory(source); } } \ No newline at end of file diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/entity/enums/FaqCategory.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/entity/enums/FaqCategory.java index 5d90c9c..9e3bde7 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/entity/enums/FaqCategory.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/entity/enums/FaqCategory.java @@ -24,9 +24,9 @@ public enum FaqCategory { // @RequestBody @JsonCreator - public static FaqCategory findFaqCategory(String label) { + public static FaqCategory findFaqCategory(String name) { return Arrays.stream(values()) - .filter(type -> type.getLabel().equals(label)) + .filter(type -> type.name().equalsIgnoreCase(name)) .findAny() .orElseThrow( () -> new FaqException(FaqErrorCode.FAQ_CATEGORY_NOT_FOUND) From 77106c6efa632b4c65352ea47de7202834d35c2d Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 14 Jul 2025 19:42:55 +0900 Subject: [PATCH 09/11] =?UTF-8?q?=F0=9F=90=9B=20fix:=20FAQ=20=EC=9D=B8?= =?UTF-8?q?=EA=B0=80=20=EB=B0=A9=EC=8B=9D=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../faq/controller/FaqCommandController.java | 18 +++++++++++------- .../faq/controller/FaqQueryController.java | 6 +++--- .../global/security/SecurityConfig.java | 1 - 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java index 70fc0fd..e656676 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java @@ -1,6 +1,7 @@ package org.withtime.be.withtimebe.domain.faq.controller; import org.namul.api.payload.response.DefaultResponse; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -24,12 +25,12 @@ @RestController @AllArgsConstructor -@RequestMapping("/api/v1/admin") +@RequestMapping("/api/v1/faqs") public class FaqCommandController { private final FaqCommandService faqCommandService; - @Operation(summary = "자주 묻는 질문 생성 API Only Admin by 피우", description = "자주 묻는 질문 생성 API입니다. 어드민만 사용 가능합니다.") + @Operation(summary = "자주 묻는 질문 생성 API by 피우 [Only Admin]", description = "자주 묻는 질문 생성 API입니다. 어드민만 사용 가능합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공입니다."), @ApiResponse(responseCode = "403", @@ -37,7 +38,8 @@ public class FaqCommandController { - COMMON403 : "Admin 권한이 없음을 의미합니다." """) }) - @PostMapping("/faqs") + @PreAuthorize("hasRole('ADMIN')") + @PostMapping public DefaultResponse createFaq( @RequestBody @Valid FaqRequestDTO.CreateFaq request, @AuthenticatedMember Member member @@ -47,7 +49,7 @@ public DefaultResponse createFaq( return DefaultResponse.created(response); } - @Operation(summary = "자주 묻는 질문 수정 API Only Admin by 피우", description = "자주 묻는 질문 수정 API입니다. 어드민만 사용 가능합니다.") + @Operation(summary = "자주 묻는 질문 수정 API by 피우 [Only Admin]", description = "자주 묻는 질문 수정 API입니다. 어드민만 사용 가능합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공입니다."), @ApiResponse(responseCode = "403", @@ -59,7 +61,8 @@ public DefaultResponse createFaq( - FAQ404_2 : "해당하는 질문글을 찾을 수 없습니다." """) }) - @PutMapping("/faqs/{faqId}") + @PreAuthorize("hasRole('ADMIN')") + @PutMapping("/{faqId}") public DefaultResponse updateFaq( @PathVariable("faqId") Long faqId, @RequestBody @Valid FaqRequestDTO.UpdateFaq request @@ -69,7 +72,7 @@ public DefaultResponse updateFaq( return DefaultResponse.ok(response); } - @Operation(summary = "자주 묻는 질문 삭제 API Only Admin by 피우", description = "자주 묻는 질문 삭제 API입니다. 어드민만 사용 가능합니다.") + @Operation(summary = "자주 묻는 질문 삭제 API by 피우 [Only Admin]", description = "자주 묻는 질문 삭제 API입니다. 어드민만 사용 가능합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공입니다."), @ApiResponse(responseCode = "403", @@ -81,7 +84,8 @@ public DefaultResponse updateFaq( - FAQ404_2 : "해당하는 질문글을 찾을 수 없습니다." """) }) - @DeleteMapping("/faqs/{faqId}") + @PreAuthorize("hasRole('ADMIN')") + @DeleteMapping("/{faqId}") public DefaultResponse deleteFaq(@PathVariable("faqId") Long faqId) { faqCommandService.deleteFaq(faqId); return DefaultResponse.noContent(); diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqQueryController.java index 7fc38ed..e16b5ec 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqQueryController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqQueryController.java @@ -24,7 +24,7 @@ @RestController @AllArgsConstructor -@RequestMapping("/api/v1") +@RequestMapping("/api/v1/faqs") public class FaqQueryController { private final FaqQueryService faqQueryService; @@ -40,7 +40,7 @@ public class FaqQueryController { }) @Parameter(name = "faqCategory", description = "USAGE / ALGORITHM / FEATURE / SCHEDULE / ERROR / ACCOUNT") @SwaggerPageable - @GetMapping("/faqs") + @GetMapping public DefaultResponse findFaqList( @PageableDefault(page = 0, size = 10) Pageable pageable, @RequestParam FaqCategory faqCategory @@ -61,7 +61,7 @@ public DefaultResponse findFaqList( }) @Parameter(name = "faqCategory", description = "USAGE / ALGORITHM / FEATURE / SCHEDULE / ERROR / ACCOUNT") @SwaggerPageable - @GetMapping("/faqs/search") + @GetMapping("/search") public DefaultResponse findFaqListByKeyword( @PageableDefault(page = 0, size = 10) Pageable pageable, @RequestParam String keyword, 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 3117ced..237cb70 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 @@ -56,7 +56,6 @@ 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) From b3252df6945bd6cff3120924f2194517229a42ad Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 14 Jul 2025 19:48:17 +0900 Subject: [PATCH 10/11] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Notice=20=EC=9D=B8?= =?UTF-8?q?=EA=B0=80=20=EB=B0=A9=EC=8B=9D=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/NoticeCommandController.java | 13 +++++++++---- .../controller/query/NoticeQueryController.java | 14 ++++++++------ 2 files changed, 17 insertions(+), 10 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 32d1261..b47634b 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 @@ -1,6 +1,7 @@ package org.withtime.be.withtimebe.domain.notice.controller.command; import org.namul.api.payload.response.DefaultResponse; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -30,7 +31,7 @@ public class NoticeCommandController { private final NoticeCommandService noticeCommandService; - @Operation(summary = "공지사항 생성 API Only Admin by 피우", description = "공지사항 생성 API입니다. 어드민만 사용 가능합니다.") + @Operation(summary = "공지사항 생성 API by 피우 [Only Admin]", description = "공지사항 생성 API입니다. 어드민만 사용 가능합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공입니다."), @ApiResponse(responseCode = "403", @@ -38,6 +39,7 @@ public class NoticeCommandController { - COMMON403 : "Admin 권한이 없음을 의미합니다." """) }) + @PreAuthorize("hasRole('ADMIN')") @PostMapping public DefaultResponse createNotice( @RequestBody @Valid NoticeRequestDTO.CreateNotice request, @@ -48,7 +50,7 @@ public DefaultResponse createNotice( return DefaultResponse.created(response); } - @Operation(summary = "공지사항 수정 API Only Admin by 피우", description = "공지사항 수정 API입니다. 어드민만 사용 가능합니다.") + @Operation(summary = "공지사항 수정 API by 피우 [Only Admin]", description = "공지사항 수정 API입니다. 어드민만 사용 가능합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공입니다."), @ApiResponse(responseCode = "403", @@ -60,6 +62,7 @@ public DefaultResponse createNotice( - NOTICE404_2 : "해당하는 공지사항을 찾을 수 없습니다." """) }) + @PreAuthorize("hasRole('ADMIN')") @PutMapping("/{noticeId}") public DefaultResponse updateNotice( @PathVariable Long noticeId, @@ -70,7 +73,7 @@ public DefaultResponse updateNotice( return DefaultResponse.ok(response); } - @Operation(summary = "공지사항 삭제 API Only Admin by 피우", description = "공지사항 삭제 API입니다. 어드민만 사용 가능합니다.") + @Operation(summary = "공지사항 삭제 API by 피우 [Only Admin]", description = "공지사항 삭제 API입니다. 어드민만 사용 가능합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "성공입니다."), @ApiResponse(responseCode = "403", @@ -82,13 +85,14 @@ public DefaultResponse updateNotice( - NOTICE404_2 : "해당하는 공지사항을 찾을 수 없습니다." """) }) + @PreAuthorize("hasRole('ADMIN')") @DeleteMapping("/{noticeId}") public DefaultResponse softDeleteNotice(@PathVariable Long noticeId) { noticeCommandService.softDeleteNotice(noticeId); return DefaultResponse.noContent(); } - @Operation(summary = "삭제한 공지사항 되돌리기 API Only Admin by 피우", description = "삭제한 공지사항을 되돌리는 API입니다. 어드민만 사용 가능합니다.") + @Operation(summary = "삭제한 공지사항 되돌리기 API by 피우 [Only Admin]", description = "삭제한 공지사항을 되돌리는 API입니다. 어드민만 사용 가능합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공입니다."), @ApiResponse(responseCode = "403", @@ -100,6 +104,7 @@ public DefaultResponse softDeleteNotice(@PathVariable Long noticeId) { - NOTICE404_2 : "해당하는 공지사항을 찾을 수 없습니다." """) }) + @PreAuthorize("hasRole('ADMIN')") @PatchMapping("/{noticeId}") public DefaultResponse recoverDeletedNotice(@PathVariable Long noticeId) { Notice result = noticeCommandService.recoverDeletedNotice(noticeId); 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 423660f..d2a8cc5 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 @@ -5,6 +5,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -27,7 +28,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/v1") +@RequestMapping("/api/v1/notices") public class NoticeQueryController { private final NoticeQueryService noticeQueryService; @@ -43,7 +44,7 @@ public class NoticeQueryController { }) @Parameter(name = "noticeCategory", description = "SYSTEM / SERVICE") @SwaggerPageable - @GetMapping("/notices") + @GetMapping public DefaultResponse findNoticeList( @PageableDefault(page = 0, size = 10) Pageable pageable, @RequestParam String noticeCategory @@ -65,7 +66,7 @@ public DefaultResponse findNoticeList( }) @Parameter(name = "noticeCategory", description = "SYSTEM / SERVICE") @SwaggerPageable - @GetMapping("/notices/search") + @GetMapping("/search") public DefaultResponse findNoticeListByKeyword( @PageableDefault(page = 0, size = 10) Pageable pageable, @RequestParam String keyword, @@ -91,7 +92,7 @@ public DefaultResponse findNoticeListByKeyword( - NOTICE404_2 : 해당하는 공지사항을 찾을 수 없습니다. """) }) - @GetMapping("/notices/{noticeId}") + @GetMapping("/{noticeId}") public DefaultResponse findNoticeDetail( @PathVariable("noticeId") Long noticeId, @AuthenticatedMember Member member @@ -102,7 +103,7 @@ public DefaultResponse findNoticeDetail( return DefaultResponse.ok(response); } - @Operation(summary = "삭제된 공지사항 전체 조회 API Only Admin by 피우", description = "삭제된 공지사항 상세 조회 API입니다. 어드민만 사용 가능합니다.") + @Operation(summary = "삭제된 공지사항 전체 조회 API by 피우 [Only Admin]", description = "삭제된 공지사항 상세 조회 API입니다. 어드민만 사용 가능합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공입니다."), @ApiResponse(responseCode = "403", @@ -117,7 +118,8 @@ public DefaultResponse findNoticeDetail( }) @Parameter(name = "noticeCategory", description = "SYSTEM / SERVICE") @SwaggerPageable - @GetMapping("/admin/notices/trash") + @PreAuthorize("hasRole('ADMIN')") + @GetMapping("/trash") public DefaultResponse findTrashNoticeList( @PageableDefault(page = 0, size = 10) Pageable pageable, @RequestParam String noticeCategory From 79e17fb1f017bcce29b232fe005443f3e0e55cff Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 14 Jul 2025 23:41:23 +0900 Subject: [PATCH 11/11] =?UTF-8?q?=F0=9F=90=9B=20fix:=20SecurityConfig=20?= =?UTF-8?q?=EC=9D=B8=EA=B0=80=20=EC=B2=98=EB=A6=AC=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../faq/controller/FaqCommandController.java | 3 --- .../command/NoticeCommandController.java | 4 ---- .../query/NoticeQueryController.java | 1 - .../global/security/SecurityConfig.java | 20 +++++++++++++++++++ 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java b/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java index e656676..6d5495f 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/faq/controller/FaqCommandController.java @@ -38,7 +38,6 @@ public class FaqCommandController { - COMMON403 : "Admin 권한이 없음을 의미합니다." """) }) - @PreAuthorize("hasRole('ADMIN')") @PostMapping public DefaultResponse createFaq( @RequestBody @Valid FaqRequestDTO.CreateFaq request, @@ -61,7 +60,6 @@ public DefaultResponse createFaq( - FAQ404_2 : "해당하는 질문글을 찾을 수 없습니다." """) }) - @PreAuthorize("hasRole('ADMIN')") @PutMapping("/{faqId}") public DefaultResponse updateFaq( @PathVariable("faqId") Long faqId, @@ -84,7 +82,6 @@ public DefaultResponse updateFaq( - FAQ404_2 : "해당하는 질문글을 찾을 수 없습니다." """) }) - @PreAuthorize("hasRole('ADMIN')") @DeleteMapping("/{faqId}") public DefaultResponse deleteFaq(@PathVariable("faqId") Long faqId) { faqCommandService.deleteFaq(faqId); 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 b47634b..0a7d641 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 @@ -39,7 +39,6 @@ public class NoticeCommandController { - COMMON403 : "Admin 권한이 없음을 의미합니다." """) }) - @PreAuthorize("hasRole('ADMIN')") @PostMapping public DefaultResponse createNotice( @RequestBody @Valid NoticeRequestDTO.CreateNotice request, @@ -62,7 +61,6 @@ public DefaultResponse createNotice( - NOTICE404_2 : "해당하는 공지사항을 찾을 수 없습니다." """) }) - @PreAuthorize("hasRole('ADMIN')") @PutMapping("/{noticeId}") public DefaultResponse updateNotice( @PathVariable Long noticeId, @@ -85,7 +83,6 @@ public DefaultResponse updateNotice( - NOTICE404_2 : "해당하는 공지사항을 찾을 수 없습니다." """) }) - @PreAuthorize("hasRole('ADMIN')") @DeleteMapping("/{noticeId}") public DefaultResponse softDeleteNotice(@PathVariable Long noticeId) { noticeCommandService.softDeleteNotice(noticeId); @@ -104,7 +101,6 @@ public DefaultResponse softDeleteNotice(@PathVariable Long noticeId) { - NOTICE404_2 : "해당하는 공지사항을 찾을 수 없습니다." """) }) - @PreAuthorize("hasRole('ADMIN')") @PatchMapping("/{noticeId}") public DefaultResponse recoverDeletedNotice(@PathVariable Long noticeId) { Notice result = noticeCommandService.recoverDeletedNotice(noticeId); 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 d2a8cc5..ff7c0df 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 @@ -118,7 +118,6 @@ public DefaultResponse findNoticeDetail( }) @Parameter(name = "noticeCategory", description = "SYSTEM / SERVICE") @SwaggerPageable - @PreAuthorize("hasRole('ADMIN')") @GetMapping("/trash") public DefaultResponse findTrashNoticeList( @PageableDefault(page = 0, size = 10) Pageable pageable, 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 237cb70..adb2c6d 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 @@ -6,6 +6,7 @@ import org.namul.api.payload.writer.FailureResponseWriter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -19,6 +20,8 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @@ -51,10 +54,23 @@ public class SecurityConfig { "/v3/api-docs/**" }; + private RequestMatcher[] admin = { + requestMatcher(HttpMethod.GET, API_PREFIX + "/notices/trash"), + requestMatcher(HttpMethod.POST, API_PREFIX + "/notices/**"), + requestMatcher(HttpMethod.PUT, API_PREFIX + "/notices/**"), + requestMatcher(HttpMethod.PATCH, API_PREFIX + "/notices/**"), + requestMatcher(HttpMethod.DELETE, API_PREFIX + "/notices/**"), + + requestMatcher(HttpMethod.POST, API_PREFIX + "/faqs/**"), + requestMatcher(HttpMethod.PUT, API_PREFIX + "/faqs/**"), + requestMatcher(HttpMethod.DELETE, API_PREFIX + "/faqs/**"), + }; + @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(request -> request + .requestMatchers(admin).hasRole("ADMIN") .requestMatchers(allowUrl).permitAll() .anyRequest().authenticated() ) @@ -120,4 +136,8 @@ private CorsConfigurationSource corsConfigurationSource() { source.registerCorsConfiguration("/**", configuration); return source; } + + private RequestMatcher requestMatcher(HttpMethod method, String url) { + return PathPatternRequestMatcher.withDefaults().matcher(method, url); + } }