src/main/java
โโโ com.nhnacademy.todo
โโโ controller // REST API ์๋ํฌ์ธํธ ์ ์
โโโ domain // JPA ์ํฐํฐ ํด๋์ค
โโโ dto // ๋ฐ์ดํฐ ์ ์ก ๊ฐ์ฒด (DTO)
โโโ repository // JPA ๋ฆฌํฌ์งํ ๋ฆฌ ์ธํฐํ์ด์ค
โโโ service // ๋น์ฆ๋์ค ๋ก์ง ๊ตฌํ
โโโ exception // ์ฌ์ฉ์ ์ ์ ์์ธ ์ฒ๋ฆฌ
โโโ config // ์ค์ ํ์ผ (์: CORS, ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฑ)
src/main/resources
โโโ application.properties // ํ๊ฒฝ ์ค์ ํ์ผ
package inho.domain;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
@Entity
@NoArgsConstructor
@Data
@Table(name = "todos")
@Slf4j
public class Todo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "subject", nullable = false)
private String subject;
@Column(name = "event_at", nullable = false)
private String eventAt;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@JsonCreator
public Todo(
@JsonProperty("subject") String subject,
@JsonProperty("eventAt") String eventAt) {
this.subject = subject;
this.eventAt = eventAt;
log.debug("eventAt ํ
์คํธ {}", eventAt);
this.createdAt = LocalDateTime.now(); // createdAt์ ํญ์ ์ค์
}
@PrePersist
protected void onCreate() {
if (this.eventAt == null || this.eventAt.isEmpty()) {
throw new IllegalArgumentException("eventAt ํ๋๋ ํ์์
๋๋ค");
}
if (this.createdAt == null) {
this.createdAt = LocalDateTime.now();
}
}
}
subject: ํ ์ผ ์ ๋ชฉ
eventAt: ํ ์ผ ๋ ์ง
createdAt: ์์ฑ ๋ ์ง (์๋ ์ค์ )
@PrePersist: ๋ฐ์ดํฐ ์ ์ฅ ์ ํ์ ๊ฐ ๊ฒ์ฆ
package inho.repository;
import inho.domain.Todo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface TodoRepository extends JpaRepository<Todo, Long> {
List<Todo> findByEventAt(String eventAt);
long countByEventAt(String eventAt);
boolean existsById(Long id);
@Query("SELECT t from Todo t where t.eventAt LIKE :eventMonth%")
List<Todo> findEventAtStartingWith(String eventMonth);
}
package inho.service.impl;
import inho.domain.Todo;
import inho.exception.MaxTodoLimitExceededException;
import inho.exception.TodoNotFoundException;
import inho.repository.TodoRepository;
import inho.service.TodoService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
@Transactional
public class TodoServiceImpl implements TodoService {
private final TodoRepository todoRepository;
private static final int DAILY_MAX_TODO_COUNT = 8;
/**
* ์๋ก์ด TODO๋ฅผ ์ ์ฅ
*/
public Todo saveTodo(String subject, String eventAt) {
long count = todoRepository.countByEventAt(eventAt);
if (count >= DAILY_MAX_TODO_COUNT) {
throw new MaxTodoLimitExceededException("ํ๋ฃจ ์ต๋ 8๊ฐ์ TODO๋ง ๋ฑ๋ก ๊ฐ๋ฅํฉ๋๋ค.");
}
Todo todo = new Todo(subject, eventAt);
return todoRepository.save(todo);
}
/**
* ํน์ ๋ ์ง์ ๋ชจ๋ TODO ์ญ์
*/
public void deleteTodosByDate(String eventAt) {
List<Todo> todos = todoRepository.findByEventAt(eventAt);
if (todos.isEmpty()) {
throw new TodoNotFoundException("ํด๋น ๋ ์ง(" + eventAt + ")์ ๋ฑ๋ก๋ TODO๊ฐ ์์ต๋๋ค.");
}
todoRepository.deleteAll(todos);
}
/**
* ํน์ ID์ TODO ์ญ์
*/
public void deleteTodoById(Long id) {
if (!todoRepository.existsById(id)) {
throw new TodoNotFoundException("ID " + id + "์ ํด๋นํ๋ TODO๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค.");
}
todoRepository.deleteById(id);
}
/**
* ํน์ ๋ ์ง์ TODO ๋ฆฌ์คํธ ์กฐํ
*/
public List<Todo> getTodosByDate(String eventAt) {
return findTodos(eventAt, false);
}
/**
* ํน์ ์์ TODO ๋ฆฌ์คํธ ์กฐํ
*/
@Transactional(readOnly = true)
public List<Todo> getTodosByMonth(String eventMonth) {
return findTodos(eventMonth, true);
}
/**
* ํน์ ๋ ์ง์ TODO ๊ฐ์ ๋ฐํ
*/
public long countTodosByDate(String date) {
return todoRepository.countByEventAt(date);
}
/**
* ํน์ ID์ TODO ์กฐํ
*/
public Todo getTodoById(Long id) {
return todoRepository.findById(id)
.orElseThrow(() -> new TodoNotFoundException("ID " + id + "์ ํด๋นํ๋ TODO๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค."));
}
/**
* ํน์ ๋ ์ง ๋๋ ์์ TODO ๋ฆฌ์คํธ ์กฐํ (๊ณตํต ๋ก์ง)
*/
private List<Todo> findTodos(String queryParam, boolean isMonthly){
return isMonthly ? todoRepository.findEventAtStartingWith(queryParam) : todoRepository.findByEventAt(queryParam);
}
}
package inho.service;
import inho.domain.Todo;
import java.util.List;
public interface TodoService {
Todo saveTodo(String subject, String eventAt);
void deleteTodosByDate(String eventAt);
void deleteTodoById(Long id);
List<Todo> getTodosByDate(String eventAt);
List<Todo> getTodosByMonth(String eventMonth);
long countTodosByDate(String date);
Todo getTodoById(Long id);
}
๐น ํต์ฌ ์ ๋ฆฌ
์ปจํธ๋กค๋ฌ ๊ณ์ธต์ ํด๋ผ์ด์ธํธ๋ก๋ถํฐ HTTP ์์ฒญ์ ๋ฐ์ ์๋น์ค ๊ณ์ธต์ ํธ์ถํ๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ์ ์ ํ HTTP ์๋ต์ผ๋ก ๋ณํํ๋ ์ญํ ์ ํ๋ค.
์ฃผ์ ๊ธฐ๋ฅ
package inho.controller;
import inho.domain.Todo;
import inho.service.TodoService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/calendar")
public class TodoController {
private final TodoService todoService;
/**
* ์๋ก์ด TODO ์์ฑ
*/
@PostMapping("/events")
public ResponseEntity<Todo> createTodo(@RequestBody Todo todo) {
Todo savedTodo = todoService.saveTodo(todo.getSubject(), todo.getEventAt());
return ResponseEntity.status(201).body(savedTodo);
}
/**
* ํน์ ID์ TODO ์ญ์
*/
@DeleteMapping("/events/{id}")
public ResponseEntity<Void> deleteTodoById(@PathVariable Long id) {
todoService.deleteTodoById(id);
return ResponseEntity.noContent().build();
}
/**
* ํน์ ๋ ์ง์ ๋ชจ๋ TODO ์ญ์
*/
@DeleteMapping("/events/daily/{eventAt}")
public ResponseEntity<Void> deleteTodosByDate(@PathVariable String eventAt) {
todoService.deleteTodosByDate(eventAt);
return ResponseEntity.noContent().build();
}
/**
* ํน์ ๋ ์ง์ TODO ๋ฆฌ์คํธ ์กฐํ
*/
@GetMapping(value = "/events/", params = {"year", "month", "day"})
public ResponseEntity<List<Todo>> getTodosByDate(
@RequestParam int year,
@RequestParam int month,
@RequestParam int day
){
String eventAt = String.format("%04d-%02d-%02d", year, month, day);
List<Todo> todos = todoService.getTodosByDate(eventAt);
return ResponseEntity.ok(todos);
}
/**
* ํน์ ์์ TODO ๋ฆฌ์คํธ ์กฐํ
*/
@GetMapping(value = "/events/", params = {"year","month"})
public ResponseEntity<List<Todo>> getTodosByMonth(
@RequestParam int year,
@RequestParam int month
){
String eventMonth = String.format("%04d-%02d", year, month);
List<Todo> todos = todoService.getTodosByMonth(eventMonth);
return ResponseEntity.ok(todos);
}
}
ํด๋ผ์ด์ธํธ๊ฐ POST /api/calendar/events ์์ฒญ์ ๋ณด๋
TodoController์์ @RequestBody๋ก ์์ฒญ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์
TodoService์์ ๋น์ฆ๋์ค ๋ก์ง ์ํ (์ต๋ 8๊ฐ ์ ํ ์ฒดํฌ ๋ฑ)
TodoRepository๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ
์ ์ฅ๋ ๋ฐ์ดํฐ๋ฅผ ํด๋ผ์ด์ธํธ์ ๋ฐํ (201 Created ์๋ต)
ํด๋ผ์ด์ธํธ๊ฐ GET /api/calendar/events/?year=2024&month=04&day=01 ์์ฒญ
TodoController์์ @RequestParam์ ํ์ฉํด ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ์
TodoService์์ findTodos(eventAt, false) ํธ์ถ
TodoRepository์์ ํด๋น ๋ ์ง์ ๋ฐ์ดํฐ๋ฅผ ์กฐํ ํ ๋ฐํ
ํด๋ผ์ด์ธํธ์ 200 OK ์๋ต๊ณผ ํจ๊ป ๋ฆฌ์คํธ ๋ฐํ
ํด๋ผ์ด์ธํธ๊ฐ GET /api/calendar/events/?year=2024&month=04 ์์ฒญ
TodoService์์ findTodos(eventMonth, true) ํธ์ถ
TodoRepository์์ ํด๋น ์์ ๋ฐ์ดํฐ๋ฅผ ์กฐํ ํ ๋ฐํ
ํด๋ผ์ด์ธํธ์ 200 OK ์๋ต๊ณผ ํจ๊ป ๋ฆฌ์คํธ ๋ฐํ
ํด๋ผ์ด์ธํธ๊ฐ DELETE /api/calendar/events/{id} ์์ฒญ
TodoController์์ @PathVariable Long id๋ก ID ๋ฐ์
TodoService์์ deleteTodoById(id) ์คํ
๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ํด๋น ID ์ญ์ ํ 204 No Content ๋ฐํ
ํด๋ผ์ด์ธํธ๊ฐ DELETE /api/calendar/events/daily/{eventAt} ์์ฒญ
TodoService์์ deleteTodosByDate(eventAt) ์คํ
๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ํด๋น ๋ ์ง์ TODO ์ญ์ ํ 204 No Content ๋ฐํ
MaxTodoLimitExceededException: ํ๋ฃจ ์ต๋ 8๊ฐ ์ ํ ์ด๊ณผ ์ ๋ฐ์
TodoNotFoundException: ์ญ์ ๋๋ ์กฐํ ์ ํด๋น ๋ฐ์ดํฐ๊ฐ ์์ ๊ฒฝ์ฐ ๋ฐ์
์์ธ ๋ฐ์ ์ @ControllerAdvice๋ฅผ ํตํด ๊ธ๋ก๋ฒ ์์ธ ์ฒ๋ฆฌ
์ ์ ํ HTTP ์ํ ์ฝ๋์ ๋ฉ์์ง๋ฅผ ํฌํจํ JSON ์๋ต ๋ฐํ
findByEventAt(String eventAt): ํน์ ๋ ์ง์ TODO ์กฐํ
findEventAtStartingWith(String eventMonth): ํน์ ์์ TODO ์กฐํ
countByEventAt(String eventAt): ํน์ ๋ ์ง์ TODO ๊ฐ์ ํ์ธ
existsById(Long id): ํน์ ID ์กด์ฌ ์ฌ๋ถ ํ์ธ
deleteById(Long id): ํน์ ID์ TODO ์ญ์
ํด๋ผ์ด์ธํธ โ ์ปจํธ๋กค๋ฌ โ ์๋น์ค โ ๋ฆฌํฌ์งํ ๋ฆฌ โ ๋ฐ์ดํฐ๋ฒ ์ด์ค
โ ์๋ต ๋ฐํ โ
Spring Boot์ ํต์ฌ ๊ณ์ธต์ ํ์ฉํ์ฌ ๋ชจ๋ํ๋ ๊ตฌ์กฐ๋ก ๊ฐ๋ฐ๋์์ผ๋ฉฐ, ํด๋ฆฐ ์ํคํ ์ฒ๋ฅผ ์ ์งํ๋ฉด์ ์ ์ง๋ณด์์ฑ์ด ๋์ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๋ค!





