반응형

Controller

@RestController
@RequestMapping("/commute")
public class CommuteController {
    private final CommuteService service;

    public CommuteController(CommuteService service) {
        this.service = service;
    }

    @PostMapping
    public void checkWork(@RequestBody Map<String, Long> json) {
        service.checkWork(json.get("employeeId"));
    }

    @PostMapping("/worktime")
    public CommuteTotalResponse checkTime(@RequestBody CommuteGetDto request) {
        return service.checkTime(request);
    }
}

CommuteGetDto ( requestDTO)

@Getter
public class CommuteGetDto {
    private Long employeeId;
    @DateTimeFormat(pattern = "yyyy-MM")
    private YearMonth searchDate;
}

 

@DateTimeFormat은 json에서 문자열로오는 데이터를 YearMonth타입으로 매칭시켜주기위해서 사용했다.

 

Service

@Service
@RequiredArgsConstructor
@Slf4j
public class CommuteService {

    private final CommuteHistoryRepository repository;
    private final MemberRepository memberRepository;

    @Transactional
    public void checkWork(Long employeeId) {
        //해당 아이디의 직원을 조회
        Employee employee = memberRepository.findById(employeeId).orElseThrow(()-> new IllegalArgumentException("직원 정보가없습니다."));
        //직원의 금일 근무정보가 있는지 조회
        EmployeeTodayCommuteList employeeTodayCommuteList = new EmployeeTodayCommuteList(employee);
        checkStartOrEndWorking(employeeTodayCommuteList, employee);
    }

    private void checkStartOrEndWorking(EmployeeTodayCommuteList employeeTodayCommuteList, Employee employee) {
        // 금일근무정보가 없거나 ,근무기록은 있지만 근무중이 아닌경우(이미퇴근) ,새로 출근기록 생성
        if (employeeTodayCommuteList.isWorkEnd()) {
            CommuteHistory save = repository.save(new CommuteHistory(employee));
            save.startWorking();
        } else {
            //출근 기록이있는경우 퇴근으로
            employeeTodayCommuteList.getWorkingCommuteHistory().endWorking();
        }
    }
    
    @Transactional(readOnly = true)
    public CommuteTotalResponse checkTime(CommuteGetDto request) {
        Employee employee = memberRepository.findById(request.getEmployeeId())
                .orElseThrow(IllegalArgumentException::new);
        CommuteTimeResponseList commuteTimeResponseList = new CommuteTimeResponseList(request, employee);

        return new CommuteTotalResponse(commuteTimeResponseList.getCommuteTimeResponseList());
    }

}

 

@Transactional(readOnly = true) 는 데이터를 조회하기만 할때 사용한다.

이렇게사용하면 조회용 트랜잭션이 생성되서 성능적인 측면에서 좋다고 한다.

CommuteTimeResponse(DTO)

@Getter
@Setter
@NoArgsConstructor
public class CommuteTimeResponse {
    private String date;
    private Integer workingMinutes;
    private boolean usingDayOff;
}

CommuteTimeResponseList(일급컬렉션)

@Getter
public class CommuteTimeResponseList {
    List<CommuteTimeResponse> commuteTimeResponseList;

    public CommuteTimeResponseList(CommuteGetDto request, Employee employee) {
        List<CommuteTimeResponse> collect = employee.getCommuteHistories().stream()
                //연월 필터링
                .filter(result ->
                        result.isSameYearMonth(request.getSearchDate())
                        && !result.getIsWorking()
                        )
                //그루핑
                .collect(Collectors.groupingBy(
                        result -> dateTimeToFormatString(result.getStartTime(), "yyyy-MM-dd"),
                        Collectors.summingInt(result -> diffMinute(result.getStartTime(), result.getEndTime()))
                ))
                .entrySet().stream()
                .map(result -> {
                    CommuteTimeResponse response = new CommuteTimeResponse();
                    response.setDate(result.getKey());
                    response.setWorkingMinutes(result.getValue());
                    return response;
                }).collect(Collectors.toList());
                
                //정렬
        	List<CommuteTimeResponse> list = collect.stream()
                	.sorted(Comparator.comparing(response -> LocalDate.parse(response.getDate())))
                	.toList();

        this.commuteTimeResponseList = list;
    }

    private String dateTimeToFormatString(LocalDateTime time, String format) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
        return time.format(formatter);
    }

    private Integer diffMinute(LocalDateTime start, LocalDateTime end) {
        Duration duration = Duration.between(start, end);
        return (int) duration.toMinutes();
    }
}

 

아직 리팩토링이 더필요한 부분이긴한데.. 서비스에서 출퇴근리스트를 필터링하고 그룹화해줘서 만들었던 메서드를 일급컬렉션으로 옮기고 응답을 해주는 객체인 CommuteTimeResponse로 매핑해주는 메서드 들이다. 이런 부분들을 jpql로 애초에 응답객체의 형식에맞춰서 데이터를 조회해올수도있지만 직원객체에서 출퇴근데이터를 이미 가지고있어서 나는 그냥 있는데이터를 처리해서 응답객체를 만들어주는 방법을 사용했다. 성능적으로 뭐가 더 좋은지는 잘 모르겠다. 

 

CommuteTotalResponse(DTO)

@Getter
@Setter
public class CommuteTotalResponse {
    private List<CommuteTimeResponse> detail = new ArrayList<>();
    private Integer sum;

    public CommuteTotalResponse(List<CommuteTimeResponse> detail) {
        this.detail = detail;
        this.sum = detail.stream().mapToInt(CommuteTimeResponse::getWorkingMinutes).sum();
    }
}

조회해서 매핑한데이터를 이클래스로 감싼다음 리턴해주어야 위에서봤던 Json의 응답형식에 맞출수있다.

{
    "detail": [
        {
            "date": "2024-03-01",
            "workingMinutes": 0,
            "usingDayOff": true
        },
        {
            "date": "2024-03-02",
            "workingMinutes": 0,
            "usingDayOff": true
        },
        {
            "date": "2024-03-03",
            "workingMinutes": 180,
            "usingDayOff": false
        },
        {
            "date": "2024-03-04",
            "workingMinutes": 120,
            "usingDayOff": false
        },
        {
            "date": "2024-03-05",
            "workingMinutes": 118,
            "usingDayOff": false
        },
        {
            "date": "2024-03-06",
            "workingMinutes": 0,
            "usingDayOff": true
        },
        {
            "date": "2024-03-07",
            "workingMinutes": 0,
            "usingDayOff": true
        },
        {
            "date": "2024-03-11",
            "workingMinutes": 0,
            "usingDayOff": false
        }
    ],
    "sum": 418
 

 

이렇게 직원정보와 검색날짜에 맞춰서 데이터가 잘 조회되는것을 볼수있다 usingDayOff부분은

이후에 진행하면서 추가된 기능의 부분이니 지금은 무시하자.


추가수정

CommuteController

package Company.demo.commute.controller;

import Company.demo.commute.dto.request.CommuteGetDto;
import Company.demo.commute.dto.response.CommuteTotalResponse;
import Company.demo.commute.service.CommuteService;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/commute")
public class CommuteController {
    private final CommuteService service;

    public CommuteController(CommuteService service) {
        this.service = service;
    }

    @PostMapping
    public void checkWork(@RequestBody Map<String, Long> json) {
        service.checkWork(json.get("employeeId"));
    }

	//PostMapping 이던 부분을 Get으로 변경 (조회하는 메서드에 알맞게 수정)
    @GetMapping("/worktime")
    public CommuteTotalResponse checkTime(CommuteGetDto request) {
        System.out.println("request.getSearchDate() = " + request.getSearchDate());
        System.out.println("request.getEmployeeId() = " + request.getEmployeeId());
        return service.checkTime(request);
    }
}

 

CommuteTimeResponseList

@Getter
public class CommuteTimeResponseList {
    List<CommuteTimeResponse> commuteTimeResponseList;

    public CommuteTimeResponseList(CommuteGetDto request, Employee employee) {
        List<CommuteTimeResponse> collect = filterAndGroupCommuteHistries(request, employee);
        //연차 정보를 더해준다.
        addAnnualLeaveToCommuteTimeResponse(request, employee, collect);
        //정렬
        this.commuteTimeResponseList = sortCommuteTimeResponseListbyDate(collect);
    }

    private static List<CommuteTimeResponse> sortCommuteTimeResponseListbyDate(List<CommuteTimeResponse> collect) {
        List<CommuteTimeResponse> list = collect.stream()
                .sorted(Comparator.comparing(response -> LocalDate.parse(response.getDate())))
                .toList();
        return list;
    }

    private static void addAnnualLeaveToCommuteTimeResponse(CommuteGetDto request, Employee employee, List<CommuteTimeResponse> collect) {
        employee.getAnnualLeaveRegisters().stream()
                .filter(leave -> {
                    YearMonth startYearMonth = YearMonth.from(leave.getStartDate());
                    YearMonth endYearMonth = YearMonth.from(leave.getEndDate());
                    return !startYearMonth.isAfter(request.getSearchDate()) && !endYearMonth.isBefore(request.getSearchDate());
                })
                .forEach(leave -> {
                    LocalDate startDate = leave.getStartDate();
                    LocalDate endDate = leave.getEndDate();

                    // 검색 시작일과 휴가 시작일 중 늦은 날짜를 선택
                    LocalDate searchStart = request.getSearchDate().atDay(1);
                    LocalDate effectiveStartDate = startDate.isBefore(searchStart) ? searchStart : startDate;

                    // 검색 종료일과 휴가 종료일 중 이른 날짜를 선택
                    LocalDate searchEnd = request.getSearchDate().atEndOfMonth();
                    LocalDate effectiveEndDate = endDate.isAfter(searchEnd) ? searchEnd : endDate;

                    long daysBetween = ChronoUnit.DAYS.between(effectiveStartDate, effectiveEndDate);
                    for (long i = 0; i <= daysBetween; i++) {
                        LocalDate currentDate = leave.getStartDate().plusDays(i);
                        collect.add(new CommuteTimeResponse(currentDate));
                    }
                });
    }

    private List<CommuteTimeResponse> filterAndGroupCommuteHistries(CommuteGetDto request, Employee employee) {
        List<CommuteTimeResponse> collect = employee.getCommuteHistories().stream()
                //연월 필터링
                .filter(result ->
                        result.isSameYearMonth(request.getSearchDate())
                                && !result.getIsWorking()
                )
                //그루핑
                .collect(Collectors.groupingBy(
                        result -> dateTimeToFormatString(result.getStartTime(), "yyyy-MM-dd"),
                        Collectors.summingInt(result -> diffMinute(result.getStartTime(), result.getEndTime()))
                ))
                .entrySet().stream()
                .map(result -> {
                    CommuteTimeResponse response = new CommuteTimeResponse();
                    response.setDate(result.getKey());
                    response.setWorkingMinutes(result.getValue());
                    return response;
                }).collect(Collectors.toList());
        return collect;
    }

    private String dateTimeToFormatString(LocalDateTime time, String format) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
        return time.format(formatter);
    }

    private Integer diffMinute(LocalDateTime start, LocalDateTime end) {
        Duration duration = Duration.between(start, end);
        return (int) duration.toMinutes();
    }
}

 

 

객체를 매핑해주는 코드를 수정해줬다

필터링하고 그룹을 지어주는 메서드

연차정보를 매핑해서 추가해주는 메서드

리스트를 날짜정보에의해 정렬해주는 메서드 , 이 세가지로 분리해서 리팩토링해주고 

 

연차정보를 추가하는 메서드는 필터조건과 추가하는 방법이 수정되었다.

이전의 방법은 시작일자의 연월데이터와 검색일자의 연월데이터를 비교해서 같은것들을 필터링해주었는데 

이런 필터링방법을 쓰니 내 데이터의 특성상 시작일자는 3월 이더라도 끝나는일자가 4월인 데이터들이 같이 추가되었다.

.filter(leave -> {
    YearMonth startYearMonth = YearMonth.from(leave.getStartDate());
    YearMonth endYearMonth = YearMonth.from(leave.getEndDate());
    return 
    //연차시작일자가 검색일자와 같거나 이전인 데이터 //검색일자보다 이후이면 false
    !startYearMonth.isAfter(request.getSearchDate()) 
    && 
    //연차 끝나는일자가 검색일자보다 같거나 이후인 데이터 //검색일자보다 이전이면 false
    !endYearMonth.isBefore(request.getSearchDate());
    //두조건이 and조건이므로 검색일자를 포함하는 데이터만남는다.
})

 

ex )

검색일자는 2024-03 

휴가시작일자는 2024-02

휴가끝나는일정이 2024-04 

 

이런경우에는 검색일자가 휴가일정에 포함되어있으므로 true가 나와야한다. 

그래서 검색일자와 같거나 이전인데이터(true) && 검색일자와 같거나 이후인데이터(true) 

이렇게 조건을 잡도록 수정해주었다. 이렇게 필터링을하니 2월 3월 4월 같은 검색날짜 이전과 이후의 데이터도 필터를 통과하는데.

.forEach(leave -> {
    LocalDate startDate = leave.getStartDate();
    LocalDate endDate = leave.getEndDate();

    // 검색 시작일과 휴가 시작일 중 늦은 날짜를 선택
    //검색월의 첫째날 if 2024-03이면 3월1일
    LocalDate searchStart = request.getSearchDate().atDay(1);
    //만약 휴가시작일이 2월10일 이라면 2월10일은 검색일인 3월보다이전이므로 true -searchStart를 리턴
    LocalDate effectiveStartDate = startDate.isBefore(searchStart) ? searchStart : startDate;

    // 검색 종료일과 휴가 종료일 중 이른 날짜를 선택
    // 검색월의 마지막날 ex) 3월31일
    LocalDate searchEnd = request.getSearchDate().atEndOfMonth();
    // 만약 휴가의 마지막날이 검색의 마지막날보다 이후라면 searchEnd 리턴
    LocalDate effectiveEndDate = endDate.isAfter(searchEnd) ? searchEnd : endDate;
    
    //2월부터 4월까지가 휴가라면 각 리턴된결과에따라 3월1일부터 3월 31일까지가 startDate,endDate가 된다.
    long daysBetween = ChronoUnit.DAYS.between(effectiveStartDate, effectiveEndDate);
    for (long i = 0; i <= daysBetween; i++) {
        LocalDate currentDate = leave.getStartDate().plusDays(i);
        collect.add(new CommuteTimeResponse(currentDate));
    }
});

 

 

이렇게 몇번 실행을하고 리팩토링을 하다보니 처음에는 생각하지못했던 문제들도 마주하게되었다. 

이런 케이스들을 여러번 경험하다보면 경험적으로 미리 이런부분들을 방지할수도 있게되겠지.

반응형

+ Recent posts