diff --git a/src/main/java/com/mtvs/devlinkbackend/question/controller/QuestionController.java b/src/main/java/com/mtvs/devlinkbackend/question/controller/QuestionController.java new file mode 100644 index 0000000..9a5c214 --- /dev/null +++ b/src/main/java/com/mtvs/devlinkbackend/question/controller/QuestionController.java @@ -0,0 +1,89 @@ +package com.mtvs.devlinkbackend.question.controller; + +import com.mtvs.devlinkbackend.config.JwtUtil; +import com.mtvs.devlinkbackend.oauth2.service.EpicGamesTokenService; +import com.mtvs.devlinkbackend.question.dto.QuestionRegistRequestDTO; +import com.mtvs.devlinkbackend.question.dto.QuestionUpdateRequestDTO; +import com.mtvs.devlinkbackend.question.entity.Question; +import com.mtvs.devlinkbackend.question.service.QuestionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/question") +public class QuestionController { + + private final QuestionService questionService; + private final JwtUtil jwtUtil; + + @Autowired + public QuestionController(QuestionService questionService, JwtUtil jwtUtil) { + this.questionService = questionService; + this.jwtUtil = jwtUtil; + } + + // Create a new question + @PostMapping + public ResponseEntity createQuestion( + @RequestBody QuestionRegistRequestDTO questionRegistRequestDTO, + @RequestHeader(name = "Authorization") String accessToken) throws Exception { + + String accountId = jwtUtil.getSubjectFromToken(accessToken); + Question createdQuestion = questionService.registQuestion(questionRegistRequestDTO, accountId); + return ResponseEntity.ok(createdQuestion); + } + + // Retrieve a question by ID + @GetMapping("/{questionId}") + public ResponseEntity getQuestionById(@PathVariable long questionId) { + Question question = questionService.findQuestionByQuestionId(questionId); + if (question != null) { + return ResponseEntity.ok(question); + } else { + return ResponseEntity.notFound().build(); + } + } + + // Retrieve all questions with pagination + @GetMapping("/all") + public ResponseEntity> getAllQuestionsWithPaging(@RequestParam int page) { + List questions = questionService.findAllQuestionsWithPaging(page); + return ResponseEntity.ok(questions); + } + + // Retrieve questions by account ID with pagination + @GetMapping + public ResponseEntity> getQuestionsByAccountIdWithPaging( + @RequestParam int page, + @RequestHeader(name = "Authorization") String accessToken) throws Exception { + + String accountId = jwtUtil.getSubjectFromToken(accessToken); + List questions = questionService.findQuestionsByAccountIdWithPaging(page, accountId); + return ResponseEntity.ok(questions); + } + + // Update a question by ID + @PutMapping("/{id}") + public ResponseEntity updateQuestion( + @RequestBody QuestionUpdateRequestDTO questionUpdateRequestDTO, + @RequestHeader(name = "Authorization") String accessToken) throws Exception { + + String accountId = jwtUtil.getSubjectFromToken(accessToken); + try { + Question updatedQuestion = questionService.updateQuestion(questionUpdateRequestDTO, accountId); + return ResponseEntity.ok(updatedQuestion); + } catch (IllegalArgumentException e) { + return ResponseEntity.notFound().build(); + } + } + + // Delete a question by ID + @DeleteMapping("/{id}") + public ResponseEntity deleteQuestion(@PathVariable long id) { + questionService.deleteQuestion(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/mtvs/devlinkbackend/question/dto/QuestionRegistRequestDTO.java b/src/main/java/com/mtvs/devlinkbackend/question/dto/QuestionRegistRequestDTO.java new file mode 100644 index 0000000..a99b900 --- /dev/null +++ b/src/main/java/com/mtvs/devlinkbackend/question/dto/QuestionRegistRequestDTO.java @@ -0,0 +1,12 @@ +package com.mtvs.devlinkbackend.question.dto; + +import lombok.*; + +@Getter @Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class QuestionRegistRequestDTO { + private String title; + private String content; +} diff --git a/src/main/java/com/mtvs/devlinkbackend/question/dto/QuestionUpdateRequestDTO.java b/src/main/java/com/mtvs/devlinkbackend/question/dto/QuestionUpdateRequestDTO.java new file mode 100644 index 0000000..2925049 --- /dev/null +++ b/src/main/java/com/mtvs/devlinkbackend/question/dto/QuestionUpdateRequestDTO.java @@ -0,0 +1,13 @@ +package com.mtvs.devlinkbackend.question.dto; + +import lombok.*; + +@Getter @Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class QuestionUpdateRequestDTO { + private long questionId; + private String title; + private String content; +} diff --git a/src/main/java/com/mtvs/devlinkbackend/question/entity/Question.java b/src/main/java/com/mtvs/devlinkbackend/question/entity/Question.java new file mode 100644 index 0000000..ffdc1b7 --- /dev/null +++ b/src/main/java/com/mtvs/devlinkbackend/question/entity/Question.java @@ -0,0 +1,50 @@ +package com.mtvs.devlinkbackend.question.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Table(name = "QUESTION") +@Entity(name = "QUESTION") +@Getter +@NoArgsConstructor +public class Question { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "QUESTION_ID") + private long questionId; + + @Column(name = "TITLE") + private String title; + + @Column(name = "CONTENT") + private String content; + + @CreationTimestamp + @Column(name = "CREATED_AT", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "MODIFIED_AT") + private LocalDateTime modifiedAt; + + @Column(name = "ACCOUNT_ID") // 사용자 구분 + private String accountId; + + public Question(String title, String content, String accountId) { // Create용 생성자 + this.content = content; + this.accountId = accountId; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/src/main/java/com/mtvs/devlinkbackend/question/repository/QuestionRepository.java b/src/main/java/com/mtvs/devlinkbackend/question/repository/QuestionRepository.java new file mode 100644 index 0000000..37cb846 --- /dev/null +++ b/src/main/java/com/mtvs/devlinkbackend/question/repository/QuestionRepository.java @@ -0,0 +1,14 @@ +package com.mtvs.devlinkbackend.question.repository; + +import com.mtvs.devlinkbackend.question.entity.Question; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface QuestionRepository extends JpaRepository { + Page findQuestionsByAccountId(String accountId, Pageable pageable); +} diff --git a/src/main/java/com/mtvs/devlinkbackend/question/service/QuestionService.java b/src/main/java/com/mtvs/devlinkbackend/question/service/QuestionService.java new file mode 100644 index 0000000..e2d5d05 --- /dev/null +++ b/src/main/java/com/mtvs/devlinkbackend/question/service/QuestionService.java @@ -0,0 +1,82 @@ +package com.mtvs.devlinkbackend.question.service; + +import com.mtvs.devlinkbackend.question.dto.QuestionRegistRequestDTO; +import com.mtvs.devlinkbackend.question.dto.QuestionUpdateRequestDTO; +import com.mtvs.devlinkbackend.question.entity.Question; +import com.mtvs.devlinkbackend.question.repository.QuestionRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Service +public class QuestionService { + private final QuestionRepository questionRepository; + + private static int pageSize = 20; + + public QuestionService(QuestionRepository questionRepository) { + this.questionRepository = questionRepository; + } + + @Transactional + public Question registQuestion(QuestionRegistRequestDTO questionRegistRequestDTO, String accountId) { + return questionRepository.save( + new Question( + questionRegistRequestDTO.getTitle(), + questionRegistRequestDTO.getContent(), + accountId + ) + ); + } + + public Question findQuestionByQuestionId(long questionId) { + return questionRepository.findById(questionId).orElse(null); + } + + // findAll with pagination + public List findAllQuestionsWithPaging(int page) { + Pageable pageable = PageRequest.of(page, pageSize, Sort.by("createdAt").descending()); + Page questionPage = questionRepository.findAll(pageable); + return questionPage.getContent(); // Returns the list of questions + } + + // findQuestionsByAccountId with pagination + public List findQuestionsByAccountIdWithPaging(int page, String accountId) { + Pageable pageable = PageRequest.of(page, pageSize, Sort.by("createdAt").descending()); + Page questionPage = questionRepository.findQuestionsByAccountId(accountId, pageable); + return questionPage.getContent(); // Returns the list of questions for the given accountId + } + + public List findAllQuestions() { + return questionRepository.findAll(); + } + + @Transactional + public Question updateQuestion(QuestionUpdateRequestDTO questionUpdateRequestDTO, String accountId) { + Optional foundQuestion = + questionRepository.findById(questionUpdateRequestDTO.getQuestionId()); + if (foundQuestion.isPresent()) { + if (foundQuestion.get().getAccountId().equals(accountId)) { + Question question = foundQuestion.get(); + question.setTitle(questionUpdateRequestDTO.getTitle()); + question.setContent(questionUpdateRequestDTO.getContent()); + return question; + } else throw new IllegalArgumentException("Question Update Error : 다른 사용자가 남의 질문 내용 변경 시도"); + } + else throw new IllegalArgumentException( + "Question ID : "+ questionUpdateRequestDTO.getQuestionId() +" not found"); + } + + @Transactional + public void deleteQuestion(Long questionId) { + questionRepository.deleteById(questionId); + System.out.println("질문ID = " + questionId + " ,삭제 시간 : " + LocalDateTime.now()); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 709b8c9..f0a728b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,4 +12,23 @@ spring: authorization-grant-type: authorization_code redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" client-name: Epic Games - client-authentication-method: post \ No newline at end of file + client-authentication-method: post + provider: + epicgames: + authorization-uri: https://www.epicgames.com/id/authorize + token-uri: https://api.epicgames.dev/epic/oauth/v2/token + user-info-uri: https://api.epicgames.dev/epic/oauth/v2/userinfo + user-name-attribute: sub + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost/devlink + username: root + password: 1234 + jpa: + show-sql: true + database: mysql + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true diff --git a/src/test/java/com/mtvs/devlinkbackend/question/QuestionCRUDTest.java b/src/test/java/com/mtvs/devlinkbackend/question/QuestionCRUDTest.java new file mode 100644 index 0000000..c9e526d --- /dev/null +++ b/src/test/java/com/mtvs/devlinkbackend/question/QuestionCRUDTest.java @@ -0,0 +1,93 @@ +package com.mtvs.devlinkbackend.question; + +import com.mtvs.devlinkbackend.question.dto.QuestionRegistRequestDTO; +import com.mtvs.devlinkbackend.question.dto.QuestionUpdateRequestDTO; +import com.mtvs.devlinkbackend.question.service.QuestionService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.util.stream.Stream; + +@SpringBootTest +@Transactional +public class QuestionCRUDTest { + + @Autowired + private QuestionService questionService; + + private static Stream newQuestion() { + return Stream.of( + Arguments.of(new QuestionRegistRequestDTO("질문0", "내용0"), "계정0"), + Arguments.of(new QuestionRegistRequestDTO("질문00", "내용00"), "계정00") + ); + } + + private static Stream modifiedQuestion() { + return Stream.of( + Arguments.of(new QuestionUpdateRequestDTO(1L,"질문0", "내용0"), "계정1"), + Arguments.of(new QuestionUpdateRequestDTO(2L,"질문00" , "내용00"), "계정1") + ); + } + + @DisplayName("질문 추가 테스트") + @ParameterizedTest + @MethodSource("newQuestion") + @Order(0) + public void testCreateQuestion(QuestionRegistRequestDTO questionRegistRequestDTO, String accountId) { + Assertions.assertDoesNotThrow(() -> questionService.registQuestion(questionRegistRequestDTO, accountId)); + } + + @DisplayName("PK로 질문 조회 테스트") + @ValueSource(longs = {1,2}) + @ParameterizedTest + @Order(1) + public void testFindQuestionByQuestionId(long questionId) { + Assertions.assertDoesNotThrow(() -> + System.out.println("Question = " + questionService.findQuestionByQuestionId(questionId))); + } + + @DisplayName("질문 paging 조회 테스트") + @ValueSource(ints = {0,1}) + @ParameterizedTest + @Order(2) + public void testFindQuestionsWithPaging(int page) { + Assertions.assertDoesNotThrow(() -> + System.out.println("Question = " + questionService.findAllQuestionsWithPaging(page))); + } + + @DisplayName("계정 ID에 따른 질문 paging 조회 테스트") + @CsvSource({"0,계정1", "0,계정2"}) + @ParameterizedTest + @Order(3) + public void testFindQuestionsByAccountIdWithPaging(int page, String accountId) { + Assertions.assertDoesNotThrow(() -> + System.out.println("Question = " + questionService.findQuestionsByAccountIdWithPaging(page, accountId))); + } + + @DisplayName("질문 수정 테스트") + @MethodSource("modifiedQuestion") + @ParameterizedTest + @Order(4) + public void testUpdateQuestion(QuestionUpdateRequestDTO questionUpdateRequestDTO, String accountId) { + Assertions.assertDoesNotThrow(() -> + System.out.println(questionService.updateQuestion(questionUpdateRequestDTO, accountId))); + } + + @DisplayName("질문 삭제 테스트") + @ValueSource(longs = {0,1}) + @ParameterizedTest + @Order(5) + public void testDeleteQuestion(long questionId) { + Assertions.assertDoesNotThrow(() -> + questionService.deleteQuestion(questionId)); + } +}