diff --git a/.gitignore b/.gitignore index c2065bc..9bd38c3 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ out/ ### VS Code ### .vscode/ +.env \ No newline at end of file diff --git a/build.gradle b/build.gradle index 610d6a6..8f83701 100644 --- a/build.gradle +++ b/build.gradle @@ -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') { diff --git a/src/main/java/com/example/devSns/comment/Comment.java b/src/main/java/com/example/devSns/comment/Comment.java new file mode 100644 index 0000000..39b1273 --- /dev/null +++ b/src/main/java/com/example/devSns/comment/Comment.java @@ -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; } +} diff --git a/src/main/java/com/example/devSns/comment/CommentController.java b/src/main/java/com/example/devSns/comment/CommentController.java new file mode 100644 index 0000000..27c85a4 --- /dev/null +++ b/src/main/java/com/example/devSns/comment/CommentController.java @@ -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 create( + @PathVariable Long taskId, + @RequestBody CommentRequest req) { + return ResponseEntity.ok(service.create(taskId, req)); + } + + @GetMapping + public List list(@PathVariable Long taskId) { + return service.findByTask(taskId); + } + + @DeleteMapping("/{commentId}") + public ResponseEntity delete( + @PathVariable Long taskId, + @PathVariable Long commentId) { + service.delete(commentId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/example/devSns/comment/CommentRepository.java b/src/main/java/com/example/devSns/comment/CommentRepository.java new file mode 100644 index 0000000..e32d79a --- /dev/null +++ b/src/main/java/com/example/devSns/comment/CommentRepository.java @@ -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 { + List findByTaskId(Long taskId); +} diff --git a/src/main/java/com/example/devSns/comment/CommentService.java b/src/main/java/com/example/devSns/comment/CommentService.java new file mode 100644 index 0000000..6179550 --- /dev/null +++ b/src/main/java/com/example/devSns/comment/CommentService.java @@ -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); + } + + public List findByTask(Long taskId) { + return commentRepo.findByTaskId(taskId) + .stream().map(CommentResponse::new).toList(); + } + + public void delete(Long commentId) { + commentRepo.deleteById(commentId); + } +} diff --git a/src/main/java/com/example/devSns/comment/dto/CommentRequest.java b/src/main/java/com/example/devSns/comment/dto/CommentRequest.java new file mode 100644 index 0000000..dcddd5d --- /dev/null +++ b/src/main/java/com/example/devSns/comment/dto/CommentRequest.java @@ -0,0 +1,24 @@ +package com.example.devSns.comment.dto; + +public class CommentRequest { + private String content; + private String username; + + 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; + } +} diff --git a/src/main/java/com/example/devSns/comment/dto/CommentResponse.java b/src/main/java/com/example/devSns/comment/dto/CommentResponse.java new file mode 100644 index 0000000..6e6eacb --- /dev/null +++ b/src/main/java/com/example/devSns/comment/dto/CommentResponse.java @@ -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; } +} diff --git a/src/main/java/com/example/devSns/task/Task.java b/src/main/java/com/example/devSns/task/Task.java new file mode 100644 index 0000000..73d05b9 --- /dev/null +++ b/src/main/java/com/example/devSns/task/Task.java @@ -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 comments = new ArrayList<>(); + + 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 getComments() { return comments; } +} diff --git a/src/main/java/com/example/devSns/task/TaskController.java b/src/main/java/com/example/devSns/task/TaskController.java new file mode 100644 index 0000000..1b834ef --- /dev/null +++ b/src/main/java/com/example/devSns/task/TaskController.java @@ -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 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() { + List tasks = service.findAll().stream() + .map(TaskResponse::new) + .toList(); + return ResponseEntity.ok(tasks); + } + + @GetMapping("/{id}") + public ResponseEntity get(@PathVariable Long id) { + Task found = service.findById(id); + return ResponseEntity.ok(new TaskResponse(found)); + } + + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable Long id, @RequestBody TaskRequest req) { + Task updated = service.update(id, req); + return ResponseEntity.ok(new TaskResponse(updated)); + } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Long id) { + service.delete(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/example/devSns/task/TaskNotFound.java b/src/main/java/com/example/devSns/task/TaskNotFound.java new file mode 100644 index 0000000..7066533 --- /dev/null +++ b/src/main/java/com/example/devSns/task/TaskNotFound.java @@ -0,0 +1,5 @@ +package com.example.devSns.task; + +public class TaskNotFound extends RuntimeException { + public TaskNotFound(Long id) { super("Task not found: " + id); } +} diff --git a/src/main/java/com/example/devSns/task/TaskRepository.java b/src/main/java/com/example/devSns/task/TaskRepository.java new file mode 100644 index 0000000..3e9aa55 --- /dev/null +++ b/src/main/java/com/example/devSns/task/TaskRepository.java @@ -0,0 +1,7 @@ +//DB에 저장·조회·삭제 등을 대신 처리 + +package com.example.devSns.task; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TaskRepository extends JpaRepository { } \ No newline at end of file diff --git a/src/main/java/com/example/devSns/task/TaskService.java b/src/main/java/com/example/devSns/task/TaskService.java new file mode 100644 index 0000000..0362a94 --- /dev/null +++ b/src/main/java/com/example/devSns/task/TaskService.java @@ -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); + return repo.save(t); + } + + @Transactional(readOnly = true) + public List 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 + } + + public void delete(Long id) { repo.delete(findById(id)); } +} // 존재하지 않으면 TaskNotFound diff --git a/src/main/java/com/example/devSns/task/dto/TaskRequest.java b/src/main/java/com/example/devSns/task/dto/TaskRequest.java new file mode 100644 index 0000000..d43ba4c --- /dev/null +++ b/src/main/java/com/example/devSns/task/dto/TaskRequest.java @@ -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; +} diff --git a/src/main/java/com/example/devSns/task/dto/TaskResponse.java b/src/main/java/com/example/devSns/task/dto/TaskResponse.java new file mode 100644 index 0000000..23d5549 --- /dev/null +++ b/src/main/java/com/example/devSns/task/dto/TaskResponse.java @@ -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(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f3f10af..499d539 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,13 @@ spring.application.name=devSns + +# DB ???? (?????? ??) +spring.datasource.url=${DATASOURCE_URL} +spring.datasource.username=${DATASOURCE_USERNAME} +spring.datasource.password=${DATASOURCE_PASSWORD} +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.hikari.connection-timeout=5000 + +# JPA ?? +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect \ No newline at end of file