diff --git a/application/build.gradle.kts b/application/build.gradle.kts index fefe2aa9..b06e9278 100644 --- a/application/build.gradle.kts +++ b/application/build.gradle.kts @@ -32,6 +32,9 @@ dependencies { // aop implementation("org.springframework.boot:spring-boot-starter-aop") + // Mixpanel + implementation("com.mixpanel:mixpanel-java:_") + // test container testImplementation("org.testcontainers:testcontainers:_") testImplementation("org.testcontainers:junit-jupiter:_") diff --git a/application/src/main/java/org/depromeet/spot/application/common/config/SecurityConfig.java b/application/src/main/java/org/depromeet/spot/application/common/config/SecurityConfig.java index 3b6994a2..c067cf34 100644 --- a/application/src/main/java/org/depromeet/spot/application/common/config/SecurityConfig.java +++ b/application/src/main/java/org/depromeet/spot/application/common/config/SecurityConfig.java @@ -33,7 +33,8 @@ public class SecurityConfig { "/api/v1/members/**", "/actuator/**", "/login/oauth2/code/google/**", - "/google/**" + "/google/**", + "/trackEvent" }; @Bean diff --git a/application/src/main/java/org/depromeet/spot/application/common/jwt/JwtAuthenticationFilter.java b/application/src/main/java/org/depromeet/spot/application/common/jwt/JwtAuthenticationFilter.java index 8f8e45c2..6790e7e2 100644 --- a/application/src/main/java/org/depromeet/spot/application/common/jwt/JwtAuthenticationFilter.java +++ b/application/src/main/java/org/depromeet/spot/application/common/jwt/JwtAuthenticationFilter.java @@ -41,7 +41,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { "/api/v1/levels/info", "/kakao", "/api/v1/jwts", - "/google/callback" + "/google/callback", + "/trackEvent", }; private static final Map> AUTH_METHOD_WHITELIST = diff --git a/application/src/main/java/org/depromeet/spot/application/review/ReadReviewController.java b/application/src/main/java/org/depromeet/spot/application/review/ReadReviewController.java index aba78b4a..59b5007d 100644 --- a/application/src/main/java/org/depromeet/spot/application/review/ReadReviewController.java +++ b/application/src/main/java/org/depromeet/spot/application/review/ReadReviewController.java @@ -131,7 +131,7 @@ public BaseReviewResponse findReviewByReviewId( @Parameter(hidden = true) Long memberId, @PathVariable("reviewId") @NotNull @Parameter(description = "리뷰 PK", required = true) Long reviewId) { - ReadReviewResult readReviewResult = readReviewUsecase.findReviewById(reviewId); + ReadReviewResult readReviewResult = readReviewUsecase.findReviewById(reviewId, memberId); return BaseReviewResponse.from(readReviewResult.review()); } } diff --git a/application/src/main/resources/application.yaml b/application/src/main/resources/application.yaml index d21afd30..711ae068 100644 --- a/application/src/main/resources/application.yaml +++ b/application/src/main/resources/application.yaml @@ -5,7 +5,7 @@ server: spring: # 서브모듈 profile profiles: - active: local + active: dev group: local: - jpa @@ -13,9 +13,12 @@ spring: dev: - jpa - monitoring + - mixpanel prod: - jpa - sentry + - monitoring + - mixpanel servlet: multipart: max-file-size: 10MB diff --git a/domain/src/main/java/org/depromeet/spot/domain/mixpanel/MixpanelEvent.java b/domain/src/main/java/org/depromeet/spot/domain/mixpanel/MixpanelEvent.java new file mode 100644 index 00000000..ad4758f3 --- /dev/null +++ b/domain/src/main/java/org/depromeet/spot/domain/mixpanel/MixpanelEvent.java @@ -0,0 +1,19 @@ +package org.depromeet.spot.domain.mixpanel; + +import lombok.Getter; + +@Getter +public enum MixpanelEvent { + REVIEW_REGISTER("review_register"), + REVIEW_REGISTER_MAX("review_register"), + REVIEW_OPEN_COUNT("review_open_count"), + REVIEW_LIKE_COUNT("review_like_count"), + REVIEW_SCRAP_COUNT("review_scrap_count"), + ; + + String value; + + MixpanelEvent(String value) { + this.value = value; + } +} diff --git a/infrastructure/build.gradle.kts b/infrastructure/build.gradle.kts index 04303df5..6ce66765 100644 --- a/infrastructure/build.gradle.kts +++ b/infrastructure/build.gradle.kts @@ -39,6 +39,9 @@ dependencies { // caffeine cache implementation("com.github.ben-manes.caffeine:caffeine:_") + + // Mixpanel + implementation("com.mixpanel:mixpanel-java:_") } tasks.bootJar { enabled = false } diff --git a/infrastructure/src/main/java/org/depromeet/spot/infrastructure/mixpanel/property/MixpanelProperties.java b/infrastructure/src/main/java/org/depromeet/spot/infrastructure/mixpanel/property/MixpanelProperties.java new file mode 100644 index 00000000..8d437e82 --- /dev/null +++ b/infrastructure/src/main/java/org/depromeet/spot/infrastructure/mixpanel/property/MixpanelProperties.java @@ -0,0 +1,6 @@ +package org.depromeet.spot.infrastructure.mixpanel.property; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "mixpanel") +public record MixpanelProperties(String token) {} diff --git a/infrastructure/src/main/java/org/depromeet/spot/infrastructure/mixpanel/repository/MixpanelRepositoryImpl.java b/infrastructure/src/main/java/org/depromeet/spot/infrastructure/mixpanel/repository/MixpanelRepositoryImpl.java new file mode 100644 index 00000000..3d6980b1 --- /dev/null +++ b/infrastructure/src/main/java/org/depromeet/spot/infrastructure/mixpanel/repository/MixpanelRepositoryImpl.java @@ -0,0 +1,48 @@ +package org.depromeet.spot.infrastructure.mixpanel.repository; + +import java.io.IOException; + +import org.depromeet.spot.domain.mixpanel.MixpanelEvent; +import org.depromeet.spot.infrastructure.mixpanel.property.MixpanelProperties; +import org.depromeet.spot.usecase.port.out.mixpanel.MixpanelRepository; +import org.json.JSONObject; +import org.springframework.stereotype.Component; + +import com.mixpanel.mixpanelapi.ClientDelivery; +import com.mixpanel.mixpanelapi.MessageBuilder; +import com.mixpanel.mixpanelapi.MixpanelAPI; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MixpanelRepositoryImpl implements MixpanelRepository { + + private final MixpanelProperties mixpanelProperties; + + // mixpanelEvent는 eventName(이 단위로 이벤트가 묶임) + // distinctId는 사용자를 구분하는 데 사용됨. + @Override + public void eventTrack(MixpanelEvent mixpanelEvent, String distinctId) { + try { + + // 믹스패널 이벤트 메시지 생성 + MessageBuilder messageBuilder = new MessageBuilder(mixpanelProperties.token()); + + // 이벤트 생성 + JSONObject sentEvent = messageBuilder.event(distinctId, mixpanelEvent.getValue(), null); + + // 만든 여러 이벤트를 delivery + ClientDelivery delivery = new ClientDelivery(); + delivery.addMessage(sentEvent); + + // Mixpanel로 데이터 전송 + MixpanelAPI mixpanel = new MixpanelAPI(); + mixpanel.deliver(delivery); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/usecase/build.gradle.kts b/usecase/build.gradle.kts index 4210fd27..8aac069f 100644 --- a/usecase/build.gradle.kts +++ b/usecase/build.gradle.kts @@ -8,6 +8,10 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jpa:_") { because("@Transactional을 위해 추가") } + + // Mixpanel + implementation("com.mixpanel:mixpanel-java:_") + } tasks.bootJar { enabled = false } diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/port/in/review/ReadReviewUsecase.java b/usecase/src/main/java/org/depromeet/spot/usecase/port/in/review/ReadReviewUsecase.java index 865e2ca2..4280f65b 100644 --- a/usecase/src/main/java/org/depromeet/spot/usecase/port/in/review/ReadReviewUsecase.java +++ b/usecase/src/main/java/org/depromeet/spot/usecase/port/in/review/ReadReviewUsecase.java @@ -40,7 +40,7 @@ MyReviewListResult findMyReviewsByUserId( MyRecentReviewResult findLastReviewByMemberId(Long memberId); - ReadReviewResult findReviewById(Long reviewId); + ReadReviewResult findReviewById(Long reviewId, Long memberId); long countByIdByMemberId(Long memberId); diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/port/out/mixpanel/MixpanelRepository.java b/usecase/src/main/java/org/depromeet/spot/usecase/port/out/mixpanel/MixpanelRepository.java new file mode 100644 index 00000000..09ca9d30 --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/port/out/mixpanel/MixpanelRepository.java @@ -0,0 +1,7 @@ +package org.depromeet.spot.usecase.port.out.mixpanel; + +import org.depromeet.spot.domain.mixpanel.MixpanelEvent; + +public interface MixpanelRepository { + void eventTrack(MixpanelEvent mixpanelEvent, String distinctId); +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/service/review/CreateReviewService.java b/usecase/src/main/java/org/depromeet/spot/usecase/service/review/CreateReviewService.java index 46a12116..38ea038a 100644 --- a/usecase/src/main/java/org/depromeet/spot/usecase/service/review/CreateReviewService.java +++ b/usecase/src/main/java/org/depromeet/spot/usecase/service/review/CreateReviewService.java @@ -4,10 +4,12 @@ import java.util.Map; import org.depromeet.spot.domain.member.Member; +import org.depromeet.spot.domain.mixpanel.MixpanelEvent; import org.depromeet.spot.domain.review.Review; import org.depromeet.spot.domain.review.keyword.Keyword; import org.depromeet.spot.usecase.port.in.review.CreateReviewUsecase; import org.depromeet.spot.usecase.port.out.member.MemberRepository; +import org.depromeet.spot.usecase.port.out.mixpanel.MixpanelRepository; import org.depromeet.spot.usecase.port.out.review.ReviewRepository; import org.depromeet.spot.usecase.service.member.processor.MemberLevelProcessor; import org.depromeet.spot.usecase.service.review.processor.ReviewCreationProcessor; @@ -30,6 +32,7 @@ public class CreateReviewService implements CreateReviewUsecase { private final ReviewImageProcessor reviewImageProcessor; private final ReviewKeywordProcessor reviewKeywordProcessor; private final MemberLevelProcessor memberLevelProcessor; + private final MixpanelRepository mixpanelRepository; @Override @Transactional @@ -46,6 +49,9 @@ public CreateReviewResult create(Long blockId, Long memberId, CreateReviewComman Member levelUpdateMember = memberLevelProcessor.calculateAndUpdateMemberLevel(member); + // 믹스패널 이벤트(후기 등록 완료) 호출 + mixpanelRepository.eventTrack(MixpanelEvent.REVIEW_REGISTER, String.valueOf(memberId)); + return new CreateReviewResult(savedReview, levelUpdateMember, review.getSeat()); } diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/service/review/ReadReviewService.java b/usecase/src/main/java/org/depromeet/spot/usecase/service/review/ReadReviewService.java index 4a554c6e..5dc439df 100644 --- a/usecase/src/main/java/org/depromeet/spot/usecase/service/review/ReadReviewService.java +++ b/usecase/src/main/java/org/depromeet/spot/usecase/service/review/ReadReviewService.java @@ -5,6 +5,7 @@ import java.util.stream.Collectors; import org.depromeet.spot.domain.member.Member; +import org.depromeet.spot.domain.mixpanel.MixpanelEvent; import org.depromeet.spot.domain.review.Review; import org.depromeet.spot.domain.review.Review.ReviewType; import org.depromeet.spot.domain.review.Review.SortCriteria; @@ -15,6 +16,7 @@ import org.depromeet.spot.domain.team.BaseballTeam; import org.depromeet.spot.usecase.port.in.review.ReadReviewUsecase; import org.depromeet.spot.usecase.port.out.member.MemberRepository; +import org.depromeet.spot.usecase.port.out.mixpanel.MixpanelRepository; import org.depromeet.spot.usecase.port.out.review.BlockTopKeywordRepository; import org.depromeet.spot.usecase.port.out.review.KeywordRepository; import org.depromeet.spot.usecase.port.out.review.ReviewImageRepository; @@ -46,6 +48,7 @@ public class ReadReviewService implements ReadReviewUsecase { private final ReviewScrapRepository reviewScrapRepository; private final ReadReviewProcessor readReviewProcessor; private final PaginationProcessor paginationProcessor; + private final MixpanelRepository mixpanelRepository; private static final int TOP_KEYWORDS_LIMIT = 5; private static final int TOP_IMAGES_LIMIT = 5; @@ -185,10 +188,13 @@ public List findReviewMonths(Long memberId, ReviewType reviewTy } @Override - public ReadReviewResult findReviewById(Long reviewId) { + public ReadReviewResult findReviewById(Long reviewId, Long memberId) { Review review = reviewRepository.findById(reviewId); Review reviewWithKeywords = mapKeywordsToSingleReview(review); + // 믹스패널 이벤트(조회수) 발생 + mixpanelRepository.eventTrack(MixpanelEvent.REVIEW_OPEN_COUNT, String.valueOf(memberId)); + return ReadReviewResult.builder().review(reviewWithKeywords).build(); } diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/service/review/like/ReviewLikeService.java b/usecase/src/main/java/org/depromeet/spot/usecase/service/review/like/ReviewLikeService.java index 973418b4..e0eb93c4 100644 --- a/usecase/src/main/java/org/depromeet/spot/usecase/service/review/like/ReviewLikeService.java +++ b/usecase/src/main/java/org/depromeet/spot/usecase/service/review/like/ReviewLikeService.java @@ -23,6 +23,9 @@ public class ReviewLikeService implements ReviewLikeUsecase { private final UpdateReviewUsecase updateReviewUsecase; private final ReviewLikeRepository reviewLikeRepository; + // TODO : Service 코드와 분리하기 + // private final MixpanelRepository mixpanelRepository; + @Override @DistributedLock(key = "#reviewId") public void toggleLike(final Long memberId, final long reviewId) { @@ -34,6 +37,11 @@ public void toggleLike(final Long memberId, final long reviewId) { } addLike(memberId, reviewId, review); + + // TODO : 테스트 시에도 이벤트 발생함. + // 믹스패널 이벤트(좋아요 수) 발생 + // mixpanelRepository.eventTrack(MixpanelEvent.REVIEW_LIKE_COUNT, + // String.valueOf(memberId)); } public void cancelLike(final long memberId, final long reviewId, Review review) { diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/service/review/scrap/ReviewScrapService.java b/usecase/src/main/java/org/depromeet/spot/usecase/service/review/scrap/ReviewScrapService.java index 36ef93f2..5c372400 100644 --- a/usecase/src/main/java/org/depromeet/spot/usecase/service/review/scrap/ReviewScrapService.java +++ b/usecase/src/main/java/org/depromeet/spot/usecase/service/review/scrap/ReviewScrapService.java @@ -2,12 +2,14 @@ import java.util.List; +import org.depromeet.spot.domain.mixpanel.MixpanelEvent; import org.depromeet.spot.domain.review.Review; import org.depromeet.spot.domain.review.scrap.ReviewScrap; import org.depromeet.spot.usecase.port.in.review.ReadReviewUsecase; import org.depromeet.spot.usecase.port.in.review.UpdateReviewUsecase; import org.depromeet.spot.usecase.port.in.review.page.PageCommand; import org.depromeet.spot.usecase.port.in.review.scrap.ReviewScrapUsecase; +import org.depromeet.spot.usecase.port.out.mixpanel.MixpanelRepository; import org.depromeet.spot.usecase.port.out.review.ReviewScrapRepository; import org.depromeet.spot.usecase.service.review.ReadReviewService; import org.depromeet.spot.usecase.service.review.processor.PaginationProcessor; @@ -29,6 +31,8 @@ public class ReviewScrapService implements ReviewScrapUsecase { private final ReadReviewProcessor readReviewProcessor; private final PaginationProcessor paginationProcessor; + private final MixpanelRepository mixpanelRepository; + @Override public MyScrapListResult findMyScrappedReviews( Long memberId, MyScrapCommand command, PageCommand pageCommand) { @@ -86,6 +90,10 @@ public boolean toggleScrap(final long memberId, final long reviewId) { } addScrap(memberId, reviewId, review); + + // 믹스패널 이벤트(스크랩 수) 발생 + mixpanelRepository.eventTrack(MixpanelEvent.REVIEW_SCRAP_COUNT, String.valueOf(memberId)); + return true; } diff --git a/usecase/src/test/java/org/depromeet/spot/usecase/service/review/ReviewScrapServiceTest.java b/usecase/src/test/java/org/depromeet/spot/usecase/service/review/ReviewScrapServiceTest.java index 458de70f..a88718bc 100644 --- a/usecase/src/test/java/org/depromeet/spot/usecase/service/review/ReviewScrapServiceTest.java +++ b/usecase/src/test/java/org/depromeet/spot/usecase/service/review/ReviewScrapServiceTest.java @@ -18,6 +18,7 @@ import org.depromeet.spot.usecase.port.in.review.page.PageCommand; import org.depromeet.spot.usecase.port.in.review.scrap.ReviewScrapUsecase.MyScrapCommand; import org.depromeet.spot.usecase.port.in.review.scrap.ReviewScrapUsecase.MyScrapListResult; +import org.depromeet.spot.usecase.port.out.mixpanel.MixpanelRepository; import org.depromeet.spot.usecase.service.fake.FakeReviewScrapRepository; import org.depromeet.spot.usecase.service.review.processor.PaginationProcessor; import org.depromeet.spot.usecase.service.review.processor.ReadReviewProcessor; @@ -37,6 +38,7 @@ class ReviewScrapServiceTest { @Mock private ReadReviewService readReviewService; @Mock private ReadReviewProcessor readReviewProcessor; @Mock private PaginationProcessor paginationProcessor; + @Mock private MixpanelRepository mixpanelRepository; @BeforeEach void init() { @@ -49,7 +51,8 @@ void init() { fakeReviewScrapRepository, readReviewService, readReviewProcessor, - paginationProcessor); + paginationProcessor, + mixpanelRepository); } @Test diff --git a/versions.properties b/versions.properties index df3256ba..e120a0ad 100644 --- a/versions.properties +++ b/versions.properties @@ -22,6 +22,8 @@ plugin.io.spring.dependency-management=1.0.11.RELEASE plugin.com.diffplug.spotless=6.21.0 +version.com.mixpanel..mixpanel-java=1.5.3 + version.io.jsonwebtoken..jjwt=0.12.6 version.junit=5.9.1