Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ out/

### VS Code ###
.vscode/

### Environment variables ###
.env
*.env
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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') {
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/example/FixLog/FixLogApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
33 changes: 33 additions & 0 deletions src/main/java/com/example/FixLog/config/AwsS3Config.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
12 changes: 10 additions & 2 deletions src/main/java/com/example/FixLog/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -59,4 +67,4 @@ public PasswordEncoder passwordEncoder() {
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
}
19 changes: 17 additions & 2 deletions src/main/java/com/example/FixLog/controller/MemberController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -50,9 +52,22 @@ public ResponseEntity<Response<MemberInfoResponseDto>> getMyInfo(@Authentication
return ResponseEntity.ok(Response.success("회원 정보 조회 성공", responseDto));
}

@GetMapping("/profile-preview")
public ResponseEntity<Response<ProfilePreviewResponseDto>> getProfilePreview() {
Member member = memberService.getCurrentMemberInfo();
ProfilePreviewResponseDto dto = new ProfilePreviewResponseDto(
member.getNickname(),
member.getProfileImageUrl()
);
return ResponseEntity.ok(Response.success("닉네임&프로필사진 조회 성공", dto));
}

@DeleteMapping("/me")
public ResponseEntity<Response<Void>> withdraw(@AuthenticationPrincipal Member member) {
memberService.withdraw(member);
public ResponseEntity<Response<Void>> withdraw(
@AuthenticationPrincipal Member member,
@RequestBody WithdrawRequestDto request
) {
memberService.withdraw(member, request.getPassword());
return ResponseEntity.ok(Response.success("회원 탈퇴 성공", null));
}
}
Original file line number Diff line number Diff line change
@@ -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<Response<String>> 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<Response<String>> 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<Response<PresignResponseDto>> 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<Response<String>> updateProfileImageUrl(
@AuthenticationPrincipal Member member,
@RequestBody Map<String, String> 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<Response<String>> editBio(
@RequestBody @Valid EditBioRequestDto requestDto
) {
Member member = memberService.getCurrentMemberInfo();
memberService.editBio(member, requestDto.getBio());
return ResponseEntity.ok(Response.success("소개글 수정 성공", "SUCCESS"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,18 @@ public ResponseEntity<Response<PageResponseDto<MyPostPageResponseDto>>> getMyPos
return ResponseEntity.ok(Response.success("내가 작성한 글 보기 성공", data));
}

// 내가 좋아요한 글
@GetMapping("/likes")
public ResponseEntity<Response<PageResponseDto<MyPostPageResponseDto>>> getLikedPosts(
@AuthenticationPrincipal UserDetails userDetails,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "4") int size,
@RequestParam(defaultValue = "0") int sort) {

String email = userDetails.getUsername();
PageResponseDto<MyPostPageResponseDto> result = mypagePostService.getLikedPosts(email, page, sort, size);
return ResponseEntity.ok(Response.success("내가 좋아요한 글 보기 성공", result));
}
// // 내가 좋아요한 글
// @GetMapping("/likes")
// public ResponseEntity<Response<PageResponseDto<MyPostPageResponseDto>>> getLikedPosts(
// @AuthenticationPrincipal UserDetails userDetails,
// @RequestParam(defaultValue = "0") int page,
// @RequestParam(defaultValue = "4") int size,
// @RequestParam(defaultValue = "0") int sort) {
//
// String email = userDetails.getUsername();
// PageResponseDto<MyPostPageResponseDto> result = mypagePostService.getLikedPosts(email, page, sort, size);
// return ResponseEntity.ok(Response.success("내가 좋아요한 글 보기 성공", result));
// }


}
56 changes: 34 additions & 22 deletions src/main/java/com/example/FixLog/domain/member/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -57,68 +53,84 @@ 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<Post> 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<BookmarkFolder> 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;
member.password = password;
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<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_USER")); // 기본 권한
}

@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; // 탈퇴 계정은 비활성화
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/example/FixLog/dto/PresignResponseDto.java
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 8 additions & 0 deletions src/main/java/com/example/FixLog/dto/WithdrawRequestDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.FixLog.dto;

import lombok.Getter;

@Getter
public class WithdrawRequestDto {
private String password;
}
Loading