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
2 changes: 2 additions & 0 deletions src/main/java/com/olive/pribee/PribeeApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,13 +26,13 @@ public class MemberController implements MemberControllerDocs {

@GetMapping("/login/facebook")
public ResponseEntity<ResponseDto> 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<ResponseDto> 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));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Member, Long> {
Optional<Member> findByFacebookId(String facebookId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -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<ResponseDto> getRandomThreeQuiz() {
List<QuizRandomRes> resDto = quizService.getRandomThreeQuiz();
return ResponseEntity.status(200).body(DataResponseDto.of(resDto, 200));
}

@PostMapping
public ResponseEntity<ResponseDto> postQuizWrongPortion(@RequestBody @Valid QuizAnswerListReq quizAnswerReqs) {
quizService.postQuizWrongPortion(quizAnswerReqs);
return ResponseEntity.ok(ResponseDto.of(200));
}

}
Original file line number Diff line number Diff line change
@@ -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<ResponseDto> 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<ResponseDto> postQuizWrongPortion(QuizAnswerListReq quizAnswerReqs);

}

70 changes: 70 additions & 0 deletions src/main/java/com/olive/pribee/module/quiz/domain/entity/Quiz.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<Quiz, Long> {
@Query(value = "SELECT * FROM quiz " +
"WHERE id >= (SELECT FLOOR(RAND() * (SELECT MAX(id) FROM quiz))) " +
"ORDER BY id LIMIT 3", nativeQuery = true)
List<Quiz> findTop3RandomOptimized();

}
Original file line number Diff line number Diff line change
@@ -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<QuizAnswerReq> quizAnswerReqs

) {
}
Loading