Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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: 8 additions & 4 deletions src/main/java/com/example/FixLog/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,21 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
// 비로그인 허용 경로
.requestMatchers(HttpMethod.POST, "/members/signup").permitAll()
.requestMatchers(HttpMethod.POST, "/auth/login").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()
// 그 외 모든 요청은 인증 필요
.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 @@ -52,9 +57,8 @@ public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

// 인증 매니저 (선택: 로그인 시 AuthenticationManager 사용 가능)
@Bean
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