diff --git a/src/main/java/com/pbl/insaroad/domain/hangulsign/controller/HangulSignQuizAdminController.java b/src/main/java/com/pbl/insaroad/domain/hangulsign/controller/HangulSignQuizAdminController.java new file mode 100644 index 0000000..94bad43 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/hangulsign/controller/HangulSignQuizAdminController.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.hangulsign.controller; + +import java.util.List; + +import jakarta.validation.Valid; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +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 com.pbl.insaroad.domain.hangulsign.dto.request.QuizCreateRequest; +import com.pbl.insaroad.domain.hangulsign.dto.response.QuizCreateResponse; +import com.pbl.insaroad.domain.hangulsign.service.HangulSignQuizService; +import com.pbl.insaroad.global.response.BaseResponse; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@Tag(name = "HangulSignQuizAdmin", description = "한글 간판 퀴즈 관리자 API") +@RestController +@RequestMapping("/api/admin") +@RequiredArgsConstructor +public class HangulSignQuizAdminController { + + private final HangulSignQuizService quizService; + + @Operation(summary = "퀴즈 생성", description = "관리자가 새로운 한글 간판 퀴즈를 생성합니다.") + @PostMapping("/quizzes") + public ResponseEntity> createQuiz( + @Valid @RequestBody QuizCreateRequest request) { + QuizCreateResponse response = quizService.createQuiz(request); + return ResponseEntity.ok(BaseResponse.success("퀴즈 생성 성공", response)); + } + + @Operation(summary = "전체 퀴즈 조회", description = "관리자가 등록된 모든 퀴즈를 조회합니다.") + @GetMapping("/quizzes") + public ResponseEntity>> getAllQuizzes() { + List response = quizService.getAllQuizzes(); + return ResponseEntity.ok(BaseResponse.success("전체 퀴즈 조회 성공", response)); + } +} diff --git a/src/main/java/com/pbl/insaroad/domain/hangulsign/controller/HangulSignQuizController.java b/src/main/java/com/pbl/insaroad/domain/hangulsign/controller/HangulSignQuizController.java new file mode 100644 index 0000000..974362e --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/hangulsign/controller/HangulSignQuizController.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.hangulsign.controller; + +import jakarta.validation.Valid; + +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.RestController; + +import com.pbl.insaroad.domain.hangulsign.dto.request.AnswerRequest; +import com.pbl.insaroad.domain.hangulsign.dto.response.AnswerResponse; +import com.pbl.insaroad.domain.hangulsign.dto.response.QuizResponse; +import com.pbl.insaroad.domain.hangulsign.service.HangulSignQuizService; +import com.pbl.insaroad.global.response.BaseResponse; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@Tag(name = "HangulSignQuiz", description = "한글 간판 퀴즈 API") +@RestController +@RequestMapping("/api/hangul-sign/quizzes") +@RequiredArgsConstructor +public class HangulSignQuizController { + + private final HangulSignQuizService quizService; + + @Operation(summary = "랜덤 퀴즈 조회", description = "한글 간판 퀴즈를 랜덤으로 조회합니다.") + @GetMapping("/random") + public ResponseEntity> getRandomQuiz() { + QuizResponse response = quizService.getRandomQuiz(); + return ResponseEntity.ok(BaseResponse.success("랜덤 퀴즈 조회 성공", response)); + } + + @Operation(summary = "퀴즈 정답 제출", description = "사용자가 선택한 답을 제출하고 정답 여부를 확인합니다.") + @PostMapping("/{quizId}/answer") + public ResponseEntity> submitAnswer( + @PathVariable Long quizId, @Valid @RequestBody AnswerRequest request) { + AnswerResponse response = quizService.submitAnswer(quizId, request); + return ResponseEntity.ok(BaseResponse.success("정답 제출 완료", response)); + } +} diff --git a/src/main/java/com/pbl/insaroad/domain/hangulsign/dto/request/AnswerRequest.java b/src/main/java/com/pbl/insaroad/domain/hangulsign/dto/request/AnswerRequest.java new file mode 100644 index 0000000..9fcfa36 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/hangulsign/dto/request/AnswerRequest.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.hangulsign.dto.request; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class AnswerRequest { + + @NotBlank(message = "사용자 코드는 필수입니다.") + @Schema(description = "사용자 코드 (3자리)", example = "123") + private String userCode; + + @NotNull(message = "선택지는 필수입니다.") @Min(value = 1, message = "선택지는 1, 2, 3 중 하나여야 합니다.") + @Max(value = 3, message = "선택지는 1, 2, 3 중 하나여야 합니다.") + @Schema(description = "사용자가 선택한 답 (1, 2, 3)", example = "1") + private Integer userAnswer; +} diff --git a/src/main/java/com/pbl/insaroad/domain/hangulsign/dto/request/QuizCreateRequest.java b/src/main/java/com/pbl/insaroad/domain/hangulsign/dto/request/QuizCreateRequest.java new file mode 100644 index 0000000..d43a2ea --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/hangulsign/dto/request/QuizCreateRequest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.hangulsign.dto.request; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class QuizCreateRequest { + + @NotBlank(message = "문제 이미지 URL은 필수입니다.") + @Schema(description = "문제 이미지 URL", example = "https://example.com/question.jpg") + private String questionImageUrl; + + @NotBlank(message = "정답 이미지 URL은 필수입니다.") + @Schema(description = "정답 이미지 URL", example = "https://example.com/answer.jpg") + private String answerImageUrl; + + @NotBlank(message = "선택지 1은 필수입니다.") + @Schema(description = "선택지 1", example = "스타벅수") + private String choice1; + + @NotBlank(message = "선택지 2는 필수입니다.") + @Schema(description = "선택지 2", example = "스타벅스") + private String choice2; + + @NotBlank(message = "선택지 3은 필수입니다.") + @Schema(description = "선택지 3", example = "스타벜스") + private String choice3; + + @NotNull(message = "정답 선택지는 필수입니다.") @Min(value = 1, message = "정답 선택지는 1, 2, 3 중 하나여야 합니다.") + @Max(value = 3, message = "정답 선택지는 1, 2, 3 중 하나여야 합니다.") + @Schema(description = "정답 선택지 번호", example = "2") + private Integer correctChoice; +} diff --git a/src/main/java/com/pbl/insaroad/domain/hangulsign/dto/response/AnswerResponse.java b/src/main/java/com/pbl/insaroad/domain/hangulsign/dto/response/AnswerResponse.java new file mode 100644 index 0000000..8b80ef7 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/hangulsign/dto/response/AnswerResponse.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.hangulsign.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AnswerResponse { + + @Schema(description = "정답 여부", example = "true") + private Boolean isCorrect; + + @Schema(description = "정답 이미지 URL (정답인 경우에만 제공)", example = "https://example.com/answer.jpg") + private String answerImageUrl; + + @Schema(description = "현재 스테이지", example = "3") + private Integer currentStage; +} diff --git a/src/main/java/com/pbl/insaroad/domain/hangulsign/dto/response/QuizCreateResponse.java b/src/main/java/com/pbl/insaroad/domain/hangulsign/dto/response/QuizCreateResponse.java new file mode 100644 index 0000000..cd636a9 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/hangulsign/dto/response/QuizCreateResponse.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.hangulsign.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QuizCreateResponse { + + @Schema(description = "문제 이미지 URL", example = "https://example.com/question.jpg") + private String questionImageUrl; + + @Schema(description = "정답 이미지 URL", example = "https://example.com/answer.jpg") + private String answerImageUrl; + + @Schema(description = "선택지 1", example = "김밥천국") + private String choice1; + + @Schema(description = "선택지 2", example = "김밥나라") + private String choice2; + + @Schema(description = "선택지 3", example = "김밥왕국") + private String choice3; + + @Schema(description = "정답 선택지 번호", example = "1") + private Integer correctChoice; +} diff --git a/src/main/java/com/pbl/insaroad/domain/hangulsign/dto/response/QuizResponse.java b/src/main/java/com/pbl/insaroad/domain/hangulsign/dto/response/QuizResponse.java new file mode 100644 index 0000000..0fc6581 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/hangulsign/dto/response/QuizResponse.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.hangulsign.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QuizResponse { + + @Schema(description = "퀴즈 ID", example = "1") + private Long id; + + @Schema(description = "문제 이미지 URL", example = "https://example.com/question.jpg") + private String questionImageUrl; + + @Schema(description = "선택지 1", example = "김밥천국") + private String choice1; + + @Schema(description = "선택지 2", example = "김밥나라") + private String choice2; + + @Schema(description = "선택지 3", example = "김밥왕국") + private String choice3; +} diff --git a/src/main/java/com/pbl/insaroad/domain/hangulsign/entity/HangulSignQuiz.java b/src/main/java/com/pbl/insaroad/domain/hangulsign/entity/HangulSignQuiz.java new file mode 100644 index 0000000..2d50789 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/hangulsign/entity/HangulSignQuiz.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.hangulsign.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import com.pbl.insaroad.global.common.BaseTimeEntity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "hangul_sign_quizzes") +public class HangulSignQuiz extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "question_image_url", nullable = false, length = 255) + private String questionImageUrl; + + @Column(name = "answer_image_url", nullable = false, length = 255) + private String answerImageUrl; + + @Column(name = "choice1", nullable = false, length = 20) + private String choice1; + + @Column(name = "choice2", nullable = false, length = 20) + private String choice2; + + @Column(name = "choice3", nullable = false, length = 20) + private String choice3; + + @Column(name = "correct_choice", nullable = false, columnDefinition = "TINYINT") + private Integer correctChoice; +} diff --git a/src/main/java/com/pbl/insaroad/domain/hangulsign/exception/HangulSignQuizErrorCode.java b/src/main/java/com/pbl/insaroad/domain/hangulsign/exception/HangulSignQuizErrorCode.java new file mode 100644 index 0000000..8811056 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/hangulsign/exception/HangulSignQuizErrorCode.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.hangulsign.exception; + +import org.springframework.http.HttpStatus; + +import com.pbl.insaroad.global.exception.model.BaseErrorCode; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum HangulSignQuizErrorCode implements BaseErrorCode { + QUIZ_NOT_FOUND("QUIZ_4041", "퀴즈를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + USER_NOT_FOUND("QUIZ_4042", "사용자를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + INVALID_CHOICE("QUIZ_4001", "유효하지 않은 선택지입니다. (1, 2, 3 중 선택)", HttpStatus.BAD_REQUEST); + + private final String code; + private final String message; + private final HttpStatus status; +} diff --git a/src/main/java/com/pbl/insaroad/domain/hangulsign/mapper/HangulSignQuizMapper.java b/src/main/java/com/pbl/insaroad/domain/hangulsign/mapper/HangulSignQuizMapper.java new file mode 100644 index 0000000..6441c89 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/hangulsign/mapper/HangulSignQuizMapper.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.hangulsign.mapper; + +import org.springframework.stereotype.Component; + +import com.pbl.insaroad.domain.hangulsign.dto.request.QuizCreateRequest; +import com.pbl.insaroad.domain.hangulsign.dto.response.AnswerResponse; +import com.pbl.insaroad.domain.hangulsign.dto.response.QuizCreateResponse; +import com.pbl.insaroad.domain.hangulsign.dto.response.QuizResponse; +import com.pbl.insaroad.domain.hangulsign.entity.HangulSignQuiz; + +@Component +public class HangulSignQuizMapper { + + public QuizResponse toQuizResponse(HangulSignQuiz quiz) { + return QuizResponse.builder() + .id(quiz.getId()) + .questionImageUrl(quiz.getQuestionImageUrl()) + .choice1(quiz.getChoice1()) + .choice2(quiz.getChoice2()) + .choice3(quiz.getChoice3()) + .build(); + } + + public QuizCreateResponse toQuizCreateResponse(HangulSignQuiz quiz) { + return QuizCreateResponse.builder() + .questionImageUrl(quiz.getQuestionImageUrl()) + .answerImageUrl(quiz.getAnswerImageUrl()) + .choice1(quiz.getChoice1()) + .choice2(quiz.getChoice2()) + .choice3(quiz.getChoice3()) + .correctChoice(quiz.getCorrectChoice()) + .build(); + } + + public HangulSignQuiz toEntity(QuizCreateRequest request) { + return HangulSignQuiz.builder() + .questionImageUrl(request.getQuestionImageUrl()) + .answerImageUrl(request.getAnswerImageUrl()) + .choice1(request.getChoice1()) + .choice2(request.getChoice2()) + .choice3(request.getChoice3()) + .correctChoice(request.getCorrectChoice()) + .build(); + } + + public AnswerResponse toAnswerResponse( + boolean isCorrect, String answerImageUrl, Integer currentStage) { + return AnswerResponse.builder() + .isCorrect(isCorrect) + .answerImageUrl(answerImageUrl) + .currentStage(currentStage) + .build(); + } +} diff --git a/src/main/java/com/pbl/insaroad/domain/hangulsign/repository/HangulSignQuizRepository.java b/src/main/java/com/pbl/insaroad/domain/hangulsign/repository/HangulSignQuizRepository.java new file mode 100644 index 0000000..5a50cdc --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/hangulsign/repository/HangulSignQuizRepository.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.hangulsign.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import com.pbl.insaroad.domain.hangulsign.entity.HangulSignQuiz; + +public interface HangulSignQuizRepository extends JpaRepository { + + @Query(value = "SELECT * FROM hangul_sign_quizzes ORDER BY RAND() LIMIT 1", nativeQuery = true) + Optional findRandomQuiz(); +} diff --git a/src/main/java/com/pbl/insaroad/domain/hangulsign/service/HangulSignQuizService.java b/src/main/java/com/pbl/insaroad/domain/hangulsign/service/HangulSignQuizService.java new file mode 100644 index 0000000..96b20b0 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/hangulsign/service/HangulSignQuizService.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.hangulsign.service; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.pbl.insaroad.domain.hangulsign.dto.request.AnswerRequest; +import com.pbl.insaroad.domain.hangulsign.dto.request.QuizCreateRequest; +import com.pbl.insaroad.domain.hangulsign.dto.response.AnswerResponse; +import com.pbl.insaroad.domain.hangulsign.dto.response.QuizCreateResponse; +import com.pbl.insaroad.domain.hangulsign.dto.response.QuizResponse; +import com.pbl.insaroad.domain.hangulsign.entity.HangulSignQuiz; +import com.pbl.insaroad.domain.hangulsign.exception.HangulSignQuizErrorCode; +import com.pbl.insaroad.domain.hangulsign.mapper.HangulSignQuizMapper; +import com.pbl.insaroad.domain.hangulsign.repository.HangulSignQuizRepository; +import com.pbl.insaroad.domain.user.entity.User; +import com.pbl.insaroad.domain.user.repository.UserRepository; +import com.pbl.insaroad.global.exception.CustomException; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class HangulSignQuizService { + + private final HangulSignQuizRepository quizRepository; + private final UserRepository userRepository; + private final HangulSignQuizMapper quizMapper; + + @Transactional(readOnly = true) + public QuizResponse getRandomQuiz() { + HangulSignQuiz quiz = + quizRepository + .findRandomQuiz() + .orElseThrow(() -> new CustomException(HangulSignQuizErrorCode.QUIZ_NOT_FOUND)); + + return quizMapper.toQuizResponse(quiz); + } + + public AnswerResponse submitAnswer(Long quizId, AnswerRequest request) { + // 퀴즈 조회 + HangulSignQuiz quiz = + quizRepository + .findById(quizId) + .orElseThrow(() -> new CustomException(HangulSignQuizErrorCode.QUIZ_NOT_FOUND)); + + // 사용자 조회 + User user = + userRepository + .findByCode(request.getUserCode()) + .orElseThrow(() -> new CustomException(HangulSignQuizErrorCode.USER_NOT_FOUND)); + + // 선택지 유효성 검증 (명시적으로 한 번 더 검증) + if (request.getUserAnswer() < 1 || request.getUserAnswer() > 3) { + throw new CustomException(HangulSignQuizErrorCode.INVALID_CHOICE); + } + + // 정답 체크 + boolean isCorrect = quiz.getCorrectChoice().equals(request.getUserAnswer()); + + String answerImageUrl = null; + + // 정답인 경우에만 스테이지 업데이트 및 이미지 URL 반환 + if (isCorrect) { + answerImageUrl = quiz.getAnswerImageUrl(); + + // user.getStage() == 2일 때만 nextStage() 호출 + if (user.getStage() == 2) { + user.nextStage(); + userRepository.save(user); + log.info("[서비스] 사용자 {}의 스테이지가 {}로 업데이트되었습니다.", user.getId(), user.getStage()); + } + } + + return quizMapper.toAnswerResponse(isCorrect, answerImageUrl, user.getStage()); + } + + public QuizCreateResponse createQuiz(QuizCreateRequest request) { + HangulSignQuiz quiz = quizMapper.toEntity(request); + HangulSignQuiz savedQuiz = quizRepository.save(quiz); + + log.info("한글 간판 퀴즈가 생성되었습니다. ID: {}", savedQuiz.getId()); + + return quizMapper.toQuizCreateResponse(savedQuiz); + } + + @Transactional(readOnly = true) + public List getAllQuizzes() { + List quizzes = quizRepository.findAll(); + + return quizzes.stream().map(quizMapper::toQuizCreateResponse).toList(); + } +} diff --git a/src/main/java/com/pbl/insaroad/domain/user/repository/UserRepository.java b/src/main/java/com/pbl/insaroad/domain/user/repository/UserRepository.java index acc9696..ae76f85 100644 --- a/src/main/java/com/pbl/insaroad/domain/user/repository/UserRepository.java +++ b/src/main/java/com/pbl/insaroad/domain/user/repository/UserRepository.java @@ -3,6 +3,7 @@ */ package com.pbl.insaroad.domain.user.repository; +import java.util.Optional; import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; @@ -14,4 +15,6 @@ public interface UserRepository extends JpaRepository { @Query("select u.code from User u") Set findAllCodes(); + + Optional findByCode(String code); }