diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml new file mode 100644 index 00000000..4e8b8de5 --- /dev/null +++ b/.github/workflows/ci-test.yml @@ -0,0 +1,48 @@ +name: Run Tests on PR + +on: + pull_request: + types: [ opened, synchronize, reopened ] + +permissions: + contents: write + pull-requests: write + checks: write + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'adopt' + + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + + - name: Run tests + run: ./gradlew test + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: 테스트 결과를 PR에 코멘트로 등록합니다 + uses: EnricoMi/publish-unit-test-result-action@v1 + if: always() + with: + files: '**/build/test-results/test/TEST-*.xml' + github_token: ${{ secrets.GITHUB_TOKEN }} + seconds_between_github_reads: 5 + seconds_between_github_writes: 5 diff --git a/.github/workflows/deploy-ecs.yml b/.github/workflows/deploy-ecs.yml index 5b04a274..233e8472 100644 --- a/.github/workflows/deploy-ecs.yml +++ b/.github/workflows/deploy-ecs.yml @@ -13,7 +13,7 @@ env: ECR_REPOSITORY: poomasi-server ECS_SERVICE: poomasi-server ECS_CLUSTER: poomasi - ECS_TASK_DEFINITION: tf-staging.json + ECS_TASK_DEFINITION: tf-prod.json CONTAINER_NAME: spring PROGRESS_SLACK_CHANNEL: C080DMAE7MX permissions: @@ -64,10 +64,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Set up JDK 22 + - name: Set up JDK 21 uses: actions/setup-java@v3 with: - java-version: '22' + java-version: '21' distribution: 'adopt' - name: Grant execute permission for gradlew diff --git a/src/main/java/poomasi/domain/auth/config/SecurityBeanGenerator.java b/src/main/java/poomasi/domain/auth/config/SecurityBeanGenerator.java index 2a067e4d..2aa3d5d6 100644 --- a/src/main/java/poomasi/domain/auth/config/SecurityBeanGenerator.java +++ b/src/main/java/poomasi/domain/auth/config/SecurityBeanGenerator.java @@ -21,9 +21,6 @@ @Configuration public class SecurityBeanGenerator { - private final TokenStorageService tokenStorageService; - private final MemberService memberService; - private final TokenBlacklistService tokenBlacklistService; @Bean @Description("AuthenticationProvider를 위한 Spring bean") @@ -37,11 +34,5 @@ MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) { return new MvcRequestMatcher.Builder(introspector); } - @Bean - JwtUtil jwtUtil(){ - return new JwtUtil(tokenBlacklistService, - tokenStorageService, - memberService); - } } diff --git a/src/main/java/poomasi/domain/auth/config/SecurityConfig.java b/src/main/java/poomasi/domain/auth/config/SecurityConfig.java index 5626074e..14acee13 100644 --- a/src/main/java/poomasi/domain/auth/config/SecurityConfig.java +++ b/src/main/java/poomasi/domain/auth/config/SecurityConfig.java @@ -77,6 +77,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers(HttpMethod.GET, "/api/product/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/review/**").permitAll() .requestMatchers(HttpMethod.GET, "/health").permitAll() + .requestMatchers(HttpMethod.GET, "/api/image/**").permitAll() .requestMatchers("/api/sign-up", "/api/login", "api/reissue", "api/payment/**", "api/order/**", "api/reservation/**", "/api/v1/farmer/reservations").permitAll() .requestMatchers("/api/need-auth/**").authenticated() .anyRequest(). diff --git a/src/main/java/poomasi/domain/auth/security/userdetail/OAuth2UserDetailServiceImpl.java b/src/main/java/poomasi/domain/auth/security/userdetail/OAuth2UserDetailServiceImpl.java index 78dda49a..a595b326 100644 --- a/src/main/java/poomasi/domain/auth/security/userdetail/OAuth2UserDetailServiceImpl.java +++ b/src/main/java/poomasi/domain/auth/security/userdetail/OAuth2UserDetailServiceImpl.java @@ -11,7 +11,7 @@ import poomasi.domain.auth.security.oauth2.dto.response.OAuth2Response; import poomasi.domain.member.entity.LoginType; import poomasi.domain.member.entity.Member; -import poomasi.domain.member.entity.MemberProfile; +import poomasi.domain.member._profile.entity.MemberProfile; import poomasi.domain.member.entity.Role; import poomasi.domain.member.repository.MemberRepository; @@ -52,7 +52,7 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic //일단 없으면 가입시키는 쪽으로 구현ㄴ - Member member = memberRepository.findByEmail(email).orElse(null); + Member member = memberRepository.findByEmailAndDeletedAtIsNull(email).orElse(null); if(member == null) { member = Member.builder() .email(email) diff --git a/src/main/java/poomasi/domain/auth/security/userdetail/UserDetailsServiceImpl.java b/src/main/java/poomasi/domain/auth/security/userdetail/UserDetailsServiceImpl.java index e14131fa..86e2ff7b 100644 --- a/src/main/java/poomasi/domain/auth/security/userdetail/UserDetailsServiceImpl.java +++ b/src/main/java/poomasi/domain/auth/security/userdetail/UserDetailsServiceImpl.java @@ -19,7 +19,7 @@ public UserDetailsServiceImpl(MemberRepository memberRepository) { @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - Member member = memberRepository.findByEmail(email) + Member member = memberRepository.findByEmailAndDeletedAtIsNull(email) .orElseThrow(() -> new BusinessException(BusinessError.MEMBER_NOT_FOUND)); return new UserDetailsImpl(member); } diff --git a/src/main/java/poomasi/domain/auth/signup/controller/SignupController.java b/src/main/java/poomasi/domain/auth/signup/controller/SignupController.java deleted file mode 100644 index 3be51a8c..00000000 --- a/src/main/java/poomasi/domain/auth/signup/controller/SignupController.java +++ /dev/null @@ -1,25 +0,0 @@ -package poomasi.domain.auth.signup.controller; - -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import poomasi.domain.auth.signup.dto.response.SignUpResponse; -import poomasi.domain.auth.signup.service.SignupService; -import poomasi.domain.auth.signup.dto.request.SignupRequest; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/api") -public class SignupController { - - private final SignupService signupService; - - @PostMapping("/sign-up") - public ResponseEntity signUp(@RequestBody SignupRequest signupRequest) { - return ResponseEntity.ok(signupService - .signUp(signupRequest)); - } - -} - - diff --git a/src/main/java/poomasi/domain/auth/signup/dto/request/SignupRequest.java b/src/main/java/poomasi/domain/auth/signup/dto/request/SignupRequest.java deleted file mode 100644 index 7799d74a..00000000 --- a/src/main/java/poomasi/domain/auth/signup/dto/request/SignupRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package poomasi.domain.auth.signup.dto.request; - -public record SignupRequest(String email, String password) { -} diff --git a/src/main/java/poomasi/domain/auth/signup/dto/response/SignUpResponse.java b/src/main/java/poomasi/domain/auth/signup/dto/response/SignUpResponse.java deleted file mode 100644 index 70da0d1c..00000000 --- a/src/main/java/poomasi/domain/auth/signup/dto/response/SignUpResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package poomasi.domain.auth.signup.dto.response; - -public record SignUpResponse(String email, String message) { -} diff --git a/src/main/java/poomasi/domain/auth/signup/service/SignupService.java b/src/main/java/poomasi/domain/auth/signup/service/SignupService.java deleted file mode 100644 index fe8c0859..00000000 --- a/src/main/java/poomasi/domain/auth/signup/service/SignupService.java +++ /dev/null @@ -1,44 +0,0 @@ -package poomasi.domain.auth.signup.service; - -import jdk.jfr.Description; -import lombok.RequiredArgsConstructor; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import poomasi.domain.auth.signup.dto.request.SignupRequest; -import poomasi.domain.auth.signup.dto.response.SignUpResponse; -import poomasi.domain.member.entity.LoginType; -import poomasi.domain.member.repository.MemberRepository; -import poomasi.domain.member.entity.Member; -import poomasi.global.error.BusinessException; - -import static poomasi.domain.member.entity.Role.ROLE_CUSTOMER; -import static poomasi.global.error.BusinessError.*; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class SignupService { - - private final MemberRepository memberRepository; - private final PasswordEncoder passwordEncoder; - - @Description("카카오톡으로 먼저 회원가입이 되어 있는 경우, 계정 연동을 진행합니다. ") - @Transactional - public SignUpResponse signUp(SignupRequest signupRequest) { - String email = signupRequest.email(); - String password = signupRequest.password(); - - memberRepository.findByEmail(email) - .ifPresent(member -> { throw new BusinessException(DUPLICATE_MEMBER_EMAIL); }); - - Member newMember = new Member(email, - passwordEncoder.encode(password), - LoginType.LOCAL, - ROLE_CUSTOMER); - - memberRepository.save(newMember); - return new SignUpResponse(email, "회원 가입 성공"); - } -} - diff --git a/src/main/java/poomasi/domain/auth/token/refreshtoken/service/RefreshTokenService.java b/src/main/java/poomasi/domain/auth/token/refreshtoken/service/RefreshTokenService.java index c7111a58..5673353a 100644 --- a/src/main/java/poomasi/domain/auth/token/refreshtoken/service/RefreshTokenService.java +++ b/src/main/java/poomasi/domain/auth/token/refreshtoken/service/RefreshTokenService.java @@ -1,7 +1,6 @@ package poomasi.domain.auth.token.refreshtoken.service; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/poomasi/domain/auth/token/reissue/controller/ReissueTokenController.java b/src/main/java/poomasi/domain/auth/token/reissue/controller/ReissueTokenController.java index fc2dc627..24f80ba0 100644 --- a/src/main/java/poomasi/domain/auth/token/reissue/controller/ReissueTokenController.java +++ b/src/main/java/poomasi/domain/auth/token/reissue/controller/ReissueTokenController.java @@ -1,23 +1,29 @@ package poomasi.domain.auth.token.reissue.controller; - -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import poomasi.domain.auth.token.reissue.dto.ReissueRequest; import poomasi.domain.auth.token.reissue.dto.ReissueResponse; import poomasi.domain.auth.token.reissue.service.ReissueTokenService; @RestController +@RequiredArgsConstructor public class ReissueTokenController { - @Autowired - private ReissueTokenService reissueTokenService; + private final ReissueTokenService reissueTokenService; + + @PostMapping("/api/reissue") + public ResponseEntity reissue(@RequestHeader(HttpHeaders.AUTHORIZATION) String authorizationHeader, + @RequestBody ReissueRequest reissueRequest){ - @GetMapping("/api/reissue") - public ResponseEntity reissue(@RequestBody ReissueRequest reissueRequest){ - return ResponseEntity.ok(reissueTokenService.reissueToken(reissueRequest)); + String accessToken = authorizationHeader.replace("Bearer ", ""); + + return ResponseEntity.ok(reissueTokenService.reissueToken(accessToken, reissueRequest)); } + } diff --git a/src/main/java/poomasi/domain/auth/token/reissue/service/ReissueTokenService.java b/src/main/java/poomasi/domain/auth/token/reissue/service/ReissueTokenService.java index bc9884fb..b0b21182 100644 --- a/src/main/java/poomasi/domain/auth/token/reissue/service/ReissueTokenService.java +++ b/src/main/java/poomasi/domain/auth/token/reissue/service/ReissueTokenService.java @@ -18,9 +18,15 @@ public class ReissueTokenService { private final RefreshTokenService refreshTokenService; // 토큰 재발급 - public ReissueResponse reissueToken(ReissueRequest reissueRequest) { + public ReissueResponse reissueToken(String accessToken, ReissueRequest reissueRequest) { + Long memberId = jwtUtil.getIdFromToken(accessToken); + String refreshToken = reissueRequest.refreshToken(); - Long memberId = jwtUtil.getIdFromToken(refreshToken); + Long requestMemberId = jwtUtil.getIdFromToken(refreshToken); + + if (!requestMemberId.equals(memberId)) { + throw new BusinessException(REFRESH_TOKEN_NOT_VALID); + } checkRefreshToken(refreshToken, memberId); diff --git a/src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java b/src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java index 37a5a51a..2a046509 100644 --- a/src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java +++ b/src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java @@ -10,7 +10,7 @@ public record FarmRegisterRequest( Double longitude, String phoneNumber, String description, - Long experiencePrice, + int experiencePrice, Integer maxCapacity, Integer maxReservation ) { diff --git a/src/main/java/poomasi/domain/farm/dto/FarmResponse.java b/src/main/java/poomasi/domain/farm/dto/FarmResponse.java index 632957e3..7b7db5cb 100644 --- a/src/main/java/poomasi/domain/farm/dto/FarmResponse.java +++ b/src/main/java/poomasi/domain/farm/dto/FarmResponse.java @@ -3,15 +3,15 @@ import poomasi.domain.farm.entity.Farm; -public record FarmResponse( // FIXME: 사용자 정보 추가 및 설명/전화번호 추가 - Long id, - String name, - String address, - String addressDetail, - Double latitude, - Double longitude, - String description, - Long experiencePrice +public record FarmResponse( + Long id, + String name, + String address, + String addressDetail, + Double latitude, + Double longitude, + String description, + int experiencePrice ) { public static FarmResponse fromEntity(Farm farm) { return new FarmResponse( diff --git a/src/main/java/poomasi/domain/farm/entity/Farm.java b/src/main/java/poomasi/domain/farm/entity/Farm.java index 340d4be7..22c7b6c1 100644 --- a/src/main/java/poomasi/domain/farm/entity/Farm.java +++ b/src/main/java/poomasi/domain/farm/entity/Farm.java @@ -57,7 +57,7 @@ public class Farm { private FarmStatus status = FarmStatus.OPEN; @Comment("체험 비용") - private Long experiencePrice; + private int experiencePrice; @Comment("팀 최대 인원") private Integer maxCapacity; @@ -85,7 +85,7 @@ public class Farm { private OrderedFarm orderedFarm; @Builder - public Farm(Long id, String name, Long ownerId, String address, String addressDetail, Double latitude, Double longitude, String description, Long experiencePrice, Integer maxCapacity, Integer maxReservation) { + public Farm(Long id, String name, Long ownerId, String address, String addressDetail, Double latitude, Double longitude, String description, int experiencePrice, Integer maxCapacity, Integer maxReservation) { this.id = id; this.name = name; this.ownerId = ownerId; @@ -109,7 +109,7 @@ public Farm updateFarm(FarmUpdateRequest farmUpdateRequest) { return this; } - public void updateExpPrice(Long expPrice) { + public void updateExpPrice(int expPrice) { this.experiencePrice = expPrice; } diff --git a/src/main/java/poomasi/domain/farm/service/FarmFarmerService.java b/src/main/java/poomasi/domain/farm/service/FarmFarmerService.java index bae12aa7..fe12e1b3 100644 --- a/src/main/java/poomasi/domain/farm/service/FarmFarmerService.java +++ b/src/main/java/poomasi/domain/farm/service/FarmFarmerService.java @@ -40,7 +40,7 @@ private Farm getFarmByFarmId(Long farmId) { return farmRepository.findByIdAndDeletedAtIsNull(farmId).orElseThrow(() -> new BusinessException(FARM_NOT_FOUND)); } - public void updateFarmExpPrice(Long farmerId, Long farmId, Long expPrice) { + public void updateFarmExpPrice(Long farmerId, Long farmId, int expPrice) { Farm farm = this.getFarmByFarmId(farmId); if (!farm.getOwnerId().equals(farmerId)) { throw new BusinessException(FARM_OWNER_MISMATCH); diff --git a/src/main/java/poomasi/domain/image/controller/ImageController.java b/src/main/java/poomasi/domain/image/controller/ImageController.java index 38c2f524..26000941 100644 --- a/src/main/java/poomasi/domain/image/controller/ImageController.java +++ b/src/main/java/poomasi/domain/image/controller/ImageController.java @@ -2,11 +2,15 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; import poomasi.domain.image.dto.ImageRequest; import poomasi.domain.image.entity.Image; import poomasi.domain.image.entity.ImageType; import poomasi.domain.image.service.ImageService; +import poomasi.domain.member.entity.Member; import java.util.List; @@ -18,22 +22,28 @@ public class ImageController { // 이미지 정보 저장 @PostMapping - public ResponseEntity saveImageInfo(@RequestBody ImageRequest imageRequest) { - Image savedImage = imageService.saveImage(imageRequest); - return ResponseEntity.ok(savedImage); + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER", "ROLE_ADMIN"}) + public ResponseEntity saveImageInfo(@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestBody ImageRequest imageRequest) { + Member member = userDetails.getMember(); + Image savedImage = imageService.saveImage(member.getId(), imageRequest); + return ResponseEntity.ok(savedImage); } // 여러 이미지 정보 저장 @PostMapping("/multiple") - public ResponseEntity> saveMultipleImages(@RequestBody List imageRequests) { - List savedImages = imageService.saveMultipleImages(imageRequests); + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER", "ROLE_ADMIN"}) + public ResponseEntity> saveMultipleImages(@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestBody List imageRequests) { + Member member = userDetails.getMember(); + List savedImages = imageService.saveMultipleImages(member.getId(), imageRequests); return ResponseEntity.ok(savedImages); } // 특정 이미지 삭제 @DeleteMapping("/delete/{id}") - public ResponseEntity deleteImage(@PathVariable Long id) { - imageService.deleteImage(id); + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER", "ROLE_ADMIN"}) + public ResponseEntity deleteImage(@AuthenticationPrincipal UserDetailsImpl userDetails, @PathVariable Long id) { + Member member = userDetails.getMember(); + imageService.deleteImage(member.getId(), id); return ResponseEntity.noContent().build(); } @@ -51,15 +61,21 @@ public ResponseEntity> getImagesByTypeAndReference(@PathVariable Ima } // 이미지 정보 수정 - @PutMapping("/{id}") - public ResponseEntity updateImageInfo(@PathVariable Long id, @RequestBody ImageRequest imageRequest) { - Image updatedImage = imageService.updateImage(id, imageRequest); + @PutMapping("update/{id}") + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER", "ROLE_ADMIN"}) + public ResponseEntity updateImageInfo(@AuthenticationPrincipal UserDetailsImpl userDetails, + @PathVariable Long id, + @RequestBody ImageRequest imageRequest) { + Member member = userDetails.getMember(); + Image updatedImage = imageService.updateImage(member.getId(), id, imageRequest); return ResponseEntity.ok(updatedImage); } @PutMapping("/recover/{id}") - public ResponseEntity recoverImage(@PathVariable Long id) { - imageService.recoverImage(id); + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER", "ROLE_ADMIN"}) + public ResponseEntity recoverImage(@AuthenticationPrincipal UserDetailsImpl userDetails, @PathVariable Long id) { + Member member = userDetails.getMember(); + imageService.recoverImage(member.getId(), id); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/poomasi/domain/image/dto/ImageRequest.java b/src/main/java/poomasi/domain/image/dto/ImageRequest.java index d6e0521f..7589c9a0 100644 --- a/src/main/java/poomasi/domain/image/dto/ImageRequest.java +++ b/src/main/java/poomasi/domain/image/dto/ImageRequest.java @@ -9,7 +9,7 @@ public Image toEntity(ImageRequest imageRequest){ imageRequest.objectKey, imageRequest.imageUrl, imageRequest.type, - imageRequest.referenceId + imageRequest.referenceId // 타입이 멤버 프로필일 경우 멤버 id가 아닌 멤버 프로필 id를 넣습니다. ); } } diff --git a/src/main/java/poomasi/domain/image/entity/ImageType.java b/src/main/java/poomasi/domain/image/entity/ImageType.java index 8c6f5929..1b2b30fd 100644 --- a/src/main/java/poomasi/domain/image/entity/ImageType.java +++ b/src/main/java/poomasi/domain/image/entity/ImageType.java @@ -1,5 +1,5 @@ package poomasi.domain.image.entity; public enum ImageType { - FARM, FARM_REVIEW, PRODUCT, PRODUCT_REVIEW + FARM, FARM_REVIEW, PRODUCT, PRODUCT_REVIEW, MEMBER_PROFILE } \ No newline at end of file diff --git a/src/main/java/poomasi/domain/image/repository/ImageRepository.java b/src/main/java/poomasi/domain/image/repository/ImageRepository.java index 2b82bad1..3c7162b5 100644 --- a/src/main/java/poomasi/domain/image/repository/ImageRepository.java +++ b/src/main/java/poomasi/domain/image/repository/ImageRepository.java @@ -1,12 +1,14 @@ package poomasi.domain.image.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; import poomasi.domain.image.entity.Image; import poomasi.domain.image.entity.ImageType; import java.util.List; import java.util.Optional; +@Repository public interface ImageRepository extends JpaRepository { long countByTypeAndReferenceIdAndDeletedAtIsNull(ImageType type, Long referenceId); List findByTypeAndReferenceIdAndDeletedAtIsNull(ImageType type, Long referenceId); diff --git a/src/main/java/poomasi/domain/image/service/ImageService.java b/src/main/java/poomasi/domain/image/service/ImageService.java index 48157033..cf72c618 100644 --- a/src/main/java/poomasi/domain/image/service/ImageService.java +++ b/src/main/java/poomasi/domain/image/service/ImageService.java @@ -7,6 +7,12 @@ import poomasi.domain.image.entity.Image; import poomasi.domain.image.entity.ImageType; import poomasi.domain.image.repository.ImageRepository; +import poomasi.domain.image.validation.ImageOwnerValidator; +import poomasi.domain.image.validation.ImageOwnerValidatorFactory; +import poomasi.domain.member._profile.entity.MemberProfile; +import poomasi.domain.member._profile.service.MemberProfileService; +import poomasi.domain.member.entity.Member; +import poomasi.domain.member.repository.MemberRepository; import poomasi.global.error.BusinessException; import java.util.Date; @@ -20,20 +26,51 @@ @RequiredArgsConstructor @Transactional(readOnly = true) public class ImageService { + + private static final int DEFAULT_IMAGE_LIMIT = 5; + private static final int MEMBER_PROFILE_IMAGE_LIMIT = 1; + private final ImageRepository imageRepository; + private final ImageOwnerValidatorFactory validatorFactory; + private final MemberRepository memberRepository; + private final MemberProfileService memberProfileService; + @Transactional - public Image saveImage(ImageRequest imageRequest) { + public Image saveImage(Long memberId, ImageRequest imageRequest) { // 기존 이미지가 있는 경우 복구 또는 예외 처리 (실제 복구 로직과는 차이가 있음) + validateImageOwner(memberId, imageRequest.type(), imageRequest.referenceId()); validateImageLimit(imageRequest); Image image = findExistingOrRecoverableImage(imageRequest) .map(existingImage -> recoverImageOrThrow(existingImage, imageRequest)) .orElseGet(() -> imageRequest.toEntity(imageRequest)); + if (imageRequest.type() == ImageType.MEMBER_PROFILE) { + linkImageToMemberProfile(imageRequest.referenceId(), image); + } + return imageRepository.save(image); } + // 이미지 주인이 맞는지 검증 + private void validateImageOwner(Long memberId, ImageType type, Long referenceId) { + // 관리자는 제외 + if (isAdmin(memberId)) { + return; + } + ImageOwnerValidator validator = validatorFactory.getValidator(type); + if (validator != null && !validator.validateOwner(memberId, referenceId)) { + throw new BusinessException(IMAGE_OWNER_MISMATCH); + } + } + + private boolean isAdmin(Long memberId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new BusinessException(MEMBER_NOT_FOUND)); + return member.isAdmin(); + } + private Optional findExistingOrRecoverableImage(ImageRequest imageRequest) { return imageRepository.findByObjectKeyAndTypeAndReferenceId( imageRequest.objectKey(), imageRequest.type(), imageRequest.referenceId()); @@ -50,21 +87,34 @@ private Image recoverImageOrThrow(Image existingImage, ImageRequest imageRequest } private void validateImageLimit(ImageRequest imageRequest) { - if (imageRepository.countByTypeAndReferenceIdAndDeletedAtIsNull(imageRequest.type(), imageRequest.referenceId()) >= 5) { + int imageLimit = DEFAULT_IMAGE_LIMIT; + if (imageRequest.type() == ImageType.MEMBER_PROFILE) { + imageLimit = MEMBER_PROFILE_IMAGE_LIMIT; // 멤버 프로필 이미지는 한 장으로 제한 + } + + if (imageRepository.countByTypeAndReferenceIdAndDeletedAtIsNull(imageRequest.type(), imageRequest.referenceId()) >= imageLimit) { throw new BusinessException(IMAGE_LIMIT_EXCEED); } } + private void linkImageToMemberProfile(Long referenceId, Image savedImage) { + MemberProfile memberProfile = memberProfileService.getMemberProfileById(referenceId); + memberProfile.setProfileImage(savedImage); + memberProfileService.saveMemberProfile(memberProfile); + } + // 여러 이미지 저장 @Transactional - public List saveMultipleImages(List imageRequests) { + public List saveMultipleImages(Long memberId, List imageRequests) { return imageRequests.stream() - .map(this::saveImage) + .map(imageRequest -> saveImage(memberId, imageRequest)) .collect(Collectors.toList()); } @Transactional - public void deleteImage(Long id) { + public void deleteImage(Long memberId, Long id) { + Image image = getImageById(id); + validateImageOwner(memberId, image.getType(), image.getReferenceId()); imageRepository.deleteById(id); } @@ -79,8 +129,9 @@ public List getImagesByTypeAndReferenceId(ImageType type, Long referenceI // 이미지 수정 @Transactional - public Image updateImage(Long id, ImageRequest imageRequest) { + public Image updateImage(Long memberId, Long id, ImageRequest imageRequest) { Image image = getImageById(id); + validateImageOwner(memberId, image.getType(), image.getReferenceId()); if (!image.getType().equals(imageRequest.type()) || !image.getReferenceId().equals(imageRequest.referenceId())) { @@ -93,9 +144,9 @@ public Image updateImage(Long id, ImageRequest imageRequest) { } @Transactional - public void recoverImage(Long id) { - Image image = imageRepository.findById(id) - .orElseThrow(() -> new BusinessException(IMAGE_NOT_FOUND)); + public void recoverImage(Long memberId, Long id) { + Image image = getImageById(id); + validateImageOwner(memberId, image.getType(), image.getReferenceId()); if (image.getDeletedAt() == null) { throw new BusinessException(IMAGE_ALREADY_EXISTS); @@ -106,4 +157,5 @@ public void recoverImage(Long id) { image.setDeletedAt(null); imageRepository.save(image); } + } \ No newline at end of file diff --git a/src/main/java/poomasi/domain/image/validation/FarmOwnerValidator.java b/src/main/java/poomasi/domain/image/validation/FarmOwnerValidator.java new file mode 100644 index 00000000..ca376272 --- /dev/null +++ b/src/main/java/poomasi/domain/image/validation/FarmOwnerValidator.java @@ -0,0 +1,19 @@ +package poomasi.domain.image.validation; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import poomasi.domain.farm.repository.FarmRepository; + + +@Component +@RequiredArgsConstructor +public class FarmOwnerValidator implements ImageOwnerValidator { + private final FarmRepository farmRepository; + + @Override + public boolean validateOwner(Long memberId, Long referenceId) { + return farmRepository.findById(referenceId) + .filter(farm -> farm.getOwnerId().equals(memberId)) + .isPresent(); + } +} diff --git a/src/main/java/poomasi/domain/image/validation/ImageOwnerValidator.java b/src/main/java/poomasi/domain/image/validation/ImageOwnerValidator.java new file mode 100644 index 00000000..8b51556f --- /dev/null +++ b/src/main/java/poomasi/domain/image/validation/ImageOwnerValidator.java @@ -0,0 +1,5 @@ +package poomasi.domain.image.validation; + +public interface ImageOwnerValidator { + boolean validateOwner(Long memberId, Long referenceId); +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/image/validation/ImageOwnerValidatorFactory.java b/src/main/java/poomasi/domain/image/validation/ImageOwnerValidatorFactory.java new file mode 100644 index 00000000..d22141b2 --- /dev/null +++ b/src/main/java/poomasi/domain/image/validation/ImageOwnerValidatorFactory.java @@ -0,0 +1,27 @@ +package poomasi.domain.image.validation; + +import org.springframework.stereotype.Component; +import poomasi.domain.image.entity.ImageType; + +import java.util.EnumMap; +import java.util.Map; + +@Component +public class ImageOwnerValidatorFactory { + private final Map validators = new EnumMap<>(ImageType.class); + + public ImageOwnerValidatorFactory(FarmOwnerValidator farmOwnerValidator, + ProductOwnerValidator productOwnerValidator, + ReviewOwnerValidator reviewOwnerValidator, + MemberProfileOwnerValidator memberProfileOwnerValidator) { + validators.put(ImageType.FARM, farmOwnerValidator); + validators.put(ImageType.PRODUCT, productOwnerValidator); + validators.put(ImageType.FARM_REVIEW, reviewOwnerValidator); + validators.put(ImageType.PRODUCT_REVIEW, reviewOwnerValidator); + validators.put(ImageType.MEMBER_PROFILE, memberProfileOwnerValidator); + } + + public ImageOwnerValidator getValidator(ImageType type) { + return validators.get(type); + } +} diff --git a/src/main/java/poomasi/domain/image/validation/MemberProfileOwnerValidator.java b/src/main/java/poomasi/domain/image/validation/MemberProfileOwnerValidator.java new file mode 100644 index 00000000..b597d5b0 --- /dev/null +++ b/src/main/java/poomasi/domain/image/validation/MemberProfileOwnerValidator.java @@ -0,0 +1,18 @@ +package poomasi.domain.image.validation; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import poomasi.domain.member._profile.repository.MemberProfileRepository; + +@Component +@RequiredArgsConstructor +public class MemberProfileOwnerValidator implements ImageOwnerValidator{ + private final MemberProfileRepository memberProfileRepository; + + @Override + public boolean validateOwner(Long memberId, Long referenceId) { + return memberProfileRepository.findById(referenceId) + .filter(memberProfile -> memberProfile.getMember().getId().equals(memberId)) + .isPresent(); + } +} diff --git a/src/main/java/poomasi/domain/image/validation/ProductOwnerValidator.java b/src/main/java/poomasi/domain/image/validation/ProductOwnerValidator.java new file mode 100644 index 00000000..6afc0bfb --- /dev/null +++ b/src/main/java/poomasi/domain/image/validation/ProductOwnerValidator.java @@ -0,0 +1,19 @@ +package poomasi.domain.image.validation; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import poomasi.domain.product.repository.ProductRepository; + +@Component +@RequiredArgsConstructor +public class ProductOwnerValidator implements ImageOwnerValidator{ + private final ProductRepository productRepository; + + @Override + public boolean validateOwner(Long memberId, Long referenceId) { + return productRepository.findById(referenceId) + .filter(product -> product.getFarmerId().equals(memberId)) + .isPresent(); + } +} + diff --git a/src/main/java/poomasi/domain/image/validation/ReviewOwnerValidator.java b/src/main/java/poomasi/domain/image/validation/ReviewOwnerValidator.java new file mode 100644 index 00000000..57073e2d --- /dev/null +++ b/src/main/java/poomasi/domain/image/validation/ReviewOwnerValidator.java @@ -0,0 +1,18 @@ +package poomasi.domain.image.validation; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import poomasi.domain.review.repository.ReviewRepository; + +@Component +@RequiredArgsConstructor +public class ReviewOwnerValidator implements ImageOwnerValidator{ + private final ReviewRepository reviewRepository; + + @Override + public boolean validateOwner(Long memberId, Long referenceId) { + return reviewRepository.findById(referenceId) + .filter(review -> review.getReviewer().getId().equals(memberId)) + .isPresent(); + } +} diff --git a/src/main/java/poomasi/domain/member/_profile/controller/MemberProfileController.java b/src/main/java/poomasi/domain/member/_profile/controller/MemberProfileController.java new file mode 100644 index 00000000..97de1cdf --- /dev/null +++ b/src/main/java/poomasi/domain/member/_profile/controller/MemberProfileController.java @@ -0,0 +1,14 @@ +package poomasi.domain.member._profile.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import poomasi.domain.member._profile.service.MemberProfileService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("api/member/profile") +public class MemberProfileController { + + private final MemberProfileService memberProfileService; +} diff --git a/src/main/java/poomasi/domain/member/_profile/dto/request/MemberProfileRequest.java b/src/main/java/poomasi/domain/member/_profile/dto/request/MemberProfileRequest.java new file mode 100644 index 00000000..3492e58d --- /dev/null +++ b/src/main/java/poomasi/domain/member/_profile/dto/request/MemberProfileRequest.java @@ -0,0 +1,5 @@ +package poomasi.domain.member._profile.dto.request; + + +public record MemberProfileRequest() { +} diff --git a/src/main/java/poomasi/domain/member/_profile/dto/response/CommonProfileResponse.java b/src/main/java/poomasi/domain/member/_profile/dto/response/CommonProfileResponse.java new file mode 100644 index 00000000..cb665655 --- /dev/null +++ b/src/main/java/poomasi/domain/member/_profile/dto/response/CommonProfileResponse.java @@ -0,0 +1,25 @@ +package poomasi.domain.member._profile.dto.response; + +import lombok.AllArgsConstructor; +import poomasi.domain.image.entity.Image; +import poomasi.domain.member._profile.entity.MemberProfile; + +import java.time.LocalDateTime; + +@AllArgsConstructor +public class CommonProfileResponse implements MemberProfileResponse { + String phoneNumber; + boolean isBanned; + LocalDateTime createdAt; + Image profileImage; + + public static CommonProfileResponse fromEntity(MemberProfile profile) { + return new CommonProfileResponse( + profile.getPhoneNumber(), + profile.isBanned(), + profile.getCreatedAt(), + profile.getProfileImage() + ); + } + +} diff --git a/src/main/java/poomasi/domain/member/_profile/dto/response/CustomerProfileResponse.java b/src/main/java/poomasi/domain/member/_profile/dto/response/CustomerProfileResponse.java new file mode 100644 index 00000000..53501e03 --- /dev/null +++ b/src/main/java/poomasi/domain/member/_profile/dto/response/CustomerProfileResponse.java @@ -0,0 +1,28 @@ +package poomasi.domain.member._profile.dto.response; + +import lombok.AllArgsConstructor; +import poomasi.domain.image.entity.Image; +import poomasi.domain.member._profile.entity.CustomerProfile; + +import java.time.LocalDateTime; + +@AllArgsConstructor +public class CustomerProfileResponse implements MemberProfileResponse{ + String phoneNumber; + String address; + String addressDetail; + boolean isBanned; + LocalDateTime createdAt; + Image profileImage; + + public static CustomerProfileResponse fromEntity(CustomerProfile profile) { + return new CustomerProfileResponse( + profile.getPhoneNumber(), + profile.getAddress(), + profile.getAddressDetail(), + profile.isBanned(), + profile.getCreatedAt(), + profile.getProfileImage() + ); + } +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/member/_profile/dto/response/FarmerProfileResponse.java b/src/main/java/poomasi/domain/member/_profile/dto/response/FarmerProfileResponse.java new file mode 100644 index 00000000..53fed2f5 --- /dev/null +++ b/src/main/java/poomasi/domain/member/_profile/dto/response/FarmerProfileResponse.java @@ -0,0 +1,39 @@ +package poomasi.domain.member._profile.dto.response; + +import lombok.AllArgsConstructor; +import poomasi.domain.image.entity.Image; +import poomasi.domain.member._profile.entity.FarmerProfile; + +import java.time.LocalDateTime; +import java.util.List; + +@AllArgsConstructor +public class FarmerProfileResponse implements MemberProfileResponse { + String phoneNumber; + boolean isBanned; + LocalDateTime createdAt; + Image profileImage; + String storeName; + String farmName; + List businessRegistrationNumbers; + String storeAddress; + String storeAddressDetail; + String farmAddress; + String farmAddressDetail; + + public static FarmerProfileResponse fromEntity(FarmerProfile profile) { + return new FarmerProfileResponse( + profile.getPhoneNumber(), + profile.isBanned(), + profile.getCreatedAt(), + profile.getProfileImage(), + profile.getStoreName(), + profile.getFarmName(), + profile.getBusinessRegistrationNumbers(), + profile.getStoreAddress(), + profile.getStoreAddressDetail(), + profile.getFarmAddress(), + profile.getFarmAddressDetail() + ); + } +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/member/_profile/dto/response/MemberProfileResponse.java b/src/main/java/poomasi/domain/member/_profile/dto/response/MemberProfileResponse.java new file mode 100644 index 00000000..b191dda0 --- /dev/null +++ b/src/main/java/poomasi/domain/member/_profile/dto/response/MemberProfileResponse.java @@ -0,0 +1,18 @@ +package poomasi.domain.member._profile.dto.response; + +import poomasi.domain.member._profile.entity.CustomerProfile; +import poomasi.domain.member._profile.entity.FarmerProfile; +import poomasi.domain.member._profile.entity.MemberProfile; + +public interface MemberProfileResponse { + + static MemberProfileResponse fromEntity(MemberProfile profile) { + if (profile instanceof FarmerProfile farmerProfile) { + return FarmerProfileResponse.fromEntity(farmerProfile); + } else if (profile instanceof CustomerProfile customerProfile) { + return CustomerProfileResponse.fromEntity(customerProfile); + } else { + return CommonProfileResponse.fromEntity(profile); + } + } +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/member/_profile/entity/CustomerProfile.java b/src/main/java/poomasi/domain/member/_profile/entity/CustomerProfile.java new file mode 100644 index 00000000..ea7b83ad --- /dev/null +++ b/src/main/java/poomasi/domain/member/_profile/entity/CustomerProfile.java @@ -0,0 +1,23 @@ +package poomasi.domain.member._profile.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import lombok.Getter; + +@Entity +@Getter +@DiscriminatorValue("CUSTOMER") +public class CustomerProfile extends MemberProfile{ + @Column(nullable = true, length = 255) + private String address; + + @Column(nullable = true, length = 255) + private String addressDetail; + + @Column(nullable=true, length=255) + private Long coordinateX; + + @Column(nullable=true, length=255) + private Long coordinateY; +} diff --git a/src/main/java/poomasi/domain/member/_profile/entity/FarmerProfile.java b/src/main/java/poomasi/domain/member/_profile/entity/FarmerProfile.java new file mode 100644 index 00000000..06b946c7 --- /dev/null +++ b/src/main/java/poomasi/domain/member/_profile/entity/FarmerProfile.java @@ -0,0 +1,47 @@ +package poomasi.domain.member._profile.entity; + +import jakarta.persistence.*; +import lombok.Getter; + +import java.util.List; + +@Entity +@Getter +@DiscriminatorValue("FARMER") +public class FarmerProfile extends MemberProfile{ + @Column(nullable = true, length = 100, unique = true) + private String storeName; + + @Column(nullable = true, length = 100) + private String storeAddress; + + @Column(nullable = true, length = 100) + private String storeAddressDetail; + + @Column(nullable=true, length=100) + private Long storeCoordinateX; + + @Column(nullable=true, length=100) + private Long storeCoordinateY; + + @Column(nullable = true, length = 100, unique = true) + private String farmName; + + @Column(nullable = true, length = 100) + private String farmAddress; + + @Column(nullable = true, length = 100) + private String farmAddressDetail; + + @Column(nullable=true, length=100) + private Long farmCoordinateX; + + @Column(nullable=true, length=100) + private Long farmCoordinateY; + + @ElementCollection + @CollectionTable(name = "business_registration_numbers", joinColumns = @JoinColumn(name = "farmer_profile_id")) + @Column(nullable = true, length=255) + private List businessRegistrationNumbers; + +} diff --git a/src/main/java/poomasi/domain/member/_profile/entity/MemberProfile.java b/src/main/java/poomasi/domain/member/_profile/entity/MemberProfile.java new file mode 100644 index 00000000..af2b5849 --- /dev/null +++ b/src/main/java/poomasi/domain/member/_profile/entity/MemberProfile.java @@ -0,0 +1,50 @@ +package poomasi.domain.member._profile.entity; + +import jakarta.persistence.*; +import lombok.*; +import poomasi.domain.image.entity.Image; +import poomasi.domain.member.entity.Member; + +import java.time.LocalDateTime; + +@Getter +@Entity +@Table(name = "member_profile") +@AllArgsConstructor +@Builder +@Inheritance(strategy = InheritanceType.JOINED) +@DiscriminatorColumn(name = "profile_type") +public class MemberProfile { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = true, length = 20) + private String phoneNumber; + + @Column(nullable = false) + @Builder.Default + private boolean isBanned = false; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @Setter + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "profile_image_id") + private Image profileImage; + + @Setter + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", referencedColumnName = "id") + private Member member; + + @PrePersist + public void prePersist() { + this.createdAt = LocalDateTime.now(); + } + + public MemberProfile() { + } +} diff --git a/src/main/java/poomasi/domain/member/_profile/repository/MemberProfileRepository.java b/src/main/java/poomasi/domain/member/_profile/repository/MemberProfileRepository.java new file mode 100644 index 00000000..f59a2490 --- /dev/null +++ b/src/main/java/poomasi/domain/member/_profile/repository/MemberProfileRepository.java @@ -0,0 +1,19 @@ +package poomasi.domain.member._profile.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import poomasi.domain.member._profile.entity.FarmerProfile; +import poomasi.domain.member._profile.entity.MemberProfile; + +import java.util.Optional; + +@Repository +public interface MemberProfileRepository extends JpaRepository { + + // FarmerProfile 타입만 조회 +// @Query("SELECT p FROM FarmerProfile p WHERE p.farmName = :farmName") +// Optional findFarmerByFarmName(@Param("farmName") String farmName); + +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/member/_profile/service/MemberProfileService.java b/src/main/java/poomasi/domain/member/_profile/service/MemberProfileService.java new file mode 100644 index 00000000..3f88b3c4 --- /dev/null +++ b/src/main/java/poomasi/domain/member/_profile/service/MemberProfileService.java @@ -0,0 +1,28 @@ +package poomasi.domain.member._profile.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.member._profile.entity.MemberProfile; +import poomasi.domain.member._profile.repository.MemberProfileRepository; +import poomasi.global.error.BusinessException; + +import static poomasi.global.error.BusinessError.MEMBER_PROFILE_NOT_FOUND; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MemberProfileService { + + private final MemberProfileRepository memberProfileRepository; + + public MemberProfile getMemberProfileById(Long id){ + return memberProfileRepository.findById(id) + .orElseThrow(() -> new BusinessException(MEMBER_PROFILE_NOT_FOUND)); + } + + @Transactional + public MemberProfile saveMemberProfile(MemberProfile memberProfile){ + return memberProfileRepository.save(memberProfile); + } +} diff --git a/src/main/java/poomasi/domain/member/controller/MemberController.java b/src/main/java/poomasi/domain/member/controller/MemberController.java index 0f1cd7a4..2ee754f3 100644 --- a/src/main/java/poomasi/domain/member/controller/MemberController.java +++ b/src/main/java/poomasi/domain/member/controller/MemberController.java @@ -5,10 +5,17 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; import poomasi.domain.member.dto.request.FarmerQualificationRequest; import poomasi.domain.member.dto.response.MemberResponse; +import poomasi.domain.member.dto.response.MemberSummaryResponse; +import poomasi.domain.member.entity.Member; import poomasi.domain.member.service.MemberService; +import poomasi.domain.member.dto.request.SignupRequest; +import poomasi.domain.member.dto.response.SignUpResponse; @RestController @RequiredArgsConstructor @@ -17,29 +24,55 @@ public class MemberController { private final MemberService memberService; - @PutMapping("/toFarmer/{memberId}") - public ResponseEntity convertToFarmer(@PathVariable Long memberId, + @PostMapping("/sign-up") + public ResponseEntity signUp(@RequestBody SignupRequest signupRequest) { + return ResponseEntity.ok(memberService + .signUp(signupRequest)); + } + + @PutMapping("/toFarmer") + @Secured("ROLE_CUSTOMER") + public ResponseEntity convertToFarmer(@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestBody FarmerQualificationRequest request) { - memberService.convertToFarmer(memberId, request.hasFarmerQualification()); + Member member = userDetails.getMember(); + memberService.convertToFarmer(member, request.hasFarmerQualification()); return ResponseEntity.noContent().build(); } @PutMapping("/toCustomer/{memberId}") + @Secured("ROLE_ADMIN") public ResponseEntity convertToCustomer(@PathVariable Long memberId) { memberService.convertToCustomer(memberId); return ResponseEntity.noContent().build(); } @GetMapping("/{memberId}") + @Secured("ROLE_ADMIN") public ResponseEntity getMemberById(@PathVariable Long memberId) { MemberResponse memberResponse = memberService.getMemberById(memberId); return ResponseEntity.ok(memberResponse); } - @GetMapping - public ResponseEntity> getMembers(@PageableDefault(size = 10) Pageable pageable) { - Page memberResponses = memberService.getAllMembers(pageable); - return ResponseEntity.ok(memberResponses); + @GetMapping("/summary") + @Secured("ROLE_ADMIN") + public ResponseEntity> getMembersSummary(@PageableDefault(size = 10) Pageable pageable) { + Page memberSummaryResponses = memberService.getAllMembersSummary(pageable); + return ResponseEntity.ok(memberSummaryResponses); + } + + @GetMapping("/self") + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER", "ROLE_ADMIN"}) + public ResponseEntity getSelfMember(@AuthenticationPrincipal UserDetailsImpl userDetails) { + Member member = userDetails.getMember(); + MemberResponse memberResponse = memberService.getMemberById(member.getId()); + return ResponseEntity.ok(memberResponse); + } + + @GetMapping("/summary/{memberId}") + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER", "ROLE_ADMIN"}) + public ResponseEntity getMemberSummaryById(@PathVariable Long memberId) { + MemberSummaryResponse memberSummaryResponse = memberService.getMemberSummary(memberId); + return ResponseEntity.ok(memberSummaryResponse); } diff --git a/src/main/java/poomasi/domain/member/dto/request/SignupRequest.java b/src/main/java/poomasi/domain/member/dto/request/SignupRequest.java new file mode 100644 index 00000000..85bbe7ab --- /dev/null +++ b/src/main/java/poomasi/domain/member/dto/request/SignupRequest.java @@ -0,0 +1,4 @@ +package poomasi.domain.member.dto.request; + +public record SignupRequest(String name, String email, String password) { +} diff --git a/src/main/java/poomasi/domain/member/dto/response/MemberProfileResponse.java b/src/main/java/poomasi/domain/member/dto/response/MemberProfileResponse.java deleted file mode 100644 index 9a36d9e7..00000000 --- a/src/main/java/poomasi/domain/member/dto/response/MemberProfileResponse.java +++ /dev/null @@ -1,29 +0,0 @@ -package poomasi.domain.member.dto.response; - -import poomasi.domain.member.entity.MemberProfile; - -import java.time.LocalDateTime; - -public record MemberProfileResponse( - String name, - String phoneNumber, - String address, - String addressDetail, - Long coordinateX, - Long coordinateY, - boolean isBanned, - LocalDateTime createdAt -) { - public static MemberProfileResponse fromEntity(MemberProfile profile) { - return new MemberProfileResponse( - profile.getName(), - profile.getPhoneNumber(), - profile.getAddress(), - profile.getAddressDetail(), - profile.getCoordinateX(), - profile.getCoordinateY(), - profile.isBanned(), - profile.getCreatedAt() - ); - } -} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/member/dto/response/MemberResponse.java b/src/main/java/poomasi/domain/member/dto/response/MemberResponse.java index f19c14c3..98a8c464 100644 --- a/src/main/java/poomasi/domain/member/dto/response/MemberResponse.java +++ b/src/main/java/poomasi/domain/member/dto/response/MemberResponse.java @@ -1,9 +1,11 @@ package poomasi.domain.member.dto.response; +import poomasi.domain.member._profile.dto.response.MemberProfileResponse; import poomasi.domain.member.entity.Member; public record MemberResponse( Long id, + String name, String email, String role, MemberProfileResponse memberProfile @@ -11,6 +13,7 @@ public record MemberResponse( public static MemberResponse fromEntity(Member member) { return new MemberResponse( member.getId(), + member.getName(), member.getEmail(), member.getRole().name(), member.getMemberProfile() != null ? MemberProfileResponse.fromEntity(member.getMemberProfile()) : null diff --git a/src/main/java/poomasi/domain/member/dto/response/MemberSummaryResponse.java b/src/main/java/poomasi/domain/member/dto/response/MemberSummaryResponse.java new file mode 100644 index 00000000..f57c1bfa --- /dev/null +++ b/src/main/java/poomasi/domain/member/dto/response/MemberSummaryResponse.java @@ -0,0 +1,13 @@ +package poomasi.domain.member.dto.response; + +import poomasi.domain.image.entity.Image; +import poomasi.domain.member.entity.Member; + +public record MemberSummaryResponse(String name, Image profileImage) { + public static MemberSummaryResponse fromEntity(Member member) { + return new MemberSummaryResponse( + member.getName(), + member.getMemberProfile().getProfileImage() + ); + } +} diff --git a/src/main/java/poomasi/domain/member/dto/response/SignUpResponse.java b/src/main/java/poomasi/domain/member/dto/response/SignUpResponse.java new file mode 100644 index 00000000..bf4a5ecb --- /dev/null +++ b/src/main/java/poomasi/domain/member/dto/response/SignUpResponse.java @@ -0,0 +1,4 @@ +package poomasi.domain.member.dto.response; + +public record SignUpResponse(String name, String email, String message) { +} diff --git a/src/main/java/poomasi/domain/member/entity/Member.java b/src/main/java/poomasi/domain/member/entity/Member.java index 808de558..2292127a 100644 --- a/src/main/java/poomasi/domain/member/entity/Member.java +++ b/src/main/java/poomasi/domain/member/entity/Member.java @@ -1,16 +1,23 @@ package poomasi.domain.member.entity; import jakarta.persistence.*; +import java.time.LocalDateTime; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.SQLDelete; +import poomasi.domain.store.entity.Store; +import poomasi.domain.member._profile.entity.MemberProfile; +import poomasi.domain.store.entity.Store; import poomasi.domain.order.entity._product.ProductOrder; +import poomasi.domain.store.entity.Store; +import poomasi.domain.member._profile.entity.MemberProfile; +import poomasi.domain.store.entity.Store; import poomasi.domain.wishlist.entity.WishList; - -import java.time.LocalDateTime; -import java.util.List; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; +import java.util.*; @Getter @Entity @@ -23,6 +30,9 @@ public class Member { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = true, length = 50) + private String name; + @Column(unique = true, nullable = true, length = 50) private String email; @@ -48,7 +58,7 @@ public class Member { @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List wishLists; - @Column(name="deleted_at") + @Column(name = "deleted_at") private LocalDateTime deletedAt; @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) @@ -58,7 +68,12 @@ public class Member { @Column(nullable = true) private String farmerTierCode; - public Member(String email, String password, LoginType loginType, Role role) { + @Setter + @OneToOne(mappedBy="owner", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + Store store; + + public Member(String name, String email, String password, LoginType loginType, Role role) { + this.name = name; this.email = email; this.password = password; this.loginType = loginType; @@ -78,7 +93,9 @@ public void setMemberProfile(MemberProfile memberProfile) { } @Builder - public Member(String email, Role role, LoginType loginType, String provideId, MemberProfile memberProfile) { + public Member(Long id, String email, String password, Role role, LoginType loginType, String provideId, MemberProfile memberProfile) { + this.id = id; + this.password = password; this.email = email; this.role = role; this.loginType = loginType; @@ -97,4 +114,11 @@ public boolean isFarmer() { public boolean isAdmin() { return role == Role.ROLE_ADMIN; } + + public Store getStore() { + if(store == null) + throw new BusinessException(BusinessError.STORE_NOT_FOUND); + return store; + } + } diff --git a/src/main/java/poomasi/domain/member/entity/MemberProfile.java b/src/main/java/poomasi/domain/member/entity/MemberProfile.java deleted file mode 100644 index 6a599a71..00000000 --- a/src/main/java/poomasi/domain/member/entity/MemberProfile.java +++ /dev/null @@ -1,61 +0,0 @@ -package poomasi.domain.member.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; - -import java.time.LocalDateTime; - -@Getter -@Entity -@Table(name = "member_profile") -public class MemberProfile { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = true, length = 50) - private String name; - - @Column(nullable = true, length = 20) - private String phoneNumber; - - @Column(nullable = true, length = 255) - private String address; - - @Column(nullable = true, length = 255) - private String addressDetail; - - @Column(nullable=true, length=255) - private Long coordinateX; - - @Column(nullable=true, length=255) - private Long coordinateY; - - @Column(nullable = true, length = 50) - private boolean isBanned; - - @Column(nullable = false) - private LocalDateTime createdAt; - - @Setter - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", referencedColumnName = "id") - private Member member; - - public MemberProfile(String name, String phoneNumber, String address, Member member) { - this.name = name; - this.phoneNumber = phoneNumber; - this.address = address; - this.isBanned = false; - this.createdAt = LocalDateTime.now(); - this.member = member; - } - - public MemberProfile() { - this.name = "UNKNOWN"; // name not null 조건 때문에 임시로 넣었습니다. nullable도 true로 넣었는데 안 되네요 - this.createdAt = LocalDateTime.now(); - } - -} diff --git a/src/main/java/poomasi/domain/member/repository/MemberRepository.java b/src/main/java/poomasi/domain/member/repository/MemberRepository.java index f9bcb1c3..4e3cea7e 100644 --- a/src/main/java/poomasi/domain/member/repository/MemberRepository.java +++ b/src/main/java/poomasi/domain/member/repository/MemberRepository.java @@ -7,6 +7,6 @@ import java.util.Optional; public interface MemberRepository extends JpaRepository { - Optional findByEmail(String email); + Optional findByEmailAndDeletedAtIsNull(String email); Optional findByIdAndDeletedAtIsNull(Long id); -} +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/member/service/MemberService.java b/src/main/java/poomasi/domain/member/service/MemberService.java index 1bcee0a0..c386ee3c 100644 --- a/src/main/java/poomasi/domain/member/service/MemberService.java +++ b/src/main/java/poomasi/domain/member/service/MemberService.java @@ -1,13 +1,19 @@ package poomasi.domain.member.service; +import jdk.jfr.Description; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import poomasi.domain.member.dto.response.MemberResponse; +import poomasi.domain.member.dto.response.MemberSummaryResponse; +import poomasi.domain.member.entity.LoginType; import poomasi.domain.member.entity.Member; import poomasi.domain.member.repository.MemberRepository; +import poomasi.domain.member.dto.request.SignupRequest; +import poomasi.domain.member.dto.response.SignUpResponse; import poomasi.global.error.BusinessException; import static poomasi.domain.member.entity.Role.ROLE_CUSTOMER; @@ -20,22 +26,44 @@ public class MemberService { private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + + @Description("카카오톡으로 먼저 회원가입이 되어 있는 경우, 계정 연동을 진행합니다. ") + @Transactional + public SignUpResponse signUp(SignupRequest signupRequest) { + String name = signupRequest.name(); + String email = signupRequest.email(); + String password = signupRequest.password(); + + memberRepository.findByEmailAndDeletedAtIsNull(email) + .ifPresent(member -> { throw new BusinessException(DUPLICATE_MEMBER_EMAIL); }); + + Member newMember = new Member(name, email, + passwordEncoder.encode(password), + LoginType.LOCAL, + ROLE_CUSTOMER); + + memberRepository.save(newMember); + return new SignUpResponse(name, email, "회원 가입 성공"); + } public MemberResponse getMemberById(Long memberId) { Member member = findMemberById(memberId); return MemberResponse.fromEntity(member); } - public Page getAllMembers(Pageable pageable) { - Page members = memberRepository.findAll(pageable); - return members.map(MemberResponse::fromEntity); + public MemberSummaryResponse getMemberSummary(Long memberId) { + Member member = findMemberById(memberId); + return MemberSummaryResponse.fromEntity(member); } + public Page getAllMembersSummary(Pageable pageable) { + Page members = memberRepository.findAll(pageable); + return members.map(MemberSummaryResponse::fromEntity); + } @Transactional - public void convertToFarmer(Long memberId, Boolean hasFarmerQualification) { - Member member = findMemberById(memberId); - + public void convertToFarmer(Member member, Boolean hasFarmerQualification) { if (member.isFarmer()) { throw new BusinessException(MEMBER_ALREADY_FARMER); } diff --git a/src/main/java/poomasi/domain/product/_cart/entity/Cart.java b/src/main/java/poomasi/domain/product/_cart/entity/Cart.java index bf474106..7a50b0bb 100644 --- a/src/main/java/poomasi/domain/product/_cart/entity/Cart.java +++ b/src/main/java/poomasi/domain/product/_cart/entity/Cart.java @@ -26,7 +26,8 @@ public class Cart { private Integer count; @Builder - public Cart(Long memberId, Long productId, Boolean selected, Integer count) { + public Cart(Long id, Long memberId, Long productId, Boolean selected, Integer count) { + this.id = id; this.memberId = memberId; this.productId = productId; this.selected = selected; diff --git a/src/main/java/poomasi/domain/product/_category/entity/Category.java b/src/main/java/poomasi/domain/product/_category/entity/Category.java index b1957cdb..2e5e2b78 100644 --- a/src/main/java/poomasi/domain/product/_category/entity/Category.java +++ b/src/main/java/poomasi/domain/product/_category/entity/Category.java @@ -9,6 +9,7 @@ import jakarta.persistence.OneToMany; import java.util.ArrayList; import java.util.List; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import poomasi.domain.product._category.dto.CategoryRequest; @@ -28,6 +29,13 @@ public class Category { @JoinColumn(name = "categoryId") List products = new ArrayList<>(); + @Builder + public Category(Long id, String name) { + this.id = id; + this.name = name; + this.products = new ArrayList<>(); + } + public Category(String name) { this.name = name; } diff --git a/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java b/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java index 57828406..ffbae25b 100644 --- a/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java +++ b/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java @@ -1,6 +1,7 @@ package poomasi.domain.product.dto; import poomasi.domain.member.entity.Member; +import poomasi.domain.store.entity.Store; import poomasi.domain.product.entity.Product; public record ProductRegisterRequest( @@ -12,7 +13,7 @@ public record ProductRegisterRequest( Long price ) { - public Product toEntity(Member member) { + public Product toEntity(Member member, Store store) { return Product.builder() .categoryId(categoryId) .farmerId(member.getId()) @@ -22,6 +23,7 @@ public Product toEntity(Member member) { .imageUrl(imageUrl) .stock(stock) .price(price) + .store(store) .build(); } } diff --git a/src/main/java/poomasi/domain/product/dto/ProductResponse.java b/src/main/java/poomasi/domain/product/dto/ProductResponse.java index 6975a2a7..a759c3f4 100644 --- a/src/main/java/poomasi/domain/product/dto/ProductResponse.java +++ b/src/main/java/poomasi/domain/product/dto/ProductResponse.java @@ -14,6 +14,7 @@ public record ProductResponse( String description, String imageUrl, Long categoryId, + String storeName, List tags ) { @@ -27,6 +28,7 @@ public static ProductResponse fromEntity(Product product) { .stock(product.getStock()) .description(product.getDescription()) .imageUrl(product.getImageUrl()) + .storeName(product.getStore().getName()) .categoryId(product.getCategoryId()) .tags(tags) .build(); diff --git a/src/main/java/poomasi/domain/product/entity/Product.java b/src/main/java/poomasi/domain/product/entity/Product.java index 4dbc121c..cd80bfbc 100644 --- a/src/main/java/poomasi/domain/product/entity/Product.java +++ b/src/main/java/poomasi/domain/product/entity/Product.java @@ -1,18 +1,7 @@ package poomasi.domain.product.entity; -import jakarta.persistence.CascadeType; -import jakarta.persistence.CollectionTable; -import jakarta.persistence.Column; -import jakarta.persistence.ElementCollection; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToMany; +import jakarta.persistence.*; + import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -21,9 +10,9 @@ import lombok.NoArgsConstructor; import org.hibernate.annotations.Comment; import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.UpdateTimestamp; import poomasi.domain.order.entity._product.OrderedProduct; +import poomasi.domain.store.entity.Store; import poomasi.domain.product.dto.ProductRegisterRequest; import poomasi.domain.review.entity.Review; @@ -71,6 +60,10 @@ public class Product { @JoinColumn(name = "entityId") List reviewList = new ArrayList<>(); + @ManyToOne + @JoinColumn(name = "store_id") // 외래 키 컬럼 지정 + private Store store; + @ElementCollection(fetch = FetchType.LAZY) @CollectionTable(name = "product_tag", joinColumns = @JoinColumn(name = "product_id")) @Column(name = "enum_value") @@ -93,7 +86,9 @@ public Product(Long productId, String description, String imageUrl, Integer stock, - Long price) { + Long price, + Store store) { + this.id = productId; this.categoryId = categoryId; this.farmerId = farmerId; this.name = name; @@ -101,6 +96,7 @@ public Product(Long productId, this.imageUrl = imageUrl; this.stock = stock; this.price = price; + this.store = store; } public Product modify(ProductRegisterRequest productRegisterRequest) { diff --git a/src/main/java/poomasi/domain/product/service/ProductFarmerService.java b/src/main/java/poomasi/domain/product/service/ProductFarmerService.java index a225cfb7..52140d85 100644 --- a/src/main/java/poomasi/domain/product/service/ProductFarmerService.java +++ b/src/main/java/poomasi/domain/product/service/ProductFarmerService.java @@ -6,6 +6,8 @@ import poomasi.domain.member.entity.Member; import poomasi.domain.product._category.entity.Category; import poomasi.domain.product._category.repository.CategoryRepository; +import poomasi.domain.store.entity.Store; +import poomasi.domain.store.repository.StoreRepository; import poomasi.domain.product.dto.ProductRegisterRequest; import poomasi.domain.product.dto.UpdateProductQuantityRequest; import poomasi.domain.product.entity.Product; @@ -19,12 +21,16 @@ public class ProductFarmerService { private final ProductRepository productRepository; private final CategoryRepository categoryRepository; + private final StoreRepository storeRepository; @Transactional public Long registerProduct(Member member, ProductRegisterRequest request) { Category category = getCategory(request.categoryId()); - Product saveProduct = productRepository.save(request.toEntity(member)); + Store store = member.getStore(); + Product saveProduct = productRepository.save(request.toEntity(member,store)); + category.addProduct(saveProduct); + store.addProduct(saveProduct); return saveProduct.getId(); } @@ -47,7 +53,6 @@ public void modifyProduct(Member member, ProductRegisterRequest productRequest, @Transactional public void deleteProduct(Member member, Long productId) { - //TODO: 주인인지 알아보기 Product product = getProductByProductId(productId); checkAuth(member, product); diff --git a/src/main/java/poomasi/domain/reservation/dto/request/ReservationRequest.java b/src/main/java/poomasi/domain/reservation/dto/request/ReservationRequest.java index 9b2d3b01..412391a8 100644 --- a/src/main/java/poomasi/domain/reservation/dto/request/ReservationRequest.java +++ b/src/main/java/poomasi/domain/reservation/dto/request/ReservationRequest.java @@ -23,6 +23,7 @@ public Reservation toEntity(Member member, Farm farm, FarmSchedule farmSchedule) .reservationDate(farmSchedule.getDate()) .memberCount(memberCount) .request(request) + .price(farm.getExperiencePrice() * memberCount) .status(ReservationStatus.ACCEPTED) .build(); } diff --git a/src/main/java/poomasi/domain/reservation/dto/response/ReservationResponse.java b/src/main/java/poomasi/domain/reservation/dto/response/ReservationResponse.java index 0b397eff..c54c3116 100644 --- a/src/main/java/poomasi/domain/reservation/dto/response/ReservationResponse.java +++ b/src/main/java/poomasi/domain/reservation/dto/response/ReservationResponse.java @@ -6,14 +6,8 @@ import java.time.LocalDate; @Builder -public record ReservationResponse( - Long farmId, - Long memberId, - Long scheduleId, - LocalDate reservationDate, - int memberCount, - ReservationStatus status, - String request +public record ReservationResponse(Long farmId, Long memberId, Long scheduleId, LocalDate reservationDate, + int memberCount, ReservationStatus status, String request, int price ) { } diff --git a/src/main/java/poomasi/domain/reservation/entity/Reservation.java b/src/main/java/poomasi/domain/reservation/entity/Reservation.java index a39ca6b0..6249157e 100644 --- a/src/main/java/poomasi/domain/reservation/entity/Reservation.java +++ b/src/main/java/poomasi/domain/reservation/entity/Reservation.java @@ -60,6 +60,10 @@ public class Reservation { @Column(nullable = false) private String request; + @Comment("결제 예정 금액") + @Column(nullable = false) + private int price; + @CreationTimestamp private LocalDateTime createdAt; @@ -71,7 +75,7 @@ public class Reservation { @Builder - public Reservation(Farm farm, Member member, FarmSchedule scheduleId, LocalDate reservationDate, int memberCount, ReservationStatus status, String request) { + public Reservation(Farm farm, Member member, FarmSchedule scheduleId, LocalDate reservationDate, int memberCount, ReservationStatus status, String request, int price) { this.farm = farm; this.member = member; this.scheduleId = scheduleId; @@ -79,6 +83,7 @@ public Reservation(Farm farm, Member member, FarmSchedule scheduleId, LocalDate this.memberCount = memberCount; this.status = status; this.request = request; + this.price = price; } public ReservationResponse toResponse() { @@ -90,6 +95,7 @@ public ReservationResponse toResponse() { .memberCount(memberCount) .status(status) .request(request) + .price(price) .build(); } diff --git a/src/main/java/poomasi/domain/review/dto/ReviewResponse.java b/src/main/java/poomasi/domain/review/dto/ReviewResponse.java index ba6d42b0..1b6ac560 100644 --- a/src/main/java/poomasi/domain/review/dto/ReviewResponse.java +++ b/src/main/java/poomasi/domain/review/dto/ReviewResponse.java @@ -15,7 +15,7 @@ public static ReviewResponse fromEntity(Review review) { return new ReviewResponse( review.getId(), review.getEntityId(), - review.getReviewer().getMemberProfile().getName(), + review.getReviewer().getName(), review.getRating(), review.getContent() ); diff --git a/src/main/java/poomasi/domain/review/entity/Review.java b/src/main/java/poomasi/domain/review/entity/Review.java index 9eda7960..dda0c932 100644 --- a/src/main/java/poomasi/domain/review/entity/Review.java +++ b/src/main/java/poomasi/domain/review/entity/Review.java @@ -51,8 +51,9 @@ public class Review { private Member reviewer; @Builder - public Review(Float rating, String content, Long entityId, EntityType entityType, + public Review(Long id, Float rating, String content, Long entityId, EntityType entityType, Member reviewer) { + this.id = id; this.rating = rating; this.content = content; this.entityId = entityId; diff --git a/src/main/java/poomasi/domain/store/controller/StoreController.java b/src/main/java/poomasi/domain/store/controller/StoreController.java new file mode 100644 index 00000000..5101e889 --- /dev/null +++ b/src/main/java/poomasi/domain/store/controller/StoreController.java @@ -0,0 +1,41 @@ +package poomasi.domain.store.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import poomasi.domain.store.dto.StoreRegisterRequest; +import poomasi.domain.store.service.StoreService; + +@Controller +@RequiredArgsConstructor +@RequestMapping("/api/store") +public class StoreController { + + private final StoreService storeService; + + @Secured("ROLE_FARMER") + @PostMapping("") + public ResponseEntity addStore(@RequestBody StoreRegisterRequest storeRegisterRequest) { + storeService.addStore(storeRegisterRequest); + return ResponseEntity.ok().build(); + } + + @Secured("ROLE_FARMER") + @GetMapping("") + public ResponseEntity getStore() { + return ResponseEntity.ok(storeService.getStore()); + } + + @Secured("ROLE_FARMER") + @PutMapping("") + public ResponseEntity updateStore(@RequestBody StoreRegisterRequest storeRegisterRequest) { + storeService.updateStore(storeRegisterRequest); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/poomasi/domain/store/dto/StoreFeeRequest.java b/src/main/java/poomasi/domain/store/dto/StoreFeeRequest.java new file mode 100644 index 00000000..57d332cd --- /dev/null +++ b/src/main/java/poomasi/domain/store/dto/StoreFeeRequest.java @@ -0,0 +1,7 @@ +package poomasi.domain.store.dto; + +public record StoreFeeRequest( + Integer fee +) { + +} diff --git a/src/main/java/poomasi/domain/store/dto/StoreRegisterRequest.java b/src/main/java/poomasi/domain/store/dto/StoreRegisterRequest.java new file mode 100644 index 00000000..08e975d3 --- /dev/null +++ b/src/main/java/poomasi/domain/store/dto/StoreRegisterRequest.java @@ -0,0 +1,29 @@ +package poomasi.domain.store.dto; + +import org.hibernate.annotations.Comment; +import poomasi.domain.member.entity.Member; +import poomasi.domain.store.entity.Store; + +public record StoreRegisterRequest( + String name, + String address, + String phone, + String ownerPhone, + @Comment("사업자 번호") + String businessNumber, + @Comment("배송비") + Integer shipingFee +) { + + public Store toEntity(Member member) { + return Store.builder() + .name(name) + .address(address) + .phone(phone) + .ownerPhone(ownerPhone) + .businessNumber(businessNumber) + .shipingFee(shipingFee) + .owner(member) + .build(); + } +} diff --git a/src/main/java/poomasi/domain/store/dto/StoreResponse.java b/src/main/java/poomasi/domain/store/dto/StoreResponse.java new file mode 100644 index 00000000..3a821c63 --- /dev/null +++ b/src/main/java/poomasi/domain/store/dto/StoreResponse.java @@ -0,0 +1,54 @@ +package poomasi.domain.store.dto; + +import jakarta.validation.constraints.Positive; +import java.util.List; +import lombok.Builder; +import org.hibernate.annotations.Comment; +import org.jetbrains.annotations.NotNull; +import poomasi.domain.store.entity.Store; +import poomasi.domain.product.dto.ProductResponse; + +@Builder +public record StoreResponse( + @NotNull + String name, + + @NotNull + String address, + + String phone, + + @NotNull + String ownerPhone, + + @Comment("사업자 번호") + @NotNull + String businessNumber, + + @Comment("배송비") + @NotNull + @Positive + Integer shipingFee, + + @NotNull + String ownerName, + + List products +) { + + public static StoreResponse fromEntity(Store store) { + return StoreResponse.builder() + .name(store.getName()) + .address(store.getAddress()) + .phone(store.getPhone()) + .ownerPhone(store.getOwnerPhone()) + .businessNumber(store.getBusinessNumber()) + .shipingFee(store.getShipingFee()) + //TODO 나중에 삼항연산자 삭제 + .ownerName( store.getOwner().getMemberProfile() == null ? "" + : store.getOwner().getName()) + .products(store.getProducts().stream().map(ProductResponse::fromEntity).toList()) + .build(); + + } +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/store/entity/Store.java b/src/main/java/poomasi/domain/store/entity/Store.java new file mode 100644 index 00000000..c4be3f96 --- /dev/null +++ b/src/main/java/poomasi/domain/store/entity/Store.java @@ -0,0 +1,70 @@ +package poomasi.domain.store.entity; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import java.util.ArrayList; +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; +import poomasi.domain.member.entity.Member; +import poomasi.domain.store.dto.StoreRegisterRequest; +import poomasi.domain.product.entity.Product; + +@Entity +@NoArgsConstructor +@Getter +public class Store { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + private String address; + private String phone; + + @OneToOne(fetch = FetchType.LAZY) + private Member owner; + + private String ownerPhone; + @Comment("사업자 번호") + private String businessNumber; + @Comment("배송비") + private Integer shipingFee; + + @OneToMany(mappedBy = "store", cascade = CascadeType.ALL) + List products = new ArrayList<>(); + + @Builder + public Store(Long id, String name, String address, String phone, Member owner, + String ownerPhone, String businessNumber, Integer shipingFee) { + this.id = id; + this.name = name; + this.address = address; + this.phone = phone; + this.owner = owner; + this.ownerPhone = ownerPhone; + this.businessNumber = businessNumber; + this.shipingFee = shipingFee; + } + + public void updateStore(StoreRegisterRequest storeRegisterRequest) { + this.name = storeRegisterRequest.name(); + this.address = storeRegisterRequest.address(); + this.phone = storeRegisterRequest.phone(); + this.ownerPhone = storeRegisterRequest.ownerPhone(); + this.businessNumber = storeRegisterRequest.businessNumber(); + this.shipingFee = storeRegisterRequest.shipingFee(); + } + + public void addProduct(Product saveProduct) { + this.products.add(saveProduct); + } +} diff --git a/src/main/java/poomasi/domain/store/repository/StoreRepository.java b/src/main/java/poomasi/domain/store/repository/StoreRepository.java new file mode 100644 index 00000000..397b1b58 --- /dev/null +++ b/src/main/java/poomasi/domain/store/repository/StoreRepository.java @@ -0,0 +1,13 @@ +package poomasi.domain.store.repository; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import poomasi.domain.store.entity.Store; + +@Repository +public interface StoreRepository extends JpaRepository { + + //@Query("select s from Store s where s.owner.id = :id") + Optional findByOwnerId(Long id); +} diff --git a/src/main/java/poomasi/domain/store/service/StoreService.java b/src/main/java/poomasi/domain/store/service/StoreService.java new file mode 100644 index 00000000..5920f908 --- /dev/null +++ b/src/main/java/poomasi/domain/store/service/StoreService.java @@ -0,0 +1,56 @@ +package poomasi.domain.store.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member.entity.Member; +import poomasi.domain.store.dto.StoreRegisterRequest; +import poomasi.domain.store.dto.StoreResponse; +import poomasi.domain.store.entity.Store; +import poomasi.domain.store.repository.StoreRepository; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class StoreService { + + private final StoreRepository storeRepository; + + @Transactional + public void addStore(StoreRegisterRequest storeRegisterRequest) { + Member member = getMember(); + Store store = storeRegisterRequest.toEntity(member); + member.setStore(store); + storeRepository.save(store); + } + + public StoreResponse getStore() { + Member member = getMember(); + Store store = getStore(member); + return StoreResponse.fromEntity(store); + } + + private Store getStore(Member member) { + return storeRepository.findByOwnerId(member.getId()) + .orElseThrow(() -> new BusinessException(BusinessError.STORE_NOT_FOUND)); + } + + private Member getMember() { + Authentication authentication = SecurityContextHolder + .getContext().getAuthentication(); + Object impl = authentication.getPrincipal(); + return ((UserDetailsImpl) impl).getMember(); + } + + @Transactional + public void updateStore(StoreRegisterRequest storeRegisterRequest) { + Member member = getMember(); + Store store = getStore(member); + store.updateStore(storeRegisterRequest); + } +} diff --git a/src/main/java/poomasi/global/config/s3/S3PresignedUrlController.java b/src/main/java/poomasi/global/config/s3/S3PresignedUrlController.java index 33b6c157..44f5e52b 100644 --- a/src/main/java/poomasi/global/config/s3/S3PresignedUrlController.java +++ b/src/main/java/poomasi/global/config/s3/S3PresignedUrlController.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.*; import poomasi.global.config.aws.AwsProperties; import poomasi.global.config.s3.dto.request.PresignedUrlPutRequest; @@ -14,12 +15,14 @@ public class S3PresignedUrlController { private final AwsProperties awsProperties; @GetMapping("/presigned-url-get") + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER", "ROLE_ADMIN"}) public ResponseEntity presignedUrlGet(@RequestParam String keyname) { String presignedGetUrl = s3PresignedUrlService.createPresignedGetUrl(awsProperties.getS3().getBucket(), keyname); return ResponseEntity.ok(presignedGetUrl); } @PostMapping("/presigned-url-put") + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER", "ROLE_ADMIN"}) public ResponseEntity presignedUrlPut(@RequestBody PresignedUrlPutRequest request) { String presignedPutUrl = s3PresignedUrlService.createPresignedPutUrl(awsProperties.getS3().getBucket(), request.keyPrefix(), request.metadata()); return ResponseEntity.ok(presignedPutUrl); diff --git a/src/main/java/poomasi/global/config/s3/S3PresignedUrlService.java b/src/main/java/poomasi/global/config/s3/S3PresignedUrlService.java index 12330c8f..241375f3 100644 --- a/src/main/java/poomasi/global/config/s3/S3PresignedUrlService.java +++ b/src/main/java/poomasi/global/config/s3/S3PresignedUrlService.java @@ -52,7 +52,7 @@ public String createPresignedPutUrl(String bucketName, String keyPrefix, Map 그럴일 거의 없긴할텐데 생기면 s3 원래 파일 지워짐 String uniqueIdentifier = UUID.randomUUID().toString(); diff --git a/src/main/java/poomasi/global/error/BusinessError.java b/src/main/java/poomasi/global/error/BusinessError.java index a817f64b..6e5cfa81 100644 --- a/src/main/java/poomasi/global/error/BusinessError.java +++ b/src/main/java/poomasi/global/error/BusinessError.java @@ -4,6 +4,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; +import org.springframework.http.HttpStatus; @Getter @AllArgsConstructor @@ -27,6 +28,9 @@ public enum BusinessError { MEMBER_ALREADY_FARMER(HttpStatus.BAD_REQUEST, "이미 농부인 회원입니다."), MEMBER_ID_MISMATCH(HttpStatus.FORBIDDEN, "권한이 없는 요청입니다."), + // MemberProfile + MEMBER_PROFILE_NOT_FOUND(HttpStatus.NOT_FOUND, "회원 세부 정보가 존재하지 않습니다."), + // Auth INVALID_CREDENTIAL(HttpStatus.UNAUTHORIZED, "잘못된 비밀번호 입니다."), REFRESH_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "리프레시 토큰이 없습니다."), @@ -69,6 +73,7 @@ public enum BusinessError { IMAGE_LIMIT_EXCEED(HttpStatus.BAD_REQUEST, "사진은 최대 5장까지 등록 가능합니다."), IMAGE_ALREADY_EXISTS(HttpStatus.CONFLICT, "이미 이미지가 존재합니다"), IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "이미지를 찾을 수 없습니다."), + IMAGE_OWNER_MISMATCH(HttpStatus.FORBIDDEN, "해당 이미지의 소유자가 아닙니다."), // Order ORDER_NOT_FOUND(HttpStatus.NOT_FOUND, "주문을 찾을 수 없습니다."), @@ -83,11 +88,13 @@ public enum BusinessError { // PAYMENT - PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND , "결제를 찾을 수 없습니다."), + PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "결제를 찾을 수 없습니다."), PAYMENT_AMOUNT_MISMATCH(HttpStatus.BAD_REQUEST, "사전 결제 금액과 사후 결제 금액이 일치하지 않습니다."), PAYMENT_BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못 된 결제 요청입니다."), CHECKSUM_EXCESSIVE_REFUND_AMOUNT(HttpStatus.BAD_REQUEST, "환불 요청 금액이 환불 가능한 금액보다 더 많습니다"), + //Store + STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "등록된 상점이 없습니다."), // After sales SHIPPING_ALREADY_IN_PROGRESS(HttpStatus.BAD_REQUEST, "배송 준비 중이거나 배송 중인 주문입니다."), @@ -105,3 +112,4 @@ public enum BusinessError { private final String message; } + diff --git a/src/test/java/poomasi/domain/farm/service/FarmFarmerServiceTest.java b/src/test/java/poomasi/domain/farm/service/FarmFarmerServiceTest.java new file mode 100644 index 00000000..3fc65918 --- /dev/null +++ b/src/test/java/poomasi/domain/farm/service/FarmFarmerServiceTest.java @@ -0,0 +1,141 @@ +package poomasi.domain.farm.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import poomasi.domain.farm.dto.FarmRegisterRequest; +import poomasi.domain.farm.dto.FarmUpdateRequest; +import poomasi.domain.farm.entity.Farm; +import poomasi.domain.farm.repository.FarmRepository; +import poomasi.domain.member.entity.Member; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class FarmFarmerServiceTest { + + @InjectMocks + private FarmFarmerService farmFarmerService; + + @Mock + private FarmRepository farmRepository; + + @Nested + @DisplayName("농장 등록") + class RegisterFarm { + @Test + @DisplayName("농장이 이미 존재하는 경우 예외를 발생시킨다") + void should_throwException_when_farmAlreadyExists() { + // given + Member member = Member.builder() + .id(1L) + .build(); + Farm existingFarm = Farm.builder() + .id(1L) + .name("Existing Farm") + .ownerId(1L) + .build(); + + given(farmRepository.getFarmByOwnerIdAndDeletedAtIsNull(member.getId())).willReturn(Optional.of(existingFarm)); + + FarmRegisterRequest request = new FarmRegisterRequest("New Farm", "Address", "Detail", 1.0, 1.0, "010-1234-5678", "Description", 10000, 10, 5); + + // when & then + assertThatThrownBy(() -> farmFarmerService.registerFarm(member, request)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.FARM_ALREADY_EXISTS); + } + } + + @Nested + @DisplayName("농장 정보 업데이트") + class UpdateFarm { + @Test + @DisplayName("농장이 존재하지 않는 경우 예외를 발생시킨다") + void should_throwException_when_farmNotExist() { + // given + Long farmId = 1L; + FarmUpdateRequest request = new FarmUpdateRequest(farmId, "Updated Farm", "Description", "Address", "Detail", 1.0, 1.0); + given(farmRepository.findByIdAndDeletedAtIsNull(farmId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> farmFarmerService.updateFarm(1L, request)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.FARM_NOT_FOUND); + } + + @Test + @DisplayName("농장 소유자가 아닌 경우 예외를 발생시킨다") + void should_throwException_when_ownerMismatch() { + // given + Long farmId = 1L; + Long farmerId = 2L; + Farm farm = Farm.builder() + .id(farmId) + .name("Farm") + .ownerId(3L) + .build(); + given(farmRepository.findByIdAndDeletedAtIsNull(farmId)).willReturn(Optional.of(farm)); + + FarmUpdateRequest request = new FarmUpdateRequest(farmId, "Updated Farm", "Description", "Address", "Detail", 1.0, 1.0); + + // when & then + assertThatThrownBy(() -> farmFarmerService.updateFarm(farmerId, request)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.FARM_OWNER_MISMATCH); + } + } + + @Nested + @DisplayName("농장 삭제") + class DeleteFarm { + @Test + @DisplayName("소유자가 아닌 경우 예외를 발생시킨다") + void should_throwException_when_ownerMismatchOnDelete() { + // given + Long farmId = 1L; + Long farmerId = 2L; + Farm farm = Farm.builder() + .id(farmId) + .name("Farm") + .ownerId(3L) + .build(); + given(farmRepository.findByIdAndDeletedAtIsNull(farmId)).willReturn(Optional.of(farm)); + + // when & then + assertThatThrownBy(() -> farmFarmerService.deleteFarm(farmerId, farmId)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.FARM_OWNER_MISMATCH); + } + + @Test + @DisplayName("농장 삭제에 성공한다") + void should_deleteFarm_when_ownerMatches() { + // given + Long farmId = 1L; + Long farmerId = 1L; + Farm farm = Farm.builder() + .id(farmId) + .name("Farm") + .ownerId(farmerId) + .build(); + given(farmRepository.findByIdAndDeletedAtIsNull(farmId)).willReturn(Optional.of(farm)); + + // when + farmFarmerService.deleteFarm(farmerId, farmId); + + // then + verify(farmRepository).delete(farm); + } + } +} diff --git a/src/test/java/poomasi/domain/farm/service/FarmPlatformServiceTest.java b/src/test/java/poomasi/domain/farm/service/FarmPlatformServiceTest.java new file mode 100644 index 00000000..60759722 --- /dev/null +++ b/src/test/java/poomasi/domain/farm/service/FarmPlatformServiceTest.java @@ -0,0 +1,105 @@ +package poomasi.domain.farm.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import poomasi.domain.farm.dto.FarmResponse; +import poomasi.domain.farm.entity.Farm; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class FarmPlatformServiceTest { + + @InjectMocks + private FarmPlatformService farmPlatformService; + + @Mock + private FarmService farmService; + + @Nested + @DisplayName("ID로 농장 가져오기") + class GetFarmByFarmId { + @Test + @DisplayName("농장 ID로 농장 정보를 성공적으로 가져온다") + void should_returnFarmResponse_when_farmExists() { + // given + Long farmId = 1L; + Farm farm = Farm.builder() + .id(farmId) + .name("Test Farm") + .ownerId(1L) + .build(); + given(farmService.getFarmByFarmId(farmId)).willReturn(farm); + + // when + FarmResponse response = farmPlatformService.getFarmByFarmId(farmId); + + // then + assertThat(response.id()).isEqualTo(farmId); + assertThat(response.name()).isEqualTo("Test Farm"); + verify(farmService).getFarmByFarmId(farmId); // farmService 호출 확인 + } + + @Test + @DisplayName("농장 ID로 농장 정보를 가져오는데 농장이 존재하지 않는 경우 예외를 발생시킨다") + void should_throwException_when_farmNotExist() { + // given + Long farmId = 1L; + given(farmService.getFarmByFarmId(farmId)).willThrow(new BusinessException(BusinessError.FARM_NOT_FOUND)); + + // when & then + assertThatThrownBy(() -> farmPlatformService.getFarmByFarmId(farmId)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.FARM_NOT_FOUND); + + verify(farmService).getFarmByFarmId(farmId); + } + } + + @Nested + @DisplayName("농장 리스트 가져오기") + class GetFarmList { + @Test + @DisplayName("페이징 요청으로 농장 리스트를 성공적으로 가져온다") + void should_returnFarmResponseList_when_farmsExist() { + // given + Pageable pageable = PageRequest.of(0, 10); + Farm farm1 = Farm.builder() + .id(1L) + .name("Farm 1") + .ownerId(1L) + .build(); + Farm farm2 = Farm.builder() + .id(2L) + .name("Farm 2") + .ownerId(2L) + .build(); + List farmList = List.of(farm1, farm2); + + given(farmService.getFarmList(pageable)).willReturn(farmList); + + // when + List responseList = farmPlatformService.getFarmList(pageable); + + // then + assertThat(responseList).hasSize(2); + assertThat(responseList.get(0).name()).isEqualTo("Farm 1"); + assertThat(responseList.get(1).name()).isEqualTo("Farm 2"); + verify(farmService).getFarmList(pageable); // farmService 호출 확인 + } + } +} diff --git a/src/test/java/poomasi/domain/farm/service/FarmServiceTest.java b/src/test/java/poomasi/domain/farm/service/FarmServiceTest.java index cbcb6935..cc9603f0 100644 --- a/src/test/java/poomasi/domain/farm/service/FarmServiceTest.java +++ b/src/test/java/poomasi/domain/farm/service/FarmServiceTest.java @@ -10,10 +10,13 @@ import poomasi.domain.farm.FarmTestHelper; import poomasi.domain.farm.entity.Farm; import poomasi.domain.farm.repository.FarmRepository; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; import java.util.Optional; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.mockito.BDDMockito.given; @ExtendWith(MockitoExtension.class) @@ -42,6 +45,19 @@ void should_successGetFarm_when_farmExist() { assertThat(result.getName()).isEqualTo(farm.getName()); assertThat(result.getStatus()).isEqualTo(farm.getStatus()); } + + @Test + @DisplayName("농장이 존재하지 않는 경우 예외를 발생시킨다") + void should_throwException_when_farmNotExist() { + // given + Long farmId = 1L; + given(farmRepository.findByIdAndDeletedAtIsNull(farmId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> farmService.getFarmByFarmId(farmId)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.FARM_NOT_FOUND); + } } } diff --git a/src/test/java/poomasi/domain/store/StoreServiceTest.java b/src/test/java/poomasi/domain/store/StoreServiceTest.java new file mode 100644 index 00000000..c1327a03 --- /dev/null +++ b/src/test/java/poomasi/domain/store/StoreServiceTest.java @@ -0,0 +1,110 @@ +package poomasi.domain.store; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.Optional; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member.entity.Member; +import poomasi.domain.store.dto.StoreRegisterRequest; +import poomasi.domain.store.dto.StoreResponse; +import poomasi.domain.store.entity.Store; +import poomasi.domain.store.repository.StoreRepository; +import poomasi.domain.store.service.StoreService; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +@ExtendWith(MockitoExtension.class) +class StoreServiceTest { + + @InjectMocks + private StoreService storeService; + + @Mock + private StoreRepository storeRepository; + + @Mock + private SecurityContext securityContext; + + @Mock + private Authentication authentication; + + private Member testMember; + + @BeforeEach + void setUp() { + // 테스트 멤버와 Authentication 설정 + testMember = Member.builder().build(); + + SecurityContextHolder.setContext(securityContext); + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn(new UserDetailsImpl(testMember)); + } + + @Test + void addStore_StoreAddedSuccessfully() { + // given + StoreRegisterRequest request = mock(StoreRegisterRequest.class); + Store store = mock(Store.class); + + when(request.toEntity(any(Member.class))).thenReturn(store); + + // when + storeService.addStore(request); + + // then + verify(storeRepository, times(1)).save(store); + } + + @Test + void getStore_StoreExists_ReturnStoreResponse() { + // given + Store store = new Store(1L, "test","test","test",testMember,"test","test",100); + when(storeRepository.findByOwnerId(testMember.getId())).thenReturn(Optional.of(store)); + + // when + StoreResponse response = storeService.getStore(); + + // then + assertNotNull(response); + verify(storeRepository, times(1)).findByOwnerId(testMember.getId()); + } + + @Test + void updateStore_StoreExists_StoreUpdatedSuccessfully() { + // given + StoreRegisterRequest request = mock(StoreRegisterRequest.class); + Store store = mock(Store.class); + + when(storeRepository.findByOwnerId(testMember.getId())).thenReturn(Optional.of(store)); + + // when + storeService.updateStore(request); + + // then + verify(store, times(1)).updateStore(request); + } + + @Test + void updateStore_StoreDoesNotExist_ThrowsBusinessException() { + // given + StoreRegisterRequest request = mock(StoreRegisterRequest.class); + when(storeRepository.findByOwnerId(testMember.getId())).thenReturn(Optional.empty()); + + // when & then + BusinessException exception = assertThrows(BusinessException.class, () -> storeService.updateStore(request)); + assertEquals(BusinessError.STORE_NOT_FOUND, exception.getBusinessError()); + } + +} \ No newline at end of file diff --git a/tf-prod.json b/tf-prod.json new file mode 100644 index 00000000..8f0a6d52 --- /dev/null +++ b/tf-prod.json @@ -0,0 +1,48 @@ +{ + "family": "poomasi-server", + "containerDefinitions": [ + { + "name": "spring", + "image": "881490087445.dkr.ecr.ap-northeast-2.amazonaws.com/poomasi-server:latest", + "cpu": 0, + "portMappings": [ + { + "name": "http", + "containerPort": 8080, + "hostPort": 0, + "protocol": "tcp", + "appProtocol": "http" + } + ], + "essential": true, + "environment": [], + "mountPoints": [], + "volumesFrom": [], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/ecs/poomasi-server", + "mode": "non-blocking", + "awslogs-create-group": "true", + "max-buffer-size": "25m", + "awslogs-region": "ap-northeast-2", + "awslogs-stream-prefix": "ecs" + } + }, + "systemControls": [] + } + ], + "executionRoleArn": "arn:aws:iam::881490087445:role/ecsTaskExecutionRole", + "networkMode": "bridge", + "volumes": [], + "placementConstraints": [], + "requiresCompatibilities": [ + "EC2" + ], + "cpu": "512", + "memory": "512", + "runtimePlatform": { + "cpuArchitecture": "X86_64", + "operatingSystemFamily": "LINUX" + } +}