Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ HELP.md
**/.env.*
*.env
.env
.DS_Store
.env.properties
!.env.example
**/.env.example
Expand Down
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;
}
Comment on lines +4 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

형식 검증 누락: 메시지는 형식을 요구하지만 실제로는 NotBlank만 검증.

YYYY-MM-DD 형식을 @pattern으로 강제하거나 LocalDate 타입으로 전환하세요. 빠른 수정은 아래 패턴 추가입니다.

 package inha.gdgoc.domain.core.attendance.dto.request;

 import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.AllArgsConstructor;

 @Getter
 @NoArgsConstructor
 @AllArgsConstructor
 public class CreateDateRequest {
-    @NotBlank(message = "날짜는 YYYY-MM-DD 형식이어야 합니다.")
-    private String date;
+    @NotBlank(message = "날짜는 비어 있을 수 없습니다.")
+    @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "날짜는 YYYY-MM-DD 형식이어야 합니다.")
+    private String date;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
}
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class CreateDateRequest {
@NotBlank(message = "날짜는 비어 있을 수 없습니다.")
@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "날짜는 YYYY-MM-DD 형식이어야 합니다.")
private String date;
}
🤖 Prompt for AI Agents
In
src/main/java/inha/gdgoc/domain/core/attendance/dto/request/CreateDateRequest.java
around lines 4–15, the field uses @NotBlank but the message demands a YYYY-MM-DD
format; either change the field type to java.time.LocalDate or (quick fix) add a
validation annotation: import jakarta.validation.constraints.Pattern and apply
@Pattern(regexp="^\\d{4}-\\d{2}-\\d{2}$", message="날짜는 YYYY-MM-DD 형식이어야 합니다.")
to the date field so the format is actually enforced.

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

요청 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class SetAllRequest {
private boolean present;
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 {
@NotNull
private Boolean present;
}
🤖 Prompt for AI Agents
In
src/main/java/inha/gdgoc/domain/core/attendance/dto/request/SetAllRequest.java
around lines 4 to 12, change the primitive boolean field to the wrapper type and
annotate it with @NotNull to avoid silent false defaults when the field is
omitted: replace "private boolean present;" with "private Boolean present;" add
the javax.validation.constraints.NotNull import and annotate the field with
@NotNull; keep Lombok annotations as-is so constructors and getters are
generated; ensure any code constructing this DTO or controller methods validate
bean constraints (e.g., @Valid) so missing values produce a validation error
instead of defaulting to false.

}
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
Copy link

Choose a reason for hiding this comment

The 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"
fi

Length 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
In
src/main/java/inha/gdgoc/domain/core/attendance/dto/request/SetAttendanceRequest.java
(lines 4-12) and
src/main/java/inha/gdgoc/domain/core/attendance/dto/request/SetAllRequest.java,
change the primitive field type from boolean to the boxed Boolean, add the
import jakarta.validation.constraints.NotNull, and annotate the field with
@NotNull (e.g., @NotNull private Boolean present;) so missing values fail
validation instead of defaulting to false; finally ensure any controller or
handler receiving these DTOs has @Valid on the method parameter so the NotNull
constraint is enforced at runtime.

}
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;
}
14 changes: 14 additions & 0 deletions src/main/java/inha/gdgoc/domain/core/attendance/entity/Member.java
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; }
}
23 changes: 23 additions & 0 deletions src/main/java/inha/gdgoc/domain/core/attendance/entity/Team.java
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);
}
Loading
Loading