diff --git a/Titto_Backend/.gitignore b/Titto_Backend/.gitignore index e5df7fa..2bff8a4 100644 --- a/Titto_Backend/.gitignore +++ b/Titto_Backend/.gitignore @@ -35,4 +35,5 @@ out/ ### VS Code ### .vscode/ + .DS_Store \ No newline at end of file diff --git a/Titto_Backend/src/main/java/com/example/titto_backend/common/exception/ErrorCode.java b/Titto_Backend/src/main/java/com/example/titto_backend/common/exception/ErrorCode.java index 545c43c..1f453db 100644 --- a/Titto_Backend/src/main/java/com/example/titto_backend/common/exception/ErrorCode.java +++ b/Titto_Backend/src/main/java/com/example/titto_backend/common/exception/ErrorCode.java @@ -22,10 +22,13 @@ public enum ErrorCode { /* 404 NOT_FOUND : 리소스를 찾을 수 없음 */ USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자의 정보를 찾을 수 없습니다."), + USER_NOT_MATCH(HttpStatus.NOT_FOUND, "사용자의 정보가 일치하지 않습니다."), EMAIL_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자의 이메일을 찾을 수 없습니다."), - PROFILE_IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자의 프로필 이미지를 찾을 수 없습니다."); + PROFILE_IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자의 프로필 이미지를 찾을 수 없습니다."), + + QUESTION_NOT_FOUND(HttpStatus.NOT_FOUND, "질문을 찾을 수 없습니다."); private final HttpStatus httpStatus; private final String message; -} +} \ No newline at end of file diff --git a/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/controller/QuestionController.java b/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/controller/QuestionController.java new file mode 100644 index 0000000..daf86b6 --- /dev/null +++ b/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/controller/QuestionController.java @@ -0,0 +1,107 @@ +package com.example.titto_backend.questionBoard.controller; + +import com.example.titto_backend.questionBoard.dto.QuestionDTO; +import com.example.titto_backend.questionBoard.service.QuestionService; +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.tags.Tag; +import java.security.Principal; +import lombok.RequiredArgsConstructor; +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.data.domain.Sort.Direction; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/questions") +@Tag(name = "Question Controller", description = "질문 게시판 관련 API") +public class QuestionController { + + private final QuestionService questionService; + + + @PostMapping("/create") + @Operation( + summary = "질문 작성", + description = "질문을 작성합니다", + responses = { + @ApiResponse(responseCode = "201", description = "질문 작성 성공"), + @ApiResponse(responseCode = "500", description = "관리자 문의") + }) + public ResponseEntity createQuestion(@RequestBody QuestionDTO.Request request, + Principal principal) { + String email = principal.getName(); // 사용자의 이메일을 가져옴 + + QuestionDTO.Response savedQuestion = questionService.save(email, request); + return ResponseEntity.status(HttpStatus.CREATED).body(savedQuestion); +// try { +// QuestionDTO.Response savedQuestion = questionService.save(email, request); +// return ResponseEntity.status(HttpStatus.CREATED).body(savedQuestion); +// } catch (Exception e) { +// System.out.println(e.getMessage()); +// return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); +// } + } + + @ResponseBody + @GetMapping("/posts") + @Operation( + summary = "질문 목록 조회", + description = "질문 목록을 조회합니다", + responses = { + @ApiResponse(responseCode = "200", description = "요청 성공"), + @ApiResponse(responseCode = "500", description = "관리자 문의") + }) + public ResponseEntity> getAllQuestions(@PageableDefault(size=20, sort="createdAt", direction = Sort.Direction.DESC) @Parameter(hidden = true) Pageable pageable) { + Page questions = questionService.findAll(pageable); + return ResponseEntity.ok(questions); + } + + @ResponseBody + @GetMapping("/{postId}") + @Operation( + summary = "질문 상세 조회", + description = "질문 상세 내용을 조회합니다", + responses = { + @ApiResponse(responseCode = "200", description = "요청 성공"), + @ApiResponse(responseCode = "404", description = "질문을 찾을 수 없음") + }) + public ResponseEntity getQuestionById(@PathVariable("postId") Long postId) { + try { + QuestionDTO.Response question = questionService.findById(postId); + return ResponseEntity.ok(question); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + } + + @ResponseBody + @GetMapping("/category/{category}") + @Operation( + summary = "카테고리별 질문 조회", + description = "카테고리별 질문을 조회합니다", + responses = { + @ApiResponse(responseCode = "200", description = "요청 성공"), + @ApiResponse(responseCode = "404", description = "질문을 찾을 수 없음") + }) + public ResponseEntity> getQuestionsByCategory(@PathVariable("category") String category, + Pageable pageable) { + Page questions = questionService.findByCategory(pageable, category); + return ResponseEntity.ok(questions); + } + // Update method here + // Delete method here +} diff --git a/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/domain/Answer.java b/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/domain/Answer.java new file mode 100644 index 0000000..b5e794a --- /dev/null +++ b/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/domain/Answer.java @@ -0,0 +1,37 @@ +package com.example.titto_backend.questionBoard.domain; + +import com.example.titto_backend.auth.domain.User; +import com.example.titto_backend.common.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Getter +@Setter +public class Answer extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(name = "author") + @ManyToOne + private User author; + + @Column(name = "answer_content", nullable = false, columnDefinition = "TEXT") + private String content; + + @JoinColumn(name = "question_id") + @ManyToOne + private Question question; + + //채택 여부 + @Column(name = "is_adopted") + private boolean isAdopted; +} diff --git a/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/domain/Department.java b/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/domain/Department.java index 2cc6093..1a8ba0d 100644 --- a/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/domain/Department.java +++ b/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/domain/Department.java @@ -2,9 +2,9 @@ public enum Department { HUMANITIES, // 인문 - MANAGEMANT, // 경영 + MANAGEMENT, // 경영 SOCIETY, // 사회 MEDIA_CONTENT, // 미디어 콘텐츠 FUTURE_FUSION, // 미래융합 - SOFEWARE // 소프트웨어 + SOFTWARE // 소프트웨어 } \ No newline at end of file diff --git a/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/domain/Question.java b/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/domain/Question.java index 2657961..7b9c201 100644 --- a/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/domain/Question.java +++ b/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/domain/Question.java @@ -1,6 +1,8 @@ package com.example.titto_backend.questionBoard.domain; +import com.example.titto_backend.auth.domain.User; import com.example.titto_backend.common.BaseEntity; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -8,21 +10,37 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import java.util.List; import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; @Entity -@Table(name = "questionPosts") +@Builder +@Getter +@Setter @AllArgsConstructor @NoArgsConstructor -public class QuestionPost extends BaseEntity { +public class Question extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "questionPost_id") private Long id; + @ManyToOne + @JoinColumn(name = "author") + private User author; + + @Column(name = "status") + private Status status; + @Enumerated(EnumType.STRING) @Column(name = "department") private Department department; @@ -33,7 +51,14 @@ public class QuestionPost extends BaseEntity { @Column(name = "question_content", nullable = false, columnDefinition = "TEXT") private String content; - @Column(name = "image_url") - private String imageUrl; + //TODO: 이미지, 조회수, 댓글은 나중에 추가 +// @Column(name = "image_url") +// private String imageUrl; +// +// @Column(name = "view") +// private int view; +// +// @OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE) +// private List answerList; } diff --git a/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/dto/QuestionDTO.java b/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/dto/QuestionDTO.java new file mode 100644 index 0000000..35c98ad --- /dev/null +++ b/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/dto/QuestionDTO.java @@ -0,0 +1,112 @@ +package com.example.titto_backend.questionBoard.dto; + +import com.example.titto_backend.questionBoard.domain.Department; +import com.example.titto_backend.questionBoard.domain.Question; +import com.example.titto_backend.questionBoard.domain.Status; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +public class QuestionDTO { + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "질문 글 작성") + public static class Request { + + @Schema(description = "제목") + private String title; + + @Schema(description = "내용") + private String content; + + @Schema(description = "이미지") + private List imageList; + + @Schema(description = "카테고리") + private String department; + + @Schema(description = "상태", example = "ACTIVE or INACTIVE") + private String status; + + @Schema(description = "조회수", defaultValue = "1") + private int view; + + public Question toEntity() { + return Question.builder() + .title(title) + .content(content) + .department(Department.valueOf(department.toUpperCase())) + .status(Status.valueOf(status.toUpperCase())) + .build(); + } + } + + @Getter + @Setter + @JsonInclude(JsonInclude.Include.NON_NULL) + @Schema(description = "질문 글 조회") + public static class Response { + + @Schema(description = "질문 ID") + private Long id; + + @Schema(description = "질문 작성자 ID") + private Long authorId; + + @Schema(description = "질문 작성자 닉네임") + private String authorNickname; + + @Schema(description = "카테고리") + private String department; + + @Schema(description = "상태") + private String status; + + @Schema(description = "제목") + private String title; + + @Schema(description = "내용") + private String content; + + @Schema(description = "작성일") + private LocalDateTime createdDate; + + public Response(Question question) { + this.id = question.getId(); + this.authorId = question.getAuthor().getId(); + this.authorNickname = question.getAuthor().getNickname(); + this.department = question.getDepartment().toString(); + this.status = question.getStatus().toString(); + this.title = question.getTitle(); + this.content = question.getContent(); + } + } + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "질문 글 수정") + public static class Update { + @Schema(description = "제목") + private String title; + @Schema(description = "내용") + private String content; + // private List imageList; + @Schema(description = "카테고리") + private Department department; + @Schema(description = "상태") + private Status status; + } +} diff --git a/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/repository/AnswerRepository.java b/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/repository/AnswerRepository.java new file mode 100644 index 0000000..9a4cc6a --- /dev/null +++ b/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/repository/AnswerRepository.java @@ -0,0 +1,8 @@ +package com.example.titto_backend.questionBoard.repository; + +import com.example.titto_backend.questionBoard.domain.Answer; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AnswerRepository extends JpaRepository { + +} diff --git a/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/repository/QuestionRepository.java b/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/repository/QuestionRepository.java new file mode 100644 index 0000000..829822b --- /dev/null +++ b/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/repository/QuestionRepository.java @@ -0,0 +1,11 @@ +package com.example.titto_backend.questionBoard.repository; + +import com.example.titto_backend.questionBoard.domain.Department; +import com.example.titto_backend.questionBoard.domain.Question; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface QuestionRepository extends JpaRepository { + Page findByDepartment(Pageable pageable, Department category); +} diff --git a/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/service/QuestionService.java b/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/service/QuestionService.java new file mode 100644 index 0000000..11d3305 --- /dev/null +++ b/Titto_Backend/src/main/java/com/example/titto_backend/questionBoard/service/QuestionService.java @@ -0,0 +1,91 @@ +package com.example.titto_backend.questionBoard.service; + +import com.example.titto_backend.auth.domain.User; +import com.example.titto_backend.auth.repository.UserRepository; +import com.example.titto_backend.common.exception.CustomException; +import com.example.titto_backend.common.exception.ErrorCode; +import com.example.titto_backend.questionBoard.domain.Department; +import com.example.titto_backend.questionBoard.domain.Question; +import com.example.titto_backend.questionBoard.domain.Status; +import com.example.titto_backend.questionBoard.dto.QuestionDTO; +import com.example.titto_backend.questionBoard.dto.QuestionDTO.Response; +import com.example.titto_backend.questionBoard.repository.QuestionRepository; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class QuestionService { + private final QuestionRepository questionRepository; + private final UserRepository userRepository; + + //Create + @Transactional + public QuestionDTO.Response save(String email, QuestionDTO.Request request) throws CustomException { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + System.out.println("여긴 지나감"); + + System.out.println(request.getDepartment()); + System.out.println(request.getStatus()); + System.out.println(request.getTitle()); + System.out.println(request.getContent()); + for (String image : request.getImageList()) { + System.out.println(image); + } + + Question question = Question.builder() + .title(request.getTitle()) + .author(user) + .content(request.getContent()) + .department(Department.valueOf(request.getDepartment().toUpperCase())) + .status(Status.valueOf(request.getStatus().toUpperCase())) + .build(); + return new Response(questionRepository.save(question)); + } + + @Transactional(readOnly = true) + public Page findAll(Pageable pageable) { + return questionRepository.findAll(pageable).map(QuestionDTO.Response::new); + } + + @Transactional(readOnly = true) + public Response findById(Long postId) { + Question question = questionRepository.findById(postId) + .orElseThrow(() -> new CustomException(ErrorCode.QUESTION_NOT_FOUND)); + return new Response(question); + } + + @Transactional(readOnly = true) + public Page findByCategory(Pageable pageable, String category) { + return questionRepository.findByDepartment(pageable, Department.valueOf(category.toUpperCase())) + .map(QuestionDTO.Response::new); + } + + //Update + @Transactional + public QuestionDTO.Response update(String email, QuestionDTO.Update update, Long id) throws CustomException { + Question oldQuestion = questionRepository.findById(id) + .orElseThrow(() -> new CustomException(ErrorCode.QUESTION_NOT_FOUND)); + Question newQuestion; + if (oldQuestion.getAuthor().getEmail().equals(email)) { + newQuestion = Question.builder() + .id(id) + .title(update.getTitle()) + .content(update.getContent()) + .department(Department.valueOf(String.valueOf(update.getDepartment()))) + .status(Status.valueOf(String.valueOf(update.getStatus()))) + .build(); + } else { + throw new CustomException(ErrorCode.USER_NOT_MATCH); + } + return new Response(newQuestion); + } + + //Delete +} diff --git a/Titto_Backend/src/test/java/com/example/titto_backend/question/QuestionTest.java b/Titto_Backend/src/test/java/com/example/titto_backend/question/QuestionTest.java new file mode 100644 index 0000000..e4029f1 --- /dev/null +++ b/Titto_Backend/src/test/java/com/example/titto_backend/question/QuestionTest.java @@ -0,0 +1,32 @@ +package com.example.titto_backend.question; + +import com.example.titto_backend.questionBoard.domain.Department; +import com.example.titto_backend.questionBoard.domain.Question; +import com.example.titto_backend.questionBoard.repository.QuestionRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class QuestionTest { + @Autowired + private QuestionRepository questionRepository; + + @Test + void JpaTest() { + Question q1 = new Question(); + q1.setTitle("title1"); + q1.setContent("content1"); + q1.setDepartment(Department.SOFEWARE); + + questionRepository.save(q1); + + Question q2 = new Question(); + q2.setTitle("title2"); + q2.setContent("content2"); + q2.setDepartment(Department.FUTURE_FUSION); + + questionRepository.save(q2); + + } +}