From 94b3229b16530f1ec44dfe094643397c8bd32f2c Mon Sep 17 00:00:00 2001 From: Park Jae Hong <105151063+PHJ2000@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:55:30 +0900 Subject: [PATCH] feat(post): add CRUD with like + H2 config --- build.gradle | 9 +++ .../com/example/devSns/DevSnsApplication.java | 17 +++++ .../devSns/common/config/JpaConfig.java | 8 +++ .../exception/GlobalExceptionHandler.java | 24 +++++++ .../com/example/devSns/domain/post/Post.java | 27 ++++++++ .../devSns/domain/post/PostRepository.java | 4 ++ .../devSns/service/post/PostService.java | 59 ++++++++++++++++ .../devSns/web/post/PostController.java | 68 +++++++++++++++++++ .../example/devSns/web/post/dto/PostDtos.java | 29 ++++++++ src/main/resources/application.properties | 12 +++- 10 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/devSns/common/config/JpaConfig.java create mode 100644 src/main/java/com/example/devSns/common/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/example/devSns/domain/post/Post.java create mode 100644 src/main/java/com/example/devSns/domain/post/PostRepository.java create mode 100644 src/main/java/com/example/devSns/service/post/PostService.java create mode 100644 src/main/java/com/example/devSns/web/post/PostController.java create mode 100644 src/main/java/com/example/devSns/web/post/dto/PostDtos.java diff --git a/build.gradle b/build.gradle index 610d6a6..1ebcd77 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,15 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + + runtimeOnly 'com.h2database:h2' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + + implementation 'org.springframework.boot:spring-boot-starter-validation' } tasks.named('test') { diff --git a/src/main/java/com/example/devSns/DevSnsApplication.java b/src/main/java/com/example/devSns/DevSnsApplication.java index b965724..f600134 100644 --- a/src/main/java/com/example/devSns/DevSnsApplication.java +++ b/src/main/java/com/example/devSns/DevSnsApplication.java @@ -2,6 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Bean; +import com.example.devSns.domain.post.Post; +import com.example.devSns.domain.post.PostRepository; @SpringBootApplication public class DevSnsApplication { @@ -10,4 +14,17 @@ public static void main(String[] args) { SpringApplication.run(DevSnsApplication.class, args); } + // 실행 시 더미데이터 자동 삽입 + @Bean + CommandLineRunner init(PostRepository repo) { + return args -> { + if (repo.count() == 0) { + repo.save(Post.builder() + .content("hello h2") + .username("hong") + .likes(0) + .build()); + } + }; + } } diff --git a/src/main/java/com/example/devSns/common/config/JpaConfig.java b/src/main/java/com/example/devSns/common/config/JpaConfig.java new file mode 100644 index 0000000..839f495 --- /dev/null +++ b/src/main/java/com/example/devSns/common/config/JpaConfig.java @@ -0,0 +1,8 @@ +package com.example.devSns.common.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaConfig {} \ No newline at end of file diff --git a/src/main/java/com/example/devSns/common/exception/GlobalExceptionHandler.java b/src/main/java/com/example/devSns/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..0b968b8 --- /dev/null +++ b/src/main/java/com/example/devSns/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,24 @@ +package com.example.devSns.common.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgument(IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidation(MethodArgumentNotValidException e) { + String msg = e.getBindingResult().getFieldErrors().stream() + .map(err -> err.getField() + ": " + err.getDefaultMessage()) + .findFirst().orElse("Validation error"); + return ResponseEntity.badRequest().body(msg); + } +} diff --git a/src/main/java/com/example/devSns/domain/post/Post.java b/src/main/java/com/example/devSns/domain/post/Post.java new file mode 100644 index 0000000..867f0a4 --- /dev/null +++ b/src/main/java/com/example/devSns/domain/post/Post.java @@ -0,0 +1,27 @@ +// com.example.devSns.domain.post.Post +package com.example.devSns.domain.post; + +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import java.time.LocalDateTime; + +@Entity @Table(name="posts") +@EntityListeners(AuditingEntityListener.class) +@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder +public class Post { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(nullable=false, length=1000) + private String content; + @Column(nullable=false) + private String username; + @Column(nullable=false) + private Integer likes; + @CreatedDate @Column(updatable=false) + private LocalDateTime createdAt; + @LastModifiedDate + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/example/devSns/domain/post/PostRepository.java b/src/main/java/com/example/devSns/domain/post/PostRepository.java new file mode 100644 index 0000000..af9d0d4 --- /dev/null +++ b/src/main/java/com/example/devSns/domain/post/PostRepository.java @@ -0,0 +1,4 @@ +// com.example.devSns.domain.post.PostRepository +package com.example.devSns.domain.post; +import org.springframework.data.jpa.repository.JpaRepository; +public interface PostRepository extends JpaRepository {} \ No newline at end of file diff --git a/src/main/java/com/example/devSns/service/post/PostService.java b/src/main/java/com/example/devSns/service/post/PostService.java new file mode 100644 index 0000000..8f692b4 --- /dev/null +++ b/src/main/java/com/example/devSns/service/post/PostService.java @@ -0,0 +1,59 @@ +package com.example.devSns.service.post; + +import com.example.devSns.domain.post.Post; +import com.example.devSns.domain.post.PostRepository; +import com.example.devSns.web.post.dto.PostDtos.CreateReq; +import com.example.devSns.web.post.dto.PostDtos.UpdateReq; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class PostService { + + private final PostRepository postRepository; + + @Transactional + public Post create(CreateReq req) { + Post p = Post.builder() + .content(req.content) + .username(req.username) + .likes(0) + .build(); + return postRepository.save(p); + } + + @Transactional(readOnly = true) + public Post get(Long id) { + return postRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Post not found: " + id)); + } + + @Transactional(readOnly = true) + public List list() { + return postRepository.findAll(); + } + + @Transactional + public Post update(Long id, UpdateReq req) { + Post p = get(id); + p.setContent(req.content); + return p; + } + + @Transactional + public void delete(Long id) { + Post p = get(id); + postRepository.delete(p); + } + + @Transactional + public Post like(Long id) { + Post p = get(id); + p.setLikes(p.getLikes() + 1); + return p; + } +} diff --git a/src/main/java/com/example/devSns/web/post/PostController.java b/src/main/java/com/example/devSns/web/post/PostController.java new file mode 100644 index 0000000..6023600 --- /dev/null +++ b/src/main/java/com/example/devSns/web/post/PostController.java @@ -0,0 +1,68 @@ +package com.example.devSns.web.post; + +import com.example.devSns.domain.post.Post; +import com.example.devSns.service.post.PostService; +import com.example.devSns.web.post.dto.PostDtos.CreateReq; +import com.example.devSns.web.post.dto.PostDtos.UpdateReq; +import com.example.devSns.web.post.dto.PostDtos.Res; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/posts") +public class PostController { + + private final PostService postService; + + // Create + @PostMapping + public Res create(@Valid @RequestBody CreateReq req) { + return toRes(postService.create(req)); + } + + // Read one + @GetMapping("/{id}") + public Res get(@PathVariable Long id) { + return toRes(postService.get(id)); + } + + // Read all + @GetMapping + public List list() { + return postService.list().stream().map(this::toRes).toList(); + } + + // Update + @PutMapping("/{id}") + public Res update(@PathVariable Long id, @Valid @RequestBody UpdateReq req) { + return toRes(postService.update(id, req)); + } + + // Delete + @DeleteMapping("/{id}") + public void delete(@PathVariable Long id) { + postService.delete(id); + } + + // Like + @PostMapping("/{id}/like") + public Res like(@PathVariable Long id) { + return toRes(postService.like(id)); + } + + private Res toRes(Post p) { + Res r = new Res(); + r.id = p.getId(); + r.content = p.getContent(); + r.username = p.getUsername(); + r.likes = p.getLikes(); + r.createdAt = p.getCreatedAt(); + r.updatedAt = p.getUpdatedAt(); + return r; + } +} diff --git a/src/main/java/com/example/devSns/web/post/dto/PostDtos.java b/src/main/java/com/example/devSns/web/post/dto/PostDtos.java new file mode 100644 index 0000000..9e2d176 --- /dev/null +++ b/src/main/java/com/example/devSns/web/post/dto/PostDtos.java @@ -0,0 +1,29 @@ +package com.example.devSns.web.post.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import java.time.LocalDateTime; + +public class PostDtos { + + public static class CreateReq { + @NotBlank @Size(max = 1000) + public String content; + @NotBlank + public String username; + } + + public static class UpdateReq { + @NotBlank @Size(max = 1000) + public String content; + } + + public static class Res { + public Long id; + public String content; + public String username; + public Integer likes; + public LocalDateTime createdAt; + public LocalDateTime updatedAt; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f3f10af..f54e07f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,11 @@ -spring.application.name=devSns +spring.datasource.url=jdbc:h2:mem:devsns;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console + +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true