
ID를 기준으로 처리된다.ID르ㅜㄹ 기준으로 처리된다.id와 2024-01과 같이 연/월을 받으면, 날짜별 근무 시간과 총 합을 반환해야 한다. 이때 근무 시간은 분단위로 계산된다.1번 id를 갖는 직원에 대해 2024-01을 기준으로 조회하면, 다음과 같은 응답이 반환되어야 한다.{
"detail": [
{
"date": "2024-01-01",
"workingMinutes": 480
},
{
"date": "2024-01-02",
"workingMinutes": 490
},
... // 2024년 1월 31일까지 존재할 수 있다.
]
"sum": 10560
}
📌 edge-case
- 등록되지 않은 직원이 출근 하려는 경우
- 출근한 직원이 또 다시 출근하려는 경우
- 퇴근하려는 직원이 출근하지 않았던 경우
- 그 날, 출근했다 퇴근한 직원이 다시 출근하려는 경우
Table💡 고민 1.
테이블을 어떻게 짤까?
CREATE TABLE commute
(
id bigint auto_increment,
start_of_work datetime,
end_of_work datetime,
attendance tinyInt,
member_id bigint,
primary key (id)
);
📌
JPA Auditing
저번 피드백에서@EntityListeners(AuditingEntityListener.class)어노테이션을 사용하는
BaseEntity추상 클래스를 만들어 봤는데,BaseEntity의CreatedAt,UpdatedAt필드를 상속받고
출/퇴근시간을 자동으로 기록하는게 개인적인 목표입니다!
Controller@RestController
@RequiredArgsConstructor
public class CommuteController {
private final CommuteService commuteService;
@PostMapping("/start-of-work")
public void startOfWork(@Valid @RequestBody startOfWorkRequest request) {
commuteService.startOfWork(request);
}
@PostMapping("/end-of-work")
public void endOfWork(@Valid @RequestBody endOfWorkRequest request) {
commuteService.endOfWork(request);
}
@GetMapping("/commute")
public ResponseEntity<GetCommuteRecordResponse> GetCommuteRecord(@Valid GetCommuteRecordRequest request){
GetCommuteRecordResponse getCommuteRecordResponse = commuteService.GetCommuteRecord(request);
return ResponseEntity.ok().body(getCommuteRecordResponse);
}
}
📌 클래스를 매개변수로 사용하는 경우
클래스 형태의 객체를 매개변수로 받는 컨트롤러 메소드에서 별도의 어노테이션을 사용하지 않는 경우,
스프링은 기본적으로 쿼리 파라미터를 클래스의 프로퍼티와 매핑한다.
@RequestParam어노테이션을 사용하면 매개변수가 쿼리 파라미터로 넘어오는 것이 아니라, 매개변수 자체가 요청의 특정 파라미터와 매핑되도록 기대한다.
따라서 클래스 타입의 객체를@RequestParam으로 직접 받는다면 쿼리 파라미터 매핑이 자동으로 이뤄지지 않는다.
DTO(Request)public record endOfWorkRequest(@NotNull long id) {
}
public record GetCommuteRecordRequest(@NotNull long id, @DateTimeFormat(pattern = "yyyy-MM") YearMonth yearMonth) {
}
public record startOfWorkRequest(@NotNull long id) {
public Commute toEntity(Member member){
return Commute.builder()
.member(member)
.build();
}
}
💡 저번 피드백에서
record사용법을 배워서 record로 생성하였습니다.
Domain@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AttributeOverrides({
@AttributeOverride(name= "createdAt", column = @Column(name= "start_of_work")),
@AttributeOverride(name= "updatedAt", column = @Column(name= "end_of_work"))
})
public class Commute extends BaseEntity { //BaseEntity를 상속받음
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private boolean attendance = true; // 출근 상태
@ManyToOne(fetch= FetchType.LAZY)
private Member member;
public void endOfWork(){
this.attendance = false;
}
@Builder
public Commute(Member member){
this.member = member;
}
}
💡 저번 피드백에서
@EntityListeners(AuditingEntityListener.class)어노테이션을 사용하는
추상 클래스 생성을 권유 받았는데, 만들고 나니 써먹고 싶어져서 추상 클래스를 상속받고,
attendance값의 변경을 통해 출/퇴근을 구현해보려고 합니다.
💡 처음에는Member도메인과1 : N양방향 연관 관계로 설계를 해뒀었는데,
블로그를 작성하면서 확인해보니,Member에 맺어둔@OneToMany를 전혀 사용을 안했다는걸 깨닫고,
Commute의@ManyToOne단방향 연관관계만 살려두었습니다.
Service@Service
@Slf4j
@RequiredArgsConstructor
public class CommuteService {
private final CommuteRepository commuteRepository;
private final MemberRepository memberRepository;
@Transactional
public void startOfWork(startOfWorkRequest request) {
Member member = findMemberById(request.id());
Commute latestCommute = findLatestCommuteByMember(member);
if (latestCommute.isAttendance()) throw new AbsentCheckOutException(); //이전 기록 퇴근확인
boolean isAlreadyAttendance = LocalDate.now().equals(LocalDate.from(latestCommute.getCreatedAt()));
if (isAlreadyAttendance) throw new AlreadyAttendanceException(); //당일 출근기록 확인
commuteRepository.save(request.toEntity(member));
}
@Transactional
public void endOfWork(@RequestBody endOfWorkRequest request) {
Member member = findMemberById(request.id());
Commute latestCommute = findLatestCommuteByMember(member);
if (!latestCommute.isAttendance()) throw new AlreadyDepartureException();
latestCommute.endOfWork(); //변경감지 자동저장
}
@Transactional
public GetCommuteRecordResponse GetCommuteRecord(GetCommuteRecordRequest request) {
findMemberById(request.id());
List<GetCommuteDetail> commuteDetailList = findCommuteListByMemberIdAndStartOfWork(request);
Long sum = commuteDetailList.stream()
.map(GetCommuteDetail::workingMinutes)
.reduce(0L, Long::sum);
//commuteDetailList에서 workingMinutes를 조회, reduce로 합을 반환
return new GetCommuteRecordResponse(commuteDetailList, sum);
}
private Member findMemberById(long id) {
return memberRepository.findById(id).orElseThrow(MemberNotFoundException::new);
}
private Commute findLatestCommuteByMember(Member member) {
return commuteRepository.findLatestCommuteByMemberId(member.getId())
.orElseThrow(CommuteNotFoundException::new);
}
private List<GetCommuteDetail> findCommuteListByMemberIdAndStartOfWork(GetCommuteRecordRequest request) {
List<Commute> commuteList =
commuteRepository.findCommuteListByMemberIdAndStartOfWork(request.id(), request.yearMonth().getYear(), request.yearMonth().getMonth().getValue());
if (commuteList.isEmpty()) throw new CommuteNotFoundException(); //해당범위에 통근기록 존재 X
return commuteList.stream().map(GetCommuteDetail::from).toList(); //CommuteDetail으로 변환
}
}
💡고민 2.
현재startOfWork의isAlreadyAttendance(전 기록 퇴근처리 확인) 기능이 과연 필요한가 고민중입니다.
야근 후, 12시가 넘어서 퇴근을 찍지않고 출근을 찍을 경우를 대비해서 넣어뒀는데,
퇴근을 찍지않으면 그 날의CreatedAt(출근시간)과UpdatedAt(퇴근시간)이 동일하여
근무시간이 0으로 찍히기 떄문에 본인이 알아서 인사팀에 찾아가지 않을까요? 🤔
DTO(Reponse)@Builder
public record GetCommuteDetail(LocalDate date, long workingMinutes) {
public static GetCommuteDetail from(Commute commute){
Duration duration = Duration.between(commute.getCreatedAt(), commute.getUpdatedAt());
return GetCommuteDetail.builder()
.date(commute.getCreatedAt().toLocalDate())
.workingMinutes(duration.toMinutes())
.build();
}
}
public record GetCommuteRecordResponse(List<GetCommuteDetail> detail, long sum) {
}
💡
DTO반환 과정에서Duration을 활용해 생성시간과 수정시간의 차이를 분으로 바꿔줬습니다.
Repositorypublic interface CommuteRepository extends JpaRepository<Commute, Long> {
@Query("SELECT latestcommute FROM Commute latestcommute WHERE latestcommute.member.id = :memberId AND latestcommute.createdAt = (SELECT MAX(commute.createdAt) FROM Commute commute WHERE commute.member.id = :memberId)")
Optional<Commute> findLatestCommuteByMemberId(Long memberId);
@Query("SELECT commute FROM Commute commute WHERE commute.member.id= :memberId AND FUNCTION('YEAR', commute.createdAt)= :year AND FUNCTION('MONTH', commute.createdAt)= :month")
List<Commute> findCommuteListByMemberIdAndStartOfWork(Long memberId, int year, int month);
}
💡 다른분들께 배운 JPQL 활용해보기
findLatestCommuteByMemberId=SELECT MAX를 통해 가장 최근의 통근 기록을 조회한다.findCommuteListByMemberIdAndStartOfWork=GetCommuteRecordRequest의
①MemberId, ②year(request.yearMonth().getYear()), ③month(request.getMonth().getValue())
값을 만족하는 모든 Commute를 조회한다.
①출근 기능
ID를 기준으로 처리된다.

②퇴근 기능
ID를 기준으로 처리된다.

③특정 직원의 날짜별 근무시간을 조회하는 기능
id와 2024-01과 같이 연/월을 받으면, 날짜별 근무 시간과 총 합을 반환해야 한다. 이때 근무 시간은 분단위로 계산된다.1번 id를 갖는 직원에 대해 2024-01을 기준으로 조회하면, 다음과 같은 응답이 반환되어야 한다.

④edge-case

출근한 직원이 또 다시 출근하려는 경우
퇴근하려는 직원이 출근하지 않았던 경우
그 날, 출근했다 퇴근한 직원이 다시 출근하려는 경우