diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..6562b6c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ +# ๐Ÿ“„ Work Description +- ์„ค๋ช… + + +# โš™๏ธ ISSUE +- closed #์ด์Šˆ๋ฒˆํ˜ธ + + +# ๐Ÿ“ท Screenshot + - ๋™์˜์ƒ, ์‚ฌ์ง„, ๋กœ๊ทธ ๋“ฑ๋“ฑ + - ex) ํ์•Œ ์„ฑ๊ณต ์ด๋ฏธ์ง€, ์Šค์›จ๊ฑฐ, ํฌ์ŠคํŠธ๋งจ ๋“ฑ + + +# ๐Ÿ’ฌ To Reviewers +๋ฆฌ๋ทฐ์–ด๋“ค์—๊ฒŒ ํ•˜๊ณ  ์‹ถ์€ ๋ง + + +# ๐Ÿ”— Reference +๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ฉด์„œ ๋„์›€์ด ๋˜์—ˆ๊ฑฐ๋‚˜, ์ฐธ๊ณ ํ–ˆ๋˜ ์‚ฌ์ดํŠธ(์ฝ”๋“œ๋งํฌ) diff --git a/build.gradle b/build.gradle index 7dc7416..4e1b530 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-websocket' compileOnly 'org.projectlombok:lombok' implementation 'org.springframework.boot:spring-boot-starter-validation' developmentOnly 'org.springframework.boot:spring-boot-devtools' @@ -60,6 +61,11 @@ dependencies { implementation 'com.google.cloud:google-cloud-storage:2.20.1' implementation 'org.springframework.cloud:spring-cloud-gcp-starter-storage:1.2.8.RELEASE' implementation 'net.coobird:thumbnailator:0.4.14' + + // STOMP + implementation 'org.webjars:webjars-locator-core' + implementation 'org.webjars:sockjs-client:1.5.1' + implementation 'org.webjars:stomp-websocket:2.3.4' } tasks.named('bootBuildImage') { diff --git a/src/main/java/com/favoriteplace/app/controller/PilgrimageSocketController.java b/src/main/java/com/favoriteplace/app/controller/PilgrimageSocketController.java new file mode 100644 index 0000000..b482e66 --- /dev/null +++ b/src/main/java/com/favoriteplace/app/controller/PilgrimageSocketController.java @@ -0,0 +1,74 @@ +package com.favoriteplace.app.controller; + +import com.favoriteplace.app.domain.travel.Pilgrimage; +import com.favoriteplace.app.dto.travel.PilgrimageDto; +import com.favoriteplace.app.repository.PilgrimageRepository; +import com.favoriteplace.app.service.PilgrimageCommandService; +import com.favoriteplace.global.exception.ErrorCode; +import com.favoriteplace.global.exception.RestApiException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RestController; + +@Controller +@Slf4j +@RequiredArgsConstructor +public class PilgrimageSocketController { + private final PilgrimageCommandService pilgrimageService; + private final PilgrimageRepository pilgrimageRepository; + + /** + * ํ…Œ์ŠคํŠธ ์ปจํŠธ๋กค๋Ÿฌ + * ์š”์ฒญ /app/test + * ์‘๋‹ต /pub/pilgrimage + * @param testMsg + * @return + */ + @MessageMapping("/test") + @SendTo("/pub/pilgrimage") + public String pilgrimageCertify(String testMsg){ + log.info("socket message: " + testMsg); + return testMsg; + } + + /** + * ์œ„๋„/๊ฒฝ๋„ ์ „๋‹ฌ ์‹œ ์ƒํƒœ ๋ณ€๊ฒฝ ์•Œ๋ฆฌ๋Š” ์ปจํŠธ๋กค๋Ÿฌ + * ์š”์ฒญ ์ปจํŠธ๋กค๋Ÿฌ /app/location/{pilgrimageId} + * ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ /pub/statusUpdate/{pilgrimageId} + * @param pilgrimageId ์„ฑ์ง€์ˆœ๋ก€ ID + * @param userLocation ์œ„๋„/๊ฒฝ๋„ + * @return + */ + @MessageMapping("/location/{pilgrimageId}") + @SendTo("/pub/statusUpdate/{pilgrimageId}") + public Boolean checkUserLocation(@DestinationVariable Long pilgrimageId, PilgrimageDto.PilgrimageCertifyRequestDto userLocation) { + Pilgrimage pilgrimage = pilgrimageRepository.findById(pilgrimageId) + .orElseThrow(()->new RestApiException(ErrorCode.PILGRIMAGE_NOT_FOUND)); + + boolean isUserAtPilgrimage = pilgrimageService.isUserAtPilgrimage(pilgrimage, userLocation.getLatitude(), userLocation.getLongitude()); + + if (!isUserAtPilgrimage) { + // ์—ฌ๊ธฐ์— ์ด๋ฒคํŠธ ๋™์ž‘ ์ถ”๊ฐ€ + return false; + } + return true; + } + + /** + * ์ตœ์ดˆ ์ ‘๊ทผ ์‹œ ๋ฒ„ํŠผ ์ƒํƒœ ์ „๋‹ฌํ•˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ + * ์š”์ฒญ ์ปจํŠธ๋กค๋Ÿฌ /app/connect/{pilgrimageId} + * ์‘๋‹ต ์ปจํŠธ๋กค๋Ÿฌ /pub/statusUpdate/{pilgrimageId} + * @param pilgrimageId ์„ฑ์ง€์ˆœ๋ก€ ID + * @return + */ + @MessageMapping("/connect/{pilgrimageId}") + @SendTo("/pub/statusUpdate/{pilgrimageId}") + public Boolean sendInitialStatus(@DestinationVariable Long pilgrimageId) { + log.info("User connected to pilgrimage: " + pilgrimageId); + return true; // ์ดˆ๊ธฐ ๋ฒ„ํŠผ ์ƒํƒœ + } +} diff --git a/src/main/java/com/favoriteplace/app/dto/travel/PilgrimageSocketDto.java b/src/main/java/com/favoriteplace/app/dto/travel/PilgrimageSocketDto.java new file mode 100644 index 0000000..dea6820 --- /dev/null +++ b/src/main/java/com/favoriteplace/app/dto/travel/PilgrimageSocketDto.java @@ -0,0 +1,16 @@ +package com.favoriteplace.app.dto.travel; + +import lombok.*; + +public class PilgrimageSocketDto { + @Data + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class ButtonState { + private Boolean certifyButtonEnabled; + private Boolean guestbookButtonEnabled; + private Boolean multiGuestbookButtonEnabled; + } +} diff --git a/src/main/java/com/favoriteplace/app/service/PilgrimageCommandService.java b/src/main/java/com/favoriteplace/app/service/PilgrimageCommandService.java index be006a7..3388491 100644 --- a/src/main/java/com/favoriteplace/app/service/PilgrimageCommandService.java +++ b/src/main/java/com/favoriteplace/app/service/PilgrimageCommandService.java @@ -10,9 +10,11 @@ import com.favoriteplace.app.domain.travel.*; import com.favoriteplace.app.dto.CommonResponseDto; import com.favoriteplace.app.dto.travel.PilgrimageDto; +import com.favoriteplace.app.dto.travel.PilgrimageSocketDto; import com.favoriteplace.app.repository.*; import com.favoriteplace.global.exception.ErrorCode; import com.favoriteplace.global.exception.RestApiException; +import com.favoriteplace.global.websocket.RedisService; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -31,6 +33,7 @@ @Transactional @RequiredArgsConstructor public class PilgrimageCommandService { + private final MemberRepository memberRepository; private final RallyRepository rallyRepository; private final PilgrimageRepository pilgrimageRepository; private final LikedRallyRepository likedRallyRepository; @@ -38,7 +41,7 @@ public class PilgrimageCommandService { private final PointHistoryRepository pointHistoryRepository; private final CompleteRallyRepository completeRallyRepository; private final AcquiredItemRepository acquiredItemRepository; - private final EntityManager em; + private final RedisService redisService; /*** * ๋ž ๋ฆฌ ์ฐœํ•˜๊ธฐ @@ -127,8 +130,8 @@ private boolean checkCompleteRally(Member member, Pilgrimage pilgrimage, Long co } private boolean checkCoordinate(PilgrimageDto.PilgrimageCertifyRequestDto form, Pilgrimage pilgrimage) { - return pilgrimage.getLatitude() + 0.00135 < form.getLatitude() || pilgrimage.getLatitude() - 0.00135 > form.getLatitude() - || pilgrimage.getLongitude() + 0.00135 < form.getLongitude() || pilgrimage.getLongitude() - 0.00135 > form.getLongitude(); + return pilgrimage.getLatitude() + 0.00135 >= form.getLatitude() && pilgrimage.getLatitude() - 0.00135 <= form.getLatitude() + && pilgrimage.getLongitude() + 0.00135 >= form.getLongitude() && pilgrimage.getLongitude() - 0.00135 <= form.getLongitude(); } private void successVisitedAndPointProcess(Member member, Pilgrimage pilgrimage) { @@ -139,4 +142,74 @@ private void successVisitedAndPointProcess(Member member, Pilgrimage pilgrimage) member.updatePoint(15L); log.info("clear"); } + + public boolean isUserAtPilgrimage(Pilgrimage pilgrimage, Double latitude, Double longitude) { + return (pilgrimage.getLatitude() + 0.00135 >= latitude && pilgrimage.getLatitude() - 0.00135 <= latitude) && + (pilgrimage.getLongitude() + 0.00135 >= longitude && pilgrimage.getLongitude() - 0.00135 <= longitude); + } + + /** + * ์›น์†Œ์ผ“ ๋ฒ„ํŠผ ์ƒํƒœ ์ด๋ฒคํŠธ + * @param memberId + * @param pilgrimageId + * @param latitude + * @param longitude + * @return + */ + public PilgrimageSocketDto.ButtonState determineButtonState(Long memberId, + Long pilgrimageId, + double latitude, double longitude) { + Pilgrimage pilgrimage = pilgrimageRepository.findById(pilgrimageId) + .orElseThrow(() -> new RestApiException(ErrorCode.PILGRIMAGE_NOT_FOUND)); + + Member member = memberRepository.findById(memberId) + .orElseThrow(()->new RestApiException(ErrorCode.USER_NOT_FOUND)); + + // ์‚ฌ์šฉ์ž๊ฐ€ ์ธ์ฆ ์žฅ์†Œ์— ์žˆ๋Š”์ง€ ํ™•์ธ + boolean nearPilgrimage = isUserAtPilgrimage(pilgrimage, latitude, longitude); + // redis์— ์‚ฌ์šฉ์ž์˜ ์ธ์ฆ ๊ธฐ๋ก์ด ๋‚จ์•„์žˆ๋Š”์ง€ ํ™•์ธ (24์‹œ๊ฐ„ ์ดํ›„ ๋งŒ๋ฃŒ๋จ) + boolean isCertificationExpired = redisService.isCertificationExpired(member, pilgrimage); + // ์‚ฌ์šฉ์ž๊ฐ€ ์ง€๋‚œ 24์‹œ๊ฐ„ ๋‚ด์— ์ธ์ฆ ๋ฒ„ํŠผ ๋ˆŒ๋ €๋Š”์ง€ ํ™•์ธ + boolean certifiedInLast24Hours = checkIfCertifiedInLast24Hours(member, pilgrimage); + // ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฒˆ ์ธ์ฆํ•˜๊ธฐ์— ์ด๋ฏธ ๋ฐฉ๋ช…๋ก์„ ์ž‘์„ฑํ–ˆ๋Š”์ง€ ํ™•์ธ + boolean hasWrittenGuestbook = checkIfGuestbookWritten(member, pilgrimage); + // ์‚ฌ์šฉ์ž๊ฐ€ 24์‹œ๊ฐ„ ์ „์— ์ž‘์„ฑํ•œ ๋ฐฉ๋ช…๋ก์ด 1๊ฐœ ์ด์ƒ์ธ์ง€ ํ™•์ธ + boolean hasMultiWrittenGuestbook = checkIfMultiGuestbookWritten(member, pilgrimage); + + PilgrimageSocketDto.ButtonState newState = new PilgrimageSocketDto.ButtonState(); + if (nearPilgrimage && !certifiedInLast24Hours) { + newState.setCertifyButtonEnabled(true); // ์ธ์ฆํ•˜๊ธฐ ๋ฒ„ํŠผ ํ™œ์„ฑํ™” + newState.setGuestbookButtonEnabled(false); // ๋ฐฉ๋ช…๋ก ์“ฐ๊ธฐ ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™” + } + else if (certifiedInLast24Hours && !hasWrittenGuestbook) { + newState.setCertifyButtonEnabled(false); // ์ธ์ฆํ•˜๊ธฐ ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™” + newState.setGuestbookButtonEnabled(true); // ๋ฐฉ๋ช…๋ก ์“ฐ๊ธฐ ๋ฒ„ํŠผ ํ™œ์„ฑํ™” + } + else { + newState.setCertifyButtonEnabled(false); // ์ธ์ฆํ•˜๊ธฐ ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™” + newState.setGuestbookButtonEnabled(false); // ๋ฐฉ๋ช…๋ก ์“ฐ๊ธฐ ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™” + } + return newState; + } + + private boolean checkIfCertifiedInLast24Hours(Member member, Pilgrimage pilgrimage) { + // ์ธ์ฆ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅธ ์‹œ๊ฐ„ ํ™•์ธ + List visitedPilgrimages = visitedPilgrimageRepository + .findByPilgrimageAndMemberOrderByCreatedAtDesc(pilgrimage, member); + + ZoneId serverZoneId = ZoneId.of("Asia/Seoul"); + ZonedDateTime nowInServerTimeZone = ZonedDateTime.now(serverZoneId); + + return false; + } + + private boolean checkIfGuestbookWritten(Member member, Pilgrimage pilgrimage) { + // ๋ฐฉ๋ช…๋ก ์ž‘์„ฑ ์—ฌ๋ถ€ ํ™•์ธ + return false; + } + + private boolean checkIfMultiGuestbookWritten(Member member, Pilgrimage pilgrimage) { + // ๋ฐฉ๋ช…๋ก ๋‹คํšŒ ์ž‘์„ฑ ์—ฌ๋ถ€ ํ™•์ธ + return false; + } } diff --git a/src/main/java/com/favoriteplace/app/service/WebSocketService.java b/src/main/java/com/favoriteplace/app/service/WebSocketService.java new file mode 100644 index 0000000..bd07008 --- /dev/null +++ b/src/main/java/com/favoriteplace/app/service/WebSocketService.java @@ -0,0 +1,37 @@ +package com.favoriteplace.app.service; + +import com.favoriteplace.app.domain.travel.Pilgrimage; +import com.favoriteplace.app.dto.travel.PilgrimageSocketDto; +import com.favoriteplace.app.repository.PilgrimageRepository; +import com.favoriteplace.global.exception.ErrorCode; +import com.favoriteplace.global.exception.RestApiException; +import com.google.api.gax.rpc.ApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Service +@RequiredArgsConstructor +public class WebSocketService { + private final SimpMessagingTemplate messagingTemplate; + private final PilgrimageCommandService pilgrimageService; + private final PilgrimageRepository pilgrimageRepository; + private Map lastButtonStateCache = new ConcurrentHashMap<>(); + + public void handleLocationUpdate(Long userId, Long pilgrimageId, double latitude, double longitude) { + Pilgrimage pilgrimage = pilgrimageRepository.findById(pilgrimageId) + .orElseThrow(()->new RestApiException(ErrorCode.PILGRIMAGE_NOT_FOUND)); + + PilgrimageSocketDto.ButtonState newState = pilgrimageService.determineButtonState(userId, pilgrimage.getId(), latitude, longitude); + + PilgrimageSocketDto.ButtonState lastState = lastButtonStateCache.get(userId); + + if (lastState == null || !newState.equals(lastState)) { + lastButtonStateCache.put(userId, newState); + messagingTemplate.convertAndSend("/pub/statusUpdate/" + pilgrimageId, newState); + } + } +} diff --git a/src/main/java/com/favoriteplace/global/security/Filter/JwtAuthenticationFilter.java b/src/main/java/com/favoriteplace/global/security/Filter/JwtAuthenticationFilter.java index 46656cc..38a64c3 100644 --- a/src/main/java/com/favoriteplace/global/security/Filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/favoriteplace/global/security/Filter/JwtAuthenticationFilter.java @@ -74,6 +74,12 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse // 1. Request Header ์—์„œ JWT ํ† ํฐ ์ถ”์ถœ String token = resolveToken((HttpServletRequest) request); + + if ("websocket".equalsIgnoreCase(request.getHeader("Upgrade"))) { + chain.doFilter(request, response); + return; + } + String requestURI = httpServletRequest.getRequestURI(); // 2. validateToken ์œผ๋กœ ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ diff --git a/src/main/java/com/favoriteplace/global/security/config/RedisConfig.java b/src/main/java/com/favoriteplace/global/security/config/RedisConfig.java new file mode 100644 index 0000000..3ccf7f8 --- /dev/null +++ b/src/main/java/com/favoriteplace/global/security/config/RedisConfig.java @@ -0,0 +1,26 @@ +package com.favoriteplace.global.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + // Key serializer + template.setKeySerializer(new StringRedisSerializer()); + + // Value serializer + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + + return template; + } +} diff --git a/src/main/java/com/favoriteplace/global/security/provider/JwtTokenProvider.java b/src/main/java/com/favoriteplace/global/security/provider/JwtTokenProvider.java index d799e6b..09fa932 100644 --- a/src/main/java/com/favoriteplace/global/security/provider/JwtTokenProvider.java +++ b/src/main/java/com/favoriteplace/global/security/provider/JwtTokenProvider.java @@ -1,6 +1,7 @@ package com.favoriteplace.global.security.provider; import com.favoriteplace.app.dto.member.MemberDto.TokenInfo; +import com.favoriteplace.global.security.CustomUserDetails; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; @@ -10,6 +11,8 @@ import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import java.util.Date; +import java.util.List; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; diff --git a/src/main/java/com/favoriteplace/global/websocket/JwtChannelInterceptor.java b/src/main/java/com/favoriteplace/global/websocket/JwtChannelInterceptor.java new file mode 100644 index 0000000..1928db6 --- /dev/null +++ b/src/main/java/com/favoriteplace/global/websocket/JwtChannelInterceptor.java @@ -0,0 +1,77 @@ +package com.favoriteplace.global.websocket; + +import com.favoriteplace.app.domain.Member; +import com.favoriteplace.app.repository.MemberRepository; +import com.favoriteplace.global.exception.ErrorCode; +import com.favoriteplace.global.exception.ErrorResponse; +import com.favoriteplace.global.exception.RestApiException; +import com.favoriteplace.global.security.CustomUserDetails; +import com.favoriteplace.global.security.provider.JwtTokenProvider; +import com.google.api.gax.rpc.ApiException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +@Slf4j +public class JwtChannelInterceptor implements ChannelInterceptor { + private final MemberRepository userUtilityService; + private final JwtTokenProvider jwtProvider; + + /** + * WebSocket ์—ฐ๊ฒฐ ์ „ JWT ๊ฒ€์‚ฌํ•˜๋Š” ์ธํ„ฐ์…‰ํ„ฐ ๋ฉ”์„œ๋“œ + * @param message + * @param channel + * @return + */ + @Override + public Message preSend(Message message, MessageChannel channel) { + StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); + + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + + List authorization = accessor.getNativeHeader("Authorization"); + + if (authorization != null && !authorization.isEmpty()) { + String bearerToken = authorization.get(0); + String jwt = bearerToken.startsWith("Bearer ") ? bearerToken.substring(7) : bearerToken; + + try { + // JWT ํ† ํฐ ๊ฒ€์ฆ + if (!jwtProvider.validateToken(jwt)) { + throw new RestApiException(ErrorCode.USER_NOT_AUTHOR); + } + + Authentication authentication = jwtProvider.getAuthentication(jwt); + + Member member = userUtilityService.findByEmail(authentication.getName()) + .orElseThrow(()-> new RestApiException(ErrorCode.USER_NOT_FOUND)); + + CustomUserDetails userDetails = new CustomUserDetails(member); + UsernamePasswordAuthenticationToken userInfo = + new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(userInfo); + } catch (Exception e) { + log.error("JWT Verification Failed: " + e.getMessage()); + return null; + } + } else { + log.error("Authorization header is not found"); + return null; + } + } + return message; + } +} diff --git a/src/main/java/com/favoriteplace/global/websocket/RedisService.java b/src/main/java/com/favoriteplace/global/websocket/RedisService.java new file mode 100644 index 0000000..cfcdfb7 --- /dev/null +++ b/src/main/java/com/favoriteplace/global/websocket/RedisService.java @@ -0,0 +1,39 @@ +package com.favoriteplace.global.websocket; + +import com.favoriteplace.app.domain.Member; +import com.favoriteplace.app.domain.travel.Pilgrimage; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.time.Instant; + +@Service +@RequiredArgsConstructor +public class RedisService { + private final RedisTemplate redisTemplate; + + private static final String CERTIFICATION_KEY_PREFIX = "certification:"; + private static final Duration CERTIFICATION_EXPIRATION = Duration.ofHours(24); + + // ์‚ฌ์šฉ์ž๊ฐ€ ์ธ์ฆ ์žฅ์†Œ์— ์ ‘์†ํ•œ ์‹œ์ ์„ ์ €์žฅ + public void saveCertificationTime(Long userId, Long pilgrimageId) { + String key = CERTIFICATION_KEY_PREFIX + userId + ":" + pilgrimageId; + Instant now = Instant.now(); + redisTemplate.opsForValue().set(key, now); + redisTemplate.expire(key, CERTIFICATION_EXPIRATION); + } + + // ์ธ์ฆ ์‹œ์ ์—์„œ 24์‹œ๊ฐ„์ด ์ง€๋‚ฌ๋Š”์ง€ ํ™•์ธ + public boolean isCertificationExpired(Member member, Pilgrimage pilgrimage) { + String key = CERTIFICATION_KEY_PREFIX + member.getId() + ":" + pilgrimage.getId(); + Instant savedTime = (Instant) redisTemplate.opsForValue().get(key); + return savedTime == null || savedTime.isBefore(Instant.now().minus(CERTIFICATION_EXPIRATION)); + } + + public void deleteCertificationTime(Long userId, Long pilgrimageId) { + String key = CERTIFICATION_KEY_PREFIX + userId + ":" + pilgrimageId; + redisTemplate.delete(key); + } +} diff --git a/src/main/java/com/favoriteplace/global/websocket/WebSocketConfig.java b/src/main/java/com/favoriteplace/global/websocket/WebSocketConfig.java new file mode 100644 index 0000000..9c6755a --- /dev/null +++ b/src/main/java/com/favoriteplace/global/websocket/WebSocketConfig.java @@ -0,0 +1,34 @@ +package com.favoriteplace.global.websocket; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@Configuration +@RequiredArgsConstructor +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + private final JwtChannelInterceptor jwtChannelInterceptor; + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.setApplicationDestinationPrefixes("/app") + .enableSimpleBroker("/pub"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws") + .setAllowedOriginPatterns("*") + .withSockJS(); + } + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(jwtChannelInterceptor); + } +}