-
Notifications
You must be signed in to change notification settings - Fork 1
feat(core-attendance): 코어 회의 출석 기능 추가 #220
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
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 |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ HELP.md | |
| **/.env.* | ||
| *.env | ||
| .env | ||
| .DS_Store | ||
| .env.properties | ||
| !.env.example | ||
| **/.env.example | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<ApiResponse<DateListResponse, Void>> getDates() { | ||
| return ResponseEntity.ok( | ||
| ApiResponse.ok(CoreAttendanceMessage.DATE_LIST_RETRIEVED_SUCCESS, | ||
| new DateListResponse(service.getDates())) | ||
| ); | ||
| } | ||
|
|
||
| @PostMapping("/dates") | ||
| public ResponseEntity<ApiResponse<DateListResponse, Void>> 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<ApiResponse<DateListResponse, Void>> 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<ApiResponse<List<TeamResponse>, PageMeta>> getTeams( | ||
| @RequestParam(required = false) String leadName, | ||
| @RequestParam(required = false) String teamId | ||
| ) { | ||
| List<TeamResponse> 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<ApiResponse<String, Void>> 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<ApiResponse<String, Void>> 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<ApiResponse<String, Void>> 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<ApiResponse<String, Void>> 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<ApiResponse<Long, Void>> 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<ApiResponse<DaySummaryResponse, Void>> 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)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 파일을 내보냈습니다."; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+4
to
+12
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에서 primitive boolean → Boolean + @NotNull로 변경 필요 (누락 시 false로 침묵 기본값 되는 위험). present 필드가 전달되지 않으면 false로 처리되어 대량 출석 상태가 의도치 않게 해제될 수 있습니다. wrapper 타입과 @NotNull로 안전하게 강제하세요. package inha.gdgoc.domain.core.attendance.dto.request;
+import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class SetAllRequest {
- private boolean present;
+ @NotNull
+ private Boolean present;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
Comment on lines
+4
to
+12
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. 🛠️ Refactor suggestion 🧩 Analysis chain요청 DTO에서 primitive boolean → Boolean + @NotNull로 변경 필요 (누락 시 false로 침묵 기본값 되는 위험). 단일 출석 설정도 동일한 리스크가 있습니다. SetAllRequest와 동일하게 수정해 일관성을 맞추세요. package inha.gdgoc.domain.core.attendance.dto.request;
+import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class SetAttendanceRequest {
- private boolean present;
+ @NotNull
+ private Boolean present;
}🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Searching for SetAttendanceRequest / SetAllRequest and occurrences of 'present' field..."
rg -n --hidden -S "class SetAttendanceRequest" || true
rg -n --hidden -S "class SetAllRequest" || true
rg -n --hidden -S "SetAttendanceRequest.java" || true
rg -n --hidden -S "SetAllRequest.java" || true
echo
echo "Searching for 'private boolean present' or 'private Boolean present'..."
rg -n --hidden -S "private\s+boolean\s+present" || true
rg -n --hidden -S "private\s+Boolean\s+present" || true
echo
echo "Searching for @NotNull usages in attendance DTOs..."
rg -n --hidden -S "@NotNull" || true
echo
# Print file contents if found
SA_FILES=$(rg -l --hidden -S "class SetAttendanceRequest" || true)
if [ -n "$SA_FILES" ]; then
for f in $SA_FILES; do
echo "=== $f ==="
sed -n '1,200p' "$f" || true
done
else
echo "No SetAttendanceRequest files found"
fi
ALL_FILES=$(rg -l --hidden -S "class SetAllRequest" || true)
if [ -n "$ALL_FILES" ]; then
for f in $ALL_FILES; do
echo "=== $f ==="
sed -n '1,200p' "$f" || true
done
else
echo "No SetAllRequest files found"
fiLength of output: 2110 primitive boolean → Boolean + @NotNull 적용 (필수) 값이 누락될 때 primitive boolean이 기본값 false로 처리되어 침묵하는 위험이 있으므로, 아래 파일들에서 필드를 primitive boolean → Boolean으로 변경하고 import jakarta.validation.constraints.NotNull 후 @NotNull을 붙이세요. 또한 해당 DTO를 받는 컨트롤러/핸들러 파라미터에 @Valid가 적용되어 검증이 동작하는지 확인하세요. 대상: src/main/java/inha/gdgoc/domain/core/attendance/dto/request/SetAttendanceRequest.java, src/main/java/inha/gdgoc/domain/core/attendance/dto/request/SetAllRequest.java 예: import jakarta.validation.constraints.NotNull; @NotNull private Boolean present; 🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<String> dates; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<TeamSummary> 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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<MemberResponse> members; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Member> 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; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<String> memberIds, boolean present); | ||
|
|
||
| Map<String, Map<String, Boolean>> getDay(String dateKey); // day -> teamId -> memberId -> present | ||
| void removeDate(String dateKey); | ||
| void removeMemberEverywhere(String teamId, String memberId); | ||
| void removeTeamEverywhere(String teamId); | ||
| } |
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.
형식 검증 누락: 메시지는 형식을 요구하지만 실제로는 NotBlank만 검증.
YYYY-MM-DD 형식을 @pattern으로 강제하거나 LocalDate 타입으로 전환하세요. 빠른 수정은 아래 패턴 추가입니다.
📝 Committable suggestion
🤖 Prompt for AI Agents