diff --git a/chapter4_umc/build.gradle b/chapter4_umc/build.gradle index 32d36f0..86f1c37 100644 --- a/chapter4_umc/build.gradle +++ b/chapter4_umc/build.gradle @@ -32,8 +32,31 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // QueryDSL : OpenFeign + implementation "io.github.openfeign.querydsl:querydsl-jpa:7.0" + implementation "io.github.openfeign.querydsl:querydsl-core:7.0" + annotationProcessor "io.github.openfeign.querydsl:querydsl-apt:7.0:jpa" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' } tasks.named('test') { useJUnitPlatform() } + +// QueryDSL 관련 설정 +def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile + +sourceSets { + main.java.srcDirs += [ querydslDir ] +} + +tasks.withType(JavaCompile).configureEach { + options.generatedSourceOutputDirectory.set(querydslDir) +} + +clean.doLast { + file(querydslDir).deleteDir() +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/member/entity/Member.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/member/entity/Member.java index 532ed5b..358c16e 100644 --- a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/member/entity/Member.java +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/member/entity/Member.java @@ -1,6 +1,9 @@ // Member 클래스가 속한 패키지 경로 package com.example.chapter4_umc.domain.member.entity; +import com.example.chapter4_umc.domain.notification.entity.Notification; +import com.example.chapter4_umc.domain.review.entity.Review; +import com.example.chapter4_umc.domain.inquiry.entity.Inquiry; import com.example.chapter4_umc.domain.region.entity.Region; import com.example.chapter4_umc.domain.member.enums.Gender; import org.hibernate.annotations.ColumnDefault; diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/member/repository/MemberRepository.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/member/repository/MemberRepository.java new file mode 100644 index 0000000..74b2c04 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/member/repository/MemberRepository.java @@ -0,0 +1,7 @@ +package com.example.chapter4_umc.domain.member.repository; + +import com.example.chapter4_umc.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberRepository extends JpaRepository { +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/controller/MissionController.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/controller/MissionController.java new file mode 100644 index 0000000..e3d3536 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/controller/MissionController.java @@ -0,0 +1,36 @@ +package com.example.chapter4_umc.domain.mission.controller; + +import com.example.chapter4_umc.domain.mission.dto.MissionJoinResDTO; +import com.example.chapter4_umc.domain.mission.service.MissionService; +import com.example.chapter4_umc.global.apiPayload.ApiResponse; +import com.example.chapter4_umc.global.apiPayload.code.GeneralSuccessCode; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/missions") +@Tag(name = "Mission", description = "미션 도전 API") +public class MissionController { + + private final MissionService missionService; + + private Long getLoggedInMemberId() { + return 1L; // 하드코딩 + } + + @Operation(summary = "미션 도전하기", description = "특정 미션을 도전합니다.") + @PostMapping("/{missionId}/join") + public ApiResponse joinMission( + @PathVariable Long missionId + ) { + MissionJoinResDTO res = missionService.joinMission(missionId, getLoggedInMemberId()); + + return ApiResponse.onSuccess( + GeneralSuccessCode.MISSION_JOIN, + res + ); + } +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/dto/MissionJoinResDTO.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/dto/MissionJoinResDTO.java new file mode 100644 index 0000000..6cc2e14 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/dto/MissionJoinResDTO.java @@ -0,0 +1,13 @@ +package com.example.chapter4_umc.domain.mission.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class MissionJoinResDTO { + + private Long missionId; + private Long memberId; + private String status; +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/entity/Mission.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/entity/Mission.java index c5fadfa..ed3330b 100644 --- a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/entity/Mission.java +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/entity/Mission.java @@ -1,5 +1,6 @@ package com.example.chapter4_umc.domain.mission.entity; +import com.example.chapter4_umc.domain.member.entity.MemberMission; import com.example.chapter4_umc.domain.store.entity.Store; import jakarta.persistence.*; import lombok.*; diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/entity/MissionProgress.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/entity/MissionProgress.java new file mode 100644 index 0000000..30f446d --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/entity/MissionProgress.java @@ -0,0 +1,35 @@ +package com.example.chapter4_umc.domain.mission.entity; + +import com.example.chapter4_umc.domain.member.entity.Member; +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class MissionProgress { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "mission_progress_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "mission_id") + private Mission mission; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + private String status; + + @Builder + public MissionProgress(Mission mission, Member member, String status) { + this.mission = mission; + this.member = member; + this.status = status; + } +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/repository/MissionProgressRepository.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/repository/MissionProgressRepository.java new file mode 100644 index 0000000..75be664 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/repository/MissionProgressRepository.java @@ -0,0 +1,7 @@ +package com.example.chapter4_umc.domain.mission.repository; + +import com.example.chapter4_umc.domain.mission.entity.MissionProgress; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MissionProgressRepository extends JpaRepository { +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/repository/MissionRepository.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/repository/MissionRepository.java new file mode 100644 index 0000000..ff16ca1 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/repository/MissionRepository.java @@ -0,0 +1,7 @@ +package com.example.chapter4_umc.domain.mission.repository; + +import com.example.chapter4_umc.domain.mission.entity.Mission; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MissionRepository extends JpaRepository { +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/service/MissionService.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/service/MissionService.java new file mode 100644 index 0000000..ceb83d9 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/mission/service/MissionService.java @@ -0,0 +1,40 @@ +package com.example.chapter4_umc.domain.mission.service; + +import com.example.chapter4_umc.domain.member.entity.Member; +import com.example.chapter4_umc.domain.member.repository.MemberRepository; +import com.example.chapter4_umc.domain.mission.dto.MissionJoinResDTO; +import com.example.chapter4_umc.domain.mission.entity.Mission; +import com.example.chapter4_umc.domain.mission.entity.MissionProgress; +import com.example.chapter4_umc.domain.mission.repository.MissionProgressRepository; +import com.example.chapter4_umc.domain.mission.repository.MissionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MissionService { + + private final MissionRepository missionRepository; + private final MissionProgressRepository missionProgressRepository; + private final MemberRepository memberRepository; + + public MissionJoinResDTO joinMission(Long missionId, Long memberId) { + + Mission mission = missionRepository.findById(missionId).orElseThrow(); + Member member = memberRepository.findById(memberId).orElseThrow(); + + MissionProgress progress = MissionProgress.builder() + .mission(mission) + .member(member) + .status("도전중") + .build(); + + missionProgressRepository.save(progress); + + return MissionJoinResDTO.builder() + .missionId(missionId) + .memberId(memberId) + .status("도전중") + .build(); + } +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/region/repository/RegionRepository.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/region/repository/RegionRepository.java new file mode 100644 index 0000000..8029a8d --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/region/repository/RegionRepository.java @@ -0,0 +1,7 @@ +package com.example.chapter4_umc.domain.region.repository; + +import com.example.chapter4_umc.domain.region.entity.Region; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RegionRepository extends JpaRepository { +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/controller/MyReviewController.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/controller/MyReviewController.java new file mode 100644 index 0000000..490e4a0 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/controller/MyReviewController.java @@ -0,0 +1,43 @@ +package com.example.chapter4_umc.domain.review.controller; + +import com.example.chapter4_umc.domain.review.dto.ReviewDto; +import com.example.chapter4_umc.domain.review.dto.ReviewSearchCondition; +import com.example.chapter4_umc.domain.review.service.ReviewService; +import com.example.chapter4_umc.global.apiPayload.ApiResponse; +import com.example.chapter4_umc.global.apiPayload.code.GeneralSuccessCode; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/reviews") +@RequiredArgsConstructor +public class MyReviewController { + + private final ReviewService reviewService; + + // 로그인된 사용자 ID 가져오기 (임시 하드코딩) + private Long getLoggedInMemberId(){ + return 1L; + } + + @GetMapping("/my") + public ApiResponse> getMyReviews( + @RequestParam(required = false) String storeName, + @RequestParam(required = false) String ratingRange) { + + Long memberId = getLoggedInMemberId(); + + ReviewSearchCondition condition = new ReviewSearchCondition(); + condition.setStoreName(storeName); + condition.setRatingRange(ratingRange); + + List response = reviewService.findMyReviews(memberId, condition); + + return ApiResponse.onSuccess( + GeneralSuccessCode.REVIEW_READ, + response + ); + } +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/controller/ReviewController.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/controller/ReviewController.java new file mode 100644 index 0000000..e69de29 diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/controller/ReviewCreateController.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/controller/ReviewCreateController.java new file mode 100644 index 0000000..62aca1a --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/controller/ReviewCreateController.java @@ -0,0 +1,44 @@ +package com.example.chapter4_umc.domain.review.controller; + +import com.example.chapter4_umc.domain.review.dto.req.ReviewCreateReqDTO; +import com.example.chapter4_umc.domain.review.dto.res.ReviewCreateResDTO; +import com.example.chapter4_umc.domain.review.service.ReviewService; +import com.example.chapter4_umc.global.apiPayload.ApiResponse; +import com.example.chapter4_umc.global.apiPayload.code.GeneralSuccessCode; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1") +@Tag(name = "Review", description = "리뷰 작성 API") +public class ReviewCreateController { + + private final ReviewService reviewService; + + private Long getLoggedInMemberId() { + return 1L; + } + + @Operation( + summary = "가게 리뷰 작성", + description = "특정 가게에 리뷰를 작성합니다." + ) + @PostMapping("/{storeId}/reviews") + public ApiResponse createReview( + @PathVariable Long storeId, + @RequestBody ReviewCreateReqDTO req + ) { + req.setStoreId(storeId); + req.setMemberId(getLoggedInMemberId()); + + ReviewCreateResDTO response = reviewService.createReview(req); + + return ApiResponse.onSuccess( + GeneralSuccessCode.REVIEW_CREATED, + response + ); + } +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/converter/ReviewConverter.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/converter/ReviewConverter.java new file mode 100644 index 0000000..6457861 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/converter/ReviewConverter.java @@ -0,0 +1,50 @@ +package com.example.chapter4_umc.domain.review.converter; + +import com.example.chapter4_umc.domain.review.dto.res.ReviewCreateResDTO; +import com.example.chapter4_umc.domain.review.dto.res.ReviewDetailResDTO; +import com.example.chapter4_umc.domain.review.dto.res.ReviewListResDTO; +import com.example.chapter4_umc.domain.review.dto.res.ReviewSimpleResDTO; +import com.example.chapter4_umc.domain.review.entity.Review; + +import java.util.ArrayList; +import java.util.List; + +public class ReviewConverter { + + // 리뷰 생성 응답 + public static ReviewCreateResDTO toCreateDTO(Review review){ + return ReviewCreateResDTO.builder() + .reviewId(review.getId()) + .message("리뷰가 성공적으로 생성되었습니다.") + .build(); + } + + public static ReviewDetailResDTO toDetailDTO(Review review){ + return ReviewDetailResDTO.builder() + .reviewId(review.getId()) + .rating(review.getRating()) + .content(review.getContent()) + .imageUrl(review.getImageUrl()) + .createdAt(review.getCreatedAt().toString()) + + .memberId(review.getMember().getId()) + .memberName(review.getMember().getNickname()) + + .storeId(review.getStore().getId()) + .storeName(review.getStore().getStoreName()) + + .regionId(review.getRegion().getId()) + .regionName(review.getRegion().getRegionName()) + .build(); + } + + public static ReviewSimpleResDTO toSimpleDTO(Review review){ + return ReviewSimpleResDTO.builder() + .reviewId(review.getId()) + .rating(review.getRating()) + .content(review.getContent()) + .imageUrl(review.getImageUrl()) + .createdAt(review.getCreatedAt().toString()) + .build(); + } +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/ReviewDto.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/ReviewDto.java new file mode 100644 index 0000000..8a22af6 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/ReviewDto.java @@ -0,0 +1,34 @@ +package com.example.chapter4_umc.domain.review.dto; + +public class ReviewDto { + private Long reviewId; + private Integer rating; + private String content; + private String storeName; + private String imageUrl; + + public ReviewDto(Long reviewId, Integer rating, String content, String storeName, String imageUrl) { + this.reviewId = reviewId; + this.rating = rating; + this.content = content; + this.storeName = storeName; + this.imageUrl = imageUrl; + } + + public Long getReviewId() { + return reviewId; + } + public Integer getRating() { + return rating; + } + public String getContent() { + return content; + } + public String getStoreName() { + return storeName; + } + public String getImageUrl() { + return imageUrl; + } + +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/ReviewSearchCondition.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/ReviewSearchCondition.java new file mode 100644 index 0000000..24bbf71 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/ReviewSearchCondition.java @@ -0,0 +1,19 @@ +package com.example.chapter4_umc.domain.review.dto; + +public class ReviewSearchCondition { + private String storeName; + private String ratingRange; + + public String getStoreName() { + return storeName; + } + public void setStoreName(String storeName) { + this.storeName = storeName; + } + public String getRatingRange() { + return ratingRange; + } + public void setRatingRange(String ratingRange) { + this.ratingRange = ratingRange; + } +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/req/ReviewCreateReqDTO.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/req/ReviewCreateReqDTO.java new file mode 100644 index 0000000..be790b3 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/req/ReviewCreateReqDTO.java @@ -0,0 +1,35 @@ +package com.example.chapter4_umc.domain.review.dto.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ReviewCreateReqDTO { + + + @Schema(description = "평점 (1~5)", example = "5") + private Integer rating; + + @Schema(description = "리뷰 내용", example = "음식이 맛있어요") + private String content; + + @Schema(description = "이미지 URL", example = "https://example.com/image.jpg") + private String imageUrl; + + private Long memberId; + + private Long storeId; + + @Schema(description = "지역 ID", example = "2") + private Long regionId; + + public void setMemberId(Long memberId) { + this.memberId = memberId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/res/ReviewCreateResDTO.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/res/ReviewCreateResDTO.java new file mode 100644 index 0000000..117c382 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/res/ReviewCreateResDTO.java @@ -0,0 +1,14 @@ +package com.example.chapter4_umc.domain.review.dto.res; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class ReviewCreateResDTO { + + private Long reviewId; + private String message; +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/res/ReviewDetailResDTO.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/res/ReviewDetailResDTO.java new file mode 100644 index 0000000..c75f136 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/res/ReviewDetailResDTO.java @@ -0,0 +1,28 @@ +package com.example.chapter4_umc.domain.review.dto.res; + +import com.example.chapter4_umc.domain.review.entity.Review; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class ReviewDetailResDTO { + + private Long reviewId; + private Integer rating; + private String content; + private String imageUrl; + private String createdAt; + + private Long memberId; + private String memberName; + + private Long storeId; + private String storeName; + + private Long regionId; + private String regionName; + +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/res/ReviewListResDTO.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/res/ReviewListResDTO.java new file mode 100644 index 0000000..acda2d8 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/res/ReviewListResDTO.java @@ -0,0 +1,15 @@ +package com.example.chapter4_umc.domain.review.dto.res; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +@AllArgsConstructor +public class ReviewListResDTO { + private List reviews; + private Integer totalCount; +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/res/ReviewSimpleResDTO.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/res/ReviewSimpleResDTO.java new file mode 100644 index 0000000..82fe8cf --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/dto/res/ReviewSimpleResDTO.java @@ -0,0 +1,18 @@ +package com.example.chapter4_umc.domain.review.dto.res; + +import com.example.chapter4_umc.domain.review.entity.Review; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class ReviewSimpleResDTO { + + private Long reviewId; + private Integer rating; + private String content; + private String imageUrl; + private String createdAt; +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/repository/ReviewRepository.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/repository/ReviewRepository.java new file mode 100644 index 0000000..947959a --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/repository/ReviewRepository.java @@ -0,0 +1,7 @@ +package com.example.chapter4_umc.domain.review.repository; + +import com.example.chapter4_umc.domain.review.entity.Review; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReviewRepository extends JpaRepository { +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/repository/ReviewRepositoryCustom.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/repository/ReviewRepositoryCustom.java new file mode 100644 index 0000000..385134c --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/repository/ReviewRepositoryCustom.java @@ -0,0 +1,11 @@ +package com.example.chapter4_umc.domain.review.repository; + +import com.example.chapter4_umc.domain.review.dto.ReviewDto; +import com.example.chapter4_umc.domain.review.dto.ReviewSearchCondition; +import com.example.chapter4_umc.domain.review.entity.Review; + +import java.util.List; +public interface ReviewRepositoryCustom { + List findMyReviewsWithFilter(Long memberId, ReviewSearchCondition condition); +} + diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/repository/ReviewRepositoryCustomlmpI.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/repository/ReviewRepositoryCustomlmpI.java new file mode 100644 index 0000000..13a2051 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/repository/ReviewRepositoryCustomlmpI.java @@ -0,0 +1,68 @@ +package com.example.chapter4_umc.domain.review.repository; + +import com.example.chapter4_umc.domain.review.dto.ReviewDto; +import com.example.chapter4_umc.domain.review.dto.ReviewSearchCondition; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static com.example.chapter4_umc.domain.review.entity.QReview.review; +import static com.example.chapter4_umc.domain.store.entity.QStore.store; + +@Repository +@RequiredArgsConstructor +public class ReviewRepositoryCustomlmpI implements ReviewRepositoryCustom { + private final JPAQueryFactory queryFactory; + + @Override + public List findMyReviewsWithFilter(Long memberId, ReviewSearchCondition condition) { + return queryFactory + .select(Projections.constructor( + ReviewDto.class, + review.id, + review.rating, + review.content, + review.store.storeName, + review.imageUrl + )) + .from(review) + .join(review.store, store) + .where( + memberIdEq(memberId), + storeNameEq(condition.getStoreName()), + ratingEq(condition.getRatingRange()) + ) + .orderBy(review.createdAt.desc()) + .fetch(); + } + // 사용자 ID 필터링 + private BooleanExpression memberIdEq(Long memberId) { + return review.member.id.eq(memberId); + } + + // 가게 이름 필터링 + private BooleanExpression storeNameEq(String storeName) { + return (storeName != null && !storeName.isEmpty()) ? + review.store.storeName.eq(storeName) : null; + } + + // 별점 필터링 + private BooleanExpression ratingEq(String ratingRange) { + if (ratingRange == null || ratingRange.isEmpty()){ + return null; + } + try { + int rating = Integer.parseInt(ratingRange); + if (rating >=1 && rating <= 5 ){ + return review.rating.eq(rating); + } + return null; // 1~5점 범위 밖은 취급하지 않음. + } catch (NumberFormatException e) { + return null; + } + } +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/service/ReviewService.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/service/ReviewService.java new file mode 100644 index 0000000..1e6352d --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/review/service/ReviewService.java @@ -0,0 +1,58 @@ +package com.example.chapter4_umc.domain.review.service; + +import com.example.chapter4_umc.domain.member.entity.Member; +import com.example.chapter4_umc.domain.member.repository.MemberRepository; +import com.example.chapter4_umc.domain.region.entity.Region; +import com.example.chapter4_umc.domain.region.repository.RegionRepository; +import com.example.chapter4_umc.domain.review.converter.ReviewConverter; +import com.example.chapter4_umc.domain.review.dto.ReviewDto; +import com.example.chapter4_umc.domain.review.dto.ReviewSearchCondition; +import com.example.chapter4_umc.domain.review.dto.req.ReviewCreateReqDTO; +import com.example.chapter4_umc.domain.review.dto.res.ReviewCreateResDTO; +import com.example.chapter4_umc.domain.review.entity.Review; +import com.example.chapter4_umc.domain.review.repository.ReviewRepository; +import com.example.chapter4_umc.domain.review.repository.ReviewRepositoryCustom; +import com.example.chapter4_umc.domain.store.entity.Store; +import com.example.chapter4_umc.domain.store.repository.StoreRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ReviewService { + + private final ReviewRepository reviewRepository; + private final MemberRepository memberRepository; + private final StoreRepository storeRepository; + private final RegionRepository regionRepository; + private final ReviewRepositoryCustom reviewRepositoryCustom; + + @Transactional + public ReviewCreateResDTO createReview(ReviewCreateReqDTO req) { + + Member member = memberRepository.findById(req.getMemberId()).orElseThrow(); + Store store = storeRepository.findById(req.getStoreId()).orElseThrow(); + Region region = regionRepository.findById(req.getRegionId()).orElseThrow(); + + Review review = Review.builder() + .rating(req.getRating()) + .content(req.getContent()) + .imageUrl(req.getImageUrl()) + .member(member) + .store(store) + .region(region) + .build(); + + reviewRepository.save(review); + + return ReviewConverter.toCreateDTO(review); + } + + public List findMyReviews(Long memberId, ReviewSearchCondition condition) { + return reviewRepositoryCustom.findMyReviewsWithFilter(memberId, condition); + } + +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/store/entity/Store.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/store/entity/Store.java index 08b84ac..343a598 100644 --- a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/store/entity/Store.java +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/store/entity/Store.java @@ -1,5 +1,7 @@ package com.example.chapter4_umc.domain.store.entity; +import com.example.chapter4_umc.domain.mission.entity.Mission; +import com.example.chapter4_umc.domain.review.entity.Review; import com.example.chapter4_umc.domain.region.entity.Region; import jakarta.persistence.*; import lombok.*; diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/store/repository/StoreRepository.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/store/repository/StoreRepository.java new file mode 100644 index 0000000..6974fe2 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/domain/store/repository/StoreRepository.java @@ -0,0 +1,7 @@ +package com.example.chapter4_umc.domain.store.repository; + +import com.example.chapter4_umc.domain.store.entity.Store; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface StoreRepository extends JpaRepository { +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/Chapter4UmcApplication.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/global/Chapter4UmcApplication.java similarity index 88% rename from chapter4_umc/src/main/java/com/example/chapter4_umc/Chapter4UmcApplication.java rename to chapter4_umc/src/main/java/com/example/chapter4_umc/global/Chapter4UmcApplication.java index 018abdc..eb75e9c 100644 --- a/chapter4_umc/src/main/java/com/example/chapter4_umc/Chapter4UmcApplication.java +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/global/Chapter4UmcApplication.java @@ -1,4 +1,4 @@ -package com.example.chapter4_umc; +package com.example.chapter4_umc.global; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/ApiResponse.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/ApiResponse.java new file mode 100644 index 0000000..b390d5f --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/ApiResponse.java @@ -0,0 +1,60 @@ +package com.example.chapter4_umc.global.apiPayload; + +import com.example.chapter4_umc.global.apiPayload.code.BaseErrorCode; +import com.example.chapter4_umc.global.apiPayload.code.BaseSuccessCode; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ApiResponse { + + @JsonProperty("isSuccess") + private final Boolean isSuccess; + + @JsonProperty("code") + private final String code; + + @JsonProperty("message") + private final String message; + + @JsonProperty("result") + private final T result; + + public static ApiResponse onSuccess(BaseSuccessCode code, T result) { + return new ApiResponse<>( + true, + code.getCode(), + code.getMessage(), + result + ); + } + + public static ApiResponse onSuccess(BaseSuccessCode code){ + return new ApiResponse<>( + true, + code.getCode(), + code.getMessage(), + null + ); + } + + public static ApiResponse onFailure(BaseErrorCode code, T result){ + return new ApiResponse<>( + false, + code.getCode(), + code.getMessage(), + result + ); + } + + public static ApiResponse onFailure(BaseErrorCode code) { + return new ApiResponse<>( + false, + code.getCode(), + code.getMessage(), + null + ); + } +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/code/BaseErrorCode.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/code/BaseErrorCode.java new file mode 100644 index 0000000..bc06752 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/code/BaseErrorCode.java @@ -0,0 +1,9 @@ +package com.example.chapter4_umc.global.apiPayload.code; + +import org.springframework.http.HttpStatus; + +public interface BaseErrorCode { + HttpStatus getStatus(); + String getCode(); + String getMessage(); +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/code/BaseSuccessCode.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/code/BaseSuccessCode.java new file mode 100644 index 0000000..61e17a1 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/code/BaseSuccessCode.java @@ -0,0 +1,10 @@ +package com.example.chapter4_umc.global.apiPayload.code; + +import org.springframework.boot.autoconfigure.graphql.GraphQlProperties; +import org.springframework.http.HttpStatus; + +public interface BaseSuccessCode { + HttpStatus getStatus(); + String getCode(); + String getMessage(); +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/code/GeneralErrorCode.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/code/GeneralErrorCode.java new file mode 100644 index 0000000..ce64b7d --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/code/GeneralErrorCode.java @@ -0,0 +1,48 @@ +package com.example.chapter4_umc.global.apiPayload.code; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum GeneralErrorCode implements BaseErrorCode{ + + BAD_REQUEST(HttpStatus.BAD_REQUEST, + "COMMON400_1", + "잘못된 요청입니다."), + + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, + "AUTH401_1", + "인증이 필요합니다."), + + FORBIDDEN(HttpStatus.FORBIDDEN, + "AUTH403_1", + "요청이 거부되었습니다."), + + NOT_FOUND(HttpStatus.NOT_FOUND, + "COMMON404_1", + "요청한 리소스를 찾을 수 없습니다."), + + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, + "COMMON500_1", + "예기치 않은 서버 에러가 발생했습니다."), + + REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, + "REVIEW404_1", + "해당 리뷰를 찾을 수 없습니다."), + + REVIEW_FORBIDEN(HttpStatus.FORBIDDEN, + "REVIEW403_1", + "해당 리뷰에 접근할 권한이 없습니다."), + + REVIEW_BAD_REQUEST(HttpStatus.BAD_REQUEST, + "REVIEW400_1", + "잘못된 리뷰 요청입니다.") + ; + + private final HttpStatus status; + private final String code; + private final String message; + +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/code/GeneralSuccessCode.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/code/GeneralSuccessCode.java new file mode 100644 index 0000000..d623494 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/code/GeneralSuccessCode.java @@ -0,0 +1,51 @@ +package com.example.chapter4_umc.global.apiPayload.code; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum GeneralSuccessCode implements BaseSuccessCode { + + OK(HttpStatus.OK, + "SUCCESS200_1", + "요청이 성공적으로 처리되었습니다."), + + CREATED(HttpStatus.CREATED, + "SUCCESS201_1", + "리소스가 성공적으로 생성되었습니다."), + + UPDATED(HttpStatus.OK, + "SUCCESS200_2", + "리소스가 성공적으로 수정되었습니다."), + + DELETED(HttpStatus.OK, + "SUCCESS200_3", + "리소스가 성공적으로 삭제되었습니다."), + + REVIEW_READ(HttpStatus.OK, + "REVIEW200_1", + "리뷰 조회 성공"), + + REVIEW_CREATED(HttpStatus.CREATED, + "REVIEW201_1", + "리뷰가 성공적으로 생성되었습니다."), + + REVIEW_UPDATED(HttpStatus.OK, + "REVIEW200_2", + "리뷰 수정 성공"), + + REVIEW_DELETED(HttpStatus.OK, + "REVIEW200_3", + "리뷰 삭제 성공"), + + MISSION_JOIN(HttpStatus.OK, + "MISSION200_1", + "미션 도전 성공") + ; + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/exception/GeneralException.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/exception/GeneralException.java new file mode 100644 index 0000000..667fa37 --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/exception/GeneralException.java @@ -0,0 +1,15 @@ +package com.example.chapter4_umc.global.apiPayload.exception; + +import com.example.chapter4_umc.global.apiPayload.code.BaseErrorCode; +import lombok.Getter; + +@Getter +public class GeneralException extends RuntimeException{ + + private final BaseErrorCode code; + + public GeneralException(BaseErrorCode code) { + super(code.getMessage()); + this.code = code; + } +} diff --git a/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/handler/GeneralExceptionAdvice.java b/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/handler/GeneralExceptionAdvice.java new file mode 100644 index 0000000..261a04a --- /dev/null +++ b/chapter4_umc/src/main/java/com/example/chapter4_umc/global/apiPayload/handler/GeneralExceptionAdvice.java @@ -0,0 +1,39 @@ +package com.example.chapter4_umc.global.apiPayload.handler; + +import com.example.chapter4_umc.global.apiPayload.ApiResponse; +import com.example.chapter4_umc.global.apiPayload.code.BaseErrorCode; +import com.example.chapter4_umc.global.apiPayload.code.GeneralErrorCode; +import com.example.chapter4_umc.global.apiPayload.exception.GeneralException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GeneralExceptionAdvice { + + @ExceptionHandler(GeneralException.class) + public ResponseEntity> handleException ( + GeneralException ex + ) { + return ResponseEntity.status(ex.getCode().getStatus()) + .body(ApiResponse.onFailure( + ex.getCode(), + null + ) + ); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException ( + Exception ex + ) { + + BaseErrorCode code = GeneralErrorCode.INTERNAL_SERVER_ERROR; + return ResponseEntity.status(code.getStatus()) + .body(ApiResponse.onFailure( + code, + null + ) + ); + } +}