diff --git a/.gitignore b/.gitignore index 770696b..04c645e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ HELP.md **/.env.* *.env .env +.DS_Store .env.properties !.env.example **/.env.example diff --git a/src/main/java/inha/gdgoc/domain/core/attendance/controller/CoreAttendanceController.java b/src/main/java/inha/gdgoc/domain/core/attendance/controller/CoreAttendanceController.java new file mode 100644 index 0000000..6eb3139 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/core/attendance/controller/CoreAttendanceController.java @@ -0,0 +1,142 @@ +package inha.gdgoc.domain.core.attendance.controller; + +import inha.gdgoc.domain.core.attendance.controller.message.CoreAttendanceMessage; +import inha.gdgoc.domain.core.attendance.dto.request.CreateDateRequest; +import inha.gdgoc.domain.core.attendance.dto.response.DateListResponse; +import inha.gdgoc.domain.core.attendance.dto.response.DaySummaryResponse; +import inha.gdgoc.domain.core.attendance.dto.response.TeamResponse; +import inha.gdgoc.domain.core.attendance.service.CoreAttendanceService; +import inha.gdgoc.global.dto.response.ApiResponse; +import inha.gdgoc.global.dto.response.PageMeta; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * TODO: 임시 운용판(내일까지) + * - 권한 제거 + * - 팀 구분은 query parameter로 처리 (?leadName=김정민 또는 ?teamId=team_xxx) + * - present 값은 query로 받음 (true/false), 바디 없이 호출 가능 + * - 추후 리팩토링 시 Security/JWT/Service 분리 강화 예정 + */ +@RestController +@RequestMapping("/api/v1/core-attendance") +@RequiredArgsConstructor +public class CoreAttendanceController { + + private final CoreAttendanceService service; + + /* Dates */ + @GetMapping("/dates") + public ResponseEntity> getDates() { + return ResponseEntity.ok( + ApiResponse.ok(CoreAttendanceMessage.DATE_LIST_RETRIEVED_SUCCESS, + new DateListResponse(service.getDates())) + ); + } + + @PostMapping("/dates") + public ResponseEntity> createDate( + @Valid @RequestBody CreateDateRequest request + ) { + service.addDate(request.getDate()); + return ResponseEntity.ok( + ApiResponse.ok(CoreAttendanceMessage.DATE_CREATED_SUCCESS, + new DateListResponse(service.getDates())) + ); + } + + @DeleteMapping("/dates/{date}") + public ResponseEntity> deleteDate(@PathVariable String date) { + service.deleteDate(date); + return ResponseEntity.ok( + ApiResponse.ok(CoreAttendanceMessage.DATE_DELETED_SUCCESS, + new DateListResponse(service.getDates())) + ); + } + + /* Teams – leadName/teamId 로 필터 */ + @GetMapping("/teams") + public ResponseEntity, PageMeta>> getTeams( + @RequestParam(required = false) String leadName, + @RequestParam(required = false) String teamId + ) { + List list = service.getTeams(leadName, teamId); + + // 임시 Page 객체 생성 (page=0, size=list.size()) + var page = new PageImpl<>(list, PageRequest.of(0, Math.max(1, list.size()), + Sort.by(Sort.Direction.DESC, "createdAt")), list.size()); + + return ResponseEntity.ok( + ApiResponse.ok(CoreAttendanceMessage.TEAM_LIST_RETRIEVED_SUCCESS, list, PageMeta.of(page)) + ); + } + + /* Members – 간단한 쿼리 파라미터 방식 */ + @PostMapping("/members") + public ResponseEntity> addMember( + @RequestParam String teamId, + @RequestParam String name + ) { + service.addMember(teamId, name); + return ResponseEntity.ok(ApiResponse.ok(CoreAttendanceMessage.MEMBER_ADDED_SUCCESS, "OK")); + } + + @PutMapping("/members") + public ResponseEntity> renameMember( + @RequestParam String teamId, + @RequestParam String memberId, + @RequestParam String name + ) { + service.renameMember(teamId, memberId, name); + return ResponseEntity.ok(ApiResponse.ok(CoreAttendanceMessage.MEMBER_UPDATED_SUCCESS, "OK")); + } + + @DeleteMapping("/members") + public ResponseEntity> deleteMember( + @RequestParam String teamId, + @RequestParam String memberId + ) { + service.removeMember(teamId, memberId); + return ResponseEntity.ok(ApiResponse.ok(CoreAttendanceMessage.MEMBER_DELETED_SUCCESS, "OK")); + } + + /* Attendance – present 를 쿼리로, 바디 불필요 */ + @PutMapping("/records/one") + public ResponseEntity> setAttendance( + @RequestParam String date, // YYYY-MM-DD + @RequestParam String teamId, + @RequestParam String memberId, + @RequestParam boolean present + ) { + service.setAttendance(date, teamId, memberId, present); + return ResponseEntity.ok(ApiResponse.ok(CoreAttendanceMessage.ATTENDANCE_SET_SUCCESS, "OK")); + } + + @PutMapping("/records/all") + public ResponseEntity> setAll( + @RequestParam String date, // YYYY-MM-DD + @RequestParam String teamId, + @RequestParam boolean present + ) { + long count = service.setAll(date, teamId, present); + return ResponseEntity.ok(ApiResponse.ok(CoreAttendanceMessage.ATTENDANCE_ALL_SET_SUCCESS, count)); + } + + /* Summary – leadName/teamId 필터 가능 */ + @GetMapping("/summary") + public ResponseEntity> summary( + @RequestParam String date, // YYYY-MM-DD + @RequestParam(required = false) String leadName, + @RequestParam(required = false) String teamId + ) { + DaySummaryResponse body = service.summary(date, leadName, teamId); + return ResponseEntity.ok(ApiResponse.ok(CoreAttendanceMessage.SUMMARY_RETRIEVED_SUCCESS, body)); + } +} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/domain/core/attendance/controller/message/CoreAttendanceMessage.java b/src/main/java/inha/gdgoc/domain/core/attendance/controller/message/CoreAttendanceMessage.java new file mode 100644 index 0000000..4f06cb5 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/core/attendance/controller/message/CoreAttendanceMessage.java @@ -0,0 +1,22 @@ +package inha.gdgoc.domain.core.attendance.controller.message; + +public class CoreAttendanceMessage { + public static final String DATE_LIST_RETRIEVED_SUCCESS = "성공적으로 출석 날짜 목록을 조회했습니다."; + public static final String DATE_CREATED_SUCCESS = "성공적으로 출석 날짜를 생성했습니다."; + public static final String DATE_DELETED_SUCCESS = "성공적으로 출석 날짜를 삭제했습니다."; + + public static final String TEAM_LIST_RETRIEVED_SUCCESS = "성공적으로 팀 목록을 조회했습니다."; + public static final String TEAM_CREATED_SUCCESS = "성공적으로 팀을 생성했습니다."; + public static final String TEAM_UPDATED_SUCCESS = "성공적으로 팀 정보를 수정했습니다."; + public static final String TEAM_DELETED_SUCCESS = "성공적으로 팀을 삭제했습니다."; + + public static final String MEMBER_ADDED_SUCCESS = "성공적으로 팀원을 추가했습니다."; + public static final String MEMBER_UPDATED_SUCCESS = "성공적으로 팀원 정보를 수정했습니다."; + public static final String MEMBER_DELETED_SUCCESS = "성공적으로 팀원을 삭제했습니다."; + + public static final String ATTENDANCE_SET_SUCCESS = "성공적으로 출석을 체크했습니다."; + public static final String ATTENDANCE_ALL_SET_SUCCESS = "성공적으로 전체 출석 상태를 변경했습니다."; + + public static final String SUMMARY_RETRIEVED_SUCCESS = "성공적으로 출석 요약을 조회했습니다."; + public static final String CSV_EXPORTED_SUCCESS = "성공적으로 CSV 파일을 내보냈습니다."; +} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/domain/core/attendance/dto/request/AddMemberRequest.java b/src/main/java/inha/gdgoc/domain/core/attendance/dto/request/AddMemberRequest.java new file mode 100644 index 0000000..f726bee --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/core/attendance/dto/request/AddMemberRequest.java @@ -0,0 +1,15 @@ +// AddMemberRequest.java +package inha.gdgoc.domain.core.attendance.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class AddMemberRequest { + @NotBlank + private String name; +} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/domain/core/attendance/dto/request/CreateDateRequest.java b/src/main/java/inha/gdgoc/domain/core/attendance/dto/request/CreateDateRequest.java new file mode 100644 index 0000000..d48529d --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/core/attendance/dto/request/CreateDateRequest.java @@ -0,0 +1,15 @@ +// CreateDateRequest.java +package inha.gdgoc.domain.core.attendance.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class CreateDateRequest { + @NotBlank(message = "날짜는 YYYY-MM-DD 형식이어야 합니다.") + private String date; +} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/domain/core/attendance/dto/request/CreateTeamRequest.java b/src/main/java/inha/gdgoc/domain/core/attendance/dto/request/CreateTeamRequest.java new file mode 100644 index 0000000..401685f --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/core/attendance/dto/request/CreateTeamRequest.java @@ -0,0 +1,16 @@ +// CreateTeamRequest.java +package inha.gdgoc.domain.core.attendance.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class CreateTeamRequest { + @NotBlank + private String name; + private String lead; +} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/domain/core/attendance/dto/request/SetAllRequest.java b/src/main/java/inha/gdgoc/domain/core/attendance/dto/request/SetAllRequest.java new file mode 100644 index 0000000..a81d5c2 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/core/attendance/dto/request/SetAllRequest.java @@ -0,0 +1,13 @@ +// SetAllRequest.java +package inha.gdgoc.domain.core.attendance.dto.request; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class SetAllRequest { + private boolean present; +} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/domain/core/attendance/dto/request/SetAttendanceRequest.java b/src/main/java/inha/gdgoc/domain/core/attendance/dto/request/SetAttendanceRequest.java new file mode 100644 index 0000000..d66db5b --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/core/attendance/dto/request/SetAttendanceRequest.java @@ -0,0 +1,13 @@ +// SetAttendanceRequest.java +package inha.gdgoc.domain.core.attendance.dto.request; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class SetAttendanceRequest { + private boolean present; +} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/domain/core/attendance/dto/response/DateListResponse.java b/src/main/java/inha/gdgoc/domain/core/attendance/dto/response/DateListResponse.java new file mode 100644 index 0000000..21520be --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/core/attendance/dto/response/DateListResponse.java @@ -0,0 +1,15 @@ +// DateListResponse.java +package inha.gdgoc.domain.core.attendance.dto.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class DateListResponse { + private List dates; +} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/domain/core/attendance/dto/response/DaySummaryResponse.java b/src/main/java/inha/gdgoc/domain/core/attendance/dto/response/DaySummaryResponse.java new file mode 100644 index 0000000..823d5c7 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/core/attendance/dto/response/DaySummaryResponse.java @@ -0,0 +1,28 @@ +// DaySummaryResponse.java +package inha.gdgoc.domain.core.attendance.dto.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class DaySummaryResponse { + private String date; + private List perTeam; + private long present; + private long total; + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class TeamSummary { + private String teamId; + private String teamName; + private long present; + private long total; + } +} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/domain/core/attendance/dto/response/MemberResponse.java b/src/main/java/inha/gdgoc/domain/core/attendance/dto/response/MemberResponse.java new file mode 100644 index 0000000..45400b4 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/core/attendance/dto/response/MemberResponse.java @@ -0,0 +1,14 @@ +// MemberResponse.java +package inha.gdgoc.domain.core.attendance.dto.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class MemberResponse { + private String id; + private String name; +} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/domain/core/attendance/dto/response/TeamResponse.java b/src/main/java/inha/gdgoc/domain/core/attendance/dto/response/TeamResponse.java new file mode 100644 index 0000000..e6cc578 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/core/attendance/dto/response/TeamResponse.java @@ -0,0 +1,18 @@ +// TeamResponse.java +package inha.gdgoc.domain.core.attendance.dto.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class TeamResponse { + private String id; + private String name; + private String lead; + private List members; +} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/domain/core/attendance/entity/Member.java b/src/main/java/inha/gdgoc/domain/core/attendance/entity/Member.java new file mode 100644 index 0000000..26801f2 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/core/attendance/entity/Member.java @@ -0,0 +1,14 @@ +// inha/gdgoc/domain/core/attendance/entity/Member.java +package inha.gdgoc.domain.core.attendance.entity; + +import lombok.Getter; +import lombok.Setter; + +@Getter +public class Member { + private final String id; + @Setter + private String name; + + public Member(String id, String name){ this.id = id; this.name = name; } +} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/domain/core/attendance/entity/Team.java b/src/main/java/inha/gdgoc/domain/core/attendance/entity/Team.java new file mode 100644 index 0000000..71fd1c1 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/core/attendance/entity/Team.java @@ -0,0 +1,23 @@ +// inha/gdgoc/domain/core/attendance/entity/Team.java +package inha.gdgoc.domain.core.attendance.entity; + +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +public class Team { + private String id; + @Setter + private String name; + private String lead; + private final List members = new ArrayList<>(); + + public Team(String id, String name, String lead){ + this.id = id; this.name = name; this.lead = lead == null ? "" : lead; + } + + public void setLead(String lead){ this.lead = lead == null ? "" : lead; } +} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/domain/core/attendance/repository/AttendanceRecordRepository.java b/src/main/java/inha/gdgoc/domain/core/attendance/repository/AttendanceRecordRepository.java new file mode 100644 index 0000000..9f14f47 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/core/attendance/repository/AttendanceRecordRepository.java @@ -0,0 +1,16 @@ +// inha/gdgoc/domain/core/attendance/repository/AttendanceRecordRepository.java +package inha.gdgoc.domain.core.attendance.repository; + +import org.springframework.stereotype.Repository; + +import java.util.Map; +@Repository +public interface AttendanceRecordRepository { + void setPresence(String dateKey, String teamId, String memberId, boolean present); + long setAll(String dateKey, String teamId, Iterable memberIds, boolean present); + + Map> getDay(String dateKey); // day -> teamId -> memberId -> present + void removeDate(String dateKey); + void removeMemberEverywhere(String teamId, String memberId); + void removeTeamEverywhere(String teamId); +} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/domain/core/attendance/repository/MemberRepository.java b/src/main/java/inha/gdgoc/domain/core/attendance/repository/MemberRepository.java new file mode 100644 index 0000000..c6a0322 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/core/attendance/repository/MemberRepository.java @@ -0,0 +1,15 @@ +// inha/gdgoc/domain/core/attendance/repository/MemberRepository.java +package inha.gdgoc.domain.core.attendance.repository; + +import inha.gdgoc.domain.core.attendance.entity.Member; +import inha.gdgoc.domain.core.attendance.entity.Team; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface MemberRepository { + Member add(Team team, Member member); + Optional find(Team team, String memberId); + void remove(Team team, String memberId); +} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/domain/core/attendance/repository/TeamRepository.java b/src/main/java/inha/gdgoc/domain/core/attendance/repository/TeamRepository.java new file mode 100644 index 0000000..6fb6ac9 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/core/attendance/repository/TeamRepository.java @@ -0,0 +1,16 @@ +// inha/gdgoc/domain/core/attendance/repository/TeamRepository.java +package inha.gdgoc.domain.core.attendance.repository; + +import inha.gdgoc.domain.core.attendance.entity.Team; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.Optional; + +@Repository +public interface TeamRepository { + Team save(Team team); + Optional findById(String id); + Collection findAll(); + void deleteById(String id); +} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/domain/core/attendance/service/CoreAttendanceService.java b/src/main/java/inha/gdgoc/domain/core/attendance/service/CoreAttendanceService.java new file mode 100644 index 0000000..bc99cd2 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/core/attendance/service/CoreAttendanceService.java @@ -0,0 +1,142 @@ +// inha/gdgoc/domain/core/attendance/service/CoreAttendanceService.java +package inha.gdgoc.domain.core.attendance.service; + +import inha.gdgoc.domain.core.attendance.dto.response.DaySummaryResponse; +import inha.gdgoc.domain.core.attendance.dto.response.MemberResponse; +import inha.gdgoc.domain.core.attendance.dto.response.TeamResponse; +import inha.gdgoc.domain.core.attendance.entity.Member; +import inha.gdgoc.domain.core.attendance.entity.Team; +import inha.gdgoc.domain.core.attendance.repository.AttendanceRecordRepository; +import inha.gdgoc.domain.core.attendance.repository.MemberRepository; +import inha.gdgoc.domain.core.attendance.repository.TeamRepository; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class CoreAttendanceService { + + private final TeamRepository teamRepo; + private final MemberRepository memberRepo; + private final AttendanceRecordRepository recordRepo; + + // 날짜 목록만 내부 보관 (임시 운용) + private final LinkedHashSet dates = new LinkedHashSet<>(); + + private static String uuid(String p) { + return p + "_" + java.util.UUID.randomUUID() + .toString() + .replace("-", "") + .substring(0, 8); + } + + private static String dkey(String date) {return date.replaceAll("-", "");} + + @PostConstruct + void initSeed() { // ✅ 빈 생성 후 시드 + seed(); + } + + private void seed() { + String today = LocalDate.now().toString(); + dates.add(today); + + Team alpha = new Team(uuid("team"), "Alpha", "김정민"); + alpha.getMembers().add(new Member(uuid("m"), "홍길동")); + alpha.getMembers().add(new Member(uuid("m"), "이서연")); + teamRepo.save(alpha); + + Team beta = new Team(uuid("team"), "Beta", "이나경"); + beta.getMembers().add(new Member(uuid("m"), "장우진")); + beta.getMembers().add(new Member(uuid("m"), "유하늘")); + teamRepo.save(beta); + } + + /* Dates */ + public List getDates() {return new ArrayList<>(dates);} + + public void addDate(String date) {dates.add(date);} + + public void deleteDate(String date) { + dates.remove(date); + recordRepo.removeDate(dkey(date)); + } + + /* Teams */ + public List getTeams(String leadName, String teamId) { + return teamRepo.findAll() + .stream() + .filter(t -> leadName == null || leadName.isBlank() || t.getLead().equals(leadName)) + .filter(t -> teamId == null || teamId.isBlank() || t.getId().equals(teamId)) + .map(this::toTeamResponse) + .collect(Collectors.toList()); + } + + /* Members */ + public void addMember(String teamId, String name) { + Team t = team(teamId); + memberRepo.add(t, new Member(uuid("m"), name)); + teamRepo.save(t); + } + + public void renameMember(String teamId, String memberId, String name) { + Team t = team(teamId); + Member m = memberRepo.find(t, memberId).orElseThrow(() -> new NoSuchElementException("member not found")); + m.setName(name); + teamRepo.save(t); + } + + public void removeMember(String teamId, String memberId) { + Team t = team(teamId); + memberRepo.remove(t, memberId); + teamRepo.save(t); + recordRepo.removeMemberEverywhere(teamId, memberId); + } + + /* Attendance */ + public void setAttendance(String date, String teamId, String memberId, boolean present) { + recordRepo.setPresence(dkey(date), teamId, memberId, present); + } + + public long setAll(String date, String teamId, boolean present) { + Team t = team(teamId); + List memberIds = t.getMembers().stream().map(Member::getId).toList(); + return recordRepo.setAll(dkey(date), teamId, memberIds, present); + } + + /* Summary */ + public DaySummaryResponse summary(String date, String leadName, String teamId) { + var dm = recordRepo.getDay(dkey(date)); + var per = teamRepo.findAll() + .stream() + .filter(t -> leadName == null || leadName.isBlank() || t.getLead().equals(leadName)) + .filter(t -> teamId == null || teamId.isBlank() || t.getId().equals(teamId)) + .map(t -> { + var tm = dm.getOrDefault(t.getId(), Map.of()); + long p = t.getMembers().stream().filter(m -> tm.getOrDefault(m.getId(), false)).count(); + return new DaySummaryResponse.TeamSummary(t.getId(), t.getName(), p, t.getMembers().size()); + }) + .sorted(Comparator.comparing(DaySummaryResponse.TeamSummary::getTeamName)) + .collect(Collectors.toList()); + + long present = per.stream().mapToLong(DaySummaryResponse.TeamSummary::getPresent).sum(); + long total = per.stream().mapToLong(DaySummaryResponse.TeamSummary::getTotal).sum(); + + return new DaySummaryResponse(date, per, present, total); + } + + /* helpers */ + private Team team(String id) { + return teamRepo.findById(id).orElseThrow(() -> new NoSuchElementException("team not found: " + id)); + } + + private TeamResponse toTeamResponse(Team t) { + var ms = t.getMembers().stream().map(m -> new MemberResponse(m.getId(), m.getName())).toList(); + return new TeamResponse(t.getId(), t.getName(), t.getLead(), ms); + } +} \ No newline at end of file