Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.chaegangjo.goods.service.GoodsImageService;
import com.chaegangjo.goods.service.GoodsService;
import com.chaegangjo.member.domain.Member;
import com.chaegangjo.member.service.KeywordNotificationService;
import com.chaegangjo.member.service.MemberService;
import java.util.List;
import lombok.RequiredArgsConstructor;
Expand All @@ -28,6 +29,7 @@ public class SaveGoodsUseCase {
private final GoodsImageService goodsImageService;
private final CategoryService categoryService;
private final ChatMemberService chatMemberService;
private final KeywordNotificationService keywordNotificationService;

@Transactional
public DetailGoodsInfo execute(SaveGoodsRequest request, Long sellerId) {
Expand All @@ -38,6 +40,8 @@ public DetailGoodsInfo execute(SaveGoodsRequest request, Long sellerId) {
chatMemberService.saveChatMember(goods, seller, request.getMyQuantity(), SELLER);
List<GoodsImage> goodsImages = goodsImageService.saveGoodsImages(goods, request.imageNames().subList(1, request.imageNames().size()));

keywordNotificationService.notifyForNewGoods(goods);

return DetailGoodsInfo.of(goods, goodsImages);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.chaegangjo.member.appllication;

import com.chaegangjo.member.dto.KeywordNotificationInfo;
import com.chaegangjo.member.service.KeywordNotificationService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class DeleteMyKeywordNotificationUseCase {

private final KeywordNotificationService keywordNotificationService;

public List<KeywordNotificationInfo> execute(Long memberId, Long keywordNotificationId) {
keywordNotificationService.delete(memberId, keywordNotificationId);
return keywordNotificationService.findAllByMemberId(memberId).stream()
.map(KeywordNotificationInfo::from)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.chaegangjo.member.appllication;

import com.chaegangjo.member.dto.KeywordNotificationInfo;
import com.chaegangjo.member.service.KeywordNotificationService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class GetMyKeywordNotificationsUseCase {

private final KeywordNotificationService keywordNotificationService;

public List<KeywordNotificationInfo> execute(Long memberId) {
return keywordNotificationService.findAllByMemberId(memberId).stream()
.map(KeywordNotificationInfo::from)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.chaegangjo.member.appllication;

import com.chaegangjo.member.domain.KeywordNotification;
import com.chaegangjo.member.domain.Member;
import com.chaegangjo.member.dto.KeywordNotificationInfo;
import com.chaegangjo.member.dto.request.RegisterKeywordRequest;
import com.chaegangjo.member.service.KeywordNotificationService;
import com.chaegangjo.member.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class RegisterKeywordNotificationUseCase {

private final MemberService memberService;
private final KeywordNotificationService keywordNotificationService;

public KeywordNotificationInfo execute(Long memberId, RegisterKeywordRequest request) {
Member member = memberService.findMemberById(memberId);
KeywordNotification keywordNotification = keywordNotificationService.register(member, request.keyword());
return KeywordNotificationInfo.from(keywordNotification);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.chaegangjo.member.dto;

import com.chaegangjo.member.domain.KeywordNotification;
import io.swagger.v3.oas.annotations.media.Schema;

public record KeywordNotificationInfo(
@Schema(example = "1")
Long id,
@Schema(example = "생수")
String keyword
) {
public static KeywordNotificationInfo from(KeywordNotification keywordNotification) {
return new KeywordNotificationInfo(keywordNotification.getId(), keywordNotification.getKeyword());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.chaegangjo.member.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;

public record RegisterKeywordRequest(
@Schema(example = "생수")
String keyword) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
import com.chaegangjo.goods.dto.GoodsInfo;
import com.chaegangjo.goods.enums.TradeStatus;
import com.chaegangjo.member.appllication.*;
import com.chaegangjo.member.dto.KeywordNotificationInfo;
import com.chaegangjo.member.dto.MemberInfo;
import com.chaegangjo.member.dto.NotificationHistoryInfo;
import com.chaegangjo.member.dto.SearchHistoryInfo;
import com.chaegangjo.member.dto.request.RegisterKeywordRequest;
import com.chaegangjo.member.dto.request.SetNeighborhoodRequest;
import com.chaegangjo.security.oauth2.entity.CustomOAuth2User;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -34,6 +36,9 @@ public class MemberController {
private final GetMySearchHistoriesUseCase getMySearchHistoriesUseCase;
private final GetMyNotificationHistoriesUseCase getMyNotificationHistoriesUseCase;
private final DeleteMySearchHistoryUseCase deleteMySearchHistoryUseCase;
private final RegisterKeywordNotificationUseCase registerKeywordNotificationUseCase;
private final GetMyKeywordNotificationsUseCase getMyKeywordNotificationsUseCase;
private final DeleteMyKeywordNotificationUseCase deleteMyKeywordNotificationUseCase;

@Operation(summary = "회원 정보 조회")
// @PreAuthorize("#id == principal.id")
Expand Down Expand Up @@ -150,4 +155,34 @@ public ResponseEntity<ApiResponse<List<SearchHistoryInfo>>> deleteMySearchHistor
@AuthenticationPrincipal CustomOAuth2User user) {
return ResponseEntity.ok(ApiResponse.success(deleteMySearchHistoryUseCase.all(user.getId())));
}

@Operation(summary = "키워드 알림 등록", description = "관심 키워드를 등록하면 해당 키워드와 일치하는 상품이 근처에 등록될 때 알림을 받습니다.")
@PostMapping("/me/keywords")
public ResponseEntity<ApiResponse<KeywordNotificationInfo>> registerKeyword(
@RequestBody RegisterKeywordRequest request,
@AuthenticationPrincipal CustomOAuth2User user) {
return ResponseEntity.ok(
ApiResponse.success(registerKeywordNotificationUseCase.execute(user.getId(), request))
);
}

@Operation(summary = "나의 키워드 알림 목록 조회")
@GetMapping("/me/keywords")
public ResponseEntity<ApiResponse<List<KeywordNotificationInfo>>> getMyKeywords(
@AuthenticationPrincipal CustomOAuth2User user) {
return ResponseEntity.ok(
ApiResponse.success(getMyKeywordNotificationsUseCase.execute(user.getId()))
);
}

@Operation(summary = "키워드 알림 삭제")
@DeleteMapping("/me/keywords/{keywordNotificationId}")
public ResponseEntity<ApiResponse<List<KeywordNotificationInfo>>> deleteMyKeyword(
@Parameter(example = "1")
@PathVariable Long keywordNotificationId,
@AuthenticationPrincipal CustomOAuth2User user) {
return ResponseEntity.ok(
ApiResponse.success(deleteMyKeywordNotificationUseCase.execute(user.getId(), keywordNotificationId))
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.chaegangjo.exception;


import com.chaegangjo.exception.errorcode.KeywordNotificationErrorCode;

public class KeywordNotificationException extends BaseException {

public KeywordNotificationException(KeywordNotificationErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.chaegangjo.exception.errorcode;


import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum KeywordNotificationErrorCode implements BaseErrorCode {

KEYWORD_NOTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "KEYWORD-NOTIFICATION-001", "존재하지 않는 키워드 알림입니다."),
OWNER_MISMATCH(HttpStatus.BAD_REQUEST, "KEYWORD-NOTIFICATION-002", "본인의 키워드 알림만 삭제할 수 있습니다."),
BLANK_KEYWORD(HttpStatus.BAD_REQUEST, "KEYWORD-NOTIFICATION-003", "키워드는 공백일 수 없습니다."),
DUPLICATE_KEYWORD(HttpStatus.CONFLICT, "KEYWORD-NOTIFICATION-004", "이미 등록된 키워드입니다."),
KEYWORD_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "KEYWORD-NOTIFICATION-005", "등록 가능한 키워드 개수를 초과했습니다.")
;

private final HttpStatus status;
private final String value;
private final String message;
}
22 changes: 22 additions & 0 deletions common/src/main/java/com/chaegangjo/redis/RedisUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,28 @@ public List<Long> getNearByIds(Long memberId, int restrictDistance, Long selecte
return nearByIds;
}

public List<Long> getNearByMemberIds(Point center, int restrictDistance) {
GeoReference<Object> reference = GeoReference.fromCoordinate(center);
Distance radius = new Distance(restrictDistance, Metrics.METERS);

RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
.newGeoRadiusArgs()
.includeDistance()
.sortAscending();

GeoOperations<String, Object> geoOperations = redisTemplate.opsForGeo();
GeoResults<GeoLocation<Object>> results = geoOperations
.search(MEMBER_KEY, reference, radius, args);

List<Long> nearByMemberIds = new ArrayList<>();
for (GeoResult<RedisGeoCommands.GeoLocation<Object>> result : results) {
RedisGeoCommands.GeoLocation<Object> location = result.getContent();
nearByMemberIds.add(Long.parseLong(location.getName().toString()));
}

return nearByMemberIds;
}

private GeoResults<GeoLocation<Object>> getGeoResults(Long memberId, int restrictDistance) {
Point memberPoint = getPoint(MEMBER_KEY, String.valueOf(memberId));
if (memberPoint == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.chaegangjo.member.domain;

import com.chaegangjo.common.entity.BaseCreatedEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"member_id", "keyword"}))
public class KeywordNotification extends BaseCreatedEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, length = 30)
private String keyword;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "member_id")
private Member member;

public KeywordNotification(String keyword, Member member) {
this.keyword = keyword;
this.member = member;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.chaegangjo.member.repository;

import com.chaegangjo.member.domain.KeywordNotification;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface KeywordNotificationRepository extends JpaRepository<KeywordNotification, Long> {

List<KeywordNotification> findAllByMember_IdOrderByCreatedAtDesc(Long memberId);

boolean existsByMember_IdAndKeyword(Long memberId, String keyword);

long countByMember_Id(Long memberId);

@Query("SELECT k FROM KeywordNotification k JOIN FETCH k.member "
+ "WHERE :goodsName LIKE CONCAT('%', k.keyword, '%')")
List<KeywordNotification> findAllMatchingGoodsName(@Param("goodsName") String goodsName);
}
Loading