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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.moongeul.backend.api.book.dto;

import com.moongeul.backend.api.member.entity.ReadingTasteType;
import com.moongeul.backend.api.readingTaste.entity.ReadingTasteType;
import com.moongeul.backend.api.post.dto.PostDTO;
import com.moongeul.backend.api.post.dto.QuoteDTO;
import lombok.AllArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.moongeul.backend.api.member.dto;

import com.moongeul.backend.api.member.entity.FollowStatus;
import com.moongeul.backend.api.member.entity.ReadingTasteType;
import com.moongeul.backend.api.readingTaste.entity.ReadingTasteType;
import lombok.Builder;
import lombok.Getter;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.moongeul.backend.api.member.entity.FollowStatus;
import com.moongeul.backend.api.member.entity.PrivacyLevel;
import com.moongeul.backend.api.member.entity.ReadingTasteType;
import com.moongeul.backend.api.readingTaste.entity.ReadingTasteType;
import lombok.Builder;
import lombok.Getter;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.moongeul.backend.api.member.entity;

import com.moongeul.backend.api.readingTaste.entity.ReadingTasteType;
import com.moongeul.backend.common.entity.BaseTimeEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.moongeul.backend.api.post.dto;

import com.moongeul.backend.api.member.entity.ReadingTasteType;
import com.moongeul.backend.api.readingTaste.entity.ReadingTasteType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.moongeul.backend.api.post.dto;

import com.moongeul.backend.api.member.entity.ReadingTasteType;
import com.moongeul.backend.api.readingTaste.entity.ReadingTasteType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.moongeul.backend.api.post.dto;

import com.moongeul.backend.api.member.entity.ReadingTasteType;
import com.moongeul.backend.api.readingTaste.entity.ReadingTasteType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.moongeul.backend.api.book.entity.Book;
import com.moongeul.backend.api.member.entity.Member;
import com.moongeul.backend.api.member.entity.ReadingTasteType;
import com.moongeul.backend.api.readingTaste.entity.ReadingTasteType;
import com.moongeul.backend.api.post.entity.Post;
import com.moongeul.backend.api.post.entity.PostVisibility;
import org.springframework.data.domain.Page;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.moongeul.backend.api.question.dto;

import com.moongeul.backend.api.member.entity.ReadingTasteType;
import com.moongeul.backend.api.readingTaste.entity.ReadingTasteType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.moongeul.backend.api.member.controller;
package com.moongeul.backend.api.readingTaste.controller;

import com.moongeul.backend.api.member.dto.TestRequestDTO;
import com.moongeul.backend.api.member.dto.TestResponseDTO;
import com.moongeul.backend.api.member.service.ReadingTasteService;
import com.moongeul.backend.api.readingTaste.dto.TestRequestDTO;
import com.moongeul.backend.api.readingTaste.dto.TestResponseDTO;
import com.moongeul.backend.api.readingTaste.dto.TestStatisticsResponseDTO;
import com.moongeul.backend.api.readingTaste.service.ReadingTasteService;
import com.moongeul.backend.common.response.ApiResponse;
import com.moongeul.backend.common.response.SuccessStatus;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -27,6 +28,7 @@ public class ReadingTasteController {
summary = "독서 취향 테스트 API",
description = "독서 취향 테스트 결과를 계산하여 유형을 반환하는 API 입니다." +
"<br><br>요청 형식:" +
"<br>- guestUuid: 비회원 식별을 위한 UUID입니다.(브라우저 로컬스토리지 저장용) 로그인 여부 상관없이, 로컬스토리지에 있는 guestUuid를 전달바랍니다." +
"<br>- answers: 질문 번호(1-12)와 답변(A or B)의 Map으로 전달바랍니다." +
"<br><br>**예시:**\n" +
"```json\n" +
Expand Down Expand Up @@ -62,4 +64,32 @@ public ResponseEntity<ApiResponse<TestResponseDTO>> calculateReadingTasteType(@A
return ApiResponse.success(SuccessStatus.CALCULATE_READING_TASTE_SUCCESS, response);
}

@Operation(
summary = "독서 취향 테스트 참여자 수 반환 API",
description = "독서 취향 테스트 참여자 수를 반환하는 API 입니다."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "독서 취향 테스트 참여자 수 반환 성공"),
})
@GetMapping("/total-count")
public ResponseEntity<ApiResponse<TestStatisticsResponseDTO>> getTotalCount() {

TestStatisticsResponseDTO response = readingTasteService.getTotalParticipantsCount();
return ApiResponse.success(SuccessStatus.GET_READING_TASTE_PARTICIPANTS_SUCCESS, response);
}

@Operation(
summary = "비회원 독서 취향 테스트 결과 조회 및 로그인 연동 API",
description = "회원가입 시, 비회원 상태에서 진행하였던 독서 취향 테스트 결과를 조회하여 결과가 있다면 로그인한 계정 정보에 저장하는 API 입니다."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "독서 취향 테스트 연동 성공"),
})
@PostMapping("/link")
public ResponseEntity<ApiResponse<Void>> linkTestResult(@AuthenticationPrincipal UserDetails userDetails,
@RequestParam String guestUuid) {

readingTasteService.linkTestResult(guestUuid, userDetails.getUsername());
return ApiResponse.success_only(SuccessStatus.LINK_READING_TASTE_SUCCESS);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.moongeul.backend.api.member.dto;
package com.moongeul.backend.api.readingTaste.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
Expand All @@ -15,6 +15,13 @@
@Builder
public class TestRequestDTO {

@Schema(
description = "비회원 식별을 위한 UUID (브라우저 로컬스토리지 저장용)",
example = "550e8400-e29b-41d4-a716-446655440000"
)
@NotNull(message = "비회원 식별자는 필수입니다")
private String guestUuid;

@Schema(
description = "질문 번호(1-12)와 답변(A or B) 매핑",
example = "{\"1\":\"A\",\"2\":\"A\",\"3\":\"A\",\"4\":\"A\",\"5\":\"A\",\"6\":\"A\",\"7\":\"A\",\"8\":\"A\",\"9\":\"A\",\"10\":\"A\",\"11\":\"A\",\"12\":\"A\"}"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.moongeul.backend.api.member.dto;
package com.moongeul.backend.api.readingTaste.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.moongeul.backend.api.readingTaste.dto;

import com.moongeul.backend.api.readingTaste.entity.ReadingTasteType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TestResultRequestDTO {

private String guestUuid;
private ReadingTasteType readingTasteType;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.moongeul.backend.api.readingTaste.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TestStatisticsResponseDTO {
private long totalParticipants; // 전체 참여자 수
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.moongeul.backend.api.member.entity;
package com.moongeul.backend.api.readingTaste.entity;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.moongeul.backend.api.readingTaste.entity;

import com.moongeul.backend.common.entity.BaseTimeEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@Builder // 빌더 패턴 사용을 위한 롬복 애너테이션
@NoArgsConstructor // 기본 생성자
@AllArgsConstructor // 모든 필드를 포함한 생성자
@Table(name = "TEST_RESULT") // 데이터베이스 테이블 이름 지정
public class TestResult extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

// 비회원도 참여 가능하므로 필수값 아님 (null 가능)
@Column(name = "member_id")
private Long memberId;

// 비회원 식별 UUID: 브라우저에서 생성해 보낸 값 저장
@Column(nullable = false, length = 36)
private String guestUuid;

@Enumerated(EnumType.STRING)
private ReadingTasteType readingTasteType;

// 회원가입 후 데이터를 유저와 연결
public void updateMemberId(Long memberId) {
this.memberId = memberId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.moongeul.backend.api.readingTaste.repository;

import com.moongeul.backend.api.readingTaste.entity.TestResult;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface TestResultRepository extends JpaRepository<TestResult, Long> {

// guestUuid로 찾아 생성일(CreatedAt) 내림차순(Desc) 정렬 후 첫 번째(First) 행 반환
Optional<TestResult> findFirstByGuestUuidOrderByCreatedAtDesc(String guestUuid);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.moongeul.backend.api.member.service;
package com.moongeul.backend.api.readingTaste.service;

import com.moongeul.backend.api.member.dto.TestRequestDTO;
import com.moongeul.backend.api.member.dto.TestResponseDTO;
import com.moongeul.backend.api.readingTaste.dto.TestRequestDTO;
import com.moongeul.backend.api.readingTaste.dto.TestResponseDTO;
import com.moongeul.backend.api.member.entity.Member;
import com.moongeul.backend.api.member.entity.ReadingTasteType;
import com.moongeul.backend.api.readingTaste.dto.TestStatisticsResponseDTO;
import com.moongeul.backend.api.readingTaste.entity.ReadingTasteType;
import com.moongeul.backend.api.member.repository.MemberRepository;
import com.moongeul.backend.api.readingTaste.entity.TestResult;
import com.moongeul.backend.api.readingTaste.repository.TestResultRepository;
import com.moongeul.backend.common.exception.BadRequestException;
import com.moongeul.backend.common.exception.NotFoundException;
import com.moongeul.backend.common.response.ErrorStatus;
Expand All @@ -22,6 +25,7 @@
public class ReadingTasteService {

private final MemberRepository memberRepository;
private final TestResultRepository testResultRepository;

private static final String ANSWER_A = "A";
private static final String ANSWER_B = "B";
Expand All @@ -32,16 +36,22 @@ public TestResponseDTO calculateReadingTasteType(TestRequestDTO testRequestDTO,
// 테스트 결과 계산 (가장 높은 유형 반환)
ReadingTasteType type = findTopType(calculateScore(testRequestDTO.getAnswers()));

// 로그 엔티티 생성
TestResult testLog = TestResult.builder()
.guestUuid(testRequestDTO.getGuestUuid())
.readingTasteType(type)
.build();

// 회원이라면 취향테스트 결과 DB 저장
if(!email.equals("anonymousUser")){
Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage()));
Member member = getMemberByEmail(email);

// member 필드 readingTasteType 저장
member.updateReadingTasteType(type);
memberRepository.save(member);
member.updateReadingTasteType(type); // Member 테이블 업데이트
testLog.updateMemberId(member.getId()); // TestResult 로그에 member_id 연결
}

testResultRepository.save(testLog);

return TestResponseDTO.builder()
.readingTasteType(type.getName())
.intro(type.getIntro())
Expand Down Expand Up @@ -236,4 +246,41 @@ private ReadingTasteType findTopType(Map<ReadingTasteType, Double> typeScores){

return topType;
}

/*
* 전체 참여자 수 반환 API
*/
public TestStatisticsResponseDTO getTotalParticipantsCount() {
long count = testResultRepository.count(); // DB의 전체 레코드 개수 조회

return TestStatisticsResponseDTO.builder()
.totalParticipants(count)
.build();
}

/*
* 비회원 독서 취향 테스트 결과 조회 및 로그인 연동 API
*/
@Transactional
public void linkTestResult(String guestUuid, String email) {

Member member = getMemberByEmail(email);

// 가장 최신 테스트 결과 조회
TestResult lastTestResult = testResultRepository.findFirstByGuestUuidOrderByCreatedAtDesc(guestUuid)
.orElseThrow(() -> new NotFoundException(ErrorStatus.READING_TASTE_TEST_RESULT_NOTFOUND_EXCEPTION.getMessage()));

// 테스트 결과가 있다면 -> memberId 회원 연결 + Member 테이블 독서 취향 업데이트 반영
lastTestResult.updateMemberId(member.getId());
member.updateReadingTasteType(lastTestResult.getReadingTasteType());
}

/*
* 단순 데이터 불러오기용 코드 메서드 - 코드 깔끔하게 하기용
*/

private Member getMemberByEmail(String email) {
return memberRepository.findByEmail(email)
.orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers("/", "/h2-console/**").permitAll()
.requestMatchers("/static/**", "/index.html", "/firebase-messaging-sw.js", "/favicon.ico").permitAll()
.requestMatchers("/v3/api-docs/**", "/api-doc/**", "/swagger-ui/**").permitAll()
.requestMatchers("/api/v2/member/google/login", "/api/v2/member/kakao/login", "/api/v2/member/reissue-token", "/api/v2/reading-taste").permitAll()
.requestMatchers("/api/v2/member/google/login", "/api/v2/member/kakao/login", "/api/v2/member/reissue-token").permitAll()
.requestMatchers("/api/v2/reading-taste", "/api/v2/reading-taste/total-count").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v2/post/**").permitAll()
.anyRequest().authenticated()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public enum ErrorStatus {
ANSWER_NOTFOUND_EXCEPTION(HttpStatus.NOT_FOUND, "해당 답변을 찾을 수 없습니다."),
TERMS_NOTFOUND_EXCEPTION(HttpStatus.NOT_FOUND, "약관 정보를 찾을 수 없습니다."),
NOTIFICATION_NOTFOUND_EXCEPTION(HttpStatus.NOT_FOUND, "해당 알림을 찾을 수 없습니다."),
READING_TASTE_TEST_RESULT_NOTFOUND_EXCEPTION(HttpStatus.NOT_FOUND, "해당 UUID에 해당하는 독서 취향 테스트 결과가 없습니다."),

/**
* 400 BAD_REQUEST
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ public enum SuccessStatus {
GET_USERINFO_SUCCESS(HttpStatus.OK, "사용자 정보 조회 성공"),
GET_POST_STATS_SUCCESS(HttpStatus.OK, "기록 통계 조회 성공"),
REISSUE_TOKEN_SUCCESS(HttpStatus.OK, "토큰 재발급 성공"),
CALCULATE_READING_TASTE_SUCCESS(HttpStatus.OK, "독서 취향 테스트 결과 계산 성공"),
FOLLOW_SUCCESS(HttpStatus.OK, "팔로우 성공"),
UNFOLLOW_SUCCESS(HttpStatus.OK, "언팔로우 성공"),
GET_FOLLOWING_SUCCESS(HttpStatus.OK, "팔로잉 목록 조회 성공"),
Expand All @@ -33,6 +32,11 @@ public enum SuccessStatus {
UPDATE_PRIVACY_LEVEL_SUCCESS(HttpStatus.OK, "계정 공개 범위 수정 성공"),
TERMS_AGREE_SUCCESS(HttpStatus.OK, "약관 동의 성공"),

/* READING TASTE */
CALCULATE_READING_TASTE_SUCCESS(HttpStatus.OK, "독서 취향 테스트 결과 계산 성공"),
GET_READING_TASTE_PARTICIPANTS_SUCCESS(HttpStatus.OK, "독서 취향 테스트 참여자 수 반환 성공"),
LINK_READING_TASTE_SUCCESS(HttpStatus.OK, "독서 취향 테스트 연동 성공"),

/* BOOK */
SEARCH_BOOK_SUCCESS(HttpStatus.OK, "도서 검색 성공"),
GET_BOOK_DETAIL_SUCCESS(HttpStatus.OK, "도서 상세 조회 성공"),
Expand Down