From 098a8c29245e164e80d4e2bbc35b9695b09e2cb5 Mon Sep 17 00:00:00 2001 From: junyi04 Date: Fri, 3 Oct 2025 17:17:56 +0900 Subject: [PATCH 01/23] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 395edc5..82642b2 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ # backend-study-sns + +강준이 From 3d54a1fe17f3315982c7340a09fbd545c7913d97 Mon Sep 17 00:00:00 2001 From: junyi04 Date: Sun, 5 Oct 2025 23:20:01 +0900 Subject: [PATCH 02/23] chore: add JPA and Lombok dependencies --- build.gradle | 8 +++++++ .../com/example/devSns/controller/explanation | 1 + .../java/com/example/devSns/dto/explanation | 8 +++++++ .../com/example/devSns/entity/PostEntity.java | 0 .../com/example/devSns/entity/explanation | 22 +++++++++++++++++++ .../com/example/devSns/repository/explanation | 1 + .../com/example/devSns/service/explanation | 1 + 7 files changed, 41 insertions(+) create mode 100644 src/main/java/com/example/devSns/controller/explanation create mode 100644 src/main/java/com/example/devSns/dto/explanation create mode 100644 src/main/java/com/example/devSns/entity/PostEntity.java create mode 100644 src/main/java/com/example/devSns/entity/explanation create mode 100644 src/main/java/com/example/devSns/repository/explanation create mode 100644 src/main/java/com/example/devSns/service/explanation diff --git a/build.gradle b/build.gradle index 610d6a6..c78d56d 100644 --- a/build.gradle +++ b/build.gradle @@ -19,8 +19,16 @@ repositories { } dependencies { + // Lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + implementation 'org.springframework.boot:spring-boot-starter-web' + 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' testImplementation 'org.springframework.boot:spring-boot-starter-test' + runtimeOnly 'com.h2database:h2' // 임시 메모리 DB testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } 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..3191f3e --- /dev/null +++ b/src/main/java/com/example/devSns/controller/explanation @@ -0,0 +1 @@ +요청을 받는 곳 \ No newline at end of file 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/PostEntity.java b/src/main/java/com/example/devSns/entity/PostEntity.java new file mode 100644 index 0000000..e69de29 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/explanation b/src/main/java/com/example/devSns/repository/explanation new file mode 100644 index 0000000..97a040b --- /dev/null +++ b/src/main/java/com/example/devSns/repository/explanation @@ -0,0 +1 @@ +데이터베이스 접근 (JPA 인터페이스) \ 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..7417adb --- /dev/null +++ b/src/main/java/com/example/devSns/service/explanation @@ -0,0 +1 @@ +비지니스 로직 담당 \ No newline at end of file From 2ded9e9a476a70f082d8378cd63dc9ba70ba46ce Mon Sep 17 00:00:00 2001 From: junyi04 Date: Sun, 5 Oct 2025 23:25:04 +0900 Subject: [PATCH 03/23] feat(docs): add PostEntity and explanation --- .../com/example/devSns/entity/PostEntity.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/java/com/example/devSns/entity/PostEntity.java b/src/main/java/com/example/devSns/entity/PostEntity.java index e69de29..058a89a 100644 --- a/src/main/java/com/example/devSns/entity/PostEntity.java +++ b/src/main/java/com/example/devSns/entity/PostEntity.java @@ -0,0 +1,22 @@ +package com.example.devSns.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Getter +@Setter +public class PostEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + private String content; + private String author; +} \ No newline at end of file From 259201710a9469596ddb17f88c199ac178d0bdee Mon Sep 17 00:00:00 2001 From: junyi04 Date: Sun, 5 Oct 2025 23:28:38 +0900 Subject: [PATCH 04/23] feat(docs): add PostRepository and explanation --- .../java/com/example/devSns/repository/PostRepository.java | 7 +++++++ src/main/java/com/example/devSns/repository/explanation | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/devSns/repository/PostRepository.java 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 index 97a040b..235ff10 100644 --- a/src/main/java/com/example/devSns/repository/explanation +++ b/src/main/java/com/example/devSns/repository/explanation @@ -1 +1,5 @@ -데이터베이스 접근 (JPA 인터페이스) \ No newline at end of file +데이터베이스 접근 (JPA 인터페이스) + +PostRepository +상속만 하면 기본 CRUD가 다 생김 +- save, findAll, findById, deleteById From a8694623d8149b1c8a16be2e99a09e3b715de7c4 Mon Sep 17 00:00:00 2001 From: junyi04 Date: Sun, 5 Oct 2025 23:45:04 +0900 Subject: [PATCH 05/23] feat(docs): add PostService and explanation --- .../example/devSns/service/PostService.java | 46 +++++++++++++++++++ .../com/example/devSns/service/explanation | 34 +++++++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/devSns/service/PostService.java 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..c9630f9 --- /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); + } + +} diff --git a/src/main/java/com/example/devSns/service/explanation b/src/main/java/com/example/devSns/service/explanation index 7417adb..09b39f6 100644 --- a/src/main/java/com/example/devSns/service/explanation +++ b/src/main/java/com/example/devSns/service/explanation @@ -1 +1,33 @@ -비지니스 로직 담당 \ No newline at end of file +비지니스 로직 담당 + +PostService + +@Service +비지니스 로직이 모이는 곳 +- 트랜잭션을 붙여야 할 때도 주로 서비스에서 +- 의존성 주입 (생성자 주입) + +@Transactional(readOnly = true) +읽기 성능 최적화 +- 원자성 보장 +- 안전빵 + +private final PostRepository postRepository; +- 의존성으로 받는 레포지토리 +- 생성자 주입 +- JpaRepository 상속 + +findAll() +- 모든 게시글 조회 + +findById() +- getPost 함수에서 PK(id)로 단건 조회 +- updatePost 함수에서 수정 대상 로드, 필드 갱신, 저장 + +save +- 새 엔터티 저장 +- id가 없으면 persist, 있으면 merge + +deleteById +- PK(id)로 삭제 + From 9346fa92ae83ccd151afc02a412334540f5f73c2 Mon Sep 17 00:00:00 2001 From: junyi04 Date: Mon, 6 Oct 2025 00:02:45 +0900 Subject: [PATCH 06/23] feat(docs): add PostController and explanation --- .../devSns/controller/PostController.java | 54 +++++++++++++++++++ .../com/example/devSns/controller/explanation | 25 ++++++++- 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/devSns/controller/PostController.java 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..99dc55a --- /dev/null +++ b/src/main/java/com/example/devSns/controller/PostController.java @@ -0,0 +1,54 @@ +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 POST /posts/{id}/delete + @PostMapping("/{id}/delete") + public String delete(@PathVariable Long id) { + postService.deletePost(id); + return "redirect:/posts"; + } +} diff --git a/src/main/java/com/example/devSns/controller/explanation b/src/main/java/com/example/devSns/controller/explanation index 3191f3e..d86130e 100644 --- a/src/main/java/com/example/devSns/controller/explanation +++ b/src/main/java/com/example/devSns/controller/explanation @@ -1 +1,24 @@ -요청을 받는 곳 \ No newline at end of file +요청을 받는 곳 + +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 From e22712679ec7e479cba646ca81dd9e1d8540705c Mon Sep 17 00:00:00 2001 From: junyi04 Date: Mon, 6 Oct 2025 00:10:07 +0900 Subject: [PATCH 07/23] feat: create templates/list.html (/posts/new) --- src/main/resources/templates/list.html | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/resources/templates/list.html diff --git a/src/main/resources/templates/list.html b/src/main/resources/templates/list.html new file mode 100644 index 0000000..712cbd7 --- /dev/null +++ b/src/main/resources/templates/list.html @@ -0,0 +1,12 @@ + + + +

게시판

+ 새 글 쓰기 +
    +
  • + +
  • +
+ + \ No newline at end of file From 96b956f37e5a15f397b812cb1ee9bbd5e5089ac7 Mon Sep 17 00:00:00 2001 From: junyi04 Date: Mon, 6 Oct 2025 00:14:23 +0900 Subject: [PATCH 08/23] feat: create templates/form.html (/posts) --- src/main/resources/templates/form.html | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/resources/templates/form.html 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 From 845bb2d8175c93927a949ad46053d2ed2e4227d2 Mon Sep 17 00:00:00 2001 From: junyi04 Date: Mon, 6 Oct 2025 00:18:19 +0900 Subject: [PATCH 09/23] feat: create templates/detail.html (/posts/{id}/delete) --- src/main/resources/templates/detail.html | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/main/resources/templates/detail.html diff --git a/src/main/resources/templates/detail.html b/src/main/resources/templates/detail.html new file mode 100644 index 0000000..c585343 --- /dev/null +++ b/src/main/resources/templates/detail.html @@ -0,0 +1,15 @@ + + + +

제목

+

내용

+

+ 작성자: + +

+ +
+ +
+ + \ No newline at end of file From eb68811505190ff85662afb6d25ad349f4803f97 Mon Sep 17 00:00:00 2001 From: junyi04 Date: Mon, 6 Oct 2025 00:33:59 +0900 Subject: [PATCH 10/23] chore: update H2 database and JPA configuration --- src/main/resources/application.properties | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f3f10af..8bef822 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,23 @@ spring.application.name=devSns + +# ?? ?? +server.port=8080 + +# H2 ???? DB +spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;DATABASE_TO_UPPER=false +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +# JPA +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true + +# H2 ?? +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console + +# Thymeleaf(??? ??? ?? ?) +spring.thymeleaf.cache=false + From 8000aaf512e2de6767950b7867370aebf04fbc31 Mon Sep 17 00:00:00 2001 From: junyi04 Date: Mon, 6 Oct 2025 00:40:39 +0900 Subject: [PATCH 11/23] fix: correct typo in list.html&detail.html th name --- src/main/resources/templates/detail.html | 2 +- src/main/resources/templates/list.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/templates/detail.html b/src/main/resources/templates/detail.html index c585343..016c58c 100644 --- a/src/main/resources/templates/detail.html +++ b/src/main/resources/templates/detail.html @@ -8,7 +8,7 @@

내용

-
+
diff --git a/src/main/resources/templates/list.html b/src/main/resources/templates/list.html index 712cbd7..bd6df84 100644 --- a/src/main/resources/templates/list.html +++ b/src/main/resources/templates/list.html @@ -5,7 +5,7 @@

게시판

새 글 쓰기
  • - +
From 7437ff5bf7a2046c7c4fc61a0cf7b97dffc4b756 Mon Sep 17 00:00:00 2001 From: junyi04 Date: Tue, 7 Oct 2025 22:30:07 +0900 Subject: [PATCH 12/23] feat: add createdAt, updatedAt fields and connect CRUD --- .../devSns/controller/PostController.java | 2 +- .../com/example/devSns/entity/PostEntity.java | 21 +++++++++++++++---- .../example/devSns/service/PostService.java | 2 +- src/main/resources/application.properties | 2 +- src/main/resources/templates/list.html | 9 +++++++- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/example/devSns/controller/PostController.java b/src/main/java/com/example/devSns/controller/PostController.java index 99dc55a..bc9fd9c 100644 --- a/src/main/java/com/example/devSns/controller/PostController.java +++ b/src/main/java/com/example/devSns/controller/PostController.java @@ -51,4 +51,4 @@ 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/entity/PostEntity.java b/src/main/java/com/example/devSns/entity/PostEntity.java index 058a89a..ccaad1a 100644 --- a/src/main/java/com/example/devSns/entity/PostEntity.java +++ b/src/main/java/com/example/devSns/entity/PostEntity.java @@ -1,12 +1,11 @@ package com.example.devSns.entity; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; +import java.time.LocalDateTime; + @Entity @Getter @Setter @@ -19,4 +18,18 @@ public class PostEntity { private String title; private String content; private String author; + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + @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/service/PostService.java b/src/main/java/com/example/devSns/service/PostService.java index c9630f9..c66c5cb 100644 --- a/src/main/java/com/example/devSns/service/PostService.java +++ b/src/main/java/com/example/devSns/service/PostService.java @@ -43,4 +43,4 @@ public void deletePost(Long id) { postRepository.deleteById(id); } -} +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8bef822..6f74611 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,7 +1,7 @@ spring.application.name=devSns # ?? ?? -server.port=8080 +server.port=8081 # H2 ???? DB spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;DATABASE_TO_UPPER=false diff --git a/src/main/resources/templates/list.html b/src/main/resources/templates/list.html index bd6df84..c444459 100644 --- a/src/main/resources/templates/list.html +++ b/src/main/resources/templates/list.html @@ -6,7 +6,14 @@

게시판

  • +
    + + 작성일: + + / + 수정일: + +
- \ No newline at end of file From bbc2f3d48a3bcb0ad9b610c7e4d9aae95af8ece3 Mon Sep 17 00:00:00 2001 From: junyi04 Date: Tue, 7 Oct 2025 22:52:18 +0900 Subject: [PATCH 13/23] feat: implement edit feature with update API and edit.html --- .../devSns/controller/PostController.java | 19 +++++++++++++++++-- src/main/resources/application.properties | 2 +- src/main/resources/templates/detail.html | 6 ++++++ src/main/resources/templates/edit.html | 19 +++++++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 src/main/resources/templates/edit.html diff --git a/src/main/java/com/example/devSns/controller/PostController.java b/src/main/java/com/example/devSns/controller/PostController.java index bc9fd9c..ae06df7 100644 --- a/src/main/java/com/example/devSns/controller/PostController.java +++ b/src/main/java/com/example/devSns/controller/PostController.java @@ -35,7 +35,7 @@ public String form(Model model) { @PostMapping public String create(@ModelAttribute PostEntity postEntity) { postService.createPost(postEntity); - return "redirect:/posts"; + return "redirect:/posts"; // 글 생성 후 돌아올 주소 } // HTTP GET /posts/{id} @@ -45,10 +45,25 @@ public String detail(@PathVariable Long id, Model model) { 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"; + return "redirect:/posts"; // 삭제 후 돌아올 주소 } } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6f74611..8bef822 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,7 +1,7 @@ spring.application.name=devSns # ?? ?? -server.port=8081 +server.port=8080 # H2 ???? DB spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;DATABASE_TO_UPPER=false diff --git a/src/main/resources/templates/detail.html b/src/main/resources/templates/detail.html index 016c58c..bdaeae8 100644 --- a/src/main/resources/templates/detail.html +++ b/src/main/resources/templates/detail.html @@ -8,8 +8,14 @@

내용

+ + +
+ + + \ 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 From cc40f17205dbb075f4c13a7e4b75e935f5ffb52a Mon Sep 17 00:00:00 2001 From: junyi04 Date: Thu, 13 Nov 2025 14:31:31 +0900 Subject: [PATCH 14/23] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 +-- .../com/example/devSns/DevSnsApplication.java | 2 +- .../devSns/controller/CommentController.java | 30 +++++++++++++ .../example/devSns/entity/CommentEntity.java | 38 +++++++++++++++++ .../com/example/devSns/entity/PostEntity.java | 6 +++ .../devSns/repository/CommentRepository.java | 7 ++++ .../devSns/service/CommentService.java | 42 +++++++++++++++++++ src/main/resources/application.properties | 16 +++---- 8 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/example/devSns/controller/CommentController.java create mode 100644 src/main/java/com/example/devSns/entity/CommentEntity.java create mode 100644 src/main/java/com/example/devSns/repository/CommentRepository.java create mode 100644 src/main/java/com/example/devSns/service/CommentService.java diff --git a/build.gradle b/build.gradle index c78d56d..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) } } @@ -27,8 +27,8 @@ dependencies { 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' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - runtimeOnly 'com.h2database:h2' // 임시 메모리 DB + 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..ae11563 --- /dev/null +++ b/src/main/java/com/example/devSns/controller/CommentController.java @@ -0,0 +1,30 @@ +package com.example.devSns.controller; + +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 CommentEntity commentEntity) { + commentService.createComment(postId, commentEntity); + 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/entity/CommentEntity.java b/src/main/java/com/example/devSns/entity/CommentEntity.java new file mode 100644 index 0000000..7497902 --- /dev/null +++ b/src/main/java/com/example/devSns/entity/CommentEntity.java @@ -0,0 +1,38 @@ +package com.example.devSns.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +public class CommentEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String content; + private String username; + private int likes; + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + // Post와 N:1 관계 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private PostEntity post; + + @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 index ccaad1a..d5c47f0 100644 --- a/src/main/java/com/example/devSns/entity/PostEntity.java +++ b/src/main/java/com/example/devSns/entity/PostEntity.java @@ -5,6 +5,8 @@ import lombok.Setter; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @@ -22,6 +24,10 @@ public class PostEntity { private LocalDateTime createdAt; private LocalDateTime updatedAt; + // post를 삭제하면 댓글도 같이 삭제 + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + @PrePersist protected void onCreate() { this.createdAt = LocalDateTime.now(); 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/service/CommentService.java b/src/main/java/com/example/devSns/service/CommentService.java new file mode 100644 index 0000000..d147852 --- /dev/null +++ b/src/main/java/com/example/devSns/service/CommentService.java @@ -0,0 +1,42 @@ +package com.example.devSns.service; + +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(); + } + + // 댓글 생성 + @Transactional + public CommentEntity createComment(Long postId, CommentEntity comment) { + PostEntity postEntity = postRepository.findById(postId).orElseThrow(); + comment.setPost(postEntity); + return commentRepository.save(comment); + } + + // 댓글 삭제 + @Transactional + public void deleteComment(Long id) { + commentRepository.deleteById(id); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8bef822..e782abf 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,21 +3,17 @@ spring.application.name=devSns # ?? ?? server.port=8080 -# H2 ???? DB -spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;DATABASE_TO_UPPER=false -spring.datasource.driver-class-name=org.h2.Driver -spring.datasource.username=sa -spring.datasource.password= +# 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 -# H2 ?? -spring.h2.console.enabled=true -spring.h2.console.path=/h2-console - -# Thymeleaf(??? ??? ?? ?) +# Thymeleaf spring.thymeleaf.cache=false From f955163528b9a96d22d30b6d78680a90796a2f36 Mon Sep 17 00:00:00 2001 From: junyi04 Date: Thu, 13 Nov 2025 14:40:32 +0900 Subject: [PATCH 15/23] =?UTF-8?q?feat:=20detail=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=97=90=20=EB=8C=93=EA=B8=80=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/templates/detail.html | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/main/resources/templates/detail.html b/src/main/resources/templates/detail.html index bdaeae8..631ec47 100644 --- a/src/main/resources/templates/detail.html +++ b/src/main/resources/templates/detail.html @@ -17,5 +17,46 @@

내용

+ +
+

댓글

+ +
+

댓글이 없습니다.

+
+ +
    +
  • +

    +

    +

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

댓글 작성하기

+ +
+

+ 작성자: + +

+

+ 내용:
+ +

+ + +
+ \ No newline at end of file From a06835e9409b25b0dab4574cc063a55529c67672 Mon Sep 17 00:00:00 2001 From: junyi04 Date: Thu, 13 Nov 2025 15:48:26 +0900 Subject: [PATCH 16/23] =?UTF-8?q?refactor:=20Entity=EB=A5=BC=20DTO=20?= =?UTF-8?q?=EC=93=B0=EB=8A=94=20=EB=B0=A9=ED=96=A5=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devSns/controller/CommentController.java | 5 +-- .../example/devSns/dto/CommentRequest.java | 13 ++++++++ .../example/devSns/dto/CommentResponse.java | 20 ++++++++++++ .../example/devSns/entity/CommentEntity.java | 23 +++++++++++--- .../com/example/devSns/entity/PostEntity.java | 2 +- .../devSns/service/CommentService.java | 31 ++++++++++++++++--- src/main/resources/templates/detail.html | 3 +- 7 files changed, 83 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/example/devSns/dto/CommentRequest.java create mode 100644 src/main/java/com/example/devSns/dto/CommentResponse.java diff --git a/src/main/java/com/example/devSns/controller/CommentController.java b/src/main/java/com/example/devSns/controller/CommentController.java index ae11563..66874de 100644 --- a/src/main/java/com/example/devSns/controller/CommentController.java +++ b/src/main/java/com/example/devSns/controller/CommentController.java @@ -1,5 +1,6 @@ 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; @@ -16,8 +17,8 @@ public CommentController(CommentService commentService) { // 댓글 생성 @PostMapping("/{postId}") - public String createComment(@PathVariable Long postId, @ModelAttribute CommentEntity commentEntity) { - commentService.createComment(postId, commentEntity); + public String createComment(@PathVariable Long postId, @ModelAttribute CommentRequest request) { + commentService.createComment(postId, request); return "redirect:/posts/" + postId; } 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/entity/CommentEntity.java b/src/main/java/com/example/devSns/entity/CommentEntity.java index 7497902..07d93eb 100644 --- a/src/main/java/com/example/devSns/entity/CommentEntity.java +++ b/src/main/java/com/example/devSns/entity/CommentEntity.java @@ -2,12 +2,12 @@ import jakarta.persistence.*; import lombok.Getter; -import lombok.Setter; + import java.time.LocalDateTime; +// DTO를 사용하고 Setter를 삭제 (안전) @Entity @Getter -@Setter public class CommentEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -15,7 +15,6 @@ public class CommentEntity { private String content; private String username; - private int likes; private LocalDateTime createdAt; private LocalDateTime updatedAt; @@ -23,7 +22,23 @@ public class CommentEntity { // Post와 N:1 관계 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id") - private PostEntity post; + 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() { diff --git a/src/main/java/com/example/devSns/entity/PostEntity.java b/src/main/java/com/example/devSns/entity/PostEntity.java index d5c47f0..9a71dfc 100644 --- a/src/main/java/com/example/devSns/entity/PostEntity.java +++ b/src/main/java/com/example/devSns/entity/PostEntity.java @@ -25,7 +25,7 @@ public class PostEntity { private LocalDateTime updatedAt; // post를 삭제하면 댓글도 같이 삭제 - @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(mappedBy = "postEntity", cascade = CascadeType.ALL, orphanRemoval = true) private List comments = new ArrayList<>(); @PrePersist diff --git a/src/main/java/com/example/devSns/service/CommentService.java b/src/main/java/com/example/devSns/service/CommentService.java index d147852..0246c4e 100644 --- a/src/main/java/com/example/devSns/service/CommentService.java +++ b/src/main/java/com/example/devSns/service/CommentService.java @@ -1,5 +1,7 @@ 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; @@ -21,17 +23,36 @@ public CommentService(CommentRepository commentRepository, PostRepository postRe } // 특정 게시글 댓글 조회 - public List getCommentsByPost(Long postId) { + public List getCommentsByPost(Long postId) { PostEntity postEntity = postRepository.findById(postId).orElseThrow(); - return postEntity.getComments(); + return postEntity.getComments().stream() + .map(comment -> new CommentResponse( + comment.getId(), + comment.getUsername(), + comment.getContent(), + comment.getCreatedAt().toString() + )) + .toList(); } // 댓글 생성 @Transactional - public CommentEntity createComment(Long postId, CommentEntity comment) { + public CommentResponse createComment(Long postId, CommentRequest request) { PostEntity postEntity = postRepository.findById(postId).orElseThrow(); - comment.setPost(postEntity); - return commentRepository.save(comment); + CommentEntity comment = CommentEntity.create( + postEntity, + request.getUsername(), + request.getContent() + ); + + commentRepository.save(comment); + + return new CommentResponse( + comment.getId(), + comment.getUsername(), + comment.getContent(), + comment.getCreatedAt().toString() + ); } // 댓글 삭제 diff --git a/src/main/resources/templates/detail.html b/src/main/resources/templates/detail.html index 631ec47..cdbd81b 100644 --- a/src/main/resources/templates/detail.html +++ b/src/main/resources/templates/detail.html @@ -29,9 +29,8 @@

댓글

  • -

    +

    -
    From f090e611493c209bc005195c62ff0b84c8126cb6 Mon Sep 17 00:00:00 2001 From: junyi04 Date: Thu, 13 Nov 2025 15:56:17 +0900 Subject: [PATCH 17/23] =?UTF-8?q?feat:=20week-2=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/templates/detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/templates/detail.html b/src/main/resources/templates/detail.html index cdbd81b..a867bd4 100644 --- a/src/main/resources/templates/detail.html +++ b/src/main/resources/templates/detail.html @@ -29,7 +29,7 @@

    댓글

  • -

    +

    작성일:

    From 6bb0f9d86270769123bfddb7b4b43e0013fb6d0f Mon Sep 17 00:00:00 2001 From: junyi04 Date: Thu, 13 Nov 2025 16:26:19 +0900 Subject: [PATCH 18/23] =?UTF-8?q?feat:=20MemberEntity=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20=EB=B0=8F=20=EC=97=B0=EA=B4=80=EA=B4=80=EA=B3=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/devSns/entity/CommentEntity.java | 4 ++ .../example/devSns/entity/MemberEntity.java | 37 +++++++++++++++++++ .../com/example/devSns/entity/PostEntity.java | 4 ++ 3 files changed, 45 insertions(+) create mode 100644 src/main/java/com/example/devSns/entity/MemberEntity.java diff --git a/src/main/java/com/example/devSns/entity/CommentEntity.java b/src/main/java/com/example/devSns/entity/CommentEntity.java index 07d93eb..c263fb9 100644 --- a/src/main/java/com/example/devSns/entity/CommentEntity.java +++ b/src/main/java/com/example/devSns/entity/CommentEntity.java @@ -19,6 +19,10 @@ public class CommentEntity { private LocalDateTime createdAt; private LocalDateTime updatedAt; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private MemberEntity member; + // Post와 N:1 관계 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id") diff --git a/src/main/java/com/example/devSns/entity/MemberEntity.java b/src/main/java/com/example/devSns/entity/MemberEntity.java new file mode 100644 index 0000000..be7d664 --- /dev/null +++ b/src/main/java/com/example/devSns/entity/MemberEntity.java @@ -0,0 +1,37 @@ +package com.example.devSns.entity; + +import jakarta.persistence.*; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +public class MemberEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String username; + private String email; + + private LocalDateTime createdAt; + + @OneToMany(mappedBy = "member") + private List posts = new ArrayList<>(); + + @OneToMany(mappedBy = "member") + private List comments = new ArrayList<>(); + + protected MemberEntity() {} + + public static MemberEntity create(String username, String email) { + MemberEntity member = new MemberEntity(); + member.username = username; + member.email = email; + member.createdAt = LocalDateTime.now(); + return member; + } +} diff --git a/src/main/java/com/example/devSns/entity/PostEntity.java b/src/main/java/com/example/devSns/entity/PostEntity.java index 9a71dfc..cb0e4ce 100644 --- a/src/main/java/com/example/devSns/entity/PostEntity.java +++ b/src/main/java/com/example/devSns/entity/PostEntity.java @@ -24,6 +24,10 @@ public class PostEntity { private LocalDateTime createdAt; private LocalDateTime updatedAt; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private MemberEntity member; + // post를 삭제하면 댓글도 같이 삭제 @OneToMany(mappedBy = "postEntity", cascade = CascadeType.ALL, orphanRemoval = true) private List comments = new ArrayList<>(); From 7fff80ab94a3a9e06b74731047d55c4b5f8e140b Mon Sep 17 00:00:00 2001 From: junyi04 Date: Thu, 13 Nov 2025 17:10:37 +0900 Subject: [PATCH 19/23] =?UTF-8?q?feat:=20MemberService=20=EB=B9=84?= =?UTF-8?q?=EC=A7=80=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devSns/repository/MemberRepository.java | 10 ++++ .../example/devSns/service/MemberService.java | 46 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/main/java/com/example/devSns/repository/MemberRepository.java create mode 100644 src/main/java/com/example/devSns/service/MemberService.java diff --git a/src/main/java/com/example/devSns/repository/MemberRepository.java b/src/main/java/com/example/devSns/repository/MemberRepository.java new file mode 100644 index 0000000..21050d8 --- /dev/null +++ b/src/main/java/com/example/devSns/repository/MemberRepository.java @@ -0,0 +1,10 @@ +package com.example.devSns.repository; + +import com.example.devSns.entity.MemberEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface MemberRepository extends JpaRepository { + List findByUsernameContaining(String keyword); +} diff --git a/src/main/java/com/example/devSns/service/MemberService.java b/src/main/java/com/example/devSns/service/MemberService.java new file mode 100644 index 0000000..27441a7 --- /dev/null +++ b/src/main/java/com/example/devSns/service/MemberService.java @@ -0,0 +1,46 @@ +package com.example.devSns.service; + +import com.example.devSns.entity.CommentEntity; +import com.example.devSns.entity.MemberEntity; +import com.example.devSns.entity.PostEntity; +import com.example.devSns.repository.MemberRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +public class MemberService { + private final MemberRepository memberRepository; + + public MemberService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + // 멤버 생성 + public MemberEntity createMember(String username, String email) { + MemberEntity member = MemberEntity.create(username, email); + return memberRepository.save(member); + } + + // 단건 조회 + public MemberEntity getMember(Long id) { + return memberRepository.findById(id).orElseThrow(); + } + + // 검색 + public List search(String keyword) { + return memberRepository.findByUsernameContaining(keyword); + } + + // 멤버의 게시글 조회 + public List getMemberPosts(Long id) { + return getMember(id).getPosts(); + } + + // 멤버의 댓글 조회 + public List getMemberComments(Long id) { + return getMember(id).getComments(); + } +} From c2cd66e96713514204dc046d22143e10a6e9f98c Mon Sep 17 00:00:00 2001 From: junyi04 Date: Fri, 14 Nov 2025 13:15:04 +0900 Subject: [PATCH 20/23] =?UTF-8?q?feat:=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/devSns/entity/LikeEntity.java | 4 ++++ .../java/com/example/devSns/repository/LikeRepository.java | 4 ++++ src/main/java/com/example/devSns/service/LikeService.java | 4 ++++ 3 files changed, 12 insertions(+) create mode 100644 src/main/java/com/example/devSns/entity/LikeEntity.java create mode 100644 src/main/java/com/example/devSns/repository/LikeRepository.java create mode 100644 src/main/java/com/example/devSns/service/LikeService.java diff --git a/src/main/java/com/example/devSns/entity/LikeEntity.java b/src/main/java/com/example/devSns/entity/LikeEntity.java new file mode 100644 index 0000000..9cfc3c1 --- /dev/null +++ b/src/main/java/com/example/devSns/entity/LikeEntity.java @@ -0,0 +1,4 @@ +package com.example.devSns.entity; + +public class LikeEntity { +} diff --git a/src/main/java/com/example/devSns/repository/LikeRepository.java b/src/main/java/com/example/devSns/repository/LikeRepository.java new file mode 100644 index 0000000..a4c8607 --- /dev/null +++ b/src/main/java/com/example/devSns/repository/LikeRepository.java @@ -0,0 +1,4 @@ +package com.example.devSns.repository; + +public interface LikeRepository { +} diff --git a/src/main/java/com/example/devSns/service/LikeService.java b/src/main/java/com/example/devSns/service/LikeService.java new file mode 100644 index 0000000..aa8028d --- /dev/null +++ b/src/main/java/com/example/devSns/service/LikeService.java @@ -0,0 +1,4 @@ +package com.example.devSns.service; + +public class LikeService { +} From 5bef8a808d4474552d24253679d627222cc5ef6d Mon Sep 17 00:00:00 2001 From: junyi04 Date: Fri, 14 Nov 2025 13:30:20 +0900 Subject: [PATCH 21/23] =?UTF-8?q?feat:=20member=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/devSns/controller/MemberController.java | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/main/java/com/example/devSns/controller/MemberController.java diff --git a/src/main/java/com/example/devSns/controller/MemberController.java b/src/main/java/com/example/devSns/controller/MemberController.java new file mode 100644 index 0000000..7fffd2e --- /dev/null +++ b/src/main/java/com/example/devSns/controller/MemberController.java @@ -0,0 +1,4 @@ +package com.example.devSns.controller; + +public class MemberController { +} From fed4d156627cb9b7feb5096581d6a282cb750f29 Mon Sep 17 00:00:00 2001 From: junyi04 Date: Fri, 14 Nov 2025 13:45:55 +0900 Subject: [PATCH 22/23] =?UTF-8?q?feat:=20=EB=A9=A4=EB=B2=84=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EB=B0=8F=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=97=B0=EA=B2=B0=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/devSns/config/DataInit.java | 4 ++++ src/main/resources/templates/member-detail.html | 10 ++++++++++ src/main/resources/templates/member-form.html | 10 ++++++++++ src/main/resources/templates/member-list.html | 10 ++++++++++ src/main/resources/templates/member-search.html | 10 ++++++++++ 5 files changed, 44 insertions(+) create mode 100644 src/main/java/com/example/devSns/config/DataInit.java create mode 100644 src/main/resources/templates/member-detail.html create mode 100644 src/main/resources/templates/member-form.html create mode 100644 src/main/resources/templates/member-list.html create mode 100644 src/main/resources/templates/member-search.html diff --git a/src/main/java/com/example/devSns/config/DataInit.java b/src/main/java/com/example/devSns/config/DataInit.java new file mode 100644 index 0000000..b425897 --- /dev/null +++ b/src/main/java/com/example/devSns/config/DataInit.java @@ -0,0 +1,4 @@ +package com.example.devSns.config; + +public class DataInit { +} diff --git a/src/main/resources/templates/member-detail.html b/src/main/resources/templates/member-detail.html new file mode 100644 index 0000000..6ef6026 --- /dev/null +++ b/src/main/resources/templates/member-detail.html @@ -0,0 +1,10 @@ + + + + + $Title$ + + +$END$ + + \ No newline at end of file diff --git a/src/main/resources/templates/member-form.html b/src/main/resources/templates/member-form.html new file mode 100644 index 0000000..6ef6026 --- /dev/null +++ b/src/main/resources/templates/member-form.html @@ -0,0 +1,10 @@ + + + + + $Title$ + + +$END$ + + \ No newline at end of file diff --git a/src/main/resources/templates/member-list.html b/src/main/resources/templates/member-list.html new file mode 100644 index 0000000..6ef6026 --- /dev/null +++ b/src/main/resources/templates/member-list.html @@ -0,0 +1,10 @@ + + + + + $Title$ + + +$END$ + + \ No newline at end of file diff --git a/src/main/resources/templates/member-search.html b/src/main/resources/templates/member-search.html new file mode 100644 index 0000000..6ef6026 --- /dev/null +++ b/src/main/resources/templates/member-search.html @@ -0,0 +1,10 @@ + + + + + $Title$ + + +$END$ + + \ No newline at end of file From eeaf7d7c15000c18cfa015c21ab02cd7c9d8a058 Mon Sep 17 00:00:00 2001 From: junyi04 Date: Fri, 14 Nov 2025 13:53:18 +0900 Subject: [PATCH 23/23] =?UTF-8?q?feat:=20week3=20=EA=B3=BC=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/devSns/config/DataInit.java | 20 +++++++ .../devSns/controller/MemberController.java | 57 +++++++++++++++++++ .../devSns/controller/PostController.java | 30 +++++++++- .../com/example/devSns/entity/LikeEntity.java | 29 ++++++++++ .../example/devSns/entity/MemberEntity.java | 3 + .../com/example/devSns/entity/PostEntity.java | 3 + .../devSns/repository/LikeRepository.java | 21 ++++++- .../example/devSns/service/LikeService.java | 44 ++++++++++++++ .../example/devSns/service/MemberService.java | 12 ++++ src/main/resources/templates/detail.html | 57 ++++++++++++++++++- src/main/resources/templates/list.html | 3 + .../resources/templates/member-detail.html | 47 +++++++++++++-- src/main/resources/templates/member-form.html | 25 ++++++-- src/main/resources/templates/member-list.html | 28 +++++++-- .../resources/templates/member-search.html | 31 ++++++++-- 15 files changed, 382 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/example/devSns/config/DataInit.java b/src/main/java/com/example/devSns/config/DataInit.java index b425897..80df5fa 100644 --- a/src/main/java/com/example/devSns/config/DataInit.java +++ b/src/main/java/com/example/devSns/config/DataInit.java @@ -1,4 +1,24 @@ package com.example.devSns.config; +import com.example.devSns.entity.MemberEntity; +import com.example.devSns.repository.MemberRepository; +import jakarta.annotation.PostConstruct; +import org.springframework.stereotype.Component; + +@Component public class DataInit { + + private final MemberRepository memberRepository; + + public DataInit(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @PostConstruct + public void init() { + // Member가 없으면 테스트용 계정 자동 생성 + if (memberRepository.count() == 0) { + memberRepository.save(MemberEntity.create("tester", "tester@example.com")); + } + } } diff --git a/src/main/java/com/example/devSns/controller/MemberController.java b/src/main/java/com/example/devSns/controller/MemberController.java index 7fffd2e..1547cb7 100644 --- a/src/main/java/com/example/devSns/controller/MemberController.java +++ b/src/main/java/com/example/devSns/controller/MemberController.java @@ -1,4 +1,61 @@ package com.example.devSns.controller; +import com.example.devSns.entity.MemberEntity; +import com.example.devSns.service.MemberService; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; + +@Controller +@RequestMapping("/members") public class MemberController { + + private final MemberService memberService; + + public MemberController(MemberService memberService) { + this.memberService = memberService; + } + + // 전체 멤버 목록 + @GetMapping + public String list(Model model) { + model.addAttribute("members", memberService.getAllMembers()); + return "member-list"; + } + + // 멤버 상세 페이지 + @GetMapping("/{id}") + public String detail(@PathVariable Long id, Model model) { + MemberEntity member = memberService.getMember(id); + + model.addAttribute("member", member); + model.addAttribute("posts", member.getPosts()); + model.addAttribute("comments", member.getComments()); + + return "member-detail"; + } + + // 멤버 생성 + @GetMapping("/new") + public String newForm() { + return "member-form"; + } + + // 멤버 생성 처리 + @PostMapping + public String create(@RequestParam String username, + @RequestParam String email) { + + memberService.createMember(username, email); + return "redirect:/members"; + } + + // 멤버 검색 + // /members/search?keyword=abc + @GetMapping("/search") + public String search(@RequestParam String keyword, Model model) { + model.addAttribute("members", memberService.search(keyword)); + model.addAttribute("keyword", keyword); + return "member-search"; + } } diff --git a/src/main/java/com/example/devSns/controller/PostController.java b/src/main/java/com/example/devSns/controller/PostController.java index ae06df7..230a3da 100644 --- a/src/main/java/com/example/devSns/controller/PostController.java +++ b/src/main/java/com/example/devSns/controller/PostController.java @@ -1,6 +1,9 @@ package com.example.devSns.controller; +import com.example.devSns.entity.MemberEntity; import com.example.devSns.entity.PostEntity; +import com.example.devSns.repository.LikeRepository; +import com.example.devSns.repository.MemberRepository; import com.example.devSns.service.PostService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -11,10 +14,16 @@ public class PostController { private final PostService postService; + private final LikeRepository likeRepository; + private final MemberRepository memberRepository; // 생성자 - public PostController(PostService postService) { + public PostController(PostService postService, + LikeRepository likeRepository, + MemberRepository memberRepository) { this.postService = postService; + this.likeRepository = likeRepository; + this.memberRepository = memberRepository; } // HTTP GET /posts @@ -41,7 +50,24 @@ public String create(@ModelAttribute PostEntity postEntity) { // HTTP GET /posts/{id} @GetMapping("/{id}") public String detail(@PathVariable Long id, Model model) { - model.addAttribute("post", postService.getPost(id)); + PostEntity postEntity = postService.getPost(id); + + // 임시 멤버 아이디 + Long memberId = 1L; + MemberEntity member = memberRepository.findById(memberId).orElseThrow(); + + // 좋아요 수 + int likeCount = likeRepository.countByPostId(id); + + // 좋아요 여부 + boolean liked = likeRepository + .findByMemberAndPost(member, postEntity) + .isPresent(); + + model.addAttribute("post", postEntity); + model.addAttribute("likeCount", likeCount); + model.addAttribute("liked", liked); + model.addAttribute("memberId", memberId); return "detail"; } diff --git a/src/main/java/com/example/devSns/entity/LikeEntity.java b/src/main/java/com/example/devSns/entity/LikeEntity.java index 9cfc3c1..76218e8 100644 --- a/src/main/java/com/example/devSns/entity/LikeEntity.java +++ b/src/main/java/com/example/devSns/entity/LikeEntity.java @@ -1,4 +1,33 @@ package com.example.devSns.entity; +import jakarta.persistence.*; +import lombok.Getter; + +@Entity +@Getter public class LikeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private MemberEntity member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private PostEntity post; + + protected LikeEntity() {} + + public static LikeEntity create(MemberEntity member, PostEntity postEntity) { + LikeEntity like = new LikeEntity(); + like.member = member; + like.post = postEntity; + + // JPA 양방향 편의 메서드 -> 자동 갱신 + member.getLikes().add(like); + postEntity.getLikes().add(like); + return like; + } } diff --git a/src/main/java/com/example/devSns/entity/MemberEntity.java b/src/main/java/com/example/devSns/entity/MemberEntity.java index be7d664..bccb35d 100644 --- a/src/main/java/com/example/devSns/entity/MemberEntity.java +++ b/src/main/java/com/example/devSns/entity/MemberEntity.java @@ -25,6 +25,9 @@ public class MemberEntity { @OneToMany(mappedBy = "member") private List comments = new ArrayList<>(); + @OneToMany(mappedBy = "member") + private List likes = new ArrayList<>(); + protected MemberEntity() {} public static MemberEntity create(String username, String email) { diff --git a/src/main/java/com/example/devSns/entity/PostEntity.java b/src/main/java/com/example/devSns/entity/PostEntity.java index cb0e4ce..70b8004 100644 --- a/src/main/java/com/example/devSns/entity/PostEntity.java +++ b/src/main/java/com/example/devSns/entity/PostEntity.java @@ -32,6 +32,9 @@ public class PostEntity { @OneToMany(mappedBy = "postEntity", cascade = CascadeType.ALL, orphanRemoval = true) private List comments = new ArrayList<>(); + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List likes = new ArrayList<>(); + @PrePersist protected void onCreate() { this.createdAt = LocalDateTime.now(); diff --git a/src/main/java/com/example/devSns/repository/LikeRepository.java b/src/main/java/com/example/devSns/repository/LikeRepository.java index a4c8607..c84eb53 100644 --- a/src/main/java/com/example/devSns/repository/LikeRepository.java +++ b/src/main/java/com/example/devSns/repository/LikeRepository.java @@ -1,4 +1,23 @@ package com.example.devSns.repository; -public interface LikeRepository { +import com.example.devSns.entity.LikeEntity; +import com.example.devSns.entity.MemberEntity; +import com.example.devSns.entity.PostEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface LikeRepository extends JpaRepository { + // 특정 멤버가 특정 게시글에 좋아요 눌렀는지 확인 + Optional findByMemberAndPost(MemberEntity member, PostEntity postEntity); + + // 특정 게시글 전체 좋아요 조회 + List findByPost(PostEntity post); + + // 특정 멤버가 누른 모든 좋아요 조회 + List findByMember(MemberEntity member); + + // 좋아요 개수 조회 + int countByPostId(Long postId); } diff --git a/src/main/java/com/example/devSns/service/LikeService.java b/src/main/java/com/example/devSns/service/LikeService.java index aa8028d..1dfcfbc 100644 --- a/src/main/java/com/example/devSns/service/LikeService.java +++ b/src/main/java/com/example/devSns/service/LikeService.java @@ -1,4 +1,48 @@ package com.example.devSns.service; +import com.example.devSns.entity.LikeEntity; +import com.example.devSns.entity.MemberEntity; +import com.example.devSns.entity.PostEntity; +import com.example.devSns.repository.LikeRepository; +import com.example.devSns.repository.MemberRepository; +import com.example.devSns.repository.PostRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional public class LikeService { + private final MemberRepository memberRepository; + private final PostRepository postRepository; + private final LikeRepository likeRepository; + + public LikeService(MemberRepository memberRepository, PostRepository postRepository, LikeRepository likeRepository) { + this.memberRepository = memberRepository; + this.postRepository = postRepository; + this.likeRepository = likeRepository; + } + + // 좋아요 클릭 + public void like(Long postId, Long memberId) { + MemberEntity member = memberRepository.findById(memberId).orElseThrow(); + PostEntity postEntity = postRepository.findById(postId).orElseThrow(); + + // 중복 체크 + likeRepository.findByMemberAndPost(member, postEntity).ifPresent(like -> { + throw new IllegalStateException("이미 좋아요를 눌렀습니다."); + }); + + likeRepository.save(LikeEntity.create(member, postEntity)); + } + + // 좋아요 취소 + public void cancel(Long postId, Long memberId) { + MemberEntity member = memberRepository.findById(memberId).orElseThrow(); + PostEntity postEntity = postRepository.findById(postId).orElseThrow(); + + LikeEntity like = likeRepository.findByMemberAndPost(member, postEntity) + .orElseThrow(() -> new IllegalStateException("좋아요를 누른 적이 없습니다.")); + + likeRepository.delete(like); + } } diff --git a/src/main/java/com/example/devSns/service/MemberService.java b/src/main/java/com/example/devSns/service/MemberService.java index 27441a7..5597b4b 100644 --- a/src/main/java/com/example/devSns/service/MemberService.java +++ b/src/main/java/com/example/devSns/service/MemberService.java @@ -1,6 +1,7 @@ package com.example.devSns.service; import com.example.devSns.entity.CommentEntity; +import com.example.devSns.entity.LikeEntity; import com.example.devSns.entity.MemberEntity; import com.example.devSns.entity.PostEntity; import com.example.devSns.repository.MemberRepository; @@ -43,4 +44,15 @@ public List getMemberPosts(Long id) { public List getMemberComments(Long id) { return getMember(id).getComments(); } + + // 멤버가 누른 좋아요 목록 + public List getMemberLikes(Long id) { + return getMember(id).getLikes(); + } + + // 멤버 조회 + public List getAllMembers() { + return memberRepository.findAll(); + } + } diff --git a/src/main/resources/templates/detail.html b/src/main/resources/templates/detail.html index a867bd4..b99d7ba 100644 --- a/src/main/resources/templates/detail.html +++ b/src/main/resources/templates/detail.html @@ -1,6 +1,7 @@ +

    제목

    내용

    @@ -8,6 +9,7 @@

    내용

    + @@ -18,6 +20,23 @@

    내용

    + +
    + +
    + +
    + +
    + +

    댓글

    @@ -29,7 +48,9 @@

    댓글

  • -

    작성일:

    +

    작성일: + +

    @@ -42,6 +63,7 @@

    댓글


    +

    댓글 작성하기

    @@ -57,5 +79,38 @@

    댓글 작성하기

  • + + + \ No newline at end of file diff --git a/src/main/resources/templates/list.html b/src/main/resources/templates/list.html index c444459..62c5d7e 100644 --- a/src/main/resources/templates/list.html +++ b/src/main/resources/templates/list.html @@ -16,4 +16,7 @@

    게시판

  • + + + diff --git a/src/main/resources/templates/member-detail.html b/src/main/resources/templates/member-detail.html index 6ef6026..77de7d8 100644 --- a/src/main/resources/templates/member-detail.html +++ b/src/main/resources/templates/member-detail.html @@ -1,10 +1,45 @@ - - - - $Title$ - + -$END$ + +

    +

    + 이메일:
    + 가입일: +

    + +
    + +

    작성한 게시글

    +
    +

    작성한 게시글이 없습니다.

    +
    + +
      +
    • + + (작성일: ) +
    • +
    + +
    + +

    작성한 댓글

    +
    +

    작성한 댓글이 없습니다.

    +
    + +
      +
    • + : + + () +
    • +
    + +
    + +전체 멤버 목록 + \ No newline at end of file diff --git a/src/main/resources/templates/member-form.html b/src/main/resources/templates/member-form.html index 6ef6026..d777c99 100644 --- a/src/main/resources/templates/member-form.html +++ b/src/main/resources/templates/member-form.html @@ -1,10 +1,23 @@ - - - - $Title$ - + -$END$ + +

    새 멤버 생성

    + +
    +

    + 이름: +

    + +

    + 이메일: +

    + + +
    + +
    +전체 멤버 목록 보기 + \ No newline at end of file diff --git a/src/main/resources/templates/member-list.html b/src/main/resources/templates/member-list.html index 6ef6026..03c7934 100644 --- a/src/main/resources/templates/member-list.html +++ b/src/main/resources/templates/member-list.html @@ -1,10 +1,26 @@ - - - - $Title$ - + -$END$ +

    전체 멤버 목록

    + +새 멤버 만들기 + +
    + + +
    + +
    + +
      +
    • + + ( ) +
    • +
    + +게시글 목록 + \ No newline at end of file diff --git a/src/main/resources/templates/member-search.html b/src/main/resources/templates/member-search.html index 6ef6026..2c70fce 100644 --- a/src/main/resources/templates/member-search.html +++ b/src/main/resources/templates/member-search.html @@ -1,10 +1,29 @@ - - - - $Title$ - + -$END$ + +

    + +
    + + +
    + +
    + +
    +

    검색 결과가 없습니다.

    +
    + +
      +
    • + + ( ) +
    • +
    + +멤버 목록으로 + \ No newline at end of file