diff --git a/README.md b/README.md index 395edc5..82642b2 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ # backend-study-sns + +강준이 diff --git a/build.gradle b/build.gradle index 610d6a6..de1cb7a 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ description = 'Demo project for Spring Boot' java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(17) } } @@ -19,8 +19,16 @@ repositories { } dependencies { + // Lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + implementation 'org.springframework.boot:spring-boot-starter-web' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'com.mysql:mysql-connector-j:8.2.0' // MySQL + testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/src/main/java/com/example/devSns/DevSnsApplication.java b/src/main/java/com/example/devSns/DevSnsApplication.java index b965724..f1bf185 100644 --- a/src/main/java/com/example/devSns/DevSnsApplication.java +++ b/src/main/java/com/example/devSns/DevSnsApplication.java @@ -10,4 +10,4 @@ public static void main(String[] args) { SpringApplication.run(DevSnsApplication.class, args); } -} +} \ No newline at end of file diff --git a/src/main/java/com/example/devSns/controller/CommentController.java b/src/main/java/com/example/devSns/controller/CommentController.java new file mode 100644 index 0000000..66874de --- /dev/null +++ b/src/main/java/com/example/devSns/controller/CommentController.java @@ -0,0 +1,31 @@ +package com.example.devSns.controller; + +import com.example.devSns.dto.CommentRequest; +import com.example.devSns.entity.CommentEntity; +import com.example.devSns.service.CommentService; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +@Controller +@RequestMapping("/comments") +public class CommentController { + private final CommentService commentService; + + public CommentController(CommentService commentService) { + this.commentService = commentService; + } + + // 댓글 생성 + @PostMapping("/{postId}") + public String createComment(@PathVariable Long postId, @ModelAttribute CommentRequest request) { + commentService.createComment(postId, request); + return "redirect:/posts/" + postId; + } + + // 댓글 삭제 + @PostMapping("/{commentId}/delete") + public String deleteComment(@PathVariable Long commentId, @RequestParam Long postId) { + commentService.deleteComment(commentId); + return "redirect:/posts/" + postId; + } +} diff --git a/src/main/java/com/example/devSns/controller/PostController.java b/src/main/java/com/example/devSns/controller/PostController.java new file mode 100644 index 0000000..ae06df7 --- /dev/null +++ b/src/main/java/com/example/devSns/controller/PostController.java @@ -0,0 +1,69 @@ +package com.example.devSns.controller; + +import com.example.devSns.entity.PostEntity; +import com.example.devSns.service.PostService; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; + +@Controller +@RequestMapping("/posts") +public class PostController { + + private final PostService postService; + + // 생성자 + public PostController(PostService postService) { + this.postService = postService; + } + + // HTTP GET /posts + @GetMapping + public String list(Model model) { + model.addAttribute("posts", postService.getAllPosts()); + return "list"; + } + + // HTTP GET /posts/new + @GetMapping("/new") + public String form(Model model) { + model.addAttribute("post", new PostEntity()); + return "form"; + } + + // HTTP POST /posts + @PostMapping + public String create(@ModelAttribute PostEntity postEntity) { + postService.createPost(postEntity); + return "redirect:/posts"; // 글 생성 후 돌아올 주소 + } + + // HTTP GET /posts/{id} + @GetMapping("/{id}") + public String detail(@PathVariable Long id, Model model) { + model.addAttribute("post", postService.getPost(id)); + return "detail"; + } + + // HTTP GET /posts/{id}/edit + @GetMapping("/{id}/edit") + public String editForm(@PathVariable Long id, Model model) { + PostEntity postEntity = postService.getPost(id); + model.addAttribute("post", postEntity); + return "edit"; + } + + // HTTP POST /posts/{id}/update + @PostMapping("/{id}/update") + public String update(@PathVariable Long id, @ModelAttribute PostEntity updatedPost) { + postService.updatePost(id, updatedPost); + return "redirect:/posts/" + id; // 수정 후 돌아올 주소 + } + + // HTTP POST /posts/{id}/delete + @PostMapping("/{id}/delete") + public String delete(@PathVariable Long id) { + postService.deletePost(id); + return "redirect:/posts"; // 삭제 후 돌아올 주소 + } +} \ No newline at end of file diff --git a/src/main/java/com/example/devSns/controller/explanation b/src/main/java/com/example/devSns/controller/explanation new file mode 100644 index 0000000..d86130e --- /dev/null +++ b/src/main/java/com/example/devSns/controller/explanation @@ -0,0 +1,24 @@ +요청을 받는 곳 + +PostController +Spring MVC 패턴에서 Controller 계층 역할 +- 사용자의 요청을 받고 Service 계층을 호출해서 처리한 후, 결과를 View에 전달 + +@Controller +이 클래스는 웹 요청을 처리하는 컨트롤러임을 알려줌 +- Model에 데이터를 담아 템플릿에 전달 (templates/list.html) + +@RequestMapping +클래스 전체의 기본 URL 경로를 지정 +- 이 컨트롤러 안의 모든 요청은 /posts로 시작 +- /posts (게시글 목록), /posts/new (새 글 작성 폼) + +@GetMapping, @PostMapping +HTTP 메서드 별 라우팅 + +@PathVariable, @ModelAttribute +URL 경로 변수 바인딩 / 폼 데이터를 객체에 바인딩 + +@RestController +문자열 반환이 뷰 이름이 아닌 JSON +- REST API 만들 때 사용 \ No newline at end of file diff --git a/src/main/java/com/example/devSns/dto/CommentRequest.java b/src/main/java/com/example/devSns/dto/CommentRequest.java new file mode 100644 index 0000000..93829ab --- /dev/null +++ b/src/main/java/com/example/devSns/dto/CommentRequest.java @@ -0,0 +1,13 @@ +package com.example.devSns.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CommentRequest { + private String username; + private String content; +} + +// Controller는 CommentEntity를 직접 받지 않고 CommentRequest만 받게 됨 diff --git a/src/main/java/com/example/devSns/dto/CommentResponse.java b/src/main/java/com/example/devSns/dto/CommentResponse.java new file mode 100644 index 0000000..917576b --- /dev/null +++ b/src/main/java/com/example/devSns/dto/CommentResponse.java @@ -0,0 +1,20 @@ +package com.example.devSns.dto; + +import lombok.Getter; + +@Getter +public class CommentResponse { + private final Long id; + private final String username; + private final String content; + private final String createdAt; + + public CommentResponse(Long id, String username, String content, String createdAt) { + this.id = id; + this.username = username; + this.content = content; + this.createdAt = createdAt; + } +} + +// Entity를 외부로 그대로 내보내지 않고 DTO로 가공해서 반환 diff --git a/src/main/java/com/example/devSns/dto/explanation b/src/main/java/com/example/devSns/dto/explanation new file mode 100644 index 0000000..095c837 --- /dev/null +++ b/src/main/java/com/example/devSns/dto/explanation @@ -0,0 +1,8 @@ +계층간 데이터 교환에 사용 +- Entity 클래스를 보호 +- 필요한 데이터만 선택적으로 담을 수 있음 + +분리해서 사용하는 이유 +- Entity 객체의 변경을 피하기 위함 +- 클라이언트와 통신하는 ResponseDTO나 RequestDTO는 요구사항에 따라 자주 변경 +- 어떤 요청에서는 특정 값이 추가되거나 없을 수 있어서 분리해서 관리 \ No newline at end of file diff --git a/src/main/java/com/example/devSns/entity/CommentEntity.java b/src/main/java/com/example/devSns/entity/CommentEntity.java new file mode 100644 index 0000000..07d93eb --- /dev/null +++ b/src/main/java/com/example/devSns/entity/CommentEntity.java @@ -0,0 +1,53 @@ +package com.example.devSns.entity; + +import jakarta.persistence.*; +import lombok.Getter; + +import java.time.LocalDateTime; + +// DTO를 사용하고 Setter를 삭제 (안전) +@Entity +@Getter +public class CommentEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String content; + private String username; + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + // Post와 N:1 관계 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private PostEntity postEntity; + + // JPA 기본 생성자 + protected CommentEntity() {} + + // 정적 생성 메서드 > 엔터티 변경은 오직 메서드로 + public static CommentEntity create(PostEntity postEntity, String username, String content) { + CommentEntity comment = new CommentEntity(); + comment.postEntity = postEntity; + comment.username = username; + comment.content = content; + return comment; + } + + public void updateContent(String content) { + this.content = content; + } + + @PrePersist + public void onCreate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + public void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/example/devSns/entity/PostEntity.java b/src/main/java/com/example/devSns/entity/PostEntity.java new file mode 100644 index 0000000..9a71dfc --- /dev/null +++ b/src/main/java/com/example/devSns/entity/PostEntity.java @@ -0,0 +1,41 @@ +package com.example.devSns.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Setter +public class PostEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + private String content; + private String author; + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + // post를 삭제하면 댓글도 같이 삭제 + @OneToMany(mappedBy = "postEntity", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdated() { + this.updatedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/devSns/entity/explanation b/src/main/java/com/example/devSns/entity/explanation new file mode 100644 index 0000000..402a31e --- /dev/null +++ b/src/main/java/com/example/devSns/entity/explanation @@ -0,0 +1,22 @@ +데이터베이스와 직접적으로 맞닿는 핵심적인 클래스 +- entity를 기준으로 테이블 생성 +- Builder 패턴을 사용해서 필요한 값만 넣음 + +PostEntity 객체가 곧 DB의 한 행(row) +-> 기본 키는 id이고, 나머지는 속성임을 명시하는 클래스 + +@Entity +데이터베이스 테이블과 매핑 +- JPA가 이 클래스를 테이블로 인식 +- 데이터베이스 테이블로 매핑할 수 있게 함 + +@Id +해당 필드가 기본 키임을 나타냄 +- JPA에서 이 필드는 각 행을 고유하게 식별하는 기준 + +@GeneratedValue +기본 키의 값을 자동 생성할 때 어떤 전략을 쓸지 지정 +GenerationType.IDENTITY는 자동 증가 + +@Getter, @Setter (Lombok) +보일러플레이트 제거 \ No newline at end of file diff --git a/src/main/java/com/example/devSns/repository/CommentRepository.java b/src/main/java/com/example/devSns/repository/CommentRepository.java new file mode 100644 index 0000000..3bc9d09 --- /dev/null +++ b/src/main/java/com/example/devSns/repository/CommentRepository.java @@ -0,0 +1,7 @@ +package com.example.devSns.repository; + +import com.example.devSns.entity.CommentEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/devSns/repository/PostRepository.java b/src/main/java/com/example/devSns/repository/PostRepository.java new file mode 100644 index 0000000..d6e24c5 --- /dev/null +++ b/src/main/java/com/example/devSns/repository/PostRepository.java @@ -0,0 +1,7 @@ +package com.example.devSns.repository; + +import com.example.devSns.entity.PostEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/devSns/repository/explanation b/src/main/java/com/example/devSns/repository/explanation new file mode 100644 index 0000000..235ff10 --- /dev/null +++ b/src/main/java/com/example/devSns/repository/explanation @@ -0,0 +1,5 @@ +데이터베이스 접근 (JPA 인터페이스) + +PostRepository +상속만 하면 기본 CRUD가 다 생김 +- save, findAll, findById, deleteById diff --git a/src/main/java/com/example/devSns/service/CommentService.java b/src/main/java/com/example/devSns/service/CommentService.java new file mode 100644 index 0000000..0246c4e --- /dev/null +++ b/src/main/java/com/example/devSns/service/CommentService.java @@ -0,0 +1,63 @@ +package com.example.devSns.service; + +import com.example.devSns.dto.CommentRequest; +import com.example.devSns.dto.CommentResponse; +import com.example.devSns.entity.CommentEntity; +import com.example.devSns.entity.PostEntity; +import com.example.devSns.repository.CommentRepository; +import com.example.devSns.repository.PostRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional(readOnly = true) +public class CommentService { + private final CommentRepository commentRepository; + private final PostRepository postRepository; + + public CommentService(CommentRepository commentRepository, PostRepository postRepository) { + this.commentRepository = commentRepository; + this.postRepository = postRepository; + } + + // 특정 게시글 댓글 조회 + public List getCommentsByPost(Long postId) { + PostEntity postEntity = postRepository.findById(postId).orElseThrow(); + return postEntity.getComments().stream() + .map(comment -> new CommentResponse( + comment.getId(), + comment.getUsername(), + comment.getContent(), + comment.getCreatedAt().toString() + )) + .toList(); + } + + // 댓글 생성 + @Transactional + public CommentResponse createComment(Long postId, CommentRequest request) { + PostEntity postEntity = postRepository.findById(postId).orElseThrow(); + CommentEntity comment = CommentEntity.create( + postEntity, + request.getUsername(), + request.getContent() + ); + + commentRepository.save(comment); + + return new CommentResponse( + comment.getId(), + comment.getUsername(), + comment.getContent(), + comment.getCreatedAt().toString() + ); + } + + // 댓글 삭제 + @Transactional + public void deleteComment(Long id) { + commentRepository.deleteById(id); + } +} diff --git a/src/main/java/com/example/devSns/service/PostService.java b/src/main/java/com/example/devSns/service/PostService.java new file mode 100644 index 0000000..c66c5cb --- /dev/null +++ b/src/main/java/com/example/devSns/service/PostService.java @@ -0,0 +1,46 @@ +package com.example.devSns.service; + +import com.example.devSns.entity.PostEntity; +import com.example.devSns.repository.PostRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional(readOnly = true) +public class PostService { + private final PostRepository postRepository; + + // 생성자 + public PostService(PostRepository postRepository) { + this.postRepository = postRepository; + } + + public List getAllPosts() { + return postRepository.findAll(); + } + + public PostEntity getPost(Long id) { + return postRepository.findById(id).orElseThrow(); + } + + @Transactional + public PostEntity createPost(PostEntity postEntity) { + return postRepository.save(postEntity); + } + + @Transactional + public PostEntity updatePost(Long id, PostEntity updated) { + PostEntity postEntity = postRepository.findById(id).orElseThrow(); + postEntity.setTitle(updated.getTitle()); + postEntity.setContent(updated.getContent()); + return postRepository.save(postEntity); + } + + @Transactional + public void deletePost(Long id) { + postRepository.deleteById(id); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/devSns/service/explanation b/src/main/java/com/example/devSns/service/explanation new file mode 100644 index 0000000..09b39f6 --- /dev/null +++ b/src/main/java/com/example/devSns/service/explanation @@ -0,0 +1,33 @@ +비지니스 로직 담당 + +PostService + +@Service +비지니스 로직이 모이는 곳 +- 트랜잭션을 붙여야 할 때도 주로 서비스에서 +- 의존성 주입 (생성자 주입) + +@Transactional(readOnly = true) +읽기 성능 최적화 +- 원자성 보장 +- 안전빵 + +private final PostRepository postRepository; +- 의존성으로 받는 레포지토리 +- 생성자 주입 +- JpaRepository 상속 + +findAll() +- 모든 게시글 조회 + +findById() +- getPost 함수에서 PK(id)로 단건 조회 +- updatePost 함수에서 수정 대상 로드, 필드 갱신, 저장 + +save +- 새 엔터티 저장 +- id가 없으면 persist, 있으면 merge + +deleteById +- PK(id)로 삭제 + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f3f10af..e782abf 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,19 @@ spring.application.name=devSns + +# ?? ?? +server.port=8080 + +# MySQL +spring.datasource.url=jdbc:mysql://localhost:3306/devSns?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 +spring.datasource.username=root +spring.datasource.password=as112525@ +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +# JPA +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true + +# Thymeleaf +spring.thymeleaf.cache=false + diff --git a/src/main/resources/templates/detail.html b/src/main/resources/templates/detail.html new file mode 100644 index 0000000..a867bd4 --- /dev/null +++ b/src/main/resources/templates/detail.html @@ -0,0 +1,61 @@ + + + +

제목

+

내용

+

+ 작성자: + +

+ + + + +
+ +
+ + + + +
+

댓글

+ +
+

댓글이 없습니다.

+
+ +
    +
  • +

    +

    +

    작성일:

    + +
    + + +
    + +
    +
  • +
+ +
+ +

댓글 작성하기

+ +
+

+ 작성자: + +

+

+ 내용:
+ +

+ + +
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/edit.html b/src/main/resources/templates/edit.html new file mode 100644 index 0000000..777a6e3 --- /dev/null +++ b/src/main/resources/templates/edit.html @@ -0,0 +1,19 @@ + + + +

게시글 수정

+ +
+
+

+ +
+

+ + +
+ +
+ 취소 + + \ No newline at end of file diff --git a/src/main/resources/templates/form.html b/src/main/resources/templates/form.html new file mode 100644 index 0000000..52849d8 --- /dev/null +++ b/src/main/resources/templates/form.html @@ -0,0 +1,12 @@ + + + +

새 글 작성

+
+ + + + +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/list.html b/src/main/resources/templates/list.html new file mode 100644 index 0000000..c444459 --- /dev/null +++ b/src/main/resources/templates/list.html @@ -0,0 +1,19 @@ + + + +

게시판

+ 새 글 쓰기 +
    +
  • + +
    + + 작성일: + + / + 수정일: + + +
  • +
+