diff --git a/src/main/kotlin/uoslife/servermeeting/admin/api/AdminApi.kt b/src/main/kotlin/uoslife/servermeeting/admin/api/AdminApi.kt index 8cb78a23..1555e476 100644 --- a/src/main/kotlin/uoslife/servermeeting/admin/api/AdminApi.kt +++ b/src/main/kotlin/uoslife/servermeeting/admin/api/AdminApi.kt @@ -161,4 +161,11 @@ class AdminApi( val requestInfo = requestUtils.toRequestInfoDto(request) return ResponseEntity.status(HttpStatus.OK).body(adminService.refundPayment(requestInfo)) } + + @Operation(summary = "매칭 결과 캐시 웜업 API", description = "결제 완료된 매칭 데이터를 캐시에 저장합니다") + @PostMapping("/cache/warmup") + fun triggerCacheWarmup(@RequestParam season: Int): ResponseEntity { + adminService.warmUpCacheAsync(season) + return ResponseEntity.status(HttpStatus.NO_CONTENT).build() + } } diff --git a/src/main/kotlin/uoslife/servermeeting/admin/service/AdminService.kt b/src/main/kotlin/uoslife/servermeeting/admin/service/AdminService.kt index e87b208e..3c8c3185 100644 --- a/src/main/kotlin/uoslife/servermeeting/admin/service/AdminService.kt +++ b/src/main/kotlin/uoslife/servermeeting/admin/service/AdminService.kt @@ -5,7 +5,10 @@ import java.time.Duration import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Qualifier import org.springframework.data.redis.core.RedisTemplate +import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Service +import uoslife.servermeeting.match.service.MatchingService +import uoslife.servermeeting.meetingteam.dao.UserTeamDao import uoslife.servermeeting.global.common.dto.RequestInfoDto import uoslife.servermeeting.payment.dto.response.PaymentResponseDto import uoslife.servermeeting.payment.service.PaymentService @@ -17,7 +20,9 @@ import uoslife.servermeeting.verification.util.VerificationUtils class AdminService( private val redisTemplate: RedisTemplate, private val userService: UserService, - @Qualifier("PortOneService") private val paymentService: PaymentService + @Qualifier("PortOneService") private val paymentService: PaymentService, + private val matchingService: MatchingService, + private val userTeamDao: UserTeamDao, ) { companion object { private val logger = LoggerFactory.getLogger(AdminService::class.java) @@ -45,4 +50,18 @@ class AdminService( logger.info("[ADMIN-매칭 실패 유저 환불] $requestInfo") return result } + + @Async + fun warmUpCacheAsync(season: Int) { + logger.info("[캐시 웜업 시작]") + val participants = userTeamDao.findAllParticipantsBySeasonAndType(season) + participants.forEach { participant -> + try { + matchingService.getMatchInfo(participant.userId, participant.teamType, season) + } catch (e: Exception) { + logger.info("[캐시 웜업 실패] userId: ${participant.userId} message: ${e.message}") + } + } + logger.info("[캐시 웜업 성공] 대상 인원: ${participants.size}") + } } diff --git a/src/main/kotlin/uoslife/servermeeting/global/auth/api/AuthApi.kt b/src/main/kotlin/uoslife/servermeeting/global/auth/api/AuthApi.kt index 866c7ca0..986f5add 100644 --- a/src/main/kotlin/uoslife/servermeeting/global/auth/api/AuthApi.kt +++ b/src/main/kotlin/uoslife/servermeeting/global/auth/api/AuthApi.kt @@ -20,7 +20,7 @@ import uoslife.servermeeting.global.auth.service.AuthService import uoslife.servermeeting.global.auth.util.CookieUtils import uoslife.servermeeting.global.error.ErrorResponse -@Tag(name = "Auth", description = "Auth API") +@Tag(name = "Auth", description = "인증 API") @RestController @RequestMapping("/api/auth") class AuthApi( diff --git a/src/main/kotlin/uoslife/servermeeting/global/config/CacheConfig.kt b/src/main/kotlin/uoslife/servermeeting/global/config/CacheConfig.kt index a9cb4d4e..c4d1b532 100644 --- a/src/main/kotlin/uoslife/servermeeting/global/config/CacheConfig.kt +++ b/src/main/kotlin/uoslife/servermeeting/global/config/CacheConfig.kt @@ -23,18 +23,16 @@ class CacheConfig(private val redisConnectionFactory: RedisConnectionFactory) { @Bean fun cacheManager(): CacheManager { val objectMapper = - ObjectMapper() - .registerModule(KotlinModule.Builder().build()) // Kotlin 지원 모듈 추가 - .apply { - configure(MapperFeature.USE_STD_BEAN_NAMING, true) - configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - activateDefaultTyping( - BasicPolymorphicTypeValidator.builder() - .allowIfBaseType(Any::class.java) - .build(), - ObjectMapper.DefaultTyping.EVERYTHING - ) - } + ObjectMapper().registerModule(KotlinModule.Builder().build()).apply { + configure(MapperFeature.USE_STD_BEAN_NAMING, true) + configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + activateDefaultTyping( + BasicPolymorphicTypeValidator.builder() + .allowIfBaseType(Any::class.java) + .build(), + ObjectMapper.DefaultTyping.EVERYTHING + ) + } val defaultConfig = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofHours(24)) @@ -51,9 +49,8 @@ class CacheConfig(private val redisConnectionFactory: RedisConnectionFactory) { val configurations = mapOf( - "user-participation" to defaultConfig.entryTtl(Duration.ofDays(2)), - "match-result" to defaultConfig.entryTtl(Duration.ofDays(2)), - "partner-info" to defaultConfig.entryTtl(Duration.ofDays(2)) + "meeting-participation" to defaultConfig.entryTtl(Duration.ofDays(2)), + "match-info" to defaultConfig.entryTtl(Duration.ofDays(2)) ) return RedisCacheManager.builder(redisConnectionFactory) diff --git a/src/main/kotlin/uoslife/servermeeting/global/error/GlobalExceptionHandler.kt b/src/main/kotlin/uoslife/servermeeting/global/error/GlobalExceptionHandler.kt index 3ec283a9..10b85833 100644 --- a/src/main/kotlin/uoslife/servermeeting/global/error/GlobalExceptionHandler.kt +++ b/src/main/kotlin/uoslife/servermeeting/global/error/GlobalExceptionHandler.kt @@ -8,6 +8,7 @@ import org.springframework.http.converter.HttpMessageNotReadableException import org.springframework.security.core.AuthenticationException import org.springframework.web.HttpRequestMethodNotSupportedException import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.MissingServletRequestParameterException import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException @@ -119,4 +120,14 @@ class GlobalExceptionHandler { val response = ErrorResponse(errorCode) return ResponseEntity(response, HttpStatus.valueOf(errorCode.status)) } + + @ExceptionHandler(MissingServletRequestParameterException::class) + fun handleMissingServletRequestParameterException( + exception: MissingServletRequestParameterException + ): ResponseEntity { + logger.error("MissingServletRequestParameterException", exception) + val errorCode = ErrorCode.INVALID_INPUT_VALUE + val response = ErrorResponse(errorCode) + return ResponseEntity(response, HttpStatus.valueOf(errorCode.status)) + } } diff --git a/src/main/kotlin/uoslife/servermeeting/match/api/MatchApi.kt b/src/main/kotlin/uoslife/servermeeting/match/api/MatchApi.kt index 23eb4beb..71983e07 100644 --- a/src/main/kotlin/uoslife/servermeeting/match/api/MatchApi.kt +++ b/src/main/kotlin/uoslife/servermeeting/match/api/MatchApi.kt @@ -7,23 +7,18 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.tags.Tag -import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.userdetails.UserDetails -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* import uoslife.servermeeting.global.error.ErrorResponse -import uoslife.servermeeting.match.dto.response.MatchResultResponse -import uoslife.servermeeting.match.dto.response.MeetingParticipationResponse +import uoslife.servermeeting.match.dto.response.* import uoslife.servermeeting.match.service.MatchingService -import uoslife.servermeeting.meetingteam.dto.response.MeetingTeamInformationGetResponse +import uoslife.servermeeting.meetingteam.entity.enums.TeamType @RestController @RequestMapping("/api/match") -@Tag(name = "Match", description = "매칭 API") +@Tag(name = "Match", description = "매칭 내역 조회 API") class MatchApi( private val matchingService: MatchingService, ) { @@ -45,68 +40,26 @@ class MatchApi( ) @GetMapping("/me/participations") fun getUserMeetingParticipation( - @AuthenticationPrincipal userDetails: UserDetails + @AuthenticationPrincipal userDetails: UserDetails, + @RequestParam season: Int, ): ResponseEntity { - val result = matchingService.getUserMeetingParticipation(userDetails.username.toLong()) + val result = + matchingService.getUserMeetingParticipation(userDetails.username.toLong(), season) return ResponseEntity.ok(result) } - @Operation(summary = "매칭 결과 조회", description = "특정 매칭의 성공 여부를 조회합니다.") + @Operation(summary = "매칭 정보 조회", description = "매칭 결과와 매칭 상대의 정보를 조회합니다.") @ApiResponses( value = [ ApiResponse( responseCode = "200", - description = "매칭 결과 정보 반환", - content = - [Content(schema = Schema(implementation = MatchResultResponse::class))] - ), - ApiResponse( - responseCode = "403", - description = "해당 팀에 대한 접근 권한 없음", - content = - [ - Content( - schema = Schema(implementation = ErrorResponse::class), - examples = - [ - ExampleObject( - name = "MT03", - value = - "{\"message\": \"Unauthorized team access.\", \"status\": 403, \"code\": \"MT03\"}" - )] - )] - )] - ) - @GetMapping("/teams/{teamId}/result") - fun getMatchResult( - @PathVariable teamId: Long, - @AuthenticationPrincipal userDetails: UserDetails - ): ResponseEntity { - return ResponseEntity.ok( - matchingService.getMatchResult(userDetails.username.toLong(), teamId) - ) - } - - @Operation(summary = "매칭된 상대방 정보 조회", description = "매칭된 상대 팀의 상세 정보를 조회합니다.") - @ApiResponses( - value = - [ - ApiResponse( - responseCode = "200", - description = "매칭된 상대방 정보 반환", - content = - [ - Content( - schema = - Schema( - implementation = MeetingTeamInformationGetResponse::class - ) - )] + description = "매칭 결과 반환", + content = [Content(schema = Schema(implementation = MatchInfoResponse::class))] ), ApiResponse( responseCode = "400", - description = "매치를 찾을 수 없음", + description = "잘못된 요청", content = [ Content( @@ -114,38 +67,30 @@ class MatchApi( examples = [ ExampleObject( - name = "MT01", + name = "Meeting Team Not Found", + description = "신청 내역 없음", value = - "{\"message\": \"Match is not Found.\", \"status\": 400, \"code\": \"MT01\"}" - )] - )] - ), - ApiResponse( - responseCode = "403", - description = "해당 매치에 대한 접근 권한 없음", - content = - [ - Content( - schema = Schema(implementation = ErrorResponse::class), - examples = - [ + "{\"message\": \"Meeting Team is not Found.\", \"status\": 400, \"code\": \"M06\"}" + ), ExampleObject( - name = "MT04", + name = "Payment Not Found", + description = "결제 정보 없음", value = - "{\"message\": \"Unauthorized match access.\", \"status\": 403, \"code\": \"MT04\"}" - )] + "{\"message\": \"Payment is not Found.\", \"status\": 400, \"code\": \"P01\"}" + ), + ] )] ), ] ) - @GetMapping("/{matchId}/partner") - fun getMatchedPartnerInformation( - @PathVariable matchId: Long, + @GetMapping("/{teamType}/info") + fun getMatchInformation( @AuthenticationPrincipal userDetails: UserDetails, - ): ResponseEntity { - return ResponseEntity.status(HttpStatus.OK) - .body( - matchingService.getMatchedPartnerInformation(userDetails.username.toLong(), matchId) - ) + @PathVariable teamType: TeamType, + @RequestParam season: Int, + ): ResponseEntity { + return ResponseEntity.ok( + matchingService.getMatchInfo(userDetails.username.toLong(), teamType, season) + ) } } diff --git a/src/main/kotlin/uoslife/servermeeting/match/dao/MatchedDao.kt b/src/main/kotlin/uoslife/servermeeting/match/dao/MatchedDao.kt index 1a6eb084..a5aed6e7 100644 --- a/src/main/kotlin/uoslife/servermeeting/match/dao/MatchedDao.kt +++ b/src/main/kotlin/uoslife/servermeeting/match/dao/MatchedDao.kt @@ -1,15 +1,16 @@ package uoslife.servermeeting.match.dao -import com.querydsl.core.types.Projections import com.querydsl.jpa.impl.JPAQueryFactory import jakarta.transaction.Transactional import org.springframework.stereotype.Repository -import uoslife.servermeeting.match.dto.MatchResultDto +import uoslife.servermeeting.match.dto.response.MeetingParticipationResponse import uoslife.servermeeting.match.entity.Match import uoslife.servermeeting.match.entity.QMatch.match import uoslife.servermeeting.meetingteam.entity.MeetingTeam import uoslife.servermeeting.meetingteam.entity.QMeetingTeam.meetingTeam import uoslife.servermeeting.meetingteam.entity.QUserTeam.userTeam +import uoslife.servermeeting.meetingteam.entity.enums.TeamType +import uoslife.servermeeting.payment.entity.enums.PaymentStatus @Repository @Transactional @@ -32,34 +33,31 @@ class MatchedDao(private val queryFactory: JPAQueryFactory) { .fetchOne() } - fun findByTeam(team: MeetingTeam): Match? { - return queryFactory - .selectFrom(match) - .leftJoin(match.maleTeam, meetingTeam) - .leftJoin(match.femaleTeam, meetingTeam) - .where(match.maleTeam.eq(team).or(match.femaleTeam.eq(team))) - .fetchOne() - } + fun findUserParticipation(userId: Long, season: Int): MeetingParticipationResponse { + val teams = + queryFactory + .selectFrom(userTeam) + .join(userTeam.team) + .fetchJoin() // team을 미리 로딩 + .leftJoin(userTeam.team.payments) + .fetchJoin() // payments도 미리 로딩 + .where(userTeam.user.id.eq(userId), userTeam.team.season.eq(season)) + .fetch() - fun findMatchResultByUserIdAndTeamId(userId: Long, teamId: Long): MatchResultDto? { - return queryFactory - .select(Projections.constructor(MatchResultDto::class.java, meetingTeam.type, match.id)) - .from(userTeam) - .join(userTeam.team, meetingTeam) - .leftJoin(match) - .on(meetingTeam.eq(match.maleTeam).or(meetingTeam.eq(match.femaleTeam))) - .where(userTeam.user.id.eq(userId), userTeam.team.id.eq(teamId)) - .fetchOne() - } + val participations = + teams + .groupBy { it.team.type } + .mapValues { (_, userTeams) -> + userTeams.all { userTeam -> + val payments = userTeam.team.payments + payments?.isNotEmpty() == true && + payments.all { it.status == PaymentStatus.SUCCESS } + } + } - fun findById(matchId: Long): Match? { - return queryFactory - .selectFrom(match) - .join(match.maleTeam) - .fetchJoin() - .join(match.femaleTeam) - .fetchJoin() - .where(match.id.eq(matchId)) - .fetchOne() + return MeetingParticipationResponse( + single = participations[TeamType.SINGLE] ?: false, + triple = participations[TeamType.TRIPLE] ?: false, + ) } } diff --git a/src/main/kotlin/uoslife/servermeeting/match/dto/response/MatchInfoResponse.kt b/src/main/kotlin/uoslife/servermeeting/match/dto/response/MatchInfoResponse.kt new file mode 100644 index 00000000..b22059b7 --- /dev/null +++ b/src/main/kotlin/uoslife/servermeeting/match/dto/response/MatchInfoResponse.kt @@ -0,0 +1,34 @@ +package uoslife.servermeeting.match.dto.response + +import io.swagger.v3.oas.annotations.media.Schema +import uoslife.servermeeting.meetingteam.dto.response.UserCardProfile +import uoslife.servermeeting.user.entity.enums.GenderType + +data class MatchInfoResponse( + @Schema(description = "매칭 성공 여부") val isMatched: Boolean, + @Schema(description = "상대 팀 정보", nullable = true) val partnerTeam: PartnerTeamInfo? +) { + data class PartnerTeamInfo( + @Schema(description = "팀 이름 (3대3)") val teamName: String?, + @Schema(description = "데이트 코스 (1대1)") val course: String?, + @Schema(description = "성별") val gender: GenderType, + @Schema(description = "팀원 프로필 목록") val userProfiles: List + ) + + companion object { + fun toMatchInfoResponse( + response: MatchedMeetingTeamInformationGetResponse + ): MatchInfoResponse { + return MatchInfoResponse( + isMatched = true, + partnerTeam = + PartnerTeamInfo( + teamName = response.teamName, + course = response.course, + gender = response.gender, + userProfiles = response.userProfiles!! + ) + ) + } + } +} diff --git a/src/main/kotlin/uoslife/servermeeting/match/dto/response/MatchInformationResponse.kt b/src/main/kotlin/uoslife/servermeeting/match/dto/response/MatchInformationResponse.kt deleted file mode 100644 index 18b554ea..00000000 --- a/src/main/kotlin/uoslife/servermeeting/match/dto/response/MatchInformationResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package uoslife.servermeeting.match.dto.response - -import io.swagger.v3.oas.annotations.media.Schema - -class MatchInformationResponse( - @Schema(description = "매칭된 상대 정보") - val opponnentUserInformation: MatchedMeetingTeamInformationGetResponse -) diff --git a/src/main/kotlin/uoslife/servermeeting/match/dto/response/MatchResultResponse.kt b/src/main/kotlin/uoslife/servermeeting/match/dto/response/MatchResultResponse.kt deleted file mode 100644 index bc951fa5..00000000 --- a/src/main/kotlin/uoslife/servermeeting/match/dto/response/MatchResultResponse.kt +++ /dev/null @@ -1,10 +0,0 @@ -package uoslife.servermeeting.match.dto.response - -import io.swagger.v3.oas.annotations.media.Schema -import uoslife.servermeeting.meetingteam.entity.enums.TeamType - -data class MatchResultResponse( - @Schema(description = "신청한 미팅의 종류") val matchType: TeamType, - @Schema(description = "매칭 성공 여부") val isMatched: Boolean, - @Schema(description = "매치 ID", nullable = true) val matchId: Long?, -) diff --git a/src/main/kotlin/uoslife/servermeeting/match/dto/response/MatchedMeetingTeamInformationGetResponse.kt b/src/main/kotlin/uoslife/servermeeting/match/dto/response/MatchedMeetingTeamInformationGetResponse.kt index 07e172dd..b537d8a3 100644 --- a/src/main/kotlin/uoslife/servermeeting/match/dto/response/MatchedMeetingTeamInformationGetResponse.kt +++ b/src/main/kotlin/uoslife/servermeeting/match/dto/response/MatchedMeetingTeamInformationGetResponse.kt @@ -10,6 +10,6 @@ data class MatchedMeetingTeamInformationGetResponse( @field:NotNull @Schema(description = "팀 타입", example = "SINGLE") val teamType: TeamType, @Schema(description = "팀 이름", example = "팀 이름(1:1인 경우 null)") val teamName: String?, @field:NotNull @Schema(description = "성별", example = "MALE") val gender: GenderType, - @Schema(description = "팀에 속한 유저 정보") val leaderProfile: List?, - @Schema(description = "상대에게 전하는 데이트 코스") val course: String? + @Schema(description = "상대에게 전하는 데이트 코스") val course: String?, + @Schema(description = "팀에 속한 유저 정보") val userProfiles: List?, ) diff --git a/src/main/kotlin/uoslife/servermeeting/match/dto/response/MeetingParticipationResponse.kt b/src/main/kotlin/uoslife/servermeeting/match/dto/response/MeetingParticipationResponse.kt index 8b746482..4e77f65a 100644 --- a/src/main/kotlin/uoslife/servermeeting/match/dto/response/MeetingParticipationResponse.kt +++ b/src/main/kotlin/uoslife/servermeeting/match/dto/response/MeetingParticipationResponse.kt @@ -1,8 +1,3 @@ package uoslife.servermeeting.match.dto.response -data class MeetingParticipationResponse( - val single: ParticipationStatus, - val triple: ParticipationStatus -) - -data class ParticipationStatus(val isParticipated: Boolean, val meetingTeamId: Long?) +data class MeetingParticipationResponse(val single: Boolean, val triple: Boolean) diff --git a/src/main/kotlin/uoslife/servermeeting/match/service/MatchingService.kt b/src/main/kotlin/uoslife/servermeeting/match/service/MatchingService.kt index 066db20d..96f565c1 100644 --- a/src/main/kotlin/uoslife/servermeeting/match/service/MatchingService.kt +++ b/src/main/kotlin/uoslife/servermeeting/match/service/MatchingService.kt @@ -7,21 +7,18 @@ import uoslife.servermeeting.match.dao.MatchedDao import uoslife.servermeeting.match.dto.response.* import uoslife.servermeeting.match.entity.Match import uoslife.servermeeting.match.exception.MatchNotFoundException -import uoslife.servermeeting.match.exception.UnauthorizedMatchAccessException -import uoslife.servermeeting.match.exception.UnauthorizedTeamAccessException import uoslife.servermeeting.meetingteam.dao.UserTeamDao import uoslife.servermeeting.meetingteam.dto.request.CompletionStatus -import uoslife.servermeeting.meetingteam.dto.response.MeetingTeamInformationGetResponse import uoslife.servermeeting.meetingteam.entity.MeetingTeam -import uoslife.servermeeting.meetingteam.entity.UserTeam import uoslife.servermeeting.meetingteam.entity.enums.TeamType import uoslife.servermeeting.meetingteam.entity.enums.TeamType.SINGLE import uoslife.servermeeting.meetingteam.entity.enums.TeamType.TRIPLE import uoslife.servermeeting.meetingteam.exception.GenderNotUpdatedException import uoslife.servermeeting.meetingteam.exception.MeetingTeamNotFoundException -import uoslife.servermeeting.meetingteam.exception.OnlyTeamLeaderCanGetMatchException import uoslife.servermeeting.meetingteam.service.impl.SingleMeetingService import uoslife.servermeeting.meetingteam.service.impl.TripleMeetingService +import uoslife.servermeeting.payment.entity.enums.PaymentStatus +import uoslife.servermeeting.payment.exception.PaymentNotFoundException import uoslife.servermeeting.user.entity.User import uoslife.servermeeting.user.entity.enums.GenderType import uoslife.servermeeting.user.exception.UserNotFoundException @@ -34,57 +31,41 @@ class MatchingService( private val singleMeetingService: SingleMeetingService, private val tripleMeetingService: TripleMeetingService, ) { - @Cacheable(value = ["meeting-participation"], key = "#userId", unless = "#result == null") - fun getUserMeetingParticipation(userId: Long): MeetingParticipationResponse { - val userTeams = userTeamDao.findAllByUserIdWithPaymentStatus(userId) - - return MeetingParticipationResponse( - single = getParticipationStatus(userTeams.find { it.team.type == SINGLE }), - triple = getParticipationStatus(userTeams.find { it.team.type == TRIPLE }) - ) - } - - @Cacheable(value = ["match-result"], key = "#meetingTeamId", unless = "#result == null") - fun getMatchResult(userId: Long, meetingTeamId: Long): MatchResultResponse { - val result = - matchedDao.findMatchResultByUserIdAndTeamId(userId, meetingTeamId) - ?: throw UnauthorizedTeamAccessException() - - return MatchResultResponse( - matchType = result.teamType, - isMatched = result.matchId != null, - matchId = result.matchId - ) - } - @Cacheable( - value = ["partner-info"], - key = "#matchId + ':' + #userId", - unless = "#result == null" + value = ["meeting-participation"], + key = "#season + ':' + #userId", ) - fun getMatchedPartnerInformation( - userId: Long, - matchId: Long - ): MeetingTeamInformationGetResponse { - val response = getPartnerInformation(userId, matchId) - return convertPersistentBagToArrayList(response) + fun getUserMeetingParticipation(userId: Long, season: Int): MeetingParticipationResponse { + return matchedDao.findUserParticipation(userId, season) } - @Transactional - fun getMatchedMeetingTeamByType(userId: Long, teamType: TeamType): MatchInformationResponse { + @Cacheable( + value = ["match-info"], + key = "#season + ':' + #teamType + ':' + #userId", + ) + fun getMatchInfo(userId: Long, teamType: TeamType, season: Int): MatchInfoResponse { val userTeam = - userTeamDao.findUserWithMeetingTeam(userId, teamType) ?: throw UserNotFoundException() - val meetingTeam = userTeam.team ?: throw MeetingTeamNotFoundException() - - if (!userTeam.isLeader) throw OnlyTeamLeaderCanGetMatchException() - - val match = getMatchByGender(userTeam.user, meetingTeam) - val opponentTeam = getOpponentTeamByGender(userTeam.user, match) - val opponentUser = getOpponentLeaderUser(opponentTeam) + userTeamDao.findUserWithTeamTypeAndSeason(userId, teamType, season) + ?: throw MeetingTeamNotFoundException() + val meetingTeam = userTeam.team + + val hasInvalidPayment = + meetingTeam.payments?.any { payment -> payment.status != PaymentStatus.SUCCESS } + ?: false + if (hasInvalidPayment) { + throw PaymentNotFoundException() + } - return MatchInformationResponse( - getOpponentUserInformationByTeamType(meetingTeam, opponentUser) - ) + try { + val match = getMatchByGender(userTeam.user, meetingTeam) + val opponentTeam = getOpponentTeamByGender(userTeam.user, match) + val opponentUser = getOpponentLeaderUser(opponentTeam) + return MatchInfoResponse.toMatchInfoResponse( + getOpponentUserInformationByTeamType(meetingTeam, opponentUser) + ) + } catch (e: MatchNotFoundException) { + return MatchInfoResponse(false, null) + } } private fun getOpponentLeaderUser(opponentTeam: MeetingTeam): User { @@ -136,63 +117,4 @@ class MatchingService( } return meetingTeamInformationGetResponse.toMatchedMeetingTeamInformationGetResponse() } - - private fun getParticipationStatus(userTeam: UserTeam?): ParticipationStatus { - if (userTeam == null) { - return ParticipationStatus(false, null) - } - - return ParticipationStatus(isParticipated = true, meetingTeamId = userTeam.team.id) - } - - private fun getPartnerTeam(userGender: GenderType, match: Match): MeetingTeam { - return when (userGender) { - GenderType.MALE -> match.femaleTeam - GenderType.FEMALE -> match.maleTeam - } - } - - private fun getPartnerInformation( - userId: Long, - matchId: Long - ): MeetingTeamInformationGetResponse { - val match = matchedDao.findById(matchId) ?: throw MatchNotFoundException() - val userTeam = - userTeamDao.findUserWithMeetingTeamByMatchId(userId, matchId) - ?: throw UnauthorizedMatchAccessException() - val partnerTeam = getPartnerTeam(userTeam.team.gender, match) - - return when (partnerTeam.type) { - SINGLE -> getPartnerSingleTeamInfo(partnerTeam) - TRIPLE -> getPartnerTripleTeamInfo(partnerTeam) - } - } - - private fun getPartnerSingleTeamInfo(partnerTeam: MeetingTeam) = - singleMeetingService.getMeetingTeamInformation( - partnerTeam.userTeams.first().user.id!!, - CompletionStatus.COMPLETED - ) - - private fun getPartnerTripleTeamInfo(partnerTeam: MeetingTeam) = - tripleMeetingService.getMeetingTeamInformation( - partnerTeam.userTeams.first { it.isLeader }.user.id!!, - CompletionStatus.COMPLETED - ) - - private fun convertPersistentBagToArrayList(response: MeetingTeamInformationGetResponse) = - response.copy( - meetingTeamUserProfiles = - response.meetingTeamUserProfiles?.map { profile -> - profile.copy(interest = profile.interest?.let { ArrayList(it) }) - }, - preference = - response.preference?.let { pref -> - pref.copy( - smoking = pref.smoking?.let { ArrayList(it) }, - appearanceType = pref.appearanceType?.let { ArrayList(it) }, - eyelidType = pref.eyelidType?.let { ArrayList(it) } - ) - } - ) } diff --git a/src/main/kotlin/uoslife/servermeeting/meetingteam/dao/UserTeamDao.kt b/src/main/kotlin/uoslife/servermeeting/meetingteam/dao/UserTeamDao.kt index a4112982..4333e644 100644 --- a/src/main/kotlin/uoslife/servermeeting/meetingteam/dao/UserTeamDao.kt +++ b/src/main/kotlin/uoslife/servermeeting/meetingteam/dao/UserTeamDao.kt @@ -1,11 +1,11 @@ package uoslife.servermeeting.meetingteam.dao -import com.querydsl.jpa.JPAExpressions +import com.querydsl.core.types.Projections import com.querydsl.jpa.impl.JPAQueryFactory import jakarta.persistence.EntityManager import jakarta.transaction.Transactional import org.springframework.stereotype.Repository -import uoslife.servermeeting.match.entity.QMatch.match +import uoslife.servermeeting.meetingteam.dto.ParticipantInfo import uoslife.servermeeting.meetingteam.entity.MeetingTeam import uoslife.servermeeting.meetingteam.entity.QMeetingTeam.meetingTeam import uoslife.servermeeting.meetingteam.entity.QUserTeam.userTeam @@ -98,32 +98,39 @@ class UserTeamDao( .fetch() } - fun findAllByUserIdWithPaymentStatus(userId: Long): List { - val hasNonSuccessPayment = - JPAExpressions.selectOne() - .from(payment) - .where( - payment.meetingTeam.eq(meetingTeam), - payment.status.ne(PaymentStatus.SUCCESS) - ) - .exists() - + fun findUserWithTeamTypeAndSeason(userId: Long, teamType: TeamType, season: Int): UserTeam? { return queryFactory .selectFrom(userTeam) .join(userTeam.team, meetingTeam) + .join(userTeam.user, user) + .where( + user.id + .eq(userId) + .and(meetingTeam.type.eq(teamType)) + .and(meetingTeam.season.eq(season)) + ) .fetchJoin() - .where(userTeam.user.id.eq(userId), hasNonSuccessPayment.not()) - .fetch() + .fetchOne() } - fun findUserWithMeetingTeamByMatchId(userId: Long, matchId: Long): UserTeam? { + fun findAllParticipantsBySeasonAndType(season: Int): List { return queryFactory - .selectFrom(userTeam) + .select( + Projections.constructor( + ParticipantInfo::class.java, + userTeam.user.id, + userTeam.team.type, + ) + ) + .from(userTeam) .join(userTeam.team, meetingTeam) - .fetchJoin() - .leftJoin(match) - .on(meetingTeam.eq(match.maleTeam).or(meetingTeam.eq(match.femaleTeam))) - .where(userTeam.user.id.eq(userId), match.id.eq(matchId)) - .fetchOne() + .leftJoin(payment) + .on(payment.meetingTeam.eq(meetingTeam)) + .where( + meetingTeam.season.eq(season), + ) + .groupBy(userTeam.user.id, userTeam.team.type) + .having(payment.status.eq(PaymentStatus.SUCCESS).count().eq(payment.count())) + .fetch() } } diff --git a/src/main/kotlin/uoslife/servermeeting/meetingteam/dto/ParticipantInfo.kt b/src/main/kotlin/uoslife/servermeeting/meetingteam/dto/ParticipantInfo.kt new file mode 100644 index 00000000..25e1383c --- /dev/null +++ b/src/main/kotlin/uoslife/servermeeting/meetingteam/dto/ParticipantInfo.kt @@ -0,0 +1,5 @@ +package uoslife.servermeeting.meetingteam.dto + +import uoslife.servermeeting.meetingteam.entity.enums.TeamType + +data class ParticipantInfo(val userId: Long, val teamType: TeamType) diff --git a/src/main/kotlin/uoslife/servermeeting/meetingteam/dto/response/MeetingTeamInformationGetResponse.kt b/src/main/kotlin/uoslife/servermeeting/meetingteam/dto/response/MeetingTeamInformationGetResponse.kt index 8657602f..e3519b20 100644 --- a/src/main/kotlin/uoslife/servermeeting/meetingteam/dto/response/MeetingTeamInformationGetResponse.kt +++ b/src/main/kotlin/uoslife/servermeeting/meetingteam/dto/response/MeetingTeamInformationGetResponse.kt @@ -23,7 +23,7 @@ data class MeetingTeamInformationGetResponse( teamType = teamType, teamName = teamName, gender = gender, - leaderProfile = meetingTeamUserProfiles, + userProfiles = meetingTeamUserProfiles, course = course ) } @@ -77,7 +77,6 @@ data class UserCardProfile( @Schema(description = "외모", example = "NORMAL") val appearanceType: AppearanceType?, @Schema(description = "쌍커풀", example = "INNER") val eyelidType: EyelidType?, @Schema(description = "흡연 여부", example = "TRUE") val smoking: SmokingType?, - @Schema(description = "관심사", example = "[{ \"name\": \"여행\" }, { \"name\": \"맛집 탐방하기\"}]") - val interest: List?, + @Schema(description = "관심사", example = "[\"여행\", \"맛집 탐방\"]") val interest: List?, @field:NotNull @Schema(description = "카카오톡 ID", example = "kakaoId") val kakaoTalkId: String, ) diff --git a/src/main/kotlin/uoslife/servermeeting/meetingteam/service/util/MeetingDtoConverter.kt b/src/main/kotlin/uoslife/servermeeting/meetingteam/service/util/MeetingDtoConverter.kt index e2982607..46e77bf5 100644 --- a/src/main/kotlin/uoslife/servermeeting/meetingteam/service/util/MeetingDtoConverter.kt +++ b/src/main/kotlin/uoslife/servermeeting/meetingteam/service/util/MeetingDtoConverter.kt @@ -49,7 +49,7 @@ class MeetingDtoConverter { appearanceType = userInfo.appearanceType, eyelidType = userInfo.eyelidType, smoking = userInfo.smoking, - interest = userInfo.interest, + interest = userInfo.interest?.toList(), kakaoTalkId = userTeam.user.kakaoTalkId!! ) } catch (e: NullPointerException) { diff --git a/src/main/kotlin/uoslife/servermeeting/verification/api/VerificationApi.kt b/src/main/kotlin/uoslife/servermeeting/verification/api/VerificationApi.kt index 45cb9e15..91344c73 100644 --- a/src/main/kotlin/uoslife/servermeeting/verification/api/VerificationApi.kt +++ b/src/main/kotlin/uoslife/servermeeting/verification/api/VerificationApi.kt @@ -21,7 +21,7 @@ import uoslife.servermeeting.verification.dto.request.VerifyEmailRequest import uoslife.servermeeting.verification.dto.response.SendVerificationEmailResponse import uoslife.servermeeting.verification.service.EmailVerificationService -@Tag(name = "Verification", description = "Verification API") +@Tag(name = "Verification", description = "이메일 인증 API") @RestController @RequestMapping("/api/verification") class VerificationApi(