diff --git a/.gitignore b/.gitignore index 8704231..99a7de0 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ out/ ### VS Code ### .vscode/ +/mysql-data/ diff --git a/build.gradle b/build.gradle index 4de3cb7..1305889 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.7' } -group = 'org.festiamte' +group = 'org.festimate' version = '0.0.1-SNAPSHOT' java { @@ -58,8 +58,26 @@ dependencies { testImplementation 'io.projectreactor:reactor-test' // WebFlux 테스트 지원 testImplementation 'org.springframework.security:spring-security-test' // Security 테스트 지원 testRuntimeOnly 'org.junit.platform:junit-platform-launcher' // JUnit 테스트 실행기 + + // --- QueryDSL --- + implementation "com.querydsl:querydsl-jpa:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta' + + // --- Jakarta Persistence API --- + implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0' + annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0' +} + +sourceSets { + main { + java { + srcDir 'src/main/java' + // Q-타입 생성 위치를 IDE가 인식하도록 추가 + srcDir 'build/generated/sources/annotationProcessor/java/main' + } + } } tasks.named('test') { useJUnitPlatform() -} +} \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index a6c841e..1a26d74 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,12 @@ +version: "3" services: mysql: - image: 'mysql:latest' + image: mysql:8.0 + container_name: festimate-mysql-1 env_file: - .env ports: - - '3306:3306' + - "3306:3306" + volumes: + - ./mysql-data:/var/lib/mysql + restart: unless-stopped diff --git a/src/main/java/org/festimate/team/api/admin/AdminController.java b/src/main/java/org/festimate/team/api/admin/AdminController.java index 48db59b..389194d 100644 --- a/src/main/java/org/festimate/team/api/admin/AdminController.java +++ b/src/main/java/org/festimate/team/api/admin/AdminController.java @@ -6,7 +6,6 @@ import org.festimate.team.api.point.dto.PointHistoryResponse; import org.festimate.team.global.response.ApiResponse; import org.festimate.team.global.response.ResponseBuilder; -import org.festimate.team.infra.jwt.JwtService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -16,7 +15,6 @@ @RequestMapping("/v1/admin/festivals") @RequiredArgsConstructor public class AdminController { - private final JwtService jwtService; private final FestivalFacade festivalFacade; private final FestivalHostFacade festivalHostFacade; private final ParticipantFacade participantFacade; @@ -25,85 +23,76 @@ public class AdminController { @PostMapping() public ResponseEntity> createFestival( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @RequestBody FestivalRequest request ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); FestivalResponse response = festivalFacade.createFestival(userId, request); return ResponseBuilder.created(response); } @GetMapping() public ResponseEntity>> getAllFestivals( - @RequestHeader("Authorization") String accessToken + @RequestAttribute("userId") Long userId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); List response = festivalFacade.getAllFestivals(userId); return ResponseBuilder.ok(response); } @GetMapping("/{festivalId}") public ResponseEntity> getFestivalDetail( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable Long festivalId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); AdminFestivalDetailResponse response = festivalFacade.getFestivalDetail(userId, festivalId); return ResponseBuilder.ok(response); } @GetMapping("/{festivalId}/participants/search") public ResponseEntity>> getParticipantByNickname( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId, @RequestParam("nickname") String nickname ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); - List response = participantFacade.getParticipantByNickname(userId, festivalId, nickname); return ResponseBuilder.ok(response); } @PostMapping("/{festivalId}/points") public ResponseEntity> rechargePoints( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId, @RequestBody RechargePointRequest request ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); pointFacade.rechargePoints(userId, festivalId, request); return ResponseBuilder.ok(null); } @PostMapping("/{festivalId}/hosts") public ResponseEntity> addHost( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId, @RequestBody AddHostRequest request ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); festivalHostFacade.addHost(userId, festivalId, request); return ResponseBuilder.created(null); } @GetMapping("/{festivalId}/participants/{participantId}/points") public ResponseEntity> getParticipantPointHistory( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId, @PathVariable("participantId") Long participantId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); PointHistoryResponse response = pointFacade.getParticipantPointHistory(userId, festivalId, participantId); return ResponseBuilder.ok(response); } @GetMapping("/{festivalId}/participants/{participantId}/matchings") public ResponseEntity> getParticipantMatchingHistory( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId, @PathVariable("participantId") Long participantId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); AdminMatchingResponse response = matchingFacade.getMatchingSize(userId, festivalId, participantId); return ResponseBuilder.ok(response); } diff --git a/src/main/java/org/festimate/team/api/auth/AuthController.java b/src/main/java/org/festimate/team/api/auth/AuthController.java index 2070f17..33aca5a 100644 --- a/src/main/java/org/festimate/team/api/auth/AuthController.java +++ b/src/main/java/org/festimate/team/api/auth/AuthController.java @@ -26,7 +26,7 @@ public class AuthController { public ResponseEntity> login( @RequestHeader("Authorization") String kakaoAccessToken ) { - log.info("social login - Code: {}", kakaoAccessToken); + log.info("social login - kakaoAccessToken: {}", kakaoAccessToken); String platformId = loginFacade.getPlatformId(kakaoAccessToken); diff --git a/src/main/java/org/festimate/team/api/facade/LoginFacade.java b/src/main/java/org/festimate/team/api/facade/LoginFacade.java index 2c82621..ece800d 100644 --- a/src/main/java/org/festimate/team/api/facade/LoginFacade.java +++ b/src/main/java/org/festimate/team/api/facade/LoginFacade.java @@ -6,7 +6,7 @@ import org.festimate.team.domain.auth.service.KakaoLoginService; import org.festimate.team.domain.user.entity.Platform; import org.festimate.team.domain.user.service.UserService; -import org.festimate.team.infra.jwt.JwtService; +import org.festimate.team.infra.jwt.JwtTokenProvider; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -15,7 +15,7 @@ @RequiredArgsConstructor public class LoginFacade { - private final JwtService jwtService; + private final JwtTokenProvider jwtTokenProvider; private final UserService userService; private final KakaoLoginService kakaoLoginService; @@ -28,10 +28,10 @@ public TokenResponse login(String platformId, Platform platform) { private TokenResponse loginExistingUser(Long userId) { log.info("기존 유저 로그인 성공 - userId: {}", userId); - String newRefreshToken = jwtService.createRefreshToken(userId); + String newRefreshToken = jwtTokenProvider.createRefreshToken(userId); userService.updateRefreshToken(userId, newRefreshToken); - return new TokenResponse(userId, jwtService.createAccessToken(userId), newRefreshToken); + return new TokenResponse(userId, jwtTokenProvider.createAccessToken(userId), newRefreshToken); } public String getPlatformId(String authorization) { @@ -39,7 +39,7 @@ public String getPlatformId(String authorization) { } private TokenResponse createTemporaryToken(String platformId) { - return new TokenResponse(null, jwtService.createTempAccessToken(platformId), jwtService.createTempRefreshToken(platformId)); + return new TokenResponse(null, jwtTokenProvider.createTempAccessToken(platformId), jwtTokenProvider.createTempRefreshToken(platformId)); } } diff --git a/src/main/java/org/festimate/team/api/facade/ParticipantFacade.java b/src/main/java/org/festimate/team/api/facade/ParticipantFacade.java index 1a8bd5d..24b53b2 100644 --- a/src/main/java/org/festimate/team/api/facade/ParticipantFacade.java +++ b/src/main/java/org/festimate/team/api/facade/ParticipantFacade.java @@ -42,6 +42,7 @@ public EntryResponse createParticipant(Long userId, Long festivalId, ProfileRequ throw new FestimateException(ResponseError.PARTICIPANT_ALREADY_EXISTS); } Participant participant = participantService.createParticipant(user, festival, request); + pointService.rechargePoint(participant, 10); matchingService.matchPendingParticipants(participant); return EntryResponse.of(participant); diff --git a/src/main/java/org/festimate/team/api/facade/SignUpFacade.java b/src/main/java/org/festimate/team/api/facade/SignUpFacade.java index 823b40d..694660f 100644 --- a/src/main/java/org/festimate/team/api/facade/SignUpFacade.java +++ b/src/main/java/org/festimate/team/api/facade/SignUpFacade.java @@ -9,14 +9,14 @@ import org.festimate.team.domain.user.validator.NicknameValidator; import org.festimate.team.global.exception.FestimateException; import org.festimate.team.global.response.ResponseError; -import org.festimate.team.infra.jwt.JwtService; +import org.festimate.team.infra.jwt.JwtTokenProvider; import org.springframework.stereotype.Component; @Slf4j @Component @RequiredArgsConstructor public class SignUpFacade { - private final JwtService jwtService; + private final JwtTokenProvider jwtTokenProvider; private final UserService userService; private final NicknameValidator nicknameValidator; @@ -36,8 +36,8 @@ public void validateNickname(String nickname) { private TokenResponse createTokenResponse(User user) { log.info("signup success - userId : {}, nickname : {}", user.getUserId(), user.getNickname()); - String accessToken = jwtService.createAccessToken(user.getUserId()); - String refreshToken = jwtService.createRefreshToken(user.getUserId()); + String accessToken = jwtTokenProvider.createAccessToken(user.getUserId()); + String refreshToken = jwtTokenProvider.createRefreshToken(user.getUserId()); userService.updateRefreshToken(user.getUserId(), refreshToken); return TokenResponse.of(user.getUserId(), accessToken, refreshToken); } diff --git a/src/main/java/org/festimate/team/api/festival/FestivalController.java b/src/main/java/org/festimate/team/api/festival/FestivalController.java index d7557cd..c9a095b 100644 --- a/src/main/java/org/festimate/team/api/festival/FestivalController.java +++ b/src/main/java/org/festimate/team/api/festival/FestivalController.java @@ -7,7 +7,6 @@ import org.festimate.team.api.festival.dto.FestivalVerifyResponse; import org.festimate.team.global.response.ApiResponse; import org.festimate.team.global.response.ResponseBuilder; -import org.festimate.team.infra.jwt.JwtService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -15,25 +14,22 @@ @RequestMapping("/v1/festivals") @RequiredArgsConstructor public class FestivalController { - private final JwtService jwtService; private final FestivalFacade festivalFacade; @PostMapping("/verify") public ResponseEntity> verifyFestival( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @RequestBody FestivalVerifyRequest request ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); FestivalVerifyResponse response = festivalFacade.verifyFestival(userId, request); return ResponseBuilder.ok(response); } @GetMapping("/{festivalId}") public ResponseEntity> getFestivalInfo( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); FestivalInfoResponse response = festivalFacade.getFestivalInfo(userId, festivalId); return ResponseBuilder.ok(response); } diff --git a/src/main/java/org/festimate/team/api/matching/MatchingController.java b/src/main/java/org/festimate/team/api/matching/MatchingController.java index 3c554f3..86254db 100644 --- a/src/main/java/org/festimate/team/api/matching/MatchingController.java +++ b/src/main/java/org/festimate/team/api/matching/MatchingController.java @@ -7,7 +7,6 @@ import org.festimate.team.api.matching.dto.MatchingStatusResponse; import org.festimate.team.global.response.ApiResponse; import org.festimate.team.global.response.ResponseBuilder; -import org.festimate.team.infra.jwt.JwtService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -16,38 +15,31 @@ @RequiredArgsConstructor public class MatchingController { private final MatchingFacade matchingFacade; - private final JwtService jwtService; @PostMapping("/{festivalId}/matchings") public ResponseEntity> createMatching( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); - MatchingStatusResponse response = matchingFacade.createMatching(userId, festivalId); return ResponseBuilder.created(response); } @GetMapping("/{festivalId}/matchings") public ResponseEntity> getMatching( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); - MatchingListResponse response = matchingFacade.getMatchingList(userId, festivalId); return ResponseBuilder.ok(response); } @GetMapping("/{festivalId}/matchings/{matchingId}") public ResponseEntity> getMatchingDetail( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId, @PathVariable("matchingId") Long matchingId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); - MatchingDetailInfo response = matchingFacade.getMatchingDetail(userId, festivalId, matchingId); return ResponseBuilder.ok(response); } diff --git a/src/main/java/org/festimate/team/api/participant/ParticipantController.java b/src/main/java/org/festimate/team/api/participant/ParticipantController.java index 473f5cc..d9b9bfb 100644 --- a/src/main/java/org/festimate/team/api/participant/ParticipantController.java +++ b/src/main/java/org/festimate/team/api/participant/ParticipantController.java @@ -6,7 +6,6 @@ import org.festimate.team.domain.participant.service.ParticipantService; import org.festimate.team.global.response.ApiResponse; import org.festimate.team.global.response.ResponseBuilder; -import org.festimate.team.infra.jwt.JwtService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -15,27 +14,24 @@ @RequiredArgsConstructor public class ParticipantController { - private final JwtService jwtService; private final ParticipantService participantService; private final ParticipantFacade participantFacade; @PostMapping("/{festivalId}/participants/type") public ResponseEntity> getFestivalType( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable Long festivalId, @RequestBody TypeRequest request ) { - jwtService.parseTokenAndGetUserId(accessToken); TypeResponse response = participantService.getTypeResult(request); return ResponseBuilder.ok(response); } @GetMapping("/{festivalId}/participants/me") public ResponseEntity> entryFestival( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); EntryResponse response = participantFacade.entryFestival(userId, festivalId); return ResponseBuilder.ok(response); @@ -43,31 +39,28 @@ public ResponseEntity> entryFestival( @PostMapping("/{festivalId}/participants") public ResponseEntity> createParticipant( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId, @RequestBody ProfileRequest request ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); EntryResponse response = participantFacade.createParticipant(userId, festivalId, request); return ResponseBuilder.created(response); } @GetMapping("/{festivalId}/participants/me/profile") public ResponseEntity> getMyProfile( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); ProfileResponse response = participantFacade.getParticipantProfile(userId, festivalId); return ResponseBuilder.ok(response); } @GetMapping("/{festivalId}/participants/me/summary") public ResponseEntity> getParticipantAndPoint( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); MainUserInfoResponse response = participantFacade.getParticipantSummary(userId, festivalId); return ResponseBuilder.ok(response); @@ -75,10 +68,9 @@ public ResponseEntity> getParticipantAndPoint( @GetMapping("/{festivalId}/participants/me/type") public ResponseEntity> getParticipantType( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); DetailProfileResponse response = participantFacade.getParticipantType(userId, festivalId); return ResponseBuilder.ok(response); @@ -86,11 +78,10 @@ public ResponseEntity> getParticipantType( @PatchMapping("/{festivalId}/participants/me/message") public ResponseEntity> modifyMyMessage( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId, @RequestBody MessageRequest request ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); participantFacade.modifyMessage(userId, festivalId, request); return ResponseBuilder.created(null); diff --git a/src/main/java/org/festimate/team/api/point/PointController.java b/src/main/java/org/festimate/team/api/point/PointController.java index 2ee3c39..5596ee6 100644 --- a/src/main/java/org/festimate/team/api/point/PointController.java +++ b/src/main/java/org/festimate/team/api/point/PointController.java @@ -5,7 +5,6 @@ import org.festimate.team.api.point.dto.PointHistoryResponse; import org.festimate.team.global.response.ApiResponse; import org.festimate.team.global.response.ResponseBuilder; -import org.festimate.team.infra.jwt.JwtService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -14,15 +13,13 @@ @RequiredArgsConstructor public class PointController { - private final JwtService jwtService; private final PointFacade pointFacade; @GetMapping("/festivals/{festivalId}/participants/me/points") public ResponseEntity> getMyPointHistory( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @PathVariable("festivalId") Long festivalId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); PointHistoryResponse response = pointFacade.getMyPointHistory(userId, festivalId); return ResponseBuilder.ok(response); } diff --git a/src/main/java/org/festimate/team/api/user/UserController.java b/src/main/java/org/festimate/team/api/user/UserController.java index 96480c0..939dcb6 100644 --- a/src/main/java/org/festimate/team/api/user/UserController.java +++ b/src/main/java/org/festimate/team/api/user/UserController.java @@ -56,20 +56,18 @@ public ResponseEntity> signUp( @GetMapping("/me/nickname") public ResponseEntity> getNickname( - @RequestHeader("Authorization") String accessToken + @RequestAttribute("userId") Long userId ) { - Long userId = jwtService.parseTokenAndGetUserId(accessToken); UserInfoDto userInfoDto = userService.getUserNicknameAndAppearanceType(userId); return ResponseBuilder.ok(UserInfoResponse.from(userInfoDto.nickname(), userInfoDto.appearanceType())); } @GetMapping("/me/festivals") public ResponseEntity>> getMyFestival( - @RequestHeader("Authorization") String accessToken, + @RequestAttribute("userId") Long userId, @RequestParam("status") String status ) { userRequestValidator.statusValidate(status); - Long userId = jwtService.parseTokenAndGetUserId(accessToken); return ResponseBuilder.ok(festivalFacade.getUserFestivals(userId, status)); } } diff --git a/src/main/java/org/festimate/team/domain/matching/repository/MatchingRepository.java b/src/main/java/org/festimate/team/domain/matching/repository/MatchingRepository.java index f09239a..bc3f451 100644 --- a/src/main/java/org/festimate/team/domain/matching/repository/MatchingRepository.java +++ b/src/main/java/org/festimate/team/domain/matching/repository/MatchingRepository.java @@ -12,34 +12,7 @@ import java.util.List; import java.util.Optional; -public interface MatchingRepository extends JpaRepository { - @Query(""" - SELECT p FROM Participant p - JOIN FETCH p.user - LEFT JOIN Matching m1 - ON (m1.applicantParticipant = p OR m1.targetParticipant = p) - WHERE p.festival.festivalId = :festivalId - AND p.typeResult = :typeResult - AND p.user.gender != :gender - AND p.participantId != :participantId - AND p.participantId NOT IN ( - SELECT m.targetParticipant.participantId - FROM Matching m - WHERE m.applicantParticipant.participantId = :participantId - AND m.status = 'COMPLETED' - ) - GROUP BY p - ORDER BY COUNT(m1) ASC - """) - List findMatchingCandidates( - @Param("participantId") Long participantId, - @Param("typeResult") TypeResult typeResult, - @Param("gender") Gender gender, - @Param("festivalId") Long festivalId, - Pageable pageable - ); - - +public interface MatchingRepository extends JpaRepository, MatchingRepositoryCustom { @Query(""" SELECT m FROM Matching m WHERE m.festival.festivalId = :festivalId diff --git a/src/main/java/org/festimate/team/domain/matching/repository/MatchingRepositoryCustom.java b/src/main/java/org/festimate/team/domain/matching/repository/MatchingRepositoryCustom.java new file mode 100644 index 0000000..d9a0a87 --- /dev/null +++ b/src/main/java/org/festimate/team/domain/matching/repository/MatchingRepositoryCustom.java @@ -0,0 +1,21 @@ +package org.festimate.team.domain.matching.repository; + +import org.festimate.team.domain.participant.entity.Participant; +import org.festimate.team.domain.participant.entity.TypeResult; +import org.festimate.team.domain.user.entity.Gender; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface MatchingRepositoryCustom { + List findExcludeIds(Long applicantId); + + List findMatchingCandidatesDsl( + Long applicantId, + TypeResult typeResult, + Gender gender, + Long festivalId, + Pageable pageable, + List excludedIds + ); +} diff --git a/src/main/java/org/festimate/team/domain/matching/repository/MatchingRepositoryImpl.java b/src/main/java/org/festimate/team/domain/matching/repository/MatchingRepositoryImpl.java new file mode 100644 index 0000000..c1d7df8 --- /dev/null +++ b/src/main/java/org/festimate/team/domain/matching/repository/MatchingRepositoryImpl.java @@ -0,0 +1,65 @@ +package org.festimate.team.domain.matching.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.festimate.team.domain.matching.entity.MatchingStatus; +import org.festimate.team.domain.matching.entity.QMatching; +import org.festimate.team.domain.participant.entity.Participant; +import org.festimate.team.domain.participant.entity.QParticipant; +import org.festimate.team.domain.participant.entity.TypeResult; +import org.festimate.team.domain.user.entity.Gender; +import org.festimate.team.domain.user.entity.QUser; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +@RequiredArgsConstructor +public class MatchingRepositoryImpl implements MatchingRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + private final QParticipant p = QParticipant.participant; + private final QMatching m = QMatching.matching; + private final QMatching mSub = new QMatching("mSub"); + private final QUser u = QUser.user; + + @Override + public List findExcludeIds(Long applicantId) { + return queryFactory + .select(mSub.targetParticipant.participantId) + .from(mSub) + .where( + mSub.applicantParticipant.participantId.eq(applicantId) + .and(mSub.status.eq(MatchingStatus.COMPLETED)) + ) + .fetch(); + } + + @Override + public List findMatchingCandidatesDsl( + Long applicantId, + TypeResult typeResult, + Gender gender, + Long festivalId, + Pageable pageable, + List excludedIds + ) { + return queryFactory + .selectFrom(p) + .join(p.user, u).fetchJoin() + .leftJoin(m) + .on(m.applicantParticipant.eq(p).or(m.targetParticipant.eq(p))) + .where( + p.festival.festivalId.eq(festivalId), + p.typeResult.eq(typeResult), + u.gender.ne(gender), + p.participantId.ne(applicantId), + excludedIds.isEmpty() ? null : p.participantId.notIn(excludedIds) + ) + .groupBy(p) + .orderBy(m.count().asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + } +} diff --git a/src/main/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImpl.java b/src/main/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImpl.java index a0ac21a..77da1ba 100644 --- a/src/main/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImpl.java +++ b/src/main/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImpl.java @@ -27,11 +27,12 @@ import java.util.Map; import java.util.Optional; -import static org.festimate.team.domain.matching.validator.MatchingValidator.isMatchingDateValid; +import static org.festimate.team.domain.matching.validator.MatchingValidator.*; @Service @RequiredArgsConstructor @Slf4j +@Transactional(readOnly = true) public class MatchingServiceImpl implements MatchingService { private final MatchingRepository matchingRepository; private final PointService pointService; @@ -48,14 +49,12 @@ public MatchingStatusResponse createMatching(Participant participant, Festival f return MatchingStatusResponse.of(matching.getStatus(), matching.getMatchingId()); } - @Transactional(readOnly = true) @Override public MatchingListResponse getMatchingList(Participant participant) { List matchings = getMatchingListByParticipant(participant); return MatchingListResponse.from(matchings); } - @Transactional(readOnly = true) @Override public AdminMatchingResponse getMatchingSize(Participant participant) { int allMatchingSize = matchingRepository.countAllByApplicant(participant); @@ -63,7 +62,6 @@ public AdminMatchingResponse getMatchingSize(Participant participant) { return AdminMatchingResponse.from(completeMatchingSize, allMatchingSize); } - @Transactional(readOnly = true) @Override public MatchingDetailInfo getMatchingDetail(Participant participant, Festival festival, Long matchingId) { Matching matching = matchingRepository.findByMatchingId(matchingId) @@ -71,9 +69,8 @@ public MatchingDetailInfo getMatchingDetail(Participant participant, Festival fe if (matching.getTargetParticipant() == null || matching.getTargetParticipant().getUser() == null) { throw new FestimateException(ResponseError.TARGET_NOT_FOUND); } - if (!matching.getFestival().getFestivalId().equals(festival.getFestivalId())) { - throw new FestimateException(ResponseError.FORBIDDEN_RESOURCE); - } + isFestivalMatchValid(matching, festival); + isApplicantParticipantValid(matching, participant); return MatchingDetailInfo.from(matching); } @@ -93,16 +90,18 @@ protected Matching saveMatching(Festival festival, Optional targetP @Transactional @Override public Optional findBestCandidateByPriority(long festivalId, Participant participant) { + List excludedIds = matchingRepository.findExcludeIds(participant.getParticipantId()); List priorities = MATCHING_PRIORITIES.get(participant.getTypeResult()); Gender myGender = participant.getUser().getGender(); for (TypeResult priorityType : priorities) { - Optional candidate = matchingRepository.findMatchingCandidates( + Optional candidate = matchingRepository.findMatchingCandidatesDsl( participant.getParticipantId(), priorityType, myGender, festivalId, - PageRequest.of(0, 1) + PageRequest.of(0, 1), + excludedIds ).stream().findFirst(); if (candidate.isPresent()) { diff --git a/src/main/java/org/festimate/team/domain/matching/validator/MatchingValidator.java b/src/main/java/org/festimate/team/domain/matching/validator/MatchingValidator.java index f87c664..8e71d15 100644 --- a/src/main/java/org/festimate/team/domain/matching/validator/MatchingValidator.java +++ b/src/main/java/org/festimate/team/domain/matching/validator/MatchingValidator.java @@ -1,24 +1,32 @@ package org.festimate.team.domain.matching.validator; -import lombok.extern.slf4j.Slf4j; -import org.festimate.team.global.response.ResponseError; +import org.festimate.team.domain.festival.entity.Festival; +import org.festimate.team.domain.matching.entity.Matching; +import org.festimate.team.domain.participant.entity.Participant; import org.festimate.team.global.exception.FestimateException; +import org.festimate.team.global.response.ResponseError; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @Component -@Slf4j public class MatchingValidator { public static void isMatchingDateValid(final LocalDateTime requestMatchingTime, final LocalDateTime matchingStartAt) { - log.info("Validating requestMatchingTime. requestMatchingTime={}, matchingStartAt={}", requestMatchingTime, matchingStartAt); - if (requestMatchingTime.isBefore(matchingStartAt)) { - log.warn("Invalid requestMatchingTime: requestMatchingTime is before festival matchingStartAt"); throw new FestimateException(ResponseError.INVALID_TIME_TYPE); } + } + + public static void isApplicantParticipantValid(final Matching matching, final Participant participant) { + if (!matching.getApplicantParticipant().equals(participant)) { + throw new FestimateException(ResponseError.FORBIDDEN_RESOURCE); + } + } - log.info("matchingStartAt is valid."); + public static void isFestivalMatchValid(final Matching matching, final Festival festival) { + if (!matching.getFestival().getFestivalId().equals(festival.getFestivalId())) { + throw new FestimateException(ResponseError.FORBIDDEN_RESOURCE); + } } } \ No newline at end of file diff --git a/src/main/java/org/festimate/team/infra/config/QuerydslConfig.java b/src/main/java/org/festimate/team/infra/config/QuerydslConfig.java new file mode 100644 index 0000000..a038a97 --- /dev/null +++ b/src/main/java/org/festimate/team/infra/config/QuerydslConfig.java @@ -0,0 +1,14 @@ +package org.festimate.team.infra.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QuerydslConfig { + @Bean + public JPAQueryFactory jpaQueryFactory(EntityManager em) { + return new JPAQueryFactory(em); + } +} \ No newline at end of file diff --git a/src/main/java/org/festimate/team/infra/config/SecurityConfig.java b/src/main/java/org/festimate/team/infra/config/SecurityConfig.java index 4cbce38..5a7b5f7 100644 --- a/src/main/java/org/festimate/team/infra/config/SecurityConfig.java +++ b/src/main/java/org/festimate/team/infra/config/SecurityConfig.java @@ -1,24 +1,31 @@ package org.festimate.team.infra.config; +import lombok.RequiredArgsConstructor; +import org.festimate.team.infra.jwt.JwtAuthFilter; +import org.festimate.team.infra.jwt.JwtParser; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity +@RequiredArgsConstructor public class SecurityConfig { + private final JwtParser jwtParser; + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - .cors(cors -> {}) + .cors(cors -> { + }) .csrf(AbstractHttpConfigurer::disable) - .authorizeHttpRequests(auth -> auth - .anyRequest().permitAll() - ); + .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) + .addFilterBefore(new JwtAuthFilter(jwtParser), UsernamePasswordAuthenticationFilter.class); return http.build(); } diff --git a/src/main/java/org/festimate/team/infra/jwt/JwtAuthFilter.java b/src/main/java/org/festimate/team/infra/jwt/JwtAuthFilter.java new file mode 100644 index 0000000..6075af9 --- /dev/null +++ b/src/main/java/org/festimate/team/infra/jwt/JwtAuthFilter.java @@ -0,0 +1,40 @@ +package org.festimate.team.infra.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Slf4j +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + private final JwtParser jwtParser; + private static final String USER_ID = "userId"; + private static final String AUTHORIZATION = "Authorization"; + private static final String BEARER = "Bearer "; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String header = request.getHeader(AUTHORIZATION); + if (header != null && header.startsWith(BEARER)) { + String token = header.substring(BEARER.length()); + try { + Long userId = jwtParser.getUserIdFromToken(token); + request.setAttribute(USER_ID, userId); + } catch (Exception e) { + log.warn("JWT authentication failed: {}", e.getMessage()); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT token"); + return; + } + } + + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/org/festimate/team/infra/jwt/JwtParser.java b/src/main/java/org/festimate/team/infra/jwt/JwtParser.java new file mode 100644 index 0000000..bdfec5b --- /dev/null +++ b/src/main/java/org/festimate/team/infra/jwt/JwtParser.java @@ -0,0 +1,69 @@ +package org.festimate.team.infra.jwt; + + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.festimate.team.global.exception.FestimateException; +import org.festimate.team.global.response.ResponseError; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class JwtParser { + private final JwtTokenProvider jwtTokenProvider; + private static final String USER_ID = "userId"; + private static final String PLATFORM_ID = "platformId"; + private static final String BEARER = "Bearer "; + + public Claims parseClaims(String token) { + try { + return Jwts.parser() + .verifyWith(jwtTokenProvider.getSecretKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } catch (Exception e) { + log.error("JWT 파싱 오류: {}", e.getMessage()); + throw new FestimateException(ResponseError.INVALID_TOKEN); + } + } + + public Long getUserIdFromToken(String token) { + Claims claims = parseClaims(token); + Object userId = claims.get(USER_ID); + if (userId instanceof Number) return ((Number) userId).longValue(); + if (userId instanceof String) { + try { + return Long.parseLong((String) userId); + } catch (NumberFormatException e) { + log.error("Invalid userId format in token: {}", userId); + } + } + log.error("userId claim missing or invalid type: {}", userId); + throw new FestimateException(ResponseError.INVALID_TOKEN); + } + + public String getPlatformIdFromToken(String token) { + Claims claims = parseClaims(token); + Object platformId = claims.get(PLATFORM_ID); + if (platformId != null) return platformId.toString(); + throw new FestimateException(ResponseError.INVALID_TOKEN); + } + + public void validateToken(String token) { + if (token == null || !token.startsWith(BEARER)) { + throw new FestimateException(ResponseError.INVALID_TOKEN); + } + try { + String splitToken = token.substring(BEARER.length()); + parseClaims(splitToken); + } catch (Exception e) { + log.error("JWT 유효성 오류: {}", e.getMessage()); + throw new FestimateException(ResponseError.INVALID_TOKEN); + } + } +} + diff --git a/src/main/java/org/festimate/team/infra/jwt/JwtService.java b/src/main/java/org/festimate/team/infra/jwt/JwtService.java index c17c04d..13eeeff 100644 --- a/src/main/java/org/festimate/team/infra/jwt/JwtService.java +++ b/src/main/java/org/festimate/team/infra/jwt/JwtService.java @@ -1,192 +1,32 @@ package org.festimate.team.infra.jwt; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.festimate.team.api.auth.dto.TokenResponse; -import org.festimate.team.global.response.ResponseError; -import org.festimate.team.global.exception.FestimateException; import org.festimate.team.domain.user.entity.User; import org.festimate.team.domain.user.service.UserService; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; +import org.springframework.stereotype.Service; -import javax.crypto.SecretKey; -import java.nio.charset.StandardCharsets; -import java.util.Date; - -@Component @RequiredArgsConstructor @Slf4j +@Service public class JwtService { - private static final String USER_ID = "userId"; - private static final String PLATFROM_ID = "platformId"; - private static final String BEARER = "Bearer "; - private static final ObjectMapper objectMapper = new ObjectMapper(); - private final JwtProperties jwtProperties; + private final JwtTokenProvider tokenProvider; + private final JwtParser jwtParser; private final UserService userService; - public String createAccessToken(final Long userId) { - SecretKey secretKey = getSecretKey(); - return Jwts.builder() - .subject(userId.toString()) - .expiration(new Date(System.currentTimeMillis() + jwtProperties.getAccessExpiration())) - .claim(USER_ID, userId) - .signWith(secretKey) - .compact(); - } - - public String createRefreshToken(final Long userId) { - SecretKey secretKey = getSecretKey(); - return buildRefreshToken(userId, secretKey); - } - - public String createTempAccessToken(final String platformId) { - SecretKey secretKey = getSecretKey(); - return Jwts.builder() - .subject(platformId) - .expiration(new Date(System.currentTimeMillis() + jwtProperties.getAccessExpiration())) - .claim(PLATFROM_ID, platformId) - .signWith(secretKey) - .compact(); - } - - public String createTempRefreshToken(final String platformId) { - SecretKey secretKey = getSecretKey(); - return Jwts.builder() - .subject(platformId) - .expiration(new Date(System.currentTimeMillis() + jwtProperties.getRefreshExpiration())) - .claim(PLATFROM_ID, platformId) - .signWith(secretKey) - .compact(); - } - - - private String buildRefreshToken(Long userId, SecretKey secretKey) { - return Jwts.builder() - .subject(userId.toString()) - .expiration(new Date(System.currentTimeMillis() + jwtProperties.getRefreshExpiration())) - .claim(USER_ID, userId) - .signWith(secretKey) - .compact(); - } - - @Transactional - public TokenResponse reIssueToken(final String refreshToken) { - Long userId = parseTokenAndGetUserId(refreshToken); - User findUser = userService.getUserByIdOrThrow(userId); - String extractPrefixToken = refreshToken.split(" ")[1]; - isValidRefreshToken(findUser, extractPrefixToken); - String renewRefreshToken = createRefreshToken(userId); - findUser.updateRefreshToken(renewRefreshToken); - - return TokenResponse.of(userId, createAccessToken(userId), renewRefreshToken); - } - - private void isValidRefreshToken(User findUser, String refreshToken) { - userService.validateRefreshToken(findUser, refreshToken); - } - - public Long parseTokenAndGetUserId(String token) { - isValidToken(token); - - try { - String splitToken = token.split(" ")[1]; - SecretKey secretKey = getSecretKey(); - Long userId = parseTokenAndGetUserId(secretKey, splitToken); - - if (userService.getUserByIdOrThrow(userId).getRefreshToken() == null) { - throw new FestimateException(ResponseError.INVALID_TOKEN); - } - return userId; - } catch (JwtException | NumberFormatException e) { - log.error("JWT parsing error : {}", e.getMessage()); - throw new FestimateException(ResponseError.INVALID_TOKEN); - } + public TokenResponse reIssueToken(String refreshToken) { + Long userId = jwtParser.getUserIdFromToken(refreshToken); + User user = userService.getUserByIdOrThrow(userId); + userService.validateRefreshToken(user, refreshToken); + String newRefreshToken = tokenProvider.createRefreshToken(userId); + user.updateRefreshToken(newRefreshToken); + return TokenResponse.of(userId, tokenProvider.createAccessToken(userId), newRefreshToken); } - private Long parseTokenAndGetUserId(SecretKey secretKey, String token) { - Claims claims = Jwts.parser() - .verifyWith(secretKey) - .build() - .parseSignedClaims(token) - .getPayload(); - - if (!claims.containsKey(USER_ID)) { - log.error("잘못된 accessToken: userId 없음."); - throw new FestimateException(ResponseError.INVALID_ACCESS_TOKEN); - } - - try { - Object userIdObject = claims.get(USER_ID); - if (userIdObject instanceof Number) { - return ((Number) userIdObject).longValue(); - } else { - throw new FestimateException(ResponseError.INVALID_TOKEN); - } - } catch (Exception e) { - log.error("userId 변환 오류: {}", e.getMessage()); - throw new FestimateException(ResponseError.INVALID_TOKEN); - } - } - - public String extractPlatformUserIdFromToken(String token) { - try { - String splitToken = token.split(" ")[1]; - SecretKey secretKey = getSecretKey(); - return parseTokenAndGetPlatformUserId(secretKey, splitToken); - } catch (JwtException e) { - log.error("JWT parsing error: {}", e.getMessage()); - throw new FestimateException(ResponseError.INVALID_TOKEN); - } - } - - private String parseTokenAndGetPlatformUserId(SecretKey secretKey, String token) { - return Jwts.parser() - .verifyWith(secretKey) - .build() - .parseSignedClaims(token) - .getPayload() - .get(PLATFROM_ID) - .toString(); - } - - - private SecretKey getSecretKey() { - return Keys.hmacShaKeyFor( - jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8)); - } - - - public void isValidToken(String token) { - if (token == null || !token.startsWith(BEARER)) { - throw new FestimateException(ResponseError.INVALID_TOKEN); - } - - try { - String splitToken = token.split(" ")[1]; - SecretKey secretKey = getSecretKey(); - - parseTokenAndGetUserId(secretKey, splitToken); - } catch (JwtException | NumberFormatException e) { - log.error("JWT parsing error : {}", e.getMessage()); - throw new FestimateException(ResponseError.INVALID_TOKEN); - } - } - - public static JsonNode parseJson(String jsonString) { - try { - return objectMapper.readTree(jsonString); - } catch (Exception e) { - log.error("JSON 파싱 실패", e); - throw new IllegalArgumentException("JSON 파싱에 실패했습니다."); - } + return jwtParser.getPlatformIdFromToken(token); } } + diff --git a/src/main/java/org/festimate/team/infra/jwt/JwtTokenProvider.java b/src/main/java/org/festimate/team/infra/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..1b4dece --- /dev/null +++ b/src/main/java/org/festimate/team/infra/jwt/JwtTokenProvider.java @@ -0,0 +1,59 @@ +package org.festimate.team.infra.jwt; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; + + +@Component +@RequiredArgsConstructor +public class JwtTokenProvider { + private final JwtProperties jwtProperties; + private static final String USER_ID = "userId"; + private static final String PLATFORM_ID = "platformId"; + + public String createAccessToken(Long userId) { + return Jwts.builder() + .subject(userId.toString()) + .expiration(new Date(System.currentTimeMillis() + jwtProperties.getAccessExpiration())) + .claim(USER_ID, userId) + .signWith(getSecretKey()) + .compact(); + } + + public String createRefreshToken(Long userId) { + return Jwts.builder() + .subject(userId.toString()) + .expiration(new Date(System.currentTimeMillis() + jwtProperties.getRefreshExpiration())) + .claim(USER_ID, userId) + .signWith(getSecretKey()) + .compact(); + } + + public String createTempAccessToken(String platformId) { + return Jwts.builder() + .subject(platformId) + .expiration(new Date(System.currentTimeMillis() + jwtProperties.getAccessExpiration())) + .claim(PLATFORM_ID, platformId) + .signWith(getSecretKey()) + .compact(); + } + + public String createTempRefreshToken(String platformId) { + return Jwts.builder() + .subject(platformId) + .expiration(new Date(System.currentTimeMillis() + jwtProperties.getRefreshExpiration())) + .claim(PLATFORM_ID, platformId) + .signWith(getSecretKey()) + .compact(); + } + + public SecretKey getSecretKey() { + return Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/test/java/org/festimate/team/api/facade/LoginFacadeTest.java b/src/test/java/org/festimate/team/api/facade/LoginFacadeTest.java index fd00ee5..1233322 100644 --- a/src/test/java/org/festimate/team/api/facade/LoginFacadeTest.java +++ b/src/test/java/org/festimate/team/api/facade/LoginFacadeTest.java @@ -5,6 +5,7 @@ import org.festimate.team.domain.user.entity.Platform; import org.festimate.team.domain.user.service.UserService; import org.festimate.team.infra.jwt.JwtService; +import org.festimate.team.infra.jwt.JwtTokenProvider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -22,6 +23,8 @@ class LoginFacadeTest { @Mock private JwtService jwtService; @Mock + private JwtTokenProvider jwtTokenProvider; + @Mock private UserService userService; @Mock private KakaoLoginService kakaoLoginService; @@ -41,8 +44,8 @@ void login_existingUser_success() { when(userService.getUserIdByPlatform(Platform.KAKAO, "platformId")) .thenReturn(Optional.of(1L)); - when(jwtService.createAccessToken(1L)).thenReturn("access-token"); - when(jwtService.createRefreshToken(1L)).thenReturn("refresh-token"); + when(jwtTokenProvider.createAccessToken(1L)).thenReturn("access-token"); + when(jwtTokenProvider.createRefreshToken(1L)).thenReturn("refresh-token"); // when TokenResponse response = loginFacade.login("platformId", Platform.KAKAO); @@ -60,8 +63,8 @@ void login_newUser_tempToken_success() { when(userService.getUserIdByPlatform(Platform.KAKAO, "platformId")) .thenReturn(Optional.empty()); - when(jwtService.createTempAccessToken("platformId")).thenReturn("temp-access-token"); - when(jwtService.createTempRefreshToken("platformId")).thenReturn("temp-refresh-token"); + when(jwtTokenProvider.createTempAccessToken("platformId")).thenReturn("temp-access-token"); + when(jwtTokenProvider.createTempRefreshToken("platformId")).thenReturn("temp-refresh-token"); // when TokenResponse response = loginFacade.login("platformId", Platform.KAKAO); diff --git a/src/test/java/org/festimate/team/api/facade/ParticipantFacadeTest.java b/src/test/java/org/festimate/team/api/facade/ParticipantFacadeTest.java index fc5427b..0fb08ce 100644 --- a/src/test/java/org/festimate/team/api/facade/ParticipantFacadeTest.java +++ b/src/test/java/org/festimate/team/api/facade/ParticipantFacadeTest.java @@ -81,6 +81,24 @@ void createParticipant_success() { verify(matchingService).matchPendingParticipants(participant); } + @Test + @DisplayName("참가자 생성 시 10포인트가 충전된다") + void createParticipant_success_point() { + when(userService.getUserByIdOrThrow(1L)).thenReturn(user); + when(festivalService.getFestivalByIdOrThrow(1L)).thenReturn(festival); + when(participantService.getParticipant(user, festival)).thenReturn(null); + when(participantService.createParticipant(any(), any(), any())).thenReturn(participant); + + var response = participantFacade.createParticipant(1L, 1L, null); + + // then + assertThat(response.participantId()).isNotNull(); + // 포인트 충전 메서드 호출 여부 검증 + verify(pointService).rechargePoint(participant, 10); + // 매칭 메서드 호출 여부 검증 + verify(matchingService).matchPendingParticipants(participant); + } + @Test @DisplayName("내 프로필 조회 성공") void getParticipantProfile_success() { diff --git a/src/test/java/org/festimate/team/api/facade/SignUpFacadeTest.java b/src/test/java/org/festimate/team/api/facade/SignUpFacadeTest.java index 2d366f4..57135bf 100644 --- a/src/test/java/org/festimate/team/api/facade/SignUpFacadeTest.java +++ b/src/test/java/org/festimate/team/api/facade/SignUpFacadeTest.java @@ -8,7 +8,7 @@ import org.festimate.team.domain.user.validator.NicknameValidator; import org.festimate.team.global.exception.FestimateException; import org.festimate.team.global.response.ResponseError; -import org.festimate.team.infra.jwt.JwtService; +import org.festimate.team.infra.jwt.JwtTokenProvider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -26,7 +26,7 @@ class SignUpFacadeTest { @Mock private UserService userService; @Mock - private JwtService jwtService; + private JwtTokenProvider jwtTokenProvider; @Mock private NicknameValidator nicknameValidator; @@ -71,8 +71,8 @@ void signUp_success() { when(userService.getUserIdByPlatformId("platformId")).thenReturn(null); when(userService.signUp(signUpRequest, "platformId")).thenReturn(user); - when(jwtService.createAccessToken(1L)).thenReturn("access-token"); - when(jwtService.createRefreshToken(1L)).thenReturn("refresh-token"); + when(jwtTokenProvider.createAccessToken(1L)).thenReturn("access-token"); + when(jwtTokenProvider.createRefreshToken(1L)).thenReturn("refresh-token"); // when TokenResponse response = signUpFacade.signUp("platformId", signUpRequest); diff --git a/src/test/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImplTest.java b/src/test/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImplTest.java index 3a1b759..9233e38 100644 --- a/src/test/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImplTest.java +++ b/src/test/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImplTest.java @@ -116,6 +116,7 @@ void findBestCandidateByPriority_success() { .festival(festival) .build(); ReflectionTestUtils.setField(applicant, "participantId", 1L); + List excludedIds = matchingRepository.findExcludeIds(applicant.getParticipantId()); Participant target = Participant.builder() .user(targetUser) @@ -123,7 +124,7 @@ void findBestCandidateByPriority_success() { .festival(festival) .build(); - when(matchingRepository.findMatchingCandidates(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1))) + when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds)) .thenReturn(List.of(target)); var result = matchingService.findBestCandidateByPriority(festival.getFestivalId(), applicant); @@ -145,9 +146,10 @@ void findBestCandidate_prioritySecond_success() { .festival(festival) .build(); ReflectionTestUtils.setField(applicant, "participantId", 1L); + List excludedIds = matchingRepository.findExcludeIds(applicant.getParticipantId()); // 1순위 PHOTO에 대상 없음 - when(matchingRepository.findMatchingCandidates(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1))) + when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds)) .thenReturn(Collections.emptyList()); // 2순위 INFLUENCER에 대상 있음 @@ -157,7 +159,7 @@ void findBestCandidate_prioritySecond_success() { .festival(festival) .build(); - when(matchingRepository.findMatchingCandidates(1L, TypeResult.INFLUENCER, Gender.MAN, 1L, PageRequest.of(0, 1))) + when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.INFLUENCER, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds)) .thenReturn(List.of(secondPriorityCandidate)); var result = matchingService.findBestCandidateByPriority(festival.getFestivalId(), applicant); @@ -176,16 +178,18 @@ void findBestCandidate_priorityThird_success() { // 신청자 Participant applicant = mockParticipant(applicantUser, festival, TypeResult.INFLUENCER, 1L); + List excludedIds = matchingRepository.findExcludeIds(applicant.getParticipantId()); + // 3순위 NEWBIE에 대상 있음 Participant thirdPriorityCandidate = mockParticipant(thirdPriorityUser, festival, TypeResult.NEWBIE, 2L); // 1순위 PHOTO, 2순위 INFLUENCER 대상 없음 - when(matchingRepository.findMatchingCandidates(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1))) + when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds)) .thenReturn(Collections.emptyList()); - when(matchingRepository.findMatchingCandidates(1L, TypeResult.INFLUENCER, Gender.MAN, 1L, PageRequest.of(0, 1))) + when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.INFLUENCER, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds)) .thenReturn(Collections.emptyList()); // 3순위 대상 있음 - when(matchingRepository.findMatchingCandidates(1L, TypeResult.NEWBIE, Gender.MAN, 1L, PageRequest.of(0, 1))) + when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.NEWBIE, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds)) .thenReturn(List.of(thirdPriorityCandidate)); var result = matchingService.findBestCandidateByPriority(festival.getFestivalId(), applicant); @@ -206,13 +210,14 @@ void findBestCandidate_alreadyMatchedCandidate_empty() { .festival(festival) .build(); ReflectionTestUtils.setField(applicant, "participantId", 1L); + List excludedIds = matchingRepository.findExcludeIds(applicant.getParticipantId()); // 대상이 존재하나 이미 매칭됨 (Repository에서 필터링 됨) - when(matchingRepository.findMatchingCandidates(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1))) + when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds)) .thenReturn(Collections.emptyList()); - when(matchingRepository.findMatchingCandidates(1L, TypeResult.INFLUENCER, Gender.MAN, 1L, PageRequest.of(0, 1))) + when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.INFLUENCER, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds)) .thenReturn(Collections.emptyList()); - when(matchingRepository.findMatchingCandidates(1L, TypeResult.NEWBIE, Gender.MAN, 1L, PageRequest.of(0, 1))) + when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.NEWBIE, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds)) .thenReturn(Collections.emptyList()); Optional result = matchingService.findBestCandidateByPriority(festival.getFestivalId(), applicant); @@ -231,9 +236,10 @@ void findBestCandidateByPriority_empty() { .typeResult(TypeResult.INFLUENCER) .festival(festival) .build(); + List excludedIds = matchingRepository.findExcludeIds(applicant.getParticipantId()); - when(matchingRepository.findMatchingCandidates( - applicant.getParticipantId(), TypeResult.PHOTO, Gender.MAN, festival.getFestivalId(), PageRequest.of(0, 1) + when(matchingRepository.findMatchingCandidatesDsl( + applicant.getParticipantId(), TypeResult.PHOTO, Gender.MAN, festival.getFestivalId(), PageRequest.of(0, 1), excludedIds )).thenReturn(Collections.emptyList()); Optional result = matchingService.findBestCandidateByPriority( @@ -273,6 +279,38 @@ void getMatchingDetail_invalidFestival_throwsException() { .hasMessage(ResponseError.FORBIDDEN_RESOURCE.getMessage()); } + @Test + @DisplayName("매칭 상세 조회 - 다른 사람이 신청한 매칭을 조회할 경우 예외 발생") + void getMatchingDetail_invalidApplicant_throwsException() { + // given + User user = mockUser("사용자1", Gender.MAN, 1L); + User otherUser = mockUser("사용자2", Gender.MAN, 2L); + Festival festival = mockFestival(user, 1L, LocalDate.now().minusDays(1), LocalDate.now().plusDays(1)); + + Participant myParticipant = mockParticipant(user, festival, TypeResult.INFLUENCER, 1L); + Participant otherParticipant = mockParticipant(otherUser, festival, TypeResult.INFLUENCER, 2L); + + Matching otherMatching = Matching.builder() + .festival(festival) + .applicantParticipant(otherParticipant) + .targetParticipant(Participant.builder() + .user(mockUser("상대방", Gender.WOMAN, 3L)) + .build()) + .status(MatchingStatus.COMPLETED) + .matchDate(LocalDateTime.now()) + .build(); + + when(userService.getUserByIdOrThrow(user.getUserId())).thenReturn(user); + when(festivalService.getFestivalByIdOrThrow(festival.getFestivalId())).thenReturn(festival); + when(participantService.getParticipantOrThrow(user, festival)).thenReturn(myParticipant); + when(matchingRepository.findByMatchingId(1L)).thenReturn(Optional.of(otherMatching)); + + // when & then + assertThatThrownBy(() -> matchingService.getMatchingDetail(myParticipant, festival, 1L)) + .isInstanceOf(FestimateException.class) + .hasMessage(ResponseError.FORBIDDEN_RESOURCE.getMessage()); + } + @Test @DisplayName("매칭 상세 조회 - 보류 중인 매칭을 조회할 경우 예외 발생") void getMatchingDetail_pendingMatching_throwsException() { @@ -337,6 +375,7 @@ void findBestCandidate_multipleCandidates_returnsFirstOnly() { User applicantUser = mockUser("신청자", Gender.MAN, 1L); Festival festival = mockFestival(applicantUser, 1L, LocalDate.now().minusDays(1), LocalDate.now().plusDays(1)); Participant applicant = mockParticipant(applicantUser, festival, TypeResult.INFLUENCER, 1L); + List excludedIds = matchingRepository.findExcludeIds(applicant.getParticipantId()); User candidate1 = mockUser("후보1", Gender.WOMAN, 2L); User candidate2 = mockUser("후보2", Gender.WOMAN, 3L); @@ -344,7 +383,7 @@ void findBestCandidate_multipleCandidates_returnsFirstOnly() { Participant p2 = mockParticipant(candidate2, festival, TypeResult.PHOTO, 3L); // 후보 2명 반환 - when(matchingRepository.findMatchingCandidates(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1))) + when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds)) .thenReturn(List.of(p1, p2)); // when