diff --git a/src/main/java/com/olive/pribee/PribeeApplication.java b/src/main/java/com/olive/pribee/PribeeApplication.java index 8981a63..140037e 100644 --- a/src/main/java/com/olive/pribee/PribeeApplication.java +++ b/src/main/java/com/olive/pribee/PribeeApplication.java @@ -2,9 +2,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; @EnableMongoRepositories +@EnableJpaAuditing @SpringBootApplication public class PribeeApplication { diff --git a/src/main/java/com/olive/pribee/global/common/filter/JwtAuthenticationFilter.java b/src/main/java/com/olive/pribee/global/common/filter/JwtAuthenticationFilter.java index bc9446c..83dc165 100644 --- a/src/main/java/com/olive/pribee/global/common/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/olive/pribee/global/common/filter/JwtAuthenticationFilter.java @@ -40,7 +40,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse // accessToken 이 필요없는 경우 필터링 없이 처리 if (requestURI.startsWith("/api/auth/token") || - requestURI.startsWith("/api/auth/login/facebook")) { + requestURI.startsWith("/api/auth/login/facebook") || + requestURI.startsWith("/api/quiz/**")) { chain.doFilter(request, response); return; } diff --git a/src/main/java/com/olive/pribee/global/config/SecurityConfig.java b/src/main/java/com/olive/pribee/global/config/SecurityConfig.java index ad7a441..29a029d 100644 --- a/src/main/java/com/olive/pribee/global/config/SecurityConfig.java +++ b/src/main/java/com/olive/pribee/global/config/SecurityConfig.java @@ -45,6 +45,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers( "/api/auth/token", "/api/auth/login/facebook", + "/api/quiz/**", "/swagger-ui/**", "/webjars/**", "/swagger-ui.html", diff --git a/src/main/java/com/olive/pribee/module/auth/controller/MemberController.java b/src/main/java/com/olive/pribee/module/auth/controller/MemberController.java index 817809f..73f1097 100644 --- a/src/main/java/com/olive/pribee/module/auth/controller/MemberController.java +++ b/src/main/java/com/olive/pribee/module/auth/controller/MemberController.java @@ -11,7 +11,7 @@ import com.olive.pribee.global.common.DataResponseDto; import com.olive.pribee.global.common.ResponseDto; import com.olive.pribee.module.auth.domain.entity.Member; -import com.olive.pribee.module.auth.dto.res.LoginResDto; +import com.olive.pribee.module.auth.dto.res.LoginRes; import com.olive.pribee.module.auth.service.MemberService; import lombok.RequiredArgsConstructor; @@ -26,13 +26,13 @@ public class MemberController implements MemberControllerDocs { @GetMapping("/login/facebook") public ResponseEntity getLogin(@RequestHeader("facebook-code") String code){ - LoginResDto resDto = memberService.getAccessToken(code); + LoginRes resDto = memberService.getAccessToken(code); return ResponseEntity.status(201).body(DataResponseDto.of(resDto, 201)); } @GetMapping("/token") public ResponseEntity getAccessToken(@RequestHeader("Authorization-Refresh") String refreshToken) { - LoginResDto resDto = memberService.getNewAccessToken(refreshToken); + LoginRes resDto = memberService.getNewAccessToken(refreshToken); return ResponseEntity.status(201).body(DataResponseDto.of(resDto, 201)); } diff --git a/src/main/java/com/olive/pribee/module/auth/domain/repository/MemberRepository.java b/src/main/java/com/olive/pribee/module/auth/domain/repository/MemberRepository.java index 2ca31dc..13d4adb 100644 --- a/src/main/java/com/olive/pribee/module/auth/domain/repository/MemberRepository.java +++ b/src/main/java/com/olive/pribee/module/auth/domain/repository/MemberRepository.java @@ -3,9 +3,11 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; import com.olive.pribee.module.auth.domain.entity.Member; +@Repository public interface MemberRepository extends JpaRepository { Optional findByFacebookId(String facebookId); } diff --git a/src/main/java/com/olive/pribee/module/auth/dto/res/LoginResDto.java b/src/main/java/com/olive/pribee/module/auth/dto/res/LoginRes.java similarity index 80% rename from src/main/java/com/olive/pribee/module/auth/dto/res/LoginResDto.java rename to src/main/java/com/olive/pribee/module/auth/dto/res/LoginRes.java index 0d5c852..8c4a4ae 100644 --- a/src/main/java/com/olive/pribee/module/auth/dto/res/LoginResDto.java +++ b/src/main/java/com/olive/pribee/module/auth/dto/res/LoginRes.java @@ -7,15 +7,15 @@ @Getter @Builder(access = AccessLevel.PRIVATE) -public class LoginResDto { +public class LoginRes { @Schema(description = "accessToken", example = "eyJ0eXAiOiJKV1QiLCJhbGc...") private final String accessToken; @Schema(description = "refreshToken", example = "eyJ0eXAiOiJKV1QiLCJhbGc...") private final String refreshToken; - public static LoginResDto of(String accessToken, String refreshToken) { - return LoginResDto.builder() + public static LoginRes of(String accessToken, String refreshToken) { + return LoginRes.builder() .accessToken(accessToken) .refreshToken(refreshToken) .build(); diff --git a/src/main/java/com/olive/pribee/module/auth/service/MemberService.java b/src/main/java/com/olive/pribee/module/auth/service/MemberService.java index bd624f3..3601890 100644 --- a/src/main/java/com/olive/pribee/module/auth/service/MemberService.java +++ b/src/main/java/com/olive/pribee/module/auth/service/MemberService.java @@ -13,7 +13,7 @@ import com.olive.pribee.module.auth.domain.repository.MemberRepository; import com.olive.pribee.module.auth.dto.res.FacebookAuthRes; import com.olive.pribee.module.auth.dto.res.FacebookUserInfoRes; -import com.olive.pribee.module.auth.dto.res.LoginResDto; +import com.olive.pribee.module.auth.dto.res.LoginRes; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; @@ -32,7 +32,7 @@ public class MemberService { // facebook code 기반 facebook 로그인을 통한 접근 jwt 발급 @Transactional - public LoginResDto getAccessToken(String code) { + public LoginRes getAccessToken(String code) { // code 기반 facebook ID 조회 FacebookAuthRes facebookAuthRes = facebookAuthService.getFacebookIdWithToken(code).block(); if (facebookAuthRes == null) { @@ -68,12 +68,12 @@ public LoginResDto getAccessToken(String code) { redisUtil.setOpsForValue(member.getId() + "_refresh", jwtVo.getRefreshToken(), jwtTokenProvider.getREFRESH_TOKEN_EXPIRATION()); - return LoginResDto.of(jwtVo.getAccessToken(), jwtVo.getRefreshToken()); + return LoginRes.of(jwtVo.getAccessToken(), jwtVo.getRefreshToken()); } // refresh token 으로 새로운 accessToken 발급 @Transactional - public LoginResDto getNewAccessToken(String refreshToken) { + public LoginRes getNewAccessToken(String refreshToken) { if (refreshToken.isBlank()) { throw new AppException(GlobalErrorCode.REFRESH_TOKEN_REQUIRED); } @@ -94,21 +94,25 @@ public LoginResDto getNewAccessToken(String refreshToken) { redisUtil.setOpsForValue(tokenMember.getId() + "_refresh", jwtVo.getRefreshToken(), jwtTokenProvider.getREFRESH_TOKEN_EXPIRATION()); - return LoginResDto.of(jwtVo.getAccessToken(), jwtVo.getRefreshToken()); + return LoginRes.of(jwtVo.getAccessToken(), jwtVo.getRefreshToken()); } // 로그아웃 @Transactional public void deleteRefreshToken(Member member) { - // 사용자 refreshToken 삭제 - redisUtil.delete(member.getId() + "_refresh"); + deleteMemberRedis(member); } // 탈퇴 @Transactional public void deleteMember(Member member) { // 사용자 정보 삭제 + deleteMemberRedis(member); memberRepository.delete(member); } + private void deleteMemberRedis(Member member){ + redisUtil.delete(member.getId() + "_fb_access"); + redisUtil.delete(member.getId() + "_refresh"); + } } diff --git a/src/main/java/com/olive/pribee/module/quiz/controller/QuizController.java b/src/main/java/com/olive/pribee/module/quiz/controller/QuizController.java new file mode 100644 index 0000000..01b86d9 --- /dev/null +++ b/src/main/java/com/olive/pribee/module/quiz/controller/QuizController.java @@ -0,0 +1,39 @@ +package com.olive.pribee.module.quiz.controller; + +import java.util.List; + +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.olive.pribee.global.common.DataResponseDto; +import com.olive.pribee.global.common.ResponseDto; +import com.olive.pribee.module.quiz.dto.req.QuizAnswerListReq; +import com.olive.pribee.module.quiz.dto.res.QuizRandomRes; +import com.olive.pribee.module.quiz.service.QuizService; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/api/quiz/") +@RequiredArgsConstructor +public class QuizController implements QuizControllerDocs { + private final QuizService quizService; + + @GetMapping + public ResponseEntity getRandomThreeQuiz() { + List resDto = quizService.getRandomThreeQuiz(); + return ResponseEntity.status(200).body(DataResponseDto.of(resDto, 200)); + } + + @PostMapping + public ResponseEntity postQuizWrongPortion(@RequestBody @Valid QuizAnswerListReq quizAnswerReqs) { + quizService.postQuizWrongPortion(quizAnswerReqs); + return ResponseEntity.ok(ResponseDto.of(200)); + } + +} diff --git a/src/main/java/com/olive/pribee/module/quiz/controller/QuizControllerDocs.java b/src/main/java/com/olive/pribee/module/quiz/controller/QuizControllerDocs.java new file mode 100644 index 0000000..4e185d0 --- /dev/null +++ b/src/main/java/com/olive/pribee/module/quiz/controller/QuizControllerDocs.java @@ -0,0 +1,85 @@ +package com.olive.pribee.module.quiz.controller; + +import org.springframework.http.ResponseEntity; + +import com.olive.pribee.global.common.ResponseDto; +import com.olive.pribee.module.quiz.dto.req.QuizAnswerListReq; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "Quiz", description = "퀴즈 관련 API") +public interface QuizControllerDocs { + + @Operation(summary = "랜덤 퀴즈 3개 추출 API", description = "") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "Created", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{\n" + + " \"code\": 200,\n" + + " \"message\": \"OK\",\n" + + " \"data\": [\n" + + " {\n" + + " \"id\": 3,\n" + + " \"question\": \"생년월일을 SNS 프로필에서 비공개로 설정했지만, 아이디 또는 비밀번호에 생일과 연관된 숫자를 사용했다.\",\n" + + " \"answer1\": \"괜찮다\",\n" + + " \"answer2\": \"위험하다\",\n" + + " \"answerIsOne\": false,\n" + + " \"reason\": \"생년월일은 해킹 공격자가 비밀번호를 추측할 때 가장 먼저 시도하는 정보 중 하나다. SNS 비밀번호는 생일과 무관한 강력한 조합으로 설정하는 것이 안전하다.\",\n" + + " \"wrongPortion\": 0\n" + + " },\n" + + " {\n" + + " \"id\": 9,\n" + + " \"question\": \"친구와 찍은 사진을 SNS에 올릴 때 친구에게 허락을 받지 않았다.\",\n" + + " \"answer1\": \"괜찮다\",\n" + + " \"answer2\": \"위험하다\",\n" + + " \"answerIsOne\": false,\n" + + " \"reason\": \"본인은 괜찮더라도 친구가 사진 공개를 원치 않을 수 있으며, 초상권 및 개인정보 보호 문제가 발생할 수 있다.\",\n" + + " \"wrongPortion\": 0\n" + + " },\n" + + " {\n" + + " \"id\": 18,\n" + + " \"question\": \"회사 이메일로 온 첨부파일을 열기 전에 해야 할 행동은?\",\n" + + " \"answer1\": \"발신자를 확인하고, 출처가 불분명하면 열지 않는다.\",\n" + + " \"answer2\": \"업무 관련 내용이라면 바로 다운로드해서 실행한다.\",\n" + + " \"answerIsOne\": true,\n" + + " \"reason\": \"악성코드가 포함된 이메일 첨부파일은 기업 내 랜섬웨어 감염을 일으킬 수 있다.\",\n" + + " \"wrongPortion\": 0\n" + + " }\n" + + " ]\n" + + "}") + ) + ) + }) + ResponseEntity getRandomThreeQuiz(); + + @Operation(summary = "정답률 처리 API", description = "id 와 정답 여부를 받아 정답률을 처리하는 API 입니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{ \"code\": 200, \"message\": \"OK\" }") + ) + ), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "{ \"code\": 400, \"message\": \"해당하는 퀴즈가 없습니다.\" }") + ) + ) + }) + ResponseEntity postQuizWrongPortion(QuizAnswerListReq quizAnswerReqs); + +} + diff --git a/src/main/java/com/olive/pribee/module/quiz/domain/entity/Quiz.java b/src/main/java/com/olive/pribee/module/quiz/domain/entity/Quiz.java new file mode 100644 index 0000000..f565dd6 --- /dev/null +++ b/src/main/java/com/olive/pribee/module/quiz/domain/entity/Quiz.java @@ -0,0 +1,70 @@ +package com.olive.pribee.module.quiz.domain.entity; + +import com.olive.pribee.global.common.BaseTime; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "quiz") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder(access = AccessLevel.PRIVATE) +public class Quiz extends BaseTime { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + private String question; + + @NotNull + private String answer1; + + @NotNull + private String answer2; + + @NotNull + private Boolean answerIsOne; + + @NotNull + private String reason; + + @NotNull + private int participate; + + @NotNull + private int wrong; + + public static Quiz of(@NotNull String question, @NotNull String answer1, @NotNull String answer2, + @NotNull Boolean answerIsOne, @NotNull String reason) { + return Quiz.builder() + .question(question) + .answer1(answer1) + .answer2(answer2) + .answerIsOne(answerIsOne) + .reason(reason) + .participate(0) + .wrong(0) + .build(); + } + + public void plusParticipate() { + this.participate += 1; + } + + public void plusWrong() { + this.wrong += 1; + } +} diff --git a/src/main/java/com/olive/pribee/module/quiz/domain/repository/QuizRepository.java b/src/main/java/com/olive/pribee/module/quiz/domain/repository/QuizRepository.java new file mode 100644 index 0000000..34b0c08 --- /dev/null +++ b/src/main/java/com/olive/pribee/module/quiz/domain/repository/QuizRepository.java @@ -0,0 +1,18 @@ +package com.olive.pribee.module.quiz.domain.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import com.olive.pribee.module.quiz.domain.entity.Quiz; + +@Repository +public interface QuizRepository extends JpaRepository { + @Query(value = "SELECT * FROM quiz " + + "WHERE id >= (SELECT FLOOR(RAND() * (SELECT MAX(id) FROM quiz))) " + + "ORDER BY id LIMIT 3", nativeQuery = true) + List findTop3RandomOptimized(); + +} diff --git a/src/main/java/com/olive/pribee/module/quiz/dto/req/QuizAnswerListReq.java b/src/main/java/com/olive/pribee/module/quiz/dto/req/QuizAnswerListReq.java new file mode 100644 index 0000000..6b08d18 --- /dev/null +++ b/src/main/java/com/olive/pribee/module/quiz/dto/req/QuizAnswerListReq.java @@ -0,0 +1,33 @@ +package com.olive.pribee.module.quiz.dto.req; + +import java.util.List; + +import org.hibernate.validator.constraints.UniqueElements; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; + +@Schema(description = "퀴즈 답변 체크 요청 DTO") +public record QuizAnswerListReq( + @Schema(description = "요청하는 퀴즈 및 정답 여부 리스트", example = "[\n" + + " {\n" + + " \"id\": 1,\n" + + " \"isCorrect\": false\n" + + " },\n" + + " {\n" + + " \"id\": 3,\n" + + " \"isCorrect\": true\n" + + " },\n" + + " {\n" + + " \"id\": 4,\n" + + " \"isCorrect\": false\n" + + " }\n" + + "]") + @NotEmpty + @Size(min = 1, message = "퀴즈 답변 리스트는 최소 1개 이상이어야 합니다.") + @UniqueElements + List quizAnswerReqs + +) { +} \ No newline at end of file diff --git a/src/main/java/com/olive/pribee/module/quiz/dto/req/QuizAnswerReq.java b/src/main/java/com/olive/pribee/module/quiz/dto/req/QuizAnswerReq.java new file mode 100644 index 0000000..efff155 --- /dev/null +++ b/src/main/java/com/olive/pribee/module/quiz/dto/req/QuizAnswerReq.java @@ -0,0 +1,16 @@ +package com.olive.pribee.module.quiz.dto.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; + +@Schema(description = "퀴즈 답변 체크 요청 DTO") +public record QuizAnswerReq( + @Schema(description = "요청하는 퀴즈 id", example = "1") + @NotEmpty + Long id, + @Schema(description = "퀴즈 정답 여부", example = "true") + @NotEmpty + Boolean isCorrect + +) { +} \ No newline at end of file diff --git a/src/main/java/com/olive/pribee/module/quiz/dto/res/QuizRandomRes.java b/src/main/java/com/olive/pribee/module/quiz/dto/res/QuizRandomRes.java new file mode 100644 index 0000000..2035aa8 --- /dev/null +++ b/src/main/java/com/olive/pribee/module/quiz/dto/res/QuizRandomRes.java @@ -0,0 +1,42 @@ +package com.olive.pribee.module.quiz.dto.res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder(access = AccessLevel.PRIVATE) +@Schema(description = "퀴즈 랜덤 제공 응답 DTO") +public class QuizRandomRes { + @Schema(description = "퀴즈 id", example = "1") + private final Long id; + @Schema(description = "퀴즈 질문", example = "회사 임원으로부터 \"기밀 문서를 확인해 주세요\"라는 이메일이 왔다. 어떻게 해야 할까?") + private final String question; + @Schema(description = "답변 항목 1", example = "이메일 주소를 꼼꼼히 확인하고, 수상하면 별도 연락하여 진위를 확인한다.") + private final String answer1; + @Schema(description = "답변 항목 2", example = "상사 이메일이므로 즉시 문서를 다운로드하여 확인한다.") + private final String answer2; + @Schema(description = "답변 항목 1이 정답인지 여부", example = "true") + private final Boolean answerIsOne; + @Schema(description = "답변 설명", example = "타깃 스피어 피싱 공격은 특정 조직을 노리는 정교한 공격이므로, 반드시 진위를 확인해야 한다.") + private final String reason; + @Schema(description = "오답자 비율", example = "32.23") + private final float wrongPortion; + + public static QuizRandomRes of(Long id, String question, String answer1, String answer2, Boolean answerIsOne, + String reason, int participant, int wrong) { + float wrongPortion = participant == 0 ? 0 : (float)wrong / participant * 100; + + return QuizRandomRes.builder() + .id(id) + .question(question) + .answer1(answer1) + .answer2(answer2) + .answerIsOne(answerIsOne) + .reason(reason) + .wrongPortion(Math.round(wrongPortion * 100) / 100.0f) + .build(); + } + +} diff --git a/src/main/java/com/olive/pribee/module/quiz/error/QuizErrorCode.java b/src/main/java/com/olive/pribee/module/quiz/error/QuizErrorCode.java new file mode 100644 index 0000000..1e20dac --- /dev/null +++ b/src/main/java/com/olive/pribee/module/quiz/error/QuizErrorCode.java @@ -0,0 +1,20 @@ +package com.olive.pribee.module.quiz.error; + +import org.springframework.http.HttpStatus; + +import com.olive.pribee.global.error.ErrorCode; + +import lombok.Getter; + +@Getter +public enum QuizErrorCode implements ErrorCode { + INVALID_QUIZ_ID(HttpStatus.NOT_FOUND, "해당하는 퀴즈가 없습니다."); + + private final HttpStatus httpStatus; + private final String message; + + QuizErrorCode(HttpStatus httpStatus, String message) { + this.httpStatus = httpStatus; + this.message = message; + } +} diff --git a/src/main/java/com/olive/pribee/module/quiz/service/QuizService.java b/src/main/java/com/olive/pribee/module/quiz/service/QuizService.java new file mode 100644 index 0000000..47e7a70 --- /dev/null +++ b/src/main/java/com/olive/pribee/module/quiz/service/QuizService.java @@ -0,0 +1,69 @@ +package com.olive.pribee.module.quiz.service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.olive.pribee.global.error.exception.AppException; +import com.olive.pribee.module.quiz.domain.entity.Quiz; +import com.olive.pribee.module.quiz.domain.repository.QuizRepository; +import com.olive.pribee.module.quiz.dto.req.QuizAnswerListReq; +import com.olive.pribee.module.quiz.dto.req.QuizAnswerReq; +import com.olive.pribee.module.quiz.dto.res.QuizRandomRes; +import com.olive.pribee.module.quiz.error.QuizErrorCode; + +import lombok.RequiredArgsConstructor; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class QuizService { + private final QuizRepository quizRepository; + + // 퀴즈 3개 가져오기 + public List getRandomThreeQuiz() { + // quiz 랜덤 추출 + List quizzes = quizRepository.findTop3RandomOptimized(); + + // dto 매핑 + return quizzes.stream().map( + quiz -> QuizRandomRes.of( + quiz.getId(), + quiz.getQuestion(), + quiz.getAnswer1(), + quiz.getAnswer2(), + quiz.getAnswerIsOne(), + quiz.getReason(), + quiz.getParticipate(), + quiz.getWrong() + )).collect(Collectors.toList()); + } + + // 퀴즈 답변 반영 + @Transactional + public void postQuizWrongPortion(QuizAnswerListReq quizAnswerReqs) { + // id 기반 모든 퀴즈 조회 + Map quizMap = quizRepository.findAllById( + quizAnswerReqs.quizAnswerReqs().stream().map(QuizAnswerReq::id).toList() + ).stream().collect(Collectors.toMap(Quiz::getId, quiz -> quiz)); + + if (quizMap.size() != quizAnswerReqs.quizAnswerReqs().size()) { + throw new AppException(QuizErrorCode.INVALID_QUIZ_ID); + } + + // 참여 횟수 및 오답 수 증가 처리 + for (QuizAnswerReq req : quizAnswerReqs.quizAnswerReqs()) { + Quiz quiz = quizMap.get(req.id()); + quiz.plusParticipate(); + if (!req.isCorrect()) { + quiz.plusWrong(); + } + } + + // 변경된 퀴즈 목록 저장 + quizRepository.saveAll(quizMap.values()); + } +}