-
Notifications
You must be signed in to change notification settings - Fork 13
Feat/week 1 #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 현승민
Are you sure you want to change the base?
Feat/week 1 #11
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,3 +35,4 @@ out/ | |
|
|
||
| ### VS Code ### | ||
| .vscode/ | ||
| .env | ||
| 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; } | ||
| } |
| 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(); | ||
| } | ||
| } | ||
| 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); | ||
| } |
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
| } | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 만약 content가 1000자 이상이라면 DB에서 에러가 날 것 같습니다! 입력 값에 대해서는 항상 validation을 적용하는 게 좋습니다. @Valid와 validation관련 어노테이션을 공부해보시면 좋을 것 같네요 |
||
|
|
||
|
Comment on lines
+3
to
+6
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
| } | ||
| 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; } | ||
| } |
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; } | ||
| } | ||
| 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(); | ||
| } | ||
| } |
| 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); } | ||
| } |
| 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> { } |
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| 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; | ||
| } |
| 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(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
적절한 HTTP 상태코드 지정 좋습니다!