From b335b21e2a3e78907684d36c878eece2df5d9e5d Mon Sep 17 00:00:00 2001 From: Lee SeungJoon Date: Wed, 1 Apr 2026 17:18:35 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=204=EC=A3=BC=EC=B0=A8=20=EA=B3=BC?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + docker-compose.yml | 2 +- .../exception/GlobalExceptionHandler.java | 2 +- .../controller/CommentController.java | 33 ++++++++++++ .../controller/PostController.java | 26 ++++++++-- .../study/springbootstudy/domain/Comment.java | 34 +++++++++++++ .../study/springbootstudy/domain/Post.java | 36 +++++++++---- .../dto/CommentRequestDto.java | 13 +++++ .../springbootstudy/dto/PostRequestDto.java | 11 ++-- .../repository/CommentRepository.java | 7 +++ .../service/CommentService.java | 50 +++++++++++++++++++ .../springbootstudy/service/PostService.java | 28 +++++++++++ src/main/resources/application.yml | 2 +- test.http | 6 +-- 14 files changed, 226 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/study/springbootstudy/controller/CommentController.java create mode 100644 src/main/java/com/study/springbootstudy/domain/Comment.java create mode 100644 src/main/java/com/study/springbootstudy/dto/CommentRequestDto.java create mode 100644 src/main/java/com/study/springbootstudy/repository/CommentRepository.java create mode 100644 src/main/java/com/study/springbootstudy/service/CommentService.java diff --git a/build.gradle b/build.gradle index ca9420c..f2d1997 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ dependencies { // implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // DB를 자바 코드로 다루게 해주는 도구 + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6' runtimeOnly 'com.mysql:mysql-connector-j' // MySQL과 연결해주는 드라이버 compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/docker-compose.yml b/docker-compose.yml index d645d14..f013b17 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: image: mysql:8.0 restart: always environment: - MYSQL_DATABASE: proovy_db + MYSQL_DATABASE: springboot_study MYSQL_ROOT_PASSWORD: root ports: - "3306:3306" \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/common/exception/GlobalExceptionHandler.java b/src/main/java/com/study/springbootstudy/common/exception/GlobalExceptionHandler.java index 25ba19e..a06aa32 100644 --- a/src/main/java/com/study/springbootstudy/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/study/springbootstudy/common/exception/GlobalExceptionHandler.java @@ -10,7 +10,7 @@ @RestControllerAdvice public class GlobalExceptionHandler { - // 1. 우리가 직접 던진 GeneralException 처리 + // 1. GeneralException 처리 @ExceptionHandler(GeneralException.class) public ApiResponse handleGeneralException(GeneralException e) { return ApiResponse.onFailure(e.getErrorCode(), e.getMessage(), null); diff --git a/src/main/java/com/study/springbootstudy/controller/CommentController.java b/src/main/java/com/study/springbootstudy/controller/CommentController.java new file mode 100644 index 0000000..1fa20e1 --- /dev/null +++ b/src/main/java/com/study/springbootstudy/controller/CommentController.java @@ -0,0 +1,33 @@ +package com.study.springbootstudy.controller; + +import com.study.springbootstudy.common.ApiResponse; +import com.study.springbootstudy.dto.CommentRequestDto; +import com.study.springbootstudy.service.CommentService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +public class CommentController { + + private final CommentService commentService; + + // 댓글 생성 + @PostMapping("/api/posts/{postId}/comments") + public ApiResponse createComment( + @PathVariable Long postId, + @RequestBody @Valid CommentRequestDto request) { + Long commentId = commentService.createComment(postId, request); + return ApiResponse.onSuccess(commentId); + } + + // 댓글 삭제 + @DeleteMapping("/api/comments/{commentId}") + public ApiResponse deleteComment( + @PathVariable Long commentId, + @RequestParam String password) { + commentService.deleteComment(commentId, password); + return ApiResponse.onSuccess(null); + } +} \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/controller/PostController.java b/src/main/java/com/study/springbootstudy/controller/PostController.java index a27ad37..f3c0df3 100644 --- a/src/main/java/com/study/springbootstudy/controller/PostController.java +++ b/src/main/java/com/study/springbootstudy/controller/PostController.java @@ -9,8 +9,8 @@ import org.springframework.web.bind.annotation.*; @RestController -@RequiredArgsConstructor // Service를 주입받기 위해 꼭 필요합니다. -@RequestMapping("/api/posts") // 이 컨트롤러의 기본 주소를 설정합니다. +@RequiredArgsConstructor // Service를 주입받기 위해 꼭 필요 +@RequestMapping("/api/posts") // 이 컨트롤러의 기본 주소를 설정 public class PostController { private final PostService postService; @@ -19,13 +19,31 @@ public class PostController { @PostMapping public ApiResponse createPost(@Valid @RequestBody PostRequestDto request) { Long savedPostId = postService.createPost(request); - return ApiResponse.onSuccess(savedPostId); // 성공 시 저장된 게시글의 ID를 반환! + return ApiResponse.onSuccess(savedPostId); // 성공 시 저장된 게시글의 ID를 반환 } // 2. 게시글 상세 조회 API @GetMapping("/{postId}") public ApiResponse getPost(@PathVariable Long postId) { PostResponseDto response = postService.getPostDetail(postId); - return ApiResponse.onSuccess(response); // 성공 시 게시글 상세 정보를 반환! + return ApiResponse.onSuccess(response); // 성공 시 게시글 상세 정보를 반환 + } + + // 게시글 수정 (PUT) + @PutMapping("/api/posts/{id}") + public ApiResponse updatePost( + @PathVariable Long id, + @RequestBody @Valid PostRequestDto request) { + PostResponseDto responseDto = postService.updatePost(id, request); + return ApiResponse.onSuccess(responseDto); + } + + // 게시글 삭제 (DELETE) - 비밀번호를 쿼리 파라미터(?password=...)로 받음 + @DeleteMapping("/api/posts/{id}") + public ApiResponse deletePost( + @PathVariable Long id, + @RequestParam String password) { + postService.deletePost(id, password); + return ApiResponse.onSuccess(null); } } \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/domain/Comment.java b/src/main/java/com/study/springbootstudy/domain/Comment.java new file mode 100644 index 0000000..4e59872 --- /dev/null +++ b/src/main/java/com/study/springbootstudy/domain/Comment.java @@ -0,0 +1,34 @@ +package com.study.springbootstudy.domain; + +import com.study.springbootstudy.common.exception.GeneralException; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String content; + private String password; // 댓글 작성자 확인용 + + // N:1 관계 매핑 (게시글 하나에 여러 댓글). 지연 로딩으로 성능 최적화 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + // 권한 검증 로직 + public void validatePassword(String inputPassword) { + if (!this.password.equals(inputPassword)) { + throw new GeneralException("AUTH403", "비밀번호가 일치하지 않습니다. 수정/삭제 권한이 없습니다."); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/domain/Post.java b/src/main/java/com/study/springbootstudy/domain/Post.java index 5f5b7ea..e8a2d51 100644 --- a/src/main/java/com/study/springbootstudy/domain/Post.java +++ b/src/main/java/com/study/springbootstudy/domain/Post.java @@ -1,22 +1,38 @@ package com.study.springbootstudy.domain; +import com.study.springbootstudy.common.exception.GeneralException; import jakarta.persistence.*; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; -@Entity // 이 클래스가 DB의 테이블 역할을 한다는 선언 +@Entity @Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor @AllArgsConstructor +@Builder public class Post { - - @Id // 기본키(PK)로 지정 - @GeneratedValue(strategy = GenerationType.IDENTITY) // ID를 1, 2, 3... 자동으로 늘려줌 + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) // 빈 값을 허용하지 않음 private String title; - - @Column(columnDefinition = "TEXT", nullable = false) private String content; + + // 추가: 작성자 확인용 비밀번호 + private String password; + + // 1. 게시글 수정 로직 (Setter 대신 의미 있는 메서드 사용) + public void update(String title, String content) { + this.title = title; + this.content = content; + } + + // 2. 권한 검증 로직 (비밀번호 일치 여부 확인) + public void validatePassword(String inputPassword) { + if (!this.password.equals(inputPassword)) { + throw new GeneralException("AUTH403", "비밀번호가 일치하지 않습니다. 수정/삭제 권한이 없습니다."); + } + } } \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/dto/CommentRequestDto.java b/src/main/java/com/study/springbootstudy/dto/CommentRequestDto.java new file mode 100644 index 0000000..c392496 --- /dev/null +++ b/src/main/java/com/study/springbootstudy/dto/CommentRequestDto.java @@ -0,0 +1,13 @@ +package com.study.springbootstudy.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + +@Getter +public class CommentRequestDto { + @NotBlank(message = "댓글 내용은 필수입니다.") + private String content; + + @NotBlank(message = "비밀번호는 필수입니다.") + private String password; +} \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/dto/PostRequestDto.java b/src/main/java/com/study/springbootstudy/dto/PostRequestDto.java index da1a056..7de4448 100644 --- a/src/main/java/com/study/springbootstudy/dto/PostRequestDto.java +++ b/src/main/java/com/study/springbootstudy/dto/PostRequestDto.java @@ -3,16 +3,17 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.Getter; -import lombok.Setter; @Getter -@Setter public class PostRequestDto { - - @NotBlank(message = "게시글 제목은 필수입니다.") + @NotBlank(message = "제목은 필수입니다.") private String title; - @NotBlank(message = "내용을 입력해주세요.") + @NotBlank(message = "내용은 필수입니다.") @Size(min = 10, message = "내용은 최소 10자 이상이어야 합니다.") private String content; + + // 추가: 클라이언트로부터 비밀번호도 입력받음 + @NotBlank(message = "비밀번호는 필수입니다.") + private String password; } \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/repository/CommentRepository.java b/src/main/java/com/study/springbootstudy/repository/CommentRepository.java new file mode 100644 index 0000000..fa6e684 --- /dev/null +++ b/src/main/java/com/study/springbootstudy/repository/CommentRepository.java @@ -0,0 +1,7 @@ +package com.study.springbootstudy.repository; + +import com.study.springbootstudy.domain.Comment; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/service/CommentService.java b/src/main/java/com/study/springbootstudy/service/CommentService.java new file mode 100644 index 0000000..00a556f --- /dev/null +++ b/src/main/java/com/study/springbootstudy/service/CommentService.java @@ -0,0 +1,50 @@ +package com.study.springbootstudy.service; + +import com.study.springbootstudy.common.exception.GeneralException; +import com.study.springbootstudy.domain.Comment; +import com.study.springbootstudy.domain.Post; +import com.study.springbootstudy.dto.CommentRequestDto; +import com.study.springbootstudy.repository.CommentRepository; +import com.study.springbootstudy.repository.PostRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CommentService { + + private final CommentRepository commentRepository; + private final PostRepository postRepository; + + // [댓글 생성] + @Transactional + public Long createComment(Long postId, CommentRequestDto request) { + // 1. 부모 게시글이 있는지 먼저 확인 + Post post = postRepository.findById(postId) + .orElseThrow(() -> new GeneralException("POST404", "해당 게시글이 존재하지 않습니다.")); + + // 2. 댓글 생성 및 게시글과 연결 + Comment comment = Comment.builder() + .content(request.getContent()) + .password(request.getPassword()) + .post(post) // 핵심! 어떤 게시글의 댓글인지 매핑 + .build(); + + return commentRepository.save(comment).getId(); + } + + // [댓글 삭제] + @Transactional + public void deleteComment(Long commentId, String password) { + Comment comment = commentRepository.findById(commentId) + .orElseThrow(() -> new GeneralException("COMMENT404", "해당 댓글이 존재하지 않습니다.")); + + // 1. 비밀번호 검증 + comment.validatePassword(password); + + // 2. 삭제 + commentRepository.delete(comment); + } +} \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/service/PostService.java b/src/main/java/com/study/springbootstudy/service/PostService.java index b331031..300613a 100644 --- a/src/main/java/com/study/springbootstudy/service/PostService.java +++ b/src/main/java/com/study/springbootstudy/service/PostService.java @@ -36,4 +36,32 @@ public PostResponseDto getPostDetail(Long postId) { // 2. 찾은 게시글을 DTO에 담아 컨트롤러로 보냄 return PostResponseDto.from(post); } + + // [수정] 더티 체킹 활용 + @Transactional + public PostResponseDto updatePost(Long postId, PostRequestDto request) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new GeneralException("POST404", "해당 게시글이 존재하지 않습니다.")); + + // 1. 비밀번호 검증 + post.validatePassword(request.getPassword()); + + // 2. 수정 (트랜잭션 종료 시 JPA가 알아서 DB에 반영해 줌) + post.update(request.getTitle(), request.getContent()); + + return PostResponseDto.from(post); + } + + // [삭제] + @Transactional + public void deletePost(Long postId, String password) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new GeneralException("POST404", "해당 게시글이 존재하지 않습니다.")); + + // 1. 비밀번호 검증 + post.validatePassword(password); + + // 2. 삭제 + postRepository.delete(post); + } } \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5c64e40..02a9c99 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,6 @@ spring: datasource: - url: jdbc:mysql://localhost:3306/proovy_db?serverTimezone=Asia/Seoul + url: jdbc:mysql://localhost:3306/springboot_study?serverTimezone=Asia/Seoul username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver diff --git a/test.http b/test.http index 9e2fe7c..dfcebff 100644 --- a/test.http +++ b/test.http @@ -4,10 +4,10 @@ POST http://localhost:8080/api/posts Content-Type: application/json { - "title": "첫 DB 저장 테스트!", - "content": "프로비 프로젝트 화이팅! 10글자 넘기기." + "title": "스프링부트 첫 DB", + "content": "10글자 채우기ㅣㅣㅣㅣㅣㅣ" } ### 2. 게시글 조회 (GET) -GET http://localhost:8080/api/posts/999 \ No newline at end of file +GET http://localhost:8080/api/posts/1 \ No newline at end of file From 1371b0d79c8e0f5edd1dece1baed6fae398a282b Mon Sep 17 00:00:00 2001 From: Lee SeungJoon Date: Thu, 2 Apr 2026 15:16:25 +0900 Subject: [PATCH 2/5] =?UTF-8?q?refactor:=20=EB=B8=8C=EB=9E=9C=EC=B9=98=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/study/springbootstudy/service/PostService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/study/springbootstudy/service/PostService.java b/src/main/java/com/study/springbootstudy/service/PostService.java index 300613a..eaece61 100644 --- a/src/main/java/com/study/springbootstudy/service/PostService.java +++ b/src/main/java/com/study/springbootstudy/service/PostService.java @@ -11,12 +11,12 @@ @Service @RequiredArgsConstructor -@Transactional(readOnly = true) // 기본적으로 읽기 전용으로 설정하여 성능을 높인다. +@Transactional(readOnly = true) // 기본적으로 읽기 전용으로 설정하여 성능을 높임 public class PostService { private final PostRepository postRepository; - @Transactional // 데이터를 수정, 저장할 때는 이 어노테이션이 꼭 필요하다. + @Transactional // 데이터를 수정, 저장할 때는 이 어노테이션이 꼭 필요함 public Long createPost(PostRequestDto request) { // 1. DTO를 Entity(Post)로 변환 Post post = Post.builder() From 84627fef819323eb29b2c0be3d56eea05fcb0726 Mon Sep 17 00:00:00 2001 From: Lee SeungJoon Date: Thu, 2 Apr 2026 20:37:13 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20password=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/study/springbootstudy/controller/PostController.java | 4 ++-- .../java/com/study/springbootstudy/service/PostService.java | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/study/springbootstudy/controller/PostController.java b/src/main/java/com/study/springbootstudy/controller/PostController.java index f3c0df3..288c6f6 100644 --- a/src/main/java/com/study/springbootstudy/controller/PostController.java +++ b/src/main/java/com/study/springbootstudy/controller/PostController.java @@ -30,7 +30,7 @@ public ApiResponse getPost(@PathVariable Long postId) { } // 게시글 수정 (PUT) - @PutMapping("/api/posts/{id}") + @PutMapping("/{id}") public ApiResponse updatePost( @PathVariable Long id, @RequestBody @Valid PostRequestDto request) { @@ -39,7 +39,7 @@ public ApiResponse updatePost( } // 게시글 삭제 (DELETE) - 비밀번호를 쿼리 파라미터(?password=...)로 받음 - @DeleteMapping("/api/posts/{id}") + @DeleteMapping("/{id}") public ApiResponse deletePost( @PathVariable Long id, @RequestParam String password) { diff --git a/src/main/java/com/study/springbootstudy/service/PostService.java b/src/main/java/com/study/springbootstudy/service/PostService.java index eaece61..2332fd3 100644 --- a/src/main/java/com/study/springbootstudy/service/PostService.java +++ b/src/main/java/com/study/springbootstudy/service/PostService.java @@ -22,6 +22,7 @@ public Long createPost(PostRequestDto request) { Post post = Post.builder() .title(request.getTitle()) .content(request.getContent()) + .password(request.getPassword()) .build(); // 2. DB에 저장하고, 저장된 게시글의 번호(ID)를 반환 From 29d737f10f458358fddef00340feb7253215936d Mon Sep 17 00:00:00 2001 From: Lee SeungJoon Date: Thu, 30 Apr 2026 15:16:58 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=EB=B3=B4=EC=95=88=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20application.yml=20=EA=B9=83=20=EC=B6=94=EC=A0=81=20?= =?UTF-8?q?=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 8 +++ .../config/SecurityConfig.java | 40 ++++++++++++ .../springbootstudy/config/SwaggerConfig.java | 34 ++++++++++ .../controller/MemberController.java | 36 +++++++++++ .../controller/PostController.java | 41 ++++++------ .../study/springbootstudy/domain/Member.java | 33 ++++++++++ .../study/springbootstudy/domain/Post.java | 32 ++++++---- .../dto/MemberJoinRequestDto.java | 21 +++++++ .../dto/MemberLoginRequestDto.java | 16 +++++ .../springbootstudy/dto/PostRequestDto.java | 7 +-- .../repository/MemberRepository.java | 14 +++++ .../security/JwtAuthenticationFilter.java | 50 +++++++++++++++ .../security/JwtTokenProvider.java | 62 +++++++++++++++++++ .../service/MemberService.java | 54 ++++++++++++++++ .../springbootstudy/service/PostService.java | 41 ++++++------ src/main/resources/application.yml | 4 ++ 16 files changed, 440 insertions(+), 53 deletions(-) create mode 100644 src/main/java/com/study/springbootstudy/config/SecurityConfig.java create mode 100644 src/main/java/com/study/springbootstudy/config/SwaggerConfig.java create mode 100644 src/main/java/com/study/springbootstudy/controller/MemberController.java create mode 100644 src/main/java/com/study/springbootstudy/domain/Member.java create mode 100644 src/main/java/com/study/springbootstudy/dto/MemberJoinRequestDto.java create mode 100644 src/main/java/com/study/springbootstudy/dto/MemberLoginRequestDto.java create mode 100644 src/main/java/com/study/springbootstudy/repository/MemberRepository.java create mode 100644 src/main/java/com/study/springbootstudy/security/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/study/springbootstudy/security/JwtTokenProvider.java create mode 100644 src/main/java/com/study/springbootstudy/service/MemberService.java diff --git a/build.gradle b/build.gradle index f2d1997..6ae85e1 100644 --- a/build.gradle +++ b/build.gradle @@ -35,6 +35,14 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // Spring Security (비밀번호 암호화 및 전역 보안 필터용) + implementation 'org.springframework.boot:spring-boot-starter-security' + + // JWT 발급 및 검증을 위한 jjwt 라이브러리 (최신 0.12.x 버전 기준) + implementation 'io.jsonwebtoken:jjwt-api:0.12.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5' } tasks.named('test') { diff --git a/src/main/java/com/study/springbootstudy/config/SecurityConfig.java b/src/main/java/com/study/springbootstudy/config/SecurityConfig.java new file mode 100644 index 0000000..872ad63 --- /dev/null +++ b/src/main/java/com/study/springbootstudy/config/SecurityConfig.java @@ -0,0 +1,40 @@ +package com.study.springbootstudy.config; + +import com.study.springbootstudy.security.JwtAuthenticationFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.AbstractHttpConfigurer; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll() + .requestMatchers("/api/members/join", "/api/members/login", "/api/ping").permitAll() + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/config/SwaggerConfig.java b/src/main/java/com/study/springbootstudy/config/SwaggerConfig.java new file mode 100644 index 0000000..4ed9bb0 --- /dev/null +++ b/src/main/java/com/study/springbootstudy/config/SwaggerConfig.java @@ -0,0 +1,34 @@ +package com.study.springbootstudy.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + String jwtSchemeName = "jwtAuth"; + + // API 요청 시 헤더에 토큰을 요구하도록 설정 + SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwtSchemeName); + + // 토큰 인증 방식 설정 (Bearer 형식) + Components components = new Components() + .addSecuritySchemes(jwtSchemeName, new SecurityScheme() + .name(jwtSchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT")); + + return new OpenAPI() + .info(new Info().title("스프링부트 API 문서").version("1.0.0")) + .addSecurityItem(securityRequirement) + .components(components); + } +} \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/controller/MemberController.java b/src/main/java/com/study/springbootstudy/controller/MemberController.java new file mode 100644 index 0000000..7ce0db6 --- /dev/null +++ b/src/main/java/com/study/springbootstudy/controller/MemberController.java @@ -0,0 +1,36 @@ +package com.study.springbootstudy.controller; + +import com.study.springbootstudy.common.ApiResponse; +import com.study.springbootstudy.dto.MemberJoinRequestDto; +import com.study.springbootstudy.service.MemberService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.study.springbootstudy.dto.MemberLoginRequestDto; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/members") + +public class MemberController { + + private final MemberService memberService; + + @PostMapping("/join") + public ApiResponse join(@RequestBody @Valid MemberJoinRequestDto request) { + Long memberId = memberService.join(request); + return ApiResponse.onSuccess(memberId); // 기존에 작성하신 ApiResponse 규격에 맞게 호출 + } + + @PostMapping("/login") + public ApiResponse login(@RequestBody @Valid MemberLoginRequestDto request) { + // 서비스에서 생성된 토큰 문자열을 그대로 반환받음 + String token = memberService.login(request); + + // 추가 DTO 없이 토큰 문자열 자체를 result에 담아 응답 + return ApiResponse.onSuccess(token); + } +} \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/controller/PostController.java b/src/main/java/com/study/springbootstudy/controller/PostController.java index 288c6f6..086f811 100644 --- a/src/main/java/com/study/springbootstudy/controller/PostController.java +++ b/src/main/java/com/study/springbootstudy/controller/PostController.java @@ -8,42 +8,43 @@ import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import java.security.Principal; // 👈 시큐리티가 제공하는 인증 정보 객체 + @RestController -@RequiredArgsConstructor // Service를 주입받기 위해 꼭 필요 -@RequestMapping("/api/posts") // 이 컨트롤러의 기본 주소를 설정 +@RequiredArgsConstructor +@RequestMapping("/api/posts") public class PostController { private final PostService postService; - // 1. 게시글 생성 API + // [생성] Principal 객체를 통해 현재 로그인한 사용자의 이메일을 가져옴 @PostMapping - public ApiResponse createPost(@Valid @RequestBody PostRequestDto request) { - Long savedPostId = postService.createPost(request); - return ApiResponse.onSuccess(savedPostId); // 성공 시 저장된 게시글의 ID를 반환 + public ApiResponse createPost(@RequestBody @Valid PostRequestDto request, Principal principal) { + // principal.getName()을 호출하면, 필터에서 토큰을 쪼개 저장했던 '이메일'이 나온다 + Long postId = postService.createPost(request, principal.getName()); + return ApiResponse.onSuccess(postId); } - // 2. 게시글 상세 조회 API + // [조회] 단건 조회 (조회는 권한 검증 없이 누구나 볼 수 있도록 둔다.) @GetMapping("/{postId}") public ApiResponse getPost(@PathVariable Long postId) { PostResponseDto response = postService.getPostDetail(postId); - return ApiResponse.onSuccess(response); // 성공 시 게시글 상세 정보를 반환 + return ApiResponse.onSuccess(response); } - // 게시글 수정 (PUT) + // [수정] URL 경로의 ID, 수정할 데이터, 그리고 로그인한 사용자(Principal)를 받는다. @PutMapping("/{id}") - public ApiResponse updatePost( - @PathVariable Long id, - @RequestBody @Valid PostRequestDto request) { - PostResponseDto responseDto = postService.updatePost(id, request); - return ApiResponse.onSuccess(responseDto); + public ApiResponse updatePost(@PathVariable Long id, + @RequestBody @Valid PostRequestDto request, + Principal principal) { + PostResponseDto response = postService.updatePost(id, request, principal.getName()); + return ApiResponse.onSuccess(response); } - // 게시글 삭제 (DELETE) - 비밀번호를 쿼리 파라미터(?password=...)로 받음 + // [삭제] 기존의 @RequestParam String password를 삭제하고 Principal로 교체한다. @DeleteMapping("/{id}") - public ApiResponse deletePost( - @PathVariable Long id, - @RequestParam String password) { - postService.deletePost(id, password); - return ApiResponse.onSuccess(null); + public ApiResponse deletePost(@PathVariable Long id, Principal principal) { + postService.deletePost(id, principal.getName()); + return ApiResponse.onSuccess(null); // 삭제는 돌려줄 데이터가 없으므로 null 반환 } } \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/domain/Member.java b/src/main/java/com/study/springbootstudy/domain/Member.java new file mode 100644 index 0000000..8a84763 --- /dev/null +++ b/src/main/java/com/study/springbootstudy/domain/Member.java @@ -0,0 +1,33 @@ +package com.study.springbootstudy.domain; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) // 무분별한 객체 생성을 막는 안전장치 +public class Member { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // DB가 부여하는 고유 번호 (PK) + + @Column(nullable = false, unique = true) + private String email; // 로그인 아이디로 사용할 이메일 (중복 불가) + + @Column(nullable = false) + private String password; // 암호화되어 저장될 비밀번호 + + @Column(nullable = false) + private String name; // 사용자의 이름 또는 닉네임 + + @Builder + public Member(String email, String password, String name) { + this.email = email; + this.password = password; + this.name = name; + } +} \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/domain/Post.java b/src/main/java/com/study/springbootstudy/domain/Post.java index e8a2d51..20e1620 100644 --- a/src/main/java/com/study/springbootstudy/domain/Post.java +++ b/src/main/java/com/study/springbootstudy/domain/Post.java @@ -2,37 +2,47 @@ import com.study.springbootstudy.common.exception.GeneralException; import jakarta.persistence.*; -import lombok.AllArgsConstructor; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter -@NoArgsConstructor -@AllArgsConstructor -@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Post { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false) private String title; + + @Column(nullable = false, length = 1000) private String content; - // 추가: 작성자 확인용 비밀번호 - private String password; + // 회원(Member)과의 다대일 연관관계 설정 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @Builder + public Post(String title, String content, Member member) { + this.title = title; + this.content = content; + this.member = member; + } - // 1. 게시글 수정 로직 (Setter 대신 의미 있는 메서드 사용) public void update(String title, String content) { this.title = title; this.content = content; } - // 2. 권한 검증 로직 (비밀번호 일치 여부 확인) - public void validatePassword(String inputPassword) { - if (!this.password.equals(inputPassword)) { - throw new GeneralException("AUTH403", "비밀번호가 일치하지 않습니다. 수정/삭제 권한이 없습니다."); + // 작성자 본인인지 확인하는 검증 메서드로 변경 + public void validateAuthor(String currentUserEmail) { + if (!this.member.getEmail().equals(currentUserEmail)) { + throw new GeneralException("AUTH403", "해당 게시글에 대한 권한이 없습니다."); } } } \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/dto/MemberJoinRequestDto.java b/src/main/java/com/study/springbootstudy/dto/MemberJoinRequestDto.java new file mode 100644 index 0000000..b6e783c --- /dev/null +++ b/src/main/java/com/study/springbootstudy/dto/MemberJoinRequestDto.java @@ -0,0 +1,21 @@ +package com.study.springbootstudy.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class MemberJoinRequestDto { + + @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "유효한 이메일 형식이 아닙니다.") + private String email; + + @NotBlank(message = "비밀번호는 필수입니다.") + private String password; + + @NotBlank(message = "이름은 필수입니다.") + private String name; +} \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/dto/MemberLoginRequestDto.java b/src/main/java/com/study/springbootstudy/dto/MemberLoginRequestDto.java new file mode 100644 index 0000000..2a98203 --- /dev/null +++ b/src/main/java/com/study/springbootstudy/dto/MemberLoginRequestDto.java @@ -0,0 +1,16 @@ +package com.study.springbootstudy.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class MemberLoginRequestDto { + + @NotBlank(message = "이메일은 필수입니다.") + private String email; + + @NotBlank(message = "비밀번호는 필수입니다.") + private String password; +} \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/dto/PostRequestDto.java b/src/main/java/com/study/springbootstudy/dto/PostRequestDto.java index 7de4448..c977433 100644 --- a/src/main/java/com/study/springbootstudy/dto/PostRequestDto.java +++ b/src/main/java/com/study/springbootstudy/dto/PostRequestDto.java @@ -3,17 +3,16 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter +@NoArgsConstructor public class PostRequestDto { + @NotBlank(message = "제목은 필수입니다.") private String title; @NotBlank(message = "내용은 필수입니다.") @Size(min = 10, message = "내용은 최소 10자 이상이어야 합니다.") private String content; - - // 추가: 클라이언트로부터 비밀번호도 입력받음 - @NotBlank(message = "비밀번호는 필수입니다.") - private String password; } \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/repository/MemberRepository.java b/src/main/java/com/study/springbootstudy/repository/MemberRepository.java new file mode 100644 index 0000000..d0c1b26 --- /dev/null +++ b/src/main/java/com/study/springbootstudy/repository/MemberRepository.java @@ -0,0 +1,14 @@ +package com.study.springbootstudy.repository; + +import com.study.springbootstudy.domain.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + + // 이메일(로그인 아이디)을 통해 회원 정보를 찾는 메서드 + Optional findByEmail(String email); + + // 중복 가입 방지를 위해 이메일 존재 여부를 확인하는 메서드 + boolean existsByEmail(String email); +} \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/security/JwtAuthenticationFilter.java b/src/main/java/com/study/springbootstudy/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..a1475bf --- /dev/null +++ b/src/main/java/com/study/springbootstudy/security/JwtAuthenticationFilter.java @@ -0,0 +1,50 @@ +package com.study.springbootstudy.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Collections; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + // 1. 요청 헤더에서 "Bearer [토큰]" 추출 + String token = resolveToken(request); + + // 2. 토큰이 존재하고 유효한지 검사 + if (token != null && jwtTokenProvider.validateToken(token)) { + // 3. 토큰에서 이메일을 꺼내어 스프링 시큐리티의 인증 객체로 등록 + String email = jwtTokenProvider.getEmailFromToken(token); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(email, null, Collections.emptyList()); + + // 4. 전역 보안 컨텍스트에 현재 로그인한 사용자 정보 저장 + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + // 5. 검증이 끝나면 다음 필터나 컨트롤러로 요청을 넘김 + filterChain.doFilter(request, response); + } + + // 헤더의 "Authorization" 키값에서 순수 토큰 문자열만 잘라내는 유틸리티 메서드 + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/security/JwtTokenProvider.java b/src/main/java/com/study/springbootstudy/security/JwtTokenProvider.java new file mode 100644 index 0000000..53fbad1 --- /dev/null +++ b/src/main/java/com/study/springbootstudy/security/JwtTokenProvider.java @@ -0,0 +1,62 @@ +package com.study.springbootstudy.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Date; + +@Component +public class JwtTokenProvider { + + private final SecretKey key; + private final long expirationTime; + + // application.yml의 jwt.secret과 jwt.access-expiration 값을 주입받음 + public JwtTokenProvider(@Value("${jwt.secret}") String secretKey, + @Value("${jwt.access-expiration}") long expirationTime) { + // Base64 문자열을 디코딩하여 암호화 알고리즘에 맞는 SecretKey 객체로 변환 + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + this.expirationTime = expirationTime; + } + + // 1. 토큰 생성 메서드 (로그인 성공 시 호출) + public String createToken(String email) { + Date now = new Date(); + Date validity = new Date(now.getTime() + expirationTime); + + return Jwts.builder() + .subject(email) // 토큰의 주체(subject)에 이메일 저장 + .issuedAt(now) // 발행 시간 + .expiration(validity) // 만료 시간 + .signWith(key) // SecretKey를 사용하여 서명 + .compact(); + } + + // 2. 토큰에서 이메일 추출 메서드 (인가 과정에서 호출) + public String getEmailFromToken(String token) { + Claims claims = Jwts.parser() + .verifyWith(key) // 토큰 서명 검증 + .build() + .parseSignedClaims(token) + .getPayload(); // 페이로드(데이터) 추출 + return claims.getSubject(); // 저장해둔 이메일 반환 + } + + // 3. 토큰 유효성 검증 메서드 + public boolean validateToken(String token) { + try { + // 파싱 중 에러가 발생하지 않으면 유효한 토큰으로 간주 + Jwts.parser().verifyWith(key).build().parseSignedClaims(token); + return true; + } catch (Exception e) { + // 만료되었거나 변조된 토큰일 경우 false 반환 + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/service/MemberService.java b/src/main/java/com/study/springbootstudy/service/MemberService.java new file mode 100644 index 0000000..977c33a --- /dev/null +++ b/src/main/java/com/study/springbootstudy/service/MemberService.java @@ -0,0 +1,54 @@ +package com.study.springbootstudy.service; + +import com.study.springbootstudy.common.exception.GeneralException; +import com.study.springbootstudy.domain.Member; +import com.study.springbootstudy.dto.MemberJoinRequestDto; +import com.study.springbootstudy.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.study.springbootstudy.security.JwtTokenProvider; +import com.study.springbootstudy.dto.MemberLoginRequestDto; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MemberService { + + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; // SecurityConfig에서 등록한 빈 주입 + private final JwtTokenProvider jwtTokenProvider; + + @Transactional + public Long join(MemberJoinRequestDto request) { + // 1. 이메일 중복 검증 + if (memberRepository.existsByEmail(request.getEmail())) { + throw new GeneralException("MEMBER409", "이미 존재하는 이메일입니다."); + } + + // 2. 비밀번호 암호화 및 엔티티 생성 + Member member = Member.builder() + .email(request.getEmail()) + .password(passwordEncoder.encode(request.getPassword())) // BCrypt 암호화 적용 + .name(request.getName()) + .build(); + + // 3. DB 영속화 + return memberRepository.save(member).getId(); + } + + public String login(MemberLoginRequestDto request) { + // 1. 이메일로 회원 조회 + Member member = memberRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> new GeneralException("MEMBER404", "가입되지 않은 이메일입니다.")); + + // 2. 비밀번호 검증 (입력받은 평문 vs DB에 저장된 암호화 문자열) + if (!passwordEncoder.matches(request.getPassword(), member.getPassword())) { + throw new GeneralException("AUTH401", "비밀번호가 일치하지 않습니다."); + } + + // 3. 인증 성공 시 JWT 토큰 생성 및 반환 + return jwtTokenProvider.createToken(member.getEmail()); + } +} \ No newline at end of file diff --git a/src/main/java/com/study/springbootstudy/service/PostService.java b/src/main/java/com/study/springbootstudy/service/PostService.java index 2332fd3..57e644f 100644 --- a/src/main/java/com/study/springbootstudy/service/PostService.java +++ b/src/main/java/com/study/springbootstudy/service/PostService.java @@ -1,9 +1,11 @@ package com.study.springbootstudy.service; import com.study.springbootstudy.common.exception.GeneralException; +import com.study.springbootstudy.domain.Member; import com.study.springbootstudy.domain.Post; import com.study.springbootstudy.dto.PostRequestDto; import com.study.springbootstudy.dto.PostResponseDto; +import com.study.springbootstudy.repository.MemberRepository; import com.study.springbootstudy.repository.PostRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -11,56 +13,59 @@ @Service @RequiredArgsConstructor -@Transactional(readOnly = true) // 기본적으로 읽기 전용으로 설정하여 성능을 높임 +@Transactional(readOnly = true) public class PostService { private final PostRepository postRepository; + private final MemberRepository memberRepository; // 회원 정보를 DB에서 찾기 위해 추가 - @Transactional // 데이터를 수정, 저장할 때는 이 어노테이션이 꼭 필요함 - public Long createPost(PostRequestDto request) { - // 1. DTO를 Entity(Post)로 변환 + // [생성] 게시글 작성 시, 현재 로그인한 사용자의 이메일을 받아 작성자로 매핑 + @Transactional + public Long createPost(PostRequestDto request, String email) { + // 1. 이메일로 현재 로그인한 회원(Member) 엔티티 조회 + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new GeneralException("MEMBER404", "존재하지 않는 회원입니다.")); + + // 2. DTO를 Entity로 변환 (비밀번호 삭제, member 객체 주입) Post post = Post.builder() .title(request.getTitle()) .content(request.getContent()) - .password(request.getPassword()) + .member(member) // 👈 외래키(member_id) 연결! .build(); - // 2. DB에 저장하고, 저장된 게시글의 번호(ID)를 반환 return postRepository.save(post).getId(); } + // [조회] 단건 조회 (변경 없음) public PostResponseDto getPostDetail(Long postId) { - // 1. DB에서 ID로 게시글을 찾고, 없으면 예외를 띄움 Post post = postRepository.findById(postId) .orElseThrow(() -> new GeneralException("POST404", "해당 게시글이 존재하지 않습니다.")); - - // 2. 찾은 게시글을 DTO에 담아 컨트롤러로 보냄 return PostResponseDto.from(post); } - // [수정] 더티 체킹 활용 + // [수정] 수정 시, 현재 로그인한 이메일을 넘겨받아 작성자 본인인지 확인 @Transactional - public PostResponseDto updatePost(Long postId, PostRequestDto request) { + public PostResponseDto updatePost(Long postId, PostRequestDto request, String email) { Post post = postRepository.findById(postId) .orElseThrow(() -> new GeneralException("POST404", "해당 게시글이 존재하지 않습니다.")); - // 1. 비밀번호 검증 - post.validatePassword(request.getPassword()); + // 1. 작성자 검증 (비밀번호 대신 토큰 이메일 사용) + post.validateAuthor(email); - // 2. 수정 (트랜잭션 종료 시 JPA가 알아서 DB에 반영해 줌) + // 2. 수정 로직 수행 post.update(request.getTitle(), request.getContent()); return PostResponseDto.from(post); } - // [삭제] + // [삭제] 삭제 시, 현재 로그인한 이메일을 넘겨받아 작성자 본인인지 확인 @Transactional - public void deletePost(Long postId, String password) { + public void deletePost(Long postId, String email) { // password 파라미터 삭제, email 추가 Post post = postRepository.findById(postId) .orElseThrow(() -> new GeneralException("POST404", "해당 게시글이 존재하지 않습니다.")); - // 1. 비밀번호 검증 - post.validatePassword(password); + // 1. 작성자 검증 + post.validateAuthor(email); // 2. 삭제 postRepository.delete(post); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 02a9c99..c2e3a26 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,3 +1,7 @@ +jwt: + secret: KtW5EIXy36L1i3u4qd6Py6Yx353WJiUYlLNR31IJgugph09XSiv/Cs5UFYv+av1A0sWBBQEqRr81wGpMoUBdhg== + access-expiration: 3600000 + spring: datasource: url: jdbc:mysql://localhost:3306/springboot_study?serverTimezone=Asia/Seoul From 2c272325d0521b200b93c33665b7d8e02061ccb1 Mon Sep 17 00:00:00 2001 From: Lee SeungJoon Date: Thu, 30 Apr 2026 15:19:35 +0900 Subject: [PATCH 5/5] =?UTF-8?q?5=EC=A3=BC=EC=B0=A8=20=EB=AF=B8=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index c2065bc..9eb012d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ out/ ### VS Code ### .vscode/ + +### Security ### +src/main/resources/application.yml \ No newline at end of file