diff --git a/.gitignore b/.gitignore index c2065bc..2ca2d0c 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ out/ ### VS Code ### .vscode/ + +### Environment variables ### + .env + *.env diff --git a/build.gradle b/build.gradle index acac2b1..de3f44d 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly('io.jsonwebtoken:jjwt-jackson:0.11.5') @@ -36,6 +37,7 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.538' } tasks.named('test') { diff --git a/src/main/java/com/example/FixLog/FixLogApplication.java b/src/main/java/com/example/FixLog/FixLogApplication.java index e8c3276..2734b29 100644 --- a/src/main/java/com/example/FixLog/FixLogApplication.java +++ b/src/main/java/com/example/FixLog/FixLogApplication.java @@ -2,7 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +//Create_At 어노테이션 +@EnableJpaAuditing @SpringBootApplication public class FixLogApplication { diff --git a/src/main/java/com/example/FixLog/config/AwsS3Config.java b/src/main/java/com/example/FixLog/config/AwsS3Config.java new file mode 100644 index 0000000..7e47597 --- /dev/null +++ b/src/main/java/com/example/FixLog/config/AwsS3Config.java @@ -0,0 +1,33 @@ +package com.example.FixLog.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AwsS3Config { + + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3 amazonS3() { + // 자격증명 생성 + BasicAWSCredentials creds = new BasicAWSCredentials(accessKey, secretKey); + // 클라이언트 빌드 + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(creds)) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/config/SecurityConfig.java b/src/main/java/com/example/FixLog/config/SecurityConfig.java index c6408d6..333d4f7 100644 --- a/src/main/java/com/example/FixLog/config/SecurityConfig.java +++ b/src/main/java/com/example/FixLog/config/SecurityConfig.java @@ -32,14 +32,22 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests(auth -> auth .requestMatchers(HttpMethod.GET, "/auth/**").permitAll() .requestMatchers(HttpMethod.POST, "/auth/**").permitAll() + .requestMatchers(HttpMethod.POST, "/members/signup").permitAll() .requestMatchers(HttpMethod.GET, "/members/check-email").permitAll() .requestMatchers(HttpMethod.GET, "/members/check-nickname").permitAll() + .requestMatchers(HttpMethod.GET, "/search/**").permitAll() + .requestMatchers(HttpMethod.GET, "/posts/**").permitAll() + // h2-console (로컬 테스트용) .requestMatchers(HttpMethod.GET, "/h2-console/**").permitAll() + // 배포 확인용 임시 허용 + .requestMatchers(HttpMethod.GET, "/test", "/test/**").permitAll() + // 그 외 모든 요청은 인증 필요 .requestMatchers(HttpMethod.GET, "/test", "/test/**").permitAll() // 테스트용 허용 + .anyRequest().authenticated() ) - .headers(headers -> headers.frameOptions(frame -> frame.disable())) // H2 콘솔용 + .headers(headers -> headers.frameOptions(frame -> frame.disable())) // H2 콘솔 .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); @@ -59,4 +67,4 @@ public PasswordEncoder passwordEncoder() { public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/controller/MemberController.java b/src/main/java/com/example/FixLog/controller/MemberController.java index 8342511..1e7c9f3 100644 --- a/src/main/java/com/example/FixLog/controller/MemberController.java +++ b/src/main/java/com/example/FixLog/controller/MemberController.java @@ -2,7 +2,9 @@ import com.example.FixLog.domain.member.Member; import com.example.FixLog.dto.Response; +import com.example.FixLog.dto.WithdrawRequestDto; import com.example.FixLog.dto.member.MemberInfoResponseDto; +import com.example.FixLog.dto.member.ProfilePreviewResponseDto; import com.example.FixLog.dto.member.SignupRequestDto; import com.example.FixLog.dto.member.DuplicateCheckResponseDto; import com.example.FixLog.service.MemberService; @@ -50,9 +52,22 @@ public ResponseEntity> getMyInfo(@Authentication return ResponseEntity.ok(Response.success("회원 정보 조회 성공", responseDto)); } + @GetMapping("/profile-preview") + public ResponseEntity> getProfilePreview() { + Member member = memberService.getCurrentMemberInfo(); + ProfilePreviewResponseDto dto = new ProfilePreviewResponseDto( + member.getNickname(), + member.getProfileImageUrl() + ); + return ResponseEntity.ok(Response.success("닉네임&프로필사진 조회 성공", dto)); + } + @DeleteMapping("/me") - public ResponseEntity> withdraw(@AuthenticationPrincipal Member member) { - memberService.withdraw(member); + public ResponseEntity> withdraw( + @AuthenticationPrincipal Member member, + @RequestBody WithdrawRequestDto request + ) { + memberService.withdraw(member, request.getPassword()); return ResponseEntity.ok(Response.success("회원 탈퇴 성공", null)); } } \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/controller/MypageMemberController.java b/src/main/java/com/example/FixLog/controller/MypageMemberController.java new file mode 100644 index 0000000..630cea4 --- /dev/null +++ b/src/main/java/com/example/FixLog/controller/MypageMemberController.java @@ -0,0 +1,83 @@ +package com.example.FixLog.controller; + +import com.example.FixLog.dto.PresignResponseDto; +import com.example.FixLog.dto.Response; +import com.example.FixLog.dto.member.edit.EditNicknameRequestDto; +import com.example.FixLog.dto.member.edit.EditPasswordRequestDto; +import com.example.FixLog.dto.member.edit.EditBioRequestDto; +import com.example.FixLog.exception.CustomException; +import com.example.FixLog.exception.ErrorCode; +import com.example.FixLog.service.S3Service; +import com.example.FixLog.service.MemberService; +import com.example.FixLog.domain.member.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import jakarta.validation.Valid; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/mypage") +public class MypageMemberController { + + private final MemberService memberService; + private final S3Service s3Service; + + @PatchMapping("/members/nickname") + public ResponseEntity> editNickname( + @RequestBody @Valid EditNicknameRequestDto requestDto + ) { + Member member = memberService.getCurrentMemberInfo(); + memberService.editNickname(member, requestDto.getNickname()); + return ResponseEntity.ok(Response.success("닉네임 수정 성공", "SUCCESS")); + } + + @PatchMapping("/members/password") + public ResponseEntity> editPassword( + @RequestBody @Valid EditPasswordRequestDto requestDto + ) { + Member member = memberService.getCurrentMemberInfo(); + memberService.editPassword(member, requestDto); + return ResponseEntity.ok(Response.success("비밀번호 변경 성공", "SUCCESS")); + } + + @GetMapping("/members/profile-image/presign") + public ResponseEntity> presignProfileImage( + @AuthenticationPrincipal Member member, + @RequestParam String filename + ) { + if (member == null) throw new CustomException(ErrorCode.UNAUTHORIZED); + + String key = s3Service.generateKey("profile", filename); + String uploadUrl = s3Service.generatePresignedUrl("profile", filename, 15); + String fileUrl = s3Service.getObjectUrl(key); + + PresignResponseDto dto = new PresignResponseDto(uploadUrl, fileUrl); + return ResponseEntity.ok(Response.success("Presigned URL 발급 성공", dto)); + } + + @PatchMapping("/members/profile-image") + public ResponseEntity> updateProfileImageUrl( + @AuthenticationPrincipal Member member, + @RequestBody Map body + ) { + String imageUrl = body.get("imageUrl"); + if (imageUrl == null || imageUrl.isBlank()) { + throw new CustomException(ErrorCode.INVALID_REQUEST); + } + memberService.editProfileImage(member, imageUrl); + return ResponseEntity.ok(Response.success("프로필 이미지 저장 성공", "SUCCESS")); + } + + @PatchMapping("/members/bio") + public ResponseEntity> editBio( + @RequestBody @Valid EditBioRequestDto requestDto + ) { + Member member = memberService.getCurrentMemberInfo(); + memberService.editBio(member, requestDto.getBio()); + return ResponseEntity.ok(Response.success("소개글 수정 성공", "SUCCESS")); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/controller/MypagePostController.java b/src/main/java/com/example/FixLog/controller/MypagePostController.java index 4635ef6..87c6f23 100644 --- a/src/main/java/com/example/FixLog/controller/MypagePostController.java +++ b/src/main/java/com/example/FixLog/controller/MypagePostController.java @@ -33,18 +33,18 @@ public ResponseEntity>> getMyPos return ResponseEntity.ok(Response.success("내가 작성한 글 보기 성공", data)); } - // 내가 좋아요한 글 - @GetMapping("/likes") - public ResponseEntity>> getLikedPosts( - @AuthenticationPrincipal UserDetails userDetails, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "4") int size, - @RequestParam(defaultValue = "0") int sort) { - - String email = userDetails.getUsername(); - PageResponseDto result = mypagePostService.getLikedPosts(email, page, sort, size); - return ResponseEntity.ok(Response.success("내가 좋아요한 글 보기 성공", result)); - } +// // 내가 좋아요한 글 +// @GetMapping("/likes") +// public ResponseEntity>> getLikedPosts( +// @AuthenticationPrincipal UserDetails userDetails, +// @RequestParam(defaultValue = "0") int page, +// @RequestParam(defaultValue = "4") int size, +// @RequestParam(defaultValue = "0") int sort) { +// +// String email = userDetails.getUsername(); +// PageResponseDto result = mypagePostService.getLikedPosts(email, page, sort, size); +// return ResponseEntity.ok(Response.success("내가 좋아요한 글 보기 성공", result)); +// } } diff --git a/src/main/java/com/example/FixLog/domain/member/Member.java b/src/main/java/com/example/FixLog/domain/member/Member.java index 8ec21d2..51ea83d 100644 --- a/src/main/java/com/example/FixLog/domain/member/Member.java +++ b/src/main/java/com/example/FixLog/domain/member/Member.java @@ -41,10 +41,6 @@ public class Member implements UserDetails { @Column(nullable = false) private Boolean isDeleted = false; - public void setIsDeleted(boolean isDeleted) { - this.isDeleted = isDeleted; - } - @Enumerated(EnumType.STRING) @Column(nullable = false) private SocialType socialType = SocialType.EMAIL; @@ -57,26 +53,22 @@ public void setIsDeleted(boolean isDeleted) { @Column private LocalDateTime updatedAt; - // 프로필 사진 url, 지금은 nullable 이지만 나중에 기본값 설정 + // 프로필 사진 URL @Column private String profileImageUrl; @Column(length = 200) private String bio; + // 게시글 연관관계 @OneToMany(mappedBy = "userId", cascade = CascadeType.ALL, orphanRemoval = true) private List posts = new ArrayList<>(); - // 북마크 폴더 + // 북마크 폴더 (계정당 1개) @OneToOne(mappedBy = "userId", cascade = CascadeType.ALL, orphanRemoval = true) - private BookmarkFolder bookmarkFolderId; - // 우선은 계정 당 폴더 하나만 있는 걸로 생성 - // @OneToMany(mappedBy = "userId", cascade = CascadeType.ALL, orphanRemoval = true) - // private List bookmarkFolders = new ArrayList<>(); - - // Member 객체를 정적 팩토리 방식으로 회원가입 시에 생성하는 메서드 - // Member 객체를 정적 팩토리 방식으로 생성하는 메서드 - // Creates a Member object using a static factory method + private BookmarkFolder bookmarkFolder; + + // 정적 팩토리 메서드 public static Member of(String email, String password, String nickname, SocialType socialType) { Member member = new Member(); member.email = email; @@ -84,14 +76,34 @@ public static Member of(String email, String password, String nickname, SocialTy member.nickname = nickname; member.socialType = socialType; member.isDeleted = false; - member.profileImageUrl = "https://dummyimage.com/200x200/cccccc/ffffff&text=Profile"; // 기본 프로필 이미지(임시) + member.profileImageUrl = null; // 기본 이미지는 응답 시 처리 return member; } - public void setProfileImageUrl(String profileImageUrl) { - this.profileImageUrl = profileImageUrl; + // -------------------- 도메인 메서드 -------------------- + + public void updateNickname(String nickname) { + this.nickname = nickname; } + public void updatePassword(String encodedPassword) { + this.password = encodedPassword; + } + + public void updateProfileImage(String url) { + this.profileImageUrl = url; + } + + public void updateBio(String bio) { + this.bio = bio; + } + + public void markAsDeleted() { + this.isDeleted = true; + } + + // -------------------- Spring Security -------------------- + @Override public Collection getAuthorities() { return List.of(new SimpleGrantedAuthority("ROLE_USER")); // 기본 권한 @@ -99,26 +111,26 @@ public Collection getAuthorities() { @Override public String getUsername() { - return this.email; // 로그인 시 사용할 사용자 식별자 + return this.email; // 로그인 시 사용할 식별자 } @Override public boolean isAccountNonExpired() { - return true; // 계정 만료 여부 (true = 사용 가능) + return true; // 계정 만료 안 됨 } @Override public boolean isAccountNonLocked() { - return true; // 계정 잠금 여부 (true = 잠금 아님) + return true; // 잠금 아님 } @Override public boolean isCredentialsNonExpired() { - return true; // 비밀번호 만료 여부 + return true; // 비밀번호 만료 안 됨 } @Override public boolean isEnabled() { - return !this.isDeleted; // 탈퇴 여부 기반 활성 상태 + return !this.isDeleted; // 탈퇴 계정은 비활성화 } } \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/dto/PresignResponseDto.java b/src/main/java/com/example/FixLog/dto/PresignResponseDto.java new file mode 100644 index 0000000..39ec68f --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/PresignResponseDto.java @@ -0,0 +1,11 @@ +package com.example.FixLog.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PresignResponseDto { + private final String uploadUrl; // PUT 전용 Presigned URL + private final String fileUrl; // public하게 접근 가능한 URL +} diff --git a/src/main/java/com/example/FixLog/dto/WithdrawRequestDto.java b/src/main/java/com/example/FixLog/dto/WithdrawRequestDto.java new file mode 100644 index 0000000..680dafd --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/WithdrawRequestDto.java @@ -0,0 +1,8 @@ +package com.example.FixLog.dto; + +import lombok.Getter; + +@Getter +public class WithdrawRequestDto { + private String password; +} diff --git a/src/main/java/com/example/FixLog/dto/member/LoginResponseDto.java b/src/main/java/com/example/FixLog/dto/member/LoginResponseDto.java index fb7f80c..3fe9560 100644 --- a/src/main/java/com/example/FixLog/dto/member/LoginResponseDto.java +++ b/src/main/java/com/example/FixLog/dto/member/LoginResponseDto.java @@ -1,5 +1,7 @@ package com.example.FixLog.dto.member; +import com.example.FixLog.domain.member.Member; +import com.example.FixLog.util.DefaultImage; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.AllArgsConstructor; import lombok.Getter; @@ -12,4 +14,15 @@ public class LoginResponseDto { private String accessToken; private String nickname; private String profileImageUrl; -} + + public static LoginResponseDto from(Member member, String accessToken) { + return new LoginResponseDto( + member.getUserId(), + accessToken, + member.getNickname(), + member.getProfileImageUrl() != null + ? member.getProfileImageUrl() + : DefaultImage.PROFILE + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/dto/member/MemberInfoResponseDto.java b/src/main/java/com/example/FixLog/dto/member/MemberInfoResponseDto.java index 45ced06..1596a53 100644 --- a/src/main/java/com/example/FixLog/dto/member/MemberInfoResponseDto.java +++ b/src/main/java/com/example/FixLog/dto/member/MemberInfoResponseDto.java @@ -4,6 +4,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; +// 서비스단에서 최대한 처리하도록 수정 @Getter @AllArgsConstructor public class MemberInfoResponseDto { diff --git a/src/main/java/com/example/FixLog/dto/member/ProfilePreviewResponseDto.java b/src/main/java/com/example/FixLog/dto/member/ProfilePreviewResponseDto.java new file mode 100644 index 0000000..17f8ec7 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/member/ProfilePreviewResponseDto.java @@ -0,0 +1,11 @@ +package com.example.FixLog.dto.member; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ProfilePreviewResponseDto { + private String nickname; + private String profileImageUrl; +} diff --git a/src/main/java/com/example/FixLog/dto/member/edit/EditBioRequestDto.java b/src/main/java/com/example/FixLog/dto/member/edit/EditBioRequestDto.java new file mode 100644 index 0000000..488b097 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/member/edit/EditBioRequestDto.java @@ -0,0 +1,13 @@ +package com.example.FixLog.dto.member.edit; + +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class EditBioRequestDto { + + @Size(max = 200, message = "소개글은 최대 200자까지 입력 가능합니다.") + private String bio; +} diff --git a/src/main/java/com/example/FixLog/dto/member/edit/EditNicknameRequestDto.java b/src/main/java/com/example/FixLog/dto/member/edit/EditNicknameRequestDto.java new file mode 100644 index 0000000..dbd4ce7 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/member/edit/EditNicknameRequestDto.java @@ -0,0 +1,15 @@ +package com.example.FixLog.dto.member.edit; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class EditNicknameRequestDto { + + @NotBlank(message = "닉네임은 비어 있을 수 없습니다.") + @Size(min = 2, max = 20, message = "닉네임은 2자 이상 20자 이하로 입력해주세요.") + private String nickname; +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/dto/member/edit/EditPasswordRequestDto.java b/src/main/java/com/example/FixLog/dto/member/edit/EditPasswordRequestDto.java new file mode 100644 index 0000000..50d3931 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/member/edit/EditPasswordRequestDto.java @@ -0,0 +1,18 @@ +package com.example.FixLog.dto.member.edit; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class EditPasswordRequestDto { + + @NotBlank(message = "현재 비밀번호를 입력해주세요.") + private String currentPassword; + + @NotBlank(message = "새 비밀번호를 입력해주세요.") + @Size(min = 8, max = 30, message = "비밀번호는 8자 이상 30자 이하로 입력해주세요.") + private String newPassword; +} diff --git a/src/main/java/com/example/FixLog/dto/member/edit/EditProfileImageRequestDto.java b/src/main/java/com/example/FixLog/dto/member/edit/EditProfileImageRequestDto.java new file mode 100644 index 0000000..f9d8603 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/member/edit/EditProfileImageRequestDto.java @@ -0,0 +1,13 @@ +package com.example.FixLog.dto.member.edit; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class EditProfileImageRequestDto { + + @NotBlank(message = "프로필 이미지 URL은 필수입니다.") + private String profileImageUrl; +} diff --git a/src/main/java/com/example/FixLog/exception/ErrorCode.java b/src/main/java/com/example/FixLog/exception/ErrorCode.java index 7f7e325..61a45f4 100644 --- a/src/main/java/com/example/FixLog/exception/ErrorCode.java +++ b/src/main/java/com/example/FixLog/exception/ErrorCode.java @@ -24,7 +24,11 @@ public enum ErrorCode { SORT_NOT_EXIST(HttpStatus.BAD_REQUEST, "사용할 수 없는 정렬입니다."), INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다."), REQUIRED_TAGS_MISSING(HttpStatus.BAD_REQUEST, "태그를 선택해주세요."), - REQUIRED_CONTENT_MISSING(HttpStatus.BAD_REQUEST, "필수 본문이 입력되지 않았습니다."); + REQUIRED_CONTENT_MISSING(HttpStatus.BAD_REQUEST, "필수 본문이 입력되지 않았습니다."), + SAME_AS_OLD_PASSWORD(HttpStatus.BAD_REQUEST, "다른 비밀번호 입력 바랍니다."), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "권한이 없습니다."), + INVALID_REQUEST(HttpStatus.BAD_REQUEST, "요청 데이터가 유효하지 않습니다."), + S3_UPLOAD_FAILED(HttpStatus.BAD_REQUEST, "S3 파일 업로드에 실패했습니다."); private final HttpStatus status; private final String message; diff --git a/src/main/java/com/example/FixLog/service/AuthService.java b/src/main/java/com/example/FixLog/service/AuthService.java index e47842d..3a941e8 100644 --- a/src/main/java/com/example/FixLog/service/AuthService.java +++ b/src/main/java/com/example/FixLog/service/AuthService.java @@ -29,17 +29,7 @@ public LoginResponseDto login(LoginRequestDto requestDto) { String token = jwtUtil.createToken(member.getUserId(), member.getEmail()); - // 로그인 응답 시에도 null-safe하게 처리 - String profileUrl = member.getProfileImageUrl() != null - ? member.getProfileImageUrl() - : "https://dummyimage.com/200x200/cccccc/ffffff&text=Profile"; - - return new LoginResponseDto( - member.getUserId(), - token, - member.getNickname(), - member.getProfileImageUrl() != null - ? member.getProfileImageUrl() - : "https://your-cdn.com/images/default-profile.png"); + // 응답에서 null-safe하게 기본 이미지 처리 포함 + return LoginResponseDto.from(member, token); } -} +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/service/MemberService.java b/src/main/java/com/example/FixLog/service/MemberService.java index 5138c30..760be59 100644 --- a/src/main/java/com/example/FixLog/service/MemberService.java +++ b/src/main/java/com/example/FixLog/service/MemberService.java @@ -3,16 +3,24 @@ import com.example.FixLog.domain.bookmark.BookmarkFolder; import com.example.FixLog.domain.member.Member; import com.example.FixLog.domain.member.SocialType; +import com.example.FixLog.dto.member.LoginResponseDto; +import com.example.FixLog.dto.member.MemberInfoResponseDto; import com.example.FixLog.dto.member.SignupRequestDto; +import com.example.FixLog.dto.member.edit.EditPasswordRequestDto; import com.example.FixLog.exception.CustomException; import com.example.FixLog.exception.ErrorCode; import com.example.FixLog.repository.MemberRepository; import com.example.FixLog.repository.bookmark.BookmarkFolderRepository; +import com.example.FixLog.util.DefaultImage; +import com.example.FixLog.util.DefaultText; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; @Service @RequiredArgsConstructor @@ -22,33 +30,134 @@ public class MemberService { private final PasswordEncoder passwordEncoder; private final BookmarkFolderRepository bookmarkFolderRepository; + /** + * 회원가입 로직 + */ + @Transactional public void signup(SignupRequestDto request) { - // 이메일 중복 검사 - if (isEmailDuplicated(request.getEmail())) { - throw new CustomException(ErrorCode.EMAIL_DUPLICATED); - } + validateEmail(request.getEmail()); + validateNickname(request.getNickname()); - // 닉네임 중복 검사 - if (isNicknameDuplicated(request.getNickname())) { - throw new CustomException(ErrorCode.NICKNAME_DUPLICATED); - } - - // 문제 없으면 저장 Member member = Member.of( request.getEmail(), passwordEncoder.encode(request.getPassword()), request.getNickname(), SocialType.EMAIL ); - // 기본 프로필 이미지 URL 생성 - member.setProfileImageUrl("https://dummyimage.com/200x200/cccccc/ffffff&text=Profile"); - // 먼저 회원 정보 저장 + + memberRepository.save(member); + bookmarkFolderRepository.save(new BookmarkFolder(member)); + } + + /** + * 현재 로그인한 사용자 조회 + */ + @Transactional(readOnly = true) + public Member getCurrentMemberInfo() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + return memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.USER_EMAIL_NOT_FOUND)); + } + + /** + * 닉네임 수정 + */ + @Transactional + public void editNickname(Member member, String newNickname) { + validateNickname(newNickname); + member.updateNickname(newNickname); + memberRepository.save(member); + } + + /** + * 비밀번호 수정 + */ + @Transactional + public void editPassword(Member member, EditPasswordRequestDto dto) { + validatePasswordChange(dto.getCurrentPassword(), dto.getNewPassword(), member.getPassword()); + member.updatePassword(passwordEncoder.encode(dto.getNewPassword())); + memberRepository.save(member); + } + + /** + * 프로필 이미지 수정 + */ + @Transactional + public void editProfileImage(Member member, String newProfileImageUrl) { + String finalImage = Optional.ofNullable(newProfileImageUrl).orElse(DefaultImage.PROFILE); + member.updateProfileImage(finalImage); + memberRepository.save(member); + } + + /** + * 소개글 수정 + */ + @Transactional + public void editBio(Member member, String newBio) { + String finalBio = Optional.ofNullable(newBio).orElse(DefaultText.BIO); + member.updateBio(finalBio); + memberRepository.save(member); + } + + /** + * 회원 탈퇴 + */ + @Transactional + public void withdraw(Member member, String password) { + if (!passwordEncoder.matches(password, member.getPassword())) { + throw new CustomException(ErrorCode.INVALID_PASSWORD); + } + member.markAsDeleted(); memberRepository.save(member); + } + + /** + * 내 정보 조회 응답 DTO 생성 + */ + @Transactional(readOnly = true) + public MemberInfoResponseDto getMyInfo() { + Member member = getCurrentMemberInfo(); + + String profileImage = Optional.ofNullable(member.getProfileImageUrl()).orElse(DefaultImage.PROFILE); + String bio = Optional.ofNullable(member.getBio()).orElse(DefaultText.BIO); + + return new MemberInfoResponseDto( + member.getEmail(), + member.getNickname(), + profileImage, + bio, + member.getSocialType() + ); + } + + /** + * 로그인 응답 DTO 생성 + */ + public LoginResponseDto getLoginResponse(Member member, String accessToken) { + String profileImage = Optional.ofNullable(member.getProfileImageUrl()).orElse(DefaultImage.PROFILE); + + return new LoginResponseDto( + member.getUserId(), + accessToken, + member.getNickname(), + profileImage + ); + } + + // ========================== 검증 메서드 ========================== - // 기본 폴더 생성 - BookmarkFolder newFolder = new BookmarkFolder(member); - bookmarkFolderRepository.save(newFolder); + public void validateEmail(String email) { + if (isEmailDuplicated(email)) { + throw new CustomException(ErrorCode.EMAIL_DUPLICATED); + } + } + public void validateNickname(String nickname) { + if (isNicknameDuplicated(nickname)) { + throw new CustomException(ErrorCode.NICKNAME_DUPLICATED); + } } public boolean isEmailDuplicated(String email) { @@ -58,19 +167,13 @@ public boolean isEmailDuplicated(String email) { public boolean isNicknameDuplicated(String nickname) { return memberRepository.findByNickname(nickname).isPresent(); } - - // 현재 로그인한 사용자 정보 member 객체로 반환 - public Member getCurrentMemberInfo(){ - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String userEmail = authentication.getName(); - return memberRepository.findByEmail(userEmail) - .orElseThrow(() -> new CustomException(ErrorCode.USER_EMAIL_NOT_FOUND)); - } - - // 회원탈퇴 - public void withdraw(Member member) { - member.setIsDeleted(true); - memberRepository.save(member); - } -} + public void validatePasswordChange(String currentPassword, String newPassword, String encodedPassword) { + if (!passwordEncoder.matches(currentPassword, encodedPassword)) { + throw new CustomException(ErrorCode.INVALID_PASSWORD); + } + if (passwordEncoder.matches(newPassword, encodedPassword)) { + throw new CustomException(ErrorCode.SAME_AS_OLD_PASSWORD); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/service/MypagePostService.java b/src/main/java/com/example/FixLog/service/MypagePostService.java index b9aa201..84873af 100644 --- a/src/main/java/com/example/FixLog/service/MypagePostService.java +++ b/src/main/java/com/example/FixLog/service/MypagePostService.java @@ -23,7 +23,6 @@ @Service public class MypagePostService { - private final PostRepository postRepository; private final MemberRepository memberRepository; private final ForkRepository forkRepository; @@ -36,6 +35,7 @@ public MypagePostService(PostRepository postRepository, MemberRepository memberR this.postLikeRepository = postLikeRepository; } + // 내가 쓴 글 보기 public PageResponseDto getMyPosts(String email, int page, int sort, int size) { Member member = memberRepository.findByEmail(email) @@ -61,7 +61,6 @@ public PageResponseDto getMyPosts(String email, int page, ) ); } - // 내가 좋아요한 글 보기 public PageResponseDto getLikedPosts(String email, int page, int sort, int size) { Member member = memberRepository.findByEmail(email) @@ -86,6 +85,4 @@ public PageResponseDto getLikedPosts(String email, int pa MyPostPageResponseDto.from(post, forkCountMap.getOrDefault(post.getPostId(), 0)) ); } - - } diff --git a/src/main/java/com/example/FixLog/service/S3Service.java b/src/main/java/com/example/FixLog/service/S3Service.java new file mode 100644 index 0000000..afd6158 --- /dev/null +++ b/src/main/java/com/example/FixLog/service/S3Service.java @@ -0,0 +1,64 @@ +package com.example.FixLog.service; + +import com.amazonaws.HttpMethod; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.example.FixLog.exception.CustomException; +import com.example.FixLog.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Date; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class S3Service { + + private final AmazonS3 amazonS3; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + public String upload(MultipartFile file, String dirName) { + String key = generateKey(dirName, file.getOriginalFilename()); + + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentType(file.getContentType()); + metadata.setContentLength(file.getSize()); + + try (InputStream is = file.getInputStream()) { + amazonS3.putObject(bucket, key, is, metadata); + } catch (IOException e) { + throw new CustomException(ErrorCode.S3_UPLOAD_FAILED); + } + + return getObjectUrl(key); + } + + public String generateKey(String dirName, String filename) { + return dirName + "/" + UUID.randomUUID() + "_" + filename; + } + + public String generatePresignedUrl(String dirName, String filename, int minutes) { + String key = generateKey(dirName, filename); + Date expiration = new Date(System.currentTimeMillis() + minutes * 60L * 1000L); + + GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucket, key) + .withMethod(HttpMethod.PUT) + .withExpiration(expiration); + + URL url = amazonS3.generatePresignedUrl(request); + return url.toString(); + } + + public String getObjectUrl(String key) { + return amazonS3.getUrl(bucket, key).toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/util/DefaultImage.java b/src/main/java/com/example/FixLog/util/DefaultImage.java new file mode 100644 index 0000000..fa1fd90 --- /dev/null +++ b/src/main/java/com/example/FixLog/util/DefaultImage.java @@ -0,0 +1,5 @@ +package com.example.FixLog.util; + +public class DefaultImage { + public static final String PROFILE = "https://dummyimage.com/200x200/cccccc/ffffff&text=Profile"; // 임시 기본 프로필 이미지 +} diff --git a/src/main/java/com/example/FixLog/util/DefaultText.java b/src/main/java/com/example/FixLog/util/DefaultText.java new file mode 100644 index 0000000..defd0a6 --- /dev/null +++ b/src/main/java/com/example/FixLog/util/DefaultText.java @@ -0,0 +1,5 @@ +package com.example.FixLog.util; + +public class DefaultText { + public static final String BIO = "오늘도 에러 없는 하루 보내세요!"; +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index c1ce375..760dbb0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,22 +1,18 @@ +# Correct property name spring.application.name=FixLog - -//# DB setting -//spring.h2.console.enabled=true -''spring.h2.console.path=/h2-console - -//# DataBase Info -//spring.datasource.url=jdbc:h2:tcp://localhost/~/fixlog -//spring.datasource.driver-class-name=org.h2.Driver -//spring.datasource.username=sa -//spring.datasource.password= - -//spring.jpa.show-sql=true -//spring.jpa.hibernate.ddl-auto=update -//spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect - -//# JWT -//jwt.secret=fixlogfixlogfixlogfixlogfixlog1234 -//jwt.expiration-time=86400000 +## DB setting +#spring.h2.console.enabled=true +#spring.h2.console.path=/h2-console +# +## DataBase Info +#spring.datasource.url=jdbc:h2:tcp://localhost/~/fixlog +#spring.datasource.driver-class-name=org.h2.Driver +#spring.datasource.username=sa +#spring.datasource.password= +# +#spring.jpa.show-sql=true +#spring.jpa.hibernate.ddl-auto=update +#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect ##### dev ##### server.port=8083 @@ -26,11 +22,23 @@ spring.datasource.url=${MYSQL_URL} spring.datasource.username=${MYSQL_USERNAME} spring.datasource.password=${MYSQL_PASSWORD} -spring.jpa.hibernate.ddl-auto=create + +spring.datasource.url=jdbc:mysql://fixlog-db.c7cau8y2srl7.ap-northeast-2.rds.amazonaws.com:3306/fixlog?serverTimezone=Asia/Seoul +spring.datasource.username=admin +spring.datasource.password=${MYSQL_PASSWORD} + +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect spring.jpa.properties.hibernate.format_sql=true -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect -##### jwt ##### +# AWS S3 configuration +cloud.aws.credentials.access-key=${AWS_ACCESS_KEY_ID} +cloud.aws.credentials.secret-key=${AWS_SECRET_ACCESS_KEY} +cloud.aws.region.static=${AWS_REGION} +cloud.aws.s3.bucket=${AWS_S3_BUCKET} + + spring.jwt.secret=${JWT_KEY} # 로그 레벨 설정