diff --git a/.github/workflows/deploy.yml b/.github/workflows/master_weekly_cicd.yml similarity index 74% rename from .github/workflows/deploy.yml rename to .github/workflows/master_weekly_cicd.yml index c60fe04..bca581a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/master_weekly_cicd.yml @@ -1,9 +1,10 @@ -name: master 브랜치 자동 배포 +name: master 브랜치 merge 시 CI/CD 파이프라인 on: push: branches: - 'master' + - 'weekly/**' jobs: deploy: @@ -19,6 +20,15 @@ jobs: distribution: 'corretto' java-version: '21' + - name: AWS S3 관련 정보를 설정 파일에 주입 + uses: microsoft/variable-substitution@v1 + with: + files: ./src/main/resources/application-prod.yml + env: + aws.s3.bucket: ${{ secrets.AWS_S3_BUCKET }} + aws.s3.accessKey: ${{ secrets.AWS_S3_ACCESS_KEY }} + aws.s3.secretKey: ${{ secrets.AWS_S3_SECRET_KEY }} + - name: 빌드로 테스트 수행 및 Jar 파일 생성 run: | chmod +x ./gradlew diff --git a/.github/workflows/pr_weekly_ci.yml b/.github/workflows/pr_weekly_ci.yml new file mode 100644 index 0000000..d00ebe0 --- /dev/null +++ b/.github/workflows/pr_weekly_ci.yml @@ -0,0 +1,51 @@ +name: weekly 브랜치 PR에 대한 CI 테스트 + +on: + pull_request: + branches: + - 'weekly/**' + +jobs: + ci: + runs-on: ubuntu-latest + + permissions: + checks: write + pull-requests: write + + steps: + - name: 프로젝트 코드를 CI 서버로 옮겨오기 + uses: actions/checkout@v4 + + - name: JDK 21 설치 + uses: actions/setup-java@v4 + with: + distribution: 'corretto' + java-version: '21' + + - name: AWS S3 관련 정보를 설정 파일에 주입 + uses: microsoft/variable-substitution@v1 + with: + files: ./src/main/resources/application-prod.yml + env: + aws.s3.bucket: ${{ secrets.AWS_S3_BUCKET }} + aws.s3.accessKey: ${{ secrets.AWS_S3_ACCESS_KEY }} + aws.s3.secretKey: ${{ secrets.AWS_S3_SECRET_KEY }} + + - name: 빌드 테스트 수행 + run: | + chmod +x ./gradlew + ./gradlew clean build + + - name: 테스트 수행 결과 보고 + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + files: '**/build/test-results/test/TEST-*.xml' + + - name: 테스트 실패 시, 실패한 코드 라인에 코멘트 자동 등록 + uses: mikepenz/action-junit-report@v3 + if: always() + with: + report_paths: '**/build/test-results/test/TEST-*.xml' + token: ${{ github.token }} diff --git a/.gitignore b/.gitignore index c2065bc..8eb016e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ +application-dev.yml + ### STS ### .apt_generated .classpath diff --git a/build.gradle b/build.gradle index 9a8dd05..2d19b2c 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.657' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' diff --git a/src/main/java/com/potatocake/everymoment/config/AwsS3Properties.java b/src/main/java/com/potatocake/everymoment/config/AwsS3Properties.java new file mode 100644 index 0000000..5cc7bb8 --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/config/AwsS3Properties.java @@ -0,0 +1,12 @@ +package com.potatocake.everymoment.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "aws.s3") +public record AwsS3Properties( + String accessKey, + String secretKey, + String region, + String bucket +) { +} diff --git a/src/main/java/com/potatocake/everymoment/config/SecurityConfig.java b/src/main/java/com/potatocake/everymoment/config/SecurityConfig.java index f479788..97955c9 100644 --- a/src/main/java/com/potatocake/everymoment/config/SecurityConfig.java +++ b/src/main/java/com/potatocake/everymoment/config/SecurityConfig.java @@ -15,6 +15,7 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -39,9 +40,13 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .formLogin(auth -> auth.disable()) .httpBasic(auth -> auth.disable()); + http + .headers(header -> header.frameOptions(FrameOptionsConfig::sameOrigin)); + http .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/members/login", "/h2-console/**", "/error").permitAll() + .requestMatchers("/api/members/login", "/h2-console/**", "/error", "/favicon.ico").permitAll() + .requestMatchers("/swagger-resources/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll() .anyRequest().authenticated()); http diff --git a/src/main/java/com/potatocake/everymoment/controller/.gitkeep b/src/main/java/com/potatocake/everymoment/controller/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/potatocake/everymoment/controller/DiaryController.java b/src/main/java/com/potatocake/everymoment/controller/DiaryController.java index e2c961c..e149daa 100644 --- a/src/main/java/com/potatocake/everymoment/controller/DiaryController.java +++ b/src/main/java/com/potatocake/everymoment/controller/DiaryController.java @@ -1,13 +1,18 @@ package com.potatocake.everymoment.controller; import com.potatocake.everymoment.dto.SuccessResponse; -import com.potatocake.everymoment.dto.request.DiaryAutoRequest; -import com.potatocake.everymoment.dto.request.DiaryManualRequest; +import com.potatocake.everymoment.dto.request.DiaryAutoCreateRequest; +import com.potatocake.everymoment.dto.request.DiaryFilterRequest; +import com.potatocake.everymoment.dto.request.DiaryManualCreateRequest; +import com.potatocake.everymoment.dto.response.FriendDiariesResponse; +import com.potatocake.everymoment.dto.response.FriendDiaryResponse; import com.potatocake.everymoment.dto.response.MyDiariesResponse; import com.potatocake.everymoment.dto.response.MyDiaryResponse; import com.potatocake.everymoment.dto.response.NotificationResponse; import com.potatocake.everymoment.service.DiaryService; +import com.potatocake.everymoment.service.FriendDiaryService; import java.time.LocalDate; +import lombok.RequiredArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -21,46 +26,42 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@RequiredArgsConstructor @RestController -@RequestMapping("api/diaries") +@RequestMapping("/api/diaries") public class DiaryController { private final DiaryService diaryService; - - public DiaryController(DiaryService diaryService) { - this.diaryService = diaryService; - } + private final FriendDiaryService friendDiaryService; //자동 일기 작성 @PostMapping("/auto") public ResponseEntity> createDiaryAuto( - @RequestBody DiaryAutoRequest diaryAutoRequest) { - NotificationResponse notificationResponse = diaryService.createDiaryAuto(diaryAutoRequest); + @RequestBody DiaryAutoCreateRequest diaryAutoCreateRequest) { + NotificationResponse notificationResponse = diaryService.createDiaryAuto(diaryAutoCreateRequest); SuccessResponse response = SuccessResponse.builder() .code(HttpStatus.OK.value()) .message("success") .info(notificationResponse) .build(); - return ResponseEntity.ok(response); } //수기 일기 작성 @PostMapping("/manual") public ResponseEntity> createDiaryManual( - @RequestBody DiaryManualRequest diaryManualRequest) { - diaryService.createDiaryManual(diaryManualRequest); + @RequestBody DiaryManualCreateRequest diaryManualCreateRequest) { + diaryService.createDiaryManual(diaryManualCreateRequest); SuccessResponse response = SuccessResponse.builder() .code(HttpStatus.OK.value()) .message("success") .info(null) .build(); - return ResponseEntity.ok(response); } //내 일기 전체 조회(타임라인) - @GetMapping("my") + @GetMapping("/my") public ResponseEntity> getMyDiaries( @RequestParam(required = false) String keyword, @RequestParam(required = false) String emoji, @@ -72,14 +73,24 @@ public ResponseEntity> getMyDiaries( @RequestParam(defaultValue = "0") int key, @RequestParam(defaultValue = "10") int size ) { - MyDiariesResponse myDiariesResponse = diaryService.getMyDiaries(keyword, emoji, category, date, from, until, - bookmark, key, size); + DiaryFilterRequest diaryFilterRequest = DiaryFilterRequest.builder() + .keyword(keyword) + .emoji(emoji) + .category(category) + .date(date) + .from(from) + .until(until) + .bookmark(bookmark) + .key(key) + .size(size) + .build(); + + MyDiariesResponse myDiariesResponse = diaryService.getMyDiaries(diaryFilterRequest); SuccessResponse response = SuccessResponse.builder() .code(HttpStatus.OK.value()) .message("success") .info(myDiariesResponse) .build(); - return ResponseEntity.ok(response); } @@ -92,15 +103,14 @@ public ResponseEntity> getMyDiary(@PathVariable .message("success") .info(myDiaryResponse) .build(); - return ResponseEntity.ok(response); } //일기 수정 @PatchMapping("/{id}") public ResponseEntity> updateDiary(@PathVariable Long id, - @RequestBody DiaryManualRequest diaryManualRequest) { - diaryService.updateDiary(id, diaryManualRequest); + @RequestBody DiaryManualCreateRequest diaryManualCreateRequest) { + diaryService.updateDiary(id, diaryManualCreateRequest); SuccessResponse response = SuccessResponse.builder() .code(HttpStatus.OK.value()) .message("success") @@ -144,4 +154,50 @@ public ResponseEntity> togglePrivacy(@PathVariable Long id .build(); return ResponseEntity.ok(response); } + + //전체 친구 일기 조회 + @GetMapping("/friend") + public ResponseEntity> getFriendDiaries( + @RequestParam(required = false) String keyword, + @RequestParam(required = false) String emoji, + @RequestParam(required = false) Long category, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate until, + @RequestParam(required = false) Boolean bookmark, + @RequestParam(defaultValue = "0") int key, + @RequestParam(defaultValue = "10") int size + ) { + DiaryFilterRequest diaryFilterRequest = DiaryFilterRequest.builder() + .keyword(keyword) + .emoji(emoji) + .category(category) + .date(date) + .from(from) + .until(until) + .bookmark(bookmark) + .key(key) + .size(size) + .build(); + + FriendDiariesResponse diaries = friendDiaryService.getFriendDiaries(diaryFilterRequest); + SuccessResponse response = SuccessResponse.builder() + .code(HttpStatus.OK.value()) + .message("success") + .info(diaries) + .build(); + return ResponseEntity.ok(response); + } + + //친구 일기 상제 조회 + @GetMapping("/friend/{id}") + public ResponseEntity> getFriendDiary(@PathVariable Long id) { + FriendDiaryResponse diary = friendDiaryService.getFriendDiary(id); + SuccessResponse response = SuccessResponse.builder() + .code(HttpStatus.OK.value()) + .message("success") + .info(diary) + .build(); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/com/potatocake/everymoment/controller/FriendController.java b/src/main/java/com/potatocake/everymoment/controller/FriendController.java new file mode 100644 index 0000000..d108739 --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/controller/FriendController.java @@ -0,0 +1,83 @@ +package com.potatocake.everymoment.controller; + +import com.potatocake.everymoment.dto.SuccessResponse; +import com.potatocake.everymoment.dto.response.FriendListResponse; +import com.potatocake.everymoment.dto.response.OneFriendDiariesResponse; +import com.potatocake.everymoment.service.FriendService; +import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("api/friends") +public class FriendController { + private final FriendService friendService; + + public FriendController(FriendService friendService) { + this.friendService = friendService; + } + + //특정 친구 일기 전체 조회 + @GetMapping("/{id}/diaries") + public ResponseEntity> getOneFriendDiaries( + @PathVariable Long id, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, + @RequestParam(defaultValue = "0") int key, + @RequestParam(defaultValue = "10") int size) { + OneFriendDiariesResponse diaries = friendService.OneFriendDiariesResponse(id, date, key, size); + SuccessResponse response = SuccessResponse.builder() + .code(HttpStatus.OK.value()) + .message("success") + .info(diaries) + .build(); + return ResponseEntity.ok(response); + } + + //내 친구 목록 조회 + @GetMapping("/friends") + public ResponseEntity> getFriendList( + @RequestParam(required = false) String nickname, + @RequestParam(required = false) String email, + @RequestParam(defaultValue = "0") int key, + @RequestParam(defaultValue = "10") int size) { + FriendListResponse friendList = friendService.getFriendList(nickname, email, key, size); + SuccessResponse response = SuccessResponse.builder() + .code(HttpStatus.OK.value()) + .message("success") + .info(friendList) + .build(); + return ResponseEntity.ok(response); + } + + //내 친구 삭제 + @DeleteMapping("/{id}") + public ResponseEntity> deleteFriend(@PathVariable Long id) { + friendService.deleteFriend(id); + SuccessResponse response = SuccessResponse.builder() + .code(HttpStatus.OK.value()) + .message("success") + .info(null) + .build(); + return ResponseEntity.ok(response); + } + + //친한 친구 설정 + @PatchMapping("/{id}/bookmark") + public ResponseEntity> toggleCloseFriend(@PathVariable Long id) { + friendService.toggleCloseFriend(id); + SuccessResponse response = SuccessResponse.builder() + .code(HttpStatus.OK.value()) + .message("success") + .info(null) + .build(); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/potatocake/everymoment/controller/MemberController.java b/src/main/java/com/potatocake/everymoment/controller/MemberController.java new file mode 100644 index 0000000..1146a94 --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/controller/MemberController.java @@ -0,0 +1,79 @@ +package com.potatocake.everymoment.controller; + +import com.potatocake.everymoment.dto.SuccessResponse; +import com.potatocake.everymoment.dto.response.MemberDetailResponse; +import com.potatocake.everymoment.dto.response.MemberResponse; +import com.potatocake.everymoment.dto.response.MemberSearchResponse; +import com.potatocake.everymoment.exception.ErrorCode; +import com.potatocake.everymoment.exception.GlobalException; +import com.potatocake.everymoment.security.MemberDetails; +import com.potatocake.everymoment.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RequiredArgsConstructor +@RequestMapping("/api/members") +@RestController +public class MemberController { + + private final MemberService memberService; + + @GetMapping + public ResponseEntity> searchMembers( + @RequestParam(required = false) String nickname, + @RequestParam(required = false) String email, + @RequestParam(required = false) Long key, + @RequestParam(defaultValue = "10") int size) { + + MemberSearchResponse response = memberService.searchMembers(nickname, email, key, size); + + return ResponseEntity.ok() + .body(SuccessResponse.ok(response)); + } + + @GetMapping("/me") + public ResponseEntity> myInfo( + @AuthenticationPrincipal MemberDetails memberDetails) { + MemberDetailResponse response = memberService.getMyInfo(memberDetails.getId()); + + return ResponseEntity.ok() + .body(SuccessResponse.ok(response)); + } + + @GetMapping("/{memberId}") + public ResponseEntity> memberInfo(@PathVariable Long memberId) { + MemberResponse response = memberService.getMemberInfo(memberId); + + return ResponseEntity.ok() + .body(SuccessResponse.ok(response)); + } + + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity> updateMemberInfo(@AuthenticationPrincipal MemberDetails memberDetails, + @RequestParam(required = false) MultipartFile profileImage, + @RequestParam(required = false) String nickname) { + validateProfileUpdate(profileImage, nickname); + + memberService.updateMemberInfo(memberDetails.getId(), profileImage, nickname); + + return ResponseEntity.ok() + .body(SuccessResponse.ok()); + } + + private void validateProfileUpdate(MultipartFile profileImage, String nickname) { + if (profileImage == null && !StringUtils.hasText(nickname)) { + throw new GlobalException(ErrorCode.INFO_REQUIRED); + } + } + +} diff --git a/src/main/java/com/potatocake/everymoment/dto/SuccessResponse.java b/src/main/java/com/potatocake/everymoment/dto/SuccessResponse.java index 0d586bd..c6ffae0 100644 --- a/src/main/java/com/potatocake/everymoment/dto/SuccessResponse.java +++ b/src/main/java/com/potatocake/everymoment/dto/SuccessResponse.java @@ -14,11 +14,27 @@ public class SuccessResponse { private String message; private T info; - public static SuccessResponse of(T info) { + public static SuccessResponse of(int code, T info) { return SuccessResponse.builder() + .code(code) .message("success") .info(info) .build(); } + public static SuccessResponse ok(T info) { + return SuccessResponse.builder() + .code(200) + .message("success") + .info(info) + .build(); + } + + public static SuccessResponse ok() { + return SuccessResponse.builder() + .code(200) + .message("success") + .build(); + } + } diff --git a/src/main/java/com/potatocake/everymoment/dto/request/DiaryAutoRequest.java b/src/main/java/com/potatocake/everymoment/dto/request/DiaryAutoCreateRequest.java similarity index 86% rename from src/main/java/com/potatocake/everymoment/dto/request/DiaryAutoRequest.java rename to src/main/java/com/potatocake/everymoment/dto/request/DiaryAutoCreateRequest.java index 095cbf4..d1dbede 100644 --- a/src/main/java/com/potatocake/everymoment/dto/request/DiaryAutoRequest.java +++ b/src/main/java/com/potatocake/everymoment/dto/request/DiaryAutoCreateRequest.java @@ -4,7 +4,7 @@ import lombok.Getter; @Getter -public class DiaryAutoRequest { +public class DiaryAutoCreateRequest { private LocationPoint locationPoint; private String locationName; private String address; diff --git a/src/main/java/com/potatocake/everymoment/dto/request/DiaryFilterRequest.java b/src/main/java/com/potatocake/everymoment/dto/request/DiaryFilterRequest.java new file mode 100644 index 0000000..7675f25 --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/dto/request/DiaryFilterRequest.java @@ -0,0 +1,19 @@ +package com.potatocake.everymoment.dto.request; + +import java.time.LocalDate; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class DiaryFilterRequest { + private String keyword; + private String emoji; + private Long category; + private LocalDate date; + private LocalDate from; + private LocalDate until; + private Boolean bookmark; + private int key; + private int size; +} diff --git a/src/main/java/com/potatocake/everymoment/dto/request/DiaryManualRequest.java b/src/main/java/com/potatocake/everymoment/dto/request/DiaryManualCreateRequest.java similarity index 91% rename from src/main/java/com/potatocake/everymoment/dto/request/DiaryManualRequest.java rename to src/main/java/com/potatocake/everymoment/dto/request/DiaryManualCreateRequest.java index 78bde8b..849298a 100644 --- a/src/main/java/com/potatocake/everymoment/dto/request/DiaryManualRequest.java +++ b/src/main/java/com/potatocake/everymoment/dto/request/DiaryManualCreateRequest.java @@ -5,7 +5,7 @@ import lombok.Getter; @Getter -public class DiaryManualRequest { +public class DiaryManualCreateRequest { private List categories; private LocationPoint locationPoint; private String locationName; diff --git a/src/main/java/com/potatocake/everymoment/dto/response/FriendDiariesResponse.java b/src/main/java/com/potatocake/everymoment/dto/response/FriendDiariesResponse.java new file mode 100644 index 0000000..f2f096f --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/dto/response/FriendDiariesResponse.java @@ -0,0 +1,12 @@ +package com.potatocake.everymoment.dto.response; + +import java.util.List; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class FriendDiariesResponse { + private List diaries; + private Integer next; +} diff --git a/src/main/java/com/potatocake/everymoment/dto/response/FriendDiaryResponse.java b/src/main/java/com/potatocake/everymoment/dto/response/FriendDiaryResponse.java new file mode 100644 index 0000000..b442af3 --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/dto/response/FriendDiaryResponse.java @@ -0,0 +1,23 @@ +package com.potatocake.everymoment.dto.response; + +import java.time.LocalDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class FriendDiaryResponse { + private Long id; + private List categories; + private String locationName; + private String emoji; + private List file; + private String content; + private Integer likeCount; + private LocalDateTime createAt; +} diff --git a/src/main/java/com/potatocake/everymoment/dto/response/FriendDiarySimpleResponse.java b/src/main/java/com/potatocake/everymoment/dto/response/FriendDiarySimpleResponse.java new file mode 100644 index 0000000..d159936 --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/dto/response/FriendDiarySimpleResponse.java @@ -0,0 +1,23 @@ +package com.potatocake.everymoment.dto.response; + +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class FriendDiarySimpleResponse { + private Long id; + private String locationName; + private String address; + private boolean isBookmark; + private boolean isPublic; + private String emoji; + private ThumbnailResponse thumbnailResponse; + private String content; + private LocalDateTime createAt; +} diff --git a/src/main/java/com/potatocake/everymoment/dto/response/FriendListResponse.java b/src/main/java/com/potatocake/everymoment/dto/response/FriendListResponse.java new file mode 100644 index 0000000..d5131e7 --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/dto/response/FriendListResponse.java @@ -0,0 +1,12 @@ +package com.potatocake.everymoment.dto.response; + +import java.util.List; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class FriendListResponse { + private List friends; + private Integer next; +} diff --git a/src/main/java/com/potatocake/everymoment/dto/response/FriendProfileResponse.java b/src/main/java/com/potatocake/everymoment/dto/response/FriendProfileResponse.java new file mode 100644 index 0000000..5e359dc --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/dto/response/FriendProfileResponse.java @@ -0,0 +1,13 @@ +package com.potatocake.everymoment.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class FriendProfileResponse { + private Long id; + private String nickname; + private String profileImageUrl; + private boolean isClose; +} diff --git a/src/main/java/com/potatocake/everymoment/dto/response/MemberDetailResponse.java b/src/main/java/com/potatocake/everymoment/dto/response/MemberDetailResponse.java new file mode 100644 index 0000000..0180208 --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/dto/response/MemberDetailResponse.java @@ -0,0 +1,15 @@ +package com.potatocake.everymoment.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class MemberDetailResponse { + + private Long id; + private String profileImageUrl; + private String nickname; + private String email; + +} diff --git a/src/main/java/com/potatocake/everymoment/dto/response/MemberResponse.java b/src/main/java/com/potatocake/everymoment/dto/response/MemberResponse.java new file mode 100644 index 0000000..57dfcc5 --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/dto/response/MemberResponse.java @@ -0,0 +1,14 @@ +package com.potatocake.everymoment.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class MemberResponse { + + private Long id; + private String profileImageUrl; + private String nickname; + +} diff --git a/src/main/java/com/potatocake/everymoment/dto/response/MemberSearchResponse.java b/src/main/java/com/potatocake/everymoment/dto/response/MemberSearchResponse.java new file mode 100644 index 0000000..cd1f71b --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/dto/response/MemberSearchResponse.java @@ -0,0 +1,16 @@ +package com.potatocake.everymoment.dto.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.List; +import lombok.Builder; +import lombok.Getter; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Builder +@Getter +public class MemberSearchResponse { + + private List members; + private Long next; + +} diff --git a/src/main/java/com/potatocake/everymoment/dto/response/MyDiariesResponse.java b/src/main/java/com/potatocake/everymoment/dto/response/MyDiariesResponse.java index 208e5c3..48de888 100644 --- a/src/main/java/com/potatocake/everymoment/dto/response/MyDiariesResponse.java +++ b/src/main/java/com/potatocake/everymoment/dto/response/MyDiariesResponse.java @@ -8,5 +8,5 @@ @Builder public class MyDiariesResponse { private List diaries; - private Integer key; + private Integer next; } diff --git a/src/main/java/com/potatocake/everymoment/dto/response/OneFriendDiariesResponse.java b/src/main/java/com/potatocake/everymoment/dto/response/OneFriendDiariesResponse.java new file mode 100644 index 0000000..31ae287 --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/dto/response/OneFriendDiariesResponse.java @@ -0,0 +1,12 @@ +package com.potatocake.everymoment.dto.response; + +import java.util.List; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class OneFriendDiariesResponse { + private List diaries; + private Integer next; +} diff --git a/src/main/java/com/potatocake/everymoment/entity/Diary.java b/src/main/java/com/potatocake/everymoment/entity/Diary.java index 644dc82..b0b85ee 100644 --- a/src/main/java/com/potatocake/everymoment/entity/Diary.java +++ b/src/main/java/com/potatocake/everymoment/entity/Diary.java @@ -2,10 +2,14 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; import jakarta.persistence.Lob; +import jakarta.persistence.ManyToOne; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -22,10 +26,9 @@ public class Diary extends BaseTimeEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - // @ManyToOne(fetch = FetchType.LAZY) -// @JoinColumn(name = "member_id", foreignKey = @ForeignKey(name = "fk_member_id"), nullable = false) - @Column(nullable = false) - private Long memberId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false) + private Member memberId; @Lob private String content; @@ -50,4 +53,50 @@ public class Diary extends BaseTimeEntity { @Column(nullable = false) @Builder.Default private boolean isPublic = false; + + public void updateContent(String content) { + if (content != null) { + this.content = content; + } + } + + public void updateLocationPoint(String locationPoint) { + if (locationPoint != null) { + this.locationPoint = locationPoint; + } + } + + public void updateLocationName(String locationName) { + if (locationName != null) { + this.locationName = locationName; + } + } + + public void updateAddress(String address) { + if (address != null) { + this.address = address; + } + } + + public void updateEmoji(String emoji) { + if (emoji != null) { + this.emoji = emoji; + } + } + + public void updateBookmark(boolean isBookmark) { + this.isBookmark = isBookmark; + } + + public void updatePublic(boolean isPublic) { + this.isPublic = isPublic; + } + + public void toggleBookmark() { + this.isBookmark = !this.isBookmark; + } + + public void togglePublic() { + this.isPublic = !this.isPublic; + } } diff --git a/src/main/java/com/potatocake/everymoment/entity/Friend.java b/src/main/java/com/potatocake/everymoment/entity/Friend.java new file mode 100644 index 0000000..b6818cb --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/entity/Friend.java @@ -0,0 +1,43 @@ +package com.potatocake.everymoment.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +@Builder +public class Friend { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false) + private Member memberId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false) + private Member friendId; + + @Column(nullable = false) + @Builder.Default + private boolean isClose = false; + + public void toggleIsClose() { + this.isClose = !this.isClose; + } +} diff --git a/src/main/java/com/potatocake/everymoment/entity/Member.java b/src/main/java/com/potatocake/everymoment/entity/Member.java index 74d8c86..fa657f4 100644 --- a/src/main/java/com/potatocake/everymoment/entity/Member.java +++ b/src/main/java/com/potatocake/everymoment/entity/Member.java @@ -32,4 +32,9 @@ public class Member extends BaseTimeEntity { @Lob private String profileImageUrl; + public void update(String nickname, String profileImageUrl) { + this.nickname = nickname; + this.profileImageUrl = profileImageUrl; + } + } diff --git a/src/main/java/com/potatocake/everymoment/entity/Notification.java b/src/main/java/com/potatocake/everymoment/entity/Notification.java index b2fd360..7963a15 100644 --- a/src/main/java/com/potatocake/everymoment/entity/Notification.java +++ b/src/main/java/com/potatocake/everymoment/entity/Notification.java @@ -2,9 +2,13 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -21,10 +25,9 @@ public class Notification extends BaseTimeEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - // @ManyToOne(fetch = FetchType.LAZY) -// @JoinColumn(name = "member_id", foreignKey = @ForeignKey(name = "fk_member_id"), nullable = false) - @Column(nullable = false) - private Long memberId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false) + private Member memberId; @Column(nullable = false) private String content; diff --git a/src/main/java/com/potatocake/everymoment/exception/ErrorCode.java b/src/main/java/com/potatocake/everymoment/exception/ErrorCode.java index 16df143..3595494 100644 --- a/src/main/java/com/potatocake/everymoment/exception/ErrorCode.java +++ b/src/main/java/com/potatocake/everymoment/exception/ErrorCode.java @@ -1,10 +1,12 @@ package com.potatocake.everymoment.exception; +import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.CONFLICT; import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; import static org.springframework.http.HttpStatus.NOT_FOUND; import static org.springframework.http.HttpStatus.PAYLOAD_TOO_LARGE; import static org.springframework.http.HttpStatus.UNAUTHORIZED; +import static org.springframework.http.HttpStatus.UNSUPPORTED_MEDIA_TYPE; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -12,6 +14,15 @@ @Getter public enum ErrorCode { + /* Diary */ + DIARY_NOT_FOUND("존재하지 않는 일기입니다.", NOT_FOUND), + + /* Member */ + MEMBER_NOT_FOUND("존재하지 않는 회원입니다.", NOT_FOUND), + + /* Friend */ + FRIEND_NOT_FOUND("존재하지 않는 친구입니다.", NOT_FOUND), + /* CategoryService */ ALREADY_EXISTS_CATEGORY("이미 존재하는 카테고리입니다.", CONFLICT), @@ -20,7 +31,7 @@ public enum ErrorCode { /* File */ FILE_NOT_FOUND("존재하지 않는 파일입니다.", NOT_FOUND), - FILE_SIZE_EXCEEDED("전체 파일 크기를 10MB 이하로 첨부해 주세요.", PAYLOAD_TOO_LARGE), + FILE_SIZE_EXCEEDED("각 파일은 1MB 이하로, 전체 파일 크기는 10MB 이하로 첨부해 주세요.", PAYLOAD_TOO_LARGE), /* Comment */ COMMENT_NOT_FOUND("존재하지 않는 댓글입니다.", NOT_FOUND), @@ -28,7 +39,13 @@ public enum ErrorCode { UNKNOWN_ERROR("알 수 없는 오류가 발생했습니다.", INTERNAL_SERVER_ERROR), LOGIN_FAILED("로그인에 실패했습니다.", UNAUTHORIZED), - LOGIN_REQUIRED("유효한 인증 정보가 필요합니다.", UNAUTHORIZED); + LOGIN_REQUIRED("유효한 인증 정보가 필요합니다.", UNAUTHORIZED), + + /* S3FileUploader */ + INVALID_FILE_TYPE("이미지 파일 형식만 첨부가 가능합니다. (JPEG, PNG)", UNSUPPORTED_MEDIA_TYPE), + FILE_STORE_FAILED("파일 저장에 실패했습니다.", INTERNAL_SERVER_ERROR), + + INFO_REQUIRED("정보를 입력해 주세요.", BAD_REQUEST); private final String message; private final HttpStatus status; diff --git a/src/main/java/com/potatocake/everymoment/exception/GlobalExceptionHandler.java b/src/main/java/com/potatocake/everymoment/exception/GlobalExceptionHandler.java index 2052234..797ffcb 100644 --- a/src/main/java/com/potatocake/everymoment/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/potatocake/everymoment/exception/GlobalExceptionHandler.java @@ -1,5 +1,9 @@ package com.potatocake.everymoment.exception; +import static com.potatocake.everymoment.exception.ErrorCode.FILE_SIZE_EXCEEDED; +import static com.potatocake.everymoment.exception.ValidationErrorMessage.VALIDATION_ERROR; +import static org.springframework.http.HttpStatus.PAYLOAD_TOO_LARGE; + import lombok.extern.slf4j.Slf4j; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -8,6 +12,8 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.multipart.MaxUploadSizeExceededException; +import org.springframework.web.multipart.support.MissingServletRequestPartException; @Slf4j @Order(Ordered.HIGHEST_PRECEDENCE) @@ -16,10 +22,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity invalid(MethodArgumentNotValidException e) { - ErrorResponse errorResponse = ErrorResponse.builder() - .code(400) - .message(ValidationErrorMessage.VALIDATION_ERROR) - .build(); + ErrorResponse errorResponse = getErrorResponse(400, VALIDATION_ERROR); e.getFieldErrors().forEach(filedError -> errorResponse.addValidation(filedError.getField(), filedError.getDefaultMessage())); @@ -28,6 +31,22 @@ public ResponseEntity invalid(MethodArgumentNotValidException e) .body(errorResponse); } + @ExceptionHandler(MissingServletRequestPartException.class) + public ResponseEntity missingRequestPart(MissingServletRequestPartException e) { + ErrorResponse errorResponse = getErrorResponse(400, e.getMessage()); + + return ResponseEntity.badRequest() + .body(errorResponse); + } + + @ExceptionHandler(MaxUploadSizeExceededException.class) + public ResponseEntity fileMaxSize() { + ErrorResponse errorResponse = getErrorResponse(PAYLOAD_TOO_LARGE.value(), FILE_SIZE_EXCEEDED.getMessage()); + + return ResponseEntity.status(PAYLOAD_TOO_LARGE) + .body(errorResponse); + } + @ExceptionHandler(GlobalException.class) public ResponseEntity handlerCustomException(GlobalException e) { HttpStatus status = e.getErrorCode().getStatus(); diff --git a/src/main/java/com/potatocake/everymoment/repository/DiaryCategoryRepository.java b/src/main/java/com/potatocake/everymoment/repository/DiaryCategoryRepository.java index 6770d22..2989173 100644 --- a/src/main/java/com/potatocake/everymoment/repository/DiaryCategoryRepository.java +++ b/src/main/java/com/potatocake/everymoment/repository/DiaryCategoryRepository.java @@ -1,12 +1,9 @@ package com.potatocake.everymoment.repository; import com.potatocake.everymoment.entity.DiaryCategory; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; -@Repository public interface DiaryCategoryRepository extends JpaRepository { List findByCategoryId(Long categoryId); } diff --git a/src/main/java/com/potatocake/everymoment/repository/DiaryRepository.java b/src/main/java/com/potatocake/everymoment/repository/DiaryRepository.java index 4e48c49..deff65c 100644 --- a/src/main/java/com/potatocake/everymoment/repository/DiaryRepository.java +++ b/src/main/java/com/potatocake/everymoment/repository/DiaryRepository.java @@ -3,8 +3,6 @@ import com.potatocake.everymoment.entity.Diary; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -import org.springframework.stereotype.Repository; -@Repository public interface DiaryRepository extends JpaRepository, JpaSpecificationExecutor { } diff --git a/src/main/java/com/potatocake/everymoment/repository/FriendRepository.java b/src/main/java/com/potatocake/everymoment/repository/FriendRepository.java new file mode 100644 index 0000000..5eb96f7 --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/repository/FriendRepository.java @@ -0,0 +1,14 @@ +package com.potatocake.everymoment.repository; + +import com.potatocake.everymoment.entity.Friend; +import com.potatocake.everymoment.entity.Member; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface FriendRepository extends JpaRepository, JpaSpecificationExecutor { + Optional findByMemberIdAndFriendId(Member member, Member friend); + + List findAllFriendIdsByMemberId(Member member); +} diff --git a/src/main/java/com/potatocake/everymoment/repository/MemberRepository.java b/src/main/java/com/potatocake/everymoment/repository/MemberRepository.java index 51c90a8..03b501c 100644 --- a/src/main/java/com/potatocake/everymoment/repository/MemberRepository.java +++ b/src/main/java/com/potatocake/everymoment/repository/MemberRepository.java @@ -2,6 +2,9 @@ import com.potatocake.everymoment.entity.Member; import java.util.Optional; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Window; import org.springframework.data.jpa.repository.JpaRepository; public interface MemberRepository extends JpaRepository { @@ -10,4 +13,7 @@ public interface MemberRepository extends JpaRepository { boolean existsByEmail(String email); + Window findByNicknameContainingAndEmailContaining(String nickname, String email, ScrollPosition position, + Pageable pageable); + } diff --git a/src/main/java/com/potatocake/everymoment/repository/NotificationRepository.java b/src/main/java/com/potatocake/everymoment/repository/NotificationRepository.java index c380eaf..a3a5974 100644 --- a/src/main/java/com/potatocake/everymoment/repository/NotificationRepository.java +++ b/src/main/java/com/potatocake/everymoment/repository/NotificationRepository.java @@ -2,8 +2,6 @@ import com.potatocake.everymoment.entity.Notification; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -@Repository public interface NotificationRepository extends JpaRepository { } diff --git a/src/main/java/com/potatocake/everymoment/security/MemberDetails.java b/src/main/java/com/potatocake/everymoment/security/MemberDetails.java index ca46ecc..c93d8e0 100644 --- a/src/main/java/com/potatocake/everymoment/security/MemberDetails.java +++ b/src/main/java/com/potatocake/everymoment/security/MemberDetails.java @@ -34,4 +34,8 @@ public String getUsername() { return member.getEmail(); } + public Long getId() { + return member.getId(); + } + } diff --git a/src/main/java/com/potatocake/everymoment/security/filter/JwtFilter.java b/src/main/java/com/potatocake/everymoment/security/filter/JwtFilter.java index ed43217..e433c6a 100644 --- a/src/main/java/com/potatocake/everymoment/security/filter/JwtFilter.java +++ b/src/main/java/com/potatocake/everymoment/security/filter/JwtFilter.java @@ -12,13 +12,11 @@ import java.io.IOException; import java.util.Optional; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; -@Slf4j @RequiredArgsConstructor public class JwtFilter extends OncePerRequestFilter { @@ -30,7 +28,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse Optional token = jwtUtil.resolveToken(request); if (token.isEmpty() || jwtUtil.isExpired(token.get())) { - log.info("인증 실패 - 토큰 없음 또는 만료됨"); filterChain.doFilter(request, response); return; } diff --git a/src/main/java/com/potatocake/everymoment/security/filter/LoginFilter.java b/src/main/java/com/potatocake/everymoment/security/filter/LoginFilter.java index 94ca636..6077c30 100644 --- a/src/main/java/com/potatocake/everymoment/security/filter/LoginFilter.java +++ b/src/main/java/com/potatocake/everymoment/security/filter/LoginFilter.java @@ -70,7 +70,7 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR JwtResponse jwt = JwtResponse.of(token); - objectMapper.writeValue(response.getWriter(), SuccessResponse.of(jwt)); + objectMapper.writeValue(response.getWriter(), SuccessResponse.ok(jwt)); } @Override diff --git a/src/main/java/com/potatocake/everymoment/service/.gitkeep b/src/main/java/com/potatocake/everymoment/service/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/potatocake/everymoment/service/DiaryService.java b/src/main/java/com/potatocake/everymoment/service/DiaryService.java index b0532e6..2adb77d 100644 --- a/src/main/java/com/potatocake/everymoment/service/DiaryService.java +++ b/src/main/java/com/potatocake/everymoment/service/DiaryService.java @@ -1,9 +1,13 @@ package com.potatocake.everymoment.service; -import com.potatocake.everymoment.dto.request.DiaryAutoRequest; -import com.potatocake.everymoment.dto.request.DiaryManualRequest; +import com.potatocake.everymoment.dto.request.DiaryAutoCreateRequest; +import com.potatocake.everymoment.dto.request.DiaryFilterRequest; +import com.potatocake.everymoment.dto.request.DiaryManualCreateRequest; import com.potatocake.everymoment.dto.response.CategoryResponse; import com.potatocake.everymoment.dto.response.FileResponse; +import com.potatocake.everymoment.dto.response.FriendDiariesResponse; +import com.potatocake.everymoment.dto.response.FriendDiarySimpleResponse; +import com.potatocake.everymoment.dto.response.FriendDiaryResponse; import com.potatocake.everymoment.dto.response.MyDiariesResponse; import com.potatocake.everymoment.dto.response.MyDiaryResponse; import com.potatocake.everymoment.dto.response.MyDiarySimpleResponse; @@ -11,19 +15,33 @@ import com.potatocake.everymoment.dto.response.ThumbnailResponse; import com.potatocake.everymoment.entity.Diary; import com.potatocake.everymoment.entity.DiaryCategory; +import com.potatocake.everymoment.entity.Member; import com.potatocake.everymoment.entity.Notification; +import com.potatocake.everymoment.exception.ErrorCode; +import com.potatocake.everymoment.exception.GlobalException; import com.potatocake.everymoment.repository.DiaryCategoryRepository; import com.potatocake.everymoment.repository.DiaryRepository; +import com.potatocake.everymoment.repository.MemberRepository; import com.potatocake.everymoment.repository.NotificationRepository; -import java.time.LocalDate; +import com.potatocake.everymoment.security.MemberDetails; +import com.potatocake.everymoment.util.JwtUtil; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.domain.Specification; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +@RequiredArgsConstructor +@Transactional @Service public class DiaryService { @@ -31,23 +49,18 @@ public class DiaryService { private final DiaryCategoryRepository diaryCategoryRepository; private final NotificationRepository notificationRepository; - public DiaryService(DiaryRepository diaryRepository, DiaryCategoryRepository diaryCategoryRepository, - NotificationRepository notificationRepository) { - this.diaryRepository = diaryRepository; - this.diaryCategoryRepository = diaryCategoryRepository; - this.notificationRepository = notificationRepository; - } - // 자동 일기 저장 (LocationPoint, Name, Adress 만 저장) - public NotificationResponse createDiaryAuto(DiaryAutoRequest diaryAutoRequest) { - // member Id 가져옴 - Long memberId = 1L; + public NotificationResponse createDiaryAuto(DiaryAutoCreateRequest diaryAutoCreateRequest) { + // member 가져옴 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal(); + Member currentMember = memberDetails.getMember(); Diary diary = Diary.builder() - .memberId(memberId) - .locationPoint(diaryAutoRequest.getLocationPoint().toString()) - .locationName(diaryAutoRequest.getLocationName()) - .address(diaryAutoRequest.getAddress()) + .memberId(currentMember) + .locationPoint(diaryAutoCreateRequest.getLocationPoint().toString()) + .locationName(diaryAutoCreateRequest.getLocationName()) + .address(diaryAutoCreateRequest.getAddress()) .build(); Diary savedDiary = diaryRepository.save(diary); @@ -56,7 +69,7 @@ public NotificationResponse createDiaryAuto(DiaryAutoRequest diaryAutoRequest) { String content = "현재 " + savedDiary.getLocationName() + "에 머무르고 있어요! 지금 기분은 어떠신가요?"; Notification notification = Notification.builder() - .memberId(memberId) + .memberId(currentMember) .content(content) .type("MOOD_CHECK") .targetId(savedDiary.getId()) @@ -77,19 +90,21 @@ public NotificationResponse createDiaryAuto(DiaryAutoRequest diaryAutoRequest) { } // 수동 일기 작성 - public void createDiaryManual(DiaryManualRequest diaryManualRequest) { - // member Id 가져옴 - Long memberId = 1L; + public void createDiaryManual(DiaryManualCreateRequest diaryManualCreateRequest) { + // member Id + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal(); + Member currentMember = memberDetails.getMember(); Diary diary = Diary.builder() - .memberId(memberId) - .content(diaryManualRequest.getContent()) - .locationPoint(diaryManualRequest.getLocationPoint().toString()) - .locationName(diaryManualRequest.getLocationName()) - .address(diaryManualRequest.getAddress()) - .emoji(diaryManualRequest.getEmoji()) - .isBookmark(diaryManualRequest.isBookmark()) - .isPublic(diaryManualRequest.isPublic()) + .memberId(currentMember) + .content(diaryManualCreateRequest.getContent()) + .locationPoint(diaryManualCreateRequest.getLocationPoint().toString()) + .locationName(diaryManualCreateRequest.getLocationName()) + .address(diaryManualCreateRequest.getAddress()) + .emoji(diaryManualCreateRequest.getEmoji()) + .isBookmark(diaryManualCreateRequest.isBookmark()) + .isPublic(diaryManualCreateRequest.isPublic()) .build(); Diary savedDiary = diaryRepository.save(diary); @@ -100,129 +115,111 @@ public void createDiaryManual(DiaryManualRequest diaryManualRequest) { } // 내 일기 전체 조회 (타임라인) - public MyDiariesResponse getMyDiaries(String keyword, String emoji, Long category, - LocalDate date, LocalDate from, LocalDate until, - Boolean isBookmark, int key, int size) { - //member id 가져옴 - Long memberId = 1L; + @Transactional(readOnly = true) + public MyDiariesResponse getMyDiaries(DiaryFilterRequest diaryFilterRequest) { + //member 가져옴 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal(); + Member currentMember = memberDetails.getMember(); Page diaryPage; - if (category == null) { + if (diaryFilterRequest.getCategory() == null) { // category가 null인 경우 - Specification spec = DiarySpecification.filterDiaries(keyword, emoji, date, from, until, isBookmark) - .and((root, query, builder) -> builder.equal(root.get("memberId"), memberId)); + Specification spec = DiarySpecification.filterDiaries(diaryFilterRequest.getKeyword(), + diaryFilterRequest.getEmoji(), diaryFilterRequest.getDate(), diaryFilterRequest.getFrom(), + diaryFilterRequest.getUntil(), diaryFilterRequest.getBookmark()) + .and((root, query, builder) -> builder.equal(root.get("memberId"), currentMember)); - diaryPage = diaryRepository.findAll(spec, PageRequest.of(key, size)); + diaryPage = diaryRepository.findAll(spec, + PageRequest.of(diaryFilterRequest.getKey(), diaryFilterRequest.getSize())); } else { // category가 있는 경우 - DiaryCategory에서 category 같은 것 찾음 - List diaryCategoryList = diaryCategoryRepository.findByCategoryId(category); + List diaryCategoryList = diaryCategoryRepository.findByCategoryId( + diaryFilterRequest.getCategory()); // Diary 중에 memberId같은 것 가져옴 List DiaryIdList = diaryCategoryList.stream() .filter(diaryCategory -> diaryCategory.getDiary().getMemberId() - .equals(memberId)) // memberId가 일치하는 경우 필터링 + .equals(currentMember)) // memberId가 일치하는 경우 필터링 .map(diaryCategory -> diaryCategory.getDiary().getId()) .collect(Collectors.toList()); // 가져온 DiaryId로 일기 찾음 Specification spec = (root, query, builder) -> root.get("id").in(DiaryIdList); - diaryPage = diaryRepository.findAll(spec, PageRequest.of(key, size)); + diaryPage = diaryRepository.findAll(spec, + PageRequest.of(diaryFilterRequest.getKey(), diaryFilterRequest.getSize())); } List diaryDTOs = diaryPage.getContent().stream() .map(this::convertToMyDiarySimpleResponseDto) .collect(Collectors.toList()); - Integer nextPage = diaryPage.hasNext() ? key + 1 : null; + Integer nextPage = diaryPage.hasNext() ? diaryFilterRequest.getKey() + 1 : null; - MyDiariesResponse myDiariesResponse = MyDiariesResponse.builder() + return MyDiariesResponse.builder() .diaries(diaryDTOs) - .key(nextPage) + .next(nextPage) .build(); - - return myDiariesResponse; } // 내 일기 상세 조회 + @Transactional(readOnly = true) public MyDiaryResponse getMyDiary(Long id) { - Diary diary = diaryRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException("Diary not found")); - + Diary diary = getExistDiary(id); return convertToMyDiaryResponseDto(diary); } // 내 일기 수정 - public void updateDiary(Long id, DiaryManualRequest diaryManualRequest) { - Diary existingDiary = diaryRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException("Diary not found")); + public void updateDiary(Long id, DiaryManualCreateRequest diaryManualCreateRequest) { + Diary existingDiary = getExistDiary(id); //카테고리 업데이트 //파일 업데이트 //다이어리 업데이트 - Diary updatedDiary = Diary.builder() - .id(existingDiary.getId()) - .memberId(1L) - .content(diaryManualRequest.getContent() != null ? diaryManualRequest.getContent() - : existingDiary.getContent()) - .locationPoint( - diaryManualRequest.getLocationPoint() != null ? diaryManualRequest.getLocationPoint() - .toString() : existingDiary.getLocationPoint()) - .locationName(diaryManualRequest.getLocationName() != null ? diaryManualRequest.getLocationName() - : existingDiary.getLocationName()) - .address(diaryManualRequest.getAddress() != null ? diaryManualRequest.getAddress() - : existingDiary.getAddress()) - .emoji(diaryManualRequest.getEmoji() != null ? diaryManualRequest.getEmoji() - : existingDiary.getEmoji()) - .build(); - - diaryRepository.save(updatedDiary); + existingDiary.updateContent(diaryManualCreateRequest.getContent()); + existingDiary.updateLocationPoint(diaryManualCreateRequest.getLocationPoint() != null + ? diaryManualCreateRequest.getLocationPoint().toString() : null); + existingDiary.updateLocationName(diaryManualCreateRequest.getLocationName()); + existingDiary.updateAddress(diaryManualCreateRequest.getAddress()); + existingDiary.updateEmoji(diaryManualCreateRequest.getEmoji()); + existingDiary.updateBookmark(diaryManualCreateRequest.isBookmark()); + existingDiary.updatePublic(diaryManualCreateRequest.isPublic()); } // 내 일기 삭제 public void deleteDiary(Long id) { - diaryRepository.deleteById(id); + Diary existingDiary = getExistDiary(id); + diaryRepository.delete(existingDiary); } // 내 일기 북마크 설정 public void toggleBookmark(Long id) { - Diary diary = diaryRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException("Diary not found")); - - Diary updatedDiary = Diary.builder() - .id(diary.getId()) - .memberId(diary.getMemberId()) - .content(diary.getContent()) - .locationPoint(diary.getLocationPoint()) - .locationName(diary.getLocationName()) - .address(diary.getAddress()) - .emoji(diary.getEmoji()) - .isBookmark(!diary.isBookmark()) // 북마크 토글 - .isPublic(diary.isPublic()) - .build(); - - diaryRepository.save(updatedDiary); + Diary existingDiary = getExistDiary(id); + existingDiary.toggleBookmark(); } // 내 일기 공개 설정 public void togglePrivacy(Long id) { - Diary diary = diaryRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException("Diary not found")); - - Diary updatedDiary = Diary.builder() - .id(diary.getId()) - .memberId(diary.getMemberId()) - .content(diary.getContent()) - .locationPoint(diary.getLocationPoint()) - .locationName(diary.getLocationName()) - .address(diary.getAddress()) - .emoji(diary.getEmoji()) - .isBookmark(diary.isBookmark()) - .isPublic(!diary.isPublic()) // 공개여부 토글 - .build(); + Diary existingDiary = getExistDiary(id); + existingDiary.togglePublic(); + } + + // 로그인한 유저의 일기가 맞는지 확인 후 일기 반환 + private Diary getExistDiary(Long diaryId){ + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal(); + Member currentMember = memberDetails.getMember(); + + Diary diary = diaryRepository.findById(diaryId) + .orElseThrow(() -> new GlobalException(ErrorCode.DIARY_NOT_FOUND)); + + if(!Objects.equals(currentMember.getId(), diary.getMemberId().getId())){ + throw new GlobalException(ErrorCode.DIARY_NOT_FOUND); + } - diaryRepository.save(updatedDiary); + return diary; } //상세 조회시 일기DTO 변환 diff --git a/src/main/java/com/potatocake/everymoment/service/DiarySpecification.java b/src/main/java/com/potatocake/everymoment/service/DiarySpecification.java index e5031a4..7554307 100644 --- a/src/main/java/com/potatocake/everymoment/service/DiarySpecification.java +++ b/src/main/java/com/potatocake/everymoment/service/DiarySpecification.java @@ -22,9 +22,11 @@ public static Specification filterDiaries(String keyword, String emoji, L if (emoji != null) { predicate = builder.and(predicate, builder.equal(root.get("emoji"), emoji)); } - if (date != null) { - predicate = builder.and(predicate, builder.equal(builder.function("DATE", LocalDate.class, root.get("createAt")), date)); - } + + // 날짜 필터링 (null일 경우 오늘 날짜로 기본값 설정) + LocalDate filterDate = (date != null) ? date : LocalDate.now(); + predicate = builder.and(predicate, builder.equal(root.get("createAt").as(LocalDate.class), filterDate)); + if (from != null && until != null) { predicate = builder.and(predicate, builder.between(root.get("createAt"), from.atStartOfDay(), until.plusDays(1).atStartOfDay())); } diff --git a/src/main/java/com/potatocake/everymoment/service/FriendDiaryService.java b/src/main/java/com/potatocake/everymoment/service/FriendDiaryService.java new file mode 100644 index 0000000..d6d4ee1 --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/service/FriendDiaryService.java @@ -0,0 +1,161 @@ +package com.potatocake.everymoment.service; + +import com.potatocake.everymoment.dto.request.DiaryFilterRequest; +import com.potatocake.everymoment.dto.response.CategoryResponse; +import com.potatocake.everymoment.dto.response.FileResponse; +import com.potatocake.everymoment.dto.response.FriendDiariesResponse; +import com.potatocake.everymoment.dto.response.FriendDiaryResponse; +import com.potatocake.everymoment.dto.response.FriendDiarySimpleResponse; +import com.potatocake.everymoment.dto.response.ThumbnailResponse; +import com.potatocake.everymoment.entity.Diary; +import com.potatocake.everymoment.entity.DiaryCategory; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.exception.ErrorCode; +import com.potatocake.everymoment.exception.GlobalException; +import com.potatocake.everymoment.repository.DiaryCategoryRepository; +import com.potatocake.everymoment.repository.DiaryRepository; +import com.potatocake.everymoment.repository.FriendRepository; +import com.potatocake.everymoment.security.MemberDetails; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional +@Service +public class FriendDiaryService { + private final DiaryRepository diaryRepository; + private final DiaryCategoryRepository diaryCategoryRepository; + private final FriendRepository friendRepository; + + //친구 일기 조회 + public FriendDiariesResponse getFriendDiaries(DiaryFilterRequest diaryFilterRequest) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal(); + Member currentMember = memberDetails.getMember(); + + List friends = friendRepository.findAllFriendIdsByMemberId(currentMember); + List friendIdList = friends.stream() + .map(Member::getId) + .collect(Collectors.toList()); + + Page diaryPage; + + if (diaryFilterRequest.getCategory() == null) { + // category가 null인 경우 + Specification spec = DiarySpecification.filterDiaries(diaryFilterRequest.getKeyword(), + diaryFilterRequest.getEmoji(), diaryFilterRequest.getDate(), diaryFilterRequest.getFrom(), + diaryFilterRequest.getUntil(), diaryFilterRequest.getBookmark()) + .and((root, query, builder) -> root.get("memberId").in(friendIdList)); // memberIds 목록에서 검색 + + diaryPage = diaryRepository.findAll(spec, PageRequest.of(diaryFilterRequest.getKey(), diaryFilterRequest.getSize())); + } else { + // category가 있는 경우 - DiaryCategory에서 category 같은 것 찾음 + List diaryCategories = diaryCategoryRepository.findByCategoryId(diaryFilterRequest.getCategory()); + + // Diary중에 memberId같은 것 가져옴 + List filteredDiaryIds = diaryCategories.stream() + .filter(diaryCategory -> friendIdList.contains(diaryCategory.getDiary().getMemberId())) // memberIds 목록에서 필터링 + .map(diaryCategory -> diaryCategory.getDiary().getId()) + .collect(Collectors.toList()); + + // 가져온 diaryId로 일기 찾음 + Specification spec = (root, query, builder) -> root.get("id").in(filteredDiaryIds); + diaryPage = diaryRepository.findAll(spec, PageRequest.of(diaryFilterRequest.getKey(), diaryFilterRequest.getSize())); + } + + List friendDiarySimpleResponseList = diaryPage.getContent().stream() + .map(this::convertToFriendDiariesResponseDTO) + .collect(Collectors.toList()); + + Integer nextPage = diaryPage.hasNext() ? diaryFilterRequest.getKey() + 1 : null; + + FriendDiariesResponse friendDiariesResponse = FriendDiariesResponse.builder() + .diaries(friendDiarySimpleResponseList) + .next(nextPage) + .build(); + + return friendDiariesResponse; + } + + // 친구 다이어리 하나 조회 + public FriendDiaryResponse getFriendDiary(Long id) { + Diary diary = diaryRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Diary not found")); + + //글쓴사람이 친구인지 확인 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal(); + Member currentMember = memberDetails.getMember(); + + List friends = friendRepository.findAllFriendIdsByMemberId(currentMember); + List friendIdList = friends.stream() + .map(Member::getId) + .collect(Collectors.toList()); + + if(!friendIdList.contains(diary.getMemberId())){ + throw new GlobalException(ErrorCode.FRIEND_NOT_FOUND); + } + //카테고리 찾음 + CategoryResponse categoryResponseDTO = CategoryResponse.builder() + .id(1L) + .categoryName("일상") + .build(); + List categoryResponseDTOList = new ArrayList<>(); + categoryResponseDTOList.add(categoryResponseDTO); + + //파일 찾음 + FileResponse fileResponse = FileResponse.builder() + .id(1L) + .imageUrl("image1.url") + .order(1) + .build(); + List fileResponseDTOList = new ArrayList<>(); + fileResponseDTOList.add(fileResponse); + + //like 갯수 반환 + Integer likeCount = 11; + + FriendDiaryResponse diaryResponseDTO = FriendDiaryResponse.builder() + .id(diary.getId()) + .categories(categoryResponseDTOList) + .locationName(diary.getLocationName()) + .emoji(diary.getEmoji()) + .file(fileResponseDTOList) + .content(diary.getContent()) + .likeCount(likeCount) + .createAt(diary.getCreateAt()) + .build(); + + return diaryResponseDTO; + } + + //친구 일기 DTO변환 + private FriendDiarySimpleResponse convertToFriendDiariesResponseDTO(Diary savedDiary) { + //파일 찾음 + ThumbnailResponse thumbnailResponse = ThumbnailResponse.builder() + .id(1L) + .imageUrl("image1.url") + .build(); + + return FriendDiarySimpleResponse.builder() + .id(savedDiary.getId()) + .address(savedDiary.getAddress()) + .locationName(savedDiary.getLocationName()) + .isBookmark(savedDiary.isBookmark()) + .isPublic(savedDiary.isPublic()) + .emoji(savedDiary.getEmoji()) + .thumbnailResponse(thumbnailResponse) + .content(savedDiary.getContent()) + .createAt(savedDiary.getCreateAt()) + .build(); + } +} diff --git a/src/main/java/com/potatocake/everymoment/service/FriendService.java b/src/main/java/com/potatocake/everymoment/service/FriendService.java new file mode 100644 index 0000000..d252170 --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/service/FriendService.java @@ -0,0 +1,159 @@ +package com.potatocake.everymoment.service; + +import com.potatocake.everymoment.dto.response.FriendDiarySimpleResponse; +import com.potatocake.everymoment.dto.response.FriendListResponse; +import com.potatocake.everymoment.dto.response.FriendProfileResponse; +import com.potatocake.everymoment.dto.response.OneFriendDiariesResponse; +import com.potatocake.everymoment.dto.response.ThumbnailResponse; +import com.potatocake.everymoment.entity.Diary; +import com.potatocake.everymoment.entity.Friend; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.exception.ErrorCode; +import com.potatocake.everymoment.exception.GlobalException; +import com.potatocake.everymoment.repository.DiaryRepository; +import com.potatocake.everymoment.repository.FriendRepository; +import com.potatocake.everymoment.repository.MemberRepository; +import com.potatocake.everymoment.security.MemberDetails; +import java.time.LocalDate; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional +@Service +public class FriendService { + + private final FriendRepository friendRepository; + private final MemberRepository memberRepository; + private final DiaryRepository diaryRepository; + + //특정 친구 일기 조회 + @Transactional(readOnly = true) + public OneFriendDiariesResponse OneFriendDiariesResponse(Long id, LocalDate date, int key, int size) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal(); + Member currentMember = memberDetails.getMember(); + + //친구인지 확인 + Member friend = memberRepository.findById(id) + .orElseThrow(() -> new GlobalException(ErrorCode.MEMBER_NOT_FOUND)); + friendRepository.findByMemberIdAndFriendId(currentMember, friend) + .orElseThrow(() -> new GlobalException(ErrorCode.FRIEND_NOT_FOUND)); + + Pageable pageable = PageRequest.of(key, size); + + Page diaries = diaryRepository.findAll(DiarySpecification.filterDiaries(null, null, date, null, null, null) + .and((root, query, builder) -> builder.equal(root.get("memberId").get("id"), id)), pageable); + + List diaryList = diaries.getContent().stream() + .map(this::convertToFriendDiariesResponseDTO) + .collect(Collectors.toList()); + + Integer nextPage = diaries.hasNext() ? key + 1 : null; + + return OneFriendDiariesResponse.builder() + .diaries(diaryList) + .next(nextPage) + .build(); + } + + //내 친구 목록 조회 + @Transactional(readOnly = true) + public FriendListResponse getFriendList(String nickname, String email, int key, int size) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal(); + Member currentMember = memberDetails.getMember(); + Long memberId = currentMember.getId(); + + Pageable pageable = PageRequest.of(key, size); + + Specification spec = FriendSpecification.filterFriends(memberId, nickname, email) + .and((root, query, builder) -> builder.equal(root.get("memberId").get("id"), memberId)); + + Page friends = friendRepository.findAll(spec, pageable); + + List friendProfiles = friends.stream() + .map(this::convertToFriendProfileResponseDTO) + .collect(Collectors.toList()); + + Integer nextPage = friends.hasNext() ? key + 1 : null; + + return FriendListResponse.builder() + .friends(friendProfiles) + .next(nextPage) + .build(); + } + + // 친구 삭제 + public void deleteFriend(Long id) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal(); + Member currentMember = memberDetails.getMember(); + + Member friendMember = memberRepository.findById(id) + .orElseThrow(() -> new GlobalException(ErrorCode.MEMBER_NOT_FOUND)); + + Friend friendMine = friendRepository.findByMemberIdAndFriendId(currentMember, friendMember) + .orElseThrow(() -> new GlobalException(ErrorCode.FRIEND_NOT_FOUND)); + Friend friendFriends = friendRepository.findByMemberIdAndFriendId(friendMember, currentMember) + .orElseThrow(() -> new GlobalException(ErrorCode.FRIEND_NOT_FOUND)); + + friendRepository.delete(friendMine); + friendRepository.delete(friendFriends); + } + + // 친한 친구 설정(토글) + public void toggleCloseFriend(Long id) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal(); + Member currentMember = memberDetails.getMember(); + + Member friendMember = memberRepository.findById(id) + .orElseThrow(() -> new GlobalException(ErrorCode.MEMBER_NOT_FOUND)); + Friend friend = friendRepository.findByMemberIdAndFriendId(currentMember, friendMember) + .orElseThrow(() -> new GlobalException(ErrorCode.FRIEND_NOT_FOUND)); + + friend.toggleIsClose(); + } + + //다이어리 DTO 변환 + private FriendDiarySimpleResponse convertToFriendDiariesResponseDTO(Diary savedDiary) { + //파일 찾음 + ThumbnailResponse thumbnailResponse = ThumbnailResponse.builder() + .id(1L) + .imageUrl("image1.url") + .build(); + + return FriendDiarySimpleResponse.builder() + .id(savedDiary.getId()) + .address(savedDiary.getAddress()) + .locationName(savedDiary.getLocationName()) + .isBookmark(savedDiary.isBookmark()) + .isPublic(savedDiary.isPublic()) + .emoji(savedDiary.getEmoji()) + .thumbnailResponse(thumbnailResponse) + .content(savedDiary.getContent()) + .createAt(savedDiary.getCreateAt()) + .build(); + } + + //친구 프로필 DTO 변환 + private FriendProfileResponse convertToFriendProfileResponseDTO(Friend friend){ + Member friendMember = friend.getFriendId(); + return FriendProfileResponse.builder() + .id(friendMember.getId()) + .nickname(friendMember.getNickname()) + .profileImageUrl(friendMember.getProfileImageUrl()) + .isClose(friend.isClose()) + .build(); + } +} diff --git a/src/main/java/com/potatocake/everymoment/service/FriendSpecification.java b/src/main/java/com/potatocake/everymoment/service/FriendSpecification.java new file mode 100644 index 0000000..6d193f1 --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/service/FriendSpecification.java @@ -0,0 +1,32 @@ +package com.potatocake.everymoment.service; + +import com.potatocake.everymoment.entity.Friend; +import com.potatocake.everymoment.entity.Member; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import org.springframework.data.jpa.domain.Specification; + +public class FriendSpecification { + public static Specification filterFriends(Long memberId, String nickname, String email) { + return (Root root, CriteriaQuery query, CriteriaBuilder builder) -> { + Predicate predicate = builder.conjunction(); + + predicate = builder.and(predicate, builder.equal(root.get("memberId").get("id"), memberId)); + + if (nickname != null) { + Join friendJoin = root.join("friendId"); + predicate = builder.and(predicate, builder.like(friendJoin.get("nickname"), "%" + nickname + "%")); + } + + if (email != null) { + Join friendJoin = root.join("friendId"); + predicate = builder.and(predicate, builder.like(friendJoin.get("email"), "%" + email + "%")); + } + + return predicate; + }; + } +} diff --git a/src/main/java/com/potatocake/everymoment/service/MemberService.java b/src/main/java/com/potatocake/everymoment/service/MemberService.java new file mode 100644 index 0000000..11b2db6 --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/service/MemberService.java @@ -0,0 +1,98 @@ +package com.potatocake.everymoment.service; + +import com.potatocake.everymoment.dto.response.MemberDetailResponse; +import com.potatocake.everymoment.dto.response.MemberResponse; +import com.potatocake.everymoment.dto.response.MemberSearchResponse; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.exception.ErrorCode; +import com.potatocake.everymoment.exception.GlobalException; +import com.potatocake.everymoment.repository.MemberRepository; +import com.potatocake.everymoment.util.PagingUtil; +import com.potatocake.everymoment.util.S3FileUploader; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Window; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +@RequiredArgsConstructor +@Transactional +@Service +public class MemberService { + + private final MemberRepository memberRepository; + private final PagingUtil pagingUtil; + private final S3FileUploader s3FileUploader; + + @Transactional(readOnly = true) + public MemberSearchResponse searchMembers(String nickname, String email, Long key, int size) { + Window window = fetchMemberWindow(nickname, email, key, size); + List members = convertToMemberResponses(window.getContent()); + Long nextKey = pagingUtil.getNextKey(window); + + return MemberSearchResponse.builder() + .members(members) + .next(nextKey) + .build(); + } + + @Transactional(readOnly = true) + public MemberDetailResponse getMyInfo(Long memberId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new GlobalException(ErrorCode.MEMBER_NOT_FOUND)); + + return MemberDetailResponse.builder() + .id(member.getId()) + .profileImageUrl(member.getProfileImageUrl()) + .nickname(member.getNickname()) + .email(member.getEmail()) + .build(); + } + + @Transactional(readOnly = true) + public MemberResponse getMemberInfo(Long memberId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new GlobalException(ErrorCode.MEMBER_NOT_FOUND)); + + return MemberResponse.builder() + .id(member.getId()) + .profileImageUrl(member.getProfileImageUrl()) + .nickname(member.getNickname()) + .build(); + } + + public void updateMemberInfo(Long id, MultipartFile profileImage, String nickname) { + Member member = memberRepository.findById(id) + .orElseThrow(() -> new GlobalException(ErrorCode.MEMBER_NOT_FOUND)); + + String profileImageUrl = s3FileUploader.uploadFile(profileImage); + + member.update(nickname, profileImageUrl); + } + + private Window fetchMemberWindow(String nickname, String email, Long key, int size) { + ScrollPosition scrollPosition = pagingUtil.createScrollPosition(key); + Pageable pageable = pagingUtil.createPageable(size); + + String searchNickname = (nickname == null) ? "" : nickname; + String searchEmail = (email == null) ? "" : email; + + return memberRepository.findByNicknameContainingAndEmailContaining(searchNickname, searchEmail, scrollPosition, + pageable); + } + + private List convertToMemberResponses(List members) { + return members.stream() + .map(member -> MemberResponse.builder() + .id(member.getId()) + .profileImageUrl(member.getProfileImageUrl()) + .nickname(member.getNickname()) + .build()) + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/com/potatocake/everymoment/util/PagingUtil.java b/src/main/java/com/potatocake/everymoment/util/PagingUtil.java new file mode 100644 index 0000000..90c5df3 --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/util/PagingUtil.java @@ -0,0 +1,29 @@ +package com.potatocake.everymoment.util; + +import com.potatocake.everymoment.entity.Member; +import java.util.Map; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Window; +import org.springframework.stereotype.Component; + +@Component +public class PagingUtil { + + public ScrollPosition createScrollPosition(Long key) { + return key == null ? ScrollPosition.offset() : ScrollPosition.forward(Map.of("id", key)); + } + + public Pageable createPageable(int size) { + return PageRequest.of(0, size, Sort.by(Sort.Direction.ASC, "id")); + } + + public Long getNextKey(Window window) { + return window.hasNext() + ? ((Member) window.getContent().get(window.getContent().size() - 1)).getId() + : null; + } + +} diff --git a/src/main/java/com/potatocake/everymoment/util/S3FileUploader.java b/src/main/java/com/potatocake/everymoment/util/S3FileUploader.java new file mode 100644 index 0000000..83b2033 --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/util/S3FileUploader.java @@ -0,0 +1,70 @@ +package com.potatocake.everymoment.util; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.potatocake.everymoment.config.AwsS3Properties; +import com.potatocake.everymoment.exception.ErrorCode; +import com.potatocake.everymoment.exception.GlobalException; +import jakarta.annotation.PostConstruct; +import java.io.IOException; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +@RequiredArgsConstructor +@EnableConfigurationProperties(AwsS3Properties.class) +@Component +public class S3FileUploader { + + private AmazonS3 amazonS3; + private final AwsS3Properties properties; + + @PostConstruct + private void s3Client() { + AWSCredentials awsCredentials = new BasicAWSCredentials(properties.accessKey(), properties.secretKey()); + + amazonS3 = AmazonS3ClientBuilder.standard() + .withRegion(properties.region()) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } + + public String uploadFile(MultipartFile file) { + String filename = UUID.randomUUID() + "_" + file.getOriginalFilename(); + + try { + validateFileType(file); + + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType(file.getContentType()); + objectMetadata.setContentLength(file.getSize()); + + amazonS3.putObject( + new PutObjectRequest(properties.bucket(), filename, file.getInputStream(), objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + } catch (IOException e) { + throw new GlobalException(ErrorCode.FILE_STORE_FAILED); + } + + return amazonS3.getUrl(properties.bucket(), filename).toString(); + } + + private void validateFileType(MultipartFile file) throws IOException { + String fileType = file.getContentType(); + + if (fileType == null || (!fileType.equals(MediaType.IMAGE_JPEG_VALUE) && !fileType.equals( + MediaType.IMAGE_PNG_VALUE))) { + throw new GlobalException(ErrorCode.INVALID_FILE_TYPE); + } + } + +} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 0000000..3fc83ae --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,29 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + username: sa + password: + + jpa: + properties: + hibernate: + format_sql: true + show-sql: true + + h2: + console: + enabled: true + + servlet: + multipart: + max-file-size: 1MB + max-request-size: 10MB + resolve-lazily: true + +aws: + s3: + client: AmazonS3 + region: ap-northeast-2 + bucket: ${AWS_S3_BUCKET} + accessKey: ${AWS_S3_ACCESS_KEY} + secretKey: ${AWS_S3_SECRET_KEY} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0f1a41c..9f96606 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,15 +1,3 @@ spring: - datasource: - url: jdbc:h2:mem:testdb - username: sa - password: - - jpa: - properties: - hibernate: - format_sql: true - show-sql: true - - h2: - console: - enabled: true + profiles: + active: prod diff --git a/src/test/java/com/potatocake/everymoment/controller/MemberControllerTest.java b/src/test/java/com/potatocake/everymoment/controller/MemberControllerTest.java new file mode 100644 index 0000000..02ad1a3 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/controller/MemberControllerTest.java @@ -0,0 +1,183 @@ +package com.potatocake.everymoment.controller; + +import static com.potatocake.everymoment.exception.ErrorCode.INFO_REQUIRED; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.potatocake.everymoment.dto.response.MemberDetailResponse; +import com.potatocake.everymoment.dto.response.MemberResponse; +import com.potatocake.everymoment.dto.response.MemberSearchResponse; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.security.MemberDetails; +import com.potatocake.everymoment.service.MemberService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.web.multipart.MultipartFile; + +@WithMockUser +@AutoConfigureMockMvc +@SpringBootTest +class MemberControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private MemberService memberService; + + @Test + @DisplayName("회원 목록 검색이 성공적으로 수행된다.") + void should_SearchMembers_When_ValidInput() throws Exception { + // given + String nickname = "testUser"; + String email = "test@test.com"; + Long key = 1L; + int size = 10; + MemberSearchResponse response = MemberSearchResponse.builder().build(); + + given(memberService.searchMembers(nickname, email, key, size)).willReturn(response); + + // when + ResultActions result = mockMvc.perform(get("/api/members") + .param("nickname", nickname) + .param("email", email) + .param("key", key.toString()) + .param("size", String.valueOf(size))); + + // then + result + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(memberService).should().searchMembers(nickname, email, key, size); + } + + @Test + @DisplayName("내 정보 조회가 성공적으로 수행된다.") + void should_ReturnMyInfo_When_ValidMember() throws Exception { + // given + Long memberId = 1L; + MemberDetails memberDetails = createMemberDetails(memberId, "test@example.com", "test"); + + MemberDetailResponse response = MemberDetailResponse.builder().build(); + given(memberService.getMyInfo(memberId)).willReturn(response); + + // when + ResultActions result = performGet("/api/members/me", memberDetails); + + // then + result + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.info").isNotEmpty()); + + then(memberService).should().getMyInfo(memberId); + } + + @Test + @DisplayName("회원 ID로 정보 조회가 성공적으로 수행된다.") + void should_ReturnMemberInfo_When_ValidMemberId() throws Exception { + // given + Long memberId = 1L; + MemberResponse response = MemberResponse.builder().build(); + + given(memberService.getMemberInfo(memberId)).willReturn(response); + + // when + ResultActions result = mockMvc.perform(get("/api/members/{memberId}", memberId)); + + // then + result + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.info").isNotEmpty()); + + then(memberService).should().getMemberInfo(memberId); + } + + @Test + @DisplayName("회원 정보 수정이 성공적으로 수행된다.") + void should_UpdateMemberInfo_When_ValidInput() throws Exception { + // given + Long memberId = 1L; + MemberDetails memberDetails = createMemberDetails(memberId, "test@example.com", "test"); + + MockMultipartFile profileImage = new MockMultipartFile("profileImage", "image.png", "image/png", new byte[]{}); + String nickname = "newNickname"; + + willDoNothing().given(memberService).updateMemberInfo(anyLong(), any(MultipartFile.class), anyString()); + + // when + ResultActions result = performMultipart("/api/members", profileImage, nickname, memberDetails); + + // then + result + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(memberService).should().updateMemberInfo(memberId, profileImage, nickname); + } + + @Test + @DisplayName("프로필 이미지와 닉네임이 모두 누락되면 예외가 발생한다.") + void should_ThrowException_When_ProfileImageAndNicknameAreMissing() throws Exception { + // when + ResultActions result = mockMvc.perform(multipart("/api/members")); + + // then + result + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(INFO_REQUIRED.getStatus().value())) + .andExpect(jsonPath("$.message").value(INFO_REQUIRED.getMessage())); + + then(memberService).shouldHaveNoInteractions(); + } + + private Member createMember(Long memberId, String email, String nickname) { + return Member.builder() + .id(memberId) + .email(email) + .nickname(nickname) + .build(); + } + + private MemberDetails createMemberDetails(Long memberId, String email, String nickname) { + Member member = createMember(memberId, email, nickname); + return new MemberDetails(member); + } + + private ResultActions performGet(String url, MemberDetails memberDetails) throws Exception { + return mockMvc.perform(get(url) + .with(user(memberDetails))); + } + + private ResultActions performMultipart(String url, MockMultipartFile file, String nickname, + MemberDetails memberDetails) throws Exception { + return mockMvc.perform(multipart(url) + .file(file) + .param("nickname", nickname) + .with(user(memberDetails))); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/repository/MemberRepositoryTest.java b/src/test/java/com/potatocake/everymoment/repository/MemberRepositoryTest.java new file mode 100644 index 0000000..4cb1bd8 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/repository/MemberRepositoryTest.java @@ -0,0 +1,96 @@ +package com.potatocake.everymoment.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.potatocake.everymoment.entity.Member; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Window; + +@DataJpaTest +class MemberRepositoryTest { + + @Autowired + private MemberRepository memberRepository; + + @Test + @DisplayName("이메일로 회원을 성공적으로 조회한다.") + void should_FindMemberByEmail_When_EmailExists() { + // given + String email = "test@test.com"; + memberRepository.save(Member.builder() + .email(email) + .nickname("testUser") + .build()); + + // when + Optional foundMember = memberRepository.findByEmail(email); + + // then + assertThat(foundMember).isPresent(); + assertThat(foundMember.get().getEmail()).isEqualTo(email); + } + + @Test + @DisplayName("이메일이 존재하는지 확인한다.") + void should_ReturnTrue_When_EmailExists() { + // given + String email = "test@test.com"; + memberRepository.save(Member.builder() + .email(email) + .nickname("testUser") + .build()); + + // when + boolean exists = memberRepository.existsByEmail(email); + + // then + assertThat(exists).isTrue(); + } + + @Test + @DisplayName("닉네임과 이메일을 포함하여 스크롤 방식으로 회원 목록을 조회한다.") + void should_FindByNicknameContainingAndEmailContaining_When_ValidScrollPosition() { + // given + String nickname = "test"; + String email = "test"; + for (int i = 1; i <= 15; i++) { + memberRepository.save(Member.builder() + .nickname(nickname + i) + .email(email + i + "@test.com") + .build()); + } + + // when + ScrollPosition scrollPosition = ScrollPosition.offset(); + PageRequest pageRequest = PageRequest.of(0, 10); + Window window = memberRepository.findByNicknameContainingAndEmailContaining(nickname, email, + scrollPosition, pageRequest); + + // then + assertThat(window).isNotNull(); + assertThat(window.getContent().size()).isEqualTo(10); + assertThat(window.hasNext()).isTrue(); // 총 15개 중 10개를 조회했으므로 다음 페이지 존재 + } + + @Test + @DisplayName("닉네임과 이메일이 일치하지 않으면 빈 결과를 반환한다.") + void should_ReturnEmpty_When_NoMatchingNicknameAndEmail() { + // given + ScrollPosition scrollPosition = ScrollPosition.offset(); + PageRequest pageRequest = PageRequest.of(0, 10); + + // when + Window window = memberRepository.findByNicknameContainingAndEmailContaining("nonexistent", + "nonexistent@test.com", scrollPosition, pageRequest); + + // then + assertThat(window.getContent()).isEmpty(); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/service/MemberServiceTest.java b/src/test/java/com/potatocake/everymoment/service/MemberServiceTest.java new file mode 100644 index 0000000..3c8e313 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/service/MemberServiceTest.java @@ -0,0 +1,120 @@ +package com.potatocake.everymoment.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import com.potatocake.everymoment.dto.response.MemberDetailResponse; +import com.potatocake.everymoment.dto.response.MemberSearchResponse; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.exception.GlobalException; +import com.potatocake.everymoment.repository.MemberRepository; +import com.potatocake.everymoment.util.PagingUtil; +import com.potatocake.everymoment.util.S3FileUploader; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +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.ScrollPosition; +import org.springframework.data.domain.Window; +import org.springframework.web.multipart.MultipartFile; + +@ExtendWith(MockitoExtension.class) +class MemberServiceTest { + + @InjectMocks + private MemberService memberService; + + @Mock + private MemberRepository memberRepository; + + @Mock + private PagingUtil pagingUtil; + + @Mock + private S3FileUploader s3FileUploader; + + @Test + @DisplayName("회원 목록 검색이 성공적으로 수행된다.") + void should_ReturnMemberList_When_ValidSearchConditions() { + // given + String nickname = "testUser"; + String email = "test@test.com"; + Long key = 1L; + int size = 10; + + List members = List.of(Member.builder().build()); + Window window = Window.from(members, ScrollPosition::offset, false); + given(memberRepository.findByNicknameContainingAndEmailContaining(anyString(), anyString(), any(), any())) + .willReturn(window); + + // when + MemberSearchResponse result = memberService.searchMembers(nickname, email, key, size); + + // then + assertThat(result).isNotNull(); + assertThat(result.getMembers()).isNotEmpty(); + + then(memberRepository).should() + .findByNicknameContainingAndEmailContaining(anyString(), anyString(), any(), any()); + } + + @Test + @DisplayName("내 정보 조회가 성공적으로 수행된다.") + void should_ReturnMyInfo_When_ValidMemberId() { + // given + Long memberId = 1L; + Member member = Member.builder().build(); + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + + // when + MemberDetailResponse result = memberService.getMyInfo(memberId); + + // then + assertThat(result).isNotNull(); + + then(memberRepository).should().findById(memberId); + } + + @Test + @DisplayName("회원 정보 수정이 성공적으로 수행된다.") + void should_UpdateMemberInfo_When_ValidInput() { + // given + Long memberId = 1L; + MultipartFile profileImage = mock(MultipartFile.class); + String nickname = "newNickname"; + + Member member = Member.builder().build(); + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(s3FileUploader.uploadFile(profileImage)).willReturn("profileUrl"); + + // when + memberService.updateMemberInfo(memberId, profileImage, nickname); + + // then + assertThat(member.getNickname()).isEqualTo("newNickname"); + + then(memberRepository).should().findById(memberId); + } + + @Test + @DisplayName("존재하지 않는 회원의 정보를 수정하려고 하면 예외가 발생한다.") + void should_ThrowException_When_MemberNotFound() { + // given + Long memberId = 1L; + given(memberRepository.findById(memberId)).willReturn(Optional.empty()); + + // when & then + assertThatExceptionOfType(GlobalException.class) + .isThrownBy(() -> memberService.updateMemberInfo(memberId, null, "newNickname")); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/util/PagingUtilTest.java b/src/test/java/com/potatocake/everymoment/util/PagingUtilTest.java new file mode 100644 index 0000000..eb110d9 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/util/PagingUtilTest.java @@ -0,0 +1,74 @@ +package com.potatocake.everymoment.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.potatocake.everymoment.entity.Member; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Window; + +class PagingUtilTest { + + private PagingUtil pagingUtil; + + @BeforeEach + void setUp() { + pagingUtil = new PagingUtil(); + } + + @Test + @DisplayName("스크롤 위치가 성공적으로 생성된다.") + void should_CreateScrollPosition_When_KeyIsNull() { + // when + ScrollPosition position = pagingUtil.createScrollPosition(null); + + // then + Assertions.assertThat(position).isNotNull(); + } + + @Test + @DisplayName("페이지 정보가 성공적으로 생성된다.") + void should_CreatePageable_When_ValidSizeProvided() { + // when + Pageable pageable = pagingUtil.createPageable(10); + + // then + assertThat(pageable).isNotNull(); + assertThat(pageable.getPageSize()).isEqualTo(10); + } + + @Test + @DisplayName("다음 페이지 키가 성공적으로 반환된다.") + void should_ReturnNextKey_When_WindowHasNext() { + // given + List members = List.of(Member.builder().id(1L).build()); + Window window = Window.from(members, ScrollPosition::offset, true); + + // when + Long nextKey = pagingUtil.getNextKey(window); + + // then + assertThat(nextKey).isNotNull(); + assertThat(nextKey).isEqualTo(1L); + } + + @Test + @DisplayName("다음 페이지가 존재하지 않을 때, 키 값으로 null 을 반환한다.") + void should_ReturnNull_When_WindowHasNoNext() { + // given + List members = List.of(Member.builder().id(1L).build()); + Window window = Window.from(members, ScrollPosition::offset, false); + + // when + Long nextKey = pagingUtil.getNextKey(window); + + // then + assertThat(nextKey).isNull(); + } + +}