Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ out/

### VS Code ###
.vscode/
.env
8 changes: 5 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ repositories {
}

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-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/com/example/devSns/comment/Comment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.example.devSns.comment;

import com.example.devSns.task.Task;
import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "comments")
public class Comment {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, length = 1000)
private String content;

private String username;
private LocalDateTime createdAt = LocalDateTime.now();

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "task_id")
private Task task;

protected Comment() {}

public Comment(String content, String username, Task task) {
this.content = content;
this.username = username;
this.task = task;
}

public Long getId() { return id; }
public String getContent() { return content; }
public String getUsername() { return username; }
public LocalDateTime getCreatedAt() { return createdAt; }
public Task getTask() { return task; }
}
37 changes: 37 additions & 0 deletions src/main/java/com/example/devSns/comment/CommentController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.example.devSns.comment;

import com.example.devSns.comment.dto.CommentRequest;
import com.example.devSns.comment.dto.CommentResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/api/tasks/{taskId}/comments")
public class CommentController {
private final CommentService service;

public CommentController(CommentService service) {
this.service = service;
}

@PostMapping
public ResponseEntity<CommentResponse> create(
@PathVariable Long taskId,
@RequestBody CommentRequest req) {
return ResponseEntity.ok(service.create(taskId, req));
}

@GetMapping
public List<CommentResponse> list(@PathVariable Long taskId) {
return service.findByTask(taskId);
}

@DeleteMapping("/{commentId}")
public ResponseEntity<Void> delete(
@PathVariable Long taskId,
@PathVariable Long commentId) {
service.delete(commentId);
return ResponseEntity.noContent().build();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

적절한 HTTP 상태코드 지정 좋습니다!

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.devSns.comment;

import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findByTaskId(Long taskId);
}
36 changes: 36 additions & 0 deletions src/main/java/com/example/devSns/comment/CommentService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.example.devSns.comment;

import com.example.devSns.comment.dto.CommentRequest;
import com.example.devSns.comment.dto.CommentResponse;
import com.example.devSns.task.Task;
import com.example.devSns.task.TaskRepository;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class CommentService {
private final CommentRepository commentRepo;
private final TaskRepository taskRepo;

public CommentService(CommentRepository commentRepo, TaskRepository taskRepo) {
this.commentRepo = commentRepo;
this.taskRepo = taskRepo;
}

public CommentResponse create(Long taskId, CommentRequest req) {
Task task = taskRepo.findById(taskId)
.orElseThrow(() -> new IllegalArgumentException("Task not found"));
Comment comment = new Comment(req.getContent(), req.getUsername(), task);
Comment saved = commentRepo.save(comment);
return new CommentResponse(saved);
}
Comment on lines +20 to +26
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안정적인 비즈니스 로직 수행을 보장하기 위해서 서비스 계층에서 @transaction을 적용하는 게 좋을 것 같습니다! readOnly 옵션에 대해서도 추가적으로 공부해보면 좋을 것 같아요


public List<CommentResponse> findByTask(Long taskId) {
return commentRepo.findByTaskId(taskId)
.stream().map(CommentResponse::new).toList();
}

public void delete(Long commentId) {
commentRepo.deleteById(commentId);
}
}
24 changes: 24 additions & 0 deletions src/main/java/com/example/devSns/comment/dto/CommentRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.devSns.comment.dto;

public class CommentRequest {
private String content;
private String username;
Comment on lines +4 to +5
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 content가 1000자 이상이라면 DB에서 에러가 날 것 같습니다! 입력 값에 대해서는 항상 validation을 적용하는 게 좋습니다. @Valid와 validation관련 어노테이션을 공부해보시면 좋을 것 같네요


Comment on lines +3 to +6
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DTO는 class 대신 Record를 사용해서 작성하는 걸 추천드립니다! 불변 객체라 계층 간 값 전달 시 안전하게 사용 가능합니다.

public CommentRequest() {}

public String getContent() {
return content;
}

public void setContent(String content) { // ← 이거 추가
this.content = content;
}

public String getUsername() {
return username;
}

public void setUsername(String username) { // ← 이거 추가
this.username = username;
}
}
23 changes: 23 additions & 0 deletions src/main/java/com/example/devSns/comment/dto/CommentResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.devSns.comment.dto;

import com.example.devSns.comment.Comment;
import java.time.LocalDateTime;

public class CommentResponse {
private Long id;
private String content;
private String username;
private LocalDateTime createdAt;

public CommentResponse(Comment comment) {
this.id = comment.getId();
this.content = comment.getContent();
this.username = comment.getUsername();
this.createdAt = comment.getCreatedAt();
}

public Long getId() { return id; }
public String getContent() { return content; }
public String getUsername() { return username; }
public LocalDateTime getCreatedAt() { return createdAt; }
}
48 changes: 48 additions & 0 deletions src/main/java/com/example/devSns/task/Task.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.example.devSns.task;

import com.example.devSns.comment.Comment;
import jakarta.persistence.*;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "tasks")
public class Task {

public enum Status { TODO, IN_PROGRESS, DONE }

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String title;

@Column(length = 2000)
private String description;

private LocalDate dueDate;
private Integer priority;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Status status = Status.TODO;

@OneToMany(mappedBy = "task", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
Comment on lines +31 to +32
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

양방향 연관관계 설정 시 무한루프에 빠지기 쉽습니다. 저도 예전에 그랬던 적이 있기도 하고 사실 단방향 연관관계만(ManyToOne만 적용) 설정하더라도 DB 테이블 구조는 같게 구성됩니다. 그래서 꼭 필요한 게 아니라면 단방향으로 구성하는 게 관리하기 쉽습니다.


protected Task() {}

public Long getId() { return id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public LocalDate getDueDate() { return dueDate; }
public void setDueDate(LocalDate dueDate) { this.dueDate = dueDate; }
public Integer getPriority() { return priority; }
public void setPriority(Integer priority) { this.priority = priority; }
public Status getStatus() { return status; }
public void setStatus(Status status) { this.status = status; }
public List<Comment> getComments() { return comments; }
}
53 changes: 53 additions & 0 deletions src/main/java/com/example/devSns/task/TaskController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.example.devSns.task;

import com.example.devSns.task.dto.TaskRequest;
import com.example.devSns.task.dto.TaskResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.net.URI;
import java.util.List;

@RestController
@RequestMapping("/api/tasks")
public class TaskController {

private final TaskService service;

public TaskController(TaskService service) {
this.service = service;
}

@PostMapping
public ResponseEntity<TaskResponse> create(@RequestBody TaskRequest req) {
Task saved = service.create(req);
return ResponseEntity.created(URI.create("/api/tasks/" + saved.getId()))
.body(new TaskResponse(saved));
}

@GetMapping
public ResponseEntity<List<TaskResponse>> list() {
List<TaskResponse> tasks = service.findAll().stream()
.map(TaskResponse::new)
.toList();
return ResponseEntity.ok(tasks);
}

@GetMapping("/{id}")
public ResponseEntity<TaskResponse> get(@PathVariable Long id) {
Task found = service.findById(id);
return ResponseEntity.ok(new TaskResponse(found));
}

@PutMapping("/{id}")
public ResponseEntity<TaskResponse> update(@PathVariable Long id, @RequestBody TaskRequest req) {
Task updated = service.update(id, req);
return ResponseEntity.ok(new TaskResponse(updated));
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
service.delete(id);
return ResponseEntity.noContent().build();
}
}
5 changes: 5 additions & 0 deletions src/main/java/com/example/devSns/task/TaskNotFound.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.devSns.task;

public class TaskNotFound extends RuntimeException {
public TaskNotFound(Long id) { super("Task not found: " + id); }
}
7 changes: 7 additions & 0 deletions src/main/java/com/example/devSns/task/TaskRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//DB에 저장·조회·삭제 등을 대신 처리

package com.example.devSns.task;

import org.springframework.data.jpa.repository.JpaRepository;

public interface TaskRepository extends JpaRepository<Task, Long> { }
46 changes: 46 additions & 0 deletions src/main/java/com/example/devSns/task/TaskService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//컨트롤러와 DB 사이의 중간 관리자

package com.example.devSns.task;

import com.example.devSns.task.dto.TaskRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@Transactional
public class TaskService {
private final TaskRepository repo;
public TaskService(TaskRepository repo) { this.repo = repo; }

public Task create(TaskRequest r) {
Task t = new Task();
if (r.title != null) t.setTitle(r.title);
t.setDescription(r.description);
t.setDueDate(r.dueDate);
t.setPriority(r.priority);
t.setStatus(r.status == null ? Task.Status.TODO : r.status);
Comment on lines +19 to +23
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DTO를 Record로 만들고 dto에 entity 변환 로직을 구성하거나 mapper 등을 두면 서비스 로직을 간결하게 만들 수 있습니다.

return repo.save(t);
}

@Transactional(readOnly = true)
public List<Task> findAll() { return repo.findAll(); }

@Transactional(readOnly = true)
public Task findById(Long id) { // 값이 없으면 TaskNotFound
return repo.findById(id).orElseThrow(() -> new TaskNotFound(id));
}

public Task update(Long id, TaskRequest r) { //부분 수정 가능
Task t = findById(id);
if (r.title != null) t.setTitle(r.title);
if (r.description != null) t.setDescription(r.description);
if (r.dueDate != null) t.setDueDate(r.dueDate);
if (r.priority != null) t.setPriority(r.priority);
if (r.status != null) t.setStatus(r.status);
return t; // JPA dirty checking
}
Comment on lines +35 to +43
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 부분은 task 클래스에서 처리하는 게 더 좋을 것 같습니다! setter를 사용하기보단 findById로 task를 찾고 task.update를 호출해서 task 내부적으로 처리하면 더 깔끔해질 것 같아요


public void delete(Long id) { repo.delete(findById(id)); }
} // 존재하지 않으면 TaskNotFound
14 changes: 14 additions & 0 deletions src/main/java/com/example/devSns/task/dto/TaskRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//client가 보낸 json 요청 받기 위한 dto

package com.example.devSns.task.dto;

import com.example.devSns.task.Task;
import java.time.LocalDate;

public class TaskRequest {
public String title;
public String description;
public LocalDate dueDate;
public Integer priority;
public Task.Status status;
}
24 changes: 24 additions & 0 deletions src/main/java/com/example/devSns/task/dto/TaskResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//DB에서 가져온 엔티티를 clinet에게 보낼 형식으로 만드는 DTO

package com.example.devSns.task.dto;

import com.example.devSns.task.Task;
import java.time.LocalDate;

public class TaskResponse {
public Long id;
public String title;
public String description;
public LocalDate dueDate;
public Integer priority;
public Task.Status status;

public TaskResponse(Task t) {
this.id = t.getId();
this.title = t.getTitle();
this.description = t.getDescription();
this.dueDate = t.getDueDate();
this.priority = t.getPriority();
this.status = t.getStatus();
}
}
Loading