Skip to content

Commit ced3d9f

Browse files
authored
feat/#34-데드라인일정관리
feat/#34 : 데드라인일정관리
2 parents 436f249 + 0ae96e3 commit ced3d9f

19 files changed

+997
-20
lines changed

src/main/java/com/rightmark/domain/calendar/dto/CalendarDtos.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ public static class CreateResponse {
8282
@AllArgsConstructor
8383
@NoArgsConstructor
8484
public static class UpdateRequest {
85+
// @JsonAlias("dueAt") // camelCase도 허용
8586
private String dueAt; // optional
8687
private String title; // optional
8788
private String memo; // optional
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.rightmark.domain.deadline.api;
2+
3+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
4+
import org.springframework.web.bind.annotation.DeleteMapping;
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.PatchMapping;
7+
import org.springframework.web.bind.annotation.PathVariable;
8+
import org.springframework.web.bind.annotation.PostMapping;
9+
import org.springframework.web.bind.annotation.RequestBody;
10+
import org.springframework.web.bind.annotation.RequestMapping;
11+
import org.springframework.web.bind.annotation.RestController;
12+
13+
import com.rightmark.domain.deadline.application.DeadlineService;
14+
import com.rightmark.domain.deadline.dto.DeadlineDtos;
15+
import com.rightmark.global.common.code.status.SuccessStatus;
16+
import com.rightmark.global.common.response.BaseResponse;
17+
18+
import jakarta.validation.Valid;
19+
import lombok.RequiredArgsConstructor;
20+
21+
@RestController
22+
@RequestMapping("/api")
23+
@RequiredArgsConstructor
24+
public class ChecklistItemRestController {
25+
26+
private final DeadlineService service;
27+
28+
// 12) 아이템 조회
29+
@GetMapping("/checklists/{checklistId}/items")
30+
public BaseResponse<DeadlineDtos.ItemsResponse> listItems(
31+
@AuthenticationPrincipal Long userId,
32+
@PathVariable Long checklistId) {
33+
var resp = service.listItems(userId, checklistId);
34+
return BaseResponse.onSuccess(SuccessStatus.CHECKLIST_ITEM_LIST_SUCCESS, resp);
35+
}
36+
37+
// 13) 아이템 추가
38+
@PostMapping("/checklists/{checklistId}/items")
39+
public BaseResponse<DeadlineDtos.ItemCreateResponse> addItem(
40+
@AuthenticationPrincipal Long userId,
41+
@PathVariable Long checklistId,
42+
@RequestBody @Valid DeadlineDtos.ItemCreateRequest req) {
43+
var resp = service.addItem(userId, checklistId, req);
44+
return BaseResponse.onSuccess(SuccessStatus.CHECKLIST_ITEM_CREATE_SUCCESS, resp);
45+
}
46+
47+
// 14) 아이템 삭제
48+
@DeleteMapping("/items/{itemId}")
49+
public BaseResponse<Void> deleteItem(
50+
@AuthenticationPrincipal Long userId,
51+
@PathVariable Long itemId) {
52+
service.deleteItem(userId, itemId);
53+
return BaseResponse.onSuccess(SuccessStatus.CHECKLIST_ITEM_DELETE_SUCCESS, null);
54+
}
55+
56+
// 15) 아이템 완료/해제
57+
@PatchMapping("/items/{itemId}/complete")
58+
public BaseResponse<DeadlineDtos.ItemToggleResponse> toggleItem(
59+
@AuthenticationPrincipal Long userId,
60+
@PathVariable Long itemId,
61+
@RequestBody DeadlineDtos.ItemToggleRequest req) {
62+
var resp = service.toggleItem(userId, itemId, req);
63+
return BaseResponse.onSuccess(SuccessStatus.CHECKLIST_ITEM_TOGGLE_SUCCESS, resp);
64+
}
65+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.rightmark.domain.deadline.api;
2+
3+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
4+
import org.springframework.web.bind.annotation.DeleteMapping;
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.PathVariable;
7+
import org.springframework.web.bind.annotation.PostMapping;
8+
import org.springframework.web.bind.annotation.RequestBody;
9+
import org.springframework.web.bind.annotation.RequestMapping;
10+
import org.springframework.web.bind.annotation.RequestParam;
11+
import org.springframework.web.bind.annotation.RestController;
12+
13+
import com.rightmark.domain.deadline.application.DeadlineService;
14+
import com.rightmark.domain.deadline.dto.DeadlineDtos;
15+
import com.rightmark.global.common.code.status.SuccessStatus;
16+
import com.rightmark.global.common.response.BaseResponse;
17+
18+
import jakarta.validation.Valid;
19+
import lombok.RequiredArgsConstructor;
20+
21+
@RestController
22+
@RequestMapping("/api")
23+
@RequiredArgsConstructor
24+
public class DeadlineRestController {
25+
26+
private final DeadlineService service;
27+
28+
/** 6) 데드라인 일정 조회 */
29+
@GetMapping("/deadlines")
30+
public BaseResponse<DeadlineDtos.ListResponse> deadlines(
31+
@AuthenticationPrincipal Long userId,
32+
@RequestParam(value = "eventId", required = false) Long eventId, // 옵션 필터
33+
@RequestParam(value = "page", required = false) Integer page,
34+
@RequestParam(value = "size", required = false) Integer size) {
35+
var resp = service.deadlines(userId, eventId);
36+
return BaseResponse.onSuccess(SuccessStatus.DEADLINE_LIST_SUCCESS, resp);
37+
}
38+
39+
/** 7) 데드라인 일정 추가 */
40+
@PostMapping("/deadlines")
41+
public BaseResponse<DeadlineDtos.CreateResponse> addDeadline(
42+
@AuthenticationPrincipal Long userId,
43+
@RequestBody @Valid DeadlineDtos.CreateRequest req) {
44+
var resp = service.addDeadline(userId, req);
45+
return BaseResponse.onSuccess(SuccessStatus.DEADLINE_CREATE_SUCCESS, resp);
46+
}
47+
48+
/** 8) 데드라인 일정 삭제 */
49+
@DeleteMapping("/deadlines/{deadlineId}")
50+
public BaseResponse<Void> deleteDeadline(
51+
@AuthenticationPrincipal Long userId,
52+
@PathVariable Long deadlineId) {
53+
service.deleteDeadline(userId, deadlineId);
54+
return BaseResponse.onSuccess(SuccessStatus.DEADLINE_DELETE_SUCCESS, null);
55+
}
56+
57+
/** 9) 체크리스트 조회 */
58+
@GetMapping("/deadlines/{deadlineId}/checklists")
59+
public BaseResponse<DeadlineDtos.ChecklistListResponse> checklists(
60+
@AuthenticationPrincipal Long userId,
61+
@PathVariable Long deadlineId) {
62+
var resp = service.checklists(userId, deadlineId);
63+
return BaseResponse.onSuccess(SuccessStatus.CHECKLIST_LIST_SUCCESS, resp);
64+
}
65+
66+
/** 10) 체크리스트 추가 */
67+
@PostMapping("/deadlines/{deadlineId}/checklists")
68+
public BaseResponse<DeadlineDtos.ChecklistCreateResponse> addChecklist(
69+
@AuthenticationPrincipal Long userId,
70+
@PathVariable Long deadlineId,
71+
@RequestBody @Valid DeadlineDtos.ChecklistCreateRequest req) {
72+
var resp = service.addChecklist(userId, deadlineId, req);
73+
return BaseResponse.onSuccess(SuccessStatus.CHECKLIST_CREATE_SUCCESS, resp);
74+
}
75+
76+
/** 11) 체크리스트 삭제 */
77+
@DeleteMapping("/deadlines/{deadlineId}/checklists/{checklistId}")
78+
public BaseResponse<Void> deleteChecklist(
79+
@AuthenticationPrincipal Long userId,
80+
@PathVariable Long deadlineId,
81+
@PathVariable Long checklistId) {
82+
service.deleteChecklist(userId, deadlineId, checklistId);
83+
return BaseResponse.onSuccess(SuccessStatus.CHECKLIST_DELETE_SUCCESS, null);
84+
}
85+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.rightmark.domain.deadline.application;
2+
3+
import com.rightmark.domain.deadline.dto.DeadlineDtos;
4+
5+
public interface DeadlineService {
6+
7+
// 6. 데드라인 일정 조회
8+
DeadlineDtos.ListResponse deadlines(Long userId, Long eventId);
9+
10+
// 7. 데드라인 일정 추가
11+
DeadlineDtos.CreateResponse addDeadline(Long userId, DeadlineDtos.CreateRequest req);
12+
13+
// 8. 데드라인 일정 삭제
14+
void deleteDeadline(Long userId, Long deadlineId);
15+
16+
// 9. 체크리스트 조회
17+
DeadlineDtos.ChecklistListResponse checklists(Long userId, Long deadlineId);
18+
19+
// 10. 체크리스트 추가
20+
DeadlineDtos.ChecklistCreateResponse addChecklist(Long userId, Long deadlineId,
21+
DeadlineDtos.ChecklistCreateRequest req);
22+
23+
// 11. 체크리스트 삭제
24+
void deleteChecklist(Long userId, Long deadlineId, Long checklistId);
25+
26+
// 12. 체크리스트 항목 조회
27+
DeadlineDtos.ItemsResponse listItems(Long userId, Long checklistId);
28+
29+
// 13. 체크리스트 항목 추가
30+
DeadlineDtos.ItemCreateResponse addItem(Long userId, Long checklistId, DeadlineDtos.ItemCreateRequest req);
31+
32+
// 14. 체크리스트 항목 삭제
33+
void deleteItem(Long userId, Long itemId);
34+
35+
// 15. 체크리스트 항목 토글
36+
DeadlineDtos.ItemToggleResponse toggleItem(Long userId, Long itemId, DeadlineDtos.ItemToggleRequest req);
37+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package com.rightmark.domain.deadline.application;
2+
3+
import java.time.OffsetDateTime;
4+
import java.time.format.DateTimeFormatter;
5+
import java.util.List;
6+
7+
import org.springframework.stereotype.Service;
8+
import org.springframework.transaction.annotation.Transactional;
9+
10+
import com.rightmark.domain.calendar.domain.entity.CalendarEvent;
11+
import com.rightmark.domain.calendar.domain.repository.CalendarEventRepository;
12+
import com.rightmark.domain.calendar.exception.CalendarEventNotFoundException;
13+
import com.rightmark.domain.deadline.converter.DeadlineConverter;
14+
import com.rightmark.domain.deadline.domain.entity.Checklist;
15+
import com.rightmark.domain.deadline.domain.entity.ChecklistItem;
16+
import com.rightmark.domain.deadline.domain.entity.Deadline;
17+
import com.rightmark.domain.deadline.domain.repository.ChecklistItemRepository;
18+
import com.rightmark.domain.deadline.domain.repository.ChecklistRepository;
19+
import com.rightmark.domain.deadline.domain.repository.DeadlineRepository;
20+
import com.rightmark.domain.deadline.dto.DeadlineDtos;
21+
import com.rightmark.domain.deadline.exception.ChecklistItemNotFoundException;
22+
import com.rightmark.domain.deadline.exception.DeadlineNotFoundException;
23+
import com.rightmark.domain.user.domain.entity.User;
24+
import com.rightmark.domain.user.domain.repository.UserRepository;
25+
import com.rightmark.domain.user.exception.UserNotFoundException;
26+
27+
import lombok.RequiredArgsConstructor;
28+
29+
@Service
30+
@RequiredArgsConstructor
31+
@Transactional
32+
public class DeadlineServiceImpl implements DeadlineService {
33+
34+
private final CalendarEventRepository eventRepo;
35+
private final DeadlineRepository deadlineRepo;
36+
private final ChecklistRepository checklistRepo;
37+
private final ChecklistItemRepository itemRepo;
38+
private final UserRepository userRepo;
39+
40+
private static final DateTimeFormatter ISO = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
41+
42+
private User mustUser(Long userId) {
43+
return userRepo.findById(userId).orElseThrow(UserNotFoundException::new);
44+
}
45+
46+
private OffsetDateTime parseIso(String s) {
47+
return OffsetDateTime.parse(s, ISO);
48+
}
49+
50+
/** 6) 데드라인 조회: eventId가 있으면 필터, 없으면 전체. page 메타는 응답에 포함하지 않음 */
51+
@Override
52+
@Transactional(readOnly = true)
53+
public DeadlineDtos.ListResponse deadlines(Long userId, Long eventId) {
54+
List<Deadline> ds;
55+
if (eventId == null) {
56+
ds = deadlineRepo.findAllByUserId(userId);
57+
} else {
58+
ds = deadlineRepo.findAllByUserIdAndEventId(userId, eventId);
59+
}
60+
// fetch join이라 event 로딩됨 → 그대로 변환
61+
var lines = ds.stream()
62+
.map(d -> {
63+
int checklistCount = checklistRepo
64+
.findByDeadline_IdAndDeadline_Owner_IdOrderByIdAsc(d.getId(), userId).size();
65+
return DeadlineConverter.toView(d, checklistCount);
66+
})
67+
.toList();
68+
return DeadlineDtos.ListResponse.builder().deadlines(lines)
69+
.page(0).size(lines.size()).totalElements(lines.size()).totalPages(1).last(true)
70+
.build();
71+
}
72+
73+
/** 7) 데드라인 추가: eventId, note만 받아 progress=0으로 생성 */
74+
@Override
75+
public DeadlineDtos.CreateResponse addDeadline(Long userId, DeadlineDtos.CreateRequest req) {
76+
CalendarEvent event = eventRepo.findByIdAndOwner_Id(req.getEventId(), userId)
77+
.orElseThrow(CalendarEventNotFoundException::new);
78+
79+
Deadline saved = deadlineRepo.save(Deadline.create(event, req.getNote())); // progress=0 preset
80+
return DeadlineDtos.CreateResponse.builder()
81+
.deadlineId(saved.getId())
82+
.eventId(event.getId())
83+
.progress(saved.getProgress())
84+
.note(saved.getNote())
85+
.build();
86+
}
87+
88+
/** 8) 데드라인 삭제 */
89+
@Override
90+
public void deleteDeadline(Long userId, Long deadlineId) {
91+
var d = deadlineRepo.findByIdAndEvent_Owner_Id(deadlineId, userId).orElseThrow(DeadlineNotFoundException::new);
92+
deadlineRepo.delete(d); // FK ON DELETE CASCADE (선택) or orphanRemoval 설정은 엔티티 설계에 맞춰 정책적으로
93+
}
94+
95+
/** 9) 체크리스트 조회 */
96+
@Transactional(readOnly = true)
97+
@Override
98+
public DeadlineDtos.ChecklistListResponse checklists(Long userId, Long deadlineId) {
99+
var d = deadlineRepo.findByIdAndEvent_Owner_Id(deadlineId, userId).orElseThrow(DeadlineNotFoundException::new);
100+
var lists = checklistRepo.findByDeadline_IdAndDeadline_Owner_IdOrderByIdAsc(d.getId(), userId)
101+
.stream().map(DeadlineConverter::toChecklistView).toList();
102+
return DeadlineConverter.toChecklistListResponse(deadlineId, lists);
103+
}
104+
105+
/** 10) 체크리스트 추가 */
106+
@Override
107+
public DeadlineDtos.ChecklistCreateResponse addChecklist(Long userId, Long deadlineId,
108+
DeadlineDtos.ChecklistCreateRequest req) {
109+
var d = deadlineRepo.findByIdAndEvent_Owner_Id(deadlineId, userId).orElseThrow(DeadlineNotFoundException::new);
110+
var saved = checklistRepo.save(Checklist.create(d, req.getTitle()));
111+
return DeadlineConverter.toChecklistCreateResponse(saved);
112+
}
113+
114+
/** 11) 체크리스트 삭제 */
115+
@Override
116+
public void deleteChecklist(Long userId, Long deadlineId, Long checklistId) {
117+
var list = checklistRepo.findByIdAndDeadline_Owner_Id(checklistId, userId)
118+
.orElseThrow(ChecklistItemNotFoundException::new);
119+
checklistRepo.delete(list);
120+
}
121+
122+
// ===== 진행률 재계산 유틸 =====
123+
private int recalcChecklistProgress(Long checklistId) {
124+
long total = itemRepo.countByChecklist_Id(checklistId);
125+
if (total == 0)
126+
return 0;
127+
long done = itemRepo.countByChecklist_IdAndDoneTrue(checklistId);
128+
return (int) Math.floor((done * 100.0) / total);
129+
}
130+
131+
private int recalcDeadlineProgress(Long deadlineId, Long userId) {
132+
// 모든 체크리스트의 진행률 평균
133+
var lists = checklistRepo.findByDeadline_IdAndDeadline_Owner_IdOrderByIdAsc(deadlineId, userId);
134+
if (lists.isEmpty())
135+
return 0;
136+
int sum = 0;
137+
for (var cl : lists)
138+
sum += recalcChecklistProgress(cl.getId());
139+
return (int) Math.floor(sum / (double) lists.size());
140+
}
141+
142+
private int recalcAndSaveDeadlineProgress(Long deadlineId, Long userId) {
143+
var d = deadlineRepo.findByIdAndEvent_Owner_Id(deadlineId, userId).orElseThrow(DeadlineNotFoundException::new);
144+
int p = recalcDeadlineProgress(deadlineId, userId);
145+
d.setProgress(p);
146+
return p;
147+
}
148+
149+
// ===== 12) 아이템 조회 =====
150+
@Transactional(readOnly = true)
151+
public DeadlineDtos.ItemsResponse listItems(Long userId, Long checklistId) {
152+
var checklist = checklistRepo.findByIdAndDeadline_Owner_Id(checklistId, userId)
153+
.orElseThrow(DeadlineNotFoundException::new); // 혹은 ChecklistNotFoundException
154+
155+
// 조회 시 재계산 규칙 → 데드라인 진행률 갱신
156+
int deadlineProgress = recalcAndSaveDeadlineProgress(checklist.getDeadline().getId(), userId);
157+
158+
var items = itemRepo.findByChecklist_IdOrderByIdAsc(checklistId).stream()
159+
.map(DeadlineConverter::toItemView).toList();
160+
161+
return DeadlineConverter.toItemsResponse(checklistId, checklist.getDeadline().getId(), deadlineProgress, items);
162+
}
163+
164+
// ===== 13) 아이템 추가 =====
165+
public DeadlineDtos.ItemCreateResponse addItem(Long userId, Long checklistId, DeadlineDtos.ItemCreateRequest req) {
166+
var checklist = checklistRepo.findByIdAndDeadline_Owner_Id(checklistId, userId)
167+
.orElseThrow(DeadlineNotFoundException::new);
168+
169+
var saved = itemRepo.save(ChecklistItem.create(checklist, req.getContent()));
170+
171+
int deadlineProgress = recalcAndSaveDeadlineProgress(checklist.getDeadline().getId(), userId);
172+
return DeadlineConverter.toItemCreateResponse(saved, deadlineProgress);
173+
}
174+
175+
// ===== 14) 아이템 삭제 =====
176+
public void deleteItem(Long userId, Long itemId) {
177+
var item = itemRepo.findById(itemId).orElseThrow(ChecklistItemNotFoundException::new);
178+
var deadline = item.getChecklist().getDeadline();
179+
if (!deadline.getOwner().getId().equals(userId))
180+
throw new ChecklistItemNotFoundException();
181+
182+
itemRepo.delete(item);
183+
recalcAndSaveDeadlineProgress(deadline.getId(), userId);
184+
}
185+
186+
// ===== 15) 아이템 완료/해제 =====
187+
public DeadlineDtos.ItemToggleResponse toggleItem(Long userId, Long itemId, DeadlineDtos.ItemToggleRequest req) {
188+
var item = itemRepo.findById(itemId).orElseThrow(ChecklistItemNotFoundException::new);
189+
var deadline = item.getChecklist().getDeadline();
190+
if (!deadline.getOwner().getId().equals(userId))
191+
throw new ChecklistItemNotFoundException();
192+
193+
item.setDone(req.isDone());
194+
int deadlineProgress = recalcAndSaveDeadlineProgress(deadline.getId(), userId);
195+
return DeadlineConverter.toItemToggleResponse(item, deadlineProgress);
196+
}
197+
}

0 commit comments

Comments
 (0)