Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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;
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;
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/faqs")
public class FaqCommandController {

private final FaqCommandService faqCommandService;

@Operation(summary = "자주 묻는 질문 생성 API by 피우 [Only Admin]", description = "자주 묻는 질문 생성 API입니다. 어드민만 사용 가능합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "성공입니다."),
@ApiResponse(responseCode = "403",
description = """
- COMMON403 : "Admin 권한이 없음을 의미합니다."
""")
})
@PostMapping
public DefaultResponse<FaqResponseDTO.Faq> 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);
}

@Operation(summary = "자주 묻는 질문 수정 API by 피우 [Only Admin]", description = "자주 묻는 질문 수정 API입니다. 어드민만 사용 가능합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "성공입니다."),
@ApiResponse(responseCode = "403",
description = """
- COMMON403 : "Admin 권한이 없음을 의미합니다."
"""),
@ApiResponse(responseCode = "404",
description = """
- FAQ404_2 : "해당하는 질문글을 찾을 수 없습니다."
""")
})
@PutMapping("/{faqId}")
public DefaultResponse<FaqResponseDTO.Faq> 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);
}

@Operation(summary = "자주 묻는 질문 삭제 API by 피우 [Only Admin]", description = "자주 묻는 질문 삭제 API입니다. 어드민만 사용 가능합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "성공입니다."),
@ApiResponse(responseCode = "403",
description = """
- COMMON403 : "Admin 권한이 없음을 의미합니다."
"""),
@ApiResponse(responseCode = "404",
description = """
- FAQ404_2 : "해당하는 질문글을 찾을 수 없습니다."
""")
})
@DeleteMapping("/{faqId}")
public DefaultResponse<String> deleteFaq(@PathVariable("faqId") Long faqId) {
faqCommandService.deleteFaq(faqId);
return DefaultResponse.noContent();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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.entity.enums.FaqCategory;
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/faqs")
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
public DefaultResponse<FaqResponseDTO.FaqList> findFaqList(
@PageableDefault(page = 0, size = 10) Pageable pageable,
@RequestParam FaqCategory faqCategory
) {
Page<Faq> result = faqQueryService.findFaqList(pageable, faqCategory);
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("/search")
public DefaultResponse<FaqResponseDTO.FaqList> findFaqListByKeyword(
@PageableDefault(page = 0, size = 10) Pageable pageable,
@RequestParam String keyword,
@RequestParam FaqCategory faqCategory
) {
Page<Faq> result = faqQueryService.findFaqListByKeyword(pageable, keyword, faqCategory);
FaqResponseDTO.FaqList response = FaqConverter.toFaqList(result);
return DefaultResponse.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -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<String, FaqCategory> {

@Override
public FaqCategory convert(String source) {
if(!StringUtils.hasText(source)) throw new FaqException(FaqErrorCode.FAQ_CATEGORY_EMPTY);
return FaqCategory.findFaqCategory(source);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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.domain.member.entity.Member;
import org.withtime.be.withtimebe.global.error.code.FaqErrorCode;
import org.withtime.be.withtimebe.global.error.exception.FaqException;

public class FaqConverter {

// Response DTO : FaqResponseDTO.FaqList
public static FaqResponseDTO.FaqList toFaqList(Page<Faq> faqPage) {

List<FaqResponseDTO.Faq> 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();
}

public static Faq toFaqEntity(FaqRequestDTO.CreateFaq request, Member member) {

return Faq.builder()
.member(member)
.title(request.title())
.content(request.content())
.faqCategory(request.faqCategory())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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 jakarta.validation.constraints.NotBlank;

import jakarta.validation.constraints.NotNull;
import lombok.Builder;

public class FaqRequestDTO {

public record CreateFaq(
@NotBlank(message = "제목을 입력해주세요")
String title,
@NotBlank(message = "내용을 입력해주세요")
String content,
@NotNull(message = "질문 유형을 입력해주세요")
FaqCategory faqCategory
) {}

public record UpdateFaq (
@NotBlank(message = "제목을 입력해주세요")
String title,
@NotBlank(message = "내용을 입력해주세요")
String content
) {}
}
Original file line number Diff line number Diff line change
@@ -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<Faq> faqList,
Integer totalPages, // 전체 페이지 개수
Integer currentPage, // 현재 페이지 번호
Integer currentSize, // 현재 페이지의 크기
Boolean hasNextPage // 다음 페이지 존재 여부
) {}

@Builder
public record Faq(
Long faqId, // 자주 묻는 질문글 식별자 값
String title, // 질문글 제목
String content // 내용
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.withtime.be.withtimebe.domain.faq.entity;

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;

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;

public void updateFields(FaqRequestDTO.UpdateFaq request) {
this.title = request.title();
this.content = request.content();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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;

@Getter
@AllArgsConstructor
public enum FaqCategory {
USAGE("서비스 이용 방법"),
ALGORITHM("추천 알고리즘 관련"),
FEATURE("기능 및 사용성"),
SCHEDULE("예약/일정 관리"),
ERROR("기타/문의 오류 신고"),
ACCOUNT("계정 및 개인정보");

private final String label;

// @RequestBody
@JsonCreator
public static FaqCategory findFaqCategory(String name) {
return Arrays.stream(values())
.filter(type -> type.name().equalsIgnoreCase(name))
.findAny()
.orElseThrow(
() -> new FaqException(FaqErrorCode.FAQ_CATEGORY_NOT_FOUND)
);
}
}
Loading