From ff910cb4bb6423988dfb0d5ab7590bed73f8e594 Mon Sep 17 00:00:00 2001 From: khyun722 Date: Thu, 12 Mar 2026 01:04:06 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[FEAT]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=A9=B4=EC=A0=91=20=EB=82=B4=EC=97=AD=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=ED=95=84=ED=84=B0=EB=A7=81/=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20Redis=20=EC=A7=81?= =?UTF-8?q?=EB=A0=AC=ED=99=94=20=EC=84=A4=EC=A0=95=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InterviewSessionRepositoryCustom.java | 9 +++- .../InterviewSessionRepositoryImpl.java | 44 +++++++++++++++++-- .../controller/MypageInterviewController.java | 10 +++-- .../service/MypageInterviewService.java | 6 ++- .../aibe/team2/global/redis/RedisConfig.java | 1 + 5 files changed, 60 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryCustom.java b/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryCustom.java index 7bb3731..5b4cf4a 100644 --- a/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryCustom.java +++ b/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryCustom.java @@ -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 findInterviewSessionList(Long memberId, Pageable pageable); + Page findInterviewSessionList( + Long memberId, + InterviewType type, + String keyword, + Pageable pageable + ); } diff --git a/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryImpl.java b/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryImpl.java index 4c37344..49d02ca 100644 --- a/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryImpl.java +++ b/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryImpl.java @@ -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; @@ -21,7 +24,12 @@ public class InterviewSessionRepositoryImpl implements InterviewSessionRepositor private final JPAQueryFactory queryFactory; @Override - public Page findInterviewSessionList(Long memberId, Pageable pageable) { + public Page findInterviewSessionList( + Long memberId, + InterviewType type, + String keyword, + Pageable pageable + ) { // 1. 실제 데이터 조회 쿼리 List content = queryFactory @@ -40,7 +48,11 @@ public Page 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()) // 페이지 크기 @@ -50,9 +62,35 @@ public Page findInterviewSessionList(Long memberId JPAQuery 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) + ); // 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()); + } + + // 검색어 포함 여부 확인 + private BooleanExpression containsKeyword(String keyword) { + if (!StringUtils.hasText(keyword)) { + return null; // 검색어가 없으면 조건을 무시 + } + // 예시: 회사명 또는 자기소개서 제목에 검색어가 포함되어 있는지 검사 + return jobPosting.companyName.contains(keyword) + .or(resume.title.contains(keyword)); + } } diff --git a/src/main/java/com/aibe/team2/domain/mypage/controller/MypageInterviewController.java b/src/main/java/com/aibe/team2/domain/mypage/controller/MypageInterviewController.java index 3003088..17ff5f5 100644 --- a/src/main/java/com/aibe/team2/domain/mypage/controller/MypageInterviewController.java +++ b/src/main/java/com/aibe/team2/domain/mypage/controller/MypageInterviewController.java @@ -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; @@ -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; @@ -28,10 +27,13 @@ public class MypageInterviewController { public ResponseEntity> 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, + @RequestParam(required = false) String keyword ) { Pageable pageRequest = PageRequest.of(page, size); - Page response = mypageInterviewService.getInterviewSessionList(memberId, pageRequest); + Page response = + mypageInterviewService.getInterviewSessionList(memberId, type, keyword, pageRequest); return ResponseEntity.ok(response); } diff --git a/src/main/java/com/aibe/team2/domain/mypage/service/MypageInterviewService.java b/src/main/java/com/aibe/team2/domain/mypage/service/MypageInterviewService.java index 104ee48..7b1377a 100644 --- a/src/main/java/com/aibe/team2/domain/mypage/service/MypageInterviewService.java +++ b/src/main/java/com/aibe/team2/domain/mypage/service/MypageInterviewService.java @@ -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; @@ -15,8 +17,8 @@ public class MypageInterviewService { private final InterviewSessionRepository interviewSessionRepository; - public Page getInterviewSessionList(Long memberId, Pageable pageable) { + public Page getInterviewSessionList(Long memberId, InterviewType type, String keyword, Pageable pageable) { - return interviewSessionRepository.findInterviewSessionList(memberId, pageable); + return interviewSessionRepository.findInterviewSessionList(memberId, type, keyword, pageable); } } diff --git a/src/main/java/com/aibe/team2/global/redis/RedisConfig.java b/src/main/java/com/aibe/team2/global/redis/RedisConfig.java index 973e484..920f4c1 100644 --- a/src/main/java/com/aibe/team2/global/redis/RedisConfig.java +++ b/src/main/java/com/aibe/team2/global/redis/RedisConfig.java @@ -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(); From c01dbbdac39fe4799852a382c88d43fa46f82393 Mon Sep 17 00:00:00 2001 From: khyun722 Date: Thu, 12 Mar 2026 09:31:46 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20AI=20Code=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InterviewSessionRepositoryImpl.java | 72 ++++++++----------- .../controller/MypageInterviewController.java | 2 +- 2 files changed, 31 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryImpl.java b/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryImpl.java index 49d02ca..b0c02c2 100644 --- a/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryImpl.java +++ b/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryImpl.java @@ -30,58 +30,35 @@ public Page findInterviewSessionList( String keyword, Pageable pageable ) { - - // 1. 실제 데이터 조회 쿼리 - List content = queryFactory - .select(Projections.constructor(InterviewSessionListResponse.class, - interviewSession.id, - resume.title, - jobPosting.companyName, - jobPosting.jobTitle, - interviewSession.interviewMode, - interviewSession.interviewType, - interviewSession.status, - interviewSession.finalScore, - interviewSession.createdAt - )) - .from(interviewSession) - // [핵심] 엔티티에 연관관계가 없으므로 ON 절로 ID 값을 직접 매칭 - .leftJoin(resume).on(interviewSession.resumeId.eq(resume.id)) - .leftJoin(jobPosting).on(interviewSession.jobPostingId.eq(jobPosting.id)) - .where( - interviewSession.memberId.eq(memberId), - eqType(type), // 모드 필터 (VOICE, TEXT) - containsKeyword(keyword) // 검색어 필터 - ) - .orderBy(interviewSession.createdAt.desc()) // 최신순 정렬 - .offset(pageable.getOffset()) // 페이징 시작점 - .limit(pageable.getPageSize()) // 페이지 크기 - .fetch(); - - // 2. 전체 개수 조회 쿼리 - JPAQuery countQuery = queryFactory - .select(interviewSession.count()) + // 1. 공통 조건(from, join, where)을 포함하는 기본 쿼리 생성 + JPAQuery baseQuery = queryFactory .from(interviewSession) - // ✨ [수정됨] 검색어 필터링을 위해 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) ); - // 3. 데이터와 카운트 쿼리를 조합하여 Page 객체로 반환 - return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); - } + // 2. 실제 데이터 조회 쿼리 (기본 쿼리를 복제한 후 select, orderBy, offset, limit 추가) + List content = baseQuery.clone() + .select(Projections.constructor(InterviewSessionListResponse.class, + interviewSession.id, + resume.title, + // ... 나머지 필드들 ... + interviewSession.createdAt + )) + .orderBy(interviewSession.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); - // 면접 모드(VOICE, TEXT) 일치 여부 확인 - private BooleanExpression eqType(InterviewType type) { - if (type == null) { - return null; // 프론트에서 값이 안 오면(전체 조회 시) 조건을 무시 - } - return interviewSession.interviewType.eq(type.name()); + // 3. 전체 개수 조회 쿼리 (기본 쿼리를 복제한 후 count 추가) + JPAQuery countQuery = baseQuery.clone().select(interviewSession.count()); + + // 4. 결과 반환 + return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); } // 검색어 포함 여부 확인 @@ -93,4 +70,15 @@ private BooleanExpression containsKeyword(String keyword) { return jobPosting.companyName.contains(keyword) .or(resume.title.contains(keyword)); } + + private BooleanExpression eqType(InterviewType type) { + if (type == null) { + return null; // 프론트에서 값이 안 오면(전체 조회 시) 조건을 무시 + } + + // 1. 현재 엔티티의 필드가 String 타입이므로, Enum 객체를 String으로 변환(type.name())하여 비교합니다. + // 2. 나중을 위해 기술 부채(Technical Debt)를 기록해 둡니다. + // TODO: 향후 InterviewSession 엔티티의 interviewType 필드를 Enum으로 변경 후 eq(type)으로 리팩토링 필요 + return interviewSession.interviewType.eq(type.name()); + } } diff --git a/src/main/java/com/aibe/team2/domain/mypage/controller/MypageInterviewController.java b/src/main/java/com/aibe/team2/domain/mypage/controller/MypageInterviewController.java index 17ff5f5..50bffa1 100644 --- a/src/main/java/com/aibe/team2/domain/mypage/controller/MypageInterviewController.java +++ b/src/main/java/com/aibe/team2/domain/mypage/controller/MypageInterviewController.java @@ -28,7 +28,7 @@ public ResponseEntity> getInterviewSessionLis @LoginMemberId Long memberId, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, - @RequestParam(name = "mode", required = false) InterviewType type, + @RequestParam(name = "type", required = false) InterviewType type, @RequestParam(required = false) String keyword ) { Pageable pageRequest = PageRequest.of(page, size); From 1cf5211b733f6fad889e81871388de1028af72ef Mon Sep 17 00:00:00 2001 From: khyun722 Date: Thu, 12 Mar 2026 09:43:15 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20AI=20Code=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InterviewSessionRepositoryCustom.java | 6 ++--- .../InterviewSessionRepositoryImpl.java | 22 +++++++++---------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryCustom.java b/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryCustom.java index 5b4cf4a..f9c93e1 100644 --- a/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryCustom.java +++ b/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryCustom.java @@ -1,13 +1,13 @@ 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; +import java.util.List; + public interface InterviewSessionRepositoryCustom { - Page findInterviewSessionList( + List findInterviewSessionList( Long memberId, InterviewType type, String keyword, diff --git a/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryImpl.java b/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryImpl.java index b0c02c2..fcabac8 100644 --- a/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryImpl.java +++ b/src/main/java/com/aibe/team2/domain/interview/repository/InterviewSessionRepositoryImpl.java @@ -7,7 +7,6 @@ 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; @@ -24,7 +23,7 @@ public class InterviewSessionRepositoryImpl implements InterviewSessionRepositor private final JPAQueryFactory queryFactory; @Override - public Page findInterviewSessionList( + public List findInterviewSessionList( Long memberId, InterviewType type, String keyword, @@ -41,24 +40,23 @@ public Page findInterviewSessionList( containsKeyword(keyword) ); - // 2. 실제 데이터 조회 쿼리 (기본 쿼리를 복제한 후 select, orderBy, offset, limit 추가) - List content = baseQuery.clone() + // 2. 실제 데이터 조회 쿼리 후 곧바로 List 반환 + return baseQuery.clone() .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(); - - // 3. 전체 개수 조회 쿼리 (기본 쿼리를 복제한 후 count 추가) - JPAQuery countQuery = baseQuery.clone().select(interviewSession.count()); - - // 4. 결과 반환 - return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); + .fetch(); // fetch()는 List를 반환합니다. } // 검색어 포함 여부 확인