Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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,9 +1,16 @@
package com.aibe.team2.domain.interview.repository;

import com.aibe.team2.domain.interview.enums.InterviewMode;
import com.aibe.team2.domain.interview.enums.InterviewType;
import com.aibe.team2.domain.mypage.dto.response.InterviewSessionListResponse;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface InterviewSessionRepositoryCustom {
Page<InterviewSessionListResponse> findInterviewSessionList(Long memberId, Pageable pageable);
Page<InterviewSessionListResponse> findInterviewSessionList(
Long memberId,
InterviewType type,
String keyword,
Pageable pageable
);
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.aibe.team2.domain.interview.repository;

import com.aibe.team2.domain.interview.enums.InterviewType;
import com.aibe.team2.domain.mypage.dto.response.InterviewSessionListResponse;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.util.StringUtils;

import java.util.List;

Expand All @@ -21,7 +24,12 @@ public class InterviewSessionRepositoryImpl implements InterviewSessionRepositor
private final JPAQueryFactory queryFactory;

@Override
public Page<InterviewSessionListResponse> findInterviewSessionList(Long memberId, Pageable pageable) {
public Page<InterviewSessionListResponse> findInterviewSessionList(
Long memberId,
InterviewType type,
String keyword,
Pageable pageable
) {

// 1. 실제 데이터 조회 쿼리
List<InterviewSessionListResponse> content = queryFactory
Expand All @@ -40,7 +48,11 @@ public Page<InterviewSessionListResponse> findInterviewSessionList(Long memberId
// [핵심] 엔티티에 연관관계가 없으므로 ON 절로 ID 값을 직접 매칭
.leftJoin(resume).on(interviewSession.resumeId.eq(resume.id))
.leftJoin(jobPosting).on(interviewSession.jobPostingId.eq(jobPosting.id))
.where(interviewSession.memberId.eq(memberId))
.where(
interviewSession.memberId.eq(memberId),
eqType(type), // 모드 필터 (VOICE, TEXT)
containsKeyword(keyword) // 검색어 필터
)
.orderBy(interviewSession.createdAt.desc()) // 최신순 정렬
.offset(pageable.getOffset()) // 페이징 시작점
.limit(pageable.getPageSize()) // 페이지 크기
Expand All @@ -50,9 +62,35 @@ public Page<InterviewSessionListResponse> findInterviewSessionList(Long memberId
JPAQuery<Long> countQuery = queryFactory
.select(interviewSession.count())
.from(interviewSession)
.where(interviewSession.memberId.eq(memberId));
// ✨ [수정됨] 검색어 필터링을 위해 JOIN이 필요하므로 카운트 쿼리에도 추가
.leftJoin(resume).on(interviewSession.resumeId.eq(resume.id))
.leftJoin(jobPosting).on(interviewSession.jobPostingId.eq(jobPosting.id))
// ✨ [수정됨] 데이터 쿼리와 완벽하게 동일한 조건 적용
.where(
interviewSession.memberId.eq(memberId),
eqType(type),
containsKeyword(keyword)
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

내용 조회 쿼리와 개수 조회 쿼리에서 from, leftJoin, where 절이 중복되고 있습니다. 이는 향후 쿼리 조건이 변경될 때 두 군데를 모두 수정해야 하므로 유지보수 과정에서 실수를 유발할 수 있습니다.

다음과 같이 공통 쿼리 로직을 추출하여 중복을 제거하는 리팩토링을 고려해볼 수 있습니다.

@Override
public Page<InterviewSessionListResponse> findInterviewSessionList(
        Long memberId,
        InterviewType type,
        String keyword,
        Pageable pageable
) {
    // from, join, where 절을 포함하는 기본 쿼리
    JPAQuery<?> baseQuery = queryFactory
            .from(interviewSession)
            .leftJoin(resume).on(interviewSession.resumeId.eq(resume.id))
            .leftJoin(jobPosting).on(interviewSession.jobPostingId.eq(jobPosting.id))
            .where(
                    interviewSession.memberId.eq(memberId),
                    eqType(type),
                    containsKeyword(keyword)
            );

    // 1. 실제 데이터 조회 쿼리 (select, orderBy, offset, limit 추가)
    List<InterviewSessionListResponse> content = baseQuery.clone() // baseQuery를 복제하여 사용
            .select(Projections.constructor(InterviewSessionListResponse.class,
                interviewSession.id,
                resume.title,
                jobPosting.companyName,
                jobPosting.jobTitle,
                interviewSession.interviewMode,
                interviewSession.interviewType,
                interviewSession.status,
                interviewSession.finalScore,
                interviewSession.createdAt
            ))
            .orderBy(interviewSession.createdAt.desc())
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch();

    // 2. 전체 개수 조회 쿼리 (select count 추가)
    JPAQuery<Long> countQuery = baseQuery.clone().select(interviewSession.count());

    // 3. 데이터와 카운트 쿼리를 조합하여 Page 객체로 반환
    return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
}

JPAQuery는 상태를 가지므로, clone()을 사용하여 각 쿼리를 독립적으로 실행하는 것이 중요합니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정완료


// 3. 데이터와 카운트 쿼리를 조합하여 Page 객체로 반환
return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
}

// 면접 모드(VOICE, TEXT) 일치 여부 확인
private BooleanExpression eqType(InterviewType type) {
if (type == null) {
return null; // 프론트에서 값이 안 오면(전체 조회 시) 조건을 무시
}
return interviewSession.interviewType.eq(type.name());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

InterviewSession 엔티티의 interviewType 필드가 String 타입이어서 type.name()으로 비교하고 있습니다. 이는 enum의 이름이 변경될 경우 문제가 발생할 수 있어 잠재적으로 취약합니다. 장기적인 유지보수성을 위해 InterviewSession 엔티티의 interviewType 필드 타입을 InterviewType enum으로 변경하고 @Enumerated(EnumType.STRING)을 사용하는 것을 고려해보세요. 이렇게 하면 interviewSession.interviewType.eq(type)와 같이 타입-안전한 비교가 가능해져 코드가 더 견고해집니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추후 수정 예정


// 검색어 포함 여부 확인
private BooleanExpression containsKeyword(String keyword) {
if (!StringUtils.hasText(keyword)) {
return null; // 검색어가 없으면 조건을 무시
}
// 예시: 회사명 또는 자기소개서 제목에 검색어가 포함되어 있는지 검사
return jobPosting.companyName.contains(keyword)
.or(resume.title.contains(keyword));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.aibe.team2.domain.mypage.controller;

import com.aibe.team2.domain.auth.dto.CustomUserDetails;
import com.aibe.team2.domain.interview.enums.InterviewType;
import com.aibe.team2.domain.mypage.dto.response.InterviewSessionListResponse;
import com.aibe.team2.domain.mypage.service.MypageInterviewService;
import com.aibe.team2.global.common.annotation.LoginMemberId;
Expand All @@ -10,7 +10,6 @@
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
Expand All @@ -28,10 +27,13 @@ public class MypageInterviewController {
public ResponseEntity<Page<InterviewSessionListResponse>> getInterviewSessionList(
@LoginMemberId Long memberId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size
@RequestParam(defaultValue = "10") int size,
@RequestParam(name = "mode", required = false) InterviewType type,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

요청 파라미터의 이름은 mode인데, 이를 받는 변수명은 type으로 되어 있어 불일치가 발생합니다. 이는 코드를 읽는 개발자에게 혼동을 줄 수 있습니다. 코드의 명확성과 일관성을 위해 파라미터 이름을 변수명과 일치시키는 것이 좋습니다.

Suggested change
@RequestParam(name = "mode", required = false) InterviewType type,
@RequestParam(name = "type", required = false) InterviewType type,

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정완료

@RequestParam(required = false) String keyword
) {
Pageable pageRequest = PageRequest.of(page, size);
Page<InterviewSessionListResponse> response = mypageInterviewService.getInterviewSessionList(memberId, pageRequest);
Page<InterviewSessionListResponse> response =
mypageInterviewService.getInterviewSessionList(memberId, type, keyword, pageRequest);

return ResponseEntity.ok(response);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.aibe.team2.domain.mypage.service;

import com.aibe.team2.domain.interview.enums.InterviewMode;
import com.aibe.team2.domain.interview.enums.InterviewType;
import com.aibe.team2.domain.interview.repository.InterviewSessionRepository;
import com.aibe.team2.domain.mypage.dto.response.InterviewSessionListResponse;
import lombok.RequiredArgsConstructor;
Expand All @@ -15,8 +17,8 @@ public class MypageInterviewService {

private final InterviewSessionRepository interviewSessionRepository;

public Page<InterviewSessionListResponse> getInterviewSessionList(Long memberId, Pageable pageable) {
public Page<InterviewSessionListResponse> getInterviewSessionList(Long memberId, InterviewType type, String keyword, Pageable pageable) {

return interviewSessionRepository.findInterviewSessionList(memberId, pageable);
return interviewSessionRepository.findInterviewSessionList(memberId, type, keyword, pageable);
}
}
1 change: 1 addition & 0 deletions src/main/java/com/aibe/team2/global/redis/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public GenericJackson2JsonRedisSerializer customJsonSerializer() {
.allowIfBaseType("java.util") // List, Map 등 자바 기본 컬렉션 허용
.allowIfBaseType("java.time") // LocalDateTime 등 시간 객체 허용
.allowIfBaseType("java.lang") // String, Long, Integer 등 기본 래퍼 타입 허용
.allowIfBaseType("java.math") // [추가] BigDecimal 등 수학 관련 객체 허용!
.build();

ObjectMapper objectMapper = new ObjectMapper();
Expand Down