diff --git a/src/main/java/com/numberone/backend/domain/article/dto/request/UploadArticleRequest.java b/src/main/java/com/numberone/backend/domain/article/dto/request/UploadArticleRequest.java index 9bcea7c0..34650b0d 100644 --- a/src/main/java/com/numberone/backend/domain/article/dto/request/UploadArticleRequest.java +++ b/src/main/java/com/numberone/backend/domain/article/dto/request/UploadArticleRequest.java @@ -23,8 +23,6 @@ public class UploadArticleRequest { @NotNull(message = """ 게시글의 태그를 하나 선택해주세요. - - LIFE(일상), FRAUD(사기), SAFETY(안전), REPORT(제보) """) private ArticleTag articleTag; // 게시글 태그 @@ -34,4 +32,7 @@ public class UploadArticleRequest { private Double longitude; private Double latitude; + @NotNull(message = "동 위치 정보 제공 동의는 null 일 수 없습니다.") + private boolean regionAgreementCheck; // 동 정보 제공 동의 + } diff --git a/src/main/java/com/numberone/backend/domain/article/dto/response/ArticleSearchParameter.java b/src/main/java/com/numberone/backend/domain/article/dto/response/ArticleSearchParameter.java index 5eb8b6dd..66ce5453 100644 --- a/src/main/java/com/numberone/backend/domain/article/dto/response/ArticleSearchParameter.java +++ b/src/main/java/com/numberone/backend/domain/article/dto/response/ArticleSearchParameter.java @@ -13,4 +13,5 @@ public class ArticleSearchParameter { private Long lastArticleId; private Double longitude; private Double latitude; + private String regionLv2; // 구 } diff --git a/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java b/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java index a695eb61..e95aa792 100644 --- a/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java +++ b/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java @@ -32,6 +32,7 @@ public class GetArticleDetailResponse { private String ownerName; private String ownerNickName; private String address; + private String regionLv2; private Long ownerMemberId; private String ownerProfileImageUrl; @@ -67,6 +68,8 @@ public static GetArticleDetailResponse of( .isLiked(memberLikedArticleList.contains(article.getId())) .articleTag(article.getArticleTag()) .commentCount(commentCount) + .regionLv2(Optional.ofNullable(article.getLv2()) + .orElse("")) .build(); } diff --git a/src/main/java/com/numberone/backend/domain/article/entity/Article.java b/src/main/java/com/numberone/backend/domain/article/entity/Article.java index 92f0785f..c0bee56e 100644 --- a/src/main/java/com/numberone/backend/domain/article/entity/Article.java +++ b/src/main/java/com/numberone/backend/domain/article/entity/Article.java @@ -54,6 +54,15 @@ public class Article extends BaseTimeEntity { @Comment("게시글 작성 당시 주소") private String address; + @Comment("시/도") + private String lv1; + + @Comment("구/군") + private String lv2; + + @Comment("동/읍/면") + private String lv3; + @ColumnDefault("0") @Comment("게시글 좋아요 개수") private Integer likeCount; @@ -89,6 +98,13 @@ public void updateAddress(String address) { this.address = address; } + public void updateAddressDetail (String[] addressDetails) { + int length = addressDetails.length; + this.lv1 = length > 0 ? addressDetails[0] : ""; + this.lv2 = length > 1 ? addressDetails[1] : ""; + this.lv3 = length > 2 ? addressDetails[2] : ""; + } + public void increaseLikeCount() { this.likeCount++; } diff --git a/src/main/java/com/numberone/backend/domain/article/repository/custom/ArticleRepositoryCustomImpl.java b/src/main/java/com/numberone/backend/domain/article/repository/custom/ArticleRepositoryCustomImpl.java index f4160bad..41dc34df 100644 --- a/src/main/java/com/numberone/backend/domain/article/repository/custom/ArticleRepositoryCustomImpl.java +++ b/src/main/java/com/numberone/backend/domain/article/repository/custom/ArticleRepositoryCustomImpl.java @@ -35,7 +35,8 @@ public Slice getArticlesNoOffSetPaging(ArticleSearchPara .where( ltArticleId(param.getLastArticleId()), checkTagCondition(param.getTag()), - article.articleStatus.eq(ArticleStatus.ACTIVATED) + article.articleStatus.eq(ArticleStatus.ACTIVATED), + article.lv2.eq(param.getRegionLv2()) ) .orderBy(article.id.desc()) .limit(pageable.getPageSize() + 1) diff --git a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java index 52de3c0d..ca6f8a7e 100644 --- a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java +++ b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java @@ -19,7 +19,10 @@ import com.numberone.backend.domain.member.entity.Member; import com.numberone.backend.domain.member.repository.MemberRepository; import com.numberone.backend.domain.notification.entity.NotificationTag; +import com.numberone.backend.domain.notificationregion.entity.NotificationRegion; +import com.numberone.backend.domain.notificationregion.repository.NotificationRegionRepository; import com.numberone.backend.domain.token.util.SecurityContextProvider; +import com.numberone.backend.exception.conflict.UnauthorizedLocationException; import com.numberone.backend.exception.notfound.NotFoundArticleException; import com.numberone.backend.exception.notfound.NotFoundMemberException; import com.numberone.backend.support.fcm.service.FcmMessageProvider; @@ -53,6 +56,7 @@ public class ArticleService { private final ArticleImageRepository articleImageRepository; private final CommentRepository commentRepository; private final ArticleLikeRepository articleLikeRepository; + private final NotificationRegionRepository notificationRegionRepository; private final S3Provider s3Provider; private final LocationProvider locationProvider; private final FcmMessageProvider fcmMessageProvider; @@ -107,15 +111,31 @@ public UploadArticleResponse uploadArticle(UploadArticleRequest request) { // 4. 작성자 주소 설정 Double latitude = request.getLatitude(); Double longitude = request.getLongitude(); - if (latitude != null && longitude != null) { + String address = ""; + if (latitude != null && longitude != null && request.isRegionAgreementCheck()) { // 주소가 null 이 아닌 경우에만 api 요청하여 update - String address = locationProvider.pos2address(request.getLatitude(), request.getLongitude()); + address = locationProvider.pos2address(request.getLatitude(), request.getLongitude()); article.updateAddress(address); } + if (!address.isEmpty()) { + String[] regionInfo = address.split(" "); + article.updateAddressDetail(regionInfo); + validateLocation(owner, address); + } + return UploadArticleResponse.of(article, imageUrls, thumbNailImageUrl); } + public void validateLocation(Member member, String realLocation) { + List regionLv2List = member.getNotificationRegions() + .stream().map(NotificationRegion::getLv2).toList(); + String[] realRegions = realLocation.split(" "); + + if (realRegions.length >= 1 && !regionLv2List.contains(realRegions[1])) { + throw new UnauthorizedLocationException(); + } + } @Transactional public DeleteArticleResponse deleteArticle(Long articleId) { diff --git a/src/main/java/com/numberone/backend/domain/member/controller/MemberController.java b/src/main/java/com/numberone/backend/domain/member/controller/MemberController.java index 2344e859..bfa65dde 100644 --- a/src/main/java/com/numberone/backend/domain/member/controller/MemberController.java +++ b/src/main/java/com/numberone/backend/domain/member/controller/MemberController.java @@ -2,6 +2,7 @@ import com.numberone.backend.domain.member.dto.request.OnboardingRequest; import com.numberone.backend.domain.member.dto.request.BuyHeartRequest; +import com.numberone.backend.domain.member.dto.response.GetNotificationRegionResponse; import com.numberone.backend.domain.member.dto.response.HeartCntResponse; import com.numberone.backend.domain.member.dto.response.UploadProfileImageResponse; import com.numberone.backend.domain.member.service.MemberService; @@ -36,7 +37,7 @@ public class MemberController { @Operation(summary = "회원 프로필 사진 업로드 API", description = """ 1. 반드시 access token 을 헤더에 포함하여 호출해주세요. (유저를 식별하기 위함입니다.) - + 2. 프로필 사진은 MultipartFile 으로 반드시 image 라는 이름으로 보내주세요 """) @PostMapping("/profile-image") @@ -67,7 +68,15 @@ public ResponseEntity getHeart(Authentication authentication) 온보딩에서 선택한 닉네임, 재난유형, 알림지역 데이터를 body에 담아 전달해주세요. """) @PostMapping("/onboarding") - public void initMemberData(Authentication authentication, @Valid @RequestBody OnboardingRequest onboardingRequest){ + public void initMemberData(Authentication authentication, @Valid @RequestBody OnboardingRequest onboardingRequest) { memberService.initMemberData(authentication.getName(), onboardingRequest); } + + @Operation(summary = "사용자가 온보딩 시 추가한 지역 리스트 가져오기", description = """ + 게시글 커뮤니티 지역 구분으로 사용할 수 있습니다. + """) + @GetMapping("/regions") + public ResponseEntity getNotificationRegions() { + return ResponseEntity.ok(memberService.getNotificationRegionLv2()); + } } diff --git a/src/main/java/com/numberone/backend/domain/member/dto/response/GetNotificationRegionResponse.java b/src/main/java/com/numberone/backend/domain/member/dto/response/GetNotificationRegionResponse.java new file mode 100644 index 00000000..bc92d550 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/member/dto/response/GetNotificationRegionResponse.java @@ -0,0 +1,20 @@ +package com.numberone.backend.domain.member.dto.response; + +import com.numberone.backend.domain.notificationregion.entity.NotificationRegion; +import lombok.*; + +import java.util.List; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class GetNotificationRegionResponse { + List regions; + public static GetNotificationRegionResponse of (List notificationRegions){ + return GetNotificationRegionResponse.builder() + .regions(notificationRegions.stream().map(NotificationRegion::getLv2).toList()) + .build(); + } +} diff --git a/src/main/java/com/numberone/backend/domain/member/entity/Member.java b/src/main/java/com/numberone/backend/domain/member/entity/Member.java index 67de046c..51213ce2 100644 --- a/src/main/java/com/numberone/backend/domain/member/entity/Member.java +++ b/src/main/java/com/numberone/backend/domain/member/entity/Member.java @@ -4,6 +4,7 @@ import com.numberone.backend.domain.like.entity.ArticleLike; import com.numberone.backend.domain.like.entity.CommentLike; import com.numberone.backend.domain.like.entity.ConversationLike; +import com.numberone.backend.domain.like.entity.ConversationLike; import jakarta.persistence.*; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -12,12 +13,12 @@ import com.numberone.backend.domain.notificationdisaster.entity.NotificationDisaster; import com.numberone.backend.domain.notificationregion.entity.NotificationRegion; import com.numberone.backend.domain.support.entity.Support; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.Comment; -import org.springframework.web.bind.annotation.RequestMapping; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/com/numberone/backend/domain/member/service/MemberService.java b/src/main/java/com/numberone/backend/domain/member/service/MemberService.java index 7107ebc6..f7f74b93 100644 --- a/src/main/java/com/numberone/backend/domain/member/service/MemberService.java +++ b/src/main/java/com/numberone/backend/domain/member/service/MemberService.java @@ -1,19 +1,20 @@ package com.numberone.backend.domain.member.service; -import com.numberone.backend.domain.member.dto.response.UploadProfileImageResponse; import com.numberone.backend.domain.disaster.util.DisasterType; +import com.numberone.backend.domain.member.dto.request.BuyHeartRequest; import com.numberone.backend.domain.member.dto.request.OnboardingAddress; import com.numberone.backend.domain.member.dto.request.OnboardingDisasterType; import com.numberone.backend.domain.member.dto.request.OnboardingRequest; -import com.numberone.backend.domain.member.dto.request.BuyHeartRequest; +import com.numberone.backend.domain.member.dto.response.GetNotificationRegionResponse; import com.numberone.backend.domain.member.dto.response.HeartCntResponse; +import com.numberone.backend.domain.member.dto.response.UploadProfileImageResponse; import com.numberone.backend.domain.member.entity.Member; import com.numberone.backend.domain.member.repository.MemberRepository; -import com.numberone.backend.domain.token.util.SecurityContextProvider; import com.numberone.backend.domain.notificationdisaster.entity.NotificationDisaster; import com.numberone.backend.domain.notificationdisaster.repository.NotificationDisasterRepository; import com.numberone.backend.domain.notificationregion.entity.NotificationRegion; import com.numberone.backend.domain.notificationregion.repository.NotificationRegionRepository; +import com.numberone.backend.domain.token.util.SecurityContextProvider; import com.numberone.backend.exception.notfound.NotFoundMemberException; import com.numberone.backend.support.s3.S3Provider; import lombok.RequiredArgsConstructor; @@ -22,6 +23,8 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.util.List; + @RequiredArgsConstructor @Service @Slf4j @@ -80,7 +83,7 @@ public HeartCntResponse getHeart(String email) { } @Transactional - public UploadProfileImageResponse uploadProfileImage(MultipartFile image){ + public UploadProfileImageResponse uploadProfileImage(MultipartFile image) { String email = SecurityContextProvider.getAuthenticatedUserEmail(); Member member = memberRepository.findByEmail(email) .orElseThrow(NotFoundMemberException::new); @@ -93,4 +96,11 @@ public UploadProfileImageResponse uploadProfileImage(MultipartFile image){ return UploadProfileImageResponse.of(imageUrl); } + public GetNotificationRegionResponse getNotificationRegionLv2() { + String principal = SecurityContextProvider.getAuthenticatedUserEmail(); + Member member = memberRepository.findByEmail(principal) + .orElseThrow(NotFoundMemberException::new); + return GetNotificationRegionResponse.of(member.getNotificationRegions()); + } + } diff --git a/src/main/java/com/numberone/backend/domain/notificationregion/repository/NotificationRegionRepository.java b/src/main/java/com/numberone/backend/domain/notificationregion/repository/NotificationRegionRepository.java index 6c9b2f09..a75d5cda 100644 --- a/src/main/java/com/numberone/backend/domain/notificationregion/repository/NotificationRegionRepository.java +++ b/src/main/java/com/numberone/backend/domain/notificationregion/repository/NotificationRegionRepository.java @@ -3,6 +3,8 @@ import com.numberone.backend.domain.notificationregion.entity.NotificationRegion; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface NotificationRegionRepository extends JpaRepository { void deleteAllByMemberId(Long memberId); } diff --git a/src/main/java/com/numberone/backend/exception/conflict/UnauthorizedLocationException.java b/src/main/java/com/numberone/backend/exception/conflict/UnauthorizedLocationException.java new file mode 100644 index 00000000..6edf5d53 --- /dev/null +++ b/src/main/java/com/numberone/backend/exception/conflict/UnauthorizedLocationException.java @@ -0,0 +1,9 @@ +package com.numberone.backend.exception.conflict; + +import static com.numberone.backend.exception.context.CustomExceptionContext.UNAUTHORIZED_LOCATION_ERROR; + +public class UnauthorizedLocationException extends ConflictException { + public UnauthorizedLocationException() { + super(UNAUTHORIZED_LOCATION_ERROR); + } +} diff --git a/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java b/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java index ca8601b0..ebd60543 100644 --- a/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java +++ b/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java @@ -37,13 +37,14 @@ public enum CustomExceptionContext implements ExceptionContext { //후원 페이지 관련 예외 NOT_FOUND_SUPPORT("존재하지 않는 후원 관계입니다.", 7000), NOT_FOUND_SPONSOR("존재하지 않는 후원입니다.", 7001), - BAD_REQUEST_HEART("후원을 하기에는 사용자의 마음 갯수가 부족합니다.",7002), + BAD_REQUEST_HEART("후원을 하기에는 사용자의 마음 갯수가 부족합니다.", 7002), // article 관련 예외 NOT_FOUND_ARTICLE("해당 게시글을 찾을 수 없습니다.", 8000), // article image 관련 예외 NOT_FOUND_ARTICLE_IMAGE("해당 이미지를 찾을 수 없습니다.", 9000), + UNAUTHORIZED_LOCATION_ERROR("사용자가 해당 요청을 처리할 수 없는 지역에 위치하고 있습니다.", 9001), // comment 관련 예외 NOT_FOUND_COMMENT("해당 댓글을 찾을 수 없습니다.", 10000), @@ -54,7 +55,7 @@ public enum CustomExceptionContext implements ExceptionContext { //conversation 관련 예외 NOT_FOUND_CONVERSATION("해당 대화를 찾을 수 없습니다.", 12000), - BAD_REQUEST_CONVERSATION_SORT("정렬 기준 값을 올바르게 전달해주세요. (popularity 또는 time)",12001) + BAD_REQUEST_CONVERSATION_SORT("정렬 기준 값을 올바르게 전달해주세요. (popularity 또는 time)", 12001) ; private final String message;